Я пытаюсь создать LSTM, который предсказывает шестой спортивный матч для команды А на основе последовательности из 5 предыдущих матчей. Мои данные имеют такую структуру. Команда A, игра 1 против случайной команды, команда B, игра 1 против случайной команды, ... Команда A, игра 5 против случайной команды, команда B, игра 5 против случайной команды. Команда Б — это команда, с которой команда А играет в шестой игре, и результат — это результат. Каждая последовательность состоит из 124 функций, которые представляют собой комбинацию i-й игры команды A и i-й игры команды B.
Моя проблема в том, что мои потери в тестах сразу же растут, и я не могу добиться, чтобы они постоянно уменьшались. Я возился с гиперпараметрами, но ни один из них, похоже, не оказал заметного эффекта. Что я могу сделать?
import numpy as np
import torch
from torch import nn
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
from torch.utils.data import TensorDataset, DataLoader
def main():
# Device
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(torch.cuda.is_available())
print(f'Running on device: {device}')
# Process data
data = pd.read_csv('matchup_data.csv')
# Columns that are one-hot encoded for the label
labels = data['label']
# Remove the original one-hot encoded label columns from the features data
data = data.drop('label', axis=1)
num_features = 124
samples = 531
timesteps = 5
# Convert features and labels to tensors
dataT = torch.tensor(data.values).float()
dataT = dataT.view(samples, timesteps, num_features)
labelsT = torch.tensor(labels.values).float()
labelsT = labelsT.unsqueeze(1)
print(dataT)
# Split to test and train data
train_data, test_data, train_labels, test_labels = train_test_split(dataT, labelsT, test_size=.1)
train_dataset = TensorDataset(train_data, train_labels)
test_dataset = TensorDataset(test_data, test_labels)
batch_size = 2 # Choose a batch size that fits your data and model
train_loader = DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=False)
# Layer parameters
input_size = 124
hidden_size = 64
num_layers = 2
output_size = 1
# Net and net parameters
net = LSTMnet(input_size, output_size, hidden_size, num_layers).to(device)
print(net)
loss_function = nn.BCEWithLogitsLoss()
optimizer = torch.optim.Adam(net.parameters(), lr=0.00001)
train_accuracy, train_losses, test_accuracy, test_losses = trainModel(100, net, optimizer, loss_function,
train_loader, test_loader, device)
print(np.max(train_accuracy))
print(np.min(train_losses))
print(np.max(test_accuracy))
print(np.min(test_losses))
# Plot accuracy and loss
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.plot(train_accuracy, label='Train Accuracy')
plt.plot(test_accuracy, label='Test Accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend()
plt.subplot(1, 2, 2)
plt.plot(train_losses, label='Train Loss')
plt.plot(test_losses, label='Test Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.tight_layout()
plt.show()
class LSTMnet(nn.Module):
def __init__(self, input_size, output_size, num_hidden, num_layers):
super().__init__()
self.input_size = input_size
self.num_hidden = num_hidden
self.num_layers = num_layers
# RNN layer
self.lstm = nn.LSTM(input_size, num_hidden, num_layers)
self.dropout = nn.Dropout(0.6)
# linear layer for output
self.out = nn.Linear(num_hidden, output_size)
def forward(self, x):
# Run through RNN layer
y, hidden = self.lstm(x)
# pass through dropout
y = self.dropout(y)
# Pass to linear layer
output = self.out(y)
return output, hidden
def trainModel(num_epochs, net, optimizer, loss_function, train_data, test_data, device):
# Variable initialization
train_accuracy = np.zeros(num_epochs)
train_losses = np.zeros(num_epochs)
test_accuracy = np.zeros(num_epochs)
test_losses = np.zeros(num_epochs)
for epochi in range(num_epochs):
net.train()
segment_loss = []
segment_accuracy = []
for X, y in train_data:
X = X.to(device)
y = y.to(device)
output, _ = net(X) # Unpack the tuple to get the output
output = output[:, -1, :]
loss = loss_function(output, y) # Use .squeeze() to ensure the dimensions match
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Convert output logits to probabilities using sigmoid
probabilities = torch.sigmoid(output)
# Convert probabilities to binary predictions
predicted = (probabilities > 0.5).float()
# Calculate accuracy
acc = (predicted == y).float().mean() * 100
segment_loss.append(loss.item())
segment_accuracy.append(acc.item())
train_losses[epochi] = np.mean(segment_loss)
train_accuracy[epochi] = np.mean(segment_accuracy)
net.eval()
test_loss = []
test_acc = []
with torch.no_grad():
for X, y in test_data:
X = X.to(device)
y = y.to(device)
output, _ = net(X) # Unpack the tuple to get the output
output = output[:, -1, :]
loss = loss_function(output, y) # Use .squeeze() to ensure the dimensions match
# Convert output logits to probabilities using sigmoid
probabilities = torch.sigmoid(output)
# Convert probabilities to binary predictions
predicted = (probabilities > 0.5).float()
# Calculate accuracy
acc = (predicted == y).float().mean() * 100
test_loss.append(loss.item())
test_acc.append(acc.item())
test_losses[epochi] = np.mean(test_loss)
test_accuracy[epochi] = np.mean(test_acc)
return train_accuracy, train_losses, test_accuracy, test_losses
if __name__ == "__main__":
main()
Я считаю, что это правильный порядок: он имеет порядок (количество строк данных, количество последовательностей в одной строке, количество функций для каждой последовательности)
Насколько я понимаю, форма X
в net(X)
равна (2, 5, 62 + 62), а форма y
в loss_function(output, y)
равна (2, 1). Входная последовательность: [A против rand + B против rand (игра 1), ..., A против rand + B против rand (игра 5)]. Результат соответствует [A против B (игра 6)].
Если ваши данные упорядочены (seq_length, batch_size, n_features)
, что, я думаю, соответствует вашему комментарию выше, то я думаю, вам нужно изменить строку:
#output = output[:, -1, :] #accesses last sample only
output = output[-1, :, :] #corrected - access last frame from each sample
В настоящее время он считывает последний образец из каждого пакета, а не последний кадр из каждого образца. В результате оптимизация выполняется только на основе одного образца на партию, что может вызывать переобучение.
Корректировку необходимо будет применять как в обучающем, так и в тестовом цикле.
Чтобы избежать переобучения, попробуйте уменьшить n_layers
до 1 и hidden_size
до 16 (возможно, удалите lr=
из Adam
, чтобы использовать значение по умолчанию). Это связано с тем, что входная размерность кажется немного большой, а количество выборок может быть небольшим в относительном выражении. Это должно привести к тому, что оно не будет так быстро расходиться.
Если это сработает, вы можете попробовать еще одну вещь — использовать слой Conv1d
для «сжатия» 124-мерных входных данных до чего-то меньшего. Это должно сохранить важную информацию, а также уменьшить размерность данных, предотвращая переобучение. Вот простой способ изменить существующий код без необходимости внесения каких-либо других изменений:
#Just the LSTM
#net = LSTMnet(input_size, output_size, hidden_size, num_layers).to(device)
#Conv1d to reduce feature size from 124 down to 32, followed by an LSTM
net = nn.Sequential(
nn.Conv1d(in_channels=124, out_channels=64, kernel_size=1),
nn.BatchNorm1d(64),
nn.ReLU(),
nn.Conv1d(in_channels=64, out_channels=32, kernel_size=1),
nn.BatchNorm1d(32),
nn.ReLU(),
#Finally, the LSTM
LSTMnet(input_size=32, output_size=output_size, num_hidden=32, num_layers=2)
).to(device)
Вы можете начать с первой строки nn.Conv1d
и добавлять остальные строки в зависимости от того, считаете ли вы, что оно того стоит. Входные и выходные формы net
не меняются; просто внутри он отображает размер объекта на что-то меньшее, прежде чем подавать его в LSTM.
Что касается потерь в тестах, которые сначала падают, а затем растут... хотя потери в обучении должны продолжать снижаться, потери в тестах вполне могут сначала снизиться, а затем увеличиться. Это происходит естественным образом и просто указывает на то, что модель теперь переоснащается. Вам просто нужно будет прекратить тренировку, прежде чем она начнет переобучаться (если вы тренируетесь достаточно долго, вы почти всегда в конечном итоге увидите тенденцию проигрыша в тестах то вниз, то вверх). Обычно я отслеживаю результаты теста (например, точность в вашем случае) и прекращаю тренировку, когда результат достигает своего пика, прежде чем он начинает снижаться, что указывает на переобучение.
В своем ответе выше я изначально использовал kernel_size
, равный 1, но это означает, что сверточные слои не извлекают информацию из других временных шагов, и поэтому данные, передаваемые в LSTM, не так полезны, как могли бы быть.
Вы можете попробовать увеличить kernel_size
и одновременно установить padding='same'
, если хотите сохранить ту же длину последовательности. Игра с dilation=
также позволяет слою конвекторов видеть другие части последовательности, и, возможно, на это стоит обратить внимание, если у вас получится с kernel_size
.
Обратите внимание: пока вы используете только конечный результат LSTM, вы можете использовать Conv1d
(где ядро смотрит как назад, так и вперед во времени). В противном случае, я думаю, вам придется переключиться на причинно-следственный сверточный слой. Это будет актуально, если вы переключитесь на архитектуру «последовательность-последовательность» (в настоящее время это последовательность-вектор, но могут быть преимущества конвергенции, если переосмыслить ее как «последовательность-вектор»).
Я отредактировал свой код так, чтобы batch_first = true
, и изменил форму данных, чтобы она соответствовала ожиданиям lstm там, где находится форма (bs, sl, n)
. После этого я сохранил строку output = output[:, -1, :]
прежней. Это должно решить проблему доступа только к последнему образцу, но мои потери при тестировании все еще растут.
Я понимаю. Насколько я понимаю, форма X
в net(X)
равна (2, 5, 62 + 62), а форма y
в loss_function(output, y)
равна (2, 1). Входная последовательность: [A против rand + B против rand (игра 1), ..., A против rand + B против rand (игра 5)]. Результат соответствует [A против B (игра 6)]. Чтобы избежать переобучения, попробуйте уменьшить n_layers
до 1 и hidden_size
до 16 (возможно, удалите lr=
из Adam
, чтобы использовать значение по умолчанию). Это связано с тем, что входная размерность кажется немного большой, а количество выборок может быть небольшим в относительном выражении. Будет ли она расходиться так же быстро, если вы сожмете модель таким образом?
Итак, произошло что-то действительно интересное. На данный момент я сократил последовательность до 1, чтобы у меня было гораздо больше данных для работы. На данный момент данные таковы (команда A: матч 1 против какой-то команды, команда B: матч 1 против какой-то команды и команда A против команды B в качестве метки). Мой hidden_size
сейчас 8, а мой num_layers
сейчас 1 1. Я оставил свой lr
как 0,00001, потому что он дает мне стабильные результаты. Сейчас потери в обучении и тестах снижаются очень медленно, но через определенное количество эпох мои потери в тестах начинают медленно расти. Я буду продолжать возиться с параметрами и буду держать вас в курсе.
Также да, ваше понимание данных правильное. Причина снижения последовательности до 1 заключается в том, что у многих команд нет как минимум 5 матчей.
Похоже, что это был вопрос переобучения (то есть настройки гиперпараметров), а не что-то не так с логикой программы. Еще одна вещь, которую вы можете попробовать, — это использовать слой Conv1d
, чтобы «сжать» 124-мерные входные данные до чего-то меньшего. Это должно сохранить важную информацию, а также уменьшить размерность данных, предотвращая переобучение.
Хммм, я пытался это сделать, но у меня все еще наблюдается явление, когда потеря теста снижается, а затем снова увеличивается. Я продолжу возиться с гиперпараметрами, чтобы посмотреть, что получится.
Я думаю, возможно, мне не хватает данных. Я попробую некоторые методы увеличения данных и посмотрим, что получится.
Да, возможно, вам понадобится больше данных, чтобы выявить надежную закономерность. Может быть, данные не являются строго прогнозирующими? Задача звучит так: основываясь на результатах команд A и B в 5 последовательно сыгранных матчах против других, победит ли команда A команду B? В зависимости от вида спорта, хотя я и не эксперт, я ожидаю, что предсказательная способность будет больше, чем случайность, но с широким диапазоном ошибок (в финалах некоторых видов спорта результат может быть очень неопределенным). Если есть запись предсказаний, сделанных людьми, это может указывать на базовую точность, которую модель должна хотя бы немного превзойти.
Что касается потерь в тестах, которые сначала падают, а затем растут... хотя потери в обучении должны продолжать снижаться, потери в тестах вполне могут сначала снизиться, а затем увеличиться. Это происходит естественным образом и просто указывает на то, что модель теперь переоснащается. Вам просто нужно будет прекратить тренировку, прежде чем она начнет переобучаться (если вы тренируетесь достаточно долго, вы почти всегда в конечном итоге увидите тенденцию проигрыша в тестах то вниз, то вверх). Обычно я отслеживаю результаты теста (например, точность в вашем случае) и прекращаю тренировку, когда результат достигает своего пика, прежде чем он начинает снижаться, что указывает на переобучение.
Итак, я возился с Optuna и могу получить точность до 78% с меньшим набором данных и сейчас около 61% с большим набором данных. Однако теперь у меня вопрос: я сохраняю модель в эпоху с наивысшей точностью тестирования. Это действительно или мне следует сохранить его с наименьшими потерями?
Я думаю, вы правы, сохраняя модель с высочайшей точностью тестирования. Результат теста часто является окончательным показателем того, насколько полезна модель. Потеря больше похожа на математический инструмент, позволяющий алгоритму сходиться. Потери, возможно, не поддаются прямой интерпретации, но оценка обычно интерпретируется, потому что именно так мы решаем, подходит ли модель для своего предназначения. Что касается более крупного набора данных... увеличение размера модели может улучшить ситуацию на 61%, поскольку кажется, что она может не соответствовать требованиям.
Я попытался увеличить модель, но не смог решить проблему недостаточной подгонки. Я сделал свой LSTM двунаправленным и посмотрим, как это сработает для меня.
Я понимаю. Я заметил, что изначально предлагал kernel_size=1
, но думаю, стоит посмотреть на большие размеры — я обновил конец своего ответа некоторыми комментариями по этому поводу.
Считаете ли вы, что разумнее использовать это как двоичный классификатор, где каждое сопоставление не зависит от другого, или разумнее использовать его как многоклассовый классификатор? Часто бывают случаи, когда, если данные представляют собой команду «А» против команды «Б», я получу низкую вероятность победы для них обеих.
Если результат на каждом этапе является двоичным (т.е. результатом является либо A, либо B, а не A и B), то я думаю, что использование многоклассового вывода будет подходящим, поскольку оно заставляет модель принимать решение либо A, либо B. Это способ включения ваших предварительных знаний о данных в модель (ограничение типа выходных данных, которые она может производить). Я думаю, это то, что вы изначально делали с BCEWithLogitsLoss
.
Какова ориентация вашего тензора данных?
nn.LSTM
ожидает, что входные данные будут иметь форму(sl, bs, n)
, где ось пакета будет второй осью. Если ваш тензор отформатирован(bs, sl, n)
, вам нужно передатьbatch_first=True
вnn.LSTM