Форум программистов, компьютерный форум, киберфорум
AI_Generated
Войти
Регистрация
Восстановить пароль

Создание нейросети с PyTorch

Запись от AI_Generated размещена 19.06.2025 в 21:17
Показов 7955 Комментарии 1

Нажмите на изображение для увеличения
Название: Создание нейросети с PyTorch.jpg
Просмотров: 162
Размер:	163.5 Кб
ID:	10909
Ключевое преимущество PyTorch — его питоновская натура. В отличие от TensorFlow, который изначально был построен как статический вычислительный граф, PyTorch предлагает динамический подход. Это означает, что вы можите менять архитектуру сети на лету, отлаживать код привычными средствами и не запускать отдельную сессию для вычислений. Для меня как разработчика такой подход ближе к тому, как мы обычно пишем и отлаживаем код.

TensorFlow долгое время господствовал на рынке глубокого обучения, предлагая промышленный стандарт и обширную экосистему. Но его API часто критиковали за излишнюю сложность и вербозность. PyTorch же изначально проектировался с фокусом на удобство и гибкость. Если вы научный сотрудник или исследователь, который часто экспериментирует с новыми архитектурами — PyTorch буквально создан для вас.

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
# Сравните простоту определения модели в PyTorch
class SimpleNN(nn.Module): 
    def __init__(self): 
        super(SimpleNN, self).__init__() 
        self.fc1 = nn.Linear(2, 5) 
        self.relu = nn.ReLU() 
        self.fc2 = nn.Linear(5, 1) 
    
    def forward(self, x): 
        x = self.fc1(x) 
        x = self.relu(x) 
        x = self.fc2(x) 
        return x
Однако не думайте, что PyTorch — это только для академии. Facebook (теперь Meta) разработал этот инструмент для своих внутренних нужд, и он вполне годится для промышленного применения. Система дифференцирования в PyTorch — это настоящее чудо инженерной мысли, позволяющее автоматически вычислять градиенты сложных функций без дополнительных усилий с вашей стороны. Еще один фактор в пользу PyTorch — это сообщество. За последние годы выросла огромная экосистема библиотек, построенных на его основе: PyTorch Lightning для более структурированной организации кода, fastai для упрощения типовых задач, torchvision для работы с изображениями, torchaudio для звука и многие другие.

И все же, я не могу не отметить — выбор инструмента всегда зависит от задачи. Если вам важна легкость развертывания в продакшн через TensorFlow Serving или TensorFlow.js, возможно, стоит выбрать TensorFlow. Но если ваш приоритет — исследовательская гибкость и понятный, привычный рабочий процесс — PyTorch станет вашим верным союзником в мире глубокого обучения.

Архитектура нейросети: от биологических нейронов к математическим моделям



Меня всегда завораживала идея того, что мы пытаемся воссоздать мыслительные процессы, используя математические абстракции. Нейронные сети — это не просто модный инструмент машинного обучения, это попытка хотя бы отдаленно смоделировать работу самого сложного устройства во Вселенной — человеческого мозга.

Основы работы искусственных нейронов



В чем же суть нейрона? Биологический нейрон получает сигналы от других нейронов через дендриты, обрабатывает их в соме (теле клетки) и, если суммарный сигнал достаточно силен, генерирует импульс, который передается дальше по аксону. Искусственный нейрон — это чрезвычайно упрощенная версия этого процеса.

Python
1
2
3
4
5
6
def simple_neuron(inputs, weights, bias):
    # Взвешенная сумма входов
    weighted_sum = sum(x * w for x, w in zip(inputs, weights)) + bias
    # Активация (в данном случае простой порог)
    output = 1 if weighted_sum > 0 else 0
    return output
Но даже такая примитивная модель демонстрирует фундаментальный принцип: нейрон — это функция, принимающая несколько входов, взвешивающая их и применяющая некую функцию активации для получения выхода. Удивительно, что из таких простых элементов можно построить системы, способные распознавать изображения, играть в го и генерировать текст, иногда не хуже человека.

Прямое и обратное распространение ошибки



Нейронная сеть — это не хаотичный набор нейронов, а организованная структура. Типичная архитектура включает входной слой, один или несколько скрытых слоев и выходной слой. При прямом распространении (forward propagation) сигнал идет от входа к выходу, проходя через все слои и трансформируясь на каждом этапе.

Python
1
2
3
4
5
6
7
8
9
10
def forward_pass(inputs, network):
    activations = inputs
    for layer in network:
        new_activations = []
        for neuron in layer:
            weighted_sum = sum(a * w for a, w in zip(activations, neuron['weights'])) + neuron['bias']
            activation = activation_function(weighted_sum)  # Функция активации
            new_activations.append(activation)
        activations = new_activations
    return activations
Но как сеть учится? Тут на сцену выходит обратное распространение ошибки (backpropagation) — алгоритм, который я считаю одним из самых важных в истории искуственного интеллекта. Представьте, что вы прошли через лабиринт и в конце обнаружили, что ошиблись. Как понять, где именно вы свернули не туда? Backprop решает эту проблему, вычисляя, насколько каждый нейрон «виноват» в итоговой ошибке.

Когда я впервые пытался реализовать backprop с нуля, я потратил почти неделю, разбираясь с частными производными и цепным правилом. К счастью, с PyTorch это происходит автоматически:

Python
1
2
3
4
5
6
7
# Forward pass
outputs = model(inputs)
loss = criterion(outputs, targets)
 
# Backward pass
loss.backward()  # Вычисляет градиенты для всех параметров
optimizer.step()  # Обновляет параметры на основе градиентов

Функции активации и их роль в обучении



Функции активации — это своего рода переключатели, решающие, должен ли нейрон "сработать". Без них нейронные сети были бы просто линейными преобразованиями, не способными моделировать сложные зависимости. Исторически первой была ступенчатая функция (как в моем первом примере), но с ней невозможно использовать градиентный спуск, потому что её производная либо равна нулю, либо не существует. Поэтому появились дифференцируемые функции: сигмоида, гиперболический тангенс, и моя любимая — ReLU (Rectified Linear Unit):

Python
1
2
3
4
5
6
7
8
def sigmoid(x):
    return 1 / (1 + math.exp(-x))
 
def tanh(x):
    return (math.exp(x) - math.exp(-x)) / (math.exp(x) + math.exp(-x))
 
def relu(x):
    return max(0, x)
ReLU стала настоящим прорывом из-за своей простоты и эффективности. Она решает проблему "затухающих градиентов", характерную для сигмоиды и tanh, что позволяет обучать действительно глубокие сети. Но и у неё есть недостатки — "мертвые нейроны", которые никогда не активируются. Отсюда появились модификации: Leaky ReLU, Parametric ReLU, ELU и другие.

Нейросети нейросети что это за?
Объясните популярно кто специалист зачем придумали нейросети что это такое вообще? Я узнал о...

Нейросетевое программирование
задача состоит в следующем: допустим есть 10 акций, в итоге на определенную сумму надо собрать...

Обучение нейросетей в С++
Обучаю 2 слойную нейронную сеть методом обратного распространения ошибки - на вход подается массив...

Нейросети
Слышал, что нейросети на самом деле моделятся программно. А как? Как делать классы сети и нейрона?...


Механизм автоматического дифференцирования в PyTorch



Одно из главных преимуществ PyTorch — это его система автоматического дифференцирования. Честно говоря, когда я впервые увидел, как легко вычисляются градиенты в PyTorch, я подумал, что это какая-то магия. Но на самом деле механизм основан на отслеживании вычислительного графа. Каждый тензор в PyTorch имеет атрибут .grad для хранения градиента и атрибут .requires_grad для указания, нужно ли вычислять этот градиент.

Python
1
2
3
4
x = torch.tensor([2.0], requires_grad=True)
y = x**2 + 3*x + 1
y.backward()  # Вычисляет dy/dx
print(x.grad)  # Выводит 2*x + 3 = 2*2 + 3 = 7
Под капотом PyTorch строит динамический вычислительный граф, где узлы — это операции (возведение в степень, умножение, сложение), а рёбра — это потоки данных между ними. Каждая операция знает, как вычислить свой градиент, и используя цепное правило, PyTorch автоматически собирает градиент для всего графа. Это позволяет нам сфокусироваться на архитектуре модели и бизнес-логике, не отвлекаясь на ручное вычисление производных — задачу, которая становится невыносимо сложной для реальных нейросетей с миллионами параметров.

Переобучение и недообучение: как найти баланс в архитектуре



Одна из самых сложных задач при проектировании нейросети — выбор правильной архитектуры. Слишком простая модель не сможет уловить все нюансы данных (недообучение), а слишком сложная начнет запоминать шум вместо паттернов (переобучение). Я сталкивался с этой проблемой бессчетное количество раз. Помню проект, где моя модель показывала 99% точности на обучающей выборке и всего 65% на тестовой. Класический случай переобучения!

Для борьбы с переобучением используют различные методы регуляризации:

1. L1/L2-регуляризация — добавление штрафа за большие веса.
2. Dropout — случайное "выключение" нейронов во время обучения.
3. Ранняя остановка — прекращение обучения, когда ошибка на валидационной выборке начинает расти.
4. Аугментация данных — искуственное увеличение обучающей выборки.

В PyTorch эти методы легко реализуются:

Python
1
2
3
4
5
# L2-регуляризация через weight_decay
optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=0.0001)
 
# Dropout слой
self.dropout = nn.Dropout(0.5)

Динамические вычислительные графы против статических: преимущества PyTorch



Исторически фреймворки глубокого обучения разделились на два лагеря: с статическими графами (TensorFlow до 2.0, Theano) и динамическими (PyTorch, DyNet). В статическом подходе граф вычислений определяется заранее, компилируется и только потом выполняется. В динамическом — граф строится на лету, во время выполнения. PyTorch выбрал динамический подход, что даёт ряд преимуществ:
1. Легче отлаживать код — вы можете использовать обычные отладчики Python (pdb, PyCharm debugger).
2. Более интуитивное поведение — код выполняется именно так, как написан.
3. Гибкость при разработке сложных архитектур — можно изменять сеть в зависимости от входных данных или промежуточных результатов.
Это особенно ценно для исследовательской работы. Когда я экспериментирую с новыми идеями, мне нужна возможность быстро менять структуру сети, запускать код строчка за строчкой и видеть промежуточные результаты. PyTorch делает это максимально удобным.

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Пример динамичности PyTorch - меняем архитектуру в зависимости от условий
def create_dynamic_layers(input_size, complexity_level):
    layers = [nn.Linear(input_size, 64), nn.ReLU()]
    
    # Динамически добавляем слои в зависимости от сложности задачи
    if complexity_level > 1:
        layers.extend([nn.Linear(64, 128), nn.ReLU()])
    if complexity_level > 2:
        layers.extend([nn.Linear(128, 256), nn.ReLU(), nn.Dropout(0.3)])
    
    output_size = 64 if complexity_level == 1 else 128 if complexity_level == 2 else 256
    layers.append(nn.Linear(output_size, 10))
    
    return nn.Sequential(*layers)
Статический подход, используемый в ранних версиях TensorFlow, имеет свои преимущества — например, оптимизация графа перед выполнением и эффективное развертывание на серверах. Но разработка моделей с ним часто превращается в головную боль. Не случайно TensorFlow 2.0 перешел на eager execution (жадное вычисление), по сути приблизившись к модели PyTorch.

Для меня интересно, что выбор между статическим и динамическим подходом в каком-то смысле отражает философское различие. Статический граф — это декларативный стиль: "вот что я хочу вычислить". Динамический — императивный: "вот как я хочу это вычислить". И, как обычно в программировании, нет правильного ответа — всё зависит от контекста.

Внутренняя архитектура нейросетей: слои, веса и топологии



Если вернуться к базовым составляющим нейросетей, то в PyTorch всё крутится вокруг модулей (nn.Module). Это основной строительный блок, из которого собираются сети любой сложности. Давайте разберемся, что скрывается за простым определением модуля:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyNetwork(nn.Module):
    def __init__(self):
        super(MyNetwork, self).__init__()
        self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1)
        self.pool = nn.MaxPool2d(2)
        self.fc1 = nn.Linear(16 * 14 * 14, 128)
        self.fc2 = nn.Linear(128, 10)
        
    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = self.pool(x)
        x = x.view(-1, 16 * 14 * 14)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return x
Что здесь происходит? В __init__ мы определяем слои и другие компоненты сети, а в forward — как данные будут проходить через эти компоненты. PyTorch автоматически отслеживает и сохраняет все параметры (веса и смещения), определенные внутри Module, что позволяет:

1. Легко переносить модель между CPU и GPU (model.to(device)).
2. Сохранять и загружать параметры (torch.save(model.state_dict(), path)).
3. Включать или исключать параметры из оптимизации.
4. Применять различные инициализации весов.

Здесь кроется еще одно преимущество PyTorch — модульность. Вы можете создавать сложные архитектуры, комбинируя и вкладывая модули друг в друга. Например, реализовать архитектуру ResNet с пропускными соединениями:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ResidualBlock(nn.Module):
    def __init__(self, in_channels, out_channels):
        super(ResidualBlock, self).__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1)
        
        # Пропускное соединение, если размерности не совпадают
        self.shortcut = nn.Sequential()
        if in_channels != out_channels:
            self.shortcut = nn.Conv2d(in_channels, out_channels, kernel_size=1)
    
    def forward(self, x):
        residual = x
        out = F.relu(self.conv1(x))
        out = self.conv2(out)
        out += self.shortcut(residual)  # Пропускное соединение
        out = F.relu(out)
        return out
Такая гибкость позволяет экспериментировать с самыми разными топологиями сетей — от простых последовательных до сложных графовых структур с множественными входами и выходами.

Инициализация параметров и её влияние на сходимость



В моей практике правильная инициализация весов иногда была тем фактором, который превращал неработающую модель в успешную. Представьте, что вы начинаете обучение с нулевыми весами — все нейроны будут выдавать одинаковые значения, а градиенты будут одинаковыми для всех весов одного слоя. Сеть не сможет "разорвать симметрию" и научиться чему-то полезному. PyTorch предлагает различные стратегии инициализации:

Python
1
2
3
4
5
6
7
8
# Инициализация весов из нормального распределения
nn.init.normal_(layer.weight, mean=0, std=0.01)
 
# Инициализация Ксавьера/Глорота
nn.init.xavier_uniform_(layer.weight)
 
# Инициализация Кайминга Хе
nn.init.kaiming_normal_(layer.weight, nonlinearity='relu')
Выбор конкретного метода зависит от используемой функции активации и глубины сети. Например, для ReLU чаще используют инициализацию Кайминга Хе, которая учитывает, что ReLU "обрезает" отрицательные значения, что влияет на дисперсию активаций.

Я заметил, что многие начинающие разработчики не уделяют инициализации должного внимания, полагая, что оптимизатор всё исправит. Но хорошая инициализация может значительно ускорить сходимость и помочь избежать проблем с затухающими или взрывными градиентами.

Современные архитектуры: от простых сетей к трансформерам



Эволюция архитектур нейронных сетей напоминает биологическую эволюцию — от простых организмов к сложным. Мы начали с простых многослойных перцептронов, перешли к сверточным сетям для обработки изображений и рекуррентным для последовательностей, а теперь работаем с трансформерами и моделями внимания, которые произвели революцию в обработке естественного языка и компьютерном зрении. PyTorch прекрасно подходит для реализации любых архитектур, от классических до самых современных. Он не навязывает определенную структуру, а предоставляет гибкие инструменты для воплощения ваших идей.

Лично я начинал с простых сетей, затем освоил CNN и RNN, а сейчас работаю с трансформерами. Каждый переход открывал новые возможности и подходы к решению задач. И во всех случаях PyTorch делал процесс разработки максимально комфортным благодаря своей интуитивности и гибкости.

Настройка окружения PyTorch и первые шаги



Любая работа с нейросетями начинается с настройки окружения — это как подготовка кухни перед приготовлением сложного блюда. Прежде чем мы сможем создать даже самую простую модель, нам предстоит установить PyTorch и сконфигурировать все сопутствующие компоненты. И поверьте моему опыту — правильная настройка с самого начала сэкономит вам десятки часов отладки в будущем.

Установка и конфигурация библиотеки



Существует несколько способов установить PyTorch, но лично я предпочитаю использовать pip, как самый универсальный метод:

Python
1
pip install torch torchvision torchaudio
Однако, стоит помнить, что для разных платформ и конфигураций команда может отличаться. Например, если вы хотите использовать GPU от NVIDIA, то команда будет иной. Я всегда рекомендую зайти на официальный сайт PyTorch, где есть интерактивный конструктор команды установки — вы просто выбираете свою операционную систему, пакетный менеджер, версию Python и тип ускорителя (CUDA).
Для рабочих проектов я предпочитаю использовать виртуальное окружение, чтобы изолировать зависимости:

Bash
1
2
3
python -m venv pytorch_env
source pytorch_env/bin/activate  # На Windows: pytorch_env\Scripts\activate
pip install torch torchvision torchaudio
После установки не забудьте проверить, что всё работает корректно:

Python
1
2
import torch
print(torch.__version__)
Если вы увидели версию — поздравляю, основная часть установки прошла успешно!

Создание базовой структуры проекта



За годы работы я выработал определенную структуру для проектов с PyTorch, которая хорошо масштабируется по мере усложнения. Вот как она выглядит:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
project_root/
  ├── data/            # Данные для обучения и тестирования
  ├── models/          # Определения моделей
  │   ├── __init__.py
  │   └── simple_nn.py
  ├── utils/           # Вспомогательные функции
  │   ├── __init__.py
  │   └── data_loader.py
  ├── configs/         # Конфигурационные файлы
  │   └── config.yaml
  ├── train.py         # Скрипт для обучения
  ├── evaluate.py      # Скрипт для оценки модели
  ├── inference.py     # Скрипт для использования обученной модели
  └── requirements.txt # Зависимости проекта
Такая структура может показаться избыточной для простых проектов, но поверьте, когда ваша нейронка разрастется до нескольких моделей с разными конфигурациями и предобработками данных, вы скажете мне спасибо. Отделение данных от кода и разбиение кода на логические модули сильно упрощает поддержку и расширение проекта.

Проверка совместимости GPU и настройка CUDA



Возможность использовать GPU критически важна для серьезной работы с нейросетями. На CPU обучение даже средней модели может занять дни вместо часов на GPU. Но настройка GPU-ускорения бывает... скажем так, не самой приятной частью процесса. Сначала проверим, видит ли PyTorch ваш GPU:

Python
1
2
3
import torch
print(torch.cuda.is_available())
print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else "GPU не доступен")
Если PyTorch не видит ваш GPU, хотя он физически присутсвует, возможно:
1. Не установлены драйверы NVIDIA
2. Не установлен CUDA Toolkit
3. Версия PyTorch не соответствует версии CUDA

Для максимальной производительности я рекомендую использовать последнюю версию драйверов и CUDA, совместимую с вашей версией PyTorch. Проверить совместимость можно на сайте PyTorch. После настройки CUDA работать с тензорами на GPU очень просто:

Python
1
2
3
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
x = torch.tensor([1.0, 2.0, 3.0])
x = x.to(device)  # Перемещаем тензор на GPU
Помните, что отладка кода на GPU может быть сложнее из-за ограниченного доступа к памяти устройства. Я часто разрабатываю модель на CPU, а потом переношу её на GPU для обучения.

Интеграция с Jupyter Notebook и настройка среды разработки



Jupyter Notebook (или его современный вариант JupyterLab) — мой любимый инструмент для экспериментов с нейросетями. Он позволяет комбинировать код, визуализации и текстовые пояснения в одном документе. Установка проста:

Bash
1
pip install jupyter
А запуск еще проще:

Bash
1
jupyter notebook
Для PyTorch есть несколько полезных расширений Jupyter, например, ipywidgets для интерактивных элементов управления и tensorboard для визуализации процесса обучения.

Bash
1
pip install ipywidgets tensorboard
Что касается полноценной среды разработки, мне нравится PyCharm с плагином для Python и поддержкой Jupyter. Visual Studio Code тоже отличный выбор, особенно с расширениями Python и Jupyter. Независимо от выбраной среды, я настоятельно рекомендую настроить линтер (например, flake8) и форматтер кода (black) — они помогут держать код чистым и соответствующим стандартам:

Bash
1
pip install flake8 black
Еще один совет из моей практики: создайте файл .env в корне проекта для хранения переменных окружения (пути к данным, API-ключи и т.д.) и используйте библиотеку python-dotenv для их загрузки. Это упростит перенос проекта между различными окружениями и убережет вас от случайной публикации чувствительных данных в репозитории.

Полностью настроив окружение, вы готовы перейти к самому интересному — созданию и обучению нейронных сетей! В следующей главе мы познакомимся с основной абстракцией PyTorch — тензорами, и научимся выполнять над ними различные операции.

Работа с тензорами и операции над многомерными массивами



Переходя от теории к практике, давайте разберемся с основным строительным блоком PyTorch — тензорами. Когда я впервые столкнулся с термином "тензор", он показался мне чем-то устрашающе сложным из высшей математики. На деле всё оказалось проще: тензор — это обобщение скаляров, векторов и матриц на произвольное количество измерений.

Основы тензорных вычислений и их математические принципы



В PyTorch тензор — это многомерный массив, очень похожий на NumPy массивы, но с дополнительными возможностями для работы на GPU и автоматического дифференцирования. Создать тензор можно разными способами:

Python
1
2
3
4
5
6
7
8
9
10
11
# Создание тензора из списка
x = torch.tensor([1, 2, 3])
 
# Создание тензора с нулями
zeros = torch.zeros(2, 3)  # Матрица 2x3 из нулей
 
# Создание тензора со случайными значениями
random_tensor = torch.rand(2, 3, 4)  # Трехмерный тензор 2x3x4
 
# Создание тензора с определённым шагом
range_tensor = torch.arange(0, 10, step=2)  # [0, 2, 4, 6, 8]
Что действительно важно понимать — тензоры не просто хранят данные, но и "помнят" свою вычислительную историю, если мы укажем requires_grad=True. Это ключевая особеность для автоматического дифференцирования.

Python
1
2
3
4
x = torch.tensor([2.0], requires_grad=True)
y = x**3 + 5*x
y.backward()
print(x.grad)  # Выведет 3*x^2 + 5 = 3*2^2 + 5 = 17
На практике мне не раз приходилось использовать тензоры разной размерности — от простых векторов для хранения весов нейрона до пятимерных тензоров для обработки видеопоследовательностей (время, канал, глубина, высота, ширина). PyTorch делает работу с ними удивительно интуитивной.

Математически тензоры следуют тем же правилам линейной алгебры, что и векторы с матрицами, но с большим количеством измерений. Основные операции включают:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Сложение и вычитание
a = torch.tensor([1, 2, 3])
b = torch.tensor([4, 5, 6])
c = a + b  # [5, 7, 9]
 
# Умножение на скаляр
d = a * 2  # [2, 4, 6]
 
# Скалярное произведение
dot_product = torch.dot(a, b)  # 1*4 + 2*5 + 3*6 = 32
 
# Матричное умножение
m1 = torch.tensor([[1, 2], [3, 4]])
m2 = torch.tensor([[5, 6], [7, 8]])
m3 = torch.matmul(m1, m2)  # или m1 @ m2 в Python 3.5+
При работе с тензорами я часто использую операции вещания (broadcasting), которые позволяют выполнять операции между тензорами разных размеров. Это весьма удобно и экономит память:

Python
1
2
3
4
# Broadcasting: добавление вектора к каждой строке матрицы
matrix = torch.tensor([[1, 2, 3], [4, 5, 6]])
vector = torch.tensor([1, 0, 1])
result = matrix + vector  # [[2, 2, 4], [5, 5, 7]]

Оптимизация тензорных операций для ускорения вычислений



В реальных проектах производительность часто становится узким местом. Я вспоминаю, как мучился с обработкой большого датасета изображений, пока не оптимизировал операции с тензорами. Вот некоторые приемы, которые я активно использую:

1. Векторизация вместо циклов. Операция над целым тензором всегда быстрее, чем поэлементные операции в цикле:

Python
1
2
3
4
5
6
7
8
# Медленно
result = torch.zeros(1000)
for i in range(1000):
    result[i] = torch.sin(torch.tensor(i * 0.01))
 
# Быстро
indices = torch.arange(1000)
result = torch.sin(indices * 0.01)
2. Предварительное выделение памяти. Создание новых тензоров в цикле вызывает лишние операции выделения памяти:

Python
1
2
3
4
5
6
7
8
9
10
# Неэффективно
for i in range(100):
    result = torch.zeros(10000)  # Каждый раз создаём новый тензор
    # Делаем что-то с result
 
# Эффективно
result = torch.zeros(10000)  # Создаём один раз
for i in range(100):
    result.zero_()  # Обнуляем существующий тензор
    # Делаем что-то с result
3. Использование встроенных операций вместо собственных реализаций. Встроенные функции обычно оптимизированы на уровне C++/CUDA:

Python
1
2
3
4
5
6
7
8
9
# Вместо этого:
def my_normalization(x):
    mean = x.mean(dim=1, keepdim=True)
    std = x.std(dim=1, keepdim=True)
    return (x - mean) / std
 
# Используйте это:
import torch.nn.functional as F
normalized = F.normalize(x, p=2, dim=1)
4. Конкатенация операций для уменьщения проходов по памяти:

Python
1
2
3
4
5
6
7
# Вместо последовательных операций:
x = x + 1
x = x * 2
x = torch.sqrt(x)
 
# Лучше использовать композицию функций:
x = torch.sqrt(2 * (x + 1))
Отдельная история — оптимизация под конкретное железо. Например, для NVIDIA GPU можно использовать cuDNN библиотеку, которая специально оптимизирована для сверточных и рекуррентных сетей:

Python
1
torch.backends.cudnn.benchmark = True  # Автоматически выбирает лучший алгоритм
Но будьте остарожны с этой настройкой — она хороша, когда размеры входных данных постоянны, но может замедлить работу, если они меняются от батча к батчу.

Перенос тензоров между CPU и GPU: практические аспекты



Эффективное использование GPU — это целое искуство. Неправильное управление памятью может привести к её утечкам или фрагментации, что снижает производительность и даже вызывает аварийное завершение программы. Базовые операции переноса тензоров между устройствами выглядят так:

Python
1
2
3
4
5
6
7
8
9
10
11
12
# Определяем устройство
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
 
# Создаём тензор на CPU и переносим на GPU
x_cpu = torch.tensor([1, 2, 3])
x_gpu = x_cpu.to(device)
 
# Или сразу создаём на нужном устройстве
y_gpu = torch.tensor([4, 5, 6], device=device)
 
# Возвращаем обратно на CPU для, например, вывода или сохранения
y_cpu = y_gpu.cpu()
На практике я заметил, что частые переносы данных между CPU и GPU могут стать узким местом из-за ограниченной пропускной способности шины PCIe. Поэтому я стараюсь минимизировать такие перемещения и делаю максимум вычислений на одном устройстве.

Еще один важный момент — освобождение памяти GPU. PyTorch использует сборщик мусора Python, но иногда этого недостаточно:

Python
1
2
3
# Явное освобождение памяти
del x_gpu
torch.cuda.empty_cache()
При работе с большими моделями (особенно в области компьютерного зрения или обработки естественного языка) управление памятью становится критичным. Техники вроде микробатчинга, градиентного накопления и освобождения промежуточных тензоров могут быть спасением:

Python
1
2
3
4
5
6
7
8
9
10
11
# Пример градиентного накопления
model.zero_grad()
for i, (inputs, targets) in enumerate(data_loader):
    inputs, targets = inputs.to(device), targets.to(device)
    outputs = model(inputs)
    loss = criterion(outputs, targets) / accumulation_steps
    loss.backward()
    
    if (i + 1) % accumulation_steps == 0:
        optimizer.step()
        model.zero_grad()
Такой подход позволяет работать с эффективно большими батчами, даже если физически они не помещаются в память GPU.

В реальных проэктах я почти всегда пишу обертки вокруг стандартных операций PyTorch, которые автоматически обрабатывают перенос данных на нужное устройство и проверяют валидность входных тензоров. Это избавляет от множества рутинных ошибок и делает код более надежным.

Создание многослойного перцептрона



Пришло время заняться самым интересным — созданием полноценной нейросети в PyTorch. Многослойный перцептрон (MLP) — это базовая архитектура нейронной сети, от которой можно оттолкнуться перед погружением в более сложные модели. Несмотря на кажущуюся простоту, эта архитектура способна решать удивительно широкий спектр задач — от классификации и регрессии до генерации данных.

Определение архитектуры сети



Прежде чем писать код, нужно определиться с архитектурой. Ключевые моменты, которые я всегда учитываю:

1. Размер входного и выходного слоя (зависит от задачи).
2. Количество скрытых слоев и нейронов в них.
3. Функции активации.
4. Способы регуляризации.

Для примера возьмем задачу классификации рукописных цифр из набора MNIST. У нас 28×28 пиксельные изображения (784 входных признака) и 10 классов на выходе. Обычно я начинаю с простой архитектуры и постепенно её усложняю, если необходимо:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MNISTClassifier(nn.Module):
    def __init__(self):
        super(MNISTClassifier, self).__init__()
        # Входной слой: 784 нейрона (28x28 пикселей)
        # Первый скрытый слой: 128 нейронов
        self.fc1 = nn.Linear(784, 128)
        # Второй скрытый слой: 64 нейрона
        self.fc2 = nn.Linear(128, 64)
        # Выходной слой: 10 нейронов (по одному на каждую цифру)
        self.fc3 = nn.Linear(64, 10)
        
    def forward(self, x):
        # Преобразуем входные данные из 2D в 1D
        x = x.view(-1, 784)
        # Прогоняем через первый слой и активируем
        x = F.relu(self.fc1(x))
        # Прогоняем через второй слой и активируем
        x = F.relu(self.fc2(x))
        # Выходной слой (без активации, она будет в функции потерь)
        x = self.fc3(x)
        return x
Обратите внимание, что в выходном слое я не применяю функцию активации — это потому, что буду использовать CrossEntropyLoss, которая уже включает в себя softmax-активацию.

Реализация слоев и связей между ними



В PyTorch слои могут быть соединены двумя основными способами:
1. Через прямое определение в методе forward, как в примере выше.
2. Через использование nn.Sequential для создания последовательности слоев.
Второй подход часто делает код более компактным и читаемым:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MNISTClassifierSequential(nn.Module):
    def __init__(self):
        super(MNISTClassifierSequential, self).__init__()
        self.flatten = nn.Flatten()
        self.model = nn.Sequential(
            nn.Linear(784, 128),
            nn.ReLU(),
            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Linear(64, 10)
        )
        
    def forward(self, x):
        x = self.flatten(x)
        return self.model(x)
Но первый подход даёт больше гибкости, если нужны разветвления или нестандартные соединения (что чато случается в сложных архитектурах).

Инициализация весов и смещений



По умолчанию PyTorch инициализирует веса из равномерного распределения, а смещения нулями. Но иногда стандартная инициализация не подходит. Я часто инициализирую веса с помощью метода Ксавьера или Кайминга:

Python
1
2
3
4
5
6
7
8
9
10
def init_weights(m):
    if isinstance(m, nn.Linear):
        # Инициализация Ксавьера для линейных слоев
        nn.init.xavier_uniform_(m.weight)
        # Инициализация смещений маленькими значениями
        nn.init.constant_(m.bias, 0.01)
 
# Применяем инициализацию ко всем слоям модели
model = MNISTClassifier()
model.apply(init_weights)
Правильная инициализация крайне важна для глубоких сетей — она помогает избежать затухающих или взрывных градиентов. Я на своём опыте узнал, что разные задачи и архитектуры могут требовать разных стратегий инициализации.

Техники регуляризации: Dropout, BatchNorm и Weight Decay



В реальных проектах переобучение — одна из главных проблем. Я использую несколько методов для борьбы с ним:

1. Dropout — случайное "выключение" нейронов во время обучения:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class RegularizedMLP(nn.Module):
    def __init__(self, dropout_rate=0.5):
        super(RegularizedMLP, self).__init__()
        self.fc1 = nn.Linear(784, 128)
        # Добавляем dropout после первого слоя
        self.dropout1 = nn.Dropout(dropout_rate)
        self.fc2 = nn.Linear(128, 64)
        self.dropout2 = nn.Dropout(dropout_rate)
        self.fc3 = nn.Linear(64, 10)
        
    def forward(self, x):
        x = x.view(-1, 784)
        x = F.relu(self.fc1(x))
        # Применяем dropout
        x = self.dropout1(x)
        x = F.relu(self.fc2(x))
        x = self.dropout2(x)
        x = self.fc3(x)
        return x
2. BatchNorm — нормализация активаций в каждом мини-батче:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class BatchNormMLP(nn.Module):
    def __init__(self):
        super(BatchNormMLP, self).__init__()
        self.fc1 = nn.Linear(784, 128)
        # Добавляем BatchNorm после первого слоя
        self.bn1 = nn.BatchNorm1d(128)
        self.fc2 = nn.Linear(128, 64)
        self.bn2 = nn.BatchNorm1d(64)
        self.fc3 = nn.Linear(64, 10)
        
    def forward(self, x):
        x = x.view(-1, 784)
        x = self.fc1(x)
        # Применяем BatchNorm перед активацией
        x = self.bn1(x)
        x = F.relu(x)
        x = self.fc2(x)
        x = self.bn2(x)
        x = F.relu(x)
        x = self.fc3(x)
        return x
3. Weight Decay — штраф за большие веса (L2-регуляризация):

Python
1
optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=0.0001)
Часто я комбинирую эти методы для максимального эффекта. Например, BatchNorm + Dropout (но важно помнить, что BatchNorm должен идти перед Dropout) и Weight Decay в оптимизаторе.

Однако стоит помнить, что регуляризация — это всегда компромис между недообучением и переобучением. Слишком сильная регуляризация может помешать модели выучить полезные паттерны в данных.

Процесс обучения: оптимизаторы, функции потерь и метрики



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

Выбор подходящего оптимизатора



Оптимизатор — это алгоритм, который корректирует веса сети на основе градиентов. PyTorch предлагает богатый выбор оптимизаторов, и их правильный подбор может радикально повлиять на качество и скорость обучения.
Стандартный стохастический градиентный спуск (SGD) — самый базовый вариант:

Python
1
optimizer = optim.SGD(model.parameters(), lr=0.01)
Он прост и понятен, но часто страдает от "застревания" в локальных минимумах и медленной сходимости. Поэтому на практике я чаще использую более продвинутые варианты:

1. Adam — мой личный фаворит для большинства задач, комбинирующий моментум и адаптивные скорости обучения:

Python
1
optimizer = optim.Adam(model.parameters(), lr=0.001, betas=(0.9, 0.999))
2. RMSprop — хорошо работает с рекуррентными сетями:

Python
1
optimizer = optim.RMSprop(model.parameters(), lr=0.001, alpha=0.99)
3. SGD с моментумом — ускоряет сходимость, добавляя "инерцию" к обновлениям весов:

Python
1
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
Важно понимать, что за каждым оптимизатором стоит своя математическая модель. Например, Adam (Adaptive Moment Estimation) использует оценки первого и второго моментов градиентов для адаптации скорости обучения для каждого параметра. Это особенно полезно, когда разные параметры требуют разных скоростей обучения.

В одном из моих проэктов я обнаружил интересную закономерность: для сверточных сетей SGD с моментумом часто давал лучшую финальную точность, хотя и сходился медленнее, чем Adam. Для трансформеров же Adam почти всегда оказывался лучшим выбором.

Функции потерь: математика ошибки



Функция потерь (loss function) количественно определяет, насколько предсказания модели отличаются от истинных значений. Выбор подходящей функции потерь критичен и зависит от типа решаемой задачи. Для задач классификации я обычно использую кросс-энтропию:

Python
1
criterion = nn.CrossEntropyLoss()
Эта функция сочетает softmax-активацию и вычисление отрицательного логарифма правдоподобия, что делает её идеальной для многоклассовой классификации. Для задач регрессии стандартный выбор — среднеквадратичная ошибка:

Python
1
criterion = nn.MSELoss()
Но иногда я экспериментирую с абсолютной ошибкой (L1Loss), особенно когда данные содержат выбросы, к которым MSE слишком чувствительна:

Python
1
criterion = nn.L1Loss()
Для более сложных задач, таких как сегментация изображений или обнаружение объектов, могут потребоваться специализированные функции потерь вроде Dice Loss или Focal Loss.
Важный момент, который я узнал из опыта: иногда имеет смысл комбинировать несколько функций потерь. Например, в задачах генерации изображений часто используют комбинацию перцептивной потери (для структурного сходства) и пиксельной потери (L1 или L2):

Python
1
combined_loss = perceptual_loss_weight * perceptual_loss + pixel_loss_weight * pixel_loss

Настройка скорости обучения



Скорость обучения (learning rate) — гиперпараметр, определяющий размер шага при обновлении весов. Слишком большая скорость может привести к расходимости (веса "улетают" в бесконечность), а слишком маленькая — к медленной сходимости или застреванию в локальных минимумах. Вместо фиксированной скорости обучения я почти всегда использую планировщики (schedulers), которые динамически меняют её в процессе тренировки:

Python
1
2
3
4
5
# Уменьшение скорости в 10 раз каждые 30 эпох
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
 
# Или более плавное экспоненциальное уменьшение
scheduler = optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.95)
Особенно эффективен планировщик с циклической скоростью обучения, который периодически повышает и понижает learning rate, помогая "выпрыгнуть" из локальных минимумов:

Python
1
2
3
4
scheduler = optim.lr_scheduler.CyclicLR(optimizer, 
                                         base_lr=0.001, 
                                         max_lr=0.1, 
                                         step_size_up=2000)
Лично для меня настройка скорости обучения всегда была больше искуством, чем наукой. Я часто использую метод "One Cycle Policy", предложенный Лесли Смитом, который сначала увеличивает скорость, а затем плавно уменьшает её:

Python
1
2
3
scheduler = optim.lr_scheduler.OneCycleLR(optimizer, 
                                           max_lr=0.1, 
                                           total_steps=num_epochs * len(data_loader))

Мониторинг прогресса тренировки



Без эффективного мониторинга обучения вы буквально "летите вслепую". Я отслеживаю несколько ключевых метрик, которые дают полную картину происходящего:

1. Потери на обучающей и валидационной выборках — первый индикатор переобучения, если валидационные потери растут, а обучающие продолжают падать.
2. Метрики качества модели — зависят от задачи (точность для классификации, IoU для сегментации и т.д.).
3. Грдиенты — их норма помогает выявить проблемы с исчезающими или взрывными градиентами.

Для визуализации этих метрик я чаще всего использую TensorBoard, которая отлично интегрируется с PyTorch:

Python
1
2
3
4
5
6
7
8
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter('runs/experiment_1')
 
# В процессе обучения логируем метрики
writer.add_scalar('Loss/train', train_loss, global_step=epoch)
writer.add_scalar('Loss/validation', val_loss, global_step=epoch)
writer.add_scalar('Accuracy/train', train_acc, global_step=epoch)
writer.add_scalar('Accuracy/validation', val_acc, global_step=epoch)
TensorBoard также позволяет визуализировать архитектуру сети, распределение весов и градиентов, что бывает крайне полезно при отладке.
Еще один практический прием, который я использую — ранняя остановка (early stopping). Это просто спасение от переобучения:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
best_val_loss = float('inf')
patience = 10
counter = 0
 
for epoch in range(num_epochs):
  train_loss = train_epoch(model, train_loader, optimizer, criterion)
  val_loss = validate_epoch(model, val_loader, criterion)
  
  if val_loss < best_val_loss:
      best_val_loss = val_loss
      torch.save(model.state_dict(), 'best_model.pth')
      counter = 0
  else:
      counter += 1
      if counter >= patience:
          print(f"Early stopping at epoch {epoch}")
          break
Этот простой механизм останавливает обучение, когда валидационная ошибка перестает улучшаться в течение заданного количества эпох, и сохраняет лучшую версию модели.

Отладка и оптимизация производительности нейросети



Знаете, что отличает профессионала от новичка в области глубокого обучения? Не объем знаний о последних архитектурах и не умение писать заумные формулы. Ключевое отличие — способность эффективно отлаживать и оптимизировать нейросети. Я потратил бесчисленные часы, копаясь в непонятных ошибках и разбираясь с утечками памяти, и могу с уверенностью сказать: даже самая элегантная архитектура бесполезна, если вы не можете заставить её работать стабильно и быстро.

Распространенные ошибки при создании нейросетей и способы их устранения



Большинство проблем с нейросетями сводится к нескольким типичным ошибкам, которые я встречаю снова и снова.

Проблемы с размерностями тензоров



"Expected input of shape [64, 3, 224, 224], got [64, 224, 224, 3]" — как часто я видел подобные сообщения! PyTorch ожидает тензоры в формате [батч, каналы, высота, ширина], а не [батч, высота, ширина, каналы], как в NumPy или TensorFlow.
Для диагностики таких проблем я вставляю print с размерами тензоров в ключевых точках:

Python
1
2
3
print(f"Shape after conv1: {x.shape}")
x = self.conv1(x)
print(f"Shape after conv2: {x.shape}")
Лучший способ предотвратить ошибки с размерностями — явно проверять их:

Python
1
2
3
4
5
6
def forward(self, x):
  # Проверка входной размерности
  assert x.shape[1] == 3, f"Expected 3 channels, got {x.shape[1]}"
  assert x.shape[2] == x.shape[3], f"Expected square image, got {x.shape[2]}x{x.shape[3]}"
  
  # Дальнейшие вычисления...

Проблемы с градиентами



Исчезающие или взрывные градиенты — частая проблема в глубоких сетях. Симптомы: потери не меняются, точность застревает, или модель выдает NaN. Я использую простой трюк для проверки градиентов:

Python
1
2
3
for name, param in model.named_parameters():
  if param.requires_grad:
      print(f"{name}: {param.grad.norm().item() if param.grad is not None else 'None'}")
Если градиенты слишком малы (<1e-7) или слишком большие (>1e3), скорее всего, у вас проблемы. Решения:

1. Для исчезающих градиентов — замените функции активации (tanh или sigmoid на ReLU), используйте BatchNorm или ResNet-подобные пропускные соединения.
2. Для взрывных градиентов — примените градиентное отсечение:

Python
1
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

Утечки памяти



Самая коварная проблема, с которой я боролся — утечки памяти на GPU. PyTorch обычно хорошо управляет памятью, но иногда случаются неприятности.
Одна из распространенных причин — хранение тензоров в Python-структурах данных вне цикла обучения:

Python
1
2
3
4
5
# Неправильно - потенциальная утечка
all_outputs = []
for batch in dataloader:
  outputs = model(batch)
  all_outputs.append(outputs)  # Накапливаем тензоры в списке
Вместо этого лучше использовать .detach().cpu() для тензоров, которые нужно сохранить:

Python
1
2
3
4
5
# Правильно
all_outputs = []
for batch in dataloader:
  outputs = model(batch)
  all_outputs.append(outputs.detach().cpu())  # Отделяем от вычислительного графа и переносим на CPU

Профилирование памяти и вычислительных ресурсов



Чтобы оптимизировать, сначала нужно измерить. PyTorch предлагает несколько инструментов для профилирования.

Профилирование памяти



Я часто использую torch.cuda.memory_summary() для получения общей картины использования памяти:

Python
1
print(torch.cuda.memory_summary())
Для более детального анализа есть инструмент torch.cuda.memory_stats(), который показывает выделение и освобождение блоков памяти. Когда я сталкиваюсь с особенно упрямыми утечками, перехожу к тяжелой артиллерии — профилировщику PyTorch:

Python
1
2
3
4
5
6
7
8
with torch.profiler.profile(
  activities=[torch.profiler.ProfilerActivity.CPU, torch.profiler.ProfilerActivity.CUDA],
  profile_memory=True,
  record_shapes=True
) as prof:
  model(inputs)
 
print(prof.key_averages().table(sort_by="cuda_memory_usage", row_limit=10))

Оптимизация производительности



После выявления узких мест можно применить несколько мощных оптимизаций:

1. Смешанная точность (mixed precision) — использование 16-битных чисел вместо 32-битных может ускорить обучение в 2-3 раза при минимальной потере точности:

Python
1
2
3
4
5
6
7
8
9
10
11
12
from torch.cuda.amp import autocast, GradScaler
 
scaler = GradScaler()
 
for batch in dataloader:
  with autocast():
      outputs = model(batch)
      loss = criterion(outputs, targets)
      
  scaler.scale(loss).backward()
  scaler.step(optimizer)
  scaler.update()
2. Оптимизация загрузки данных — часто бутылочным горлышком становится не сама модель, а процесс загрузки данных. Увеличьте num_workers в DataLoader и используйте пин-память:

Python
1
dataloader = DataLoader(dataset, batch_size=32, num_workers=4, pin_memory=True)
3. Вычислительные оптимизации — кастомизация CUDA-ядер и использование библиотеки NVIDIA Apex для дальнейшей оптимизации.

Я однажды работал над проэктом, где простая замена for-циклов на векторизованные операции и применение смешанной точности ускорили обучение почти в 5 раз. Такие оптимизации особенно важны, когда вы работаете с огромными моделями на ограниченных ресурсах.

Никогда не оптимизируйте вслепую — всегда измеряйте производительность до и после изменений. Иногда интуитивно "быстрые" решения на деле оказываются медленнее из-за сложных взаимодействий между уровнями абстракции. Доверяйте данным профилирования, а не интуиции.

Пример: классификация изображений CIFAR-10



Соберем всё, что мы обсуждали, в один цельный пример и создадим сверточную нейронную сеть для классификации изображений из датасета CIFAR-10.

CIFAR-10 — это набор из 60 000 цветных изображений размером 32×32 пикселя, разделенных на 10 классов (самолеты, автомобили, птицы, кошки, олени, собаки, лягушки, лошади, корабли и грузовики). Это стандартный датасет для тестирования алгоритмов компьютерного зрения, нечто вроде "Hello, World!" в мире классификации изображений.

Начнем с импорта необходимых библиотек и настройки устройства для вычислений:

Python
1
2
3
4
5
6
7
8
9
10
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
 
# Определяем устройство
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Используем устройство: {device}")
Теперь подготовим данные. CIFAR-10 требует некоторой предобработки — нормализации и аугментации:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Преобразования для обучающей выборки (с аугментацией)
transform_train = transforms.Compose([
    transforms.RandomCrop(32, padding=4),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616))
])
 
# Преобразования для тестовой выборки (только нормализация)
transform_test = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616))
])
 
# Загружаем данные
batch_size = 128
 
train_dataset = torchvision.datasets.CIFAR10(
    root='./data', train=True, download=True, transform=transform_train)
train_loader = DataLoader(
    train_dataset, batch_size=batch_size, shuffle=True, num_workers=2)
 
test_dataset = torchvision.datasets.CIFAR10(
    root='./data', train=False, download=True, transform=transform_test)
test_loader = DataLoader(
    test_dataset, batch_size=batch_size, shuffle=False, num_workers=2)
 
# Классы в CIFAR-10
classes = ('самолет', 'автомобиль', 'птица', 'кошка', 'олень', 
           'собака', 'лягушка', 'лошадь', 'корабль', 'грузовик')
Теперь определим архитектуру сети. Я остановлюсь на относительно простой сверточной сети с несколькими сверточными слоями, за которыми следуют полносвязные:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class CifarCNN(nn.Module):
    def __init__(self):
        super(CifarCNN, self).__init__()
        self.features = nn.Sequential(
            # Первый сверточный блок
            nn.Conv2d(3, 32, kernel_size=3, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(inplace=True),
            nn.Conv2d(32, 32, kernel_size=3, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.Dropout(0.25),
            
            # Второй сверточный блок
            nn.Conv2d(32, 64, kernel_size=3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.Conv2d(64, 64, kernel_size=3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.Dropout(0.25)
        )
        
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(64 * 8 * 8, 512),
            nn.BatchNorm1d(512),
            nn.ReLU(inplace=True),
            nn.Dropout(0.5),
            nn.Linear(512, 10)
        )
        
    def forward(self, x):
        x = self.features(x)
        x = self.classifier(x)
        return x
 
# Создаем модель и переносим на устройство
model = CifarCNN().to(device)
Определим функцию потерь, оптимизатор и планировщик скорости обучения:

Python
1
2
3
4
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-5)
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
    optimizer, mode='max', factor=0.5, patience=5, verbose=True)
Теперь напишем функции для обучения и тестирования:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def train_epoch(model, loader, optimizer, criterion, device):
    model.train()
    running_loss = 0.0
    correct = 0
    total = 0
    
    for inputs, targets in loader:
        inputs, targets = inputs.to(device), targets.to(device)
        
        optimizer.zero_grad()
        outputs = model(inputs)
        loss = criterion(outputs, targets)
        loss.backward()
        optimizer.step()
        
        running_loss += loss.item() * inputs.size(0)
        _, predicted = outputs.max(1)
        total += targets.size(0)
        correct += predicted.eq(targets).sum().item()
    
    return running_loss / total, 100. * correct / total
 
def test(model, loader, criterion, device):
    model.eval()
    running_loss = 0.0
    correct = 0
    total = 0
    
    with torch.no_grad():
        for inputs, targets in loader:
            inputs, targets = inputs.to(device), targets.to(device)
            outputs = model(inputs)
            loss = criterion(outputs, targets)
            
            running_loss += loss.item() * inputs.size(0)
            _, predicted = outputs.max(1)
            total += targets.size(0)
            correct += predicted.eq(targets).sum().item()
    
    return running_loss / total, 100. * correct / total
И наконец, обучаем модель:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
num_epochs = 50
best_acc = 0.0
 
for epoch in range(num_epochs):
    train_loss, train_acc = train_epoch(
        model, train_loader, optimizer, criterion, device)
    test_loss, test_acc = test(model, test_loader, criterion, device)
    
    # Обновляем планировщик
    scheduler.step(test_acc)
    
    # Сохраняем лучшую модель
    if test_acc > best_acc:
        best_acc = test_acc
        torch.save(model.state_dict(), 'cifar10_best.pth')
    
    print(f'Эпоха {epoch+1}/{num_epochs}, Потери: {train_loss:.4f}, Точность: {train_acc:.2f}%, '
          f'Тест потери: {test_loss:.4f}, Тест точность: {test_acc:.2f}%')
 
print(f'Лучшая точность на тесте: {best_acc:.2f}%')
При запуске на моем ноутбуке с GeForce GTX 1650 эта сеть достигает примерно 87-89% точности на тестовой выборке после 50 эпох. Не плохо для такой относительно простой архитектуры!

Для более высокой точности я бы рекомендовал использовать предобученные модели вроде ResNet или EfficientNet, доступные через torchvision.models, и применить трансферное обучение. Но даже этот пример демонстрирует всю мощь и простоту PyTorch для решения реальных задач.

Сохранение и загрузка обученной модели для продакшн-использования



После долгих часов обучения и оптимизации модели наступает момент, когда нужно отправить её в "реальный мир". Здесь многие новички допускают критические ошибки, сохраняя модель неправильным образом или не учитывая особености промышленной эксплуатации. В PyTorch есть два основных способа сохранения модели. Первый — сохранение всей модели целиком:

Python
1
2
3
4
5
6
# Сохранение полной модели
torch.save(model, 'full_model.pth')
 
# Загрузка
loaded_model = torch.load('full_model.pth')
loaded_model.eval()  # Переключаем в режим инференса
Этот способ прост, но имеет серьезные недостатки. Сохраняется вся Python-структура класса, что делает модель зависимой от конкретной версии кода. Если вы измените определение класса, загрузка может сломаться. Поэтому я предпочитаю второй подход — сохранение только словаря состояния (state_dict):

Python
1
2
3
4
5
6
7
# Сохранение только весов
torch.save(model.state_dict(), 'model_weights.pth')
 
# Загрузка
model = YourModelClass()  # Сначала создаем экземпляр модели
model.load_state_dict(torch.load('model_weights.pth'))
model.eval()
Для продакшн я часто применяю квантизацию — снижение точности весов с float32 до int8, что ускоряет инференс в несколько раз:

Python
1
2
3
4
# Квантизация модели
quantized_model = torch.quantization.quantize_dynamic(
    model, {nn.Linear}, dtype=torch.qint8
)
Еще один важный аспект — платформенная независимость. Для развертывания на различных устройствах я использую ONNX:

Python
1
2
3
4
dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(model, dummy_input, "model.onnx", 
                 export_params=True, 
                 opset_version=11)
Для промышленного развертывания TorchServe — отличный инструмент, который упрощает создание API для вашей модели. А на мобильных устройствах я рекомендую использовать TorchScript или ML Kit.

И последний совет из моего опыта: всегда проверяйте сохраненную модель перед отправкой в продакшн. Не раз я сталкивался с ситуацией, когда локально всё работало отлично, а в проде выдавало странные результаты из-за разницы в препроцесинге или версиях библиотек.

Библиотека FANN: нейросети
Добрый день. Недавно начал заниматься нейросетями. Нашел исходники библиотеки FANN . Разобрался с...

Нейросетевое распознавание изображений
Всем добрый день! Есть у меня курсовой проект &quot;Нейросетевое распознавание пола человека по...

Работа с нейросетью
Доброго веремени суток! Хочу создать на базе библиотек FANN сеть, такую чтоб она закидала новые...

Простая нейросеть
Привет всем! Есть задача: Научить нейросеть ставить диагноз. 1 диагноз - 1 сеть. Сеть 3 слоя:...

Распознавание символов с помощью нейросети
Здравствуйте! у меня дипломная работа, так называемая &quot;Распознавание символов с помощью нейросети&quot;....

Посоветуйте хорошую книжку по нейросетям
Посоветуйте хорошую книжку по нейросетям или ссылку

Как распознать изображения с помощью нейросети
Всем привет. Вот сижу и пытаюсь понять как распознать изображения с помощью нейросети. Мне нужно...

Методы распознавания текстов с изображения (обработка, распознавание пробела) при использовании Нейросетей
как распознать пробел в изображении и такие буквы как &quot;ы&quot; так как если просто резать изображение на...

Построить нейросеть, которая моделирует последовательное соединение резисторов
Доброго времени суток! Пришлось мне на практике столкнуться с новой и непонятной мне логической...

Обучение нейросети
Честно скажу, долго думал в какую ветку писать вопрос, но решил сюда. Суть проста: написал...

как написать простейшую нейросеть на C++?
как написать простейшую нейросеть на C++ visual studio 2010??? нашол токо исходники на С# но я его...

Нейросеть на FANN ошибка unresolved external '_fann_run' referenced from почему?
#include &lt;fann.h&gt; #include &lt;conio.h&gt; #include &lt;iostream&gt; using namespace std; int main() {...

Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 1
Комментарии
  1. Старый комментарий
    Аватар для Catstail
    Отличная статья! Спасибо!
    Запись от Catstail размещена 24.06.2025 в 11:27 Catstail вне форума
 
Новые блоги и статьи
Раскрываем внутренние механики Android с помощью контекста и манифеста
mobDevWorks 07.07.2025
Каждый Android-разработчик сталкивается с Context и манифестом буквально в первый день работы. Но много ли мы задумываемся о том, что скрывается за этими обыденными элементами? Я, честно говоря,. . .
API на базе FastAPI с Python за пару минут
AI_Generated 07.07.2025
FastAPI - это относительно молодой фреймворк для создания веб-API, который за короткое время заработал бешеную популярность в Python-сообществе. И не зря. Я помню, как впервые запустил приложение на. . .
Основы WebGL. Раскрашивание вершин с помощью VBO
8Observer8 05.07.2025
На русском https:/ / vkvideo. ru/ video-231374465_456239020 На английском https:/ / www. youtube. com/ watch?v=oskqtCrWns0 Исходники примера:
Мониторинг микросервисов с OpenTelemetry в Kubernetes
Mr. Docker 04.07.2025
Проблема наблюдаемости (observability) в Kubernetes - это не просто вопрос сбора логов или метрик. Это целый комплекс вызовов, которые возникают из-за самой природы контейнеризации и оркестрации. К. . .
Проблемы с Kotlin и Wasm при создании игры
GameUnited 03.07.2025
В современном мире разработки игр выбор технологии - это зачастую балансирование между удобством разработки, переносимостью и производительностью. Когда я решил создать свою первую веб-игру, мой. . .
Создаем микросервисы с Go и Kubernetes
golander 02.07.2025
Когда я только начинал с микросервисами, все спорили о том, какой язык юзать. Сейчас Go (или Golang) фактически захватил эту нишу. И вот почему этот язык настолько заходит для этих задач: . . .
C++23, квантовые вычисления и взаимодействие с Q#
bytestream 02.07.2025
Я всегда с некоторым скептицизмом относился к громким заявлениям о революциях в IT, но квантовые вычисления - это тот случай, когда революция действительно происходит прямо у нас на глазах. Последние. . .
Вот в чем сила LM.
Hrethgir 02.07.2025
как на английском будет “обслуживание“ Слово «обслуживание» на английском языке может переводиться несколькими способами в зависимости от контекста: * **Service** — самый распространённый. . .
Использование Keycloak со Spring Boot и интеграция Identity Provider
Javaican 01.07.2025
Два года назад я получил задачу, которая сначала показалась тривиальной: интегрировать корпоративную аутентификацию в микросервисную архитектуру. На тот момент у нас было семь Spring Boot приложений,. . .
Содержание темы с примерами на WebGL
8Observer8 01.07.2025
Все примеры из книги Мацуды и Ли в песочнице JSFiddle Пример выводит точку красного цвета размером 10 пикселей на WebGL 1. 0 и 2. 0 WebGL 1. 0. Передача координаты точки из главной программы в. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru