Визуальное руководство по тензорам PyTorch: От нуля до нейронных сетей за 30 минут
Автор: Евгений Падежнов
PyTorch работает везде — от автопилота Tesla до ChatGPT. Но все застревают на тензорах сначала. Я потратил две недели на отладку ошибок с размерностями, когда начинал. Вот что я хотел бы, чтобы мне показали в первый день.
Это руководство показывает операции с тензорами визуально. Вы поймёте размерности, операции и ускорение на GPU. Включены реальные примеры кода из продакшн-проектов.
Что такое тензоры на самом деле
Тензоры — это просто многомерные массивы. Представьте Excel-таблицу, которая может иметь больше 2 измерений. Документация PyTorch называет их «центральной абстракцией данных в PyTorch».
Вот иерархия:
- 0D тензор = скаляр (одно число)
- 1D тензор = вектор (список чисел)
- 2D тензор = матрица (таблица чисел)
- 3D+ тензор = тензор (куб или гиперкуб чисел)
import torch
# 0D - скаляр
scalar = torch.tensor(42)
# 1D - вектор (5 элементов)
vector = torch.tensor([1, 2, 3, 4, 5])
# 2D - матрица (3x3)
matrix = torch.tensor([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# 3D - тензор (2x3x4)
tensor_3d = torch.randn(2, 3, 4)
Я использую такую ментальную модель: таблица со слоями. Первое измерение = количество листов. Второе = строки. Третье = столбцы.
Тензоры PyTorch превосходят массивы NumPy по трём причинам. Руководство GitHub Tensors-101 точно описывает:
- GPU-ускорение — операции в миллионы раз быстрее
- Автоматическое дифференцирование — градиенты вычисляются автоматически
- Экосистема глубокого обучения — встроенные функции потерь, оптимизаторы, слои
На практике я перешёл с NumPy на PyTorch даже для задач без нейронок. GPU-ускорение матричных операций просто безумное.
Тип данных по умолчанию — 32-битный float (torch.float32). Занимает 4 байта на число. Для меток классов используйте torch.int64 (8 байт). Для обучения со смешанной точностью — torch.float16 (2 байта).
# По умолчанию float32
x = torch.tensor([1.0, 2.0, 3.0])
print(x.dtype) # torch.float32
# Явные типы
integers = torch.tensor([1, 2, 3], dtype=torch.int64)
half_precision = torch.tensor([1.0, 2.0], dtype=torch.float16)
Создание тензоров: Четыре метода, которые важны
PyTorch Essentials показывает множество подходов к инициализации. Вот те, которые я реально использую ежедневно.
Метод 1: Из списков Python
# Простой список в тензор
data = [[1, 2, 3], [4, 5, 6]]
tensor = torch.tensor(data)
# Из NumPy (супер часто)
import numpy as np
numpy_array = np.array([[1, 2], [3, 4]])
tensor_from_numpy = torch.from_numpy(numpy_array)
Метод 2: Предзаполненные тензоры
# Нули — для инициализации
zeros = torch.zeros(3, 4) # Матрица 3x4 из нулей
# Единицы — для масок
ones = torch.ones(2, 3, 4) # Тензор 2x3x4 из единиц
# Случайные — для инициализации весов
random = torch.rand(5, 5) # значения между 0 и 1
Метод 3: Как существующий тензор
x = torch.tensor([[1, 2], [3, 4]])
# Та же форма, другие значения
zeros_like_x = torch.zeros_like(x)
ones_like_x = torch.ones_like(x)
random_like_x = torch.rand_like(x)
Метод 4: Специальные инициализации
# Единичная матрица
eye = torch.eye(3) # 3x3 единичная
# Диапазон
range_tensor = torch.arange(0, 10, 2) # [0, 2, 4, 6, 8]
# Linspace
linear = torch.linspace(0, 1, 5) # 5 точек от 0 до 1
Короче, torch.empty() создаёт неинициализированные тензоры. Выглядит случайно, но это просто мусор из памяти. Никогда не используйте для реальных вычислений — кошмар при отладке.
# НЕ ДЕЛАЙТЕ ТАК
bad = torch.empty(2, 3) # Содержит случайный мусор памяти
# ДЕЛАЙТЕ ТАК
good = torch.zeros(2, 3) # Правильно инициализировано
Совет из опыта отладки: всегда выводите формы тензоров после создания. Экономит часы охоты за «RuntimeError: shape mismatch».
Формы тензоров: Источник ошибок №1
«Форма — это самое важное свойство тензора. Почти каждый баг в глубоком обучении, по сути, является несовпадением форм» — прямо из репозитория Tensors-101. Стопроцентная правда.
Стандартные соглашения по типам данных:
- Табличные данные:
(batch, features) - Изображения:
(batch, channels, height, width) - Текст/NLP:
(batch, sequence_length, embedding_dim)
# Проверка формы
tensor = torch.randn(64, 3, 224, 224) # Батч из 64 RGB изображений
print(tensor.shape) # torch.Size([64, 3, 224, 224])
print(tensor.size()) # То же самое
# Отдельные измерения
print(tensor.shape[0]) # 64 (размер батча)
print(tensor.shape[1]) # 3 (каналы)
Операции изменения формы, которые я использую в каждом проекте:
# Исходный тензор
x = torch.randn(4, 3, 2)
# Reshape (общее количество элементов должно совпадать)
reshaped = x.reshape(6, 4) # 4*3*2 = 24 = 6*4
# View (то же, что reshape, но использует общую память)
viewed = x.view(12, 2)
# Flatten для полносвязных слоёв
flattened = x.flatten() # Форма: [24]
batch_flattened = x.flatten(start_dim=1) # Сохраняем батч: [4, 6]
# Squeeze удаляет измерения размера 1
y = torch.randn(1, 3, 1, 5)
squeezed = y.squeeze() # Форма: [3, 5]
# Unsqueeze добавляет измерение размера 1
z = torch.randn(3, 5)
unsqueezed = z.unsqueeze(0) # Форма: [1, 3, 5]
unsqueezed = z.unsqueeze(-1) # Форма: [3, 5, 1]
Общий паттерн отладки форм:
def debug_shapes(name, tensor):
print(f"{name}: shape={tensor.shape}, dtype={tensor.dtype}, device={tensor.device}")
# Используйте везде во время разработки
x = torch.randn(32, 10)
debug_shapes("input", x)
model_output = model(x)
debug_shapes("output", model_output)
Несовпадения форм чаще всего происходят здесь:
- Несовпадение размера батча между слоями
- Забыли сделать flatten перед полносвязным слоем
- Неправильный порядок каналов (channels-first vs channels-last)
- Путаница с правилами broadcasting
Мой чек-лист отладки:
- Выводите формы до и после каждой операции
- Используйте
assertдля ожидаемых форм в критических местах - Держите батч-измерение консистентным (всегда первым)
- Документируйте ожидаемые формы в комментариях
Операции на GPU: Скорость, которая меняет всё
Руководство Codegenes упоминает гибкость устройств. Вот что реально важно для скорости.
Сначала проверьте доступность GPU:
# Доступна ли CUDA?
print(torch.cuda.is_available()) # True/False
# Сколько GPU?
print(torch.cuda.device_count())
# Имя текущей GPU
if torch.cuda.is_available():
print(torch.cuda.get_device_name(0))
Перемещение тензоров между устройствами:
# Создаём на CPU
cpu_tensor = torch.randn(1000, 1000)
# Перемещаем на GPU (если доступна)
if torch.cuda.is_available():
gpu_tensor = cpu_tensor.cuda()
# Или более явно
gpu_tensor = cpu_tensor.to('cuda')
# Или на конкретную GPU
gpu_tensor = cpu_tensor.to('cuda:0')
# Обратно на CPU
back_to_cpu = gpu_tensor.cpu()
Лучший паттерн — код, независимый от устройства:
# Устанавливаем устройство один раз
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Используем устройство: {device}")
# Создаём сразу на нужном устройстве
x = torch.randn(100, 100, device=device)
y = torch.randn(100, 100, device=device)
# Операции остаются на том же устройстве
z = x @ y # Умножение матриц на GPU
Золотое правило из опыта: оба тензора должны быть на одном устройстве. Это падает:
# НЕ ДЕЛАЙТЕ ТАК
cpu_tensor = torch.randn(10, 10)
gpu_tensor = torch.randn(10, 10).cuda()
# result = cpu_tensor + gpu_tensor # RuntimeError!
# ДЕЛАЙТЕ ТАК
result = cpu_tensor.cuda() + gpu_tensor
Сравнение скорости на моей RTX 3090:
import time
size = 5000
cpu_a = torch.randn(size, size)
cpu_b = torch.randn(size, size)
# Замер CPU
start = time.time()
cpu_result = cpu_a @ cpu_b
cpu_time = time.time() - start
# Замер GPU
gpu_a = cpu_a.cuda()
gpu_b = cpu_b.cuda()
torch.cuda.synchronize() # Ждём завершения передачи
start = time.time()
gpu_result = gpu_a @ gpu_b
torch.cuda.synchronize() # Ждём завершения вычисления
gpu_time = time.time() - start
print(f"CPU: {cpu_time:.4f}s")
print(f"GPU: {gpu_time:.4f}s")
print(f"Ускорение: {cpu_time/gpu_time:.1f}x")
Типичные результаты: ускорение в 50-200 раз для больших матричных операций. Ну чёт такое.
Советы по управлению памятью:
# Проверка использования памяти GPU
print(f"Выделено: {torch.cuda.memory_allocated()/1024**3:.2f} GB")
print(f"Кэшировано: {torch.cuda.memory_reserved()/1024**3:.2f} GB")
# Очистка кэша при необходимости
torch.cuda.empty_cache()
# Явное удаление тензоров
del large_tensor
torch.cuda.empty_cache()
Базовые операции, которые вы реально будете использовать
Документация PyTorch покрывает много операций. Вот те, которые я использую в каждом проекте.
Арифметические операции:
a = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32)
b = torch.tensor([[5, 6], [7, 8]], dtype=torch.float32)
# Поэлементные операции
add = a + b # или torch.add(a, b)
subtract = a - b
multiply = a * b # поэлементно, НЕ матричное умножение
divide = a / b
# Операции in-place (экономят память)
a.add_(1) # добавляет 1 ко всем элементам, изменяет a
a.mul_(2) # умножает все на 2
Матричные операции:
# Матричное умножение — три способа
result1 = a @ b # рекомендуется
result2 = torch.matmul(a, b)
result3 = a.mm(b) # только для 2D тензоров
# Батчевое матричное умножение
batch_a = torch.randn(32, 10, 20)
batch_b = torch.randn(32, 20, 30)
batch_result = batch_a @ batch_b # Форма: [32, 10, 30]
# Транспонирование
transposed = a.T # или a.transpose(0, 1)
Операции сокращения:
x = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float32)
# Сумма
total_sum = x.sum() # 21
row_sum = x.sum(dim=1) # [6, 15]
col_sum = x.sum(dim=0) # [5, 7, 9]
# Среднее
mean_all = x.mean() # 3.5
mean_rows = x.mean(dim=1) # [2, 5]
# Max/Min
max_value = x.max() # 6
max_per_row = x.max(dim=1) # возвращает (значения, индексы)
values, indices = x.max(dim=1)
Индексация и срезы:
tensor = torch.randn(3, 4, 5)
# Базовая индексация
first_element = tensor[0] # Форма: [4, 5]
specific = tensor[1, 2, 3] # Одно значение
# Срезы
first_two = tensor[:2] # Первые 2 по dim 0
middle = tensor[:, 1:3, :] # Срез dim 1
# Продвинутая индексация
mask = tensor > 0
positive_values = tensor[mask] # 1D тензор положительных значений
# Выбор конкретных индексов
indices = torch.tensor([0, 2])
selected = torch.index_select(tensor, dim=0, index=indices)
Общие паттерны из продакшна:
# Ограничение значений диапазоном
clamped = torch.clamp(tensor, min=0, max=1)
# Замена NaN значений
tensor[torch.isnan(tensor)] = 0
# Конкатенация тензоров
concat_dim0 = torch.cat([tensor1, tensor2], dim=0)
stack = torch.stack([tensor1, tensor2]) # Новое измерение
# Разделение тензора
chunks = torch.chunk(tensor, chunks=3, dim=0)
split = torch.split(tensor, split_size_or_sections=2, dim=1)
Частые ловушки и как их избежать
После отладки сотен ошибок с тензорами, вот паттерны.
Ловушка 1: Отслеживание градиентов когда не нужно
# Неправильно — отслеживает градиенты без необходимости
data = torch.randn(100, 100, requires_grad=True)
processed = data * 2 + 1 # Всё ещё отслеживает градиенты
# Правильно — без отслеживания градиентов для препроцессинга данных
data = torch.randn(100, 100)
# Или явно отключить
with torch.no_grad():
processed = data * 2 + 1
Ловушка 2: In-place операции ломают градиенты
# Неправильно — ломает вычисление градиентов
x = torch.randn(5, requires_grad=True)
x += 1 # In-place операция
# Правильно
x = torch.randn(5, requires_grad=True)
x = x + 1 # Новый тензор
Ловушка 3: Неправильные предположения о broadcasting
# Неожиданный broadcasting
a = torch.randn(3, 1) # Форма: [3, 1]
b = torch.randn(1, 4) # Форма: [1, 4]
c = a + b # Форма: [3, 4] — может быть не то, что хотели!
# Будьте явны с формами
a = torch.randn(3, 4)
b = torch.randn(3, 4)
c = a + b # Ясное намерение
Ловушка 4: Утечки памяти с GPU тензорами
# Неправильно — накапливается GPU память
losses = []
for batch in dataloader:
loss = model(batch)
losses.append(loss) # Сохраняет весь граф вычислений!
# Правильно
losses = []
for batch in dataloader:
loss = model(batch)
losses.append(loss.item()) # Только число
Ловушка 5: Предположение о непрерывной памяти
# После транспонирования тензор может быть не непрерывным
x = torch.randn(3, 4)
x_t = x.transpose(0, 1)
print(x_t.is_contiguous()) # False
# Некоторые операции требуют непрерывности
x_t_contiguous = x_t.contiguous()
# Или просто используйте reshape, который это обрабатывает
x_reshaped = x.reshape(4, 3)
Примеры из реальных проектов
Покажу операции с тензорами из настоящего продакшн-кода.
Пайплайн препроцессинга изображений:
def preprocess_batch(images, device='cuda'):
"""Изображения из dataloader в готовые для модели тензоры"""
# images форма: (batch, height, width, channels) - numpy
# Конвертируем в тензор и нормализуем к [0,1]
tensor_images = torch.from_numpy(images).float() / 255.0
# Переставляем в формат PyTorch: (batch, channels, height, width)
tensor_images = tensor_images.permute(0, 3, 1, 2)
# Нормализуем со статистиками ImageNet
mean = torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1)
std = torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1)
normalized = (tensor_images - mean) / std
return normalized.to(device)
Кастомная функция потерь с операциями тензоров:
def focal_loss(predictions, targets, gamma=2.0, alpha=0.25):
"""Focal loss для несбалансированной классификации"""
# predictions: (batch, num_classes)
# targets: (batch,) - индексы классов
ce_loss = F.cross_entropy(predictions, targets, reduction='none')
probs = torch.softmax(predictions, dim=1)
# Получаем вероятность правильного класса
batch_size = targets.shape[0]
probs_correct = probs[torch.arange(batch_size), targets]
# Применяем focal term
focal_weight = (1 - probs_correct) ** gamma
focal_loss = alpha * focal_weight * ce_loss
return focal_loss.mean()
Эффективная батчевая обработка:
def process_large_dataset(data, model, batch_size=32):
"""Обработка датасета, слишком большого для памяти"""
device = next(model.parameters()).device
results = []
# Обрабатываем чанками
for i in range(0, len(data), batch_size):
batch = data[i:i+batch_size]
batch_tensor = torch.tensor(batch, device=device)
with torch.no_grad():
output = model(batch_tensor)
results.append(output.cpu()) # Переносим обратно для экономии GPU памяти
# Конкатенируем все результаты
return torch.cat(results, dim=0)
Проверено на себе. Эти паттерны покрывают 90% операций с тензорами в продакшне.
Эффективная отладка тензорного кода
Отладка — это половина времени разработки. Вот мой воркфлоу.
Шаг 1: Утилиты для отладки форм
class TensorDebugger:
def __init__(self, verbose=True):
self.verbose = verbose
def check(self, tensor, name, expected_shape=None):
if self.verbose:
print(f"\n{name}:")
print(f" Форма: {tensor.shape}")
print(f" Устройство: {tensor.device}")
print(f" Тип: {tensor.dtype}")
print(f" Min: {tensor.min().item():.4f}")
print(f" Max: {tensor.max().item():.4f}")
print(f" Mean: {tensor.mean().item():.4f}")
if expected_shape:
assert tensor.shape == expected_shape, \
f"Ожидалось {expected_shape}, получено {tensor.shape}"
return tensor
# Использование
debug = TensorDebugger()
x = torch.randn(32, 10)
x = debug.check(x, "input", expected_shape=(32, 10))
Шаг 2: Проверка потока градиентов
def check_gradients(model):
"""Проверка правильности потока градиентов"""
for name, param in model.named_parameters():
if param.grad is not None:
grad_norm = param.grad.norm().item()
param_norm = param.norm().item()
print(f"{name}: grad_norm={grad_norm:.4f}, param_norm={param_norm:.4f}")
if grad_norm == 0:
print(f" ВНИМАНИЕ: Нулевой градиент!")
elif grad_norm > 100:
print(f" ВНИМАНИЕ: Большой градиент!")
Шаг 3: Профилирование памяти
def profile_memory(func):
"""Декоратор для профилирования использования GPU памяти"""
def wrapper(*args, **kwargs):
torch.cuda.reset_peak_memory_stats()
torch.cuda.synchronize()
start_memory = torch.cuda.memory_allocated()
result = func(*args, **kwargs)
torch.cuda.synchronize()
end_memory = torch.cuda.memory_allocated()
peak_memory = torch.cuda.max_memory_allocated()
print(f"\nПрофиль памяти для {func.__name__}:")
print(f" Использовано: {(end_memory - start_memory) / 1024**2:.2f} MB")
print(f" Пик: {peak_memory / 1024**2:.2f} MB")
return result
return wrapper
Общие команды отладки:
# Включить обнаружение аномалий (находит где возник NaN)
torch.autograd.set_detect_anomaly(True)
# Вывод тензора без научной нотации
torch.set_printoptions(precision=4, sci_mode=False)
# Проверка на NaN/Inf
has_nan = torch.isnan(tensor).any()
has_inf = torch.isinf(tensor).any()
# Найти где произошла ошибка
try:
result = risky_operation(tensor)
except RuntimeError as e:
print(f"Ошибка: {e}")
print(f"Форма входа: {tensor.shape}")
print(f"Устройство входа: {tensor.device}")
raise
Часто задаваемые вопросы
Как выбрать между .reshape() и .view()?
Используйте .view() когда нужна эффективность памяти и вы знаете, что тензор непрерывный. Используйте .reshape() когда хотите чтобы «просто работало» — он автоматически обрабатывает непрерывные тензоры, копируя при необходимости. На практике я по умолчанию использую .reshape(), если только не оптимизирую горячие пути.
Почему мой код работает на CPU но падает на GPU?
Чаще всего: тензоры на разных устройствах. Всегда проверяйте атрибутом .device. Второе: нехватка памяти — GPU имеет ограниченную RAM. Используйте меньшие батчи или накопление градиентов. Третье: некоторые операции не реализованы для CUDA. Проверьте документацию PyTorch для конкретной функции.
Когда использовать torch.no_grad()?
Используйте torch.no_grad() для любого кода, который не требует градиентов: инференс, препроцессинг данных, вычисление метрик. Экономит память и ускоряет вычисления. Работает — значит правильно. Не используйте во время forward pass обучения или градиент будет None.
Как дебажить "RuntimeError: shape mismatch"?
Выводите формы перед падающей операцией. Используйте мой класс TensorDebugger выше. Добавьте assert для ожидаемых форм по всему коду. Большинство ошибок форм происходят при: несовпадениях батч-измерения, забытом flatten перед линейным слоем, или неправильном измерении в операциях сокращения.
Заключение
Тензоры PyTorch не сложные, как только вы понимаете формы и устройства. Начните с CPU, переходите на GPU когда нужна скорость. Всегда проверяйте формы. Используйте утилиты отладки, которые я показал.
20% операций, которые я описал, покрывают 80% случаев использования. Матричное умножение, изменение формы и управление устройствами — освойте это в первую очередь. Остальное придёт естественно.
Следующий шаг: постройте что-нибудь. Даже простые матричные операции на GPU покажут вам разницу в скорости. Тот момент «ага», когда вы видите ускорение в 100 раз, заставляет всё встать на свои места.