In this repository, I trained using the Duke University method and tested the MNIST dataset of handmade numbers using PyTorch Коротко: код загружает датасет MNIST, строит простую сверточную сеть, обучает её 3 эпохи оптимизатором Adam с функцией потерь CrossEntropyLoss и оценивает accuracy на тесте. Ниже — разбор каждой строки и параметра простыми словами, с мини-иллюстрациями-пояснениями в тексте.
import torch,import torch.nn as nn,import torch.nn.functional as F: подключают PyTorch и его модули для нейросетей;nnсодержит готовые слои,F— функции активации и пулы без параметров.from torchvision import datasets, transforms: даёт доступ к популярным датасетам (MNIST) и преобразованиям изображений.from tqdm.notebook import tqdm, trange: прогресс-бары в ноутбуке для циклов по батчам и эпохам.
Иллюстрация-идея: “кубики” — torch (базовые тензоры), nn (слои), F (функции), datasets (данные), transforms (преобразования), tqdm (индикатор).
-
device = torch.device("cuda" if torch.cuda.is_available() else "cpu"): выбирает GPU при наличии, иначе CPU; перенос тензоров и модели на это устройство ускоряет расчёты на видеокарте. -
transform = transforms.Compose([...]): конвейер преобразований.-
transforms.ToTensor(): конвертирует PIL-изображение MNIST из в тензор float в диапазоне. -
transforms.Normalize((0.1307,), (0.3081,)): нормирует по формуле$$x'=(x-\mu)/\sigma$$ с усреднением по всему датасету; значения 0.1307 и 0.3081 — среднее и стандартное отклонение MNIST после масштабирования к. Это ускоряет обучение.
-
Иллюстрация-идея: шкала серого 0…1 → “центрирование” вокруг 0 и “сжатие” по σ.
mnist_train = datasets.MNIST(root="./datasets", train=True, transform=transform, download=True): загружает обучающую часть MNIST в папку, применяетtransform, скачивает при отсутствии.mnist_test = datasets.MNIST(root="./datasets", train=False, transform=transform, download=True): тестовая часть с теми же преобразованиями.train_loader = torch.utils.data.DataLoader(mnist_train, batch_size=100, shuffle=True): создаёт итератор по батчам размера 100 и перемешивает данные каждый эпохой для лучшей обобщающей способности.test_loader = torch.utils.data.DataLoader(mnist_test, batch_size=100, shuffle=False): тест не перемешивают; размер батча 100 ускоряет оценку.
Пояснение параметров DataLoader:
- batch_size: сколько образцов в одном шаге; компромисс между скоростью и стабильностью градиентов.
- shuffle: True — случайный порядок в каждой эпохе (используется RandomSampler под капотом).
-
class MNIST_CNN(nn.Module):объявление модели. Наследование отnn.Moduleпозволяет регистрировать слои и параметры. -
self.conv1 = nn.Conv2d(1, 32, 3, 1): сверточный слой 2D. Параметры: in_channels=1 (ч/б), out_channels=32 (фильтры), kernel_size=3 (3×3 окно), stride=1 (шаг 1). Результат: 32 карт признаков. -
self.conv2 = nn.Conv2d(32, 64, 3, 1): второй свёрточный слой, вход 32 канала, выход 64, ядро 3×3, шаг 1.[1] -
self.dropout1 = nn.Dropout(0.25): случайно зануляет 25% нейронов при обучении для регуляризации (борьба с переобучением). -
self.dropout2 = nn.Dropout(0.5): зануляет 50% перед последним слоем. -
self.fc1 = nn.Linear(9216, 128): полносвязный слой из 9216 признаков в 128. Число 9216 получается из размерности после свёрток и пулинга: вход 28×28 → conv3x3 stride1 без padding уменьшает на 2 пикселя с каждой стороны; дважды: 28→26→24; затем max-pool 2×2 делит пополам: 24→12; 64 карт по 12×12:$$64×12×12=9216$$ . -
self.fc2 = nn.Linear(128, 10): выходной слой на 10 классов цифр 0–9, выдаёт логиты (не вероятности). -
def forward(self, x):прямой проход:-
x = F.relu(self.conv1(x)): свёртка → ReLU добавляет нелинейность. -
x = F.relu(self.conv2(x)): вторая свёртка + ReLU. -
x = F.max_pool2d(x, 2): максимальный пулинг 2×2 уменьшает размер карт в 2 раза, выделяя “самые сильные” признаки. -
x = self.dropout1(x): регуляризация. -
x = torch.flatten(x, 1): разворачивает карты признаков в вектор, начиная с оси 1 (оставляя размер батча). -
x = F.relu(self.fc1(x)): полносвязный слой + ReLU. -
x = self.dropout2(x): ещё регуляризация. -
x = self.fc2(x): логиты классов, без softmax. Для CrossEntropyLoss это правильно — softmax встроен в лосс. -
return x: возврат логитов.
-
Иллюстрация-идея: “конвейер” — изображение 28×28 → conv → conv → pool → flatten → fc → fc (10 логитов).
model = MNIST_CNN().to(device): создаёт модель и переносит на CPU или GPU.criterion = nn.CrossEntropyLoss(): функция потерь для многоклассовой классификации; принимает логиты и целочисленные метки классов, внутри применяетLogSoftmax+NLLLoss, one-hot не нужен.optimizer = torch.optim.Adam(model.parameters(), lr=0.001): оптимизатор Adam с шагом обучения 0.001; адаптивно настраивает скорость для каждого параметра.
Параметры CrossEntropyLoss и почему так:
- Вход: логиты размера [batch, classes]. Цели: индексы классов [batch], а не one-hot.
- Опции:
weightдля дисбаланса классов,ignore_indexдля пропуска,reduction(“mean” по умолчанию).
for epoch in trange(3):: 3 эпохи обучения;trangeпоказывает прогресс.model.train(): включает режим обучения (активирует Dropout и, при наличии, BatchNorm).for images, labels in tqdm(train_loader):: итерируемся по батчам изображений и меток.images = images.to(device); labels = labels.to(device): переносим данные на устройство модели.optimizer.zero_grad(): обнуляем прошлые градиенты (иначе будут накапливаться).y = model(images): прямой проход, получаем логиты.loss = criterion(y, labels): считаем кросс-энтропийную потерю между логитами и метками.loss.backward(): обратное распространение, вычисление градиентов параметров модели.optimizer.step(): обновление параметров по правилам Adam.
Иллюстрация-идея: цикл “прямой проход → loss → градиент → шаг оптимизатора” повторяется для каждого батча и эпохи.
model.eval(): режим оценки (Dropout выключен).correct = 0: счётчик верных предсказаний.total = len(mnist_test): всего тестовых образцов (10000 в MNIST).with torch.no_grad():: отключает градиенты для экономии памяти/скорости при инференсе.for images, labels in tqdm(test_loader):: перебор тестовых батчей.y = model(images): логиты.predictions = torch.argmax(y, dim=1): выбираем индекс максимального логита — предсказанный класс.correct += (predictions == labels).sum().item(): суммируем верные.print('Test accuracy: {:.4f}'.format(correct / total)): выводим долю верных — accuracy.
Иллюстрация-идея: “столбик” верных предсказаний растёт по мере прохода по тестовым батчам, итог — доля верных.
- ToTensor уже делит на 255 → значения в. Поэтому использовать среднее и std, предвычисленные для уже масштабированных пикселей:
$$\mu≈0.1307,\ \sigma≈0.3081$$ . Это общепринятые константы для MNIST в PyTorch-примерах. - Нормализация ускоряет сходимость и стабилизирует градиенты; CrossEntropyLoss ожидает логиты, softmax применять отдельно не нужно.
- batch_size=100: учимся “порциями” по 100 картинок — быстрее и стабильнее, чем по одной; можно менять в зависимости от памяти GPU/CPU.
- shuffle=True (на train): каждую эпоху порядок перемешан, чтобы сеть не запоминала последовательности и лучше обобщала.
- lr=0.001: скорость обучения для Adam; слишком большая — скачет и не сходится, слишком маленькая — учится медленно.
- Dropout(0.25/0.5): “выключает” часть нейронов случайно при обучении, чтобы сеть не переобучалась на шум и частные паттерны.
- CrossEntropyLoss: измеряет “насколько уверенно” сеть ставит высокий логит правильному классу; чем выше логит правильного класса относительно остальных, тем меньше лосс.
- Ускорить загрузку данных: добавить num_workers>0 и pin_memory=True в DataLoader при обучении на CUDA.
- Добавить лёгкие аугментации (например, RandomRotation) — рукописные цифры могут быть повёрнуты/сдвинуты.
- Увеличить эпохи до 5–10 — точность обычно вырастет.
Пример улучшенного DataLoader (для GPU):
train_loader = DataLoader(mnist_train, batch_size=128, shuffle=True, num_workers=2, pin_memory=True)— быстрее подаёт батчи на видеокарту при достаточных ресурсах.
Доп. справки:
- Общая схема обучения и примеры CNN в PyTorch туториалах.
- Детали CrossEntropyLoss и почему не нужен one-hot.
- Объяснение Normalize и значений для MNIST.
- Пояснения к DataLoader и shuffle.