Потери при тестировании немедленно возрастают в LSTM

Я пытаюсь создать 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()

Какова ориентация вашего тензора данных? nn.LSTM ожидает, что входные данные будут иметь форму (sl, bs, n), где ось пакета будет второй осью. Если ваш тензор отформатирован (bs, sl, n), вам нужно передать batch_first=True в nn.LSTM

Karl 09.04.2024 23:58

Я считаю, что это правильный порядок: он имеет порядок (количество строк данных, количество последовательностей в одной строке, количество функций для каждой последовательности)

WILLYB 10.04.2024 04:28
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
2
133
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 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)].

Если ваши данные упорядочены (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, :] прежней. Это должно решить проблему доступа только к последнему образцу, но мои потери при тестировании все еще растут.

WILLYB 10.04.2024 19:55

Я понимаю. Насколько я понимаю, форма 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, чтобы использовать значение по умолчанию). Это связано с тем, что входная размерность кажется немного большой, а количество выборок может быть небольшим в относительном выражении. Будет ли она расходиться так же быстро, если вы сожмете модель таким образом?

MuhammedYunus 10.04.2024 23:21

Итак, произошло что-то действительно интересное. На данный момент я сократил последовательность до 1, чтобы у меня было гораздо больше данных для работы. На данный момент данные таковы (команда A: матч 1 против какой-то команды, команда B: матч 1 против какой-то команды и команда A против команды B в качестве метки). Мой hidden_size сейчас 8, а мой num_layers сейчас 1 1. Я оставил свой lr как 0,00001, потому что он дает мне стабильные результаты. Сейчас потери в обучении и тестах снижаются очень медленно, но через определенное количество эпох мои потери в тестах начинают медленно расти. Я буду продолжать возиться с параметрами и буду держать вас в курсе.

WILLYB 11.04.2024 07:20

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

WILLYB 11.04.2024 07:22

Похоже, что это был вопрос переобучения (то есть настройки гиперпараметров), а не что-то не так с логикой программы. Еще одна вещь, которую вы можете попробовать, — это использовать слой Conv1d, чтобы «сжать» 124-мерные входные данные до чего-то меньшего. Это должно сохранить важную информацию, а также уменьшить размерность данных, предотвращая переобучение.

MuhammedYunus 11.04.2024 14:07

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

WILLYB 11.04.2024 23:23

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

WILLYB 11.04.2024 23:29

Да, возможно, вам понадобится больше данных, чтобы выявить надежную закономерность. Может быть, данные не являются строго прогнозирующими? Задача звучит так: основываясь на результатах команд A и B в 5 последовательно сыгранных матчах против других, победит ли команда A команду B? В зависимости от вида спорта, хотя я и не эксперт, я ожидаю, что предсказательная способность будет больше, чем случайность, но с широким диапазоном ошибок (в финалах некоторых видов спорта результат может быть очень неопределенным). Если есть запись предсказаний, сделанных людьми, это может указывать на базовую точность, которую модель должна хотя бы немного превзойти.

MuhammedYunus 12.04.2024 00:02

Что касается потерь в тестах, которые сначала падают, а затем растут... хотя потери в обучении должны продолжать снижаться, потери в тестах вполне могут сначала снизиться, а затем увеличиться. Это происходит естественным образом и просто указывает на то, что модель теперь переоснащается. Вам просто нужно будет прекратить тренировку, прежде чем она начнет переобучаться (если вы тренируетесь достаточно долго, вы почти всегда в конечном итоге увидите тенденцию проигрыша в тестах то вниз, то вверх). Обычно я отслеживаю результаты теста (например, точность в вашем случае) и прекращаю тренировку, когда результат достигает своего пика, прежде чем он начинает снижаться, что указывает на переобучение.

MuhammedYunus 12.04.2024 00:22

Итак, я возился с Optuna и могу получить точность до 78% с меньшим набором данных и сейчас около 61% с большим набором данных. Однако теперь у меня вопрос: я сохраняю модель в эпоху с наивысшей точностью тестирования. Это действительно или мне следует сохранить его с наименьшими потерями?

WILLYB 18.04.2024 10:54

Я думаю, вы правы, сохраняя модель с высочайшей точностью тестирования. Результат теста часто является окончательным показателем того, насколько полезна модель. Потеря больше похожа на математический инструмент, позволяющий алгоритму сходиться. Потери, возможно, не поддаются прямой интерпретации, но оценка обычно интерпретируется, потому что именно так мы решаем, подходит ли модель для своего предназначения. Что касается более крупного набора данных... увеличение размера модели может улучшить ситуацию на 61%, поскольку кажется, что она может не соответствовать требованиям.

MuhammedYunus 18.04.2024 11:09

Я попытался увеличить модель, но не смог решить проблему недостаточной подгонки. Я сделал свой LSTM двунаправленным и посмотрим, как это сработает для меня.

WILLYB 21.04.2024 21:33

Я понимаю. Я заметил, что изначально предлагал kernel_size=1, но думаю, стоит посмотреть на большие размеры — я обновил конец своего ответа некоторыми комментариями по этому поводу.

MuhammedYunus 21.04.2024 23:17

Считаете ли вы, что разумнее использовать это как двоичный классификатор, где каждое сопоставление не зависит от другого, или разумнее использовать его как многоклассовый классификатор? Часто бывают случаи, когда, если данные представляют собой команду «А» против команды «Б», я получу низкую вероятность победы для них обеих.

WILLYB 24.04.2024 05:59

Если результат на каждом этапе является двоичным (т.е. результатом является либо A, либо B, а не A и B), то я думаю, что использование многоклассового вывода будет подходящим, поскольку оно заставляет модель принимать решение либо A, либо B. Это способ включения ваших предварительных знаний о данных в модель (ограничение типа выходных данных, которые она может производить). Я думаю, это то, что вы изначально делали с BCEWithLogitsLoss.

MuhammedYunus 24.04.2024 10:02

Другие вопросы по теме