Дополнительные упражнения: линейная алгебра в ML
Линейная алгебра в каждой строчке ML-кода
Этот урок — мост от математики к машинному обучению. Каждое задание — реальный приём, который вы встретите в нейросетях:
| Задание | Где встречается |
|---|---|
| one-hot | метки классов для функции потерь, представление слов |
| матрица близостей | поиск похожих, рекомендации, механизм внимания (QK^T) |
| ближайший сосед | простейший классификатор, поиск по эмбеддингам |
| взвешенное среднее | внимание (attention) в трансформерах |
| регрессия | линейные модели, последний слой сети |
| операции с картинками | аугментация данных при обучении |
One-hot: метки как векторы
Модель для 10 классов выдаёт 10 чисел. Чтобы сравнить их с правильным ответом «цифра 3», ответ тоже делают вектором: единица на месте 3, остальные нули. Это one-hot кодирование. Строки единичной матрицы — готовые one-hot векторы, а fancy indexing выбирает нужные:
import numpy as np
y = np.array([3, 0, 2])
print(np.eye(4))
print(np.eye(4)[y]) # строки 3, 0, 2 единичной матрицы
Ближайший сосед на цифрах
Самый простой классификатор: чтобы распознать новую картинку, найдём в обучающих данных самую похожую и скажем её метку. «Похожесть» — евклидово расстояние между 64-мерными векторами.
Считать расстояние от каждой тестовой картинки до каждой обучающей в двойном цикле медленно. Раскроем квадрат:
Средний член для всех пар — это одно матричное умножение X_{test} X_{train}^T, а квадраты длин добавляются через broadcasting. Так из «формулы со школьной доски» получается быстрый код.
import numpy as np
import ml_data
import vis
X, y = ml_data.load_digits()
X_train, y_train, X_test, y_test = X[:1500], y[:1500], X[1500:], y[1500:]
d2 = (X_test ** 2).sum(1)[:, None] - 2 * X_test @ X_train.T + (X_train ** 2).sum(1)[None, :]
print("матрица расстояний:", d2.shape)
nearest = d2.argmin(axis=1)
pred = y_train[nearest]
print(f"точность: {(pred == y_test).mean():.1%}")
wrong = np.where(pred != y_test)[0][:6]
vis.images(np.concatenate([X_test[wrong], X_train[nearest[wrong]]]).reshape(-1, 8, 8),
titles=[f"это {y_test[i]}" for i in wrong] + [f"сосед {pred[i]}" for i in wrong], cols=len(wrong))
Верхний ряд — ошибки классификатора, нижний — «соседи», с которыми он их спутал. Неплохо для модели без единого параметра! Но у ближайшего соседа есть недостатки: он хранит все данные и сравнивает с каждым объектом, а для сложных задач (фотографии) расстояние между пикселями плохо отражает смысл. Нейросети решают это, обучаясь находить хорошие признаки.
Взвешенное среднее: предвкушение attention
Механизм внимания отвечает на вопрос: «из каких векторов собрать ответ и с каким весом?». Сначала каждый вектор получает оценку важности, затем оценки превращаются в веса (положительные, сумма 1 — это функция softmax), и результат — взвешенная сумма векторов:
import numpy as np
import vis
V = np.array([[1.0, 0], [0, 1], [-1, 0]]) # три вектора «значений»
for scores in ([0, 0, 0], [3, 0, 0], [0, 2, 2]):
s = np.array(scores, dtype=float)
w = np.exp(s) / np.exp(s).sum()
print("оценки", scores, "→ веса", w.round(3), "→ результат", (w @ V).round(3))
vis.bar(["v₁", "v₂", "v₃"], np.exp([3.0, 0, 0]) / np.exp([3.0, 0, 0]).sum(), title="Веса при оценках (3, 0, 0)", fmt=".2f")
Через несколько модулей именно так будет устроен Transformer, только оценки будут вычисляться скалярными произведениями векторов слов.
Аугментация: из одной картинки — несколько
Чтобы нейросеть лучше обобщала, обучающие картинки слегка меняют: отражают, поворачивают, сдвигают. Всё это — операции над матрицами:
import numpy as np
import ml_data
import vis
X, y = ml_data.load_digits()
img = X[2].reshape(8, 8)
shifted = np.roll(img, 1, axis=1) # сдвиг на пиксель вправо
vis.images([img, img[:, ::-1], img.T[::-1], shifted, 16 - img],
titles=["оригинал", "отражение", "поворот 90°", "сдвиг", "негатив"])
Аугментация должна сохранять смысл
Отражённая «2» — уже не совсем «2», а перевёрнутая «6» превращается в «9». Для цифр годятся маленькие сдвиги и повороты, для фотографий кошек — отражение слева направо.
Практика решено 0 из 8
Батч из 32 картинок 8×8 распрямили в матрицу X. Какая у неё форма?
Слой превращает вектор из 64 признаков в 10 чисел (по одному на класс). Сколько у него параметров вместе со сдвигами?
One-hot кодирование. Напишите one_hot(y, num_classes): вектор меток y (целые от 0 до
num_classes − 1) → матрица (len(y), num_classes) из нулей и единиц, где в строке i
единица стоит в столбце y[i]. Без циклов (подсказка: строки единичной матрицы).
Какие тесты проверяют код (3)
assert np.array_equal(one_hot(np.array([2, 0, 1]), 3), [[0, 0, 1], [1, 0, 0], [0, 1, 0]]) assert one_hot(np.array([9, 9]), 10).shape == (2, 10) and one_hot(np.array([9, 9]), 10)[:, 9].sum() == 2 import inspect src = inspect.getsource(one_hot) assert "for " not in src and "while " not in src, "Без циклов"
import numpy as np
def one_hot(y, num_classes):
return np.eye(num_classes)[y]
Матрица косинусных близостей. Строки E формы (n, d) — векторы объектов. Напишите
cosine_matrix(E), возвращающую (n, n), где [i, j] — косинус угла между E[i] и E[j].
Подсказка: нормализуйте строки и умножьте матрицу на себя транспонированную.
Какие тесты проверяют код (1)
E = np.array([[1.0, 0], [0, 2], [3, 3]]) S = cosine_matrix(E) assert np.shape(S) == (3, 3) assert np.allclose(np.diag(S), 1), "Каждый вектор сонаправлен сам с собой: на диагонали единицы" assert np.allclose(S[0, 1], 0) and np.allclose(S[0, 2], 1 / np.sqrt(2))
import numpy as np
def cosine_matrix(E):
U = E / np.linalg.norm(E, axis=1, keepdims=True)
return U @ U.T
Классификатор ближайшего соседа. Напишите knn_predict(X_train, y_train, X_test):
для каждой строки X_test найдите ближайшую (по евклидову расстоянию) строку X_train и
верните её метку. Результат — массив длины len(X_test). Без циклов по объектам.
Подсказка: \|a - b\|^2 = \|a\|^2 - 2\,a\cdot b + \|b\|^2 — все попарные квадраты расстояний считаются одним матричным умножением плюс broadcasting.
Какие тесты проверяют код (3)
Xtr = np.array([[0.0, 0], [10, 10], [0, 10]])
ytr = np.array([0, 1, 2])
assert np.array_equal(knn_predict(Xtr, ytr, np.array([[1.0, 1], [9, 8], [1, 9], [6, 9]])), [0, 1, 2, 1])
import ml_data
X, y = ml_data.load_digits()
pred = knn_predict(X[:1500], y[:1500], X[1500:])
acc = (pred == y[1500:]).mean()
assert acc > 0.9, f"На цифрах ближайший сосед должен угадывать > 90%, получилось {acc:.1%}"
import inspect
src = inspect.getsource(knn_predict)
assert "for " not in src and "while " not in src, "Без циклов"
import numpy as np
def knn_predict(X_train, y_train, X_test):
d2 = (X_test ** 2).sum(1)[:, None] - 2 * X_test @ X_train.T + (X_train ** 2).sum(1)[None, :]
return y_train[d2.argmin(axis=1)]
Взвешенное среднее — сердце механизма внимания. Даны «оценки» scores формы (n,) и
векторы V формы (n, d). Напишите attend(scores, V):
- превратите оценки в веса:
w = exp(scores) / sum(exp(scores))(веса положительные, сумма 1); - верните взвешенную сумму строк
Vс этими весами — вектор(d,).
Одной-двумя строками, без циклов.
Какие тесты проверяют код (2)
V = np.array([[1.0, 0], [0, 1]]) assert np.allclose(attend(np.array([0.0, 0]), V), [0.5, 0.5]), "При равных оценках — обычное среднее" out = attend(np.array([10.0, 0]), V) assert out[0] > 0.99, "Большая оценка — почти весь вес у первого вектора" rng = np.random.default_rng(0) s, V = rng.normal(size=5), rng.normal(size=(5, 3)) w = np.exp(s) / np.exp(s).sum() assert np.allclose(attend(s, V), (w[:, None] * V).sum(0))
import numpy as np
def attend(scores, V):
w = np.exp(scores) / np.exp(scores).sum()
return w @ V
Регрессия с несколькими признаками. X — матрица (N, d) признаков квартир (площадь,
этаж, расстояние до метро…), y — цены. Напишите fit_linear(X, y), которая добавляет к X
столбец единиц и решает нормальное уравнение (X^TX)\theta = X^Ty. Верните theta длины d + 1:
сначала d весов, последним — сдвиг. И predict(X, theta).
Какие тесты проверяют код (1)
rng = np.random.default_rng(0)
X = rng.uniform(0, 10, size=(100, 3))
true = np.array([2.0, -1.0, 0.5])
y = X @ true + 4 + rng.normal(0, 0.01, 100)
theta = fit_linear(X, y)
assert np.shape(theta) == (4,), "theta: d весов и сдвиг"
assert np.allclose(theta, [2, -1, 0.5, 4], atol=0.02), f"Получено {np.round(theta, 3)}"
assert np.allclose(predict(X, theta), y, atol=0.1)
import numpy as np
def fit_linear(X, y):
Xb = np.c_[X, np.ones(len(X))]
return np.linalg.solve(Xb.T @ Xb, Xb.T @ y)
def predict(X, theta):
return X @ theta[:-1] + theta[-1]
Картинка — это матрица. Напишите для изображения img (2D-массив) функции без циклов:
flip_h(img)— зеркальное отражение слева направо;rot90(img)— поворот на 90° против часовой стрелки (не используйтеnp.rot90; подсказка: транспонирование + отражение);normalize(img)— линейно растянуть значения в диапазон [0, 1]: минимум → 0, максимум → 1.
Какие тесты проверяют код (4)
a = np.array([[1, 2, 3], [4, 5, 6]])
assert np.array_equal(flip_h(a), [[3, 2, 1], [6, 5, 4]])
a = np.arange(6).reshape(2, 3)
assert np.array_equal(rot90(a), np.rot90(a)), f"Ожидалось {np.rot90(a).tolist()}, получено {np.asarray(rot90(a)).tolist()}"
import inspect
assert "np.rot90" not in inspect.getsource(rot90), "Не используйте np.rot90"
z = normalize(np.array([[2.0, 4], [6, 10]]))
assert np.allclose(z, [[0, 0.25], [0.5, 1]])
import numpy as np
def flip_h(img):
return img[:, ::-1]
def rot90(img):
return img.T[::-1]
def normalize(img):
return (img - img.min()) / (img.max() - img.min())
Войдите, чтобы отмечать пройденные уроки.