PyTorch Рекуррентная нейронная сеть (RNN)

Рекуррентные нейронные сети (RNN) — это класс архитектур нейронных сетей, специально предназначенных для обработки данных последовательностей. Они могут улавливать динамическую информацию из временных рядов или упорядоченных данных и могут обрабатывать данные последовательностей, такие как текст, временные ряды или аудио.

RNN находит широкое применение в таких задачах, как обработка естественного языка (NLP), распознавание речи и прогнозирование временных рядов.

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

Базовая структура RNN

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

Скрытое состояние:RNN использует скрытое состояние для запоминания информации в последовательности.

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

Структура рекуррентного блока выглядит следующим образом:

  • Вход (\(x_t\)): входной вектор на временном шаге \(t\).
  • Скрытое состояние (\(h_t\)): вектор скрытого состояния на временном шаге \(t\), используемый для хранения информации с предыдущих временных шагов.
  • Выход (\(y_t\)): выходной вектор на временном шаге \(t\) (необязательно, в зависимости от конкретной задачи).

Формула:

\[ h_t = f(W_{hh}h_{t-1} + W_{xh}x_t + b_h) \]

  • ht: скрытое состояние на текущем временном шаге.
  • ht-1: скрытое состояние на предыдущем временном шаге.
  • Xt: вход на текущем временном шаге.
  • Whh、Wxh: матрица весов.
  • b: член смещения.
  • f: функция активации (например, Tanh или ReLU).

Выход:Выход RNN зависит не только от текущего входа, но и от исторической информации скрытого состояния.

Формула:

\[ y_t = W_{hy}h_t + b_y \]

  • yt: выходной вектор на временном шаге t (необязательно, в зависимости от конкретной задачи).
  • Why: матрица весов из скрытого состояния в выход.

Как RNN обрабатывает последовательные данные

Развернутое представление рекуррентной нейронной сети (RNN) при обработке последовательных данных выглядит следующим образом:

RNN — это нейронная сеть для обработки последовательных данных. Она обрабатывает каждый элемент последовательности через рекуррентные связи и передает информацию на каждом временном шаге. Ниже приведено объяснение каждой части на рисунке:

  • Входная последовательность (Xt, Xt-1, Xt+1, ...): Розовые круги на рисунке представляют элементы входной последовательности. Например, Xt — это вход на текущем временном шаге, Xt-1 — вход на предыдущем временном шаге, и так далее.

  • Скрытое состояние (ht, ht-1, ht+1, ...): Зеленые прямоугольники представляют скрытое состояние RNN, которое хранит информацию о последовательности на каждом временном шаге. ht — это скрытое состояние на текущем временном шаге, ht-1 — скрытое состояние на предыдущем временном шаге.

  • Матрицы весов (U, W, V):

    • U: Матрица весов из входа в скрытое состояние, используемая для преобразования входаXtв часть скрытого состояния.
    • W: Матрица весов из скрытого состояния в скрытое состояние, используемая для преобразования скрытого состояния предыдущего временного шагаht-1в часть скрытого состояния текущего временного шага.
    • V: Весовая матрица от скрытого состояния к выходу, используемая для преобразования скрытого состоянияhtв выход.Yt。
  • Выходная последовательность (Yt, Yt-1, Yt+1, ...): Синие кружки представляют выход RNN на каждом временном шаге. Например, Yt — это выход на текущем временном шаге.

  • Рекуррентная связь: Характерной особенностью RNN является рекуррентная связь скрытого состояния, которая позволяет сети учитывать информацию с предыдущих временных шагов при обработке входа на текущем временном шаге.

  • Развёртка: На рисунке показан процесс развёртки RNN по последовательности, что помогает понять, как RNN обрабатывает данные последовательности во времени. В реальных реализациях RNN эти шаги обрабатываются параллельно, но концептуально мы можем развернуть её, чтобы понять, как течёт информация.

  • Поток информации: Информация течёт от входной последовательности к скрытому состоянию через весовую матрицу U, затем между временными шагами через весовую матрицу W и, наконец, от скрытого состояния к выходной последовательности через весовую матрицу V.


Основы RNN в PyTorch

В PyTorch RNN можно использовать для построения сложных моделей последовательностей.

PyTorch предоставляет несколько модулей RNN, включая:

  • torch.nn.RNN: Базовая ячейка RNN.
  • torch.nn.LSTM: Ячейка Long Short-Term Memory (LSTM), способная изучать долгосрочные зависимости.
  • torch.nn.GRU: Управляемый рекуррентный блок, упрощённая версия LSTM, но обычно проще в обучении.

При использовании класса RNN необходимо указать размерность входа, размерность скрытого слоя и некоторые другие гиперпараметры.

Пример реализации простой RNN на PyTorch

Ниже приведён простой пример реализации на PyTorch, в котором модель RNN используется для обработки данных последовательности и выполнения классификации.

1. Импорт необходимых библиотек

Пример

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import numpy as np

2. Определение модели RNN

Пример

class SimpleRNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(SimpleRNN, self).__init__()
        # Определение слоя RNN
        self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
        # Определение полносвязного слоя
        self.fc = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        # x: (batch_size, seq_len, input_size)
        out, _ = self.rnn(x)  # out: (batch_size, seq_len, hidden_size)
        # Использовать выход последнего временного шага последовательности как выход модели
        out = out[:, -1, :]  # (batch_size, hidden_size)
        out = self.fc(out)  # Полносвязный слой
        return out

3. Создание обучающих данных

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

Пример

# Сгенерировать случайные данные последовательностей
num_samples = 1000
seq_len = 10
input_size = 5
output_size = 2  # Предполагаем задачу бинарной классификации

# Случайно сгенерировать входные данные (batch_size, seq_len, input_size)
X = torch.randn(num_samples, seq_len, input_size)
# Случайно сгенерировать целевые метки (batch_size, output_size)
Y = torch.randint(0, output_size, (num_samples,))

# Создать загрузчик данных
dataset = TensorDataset(X, Y)
train_loader = DataLoader(dataset, batch_size=32, shuffle=True)

4. Определите функцию потерь и оптимизатор

Пример

# Создать экземпляр модели
model = SimpleRNN(input_size=input_size, hidden_size=64, output_size=output_size)

# Определяем функцию потерь и оптимизатор
criterion = nn.CrossEntropyLoss()  # Многоклассовая кросс-энтропийная потеря
optimizer = optim.Adam(model.parameters(), lr=0.001)

5. Обучите модель

Пример

num_epochs = 10

for epoch in range(num_epochs):
    model.train()  # Установить модель в режим обучения
    total_loss = 0
    correct = 0
    total = 0

    for inputs, labels in train_loader:
        # Прямой проход
        outputs = model(inputs)
        loss = criterion(outputs, labels)

        # Обратное распространение и оптимизация
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        total_loss += loss.item()

        # Вычислите точность
        _, predicted = torch.max(outputs, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

    accuracy = 100 * correct / total
    print(f"Эпоха [{epoch+1}/{num_epochs}], Потери: {total_loss / len(train_loader):.4f}, Точность: {accuracy:.2f}%")

6. Протестируйте модель

После обучения мы можем оценить производительность модели на тестовом наборе.

Пример

# Тестирование модели
model.eval()  # Перевести модель в режим оценки
with torch.no_grad():
    total = 0
    correct = 0
    for inputs, labels in train_loader:
        outputs = model(inputs)
        _, predicted = torch.max(outputs, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

    accuracy = 100 * correct / total
    print(f"Точность на тесте: {accuracy:.2f}%")

7. Полный код выглядит следующим образом:

Пример

import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
import numpy as np

# Набор данных: предсказание последовательности символов (Hello -> Elloh)
char_set = list("hello")
char_to_idx = {c: i for i, c in enumerate(char_set)}
idx_to_char = {i: c for i, c in enumerate(char_set)}

# Подготовка данных
input_str = "hello"
target_str = "elloh"
input_data = [char_to_idx[c] for c in input_str]
target_data = [char_to_idx[c] for c in target_str]

# Преобразование в one-hot encoding
input_one_hot = np.eye(len(char_set))[input_data]

# Преобразовать в тензор PyTorch
inputs = torch.tensor(input_one_hot, dtype=torch.float32)
targets = torch.tensor(target_data, dtype=torch.long)

# Гиперпараметры модели
input_size = len(char_set)
hidden_size = 8
output_size = len(char_set)
num_epochs = 200
learning_rate = 0.1

# Определить модель RNN
class RNNModel(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(RNNModel, self).__init__()
        self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
        self.fc = nn.Linear(hidden_size, output_size)

    def forward(self, x, hidden):
        out, hidden = self.rnn(x, hidden)
        out = self.fc(out)  # Применить полносвязный слой
        return out, hidden

model = RNNModel(input_size, hidden_size, output_size)

# Определяем функцию потерь и оптимизатор
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)

# Обучение RNN
losses = []
hidden = None  # Начальное скрытое состояние — None
for epoch in range(num_epochs):
    optimizer.zero_grad()

    # Прямой проход
    outputs, hidden = model(inputs.unsqueeze(0), hidden)
    hidden = hidden.detach()  # Предотвращение взрыва градиентов

    # Вычислите потери
    loss = criterion(outputs.view(-1, output_size), targets)
    loss.backward()
    optimizer.step()
    losses.append(loss.item())

    if (epoch + 1) % 20 == 0:
        print(f"Эпоха [{epoch + 1}/{num_epochs}], Потери: {loss.item():.4f}")

# Тестирование RNN
with torch.no_grad():
    test_hidden = None
    test_output, _ = model(inputs.unsqueeze(0), test_hidden)
    predicted = torch.argmax(test_output, dim=2).squeeze().numpy()

    print("Входная последовательность: ", ''.join([idx_to_char[i] for i in input_data]))
    print("Предсказанная последовательность: ", ''.join([idx_to_char[i] for i in predicted]))

Пояснение к коду:

  1. Подготовка данных:

    • Используйте последовательность символовhello, и преобразуйте её в one-hot кодирование.
    • Целевая последовательность —elloh, то есть сдвиньте один символ вправо.
  2. Построение модели:

    • Используйтеtorch.nn.RNNСоздайте рекуррентную нейронную сеть.
    • Добавить полносвязный слойtorch.nn.LinearИспользуется для преобразования скрытого состояния в выход.
  3. Этап обучения:

    • На каждой эпохе вычисляется потеря и выполняется обратное распространение.
    • Скрытое состояние пропускается черезhidden.detach()чтобы предотвратить взрыв градиентов.
  4. Этап тестирования:

    • Модель выводит предсказанные результаты символов.
  5. Визуализация:

    • Используйте Matplotlib для построения графика динамики потерь при обучении.

Предполагая, что ваша модель хорошо обучена, вывод может быть следующим:

Epoch [20/200], Loss: 0.0013
Epoch [40/200], Loss: 0.0003
Epoch [60/200], Loss: 0.0002
Epoch [80/200], Loss: 0.0001
Epoch [100/200], Loss: 0.0001
Epoch [120/200], Loss: 0.0001
Epoch [140/200], Loss: 0.0001
Epoch [160/200], Loss: 0.0001
Epoch [180/200], Loss: 0.0001
Epoch [200/200], Loss: 0.0001
Input sequence:  hello

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

8. Код визуализации:

Пример

import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
import numpy as np

# Набор данных: предсказание последовательности символов (Hello -> Elloh)
char_set = list("hello")
char_to_idx = {c: i for i, c in enumerate(char_set)}
idx_to_char = {i: c for i, c in enumerate(char_set)}

# Подготовка данных
input_str = "hello"
target_str = "elloh"
input_data = [char_to_idx[c] for c in input_str]
target_data = [char_to_idx[c] for c in target_str]

# Преобразование в one-hot encoding
input_one_hot = np.eye(len(char_set))[input_data]

# Преобразовать в тензор PyTorch
inputs = torch.tensor(input_one_hot, dtype=torch.float32)
targets = torch.tensor(target_data, dtype=torch.long)

# Гиперпараметры модели
input_size = len(char_set)
hidden_size = 8
output_size = len(char_set)
num_epochs = 200
learning_rate = 0.1

# Определить модель RNN
class RNNModel(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(RNNModel, self).__init__()
        self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
        self.fc = nn.Linear(hidden_size, output_size)

    def forward(self, x, hidden):
        out, hidden = self.rnn(x, hidden)
        out = self.fc(out)  # Применить полносвязный слой
        return out, hidden

model = RNNModel(input_size, hidden_size, output_size)

# Определяем функцию потерь и оптимизатор
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)

# Обучить RNN
losses = []
hidden = None  # Начальное скрытое состояние — None
for epoch in range(num_epochs):
    optimizer.zero_grad()

    # Прямое распространение
    outputs, hidden = model(inputs.unsqueeze(0), hidden)
    hidden = hidden.detach()  # Предотвращение взрыва градиентов

    # Вычислить потери
    loss = criterion(outputs.view(-1, output_size), targets)
    loss.backward()
    optimizer.step()
    losses.append(loss.item())

    if (epoch + 1) % 20 == 0:
        print(f"Эпоха [{epoch + 1}/{num_epochs}], Потери: {loss.item():.4f}")

# Тестировать RNN
with torch.no_grad():
    test_hidden = None
    test_output, _ = model(inputs.unsqueeze(0), test_hidden)
    predicted = torch.argmax(test_output, dim=2).squeeze().numpy()

    print("Входная последовательность: ", ''.join([idx_to_char[i] for i in input_data]))
    print("Предсказанная последовательность: ", ''.join([idx_to_char[i] for i in predicted]))

# Визуализировать потери
plt.plot(losses, label="Потери при обучении")
plt.xlabel("Эпоха")
plt.ylabel("Потери")
plt.title("Потери при обучении RNN по эпохам")
plt.legend()
plt.show()

После выполнения отображается следующее изображение:

Другие расширения