Сводка
Этот документ предназначен в качестве простого примера для прогнозирования выходных данных в ряду, представляющем дискретные последовательные данные в конечном пространстве, с использованием модели классификации. Представленная здесь концепция использует небольшую последовательность четных чисел и предсказывает следующее четное число в последовательности. Однако в реальном мире это могут быть любые произвольные значения, представляющие категориальные данные. Сеть на основе LSTM используется для прогнозирования следующего значения в серии. Входные данные берутся в окне (последовательности) длины N и передаются в сеть для предсказания следующего выходного значения. Все числа в последовательности рассматриваются как класс. В целях документа намеренно используется модель классификации, а не модель регрессии.
Последовательность и предсказание
Пример, описанный в этом документе, рассматривает конечный ряд четных чисел от 0 до 10 и длину последовательности (размер окна), равную 2, в целях пояснения. Итак, речь идет о следующем ряду — [0,2,4,6,8,10]. Поскольку размер окна равен 2, каждая входная последовательность содержит данные за 2 временных шага. Входная последовательность и прогноз приведены ниже:
[0,2] предсказывает 4, [2,4] предсказывает 6, [4,6] предсказывает 8 и [6,8] предсказывает 10.
Реализация с использованием PyTorch
Инициализация ноутбука
Импорт библиотек pytorch и numpy
import torch from torch import nn import numpy as np from torch import optim
Обработка входных данных
Входные данные будут представлять собой пакет из нескольких последовательностей длины N, который предсказывает следующее значение в последовательности. Рассмотрим, например, где N = 2 и для первых 5 четных чисел у нас будет набор входных последовательностей и выходных данных, как показано ниже:
[[0,2],[2,4],[4,6],[6,8]] → [4,6,8,10]
Здесь [0,2] приводит к 4, [2,4] приводит к 6 и так далее.
Приведенная ниже функция generate_even_nos(limit)
генерирует входные и выходные числа. group_data(data, window_size)
преобразует данные в формат скользящего окна, где элементы каждого окна служат единой последовательностью, которая загружается в LSTM.
def generate_even_nos(limit): input_data = np.arange(0,limit, 2) output_data = np.arange(4,limit+2, 2) return input_data, output_data def group_data(data, window_size): return np.lib.stride_tricks.sliding_window_view(data, window_size) input_data, output_data = generate_even_nos(10) print(input_data) # Output [0 2 4 6 8] print(output_data) # Output [ 4 6 8 10] input_data = group_data(input_data, 2) # Slides a window of # size 2 through the input data print(input_data) # Output [[0 2] # [2 4] # [4 6] # [6 8]]
Поскольку данные рассматриваются как категориальные, а не как непрерывные, нам нужно сначала преобразовать входные данные в однократно закодированные векторы.
One-Hot-Encoding — для элементов в конечном пространстве, one-hot-encoding присваивает позиционное значение для каждого элемента.
Например: - Здесь у нас есть 5 значений в диапазоне от 0 до 4. Представление 2 в горячем кодировании может быть показано следующим образом:
+---+---+---+---+---+ | 0 | 1 | 2 | 3 | 4 | +---+---+---+---+---+ | 0 | 0 | 1 | 0 | 0 | +---+---+---+---+---+
Это достигается следующим образом:
def one_hot_encode(input_data, total_map_size): dimOne = input_data.shape[0] dimTwo = input_data.shape[1] total_size = dimOne*dimTwo data_canvas = np.zeros((total_size,total_map_size)) data_canvas[np.arange(0,total_size),input_data.flatten()] = 1 data_canvas = data_canvas.reshape(dimOne, dimTwo, total_map_size) return data_canvas #Note that here total_map_size defines the total no, of unique #numbers in our set of even numbers print(one_hot_encode(input_data,11)) #Output # [[[1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.] # [0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]] // corresponds to [0,2] # [[0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0.] # [0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]] // corresponds to [2,4] # [[0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0.] # [0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]] // corresponds to [4,6] # [[0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0.] # [0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0.]]] //corresponds to [6,8]
Последний фрагмент кода для подготовки входных и целевых значений выглядит следующим образом:
input_data, target_data = generate_even_nos(10) # 10 since we need only even # numbers till 10 input_data = group_data(input_data, 2) simple_input = input_data # Taking a copy of the input data for later use input_data = torch.Tensor(one_hot_encode(input_data, 11)) # 11 since we have # 11 numbers in our # finite space output_data = torch.Tensor(target_data) print(input_data.shape) # Output torch.Size([4, 2, 11]) print(output_data.shape) # torch.Size([4])
Определение сети LSTM
В этом случае используется сеть на основе LSTM, поскольку она сохраняет контекст предыдущих данных при обработке следующего значения в последовательности, и особенно поскольку LSTM имеет запись в долговременной памяти, это поможет лучше предсказать следующее значение.
На приведенной выше блок-схеме показано, как 2 значения во входной последовательности (обратите внимание, что размер нашего окна равен 2, однако его можно изменить) используются для предсказания следующего числа в последовательности. Для каждого входного значения в последовательности существует блок LSTM. Входные значения представляют собой горячие кодированные векторы размера 11 (поскольку в нашем конечном пространстве всего 11 чисел от 0 до 11).
Входные данные для блока LSTM будут (Ht-1, Ct-1,Xt), где Ht- 1 и Ct-1 — это выходные данные скрытого слоя и выходные данные состояния ячейки из предыдущего блока LSTM, а Xt — это входное значение текущего блока LSTM. Вывод блока LSTM будет (Ht, Ct и Ot), представляющим скрытый слой блока LSTM, состояние ячейки и выход блока соответственно. Количество скрытых слоев в этом примере равно 100, поэтому трио выше будет иметь размерность 100.
Как вы заметили на блок-схеме, выходные данные последнего блока LSTM для двух вышеуказанных входных данных передаются в полносвязную нейронную сеть (FCNN), которая предсказывает, какое из следующих 11 значений (от 0 до 11) предсказывает сеть. Выход 2-го блока LSTM используется в качестве входных данных для FCNN, а не первого, поскольку на выход последнего уже влияет скрытое состояние и состояние ячейки первого.
Определение сети LSTM на основе диаграммы выше.
class Net(nn.Module): def __init__(self, n_tokens, n_hidden, n_layers, out_tokens) -> None: super().__init__() self.n_hidden = n_hidden self.lstm = nn.LSTM(n_tokens, n_hidden, n_layers, dropout = 0.25, batch_first=True) self.fc = nn.Linear(in_features=n_hidden, out_features=out_tokens) def forward(self, input_weights, hidden_weights): r_out, hidden = self.lstm(input_weights, hidden_weights) r_out = r_out[:,-1,:] out_data = self.fc(r_out) return out_data, hidden
Входными данными для LSTM будет вектор горячего кодирования, который мы вычислили выше. Как вы можете видеть из вектора с горячим кодированием, каждый вектор имеет длину, равную общему номеру. значений, которые мы должны рассмотреть в нашем конечном пространстве. Это означает, что входные данные для нашего LSTM (n_tokens
) будут иметь размерность, равную размеру каждого вектора с горячим кодированием. Прежде чем выходные данные LSTM будут переданы на вход полностью подключенного слоя, поскольку в одном окне последовательности нашего LSTM ( r_out[:,-1,:]
) может быть несколько значений, будет получен выход последнего временного шага и будет использоваться как вход в полносвязный слой.
Полносвязный слой принимает вход, равный нету. скрытых слоев в слое LSTM ( n_hidden
). Выход полносвязного слоя будет равен no. значений, которые мы имеем в нашем конечном пространстве. Это все еще можно рассматривать как длину каждого вектора горячего кодирования. Скажем, например, если у нас есть в общей сложности 10 чисел в нашем конечном пространстве, полностью связанный слой будет давать выходные данные размерности 11 (от 0 до 10), где каждое значение указывает оценку результата, являющегося значением, представленным его соответствующим индексом
Инициализация сети
net = Net(n_tokens=input_data.shape[2], n_hidden=100, n_layers=1, out_tokens=11) optimizer = optim.Adam(params=net.parameters(), lr=0.001) criterion = nn.CrossEntropyLoss()
Сеть инициализируется 100 скрытыми слоями в блоке LSTM, 1 слоем LSTM. Его отмечали в ходе различных испытаний, чем больше, тем нет. входных последовательностей в тренировочных данных лучше иметь больше нет. скрытых слоев. Сеть обучается с использованием Adam Optimizer в качестве функции градиентного спуска и CrossEntropyLoss в качестве функции потерь. CrossEntropyLoss используется, поскольку это проблема классификации.
Обучение сети
На приведенной выше диаграмме показано, как обучается сеть. Как видно выше, каждая мини-партия содержит последовательность из 2 чисел, где каждое число представлено одним своим горячим кодированным вектором размера 11. Поскольку мы рассматриваем все четные числа до 10 с длиной последовательности 2, у нас будет 4 мини-пакеты с размерностью — [4,2,11], обозначающие 4 мини-пакета
hidden = None net.train() LOG_INTERVAL=50 for epoch in range(0,1000): out, hidden = net(input_data, hidden) target_data = output_data hidden = tuple([hidden_state.data for hidden_state in hidden]) loss = criterion(out, target_data.long()) optimizer.zero_grad() loss.backward() optimizer.step() if(epoch%LOG_INTERVAL == 0): print("Epoch {0}; Loss {1}".format(epoch, loss.item()))
В этом примере сеть обучается более 1000 эпох. Начальные состояния (h0 — скрытое состояние и c0 — состояние ячейки) инициализируются как None
. После каждой эпохи скрытое состояние и состояние ячейки возвращаются в сеть для следующей последовательности.
На выходе FCNN создается вектор размера 11, где каждое значение указывает на оценку результата, являющегося значением, представленным соответствующим индексом. Затем этот вывод передается через функцию CrossEntropyLoss
, которая применяет Softmax, а затем функцию NLLLoss, эффективно преобразовывая баллы в их соответствующую вероятность среди 11 значений.
Оценка сети
net.eval() output, _ = net(input_data, hidden) print(simple_input) print(torch.argmax(output, dim=1)+1) # Output # [[0 2] # [2 4] # [4 6] # [6 8]] # tensor([ 4, 6, 8, 10])
Как видно выше, предоставление входных последовательностей генерирует ожидаемый результат.