Проекты Блог Музыка Контакты
← Все посты
Технологии 23 февраля 2026 г.

Визуальное руководство по тензорам PyTorch: От нуля до нейронных сетей за 30 минут

Автор: Евгений Падежнов

Illustration for: PyTorch Tensors Visual Guide: From Zero to Neural Networks in 30 Minutes

PyTorch работает везде — от автопилота Tesla до ChatGPT. Но все застревают на тензорах сначала. Я потратил две недели на отладку ошибок с размерностями, когда начинал. Вот что я хотел бы, чтобы мне показали в первый день.

Это руководство показывает операции с тензорами визуально. Вы поймёте размерности, операции и ускорение на GPU. Включены реальные примеры кода из продакшн-проектов.

Что такое тензоры на самом деле

Тензоры — это просто многомерные массивы. Представьте Excel-таблицу, которая может иметь больше 2 измерений. Документация PyTorch называет их «центральной абстракцией данных в PyTorch».

Вот иерархия:

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 точно описывает:

  1. GPU-ускорение — операции в миллионы раз быстрее
  2. Автоматическое дифференцирование — градиенты вычисляются автоматически
  3. Экосистема глубокого обучения — встроенные функции потерь, оптимизаторы, слои

На практике я перешёл с 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. Стопроцентная правда.

Стандартные соглашения по типам данных:

# Проверка формы
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)

Несовпадения форм чаще всего происходят здесь:

  1. Несовпадение размера батча между слоями
  2. Забыли сделать flatten перед полносвязным слоем
  3. Неправильный порядок каналов (channels-first vs channels-last)
  4. Путаница с правилами broadcasting

Мой чек-лист отладки:

Операции на 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 раз, заставляет всё встать на свои места.

Squeeze AI
  1. Тензоры PyTorch — это многомерные массивы, которые превосходят NumPy благодаря GPU-ускорению (миллионы раз быстрее), автоматическому дифференцированию и встроенной экосистеме глубокого обучения.
  2. Наиболее практичные способы создания тензоров — это конвертация из списков Python, использование предзаполненных функций (zeros, ones, rand) и импорт из NumPy, которые охватывают 90% реальных случаев.
  3. Ошибки с размерностями возникают потому, что интуиция подводит при работе с 3D+ тензорами; ментальная модель «таблица со слоями» (первое измерение = листы, второе = строки, третье = столбцы) помогает избежать основных проблем.

Squeezed by b1key AI