Матрицы и матричное умножение
Матрица как преобразование пространства, умножение матриц как композиция преобразований и линейный слой нейросети как одно матричное умножение плюс сдвиг.
Зачем это нужно
Заголовок раздела «Зачем это нужно»Если посмотреть, на что уходит время при обучении и работе трансформера, почти всё окажется умножением матриц. Проекции запросов, ключей и значений во внимании (формулу attention вы читали в уроке 1.0.3), оба слоя FFNполносвязный блок (FFN)EN feed-forward network (FFN) (feed-forward network — блок из двух слоёв нейронов, который стоит в каждом слое трансформера), выходной слой со словарём в сотню тысяч токенов — это матричные умножения. Видеокарты потому и стали главным железом ИИ, что умеют делать их невероятно быстро.
В прошлом уроке мы научились работать с векторами. Теперь научимся их преобразовывать.
Матрица — это способ одним объектом записать преобразование «вектор → вектор». А
линейный слойлинейный (полносвязный) слойEN linear layer (fully connected layer)y = xWᵀ + b. В PyTorch nn.Linear(in, out) хранит weight формы (out, in) — частый источник путаницы. нейросети — это ровно одна матрица плюс сдвиг. В конце урока вы напишете
матричное умножение руками и поймёте, что именно делает nn.Linear в PyTorch.
Матрица — таблица чисел
Заголовок раздела «Матрица — таблица чисел»МатрицаматрицаEN matrixПрямоугольная таблица чисел размера m×n. Задаёт линейное преобразование из n-мерного пространства в m-мерное. размера — это прямоугольная таблица из строк и столбцов. Размер читается «эм на эн», сначала строки:
Элемент в строке и столбце обозначают : здесь . В NumPy форма матрицы — A.shape == (2, 3),
а тот же элемент — A[1, 2], потому что в Python счёт идёт с нуля. Эта разница в индексах — классический источник
ошибок при переводе формул в код.
Матрицы в LLM огромные. Таблица эмбеддингов DeepSeek-V3 — матрица : по строке на каждый токен словаря, всего около 927 млн чисел. Каждый слой содержит ещё несколько матриц весов.
Матрица, умноженная на вектор: два взгляда
Заголовок раздела «Матрица, умноженная на вектор: два взгляда»Возьмём и . Произведение можно понимать двумя способами, и оба нужны.
Взгляд 1: строки на вектор. Каждая координата результата — скалярное произведение строки матрицы на :
Так удобно считать. И так удобно думать о нейронах: каждая строка — веса одного нейрона, а строк — это нейронов, которые смотрят на один и тот же вход.
Взгляд 2: комбинация столбцов. Результат — это линейная комбинация (урок 1.1.1) столбцов матрицы с коэффициентами из : первый столбец берём раз, второй — раза и складываем:
Так удобно понимать, что делает матрица. Об этом следующий раздел.
Формы: матрица принимает вектор длины и возвращает вектор длины . Значит, матрица — это функция
. Если длина вектора не равна числу столбцов, умножать нельзя: NumPy ответит
ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0 … (size 2 is different from 3). Сообщение длинное,
но главное в нём — в конце: какие размеры не совпали.
Матрица — это преобразование пространства
Заголовок раздела «Матрица — это преобразование пространства»Возьмём базисные векторы — единичные стрелки вдоль осей и — и умножим на них матрицу. По «взгляду 2» — это просто первый столбец , а — второй. Отсюда главная идея урока:
Столбцы матрицы — это то, куда она переводит базисные векторы. Зная, куда попали и , мы знаем, куда попадёт любой вектор .
Такие преобразования называют линейнымилинейное преобразованиеEN linear map (linear transformation)Функция f, для которой f(a + b) = f(a) + f(b) и f(c·a) = c·f(a). Каждое такое преобразование — умножение на матрицу.. Они уважают сложение и умножение на число: и . Геометрически это значит, что прямые остаются прямыми (или схлопываются в точку, как при проекции ниже), параллельные — параллельными, а начало координат остаётся на месте.
| Преобразование | Матрица | Что происходит |
|---|---|---|
| Растяжение | по вдвое длиннее, по вдвое короче | |
| Поворот на 90° | всё вращается против часовой стрелки | |
| Скос (shear) | квадрат «съезжает» вбок и становится параллелограммом | |
| Проекция | всё сплющивается на ось , информация об теряется | |
| Ничего не делать | единичная матрицаединичная матрицаEN identity matrixКвадратная матрица I с единицами на диагонали и нулями вне её; Ix = x. В NumPy — np.eye(n). : |
Обратите внимание на проекцию: она «схлопывает» плоскость в линию. Такие матрицы теряют информацию, и в следующем уроке мы научимся это измерять числом — рангом.
Неквадратные матрицы меняют размерность. Матрица переносит плоскость в трёхмерное пространство, матрица сплющивает трёхмерное пространство в плоскость. В нейросетях это обычное дело: в FFN-блоке GPT-2 вектор размерности 768 сначала «раздувают» до 3072, а потом сжимают обратно до 768.
Умножение матриц — композиция преобразований
Заголовок раздела «Умножение матриц — композиция преобразований»Что будет, если применить к вектору сначала преобразование , а потом ? Получится . Оказывается, это тоже линейное преобразование, и его матрицу называют произведением :
Читать произведение нужно справа налево: в сначала действует , потом — как в композиции функций из урока 1.0.1.
Как считать
Заголовок раздела «Как считать»Матричное умножениематричное умножениеEN matrix multiplication (matmul)(m×n)·(n×k) → (m×k), элемент cᵢⱼ = Σₜ aᵢₜbₜⱼ. Соответствует композиции преобразований. Стоит ≈ 2·m·n·k операций. определено, когда число столбцов равно числу строк . Элемент — скалярное произведение -й строки на -й столбец :
Пример:
Левый верхний элемент: первая строка на первый столбец — это . Остальные три — так же.
Правило форм: . Внутренние размеры совпадают и «сокращаются», внешние остаются. Проверяйте формы раньше, чем значения, — это сэкономит часы отладки:
| Что умножаем | Формы | Результат |
|---|---|---|
| Батч эмбеддингов на веса слоя | ||
| Матрица на вектор-столбец | ||
| Строка на столбец | — скалярное произведение | |
| Столбец на строку | — внешнее произведение |
Свойства, которые нужно знать
Заголовок раздела «Свойства, которые нужно знать»- Порядок важен: в общем случае . Возьмём поворот на 90° и растяжение по оси . Вектор после (сначала растянуть, потом повернуть) превращается в , а после (сначала повернуть, потом растянуть) — в . Растяжение по уже ничего не делает с вектором, который смотрит вверх.
- Скобки можно переставлять: . Это свойство называют ассоциативностью. Результат одинаковый, а стоимость — нет (см. ниже).
- Единичная матрица — «единица» умножения: .
import numpy as np
R = np.array([[0, -1], [1, 0]]) # поворот на 90°S = np.array([[2, 0], [0, 1]]) # растяжение по xe1 = np.array([1, 0])print((R @ S) @ e1) # [0 2] — сначала S, потом Rprint((S @ R) @ e1) # [0 1] — сначала R, потом SСколько это стоит
Заголовок раздела «Сколько это стоит»Каждый из элементов результата — скалярное произведение длины , то есть около операций. Итого умножение стоит примерно операций с плавающей точкой (FLOPs). Пример: один слой FFN GPT-2 () на 1024 токенах — это млрд операций.
Из ассоциативности следует важный инженерный трюк. Пусть и — матрицы , а — вектор.
- : сначала перемножаем матрицы ( млрд операций), потом умножаем на вектор.
- : два умножения матрицы на вектор, млн операций.
Ответ одинаковый, а второй путь в 2048 раз дешевле. На перестановке скобок построено «поглощение матриц» в механизме MLA от DeepSeek (урок 5.2.2): там тоже выбирают, в каком порядке перемножать, чтобы не делать лишней работы.
И ещё о скорости. Наивный тройной цикл на чистом Python перемножает две матрицы примерно за 1 секунду,
а NumPy на одном ядре процессора — примерно за 1 миллисекунду, то есть в тысячу с лишним раз быстрее (замер на машине
автора; у вас числа будут другими, но порядок тот же). NumPy вызывает библиотеку BLAS, где умножение матриц
оптимизировано под кэши и векторные инструкции процессора. Поэтому в практике ниже вы напишете умножение руками
один раз, чтобы понять, а дальше всегда будете использовать @.
Транспонирование
Заголовок раздела «Транспонирование»ТранспонированиетранспонированиеEN transposeСтроки становятся столбцами — Aᵀ. Полезное правило — (AB)ᵀ = BᵀAᵀ. В NumPy и PyTorch — A.T. меняет строки и столбцы местами: , форма
становится . В коде — A.T.
Полезное правило: — при транспонировании произведения множители меняются местами. Проверьте
на примере выше: , и B.T @ A.T даёт то же самое.
В статьях векторы обычно считают столбцами (матрицами ). Тогда скалярное произведение записывают как — строка на столбец, получается . А столбец на строку, , даёт целую матрицу — внешнее произведениевнешнее произведениеEN outer productСтолбец на строку — u vᵀ, матрица ранга 1. В китайских текстах «внешним произведением» иногда называют векторное произведение (cross product), смотрите по контексту.:
Все строки этой матрицы пропорциональны друг другу. Такие матрицы «ранга 1» — главные герои следующего урока и формулы градиента в уроке 1.2.3.
Линейный слой нейросети
Заголовок раздела «Линейный слой нейросети»Теперь соберём всё вместе. Слой из нейронов, каждый со своими весами (строка матрицы ) и своим смещениемсмещение (bias)EN biasСвободный член b в y = Wx + b. Не путать с «предвзятостью» модели (bias в смысле fairness) и со смещением в bias–variance. (его ещё называют сдвигом — то самое из , урок 1.0.1), считает
Это линейный (полносвязный) слой. Числа: пусть (3 нейрона, у каждого 2 входа), и . Строки на дают , плюс сдвиг — . Этот же пример печатает заготовка задания в конце урока.
В коде векторы обычно лежат строками матрицы. Так удобно обрабатывать сразу батч — пачку из примеров, которые модель считает одновременно (на видеокарте это почти так же быстро, как один пример). Батч формы превращается в формы по формуле
Читаем: каждая строка (один пример) умножается на — это то же «строки на вектор», только для всех примеров сразу, — и к каждой строке результата прибавляется . Сдвиг прибавляется к каждой строке благодаря broadcasting (урок 0.3.2). На NumPy:
import numpy as np
W = np.array([[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]) # (out, in) = (3, 2): строка — веса одного нейронаb = np.array([0.0, 0.0, 10.0]) # по одному сдвигу на нейронX = np.array([[1.0, 2.0], # батч из двух примеров, форма (2, 2) [3.0, -1.0]])Y = X @ W.T + b # (2, 2) @ (2, 3) → (2, 3), затем + b к каждой строкеprint(Y)[[ 1. 2. 13.] [ 3. -1. 12.]]Так устроен nn.Linear в PyTorch — тот же расчёт, только веса создаются случайными и хранятся внутри слоя:
import torch
torch.manual_seed(0) # фиксируем случайность, как seed в NumPylayer = torch.nn.Linear(768, 3072) # слой 768 → 3072, как в FFN GPT-2; веса — случайные числаprint(layer.weight.shape) # torch.Size([3072, 768]) — (out, in)!print(layer.bias.shape) # torch.Size([3072])# parameters() перебирает все обучаемые массивы слоя (weight и bias), numel() — число элементов в массивеprint(sum(p.numel() for p in layer.parameters())) # 2362368 = 768·3072 + 3072
x = torch.randn(32, 768) # батч из 32 случайных векторов (как rng.standard_normal)y = layer(x) # вызов слоя как функции: считает x @ W.T + bprint(y.shape) # torch.Size([32, 3072])print(torch.allclose(y, x @ layer.weight.T + layer.bias, atol=1e-6)) # True — та же формула
X = torch.randn(4, 10, 768) # (батч, токены, признаки)print(layer(X).shape) # torch.Size([4, 10, 3072]) — слой действует на последнюю осьДва линейных слоя — это один слой
Заголовок раздела «Два линейных слоя — это один слой»В уроке 1.0.1 мы видели, что : композиция прямых — снова прямая. Матричная версия того же факта:
Сколько линейных слоёв ни ставь подряд, получится одна матрица и один сдвиг . Проверим:
torch.manual_seed(0)l1 = torch.nn.Linear(4, 8)l2 = torch.nn.Linear(8, 3)W = l2.weight @ l1.weight # (3, 8) @ (8, 4) → (3, 4)b = l2.weight @ l1.bias + l2.bias # (3,)x = torch.randn(5, 4)print(torch.allclose(l2(l1(x)), x @ W.T + b, atol=1e-6)) # TrueПоэтому между линейными слоями всегда стоит нелинейность — ReLU, GELU или SiLU (урок 3.1.1). А сами линейные слои делают всю «тяжёлую» работу: именно в их матрицах хранится почти всё, что знает модель.
flowchart LR X["x, 768 чисел"] --> L1["Linear 768 → 3072<br/>W₁: 3072×768"] --> G["GELU"] --> L2["Linear 3072 → 768<br/>W₂: 768×3072"] --> Y["y, 768 чисел"]
Практика
Заголовок раздела «Практика»Реализуйте shape, transpose, matmul и linear на чистом Python. Тесты сверят ваш ответ с NumPy на случайных
неквадратных матрицах — в них перепутанные индексы сразу дают ошибку.
# Матрица — список строк: [[1, 2, 3], [4, 5, 6]] — это 2 строки и 3 столбца.
def shape(A):
"""Форма матрицы: кортеж (число строк, число столбцов).
Пример: shape([[1, 2, 3], [4, 5, 6]]) → (2, 3).
"""
# TODO: строк — len(A), столбцов — длина первой строки len(A[0])
...
def transpose(A):
"""Транспонирование: строки становятся столбцами. Форма (m, n) → (n, m).
Пример: transpose([[1, 2, 3], [4, 5, 6]]) → [[1, 4], [2, 5], [3, 6]].
Элемент [j][i] результата равен A[i][j].
"""
# TODO: ваш код
...
def matmul(A, B):
"""Матричное умножение: (m, n) @ (n, k) → (m, k).
C[i][j] = сумма по t от A[i][t] * B[t][j] — «строка i на столбец j».
Если внутренние размеры не совпадают, бросьте ValueError.
Пример: matmul([[1, 2], [3, 4]], [[5, 6], [7, 8]]) → [[19, 22], [43, 50]]
(19 = 1·5 + 2·7 — первая строка на первый столбец).
"""
# TODO: m, n = shape(A); n2, k = shape(B); проверка n == n2; затем двойной цикл по i и j
...
def linear(X, W, b):
"""Линейный слой как в PyTorch (nn.Linear): Y = X @ Wᵀ + b.
X — батч входов формы (batch, in), W — веса формы (out, in), b — список длины out.
Возвращает матрицу формы (batch, out). Используйте свои transpose и matmul.
Пример: linear([[1, 2]], [[1, 0], [0, 1], [1, 1]], [0, 0, 10]) → [[1, 2, 13]].
"""
# TODO: Y = matmul(X, transpose(W)), затем к элементу столбца j каждой строки прибавьте b[j]
...
# Проверьте себя кнопкой «Запустить»:
A = [[1, 2, 3], [4, 5, 6]]
B = [[7, 8], [9, 10], [11, 12]]
print(shape(A), transpose(A)) # (2, 3) [[1, 4], [2, 5], [3, 6]]
print(matmul(A, B)) # [[58, 64], [139, 154]]
print(linear([[1, 2]], [[1, 0], [0, 1], [1, 1]], [0, 0, 10])) # [[1, 2, 13]]
Задание для Jupyter (по желанию). Возьмите своё matmul и замерьте время умножения матриц для
через time.perf_counter(). Во сколько раз растёт время при удвоении ? Сравните с формулой
(подсказка: должно получиться около 8 раз). Затем сделайте то же для A @ B в NumPy при .
Проверь себя
Заголовок раздела «Проверь себя»- Матрица — это линейное преобразование . Её столбцы — образы базисных векторов.
- — это и «строки на вектор» (так считать), и «комбинация столбцов» (так понимать).
- Умножение матриц — композиция преобразований: , формы , порядок важен, скобки переставлять можно, стоимость .
- Линейный слой — ; в PyTorch веса хранятся как (out, in). Без нелинейности стопка линейных слоёв равна одному слою.
Дальше: ранг и SVD — как понять, сколько информации на самом деле несёт матрица, и как сжать её в десятки раз. На этом стоят LoRA и MLA.