Перейти к содержанию
Educora
Университет22 мин39 / 42

PyTorch: построение нейронных сетей

От искусственного нейрона к многослойной сети: z = w·x + b, функции активации (ReLU, сигмоида, tanh, softmax), слои nn.Linear, nn.Module и nn.Sequential, прямой проход и число параметров.

Проверь себя
В этом уроке ты узнаешь
  • Записывать математическую модель нейрона и полносвязного слоя
  • Вычислять ReLU, сигмоиду, tanh и softmax и знать, где они применяются
  • Строить сеть с помощью nn.Module и nn.Sequential и считать её параметры

Мы хотим распознать рукописную цифру: изображение — это 28 × 28 = 784 пикселя, а ответ — одна из десяти цифр от 0 до 9. Какая функция может превратить 784 числа в десять вероятностей? Нейронная сеть строит такую функцию из множества простых частей: линейных преобразований и нелинейных «изгибов» между ними. Название навеяно мозгом, но на деле это математическая функция, параметры которой подбираются градиентным спуском — ровно как в предыдущих уроках.

Искусственный нейрон

z = w · x + b = ∑ᵢ₌₁ⁿ wᵢxᵢ + b a = φ(z)
где:
  • xвходной вектор (n признаков)
  • wвеса — важность каждого входа
  • bсмещение (bias) — сдвигает порог активации
  • φфункция активации (ReLU, сигмоида…)
  • aвыход нейрона (активация)

Нейрон вычисляет взвешенную сумму входов, прибавляет смещение и пропускает результат через функцию активации. В примере ниже z = 0,4 · 0,5 + 0,3 · (−1) + (−0,2) · 2 + 0,1 = −0,4: сигмоида превращает это в 0,4013, а ReLU — в 0.

Python
import torch

x = torch.tensor([0.5, -1.0, 2.0])
w = torch.tensor([0.4, 0.3, -0.2])
b = torch.tensor(0.1)
z = w @ x + b
print(round(z.item(), 4))
print(round(torch.sigmoid(z).item(), 4), torch.relu(z).item())
Ожидаемый результат
-0.4
0.4013 0.0

Функции активации

Без активаций сеть оставалась бы линейной, сколько бы слоёв мы ни добавили. Доказательство короткое: два слоя подряд дают W₂(W₁x + b₁) + b₂ = (W₂W₁)x + (W₂b₁ + b₂) — это один линейный слой с матрицей W = W₂W₁ и смещением b = W₂b₁ + b₂. Нелинейная φ «изгибает» слои и позволяет сети описывать сложные, искривлённые границы.

ReLU(z) = max(0, z) σ(z) = 1 / (1 + e⁻ᶻ) tanh(z) = (eᶻ − e⁻ᶻ) / (eᶻ + e⁻ᶻ)ReLU(z) = max(0, z) σ(z) = 1 / (1 + e⁻ᶻ) tanh(z) = (eᶻ − e⁻ᶻ) / (eᶻ + e⁻ᶻ)
где:
  • ReLUобнуляет отрицательные значения; производная 1 при z > 0 и 0 при z < 0
  • σсигмоида: значения в (0; 1), трактуются как вероятность
  • tanhгиперболический тангенс: значения в (−1; 1), симметричен относительно нуля
softmax(z)ᵢ = eᶻⁱ / ∑ⱼ₌₁ᴷ eᶻʲsoftmax(z)ᵢ = eᶻⁱ / ∑ⱼ₌₁ᴷ eᶻʲ
где:
  • zсырые оценки (логиты) для K классов
  • softmax(z)ᵢвероятность класса i; все положительны и в сумме дают 1

В многоклассовой классификации softmax превращает логиты последнего слоя в распределение вероятностей.

ФункцияОбласть значенийГде применяется
ReLU[0; ∞)стандартный выбор в скрытых слоях: просто, быстро, градиент не затухает
Сигмоида(0; 1)выход бинарной классификации, «вентили» (LSTM)
tanh(−1; 1)рекуррентные сети, когда нужен центрированный выход
softmax(0; 1), сумма 1последний слой многоклассовой классификации

У ReLU есть слабое место: если вход нейрона отрицателен для всех примеров, его градиент всегда нулевой, и нейрон больше не учится («мёртвый ReLU»). LeakyReLU борется с этим, оставляя небольшой наклон на отрицательной стороне (например, 0,01z). В трансформерах широко используют GELU — гладкий вариант ReLU. В PyTorch всё это готово: nn.LeakyReLU(), nn.GELU(), nn.Tanh(), nn.Sigmoid().

Python
import torch

z = torch.tensor([-2.0, 0.0, 1.0, 3.0])
print(torch.relu(z))
print(torch.sigmoid(z).round(decimals=4))
print(torch.tanh(z).round(decimals=4))
p = torch.softmax(z, dim=0)
print(p.round(decimals=4), round(p.sum().item(), 4))
Ожидаемый результат
tensor([0., 0., 1., 3.])
tensor([0.1192, 0.5000, 0.7311, 0.9526])
tensor([-0.9640,  0.0000,  0.7616,  0.9951])
tensor([0.0057, 0.0418, 0.1135, 0.8390]) 1.0
Пример 1: softmax вручную

Сеть выдала логиты z = (2; 1; 0) для трёх классов. Найди вероятности.

Показать решение
e² ≈ 7,389; e¹ ≈ 2,718; e⁰ = 1. Сумма ≈ 11,107.
p ≈ (7,389 / 11,107; 2,718 / 11,107; 1 / 11,107) ≈ (0,665; 0,245; 0,090).
Логиты отличаются всего на 1, а вероятности — примерно в e ≈ 2,72 раза: softmax «усиливает» наибольшую оценку.
Python
import torch

print(torch.softmax(torch.tensor([2.0, 1.0, 0.0]), dim=0).round(decimals=3))
Ожидаемый результат
tensor([0.6650, 0.2450, 0.0900])

Слои: nn.Linear

Когда несколько нейронов смотрят на один и тот же вход, их векторы весов становятся строками одной матрицы W, и все выходы слоя вычисляются одним матричным произведением. Входы, приходящие пакетом, имеют форму (N, nin), а выходы — (N, nout).

Y = X · Wᵀ + b params = (nin + 1) · nout
где:
  • Xпакет входов, форма (N, nin)
  • Wвеса, форма (nout, nin) — layer.weight
  • bсмещения, форма (nout,) — layer.bias
  • Yвыходы, форма (N, nout)

У каждого выходного нейрона nin весов и одно смещение, поэтому в полносвязном слое (nin + 1) · nout параметров.

Python
import torch
from torch import nn

torch.manual_seed(42)
layer = nn.Linear(in_features=3, out_features=2)
print(layer.weight.shape, layer.bias.shape)
x = torch.randn(4, 3)
print(layer(x).shape)
print(sum(p.numel() for p in layer.parameters()))
Ожидаемый результат
torch.Size([2, 3]) torch.Size([2])
torch.Size([4, 2])
8

Веса инициализируются небольшими случайными значениями: по умолчанию nn.Linear выбирает их равномерно из интервала (−1/√nin; 1/√nin). Случайность важна — если бы все веса были одинаковыми, все нейроны слоя получали бы одинаковый градиент и учили бы одно и то же (проблема симметрии).

Сеть на nn.Module и nn.Sequential

В PyTorch каждая модель — наследник nn.Module. В методе __init__ создаются слои (их параметры регистрируются автоматически), а метод forward описывает вычисление от входа к выходу. Модель вызывают как model(x) — PyTorch сам запускает forward.

Python
import torch
from torch import nn

class MLP(nn.Module):
    def __init__(self, n_in, n_hidden, n_out):
        super().__init__()
        self.fc1 = nn.Linear(n_in, n_hidden)
        self.fc2 = nn.Linear(n_hidden, n_out)

    def forward(self, x):
        h = torch.relu(self.fc1(x))
        return self.fc2(h)

torch.manual_seed(42)
model = MLP(784, 128, 10)
print(model)
x = torch.randn(32, 784)
print(model(x).shape)
print(sum(p.numel() for p in model.parameters()))
Ожидаемый результат
MLP(
  (fc1): Linear(in_features=784, out_features=128, bias=True)
  (fc2): Linear(in_features=128, out_features=10, bias=True)
)
torch.Size([32, 10])
101770
Пакет из 32 изображений (32, 784) → скрытый слой (32, 128) → логиты (32, 10). Параметров: 785 · 128 + 129 · 10 = 100 480 + 1290 = 101 770.
  • model.parameters() — все обучаемые параметры; их передают оптимизатору.
  • model.to(device) — одним вызовом переносит все параметры с CPU на GPU (или обратно).
  • model.train() и model.eval() — режимы обучения и оценки (важны для слоёв Dropout и BatchNorm).
  • model.state_dict() — словарь параметров; в следующем уроке с его помощью сохраним модель в файл.
Определение
Многослойный перцептрон (MLP) и логиты

Сеть из полносвязных слоёв с активациями между ними называется многослойным перцептроном. Ненормированные выходы её последнего слоя — логиты — могут быть любыми действительными числами. Для предсказания берут индекс наибольшего логита (logits.argmax(dim=1)), для вероятностей — torch.softmax(logits, dim=1).

Как выбрать архитектуру? Для табличных данных обычно хватает 1–3 скрытых слоёв по 32–256 нейронов; начинай с маленькой модели и увеличивай её, пока это позволяют потери на валидации. Для изображений полносвязные слои слишком дороги — переход от цветного изображения 224 × 224 к слою из 1000 нейронов требует уже более 150 миллионов параметров. Эту проблему решают свёрточные сети.

Если слои просто идут друг за другом, класс писать не нужно: nn.Sequential применяет их по порядку. Активация здесь записывается как слой nn.ReLU(). named_parameters() показывает имя и форму каждого параметра — очень полезно при отладке:

Python
import torch
from torch import nn

model = nn.Sequential(
    nn.Linear(784, 128), nn.ReLU(),
    nn.Linear(128, 64), nn.ReLU(),
    nn.Linear(64, 10),
)
for name, p in model.named_parameters():
    print(name, tuple(p.shape))
print(sum(p.numel() for p in model.parameters()))
Ожидаемый результат
0.weight (128, 784)
0.bias (128,)
2.weight (64, 128)
2.bias (64,)
4.weight (10, 64)
4.bias (10,)
109386
Числа 0, 2, 4 в именах — номера слоёв внутри Sequential; у слоёв ReLU с номерами 1 и 3 параметров нет.
Пример 2: посчитай параметры вручную

Сколько параметров в сети 784 → 128 → 64 → 10? Сколько памяти они занимают во float32?

Показать решение
1-й слой: (784 + 1) · 128 = 100 480.
2-й слой: (128 + 1) · 64 = 8256.
3-й слой: (64 + 1) · 10 = 650.
Итого: 109 386 — как и в выводе кода.
Память: 109 386 · 4 байта ≈ 0,44 МБ. 92 % параметров приходится на первый слой: большие входы обходятся дорого.
Пример 3: прямой проход вручную

Сеть 2 → 2 → 1: W₁ = [[1; −1]; [0,5; 0,5]], b₁ = (0; −1), ReLU в скрытом слое, W₂ = [[2; −3]], b₂ = 1. Найди выход для x = (1; 2).

Показать решение
W₁x + b₁ = (1 − 2 + 0; 0,5 + 1 − 1) = (−1; 0,5).
ReLU: h = (0; 0,5) — первый нейрон «выключился».
y = 2 · 0 + (−3) · 0,5 + 1 = −0,5.
Обрати внимание: для этого x первый нейрон никак не влияет на выход, поэтому при обратном проходе градиент его весов тоже будет нулевым — ReLU пропускает градиент только через активные нейроны.
Python
import torch

x = torch.tensor([1.0, 2.0])
W1 = torch.tensor([[1.0, -1.0], [0.5, 0.5]])
b1 = torch.tensor([0.0, -1.0])
W2 = torch.tensor([[2.0, -3.0]])
b2 = torch.tensor([1.0])
h = torch.relu(W1 @ x + b1)
y = W2 @ h + b2
print(h, y)
Ожидаемый результат
tensor([0.0000, 0.5000]) tensor([-0.5000])

Главное

  • Нейрон вычисляет z = w · x + b, затем активацию a = φ(z); слой делает это в матричной форме: Y = X · Wᵀ + b.
  • Без активаций многослойная сеть равна одному линейному слою; в скрытых слоях обычно используют ReLU.
  • Softmax превращает логиты в вероятности с суммой 1; в nn.CrossEntropyLoss подают сырые логиты.
  • Модель наследует nn.Module: слои — в __init__, вычисления — в forward; для простой цепочки хватит nn.Sequential.
  • В полносвязном слое (nin + 1) · nout параметров; nn.Linear(n_in, n_out).weight имеет форму (nout, nin).

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Сколько параметров в слое nn.Linear(10, 5)?