Матрица как преобразование пространства

⏱ 30 мин · 🧩 заданий: 5

Матрица — это функция

В прошлом уроке матрица умножалась на вектор и давала новый вектор. Значит, матрица — это функция: на вход вектор, на выход вектор. Такие функции называются линейными преобразованиями.

Геометрически линейное преобразование деформирует пространство так, что:

  • начало координат остаётся на месте;
  • прямые остаются прямыми, а параллельные — параллельными;
  • равномерная сетка остаётся равномерной (клетки превращаются в одинаковые параллелограммы).

Поиграйте с виджетом: двигайте ползунки a, b, c, d матрицы и выбирайте примеры. Серая сетка — исходная плоскость, синяя — после преобразования. Зелёная стрелка — куда перешёл вектор e_1 = (1, 0), оранжевая — куда перешёл e_2 = (0, 1).

Столбцы — образы базисных векторов

Любой вектор раскладывается по базису: (x, y) = x \cdot e_1 + y \cdot e_2. Преобразование линейное, поэтому

A\begin{pmatrix} x \\ y\end{pmatrix} = x \cdot A e_1 + y \cdot A e_2

Достаточно знать, куда переходят e_1 и e_2, — и мы знаем, куда переходит всё. А Ae_1 — это первый столбец A, Ae_2 — второй:

A = \begin{pmatrix} a & b \\ c & d \end{pmatrix}: \quad e_1 \mapsto \begin{pmatrix} a \\ c \end{pmatrix}, \quad e_2 \mapsto \begin{pmatrix} b \\ d \end{pmatrix}

Отсюда рецепт: чтобы построить матрицу преобразования, запишите образы базисных векторов в столбцы.

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, то есть линейное преобразование плюс сдвиг. Что если поставить два таких слоя подряд?

(xW_1 + b_1)W_2 + b_2 = x\,\underbrace{(W_1 W_2)}_{W} + \underbrace{(b_1 W_2 + b_2)}_{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

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Один вариант ответа

Матрица [[0, -1], [1, 0]] переводит вектор (1, 0) в (0, 1), а (0, 1) в (-1, 0). Что это за преобразование?

Задание 2 Ввод ответа

Преобразование растягивает плоскость по оси x в 3 раза и не меняет ось y. Запишите его матрицу в виде [[a, b], [c, d]].

Задание 3 Один вариант ответа

Сначала применили преобразование A, потом B. Какая матрица описывает итог (векторы — столбцы, умножаем слева)?

Задание 4 Один вариант ответа

Сеть из трёх линейных слоёв без функций активации: y = x @ W1 @ W2 @ W3. Что она умеет?

Задание 5 Программа на Python

Напишите rotation(deg) — матрицу поворота плоскости на deg градусов против часовой стрелки (NumPy-массив 2×2):

R(\varphi) = \begin{pmatrix} \cos\varphi & -\sin\varphi \\ \sin\varphi & \cos\varphi \end{pmatrix}

и 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), "Поворот не меняет длину"

Войдите, чтобы отмечать пройденные уроки.