Читать книгу «Нейросети: создание и оптимизация будущего» онлайн полностью📖 — Джеймса Девиса — MyBook.
image


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

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

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

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

1. Затухание градиентов (Vanishing Gradients):

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

2. Взрыв градиентов (Exploding Gradients):

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

Для предотвращения этих проблем используются несколько методов:

– Нормализация (например, Batch Normalization):

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

– Инициализация весов (например, He и Xavier):

Инициализация весов с учетом распределения значений помогает предотвратить как затухание, так и взрыв градиентов. Например, метод инициализации Xavier подходит для сигмоидных и гиперболических активаций, а He – для ReLU.

– Использование регуляризирующих методов (например, Dropout):

Dropout помогает избежать переобучения, уменьшая шансы на взрыв градиентов за счёт разреживания слоев, что также увеличивает устойчивость сети.

– Сокращение длины траектории ошибки (например, Gradient Clipping):

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

Рассмотрим эти методы на практических примерах.

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

```python

import torch

import torch.nn as nn

import torch.optim as optim

# Примерный класс нейронной сети с использованием Batch Normalization

class SimpleNet(nn.Module):

def __init__(self):

super(SimpleNet, self).__init__()

self.layer1 = nn.Linear(784, 256) # Первый полносвязный слой

self.bn1 = nn.BatchNorm1d(256) # Batch Normalization после первого слоя

self.layer2 = nn.Linear(256, 128) # Второй полносвязный слой

self.bn2 = nn.BatchNorm1d(128) # Batch Normalization после второго слоя

self.layer3 = nn.Linear(128, 10) # Выходной слой (10 классов, например, для MNIST)

def forward(self, x):

x = self.layer1(x)

x = self.bn1(x) # Применение Batch Normalization

x = torch.relu(x) # Активация ReLU

x = self.layer2(x)

x = self.bn2(x) # Применение Batch Normalization

x = torch.relu(x) # Активация ReLU

x = self.layer3(x) # Применение финального линейного слоя

return x

# Пример данных и оптимизации

model = SimpleNet()

criterion = nn.CrossEntropyLoss() # Функция потерь для классификации

optimizer = optim.Adam(model.parameters(), lr=0.001) # Оптимизатор Adam

# Пример одного шага обучения

inputs = torch.randn(64, 784) # Входной батч из 64 изображений размером 28x28 (784 = 28*28)

labels = torch.randint(0, 10, (64,)) # Случайные метки классов для примера

# Обнуление градиентов

optimizer.zero_grad()

# Прямой проход

outputs = model(inputs)

loss = criterion(outputs, labels)

# Обратное распространение и обновление весов

loss.backward()

optimizer.step()

print("Значение функции потерь:", loss.item())

```

Объяснение работы Batch Normalization в коде

– `nn.BatchNorm1d(256)` и `nn.BatchNorm1d(128)` добавлены после каждого линейного слоя. Они нормализуют выходы, уменьшая разброс значений и стабилизируя обратное распространение.

– Batch Normalization вычитает среднее и делит на стандартное отклонение для каждого батча, обеспечивая равномерное распределение значений. После этого применяются параметры смещения и масштабирования, которые обучаются вместе с остальными параметрами сети.

– Использование нормализации особенно полезно в случае глубоких сетей, так как она позволяет сократить время обучения и уменьшить зависимость от инициализации весов.

Инициализация весов с использованием методов Xavier и He помогает улучшить процесс обучения нейронных сетей, особенно глубоких, за счет предотвращения проблем с затуханием или взрывом градиентов. В PyTorch это можно сделать с помощью функций из модуля `torch.nn.init`.

– Инициализация Xavier (также известная как Glorot) работает лучше всего с активациями, которые сохраняют значения в пределах (-1, 1), как, например, сигмоид или гиперболический тангенс.

– Инициализация He (также известная как Kaiming) лучше подходит для активаций ReLU и производных от нее функций, так как она помогает компенсировать тенденцию ReLU обнулять градиенты.

Ниже приведен пример нейронной сети, где используется оба подхода к инициализации:

```python

import torch

import torch.nn as nn

import torch.optim as optim

# Определяем класс нейронной сети

class SimpleNet(nn.Module):

def __init__(self):

super(SimpleNet, self).__init__()

# Определяем слои сети

self.layer1 = nn.Linear(784, 256) # Первый полносвязный слой

self.layer2 = nn.Linear(256, 128) # Второй полносвязный слой

self.layer3 = nn.Linear(128, 10) # Выходной слой (например, для 10 классов)

# Применяем инициализацию весов

self._initialize_weights()

def _initialize_weights(self):

# Инициализация первого и второго слоя методом He для ReLU активации

nn.init.kaiming_normal_(self.layer1.weight, nonlinearity='relu')

nn.init.kaiming_normal_(self.layer2.weight, nonlinearity='relu')

# Инициализация выходного слоя методом Xavier, подходящим для softmax или других линейных активаций

nn.init.xavier_normal_(self.layer3.weight)

def forward(self, x):

x = torch.relu(self.layer1(x)) # Применение ReLU после первого слоя

x = torch.relu(self.layer2(x)) # Применение ReLU после второго слоя

x = self.layer3(x) # Прямой выход для классификации (например, softmax на последнем слое)

return x

# Пример данных и оптимизации

model = SimpleNet()

criterion = nn.CrossEntropyLoss() # Функция потерь для классификации

optimizer = optim.Adam(model.parameters(), lr=0.001) # Оптимизатор Adam

# Пример одного шага обучения

inputs = torch.randn(64, 784) # Входной батч из 64 изображений размером 28x28

labels = torch.randint(0, 10, (64,)) # Случайные метки классов для примера

# Обнуление градиентов

optimizer.zero_grad()

# Прямой проход

outputs = model(inputs)

loss = criterion(outputs, labels)

# Обратное распространение и обновление весов

loss.backward()

optimizer.step()

print("Значение функции потерь:", loss.item())

```

Объяснение кода:

1. Метод He (`nn.init.kaiming_normal_`): Применяется к `layer1` и `layer2`, которые используют активацию ReLU. Эта инициализация выбирает веса из нормального распределения, масштабируя их так, чтобы среднее значение градиентов оставалось примерно постоянным по всей глубине сети.

2. Метод Xavier (`nn.init.xavier_normal_`): Применяется к `layer3`, который может завершать сеть и чаще всего используется с линейной активацией или softmax. Эта инициализация помогает сохранить градиенты в пределах разумных значений для функций с симметричным распределением вокруг нуля, таких как сигмоид или tanh.

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

```python

import torch

import torch.nn as nn

import torch.optim as optim

# Определяем класс нейронной сети с Dropout

class DropoutNet(nn.Module):

def __init__(self, dropout_prob=0.5): # dropout_prob задаёт вероятность выключения нейрона

super(DropoutNet, self).__init__()

self.layer1 = nn.Linear(784, 256) # Первый полносвязный слой

self.dropout1 = nn.Dropout(dropout_prob) # Dropout после первого слоя

self.layer2 = nn.Linear(256, 128) # Второй полносвязный слой

self.dropout2 = nn.Dropout(dropout_prob) # Dropout после второго слоя

self.layer3 = nn.Linear(128, 10) # Выходной слой для 10 классов

def forward(self, x):

x = torch.relu(self.layer1(x))

x = self.dropout1(x) # Применение Dropout после активации

x = torch.relu(self.layer2(x))

x = self.dropout2(x) # Применение Dropout после активации

x = self.layer3(x) # Прямой выход

return x

# Пример создания модели и обучения

model = DropoutNet(dropout_prob=0.5) # Создаем модель с Dropout 50%

criterion = nn.CrossEntropyLoss() # Функция потерь для классификации

optimizer = optim.Adam(model.parameters(), lr=0.001) # Оптимизатор Adam

# Пример одного шага обучения

inputs = torch.randn(64, 784) # Входной батч из 64 изображений размером 28x28

labels = torch.randint(0, 10, (64,)) # Случайные метки классов

# Обнуление градиентов

optimizer.zero_grad()

# Прямой проход

outputs = model(inputs)

loss = criterion(outputs, labels)

# Обратное распространение и обновление весов

loss.backward()

optimizer.step()

print("Значение функции потерь:", loss.item())

```

Объяснение кода:

1. Dropout Layers:

`self.dropout1` и `self.dropout2` – слои Dropout, которые добавляются после каждого скрытого слоя. Значение `dropout_prob=0.5` означает, что в каждом проходе по данным будет отключаться 50% нейронов в каждом из этих слоев.

2. Dropout в обучении и оценке:

Dropout активен только во время обучения, при вызове `model.train()`. В режиме тестирования (`model.eval()`), Dropout отключается, и все нейроны остаются активными, чтобы обеспечить полную производительность модели.

3. Регуляризация:

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

Gradient Clipping – это метод, который ограничивает значения градиентов, предотвращая их чрезмерное увеличение. Этот подход особенно полезен для рекуррентных нейронных сетей (RNN), где градиенты могут быстро расти при распространении ошибки по временной оси, что приводит к взрыву градиентов и нестабильному обучению.

Ниже приведен пример кода в PyTorch, демонстрирующий использование Gradient Clipping:

```python

import torch

import torch.nn as nn

import torch.optim as optim

# Пример класса RNN с использованием Gradient Clipping

class SimpleRNN(nn.Module):

def __init__(self, input_size, hidden_size, output_size):

super(SimpleRNN, self).__init__()

self.hidden_size = hidden_size

self.rnn = nn.RNN(input_size, hidden_size, batch_first=True) # Однослойная RNN

self.fc = nn.Linear(hidden_size, output_size) # Полносвязный слой для предсказания

def forward(self, x):

h0 = torch.zeros(1, x.size(0), self.hidden_size) # Инициализация скрытого состояния

out, _ = self.rnn(x, h0) # Передача через RNN

out = self.fc(out[:, -1, :]) # Применение линейного слоя к выходу RNN

return out

# Параметры сети и данных

input_size = 10 # Размер входных данных

hidden_size = 50 # Размер скрытого состояния

output_size = 1 # Размер выхода

model = SimpleRNN(input_size, hidden_size, output_size)

# Настройка функции потерь и оптимизатора

criterion = nn.MSELoss() # Функция потерь (например, для регрессии)

optimizer = optim.Adam(model.parameters(), lr=0.001)

# Пример данных для обучения

inputs = torch.randn(32, 5, input_size) # Батч из 32 последовательностей длиной 5 с входным размером 10

labels = torch.randn(32, output_size) # Соответствующие метки

# Обнуление градиентов

optimizer.zero_grad()

# Прямой проход и вычисление потерь

outputs = model(inputs)

loss = criterion(outputs, labels)

# Обратное распространение

loss.backward()

# Применение Gradient Clipping

torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # Ограничение градиентов

# Шаг оптимизации

optimizer.step()

print("Значение функции потерь:", loss.item())

```

Объяснение кода:

1. Gradient Clipping:

– `torch.nn.utils.clip_grad_norm_` применяет ограничение к норме градиентов. В данном случае, `max_norm=1.0` означает, что если норма градиента превышает 1.0, она будет уменьшена до этого значения.

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

2. Применение в RNN:

– Этот метод особенно эффективен в рекуррентных сетях, таких как `SimpleRNN`, где ошибка распространяется через несколько временных шагов, увеличивая риск взрыва градиентов.

3. Когда применять Gradient Clipping:

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

Эти методы помогают сделать обучение нейронных сетей более стабильным и эффективным, особенно при работе с глубокими и рекуррентными архитектурами.

2.4. Алгоритмы оптимизации
2.4.1. Основы градиентного спуска

Градиентный спуск – это способ обучения нейронных сетей, который помогает сети подбирать оптимальные значения весов, чтобы минимизировать ошибки. Представьте, что мы находимся на вершине холма и хотим спуститься в самую низкую точку, которая символизирует минимальную ошибку сети. На каждом шаге мы смотрим вокруг и выбираем направление, которое ведет вниз (градиент), и немного продвигаемся в этом направлении. Шаги, которые мы делаем, называются скоростью обучения. Если шаги слишком большие, мы можем перескочить через низину и не достигнуть цели, а если слишком маленькие, спуск займет очень много времени.

Виды градиентного спуска

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

1. Пакетный градиентный спуск:

– Здесь мы вычисляем обновление весов, используя весь набор данных сразу. Это значит, что мы рассматриваем все примеры (например, все изображения или тексты), обучаемся на них и только после этого обновляем веса.

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

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

Предположим, у нас есть задача классификации изображений, и мы используем MNIST – набор данных, содержащий изображения рукописных цифр.

```python

import torch

import torch.nn as nn

import torch.optim as optim

from torch.utils.data import DataLoader

from torchvision import datasets, transforms

# Определяем простую нейронную сеть

class SimpleNet(nn.Module):

def __init__(self):

super(SimpleNet, self).__init__()

self.fc1 = nn.Linear(28*28, 128) # Первый полносвязный слой

self.fc2 = nn.Linear(128, 10) # Второй слой для классификации (10 классов)

def forward(self, x):

x = x.view(-1, 28*28) # Преобразуем изображение в одномерный вектор

x = torch.relu(self.fc1(x)) # Применяем ReLU активацию

x = self.fc2(x) # Выходной слой

return x

# Загружаем данные MNIST

transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])

train_data = datasets.MNIST(root='./data', train=True, download=True, transform=transform)

train_loader = DataLoader(train_data, batch_size=len(train_data), shuffle=True) # Пакетный градиентный спуск (batch size = весь набор данных)

# Создаем модель, функцию потерь и оптимизатор

model = SimpleNet()

criterion = nn.CrossEntropyLoss() # Функция потерь для многоклассовой классификации

optimizer = optim.SGD(model.parameters(), lr=0.01) # Стохастический градиентный спуск

# Обучение

epochs = 1 # Одно обучение (можно увеличить количество эпох)

for epoch in range(epochs):

for data, target in train_loader: # Обрабатываем весь набор данных за одну эпоху

optimizer.zero_grad() # Обнуляем градиенты перед вычислением новых










1
...
...
11