Матрица как преобразование пространства
Матрица — это функция
В прошлом уроке матрица умножалась на вектор и давала новый вектор. Значит, матрица — это функция: на вход вектор, на выход вектор. Такие функции называются линейными преобразованиями.
Геометрически линейное преобразование деформирует пространство так, что:
- начало координат остаётся на месте;
- прямые остаются прямыми, а параллельные — параллельными;
- равномерная сетка остаётся равномерной (клетки превращаются в одинаковые параллелограммы).
Поиграйте с виджетом: двигайте ползунки a, b, c, d матрицы и выбирайте примеры. Серая сетка — исходная плоскость, синяя — после преобразования. Зелёная стрелка — куда перешёл вектор e_1 = (1, 0), оранжевая — куда перешёл e_2 = (0, 1).
Столбцы — образы базисных векторов
Любой вектор раскладывается по базису: (x, y) = x \cdot e_1 + y \cdot e_2. Преобразование линейное, поэтому
Достаточно знать, куда переходят e_1 и e_2, — и мы знаем, куда переходит всё. А Ae_1 — это первый столбец A, Ae_2 — второй:
Отсюда рецепт: чтобы построить матрицу преобразования, запишите образы базисных векторов в столбцы.
import numpy as np
import vis
A = np.array([[2, 1],
[0, 1]])
print("A·e₁ =", A @ [1, 0], "— первый столбец")
print("A·e₂ =", A @ [0, 1], "— второй столбец")
v = np.array([1, 1])
print("A·(1, 1) =", A @ v, "= A·e₁ + A·e₂")
vis.transform(A, title="A = [[2, 1], [0, 1]]", vecs=[[1, 1]])
Зоопарк преобразований
import numpy as np
import vis
phi = np.radians(30)
examples = {
"Поворот на 30°": [[np.cos(phi), -np.sin(phi)], [np.sin(phi), np.cos(phi)]],
"Растяжение по x": [[2, 0], [0, 1]],
"Сдвиг (shear)": [[1, 1], [0, 1]],
"Отражение от оси y": [[-1, 0], [0, 1]],
"Проекция на ось x": [[1, 0], [0, 0]],
}
for title, M in examples.items():
vis.transform(M, title=title)
| Преобразование | Матрица | Что делает с площадью |
|---|---|---|
| Поворот на φ | \begin{pmatrix} \cos\varphi & -\sin\varphi \\ \sin\varphi & \cos\varphi \end{pmatrix} | не меняет |
| Растяжение | \begin{pmatrix} s_x & 0 \\ 0 & s_y \end{pmatrix} | умножает на s_x s_y |
| Сдвиг | \begin{pmatrix} 1 & k \\ 0 & 1 \end{pmatrix} | не меняет |
| Отражение | \begin{pmatrix} -1 & 0 \\ 0 & 1 \end{pmatrix} | не меняет, но «выворачивает» |
| Проекция | \begin{pmatrix} 1 & 0 \\ 0 & 0 \end{pmatrix} | схлопывает в ноль |
Во сколько раз меняется площадь — это определитель матрицы (det в углу графиков). О нём — в следующем уроке.
Преобразуем картинку
Применим преобразование ко всем точкам фигуры сразу. Если точки лежат строками в матрице P формы (N, 2), то образ — P @ A.T (каждая строка умножается на A, см. правило (Ax)^T = x^T A^T).
import numpy as np
import vis
# «домик» из точек
t = np.linspace(0, 1, 20)
walls = np.concatenate([
np.c_[t, 0 * t], np.c_[1 + 0 * t, t], np.c_[1 - t, 1 + 0 * t], np.c_[0 * t, 1 - t], # квадрат
np.c_[t / 2, 1 + t / 2], np.c_[0.5 + t / 2, 1.5 - t / 2], # крыша
])
A = np.array([[1, 0.6],
[0, 1]]) # сдвиг
B = walls @ A.T
pts = np.vstack([walls, B])
labels = [0] * len(walls) + [1] * len(B)
vis.scatter(pts, labels=labels, title="Домик (синий) и после сдвига (оранжевый)", equal=True)
Композиция = произведение матриц
Применим сначала A, потом B: B(Ax) = (BA)x. Последовательное применение преобразований — это умножение их матриц, причём матрица, применённая первой, стоит справа.
Поскольку AB \ne BA, порядок важен: «повернуть, потом растянуть» — не то же, что «растянуть, потом повернуть».
import numpy as np
import vis
R = np.array([[0, -1], [1, 0]]) # поворот на 90°
S = np.array([[2, 0], [0, 1]]) # растяжение по x
vis.transform(S @ R, title="Сначала поворот, потом растяжение: S·R")
vis.transform(R @ S, title="Сначала растяжение, потом поворот: R·S")
print(S @ R)
print(R @ S)
Почему нейросетям нужна нелинейность
Слой нейросети без активации — это y = xW + b, то есть линейное преобразование плюс сдвиг. Что если поставить два таких слоя подряд?
Получился снова один линейный слой. Сто линейных слоёв подряд — всё равно один: композиция линейных преобразований линейна. Такая «глубокая» сеть умеет только поворачивать, растягивать и сдвигать пространство, а значит, может провести между классами только прямую (плоскость).
import numpy as np
rng = np.random.default_rng(0)
W1, b1 = rng.normal(size=(2, 5)), rng.normal(size=5)
W2, b2 = rng.normal(size=(5, 3)), rng.normal(size=3)
x = rng.normal(size=(4, 2))
deep = (x @ W1 + b1) @ W2 + b2
W, b = W1 @ W2, b1 @ W2 + b2
print(np.allclose(deep, x @ W + b)) # True: два слоя схлопнулись в один
Поэтому между слоями ставят нелинейную функцию активации — например, ReLU: \max(0, z). Она «сгибает» пространство, и тогда каждый следующий слой может работать уже с изогнутыми данными. Посмотрите, что ReLU делает с сеткой:
import numpy as np
import vis
g = np.linspace(-2, 2, 21)
lines = [np.c_[g, 0 * g + k] for k in g[::2]] + [np.c_[0 * g + k, g] for k in g[::2]]
P = np.vstack(lines)
A = np.array([[1.0, 0.8], [-0.6, 1.0]])
after_linear = P @ A.T
after_relu = np.maximum(0, after_linear - 0.3)
vis.scatter(after_linear, title="После линейного слоя: сетка просто перекошена", equal=True)
vis.scatter(after_relu, title="После ReLU: половина пространства прижата к осям — плоскость «сложена»", equal=True)
Именно чередование «линейное преобразование → нелинейность» позволяет нейросети постепенно распрямлять сложные данные, пока классы не станут разделимы прямой. Мы увидим это вживую в уроке про полносвязные сети.
Итог
- Матрица — линейное преобразование: сетка остаётся прямой и равномерной, начало координат — на месте.
- Столбцы матрицы — куда переходят базисные векторы. Чтобы построить матрицу, запишите образы e_1, e_2 в столбцы.
- Поворот, растяжение, сдвиг, отражение и проекция — всё это матрицы.
- Композиция преобразований — произведение матриц, первое применённое стоит справа: B(Ax) = (BA)x.
- Линейные слои подряд схлопываются в один; нейросетям нужна нелинейность между слоями.
Практика решено 0 из 5
Матрица [[0, -1], [1, 0]] переводит вектор (1, 0) в (0, 1), а (0, 1) в (-1, 0). Что это за преобразование?
Преобразование растягивает плоскость по оси x в 3 раза и не меняет ось y.
Запишите его матрицу в виде [[a, b], [c, d]].
Сначала применили преобразование A, потом B. Какая матрица описывает итог (векторы — столбцы, умножаем слева)?
Сеть из трёх линейных слоёв без функций активации: y = x @ W1 @ W2 @ W3. Что она умеет?
Напишите rotation(deg) — матрицу поворота плоскости на deg градусов против часовой стрелки
(NumPy-массив 2×2):
и rotate_points(P, deg) — поворот набора точек P формы (N, 2) (точки в строках).
Без циклов.
Какие тесты проверяют код (4)
assert np.allclose(rotation(90) @ np.array([1, 0]), [0, 1]), "Поворот (1, 0) на 90° должен дать (0, 1)" assert np.allclose(rotation(30) @ rotation(60), rotation(90)), "Два поворота — поворот на сумму углов" P = np.array([[1.0, 0], [0, 2], [1, 1]]) Q = rotate_points(P, 90) assert np.shape(Q) == (3, 2), "Форма должна сохраниться" assert np.allclose(Q, [[0, 1], [-2, 0], [-1, 1]]), "Точки — строки: умножайте на транспонированную матрицу" assert np.allclose(np.linalg.norm(rotate_points(np.array([[3.0, 4]]), 37)), 5), "Поворот не меняет длину"
import numpy as np
def rotation(deg):
phi = np.radians(deg)
return np.array([[np.cos(phi), -np.sin(phi)],
[np.sin(phi), np.cos(phi)]])
def rotate_points(P, deg):
return P @ rotation(deg).T
Войдите, чтобы отмечать пройденные уроки.