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

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

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

Нажмите на изображение для увеличения
Название: Создание нейросети с PyTorch.jpg
Просмотров: 461
Размер:	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 вне форума
 
Новые блоги и статьи
Запрет дублирования строк в табличной части
Maks 13.09.2026
Реализация из решения ниже выполнена на нетиповом справочнике "Нормы ТО" с табличной часть "Виды ТО", разработанного в КА2, со следующими реквизитами: - ВидТО (СправочникСсылка. ВидыТО); - ВидГСМ. . .
Скрипты Tampermonkey для CyberForum, ChatGPT, Claude и пр.
Jin X 06.09.2026
Скрипты Tampermonkey для CyberForum, ChatGPT, Claude и пр. Работая с форумом и нейросетями в браузере часто хочется что-то подкорректировать или добавить какого-то функционала. Ниже прикреплён. . .
Программа опроса у.з. расходомера SLS-720F
Argus19 02.09.2026
Программа опроса у. з. расходомера SLS-720F Программа опрашивает один раз в минуту три ультразвуковых расходомера SLS-720F через интерфейс RS-485 по протоколу Modbus RTU. Опрашиваются регистры. . .
Hyper-V: Компьютер должен поддерживать доверенный платформенный модуль 2.0.
Maks 31.08.2026
При установке Windows 11 на виртуальную машину Hyper-V 2-го поколения вылезла такая ошибка: Решение: в параметрах виртуальной машины, в разделе "Безопасность" (Security) активировать флаг. . .
Архитектура биовида Стива в Майнкрафте: Зачем бонобо кубический каннибализм
anaschu 30.08.2026
Кубический Вагинокапитализм в Minecraft: Математический инвариант ОДУ и рок Стивов-бонобо Главная задача разработанной «Модели Всего» — наглядно продемонстрировать наличие системной «судьбы». . .
Оттачиваю умение писать js программы.
russiannick 30.08.2026
Проектом выходного дня стало написание Книги шифров Виженера. Итогом стала версия 200, синий туман. Синий туман назван так, потому что замораживает текст под собой. Нажатие синих кнопок управляют. . .
мат медиц модель 30. презентация проекта
anaschu 27.08.2026
хоп хоп хоп хидахоп, а я кладую))
Как у меня протекала болезнь
zorxor 27.08.2026
Здравствуйте, друзья! Эта запись блога предназначена именно для вас - для моих дорогих друзей, которые знали меня лично. Чтобы ответить на вопрос - а что же со мной произошло на самом деле? Я учился. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru