Матрицы и умножение матриц
Матрица — таблица чисел
Матрица размера m×n — прямоугольная таблица из m строк и n столбцов. Элемент в строке i и столбце j обозначают a_{ij}.
В ML матрица чаще всего — это:
- данные: строка — объект, столбец — признак. Таблица из 1000 квартир по 5 признаков — матрица 1000×5;
- веса слоя нейросети: матрица, которая превращает вектор из d_in чисел в вектор из d_out чисел;
- картинка: матрица яркостей пикселей.
import numpy as np
import vis
A = np.array([[1, 2, 3],
[4, 5, 6]])
print(A.shape) # (2, 3): 2 строки, 3 столбца
print(A[1, 0]) # элемент a₂₁ = 4 (в NumPy индексы с нуля)
print(A[0]) # первая строка — вектор
print(A[:, 2]) # третий столбец — вектор
vis.heatmap(A, title="Матрица как таблица", fmt=".0f")
Простые операции
- Сложение и умножение на число — поэлементно, как у векторов (размеры должны совпадать).
- Транспонирование A^T — строки становятся столбцами: (A^T)_{ij} = a_{ji}. Матрица m×n превращается в n×m.
import numpy as np
A = np.array([[1, 2, 3], [4, 5, 6]])
B = np.ones((2, 3), dtype=int)
print(A + B)
print(3 * A)
print(A.T, A.T.shape)
Матрица на вектор: два взгляда
Пусть A — матрица m×n, x — вектор из n чисел. Результат Ax — вектор из m чисел. Его можно понимать двумя способами, и оба важны.
Взгляд 1: строки. Каждый элемент результата — скалярное произведение строки A на x:
Если каждая строка — веса одного нейрона, то Ax — это выходы m нейронов сразу.
Взгляд 2: столбцы. Результат — линейная комбинация столбцов A с коэффициентами из x:
Этот взгляд объясняет, куда матрица «переносит» пространство, — об этом следующий урок.
import numpy as np
A = np.array([[1, 2], [3, 4], [5, 6]])
x = np.array([10, 1])
print("готовое: ", A @ x)
print("строки · x: ", np.array([row @ x for row in A]))
print("комбинация столбцов:", x[0] * A[:, 0] + x[1] * A[:, 1])
Умножение матриц
Произведение C = AB матрицы A размера m×n на B размера n×k — матрица m×k:
Иначе говоря, AB — это A, умноженная на каждый столбец B, а результаты сложены в столбцы.
import numpy as np
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
print(A @ B)
print("c₁₂ вручную:", A[0] @ B[:, 1]) # 1·6 + 2·8 = 22
print(B @ A) # другой результат!
print(A * B) # а это поэлементное произведение — совсем не то же самое
@ и * — разные операции
A @ B — матричное умножение. A * B — поэлементное (произведение Адамара), формы должны совпадать или быть совместимыми для broadcasting. Путать их — частая ошибка.
Свойства
- Не коммутативно: в общем случае AB \ne BA (и даже формы могут не позволить переставить).
- Ассоциативно: (AB)C = A(BC) — можно расставлять скобки как удобно.
- Дистрибутивно: A(B + C) = AB + AC.
- Транспонирование меняет порядок: (AB)^T = B^T A^T.
Единичная матрица
I — квадратная матрица с единицами на диагонали и нулями вне её. Умножение на неё ничего не меняет: AI = IA = A. Это «единица» для матриц.
import numpy as np
A = np.array([[2, 1], [0, 3]])
I = np.eye(2)
print(A @ I)
print(np.allclose(A @ I, A), np.allclose(I @ A, A))
Сколько это стоит
Для умножения m×n на n×k нужно m·n·k умножений. Две матрицы 1000×1000 — миллиард операций. Именно поэтому нейросети обучают на видеокартах: GPU умеют делать тысячи умножений параллельно. А NumPy использует оптимизированные библиотеки (BLAS), которые в сотни раз быстрее тройного цикла на Python:
import numpy as np, time
n = 60
rng = np.random.default_rng(0)
A, B = rng.normal(size=(n, n)), rng.normal(size=(n, n))
t = time.perf_counter()
C = [[sum(A[i, t_] * B[t_, j] for t_ in range(n)) for j in range(n)] for i in range(n)]
t_loop = time.perf_counter() - t
t = time.perf_counter()
C2 = A @ B
t_np = time.perf_counter() - t
print(f"тройной цикл: {t_loop:.2f} с, NumPy: {t_np * 1000:.2f} мс")
print("результаты совпадают:", np.allclose(C, C2))
Батч через слой: X @ W
Самое важное применение. Пусть у нас батч из N объектов, каждый — вектор из d_in признаков. Сложим их строками в матрицу X размера N×d_in. Веса слоя — матрица W размера d_in×d_out, сдвиги — вектор b из d_out чисел. Тогда
даёт матрицу N×d_out: выходы слоя для всех объектов сразу, одной операцией. Сдвиг b прибавляется к каждой строке благодаря broadcasting.
import numpy as np
rng = np.random.default_rng(0)
X = rng.normal(size=(4, 3)) # 4 объекта по 3 признака
W = rng.normal(size=(3, 2)) # слой: 3 входа → 2 выхода
b = np.array([0.5, -1.0])
Y = X @ W + b
print("форма выхода:", Y.shape)
print(Y.round(3))
# то же самое циклом по объектам — медленнее, но смысл один
print(np.allclose(Y, np.array([x @ W + b for x in X])))
Это и есть линейный слой нейросети. Через несколько уроков мы напишем его как класс и научимся подбирать W и b.
Почему W размера (d_in, d_out), а не наоборот
В PyTorch веса nn.Linear хранятся как (d_out, d_in), а слой считает X @ W.T + b. Математически это одно и то же. В нашей библиотеке nanotorch мы храним (d_in, d_out), чтобы формула была без транспонирования.
Итог
- Матрица m×n — таблица чисел; в ML это данные (объекты × признаки) или веса слоя.
- Ax — это и «скалярные произведения строк на x», и «линейная комбинация столбцов».
- (m \times n) \cdot (n \times k) = (m \times k): внутренние размеры совпадают и сокращаются; c_{ij} — строка на столбец.
- AB \ne BA, но (AB)C = A(BC) и (AB)^T = B^T A^T;
@— матричное умножение,*— поэлементное. - Линейный слой на батче — одна операция XW + b.
Практика решено 0 из 6
Какой формы будет результат умножения матрицы (5, 3) на матрицу (3, 7)? Ответ в виде m, k, например 2, 4.
Какое умножение невозможно?
Вычислите вручную произведение
Ответ запишите как два числа через запятую, например 7, 8.
Какие утверждения верны для матриц A, B, C подходящих размеров?
Напишите matmul(A, B) — умножение матриц, заданных списками списков, на чистом Python
(без NumPy). Если внутренние размеры не совпадают, бросьте ValueError.
Элемент результата C[i][j] — скалярное произведение i-й строки A на j-й столбец B.
Какие тесты проверяют код (5)
assert matmul([[1, 2], [3, 4]], [[5, 6], [7, 8]]) == [[19, 22], [43, 50]]
assert matmul([[1, 2, 3]], [[1], [0], [-1]]) == [[-2]]
assert matmul([[1], [2]], [[3, 4]]) == [[3, 4], [6, 8]]
try:
matmul([[1, 2]], [[1, 2]])
assert False, "Для форм (1, 2) и (1, 2) нужно бросить ValueError"
except ValueError:
pass
import inspect
assert "numpy" not in inspect.getsource(matmul), "Без NumPy — напишите сами"
def matmul(A, B):
n, m, k = len(A), len(B), len(B[0])
if any(len(row) != m for row in A):
raise ValueError("внутренние размеры не совпадают")
return [[sum(A[i][t] * B[t][j] for t in range(m)) for j in range(k)] for i in range(n)]
Линейный слой на батче. X — матрица (N, d_in): N объектов по d_in признаков.
W — матрица весов (d_in, d_out), b — вектор сдвигов (d_out,).
Напишите linear(X, W, b), которая возвращает (N, d_out): для каждого объекта x @ W + b.
Одной строкой, без циклов.
Какие тесты проверяют код (2)
X = np.array([[1.0, 2], [0, 1], [3, -1]])
W = np.array([[1.0, 0, 2], [0, 1, -1]])
b = np.array([10.0, 20, 30])
out = linear(X, W, b)
assert np.shape(out) == (3, 3), f"Форма должна быть (3, 3), получено {np.shape(out)}"
assert np.allclose(out, [[11, 22, 30], [10, 21, 29], [13, 19, 37]])
rng = np.random.default_rng(0)
X, W, b = rng.normal(size=(8, 4)), rng.normal(size=(4, 2)), rng.normal(size=2)
assert np.allclose(linear(X, W, b), np.array([x @ W + b for x in X]))
import numpy as np
def linear(X, W, b):
return X @ W + b
Войдите, чтобы отмечать пройденные уроки.