Основная механика глубокого обучения и как мыслить в стиле PyTorch.
Делиться

Глубокое обучение формирует наш мир прямо сейчас. Более того, с начала 2010-х годов оно постепенно меняет мир программного обеспечения. В 2025 году PyTorch находится в авангарде этой революции, став одной из важнейших библиотек для обучения нейронных сетей.
Независимо от того, работаете ли вы с компьютерным зрением, создаете большие языковые модели (LLM), обучаете агента обучения с подкреплением или экспериментируете с графовыми нейронными сетями — ваш путь пересечет PyTorch, как только вы войдете в город глубокого обучения.
Все изображения, представленные в этой статье, были созданы автором.
Это руководство даст вам краткий обзор методологий и принципов проектирования PyTorch. В течение следующего часа мы отбросим все лишнее и перейдем к сути обучения нейронных сетей.
В этой статье рассказывается об основных концепциях PyTorch, а также о том, как составлять и обучать модели — от простой линейной регрессии до современного блока-трансформера .
Но что еще важнее, чем конкретные примеры кода, представленные здесь, цель этой статьи — научить основным идеям, архитектурам на уровне проекта и абстракциям для работы с PyTorch.
Другими словами, как думать «в стиле PyTorch».
Прежде чем мы дойдём до этого, важно понять основы. PyTorch построен на двух основных абстракциях: тензорах и автоматическом дифференцировании. Освойте эти два — как тензоры хранят данные и как градиенты используются для обучения нейронных сетей — и остальное в PyTorch покажется вам естественным. Давайте сначала обсудим тензоры.
1. Основы тензоров
Тензор — это многомерный массив с d-типом, устройством и опциональным отслеживанием градиента. Если вы знакомы с массивами NumPy, представляйте тензоры как массивы NumPy с несколькими важными преимуществами:
- Использование графического процессора : тензоры могут выполнять массовые параллельные операции на графическом процессоре. Поддерживаются умножение матриц, сложение и даже условные операторы.
- Граф вычислений: вместо того, чтобы представлять тензоры как изолированный блок данных, представьте их как узел на графе вычислений. (Показано ниже)
- Автоматическое дифференцирование: PyTorch автоматически вычисляет частные производные каждой выполняемой дифференцируемой операции. Вскоре мы обсудим, что это на самом деле означает и почему это так важно для обучения нейронных сетей.

2. Автоматическая дифференциация (Автоград)
Нейронные сети в PyTorch создают динамический вычислительный граф и используют его для автоматического вычисления градиентов. Давайте рассмотрим простой пример, чтобы понять это.
Начнём с чистого скалярного примера, чтобы было проще рассуждать о формах и значениях. Следующий код вычисляет z = x^2 + y^3 для скалярных x и y, а затем выполняет обратный вызов для получения dz/dx и dz/dy.
x = torch.tensor(2.0, require_grad=True) y = torch.tensor(3.0, require_grad=True) # Прямой проход: вычислить z = x^2 + y^3 z = x**2 + y**3 # Обратный проход: вычислить градиенты z.backward() dz_dx = x.grad # частная производная wrt x dz_dy = y.grad # частная производная wrt y
Что происходит:
- Мы создали два тензора x и y с параметром require_grad=True. Это указывает Autograd отслеживать их операции.
- Прямое вычисление строит небольшой график для z.
- z.backward() запускает обратный режим автодифференциации: PyTorch вычисляет градиенты и помещает их в x.grad и y.grad.
Вот как будут выглядеть результаты приведенного выше блока кода:

Если вы посчитали в уме, вот как выглядят частные производные для этого уравнения, рассчитанные аналитически (спойлер: это работает!):

Правило цепочки
Правило цепочки в математическом анализе — это фундаментальная формула для дифференцирования сложных функций, которые, по сути, являются функциями внутри других функций. Проще говоря, вы действуете снаружи внутрь, беря производную каждого «слоя» функции и перемножая их.
Рассмотрим простой пример работы цепного правила в PyTorch. Допустим, у вас есть следующее трёхшаговое уравнение:
Уравнение 1: y = x^2
Уравнение 2: z = y + 1
Уравнение 3: w = z^2
По сути, w зависит от z, z зависит от y, y зависит от x. Базовая цепочка композициональности. Допустим, вы хотите найти производную w по x.
Правило цепочек в исчислении гласит, что для нахождения dw/dx мы вычисляем градиенты по цепочке зависимостей и перемножаем их. Итак:
dw/dx = dw/dz * dz/dy * dy/dx
Давайте посмотрим, как PyTorch это делает:
# requires_grad=True сообщает PyTorch о необходимости вычисления градиентов для этого тензора x = torch.tensor(2.0, require_grad=True) # Определяет прямой проход y = x**2 z = y + 1 w = z**2 # Вычисляет градиент w.backward() # Выводит градиент print(x.grad) # 40
И всё! Просто работает.
Еще более особенным является то, что вместо определения x как скаляра, как мы сделали выше, мы можем определить его как многомерный тензор.
Вот что происходит, когда мы меняем первую строку с инициализации скаляра с помощью torch.tensor(2) на одномерный тензор с помощью torch.tensor([-1, 2])

Вот что делает PyTorch таким крутым. Вы можете одновременно (или параллельно) вычислять градиенты для нескольких элементов.
При работе над проектами глубокого обучения наши входные данные, как правило, многомерны, поэтому PyTorch выполняет большую часть тяжелой работы в фоновом режиме, распараллеливая вычисление градиента!
Формула Пайторча
Как видно из предыдущего примера, план игры PyTorch довольно прост.
- Определите «прямой проход» вашего уравнения, т. е. как ваша зависимая переменная выводится из ваших независимых переменных?
- PyTorch автоматически вычисляет обратное распространение (при условии, что ваши уравнения дифференцируемы)

3. Модели обучения
Теперь, когда мы разобрались с основами автоматической дифференциации, давайте посмотрим, как работает линейная регрессия в PyTorch. Приведённый ниже код создаёт небольшой набор данных в стиле жилищного строительства с двумя характеристиками (площадью и возрастом), нормализует их до диапазона [-1, 1] и подготавливает нас к старой доброй линейной регрессии.
df = pd.DataFrame( { «площадь»: [120, 180, 150, 210, 105], «возраст»: [5, 2, 1, 2, 1], «цена»: [30, 90, 100, 180, 85] } ) df = нормализовать(df)
Чтобы что-то сделать с PyTorch, нам нужно сначала преобразовать данные в тензоры! Обратите внимание, что тензоры данных X и Y не требуют градиентов, поскольку они константы (т.е. не меняются во время обучения).
Однако веса W и B обучаемы. Мы обновим их в соответствии с нашим набором данных. Чтобы сделать их обучаемыми с помощью обратного распространения, нам нужно установить require_grad=True для этих объявлений.
Посмотрите на код ниже:
# Обратите внимание, что это константы, мы не собираемся их обновлять X = torch.tensor(df[[«area», «age»]].values, dtype=torch.float32) Y = torch.tensor(df[[«price»]].values, dtype=torch.float32) # Это «require_grad», поэтому они являются обучаемыми весами. W = torch.rand(size=(2, 1), require_grad=True) B = torch.rand(1, require_grad=True)
Теперь сгенерируем прогноз! Прямой проход использует идиоматическое умножение и сложение матриц, то есть X @ W + B.
# Сгенерировать прогноз pred = X @ W + B
Оператор @ по сути выполняет матричное умножение между X и W. Модель X @ W + B выполняет «линейное преобразование» X. Наша цель — настроить обучаемые веса W и B таким образом, чтобы прогноз был ближе к нашей целевой истинной величине.
Затем мы вычисляем ошибку как среднеквадратичную погрешность потерь. Она вычисляет расстояние между нашим текущим прогнозом и истинным значением. Если мы вызовем loss.backward(), мы также получим градиенты обучаемых переменных на графике (то есть W и B).
loss.backward() dW = W.grad # Сообщает нам, «насколько нужно изменить W, чтобы уменьшить потери» dB = B.grad # и «насколько нужно изменить B, чтобы уменьшить потери»
dW и dB — градиенты W и B относительно функции потерь. Мы можем применить «градиентный спуск», чтобы подтолкнуть эти обучаемые параметры в направлении, указанном градиентом.
lr = 0.2 # Скорость обучения: сообщает нам, насколько нам следует обновить веса с помощью torch.no_grad(): W = W — lr * dW # Обновление W с помощью градиентного спуска B = B — lr * dB # Обновление B с помощью градиентного спуска
Понимание линейной регрессии, расчёта потерь и градиентного спуска — одни из столпов машинного обучения и, как следствие, глубокого обучения. Хотя обновление весов вручную путём вычитания градиентов возможно, на практике это невозможно для глубоких нейронных сетей с несколькими слоями весов. Эх, если бы существовал способ автоматически обновлять веса, не беспокоясь о таком отслеживании!
Примечание
Описанный выше метод итеративного обучения весам, основанный на небольших шагах в пространстве оптимизации, называется градиентным спуском. Обратите внимание, что существуют более эффективные способы обучения оптимальным значениям W и B для небольших наборов данных. Например, уравнение нормального распределения , которое даёт аналитическое решение, не требующее каких-либо шагов или итераций. Однако для больших наборов данных он требует больших вычислительных затрат. Для больших матриц стандартный подход заключается в разделении данных на мини-пакеты и применении градиентного спуска по отдельности. Этот метод известен как стохастический градиентный спуск (SGD).
Оптимизаторы
Оптимизаторы PyTorch — это алгоритмы (такие как SGD, Adam или RMSprop), которые корректируют веса и смещения модели на основе вычисленных градиентов, чтобы минимизировать функцию потерь.
Давайте проверим, как будет выглядеть приведенный выше код линейной регрессии, если мы заменим ручное обновление весов оптимизаторами PyTorch.
из torch.optim import SGD … W = torch.rand(size=(2, 1), require_grad=True) B = torch.rand(1, require_grad=True) optimizer = SGD(params = [W, B], lr=0.1) для шага в диапазоне (10): pred = X @ W + B # Прямой проход loss = ((Y — pred) ** 2).mean() # Вычислить потери loss.backward() # Вычислить градиенты optimizer.step() # Обновить W и B в соответствии с градиентами optimizer.zero_grad() # Сбросить все градиенты
Основной цикл обучения моделей в PyTorch выглядит так:
- Прямой проход для вычисления pred
- Вычислите потерю данных, найдя ошибку между прогнозом (pred) и истинным значением (Y)
- Обратный проход с loss.backward() для заполнения W.grad и B.grad.
- Выполните шаг с optimizer.step() для обновления параметров.
- Нулевые градиенты с optimizer.zero_grad() для избежания накопления.
SGD — надёжная основа для линейной регрессии. При масштабировании или столкновении с более шумными градиентами адаптивные оптимизаторы могут помочь. Именно здесь в игру вступает набор оптимизаторов с открытым исходным кодом PyTorch. Он включает в себя адаптивные оптимизаторы, такие как Adam, которые используют такие методы, как импульс и попараметрическая скорость обучения, для достижения более быстрой и стабильной сходимости при решении этих сложных задач. Вот карточка со сравнением различных популярных оптимизаторов:

Torch предлагает не только оптимизаторы, но и множество различных функций потерь! Вот несколько примеров:

4. Слои и модули
Так же, как нам не нужно писать собственные оптимизаторы, нам не нужно самостоятельно объявлять сырые тензоры и логику умножения матриц (по большей части). Модули Pytorch нас вполне удовлетворят.
Модуль PyTorch — это фундаментальный строительный блок всех нейронных сетей в PyTorch, выступающий в роли контейнера для слоёв, обучаемых параметров и логики передачи данных. Например, вместо линейного слоя, который мы написали ранее, где мы вручную задавали веса и смещения, мы можем использовать следующие строки кода:
linear_model = nn.Linear(in_size, out_size) # Torch заботится об инициализации весов predict = linear_model(input) # Прямой проход
Мы научились создавать линейные модели (ура!), но нам действительно нужно научиться обучать более крупные и глубокие нейронные сети. Простейший тип нейронной сети — многослойный персептрон (MLP). MLP, по сути, представляет собой несколько линейных слоёв с нелинейными функциями между ними.
Создание многослойных перцептронов (MLP) в Torch довольно просто. nn.Sequential — это распространённый модуль PyTorch, который используется для последовательной передачи входных данных через несколько слоёв. Вот код:
# 2-слойный MLP mlp_2_layers = nn.Sequential( nn.Linear(in_size, hidden_units), nn.ReLU(), nn.Linear(hidden_units, out_size) ) # 3-слойный MLP mlp_3_layers = nn.Sequential( nn.Linear(in_size, hidden_units), nn.ReLU(), nn.Linear(hidden_units, hidden_units), nn.ReLU(), nn.Linear(hidden_units, out_size) )
Многослойные персептроны могут обучаться композиционным и нелинейным функциям! Вот пример зигзагообразной функции и того, как её обучает двухслойный многослойный персептрон с RELU.

5. Написание собственных сетей
Torch предлагает огромный набор потрясающих слоёв и модулей, которые вдохновили на создание целых научных работ. Их можно представить как кубики Lego, из которых можно собрать любую нейронную сеть.
Нужен свёрточный сетевой слой для изображений? Используйте nn.Conv2d.
Уровень GRU для обработки последовательных токенов? Используйте nn.GRU.
Но чаще всего в ходе исследований возникает необходимость написать собственную архитектуру нейронной сети с нуля. Рецепт этого процесса следующий:
- Подкласс от nn.Module
- В функции конструктора __init__ инициализируйте все слои и веса.
- Определите метод forward(), в котором вы пишете логику прямого прохода.
Вот пример, в котором мы реализуем классическую архитектуру ResNet:
class ResNetBlock(nn.Module): def __init__(self, in_channels, out_channels, stride=1, downsample=None): super(ResNetBlock, self).__init__() self.conv1 = nn.Conv2d( in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False, ) self.bn1 = nn.BatchNorm2d(out_channels) self.conv2 = nn.Conv2d( out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False ) self.bn2 = nn.BatchNorm2d(out_channels) self.downsample = downsample def forward(self, x): residual = x out = F.relu(self.bn1(self.conv1(x))) out = self.bn2(self.conv2(out)) если self.downsample: остаток = self.downsample(x) out += остаток out = F.relu(out) возврат out
Вот и всё! Вы просто инициализируете слои и определяете граф вычислений прямого прохода, а Torch самостоятельно выполнит обратный проход.

Конечно, вы можете использовать свои собственные слои и модули как части гораздо более крупной сети! Например, вот пример написания одного блока-трансформера.
class AttentionLayer(nn.Module): def __init__(self, input_dim, attention_dim=64): super(SimpleAttention, self).__init__() # Линейные слои для вычисления внимания self.query = nn.Linear(input_dim, attention_dim) self.key = nn.Linear(input_dim, attention_dim) self.value = nn.Linear(input_dim, attention_dim) # Коэффициент масштабирования self.scale = torch.sqrt(torch.FloatTensor([attention_dim])) def forward(self, x): # x shape: (batch_size, sequence_length, input_dim) batch_size, seq_len, input_dim = x.size() # Вычислить Q, K, VQ = self.query(x) # (batch_size, seq_len, attention_dim) K = self.key(x) # (batch_size, seq_len, attention_dim) V = self.value(x) # (batch_size, seq_len, attention_dim) attention_scores = torch.matmul(Q, K.transpose(-2, -1)) / self.scale # Масштабированное скалярное произведение внимания attention_weights = F.softmax(attention_scores, dim=-1) # Преобразовать веса внимания в вероятности attended_output = torch.matmul(attention_weights, V) # Применить внимание к значениям return attended_output, attention_weights class TransformerBlock(nn.Module): «»» Один блок-трансформер, состоящий из собственного внимания и сети прямого распространения. «»» def __init__(self, embed_dim, ffn_hidden_dim): «»» Аргументы: embed_dim (int): Размерность вложения модели. ffn_hidden_dim (int): Размерность скрытого слоя в FFN. «»» super(TransformerBlock, self).__init__() self.attention = SimpleAttention(embed_dim, embed_dim) self.norm1 = nn.LayerNorm(embed_dim) self.norm2 = nn.LayerNorm(embed_dim) self.ffn = nn.Sequential( nn.Linear(embed_dim, ffn_hidden_dim), nn.ReLU(), nn.Linear(ffn_hidden_dim, embed_dim) ) def forward(self, x): «»» Прямой проход для блока-трансформера. Аргументы: x (torch.Tensor): Входной тензор формы (batch_size, sequence_length, embed_dim). Возвращает: torch.Tensor: Выходной тензор блока трансформатора. «»» # Часть собственного внимания, _ = self.attention(x) # Сложение и нормирование (остаточная связь) x = self.norm1(attended + x) # Часть прямой связи ffn_out = self.ffn(x) # Сложение и нормирование (остаточная связь) x = self.norm2(ffn_out + x) return x class TransformerEncoder(nn.Module): «»» Кодер трансформатора, который складывает несколько блоков TransformerBlock. «»» def __init__(self, num_layers, embed_dim, ffn_hidden_dim, seq_len, output_dim): «»» Аргументы: num_layers (int): Количество блоков трансформатора для укладки. embed_dim (int): Размерность вложений модели. ffn_hidden_dim (целое): размерность скрытого слоя в FFN. seq_len (целое): длина входных последовательностей. output_dim (целое): размерность конечного вывода (например, количество классов). «»» super(TransformerEncoder, self).__init__() # Создать список блоков-трансформеров self.layers = nn.ModuleList( [TransformerBlock(embed_dim, ffn_hidden_dim) for _ in range(num_layers)] ) # Финальный классификационный заголовок self.classifier = nn.Linear(embed_dim * seq_len, output_dim) def forward(self, x): «»» Прямой проход для полного кодировщика-трансформера. Аргументы: x (torch.Tensor): входной тензор формы (batch_size, sequence_length, embed_dim). Возвращает: torch.Tensor: конечные выходные логиты из классификатор. «»» # Пропускаем входные данные через все блоки преобразователя для слоя в self.layers: x = layer(x) # Сглаживаем выходные данные для классификатора x = x.view(x.size(0), -1) # Окончательный вывод классификации = self.classifier(x) return output
Обратите внимание, как первый модуль AttentionLayer вычисляет масштабированное скалярное произведение внимания. TransformerBlock применяет к нему нормы слоёв и сети прямого распространения. И наконец, модуль TransformerEncoder последовательно применяет несколько блоков Transformer! Таким образом, мы получаем модель BERT, включающую несколько стеков двунаправленных слоёв внимания, а также различные оптимизации, такие как нормы слоёв и остаточные связи.
Если вы новичок и эта часть вас ошеломляет, то это вполне ожидаемо! Преимущество PyTorch в том, что вы можете выбирать уровень сложности в зависимости от своего уровня подготовки.
На начальном этапе вам, возможно, захочется ограничиться сотнями готовых модулей Pytorch, которые он предлагает сразу из коробки. Постепенно вы почувствуете потребность в их расширении и адаптации под свои нужды. А написав пару собственных модулей, вы будете становиться всё более уверенными и опытными.
Цель этого раздела — показать вам возможности и бесконечные возможности настройки, которые вы получаете, комбинируя модули. Помните: вы пишете прямой проход, и пока весь граф дифференцируем, Torch всегда сможет выполнить автоматическое дифференцирование за вас!
Следующие шаги
Функции и концепции, рассматриваемые в этой статье, были тщательно отобраны, чтобы дать краткий обзор некоторых важнейших возможностей Torch. У меня есть видео на YouTube, в котором объясняются все эти концепции, а также некоторые дополнительные, такие как развёртывание моделей, загрузчики данных, распределения и методы обучения.
На этом статья закончена! Вот несколько ссылок, по которым вы можете узнать больше о моей работе. Спасибо за прочтение!
Поддержите меня на Patreon: https://www.patreon.com/NeuralBreakdownwithAVB
Мой канал на YouTube:
https://www.youtube.com/@avb_fj
Подписывайтесь на меня в Twitter:
https://x.com/neural_avb
Прочитайте мои статьи:
https://towardsdatascience.com/author/neural-avb/
Источник: towardsdatascience.com





















