Перейти к содержимому
Ступень 11.1 Линейная алгебра

Ранг, SVD и низкоранговые приближения

Как понять, сколько информации на самом деле хранит матрица, разложить её на «слои» по важности и сжать в десятки раз — математика, на которой стоят LoRA и MLA от DeepSeek.

УРОКstage-1/1-1-linear-algebra/03-rank-decompositions.mdx1 ч 50 минУрок 3 из 3
После урока вы сможете
  • Объяснить, что такое ранг матрицы, и посчитать, сколько чисел экономит разложение W≈BAW \approx BA ранга rr
  • Описать SVD геометрически (поворот → растяжение → поворот) и как сумму слоёв ранга 1, упорядоченных по важности
  • Сжать матрицу через SVD, предсказать ошибку по сингулярным числам и объяснить, почему структура сжимается, а шум — нет
  • Связать низкоранговые приближения с LoRA, MLA и Muon и объяснить, что такое собственные числа

Вы хотите дообучить Qwen на своих данных. В модели сотни матриц размера порядка 4096×40964096 \times 4096, в каждой 16.8 млн чисел. Обновлять их все — дорого: кроме самих весов, нужна память под их градиенты (поправки к весам, модуль 1.2) и вспомогательные числа оптимизатора (модуль 1.4) — по нескольку чисел на каждый вес. LoRALoRA (низкоранговая адаптация)EN low-rank adaptation (LoRA) предлагает другое: не трогать большую матрицу WW, а добавить к ней произведение двух узких матриц BB (4096×84096 \times 8) и AA (8×40968 \times 4096). В них всего 4096⋅8+8⋅4096=65 5364096 \cdot 8 + 8 \cdot 4096 = 65\,536 чисел — в 256 раз меньше. И это работает почти так же хорошо, как полное дообучение.

DeepSeek в 2024 году применила ту же идею к памяти. Во время генерации модель держит в памяти векторы ключей и значений для всех уже прочитанных токенов — это KV-кэшKV-кэшEN KV cache, и на длинных текстах он занимает гигабайты. Механизм многоголовое латентное вниманиемногоголовое латентное внимание (MLA)EN multi-head latent attention (MLA)Предложено DeepSeek в DeepSeek-V2 (2024). хранит для каждого токена в каждом слое не 32 768 чисел ключей и значений, а сжатый вектор из 512 чисел (плюс 64 числа на позиционную часть ключа). Почему так можно? Потому что у этих матриц низкий ранг — информации в них гораздо меньше, чем чисел. В этом уроке мы научимся эту информацию измерять (ранг), видеть (SVD) и сжимать (низкоранговое приближение).

Ранг: сколько в матрице настоящей информации

Заголовок раздела «Ранг: сколько в матрице настоящей информации»

Вспомните проекцию из прошлого урока: матрица (1000)\begin{pmatrix} 1 & 0 \\ 0 & 0 \end{pmatrix} сплющивает всю плоскость в линию. На входе два измерения, на выходе одно. Рангранг матрицыEN matrix rankЧисло линейно независимых строк (столбцов). Матрица B·A, где B размера d×r, а A — r×k, имеет ранг не больше r. матрицы — это размерность того, что остаётся на выходе: сколько независимых направлений матрица способна выдать. У проекции ранг 1, у поворота ранг 2 — он ничего не теряет.

Есть эквивалентное определение через строки: ранг — это число линейно независимых строк, то есть таких, которые нельзя получить линейной комбинацией остальных. (Удивительный факт: для столбцов получается то же самое число.)

M1=(123246369),M2=(101011112)M_1 = \begin{pmatrix} 1 & 2 & 3 \\ 2 & 4 & 6 \\ 3 & 6 & 9 \end{pmatrix}, \qquad M_2 = \begin{pmatrix} 1 & 0 & 1 \\ 0 & 1 & 1 \\ 1 & 1 & 2 \end{pmatrix}

В M1M_1 вторая строка — это первая, умноженная на 2, третья — первая, умноженная на 3. Новой информации в них нет: ранг равен 1. В M2M_2 третья строка — сумма первых двух, а первые две независимы: ранг 2. Обе матрицы 3×33 \times 3 хранят по 9 чисел, но «по-настоящему» в них одно и два направления.

import numpy as np
M1 = np.array([[1, 2, 3], [2, 4, 6], [3, 6, 9]])
M2 = np.array([[1, 0, 1], [0, 1, 1], [1, 1, 2]])
print(np.linalg.matrix_rank(M1), np.linalg.matrix_rank(M2)) # 1 2

Самый наглядный пример матрицы ранга 1 знает каждый школьник — это таблица умножения. Её элемент (i,j)(i, j) равен i⋅ji \cdot j, то есть вся таблица — внешнее произведениевнешнее произведениеEN outer productСтолбец на строку — u vᵀ, матрица ранга 1. В китайских текстах «внешним произведением» иногда называют векторное произведение (cross product), смотрите по контексту. столбца (1,…,9)(1, \dots, 9) на такую же строку: T=uv⊤T = \mathbf{u}\mathbf{v}^\top.

строка vᵀ = (1, 2, …, 9)столбец u1234567891123456789224681012141618336912151821242744812162024283236551015202530354045661218243036424854771421283542495663881624324048566472991827364554637281Таблица: 81 число.Два вектора: 9 + 9 = 18.Каждая строка — этострока vᵀ, умноженнаяна одно число.Ранг матрицы — 1.
Таблица умножения «девять-девять» — это внешнее произведение u vᵀ: столбец (1…9) на строку (1…9). Матрица ранга 1.

Это общее правило: любая матрица ранга 1 — это столбец, умноженный на строку, uv⊤\mathbf{u}\mathbf{v}^\top. Чтобы её хранить, нужно m+nm + n чисел вместо m⋅nm \cdot n. А матрица ранга rr — это сумма rr таких слоёв, и для неё хватает r(m+n)r(m + n) чисел.

Важнейшее свойство для нашего курса: ранг произведения не больше ранга каждого множителя. Пусть BB имеет форму (d×r)(d \times r), а AA — (r×k)(r \times k), причём rr маленькое. Любой вектор, проходя через BABA, сначала сжимается в rr-мерное пространство, а потом разворачивается обратно. Через «горлышко» ширины rr не пролезет больше rr независимых направлений, поэтому rank(BA)≤r\mathrm{rank}(BA) \le r.

flowchart LR
X["x<br/>4096 чисел"] --> A["A: 8 × 4096<br/>сжать"] --> Z["z<br/>8 чисел"] --> B["B: 4096 × 8<br/>развернуть"] --> Y["y = BAx<br/>4096 чисел"]
Произведение BA через «горлышко» ширины r = 8: матрица 4096 × 4096, но ранг не больше 8
Матрица m×nm \times nПолная, чиселРанг rr, чисел r(m+n)r(m+n)Экономия
4096×40964096 \times 4096, r=8r = 816 777 21665 536в 256 раз
4096×40964096 \times 4096, r=64r = 6416 777 216524 288в 32 раза
1024×40961024 \times 4096, r=8r = 84 194 30440 960в 102 раза

Квадратная матрица полного ранга (ранг равен её размеру: у поворота 2×22 \times 2 ранг 2 — полный, а у M1M_1 выше ранг 1 из возможных 3 — неполный) ничего не теряет: по выходу можно восстановить вход. Для этого у неё есть обратная матрица A−1A^{-1}, которая отменяет преобразование: A−1A=IA^{-1}A = I. Матрица неполного ранга часть информации необратимо выбрасывает — как проекция. Но если выбрасывается только несущественное, это и есть сжатие. Осталось научиться находить, что существенно. Для этого есть SVD.

Сингулярное разложениесингулярное разложение (SVD)EN singular value decomposition (SVD)A = UΣVᵀ — любую матрицу можно разложить на поворот, растяжение вдоль осей и ещё один поворот. Эквивалентно сумме слоёв ранга 1 σᵢuᵢvᵢᵀ. (Singular Value Decomposition) утверждает: любую матрицу AA размера m×nm \times n — квадратную или нет — можно записать как

A=UΣV⊤A = U \Sigma V^\top
  • V⊤V^\top и UU — ортогональные матрицы: их столбцы единичные и попарно перпендикулярны (ортогональные (перпендикулярные) векторыортогональные (перпендикулярные) векторыEN orthogonal vectorsВекторы, скалярное произведение которых равно нулю. Ортогональная матрица — та, у которой столбцы единичные и попарно ортогональны. из урока 1.1.1). Геометрически это повороты (иногда с отражением), длины они не меняют.
  • Σ\Sigma (заглавная «сигма», здесь это просто имя матрицы, а не знак суммы) — диагональная матрица: все её числа равны нулю, кроме стоящих на главной диагонали, из левого верхнего угла в правый нижний. Например, (3000.5)\begin{pmatrix} 3 & 0 \\ 0 & 0.5 \end{pmatrix} растягивает ось xx втрое, а ось yy сжимает вдвое — как растяжение из прошлого урока. На диагонали Σ\Sigma стоят неотрицательные числа σ1≥σ2≥⋯≥0\sigma_1 \ge \sigma_2 \ge \dots \ge 0. Это сингулярные числасингулярное числоEN singular valueКоэффициенты растяжения σ₁ ≥ σ₂ ≥ … ≥ 0 из SVD. Сколько из них ненулевые — таков ранг матрицы. — коэффициенты растяжения.

Читаем справа налево, как в уроке о композиции: повернуть → растянуть вдоль осей → повернуть. Любое линейное преобразование, каким бы запутанным оно ни выглядело, устроено так. Единичная окружность превращается в эллипс, его полуоси равны σ1\sigma_1 и σ2\sigma_2.

1. Вход xединичная окружность2. Vᵀx — поворотv₁, v₂ легли на оси3. ΣVᵀx — растяжениеоси × 2.29 и × 0.874. UΣVᵀx = Axэллипс — образ круга
Матрица A = (2 1; 0 1) по шагам SVD. Красная стрелка — направление v₁, которое растягивается сильнее всего (σ₁ ≈ 2.29), жёлтая — v₂ (σ₂ ≈ 0.87).
A = np.array([[2.0, 1.0], [0.0, 1.0]])
U, S, Vt = np.linalg.svd(A) # Vt — это уже транспонированная V
print(S) # [2.28824561 0.87403205] — во сколько раз растягиваются оси
# np.diag(S) строит диагональную матрицу Σ из вектора S: [[2.288, 0], [0, 0.874]]
print(np.allclose(U @ np.diag(S) @ Vt, A)) # True — разложение точное

Перемножим UΣV⊤U\Sigma V^\top по столбцам и получим ещё одну, самую полезную форму:

A=σ1u1v1⊤+σ2u2v2⊤+⋯+σrurvr⊤A = \sigma_1 \mathbf{u}_1 \mathbf{v}_1^\top + \sigma_2 \mathbf{u}_2 \mathbf{v}_2^\top + \dots + \sigma_r \mathbf{u}_r \mathbf{v}_r^\top

Читаем: ui\mathbf{u}_i — ii-й столбец UU, vi⊤\mathbf{v}_i^\top — ii-я строка V⊤V^\top, а uivi⊤\mathbf{u}_i \mathbf{v}_i^\top — их внешнее произведение («столбец на строку» из прошлого урока), то есть матрица ранга 1. Матрица — это сумма слоёв ранга 1 (как таблица умножения), упорядоченных по важности. Первый слой с самым большим σ1\sigma_1 несёт главную «тему» матрицы, следующие добавляют всё более мелкие детали. Число ненулевых сингулярных чисел и есть ранг. Проверим на нашей матрице AA:

layer1 = S[0] * np.outer(U[:, 0], Vt[0]) # σ₁·u₁·v₁ᵀ; np.outer — «столбец на строку»
layer2 = S[1] * np.outer(U[:, 1], Vt[1]) # σ₂·u₂·v₂ᵀ
print(layer1.round(3)) # [[1.894 1.171]
# [0.447 0.276]] — уже похоже на A
print((layer1 + layer2).round(3)) # [[2. 1.]
# [0. 1.]] — два слоя вместе дают A точно

Первый слой в одиночку уже близок к AA в верхней строке, где стоят самые большие числа. Второй слой с меньшим σ2\sigma_2 — поправка, в основном к нижней строке.

Две нормы матрицы удобно выражаются через сингулярные числа:

  • норма Фробениусанорма ФробениусаEN Frobenius normКорень из суммы квадратов всех элементов матрицы — «L2-норма матрицы, вытянутой в вектор». Равна √(Σ σᵢ²). ∥A∥F\lVert A \rVert_F — корень из суммы квадратов всех элементов, как L2-норма вектора. Она равна σ12+σ22+…\sqrt{\sigma_1^2 + \sigma_2^2 + \dots}. Для нашей AA: 4+1+0+1=6≈2.449\sqrt{4 + 1 + 0 + 1} = \sqrt{6} \approx 2.449, и 2.2882+0.8742\sqrt{2.288^2 + 0.874^2} — то же самое число;
  • спектральная нормаспектральная нормаEN spectral normНаибольшее сингулярное число σ₁ — во сколько раз матрица максимально может растянуть вектор. σ1\sigma_1 — во сколько раз матрица максимально растягивает вектор. Она понадобится, когда речь пойдёт об устойчивости обучения.

Квадрат сингулярного числа называют его «энергией»: доля σ12+⋯+σr2\sigma_1^2 + \dots + \sigma_r^2 от суммы всех σi2\sigma_i^2 показывает, какую часть матрицы сохраняют первые rr слоёв. Для нашей AA: σ12≈5.236\sigma_1^2 \approx 5.236, σ22≈0.764\sigma_2^2 \approx 0.764, вместе 6 (ровно ∥A∥F2\lVert A \rVert_F^2). Значит, первый слой несёт 5.236/6≈87%5.236 / 6 \approx 87\% энергии матрицы, второй — оставшиеся 13%.

Раз слои упорядочены по важности, напрашивается сжатие: оставить rr первых слоёв и выбросить остальные:

Ar=∑i=1rσiuivi⊤A_r = \sum_{i=1}^{r} \sigma_i \mathbf{u}_i \mathbf{v}_i^\top

Это низкоранговое приближениенизкоранговое приближениеEN low-rank approximationЗамена матрицы ближайшей матрицей ранга r. По теореме Эккарта — Янга лучшая получается, если оставить r старших слагаемых SVD.. Теорема Эккарта — Янга (1936) говорит, что лучше сделать нельзя: среди всех матриц ранга rr именно ArA_r ближе всего к AA. Ошибка известна заранее:

∥A−Ar∥F=σr+12+σr+22+…\lVert A - A_r \rVert_F = \sqrt{\sigma_{r+1}^2 + \sigma_{r+2}^2 + \dots}

Читаем: «ошибка — корень из суммы квадратов отброшенных сингулярных чисел». Проверим на нашей матрице AA из двух слоёв. Оставим один слой (r=1r = 1) — отброшено только σ2\sigma_2, и ошибка ∥A−A1∥F=σ2≈0.874\lVert A - A_1 \rVert_F = \sigma_2 \approx 0.874. Удобнее смотреть на относительную ошибку — ту же ошибку, делённую на «размер» самой матрицы ∥A∥F\lVert A \rVert_F: 0.874/2.449≈0.360.874 / 2.449 \approx 0.36. Не путайте её с долей энергии: относительная ошибка — это корень из потерянной доли энергии, 0.13≈0.36\sqrt{0.13} \approx 0.36. Ту же величину считает функция rel_err в коде ниже.

Значит, всё решает спектр — то, как быстро падают сингулярные числа. Проверим на двух матрицах 100×100100 \times 100: «картинке» из 30 размытых пятен и белом шуме.

import numpy as np
def low_rank(A, r):
# full_matrices=False — «экономный» режим: U и Vt без лишних столбцов и строк
U, S, Vt = np.linalg.svd(A, full_matrices=False) # S уже отсортированы по убыванию
# первые r столбцов U, первые r чисел S, первые r строк Vt (срезы из урока 0.3.1)
return U[:, :r] @ np.diag(S[:r]) @ Vt[:r] # r старших «слоёв» ранга 1
def rel_err(A, B):
return np.linalg.norm(A - B) / np.linalg.norm(A) # норма Фробениуса по умолчанию
# Рисуем «картинку» 100×100. Как именно получаются пятна, для урока неважно:
# важно лишь, что картинка гладкая, с плавными переходами, — как настоящая фотография.
rng = np.random.default_rng(0)
x = np.linspace(-1, 1, 100)
X, Y = np.meshgrid(x, x) # координаты всех 100×100 точек
img = np.zeros((100, 100))
for _ in range(30): # 30 размытых вытянутых «пятен» под разными углами
x0, y0, a = rng.uniform(-1, 1), rng.uniform(-1, 1), rng.uniform(0, np.pi)
u = (X - x0) * np.cos(a) + (Y - y0) * np.sin(a)
v = -(X - x0) * np.sin(a) + (Y - y0) * np.cos(a)
img += np.exp(-(u / 0.4) ** 2 - (v / 0.08) ** 2)
noise = rng.standard_normal((100, 100)) # белый шум того же размера
for r in [1, 2, 5, 10, 20, 40]:
print(f"r = {r:2d} чисел: {r * 200:4d} из 10000 ошибка картинки: {rel_err(img, low_rank(img, r)):.3f} ошибка шума: {rel_err(noise, low_rank(noise, r)):.3f}")
r = 1 чисел: 200 из 10000 ошибка картинки: 0.573 ошибка шума: 0.981
r = 2 чисел: 400 из 10000 ошибка картинки: 0.408 ошибка шума: 0.963
r = 5 чисел: 1000 из 10000 ошибка картинки: 0.216 ошибка шума: 0.911
r = 10 чисел: 2000 из 10000 ошибка картинки: 0.078 ошибка шума: 0.830
r = 20 чисел: 4000 из 10000 ошибка картинки: 0.008 ошибка шума: 0.683
r = 40 чисел: 8000 из 10000 ошибка картинки: 0.000 ошибка шума: 0.435
0204060110203040номер сингулярного числа iσᵢкартинка: σ₁ = 72, к i = 20 почти 0шум: σᵢ убывают медленно, от 19 до 10картинкашум
Первые 40 сингулярных чисел двух матриц 100×100: «картинки» из размытых пятен и белого шума (код ниже). Чем быстрее падает спектр, тем лучше матрица сжимается.

Картинку при r=20r = 20 мы храним в 40% чисел с ошибкой меньше 1%, а шум и при r=40r = 40 теряет 43%. Формально у картинки ранг 65 (np.linalg.matrix_rank так и скажет), но эффективный ранг около 20: остальные слои — пыль. Вывод, который стоит запомнить:

Структура сжимается, шум — нет. Если в данных есть закономерности, энергия собирается в немногих сингулярных числах. У случайной матрицы все направления равноправны, и выбросить нечего.

Обученные матрицы весов — не чистый шум, но и не гладкая картинка: их спектр обычно падает, но не до нуля. Гораздо лучше сжимаются изменения весов при дообучении. Авторы LoRA (Hu et al., 2021) предположили, что обновление ΔW\Delta W при адаптации модели к новой задаче имеет низкий «внутренний ранг», и подтвердили это экспериментами: для GPT-3 175B LoRA сократила число обучаемых параметров в 10 000 раз, а потребность в памяти GPU — втрое, а качество осталось на уровне полного дообучения. Отсюда и форма LoRA:

W′=W+ΔW=W+BA,B∈Rd×r,  A∈Rr×k,  r≪min⁡(d,k)\begin{gathered} W' = W + \Delta W = W + BA, \\ B \in \mathbb{R}^{d \times r}, \; A \in \mathbb{R}^{r \times k}, \; r \ll \min(d, k) \end{gathered}

Читаем: новые веса W′W' — это старые WW плюс добавка ΔW\Delta W («дельта» — изменение, урок 1.0.3), а добавка записана как произведение узкой BB (высотой dd и шириной всего rr) на узкую AA. Запись r≪min⁡(d,k)r \ll \min(d, k) значит «rr намного меньше обоих размеров матрицы». Это ровно «горлышко» из начала урока.

Вот как SVD превращает готовую матрицу в такую пару узких множителей (в PyTorch; о нём — заметка в уроке 1.1.2):

import torch
torch.manual_seed(0)
d_out, d_in, r = 1024, 1024, 32
# Сначала изготовим «веса» с известным спектром. QR — ещё одно разложение матрицы; здесь оно нужно только
# как рецепт получить случайную ортогональную матрицу (случайный «поворот»). Вникать в него не нужно.
U, _ = torch.linalg.qr(torch.randn(d_out, d_out)) # случайные ортогональные U и V
V, _ = torch.linalg.qr(torch.randn(d_in, d_in))
s = 1.0 / torch.arange(1, d_in + 1) ** 1.5 # быстро убывающий спектр
W = U @ torch.diag(s) @ V.T # «веса слоя» 1024×1024
U_, S_, Vh_ = torch.linalg.svd(W) # SVD в PyTorch
B = U_[:, :r] * S_[:r] # (1024, 32): столбцы uᵢ, умноженные на σᵢ
A = Vh_[:r] # (32, 1024): строки vᵢᵀ
err = torch.linalg.norm(W - B @ A) / torch.linalg.norm(W)
print(B.shape, A.shape)
print(f"чисел: {W.numel()} → {B.numel() + A.numel()} (в {W.numel() / (B.numel() + A.numel()):.0f} раз меньше)")
print(f"относительная ошибка: {err:.4f}")
torch.Size([1024, 32]) torch.Size([32, 1024])
чисел: 1048576 → 65536 (в 16 раз меньше)
относительная ошибка: 0.0198
ГдеЧто раскладываютЗачем
LoRA (урок 6.2.1)добавку к весам ΔW=BA\Delta W = BAучить в сотни раз меньше параметров
MLA, DeepSeek (урок 5.2.2)проекции ключей и значений через латент cKVc^{KV} из 512 чиселхранить в KV-кэше в десятки раз меньше
PCA (урок 2.3.2)матрицу данных: проекция на v1,…,vr\mathbf{v}_1, \dots, \mathbf{v}_rсжать признаки, нарисовать эмбеддинги на плоскости
Muon (урок 5.4.4)шаг оптимизатора G=UΣV⊤→UV⊤G = U\Sigma V^\top \to UV^\topвыровнять все направления обновления

Есть направления, которые матрица не поворачивает, а только растягивает:

Av=λvA\mathbf{v} = \lambda \mathbf{v}

Такой v\mathbf{v} называют собственным векторомсобственный векторEN eigenvectorНенулевой вектор v, который матрица только растягивает, не поворачивая — Av = λv., а λ\lambda — собственным числомсобственное числоEN eigenvalueЧисло λ, для которого Av = λv при некотором ненулевом v. Показывает, во сколько раз матрица растягивает «свои» направления.. Пример: для A=(2112)A = \begin{pmatrix} 2 & 1 \\ 1 & 2 \end{pmatrix} вектор (1,1)(1, 1) переходит в (3,3)(3, 3) — растяжение в 3 раза, λ=3\lambda = 3. Вектор (1,−1)(1, -1) переходит в себя, λ=1\lambda = 1. А у поворота на 90° вещественных собственных векторов нет вовсе: он поворачивает каждый вектор.

Собственные числа отвечают на вопрос: что будет, если применять матрицу снова и снова? Возьмём e1=(1,0)\mathbf{e}_1 = (1, 0):

kk12510
Ake1A^k \mathbf{e}_1(2,1)(2, 1)(5,4)(5, 4)(122,121)(122, 121)(29 525,29 524)(29\,525, 29\,524)

Вектор быстро разворачивается вдоль собственного направления (1,1)(1, 1) и растёт как 3k3^k. Отсюда проблема глубоких сетей и рекуррентных сетей (рекуррентная нейросетьрекуррентная нейросетьEN recurrent neural network (RNN), они обрабатывают текст по одному токену, пропуская сигнал через одну и ту же матрицу на каждом шаге): если сигнал много раз проходит через матрицы, направления с ∣λ∣>1\lvert\lambda\rvert > 1 взрываются (1.150≈1171.1^{50} \approx 117), а с ∣λ∣<1\lvert\lambda\rvert < 1 затухают (0.950≈0.0050.9^{50} \approx 0.005). Эту проблему вы встретите в уроках 3.4.1 и 3.4.3.

Связь с SVD (можно пропустить при первом чтении): сингулярные числа AA — это корни из собственных чисел матрицы A⊤AA^\top A. Для симметричной матрицы (она не меняется при транспонировании) с положительными собственными числами, как наша (2112)\begin{pmatrix} 2 & 1 \\ 1 & 2 \end{pmatrix}, SVD и разложение по собственным векторам совпадают: σ=(3,1)\sigma = (3, 1). На собственных векторах матрицы ковариации — таблицы, которая показывает, как признаки данных меняются вместе, — построен метод PCA (урок 2.3.2).

Реализуйте низкоранговое приближение через SVD, относительную ошибку, выбор ранга по доле энергии и коэффициент сжатия. Тесты проверят, что ваша ошибка совпадает с предсказанием теоремы Эккарта — Янга.

Сжатие матрицы через SVDnumpy · нужен интернет6 тестов
import numpy as np


def low_rank(A, r):
    """Лучшее приближение матрицы A матрицей ранга r.

    Возьмите SVD: U, S, Vt = np.linalg.svd(A, full_matrices=False)
    и оставьте r старших слагаемых: U[:, :r] · diag(S[:r]) · Vt[:r].
    """
    # TODO: ваш код
    ...


def relative_error(A, B):
    """Относительная ошибка ‖A − B‖ / ‖A‖ в норме Фробениуса (np.linalg.norm)."""
    # TODO: ваш код
    ...


def rank_for_energy(S, energy=0.9):
    """Минимальный ранг r, при котором доля «энергии» sum(S[:r]**2) / sum(S**2) ≥ energy.

    S — сингулярные числа по убыванию (как их возвращает np.linalg.svd).
    """
    # TODO: ваш код
    ...


def compression_ratio(m, n, r):
    """Во сколько раз меньше чисел нужно хранить: m·n против r·(m + n)."""
    # TODO: ваш код
    ...


# Проверьте себя кнопкой «Запустить»:
rng = np.random.default_rng(0)
A = rng.standard_normal((60, 5)) @ rng.standard_normal((5, 40))   # матрица 60×40 ранга 5
A += 0.01 * rng.standard_normal((60, 40))                         # плюс немного шума
for r in [1, 2, 5, 10]:
    print(r, relative_error(A, low_rank(A, r)))
print(rank_for_energy(np.linalg.svd(A, compute_uv=False), 0.99))  # 5 или меньше
print(compression_ratio(4096, 4096, 8))                           # 256.0

Задание для Jupyter (по желанию). Загрузите любую фотографию: img = plt.imread("photo.jpg").mean(axis=2) даст чёрно-белую матрицу. Сожмите её с r=5,20,50r = 5, 20, 50 и покажите результаты через plt.imshow(..., cmap="gray"). С какого rr вы перестаёте замечать разницу? Сколько процентов чисел при этом хранится? Постройте график сингулярных чисел фотографии в логарифмическом масштабе (plt.semilogy).

Проверь себя6 вопросов
1 Чему равен ранг матрицы (1224)\begin{pmatrix} 1 & 2 \\ 2 & 4 \end{pmatrix}?
2 Сколько обучаемых чисел в LoRA-добавке BABA для матрицы 4096×40964096 \times 4096 при r=16r = 16?
3 Сингулярные числа матрицы — (10,1,0.1)(10, 1, 0.1). Какова относительная ошибка (по норме Фробениуса) лучшего приближения ранга 1? Подсказка: ошибка — сумма отброшенных σi2\sqrt{\text{сумма отброшенных } \sigma_i^2}, а сама норма матрицы — сумма всех σi2\sqrt{\text{сумма всех } \sigma_i^2}. Округлите до двух знаков.
4 Какие утверждения верны?Можно выбрать несколько ответов
5 Для A=(2112)A = \begin{pmatrix} 2 & 1 \\ 1 & 2 \end{pmatrix} вектор (1,−1)(1, -1) — собственный. Чему равно его собственное число?
6 Что хранит в KV-кэше механизм MLA в DeepSeek-V2 вместо полных ключей и значений всех голов?
  • Ранг — число независимых направлений в матрице. Матрица ранга rr — сумма rr внешних произведений и хранится в r(m+n)r(m+n) числах.
  • Произведение через узкое горлышко BABA имеет ранг не больше rr — на этом стоят LoRA и MLA.
  • SVD A=UΣV⊤A = U\Sigma V^\top — поворот, растяжение, поворот; или сумма слоёв σiuivi⊤\sigma_i \mathbf{u}_i \mathbf{v}_i^\top по убыванию важности.
  • Лучшее приближение ранга rr — первые rr слоёв SVD, ошибка ∑i>rσi2\sqrt{\sum_{i>r} \sigma_i^2}. Структура сжимается, шум — нет.
  • Собственные векторы матрица только растягивает; при многократном применении ∣λ∣>1\lvert\lambda\rvert > 1 взрывается, ∣λ∣<1\lvert\lambda\rvert < 1 затухает.

Дальше: модуль 1.2 — производные. Узнаем, как измерить, насколько сильно loss реагирует на каждый параметр модели.