Фаза 01 · урок 12

Операции с тензорами

Цель урока: Вы строите трансформер. Прямой проход выглядит аккуратно. Вы запускаете его и получаете: RuntimeError: mat1 and mat2 shapes cannot be multiplied (32x768 and 512x768) . Вы смотрите на формы. Пробуете транспонирование. Теперь сообщение:…

Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.

Курс
AI Engineering from Scratch
Фаза
Математические основы
Чтение
13 мин.
Проверено
Содержание урока
  1. Цели обучения
  2. Проблема
  3. Концепция
  4. Что такое тензор
  5. Формы тензоров в глубоком обучении
  6. Как устроена раскладка в памяти
  7. Правила broadcasting
  8. Einsum: универсальная тензорная операция
  9. Соберите это
  10. Шаг 1: хранение тензора и шаги
  11. Шаг 2: reshape, squeeze, unsqueeze
  12. Шаг 3: транспонирование и permute
  13. Шаг 4: поэлементные операции и редукции
  14. Шаг 5: broadcasting с NumPy
  15. Шаг 6: операции einsum
  16. Шаг 7: механизм внимания через einsum
  17. Используйте это
  18. Scratch и NumPy
  19. Scratch и PyTorch
  20. Каждый слой нейронной сети как тензорная операция
  21. Подготовьте к выпуску
  22. Упражнения
  23. Ключевые термины
  24. Дополнительное чтение

Тензоры — общий язык данных и глубокого обучения. Через них проходит каждое изображение, каждое предложение и каждый градиент.

Тип: Практика Язык: Python Предварительные требования: фаза 1, уроки 01 (Интуиция линейной алгебры), 02 (Векторы, матрицы и операции) Время: ~90 минут

Цели обучения

  • Реализовать с нуля класс тензора с формой, шагами, изменением формы, транспонированием и поэлементными операциями
  • Применять правила broadcasting для работы с тензорами разных форм без копирования данных
  • Писать выражения einsum для скалярных произведений, умножений матриц, внешних произведений и пакетных операций
  • Прослеживать точные формы тензоров на каждом шаге многоголового внимания

Проблема

Вы строите трансформер. Прямой проход выглядит аккуратно. Вы запускаете его и получаете: RuntimeError: mat1 and mat2 shapes cannot be multiplied (32x768 and 512x768). Вы смотрите на формы. Пробуете транспонирование. Теперь сообщение: Expected 4D input (got 3D input). Вы добавляете unsqueeze. Ломается что-то ещё.

Ошибки формы — самая распространённая ошибка в коде глубокого обучения. Концептуально они несложны: у каждой операции есть контракт формы, — но быстро множатся. В трансформере связаны десятки reshape, transpose и broadcast-операций. Одна неверная ось — и ошибка каскадируется. Хуже того, некоторые ошибки формы вовсе не вызывают исключений. Они тихо выдают мусор, выполняя broadcasting по неверному измерению или суммируя по неверной оси.

Матрицы описывают попарные отношения между двумя наборами объектов. Настоящие данные не помещаются в два измерения. Пакет из 32 RGB-изображений размером 224x224 — это четырёхмерный тензор: (32, 3, 224, 224). Self-attention с 12 головами также четырёхмерен: (batch, heads, seq_len, head_dim). Нужна структура данных, обобщающаяся на любое число измерений, с операциями, которые чисто композиционируются между всеми ними. Эта структура — тензор. Освойте его операции, и ошибки форм станут тривиально отлаживаемыми.

Концепция

Что такое тензор

Тензор — это многомерный массив чисел с единым типом данных. Число измерений — его ранг (или порядок). Каждое измерение является осью. Форма — это кортеж, перечисляющий размер вдоль каждой оси.

Диаграмма к уроку «Операции с тензорами»

Общее число элементов равно произведению всех размеров. Форма (2, 3, 4) содержит 2 * 3 * 4 = 24 элемента.

Формы тензоров в глубоком обучении

Разные типы данных по соглашению соответствуют определённым формам тензоров.

Диаграмма к уроку «Операции с тензорами»

PyTorch использует NCHW (каналы первыми). TensorFlow по умолчанию использует NHWC (каналы последними). Несовпадение раскладок вызывает незаметные замедления или ошибки.

Как устроена раскладка в памяти

Двумерный массив в памяти — это одномерная последовательность байтов. Шаги (strides) указывают, сколько элементов нужно пропустить, чтобы переместиться на один шаг вдоль каждой оси.

Диаграмма к уроку «Операции с тензорами»

Транспонирование не перемещает данные. Оно меняет шаги местами, делая тензор неконтинуальным (non-contiguous): элементы строки больше не соседствуют в памяти.

Правила broadcasting

Broadcasting позволяет выполнять операции над тензорами разных форм без копирования данных. Выравнивайте формы справа. Два измерения совместимы, когда они равны или одно из них равно 1. Недостающие измерения дополняются единицами слева.

Tensor A:     (8, 1, 6, 1)
Tensor B:        (7, 1, 5)
Padded B:     (1, 7, 1, 5)
Result:       (8, 7, 6, 5)

Einsum: универсальная тензорная операция

Суммирование Эйнштейна помечает каждую ось буквой. Оси, присутствующие во входных данных, но отсутствующие в результате, суммируются. Оси, присутствующие в обоих местах, сохраняются.

Диаграмма к уроку «Операции с тензорами»

Основные шаблоны: i,i-> (скалярное произведение), i,j->ij (внешнее произведение), ii-> (след), ij->ji (транспонирование), bij,bjk->bik (пакетное умножение матриц), bhtd,bhsd->bhts (оценки внимания).

tensor-broadcast

Соберите это

Код находится в code/tensors.py. Каждый шаг ссылается на реализацию в этом файле.

Шаг 1: хранение тензора и шаги

Тензор хранит плоский список чисел вместе с метаданными формы. Шаги сообщают логике индексации, как сопоставлять многомерные индексы плоским позициям.

class Tensor:
    def __init__(self, data, shape=None):
        if isinstance(data, (list, tuple)):
            self._data, self._shape = self._flatten_nested(data)
        elif isinstance(data, np.ndarray):
            self._data = data.flatten().tolist()
            self._shape = tuple(data.shape)
        else:
            self._data = [data]
            self._shape = ()

        if shape is not None:
            total = reduce(lambda a, b: a * b, shape, 1)
            if total != len(self._data):
                raise ValueError(
                    f"Cannot reshape {len(self._data)} elements into shape {shape}"
                )
            self._shape = tuple(shape)

        self._strides = self._compute_strides(self._shape)

    @staticmethod
    def _compute_strides(shape):
        if len(shape) == 0:
            return ()
        strides = [1] * len(shape)
        for i in range(len(shape) - 2, -1, -1):
            strides[i] = strides[i + 1] * shape[i + 1]
        return tuple(strides)

Для формы (3, 4) шаги равны (4, 1): чтобы перейти на одну строку, пропустите 4 элемента; чтобы перейти на один столбец, пропустите 1 элемент.

Шаг 2: reshape, squeeze, unsqueeze

Reshape меняет форму, не меняя порядок элементов. Общее число элементов должно оставаться тем же. Используйте -1 для одного измерения, чтобы вывести его размер автоматически.

t = Tensor(list(range(12)), shape=(2, 6))
r = t.reshape((3, 4))
r = t.reshape((-1, 3))

Squeeze удаляет оси размера 1. Unsqueeze вставляет одну. Unsqueeze критичен для broadcasting: вектор смещений (D,), добавляемый к пакету (B, T, D), нужно развернуть до (1, 1, D).

t = Tensor(list(range(6)), shape=(1, 3, 1, 2))
s = t.squeeze()
v = Tensor([1, 2, 3])
u = v.unsqueeze(0)

Шаг 3: транспонирование и permute

Transpose меняет местами две оси. Permute переупорядочивает все оси. Так вы преобразуете NCHW в NHWC и обратно.

mat = Tensor(list(range(6)), shape=(2, 3))
tr = mat.transpose(0, 1)

t4d = Tensor(list(range(24)), shape=(1, 2, 3, 4))
perm = t4d.permute((0, 2, 3, 1))

После transpose или permute тензор становится неконтинуальным в памяти. В PyTorch view не работает с неконтинуальными тензорами: используйте reshape либо сначала вызовите .contiguous().

Шаг 4: поэлементные операции и редукции

Поэлементные операции (сложение, умножение, вычитание) применяются независимо к каждому элементу и сохраняют форму. Редукции (sum, mean, max) сворачивают одну или несколько осей.

a = Tensor([[1, 2], [3, 4]])
b = Tensor([[10, 20], [30, 40]])
c = a + b
d = a * 2
s = a.sum(axis=0)

Глобальный average pooling в CNN: (B, C, H, W).mean(axis=[2, 3]) даёт (B, C). Усредняющий pooling по последовательности в NLP: (B, T, D).mean(axis=1) даёт (B, D).

Шаг 5: broadcasting с NumPy

Функция demo_broadcasting_numpy() в tensors.py показывает основные шаблоны.

activations = np.random.randn(4, 3)
bias = np.array([0.1, 0.2, 0.3])
result = activations + bias

images = np.random.randn(2, 3, 4, 4)
scale = np.array([0.5, 1.0, 1.5]).reshape(1, 3, 1, 1)
result = images * scale

a = np.array([1, 2, 3]).reshape(-1, 1)
b = np.array([10, 20, 30, 40]).reshape(1, -1)
outer = a * b

Попарное расстояние через broadcasting: измените форму (M, 2) на (M, 1, 2), а (N, 2) на (1, N, 2), вычтите, возведите в квадрат, просуммируйте по последней оси и извлеките квадратный корень. Результат: (M, N).

Шаг 6: операции einsum

Функции demo_einsum() и demo_einsum_gallery() разбирают каждый распространённый шаблон.

a = np.array([1.0, 2.0, 3.0])
b = np.array([4.0, 5.0, 6.0])
dot = np.einsum("i,i->", a, b)

A = np.array([[1, 2], [3, 4], [5, 6]], dtype=float)
B = np.array([[7, 8, 9], [10, 11, 12]], dtype=float)
matmul = np.einsum("ik,kj->ij", A, B)

batch_A = np.random.randn(4, 3, 5)
batch_B = np.random.randn(4, 5, 2)
batch_mm = np.einsum("bij,bjk->bik", batch_A, batch_B)

Вычислительная стоимость свёртки равна произведению размеров всех индексов (сохраняемых и суммируемых). Для bij,bjk->bik при B=32, I=128, J=64, K=128: 32 * 128 * 64 * 128 = 33,554,432 операций умножения-сложения.

Шаг 7: механизм внимания через einsum

Функция demo_attention_einsum() реализует многоголовое внимание от начала до конца.

B, H, T, D = 2, 4, 8, 16
E = H * D

X = np.random.randn(B, T, E)
W_q = np.random.randn(E, E) * 0.02

Q = np.einsum("bte,ek->btk", X, W_q)
Q = Q.reshape(B, T, H, D).transpose(0, 2, 1, 3)

scores = np.einsum("bhtd,bhsd->bhts", Q, K) / np.sqrt(D)
weights = softmax(scores, axis=-1)
attn_output = np.einsum("bhts,bhsd->bhtd", weights, V)

concat = attn_output.transpose(0, 2, 1, 3).reshape(B, T, E)
output = np.einsum("bte,ek->btk", concat, W_o)

Каждый шаг — тензорная операция: проекция (matmul через einsum), разбиение на головы (reshape + transpose), оценки внимания (пакетный matmul через einsum), взвешенная сумма (пакетный matmul через einsum), объединение голов (transpose + reshape), выходная проекция (matmul через einsum).

Используйте это

Scratch и NumPy

Операция Scratch (класс Tensor) NumPy
Создание Tensor([[1,2],[3,4]]) np.array([[1,2],[3,4]])
Изменение формы t.reshape((3,4)) a.reshape(3,4)
Транспонирование t.transpose(0,1) a.T or a.transpose(0,1)
Squeeze t.squeeze(0) np.squeeze(a, 0)
Сумма t.sum(axis=0) a.sum(axis=0)
Einsum N/A np.einsum("ij,jk->ik", a, b)

Scratch и PyTorch

import torch

t = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float32)
t.shape
t.stride()
t.is_contiguous()

t.reshape(3, 2)
t.unsqueeze(0)
t.transpose(0, 1)
t.transpose(0, 1).contiguous()

torch.einsum("ik,kj->ij", A, B)

PyTorch добавляет autograd, поддержку GPU и оптимизированные ядра BLAS. Семантика форм идентична. Если вы понимаете версию с нуля, сообщения PyTorch об ошибках формы становятся понятными.

Каждый слой нейронной сети как тензорная операция

Операция Тензорная форма Einsum
Линейный слой Y = X @ W.T + b "bd,od->bo" + bias
Attention QKV Q = X @ W_q "btd,dh->bth"
Оценки внимания Q @ K.T / sqrt(d) "bhtd,bhsd->bhts"
Выход внимания softmax(scores) @ V "bhts,bhsd->bhtd"
Batch norm (X - mu) / sigma * gamma поэлементно + broadcast
Softmax exp(x) / sum(exp(x)) поэлементно + редукция

Подготовьте к выпуску

Этот урок создаёт два повторно используемых промпта:

  1. outputs/prompt-tensor-shapes.md — систематический промпт для отладки несовпадений форм тензоров. Включает таблицы решений для каждой распространённой операции (matmul, broadcast, cat, Linear, Conv2d, BatchNorm, softmax) и таблицу поиска исправлений.

  2. outputs/prompt-tensor-debugger.md — пошаговый отладочный промпт, который можно вставить в любого AI-ассистента, когда ошибка формы блокирует работу. Передайте ему текст ошибки и формы тензоров, чтобы получить точное исправление.

Упражнения

  1. Легко — круговой путь изменения формы. Возьмите тензор формы (2, 3, 4). Измените его форму на (6, 4), затем на (24,), а затем снова на (2, 3, 4). На каждом шаге убедитесь, что порядок элементов сохранён, напечатав плоские данные.

  2. Средне — реализуйте broadcasting. Дополните класс Tensor методом broadcast_to(shape), который расширяет измерения размера 1 до целевой формы. Затем измените _elementwise_op, чтобы он автоматически применял broadcasting перед операцией. Проверьте на формах (3, 1) и (1, 4), получающих (3, 4).

  3. Сложно — постройте einsum с нуля. Реализуйте базовую функцию einsum(subscripts, *tensors), обрабатывающую как минимум: скалярное произведение (i,i->), умножение матриц (ij,jk->ik), внешнее произведение (i,j->ij) и транспонирование (ij->ji). Разберите строку индексов, определите сокращаемые индексы и переберите все сочетания индексов. Сравните результаты с np.einsum.

  4. Сложно — трекер форм внимания. Напишите функцию, принимающую batch_size, seq_len, embed_dim и num_heads и выводящую точную форму на каждом шаге многоголового внимания: вход, проекция Q/K/V, разбиение на головы, оценки внимания, веса softmax, взвешенная сумма, объединение голов, выходная проекция. Проверьте результат относительно вывода demo_attention_einsum().

Ключевые термины

Термин Как обычно говорят Что это на самом деле означает
Тензор «Матрица, но с большим числом измерений» Многомерный массив с единым типом и определёнными формой, шагами и операциями
Ранг «Число измерений» Число осей. Ранг матрицы равен 2, а не её матричному рангу
Форма «Размер тензора» Кортеж, перечисляющий размер вдоль каждой оси. (2, 3) означает 2 строки, 3 столбца
Шаг «Как размещена память» Число элементов, которое нужно пропустить, чтобы сдвинуться на одну позицию вдоль каждой оси
Broadcasting «Оно просто работает, когда формы различаются» Строгий набор правил: выравнивать справа; измерения должны быть равны либо одно из них должно быть 1
Континуальный «Тензор обычный» Элементы последовательно хранятся в памяти без зазоров или переупорядочивания относительно логической раскладки
Einsum «Навороченный способ написать matmul» Общая нотация, выражающая любое тензорное сокращение, внешнее произведение, след или транспонирование одной строкой
View «То же, что reshape» Тензор, использующий тот же буфер памяти, но с другими метаданными формы/шагов. Не работает на неконтинуальных данных
Свёртка «Суммирование по индексу» Общая операция, при которой общий индекс между тензорами перемножается и суммируется, создавая результат меньшего ранга
NCHW / NHWC «Формат PyTorch или TensorFlow» Соглашения о раскладке в памяти для тензоров изображений. В NCHW каналы идут до пространственных измерений, в NHWC — после них

Дополнительное чтение

  • NumPy Broadcasting — канонические правила с визуальными примерами
  • PyTorch Tensor Views — когда представления работают, а когда копируют данные
  • einops — библиотека, делающая изменение формы тензоров читаемым и безопасным
  • The Illustrated Transformer — визуализирует формы тензоров, проходящие через внимание
  • Einstein Summation in NumPy — полная документация einsum с примерами

Источник: Tensor Operations — оригинал Навигация: назад: 01.11 — Сингулярное разложение · Фаза 1 — Математические основы · Полный каталог · далее: 01.13 — Численная устойчивость.