Рекуррентная нейросеть очень быстро сходится к одному решению

Рейтинг: 0Ответов: 1Опубликовано: 12.02.2023

На основе GRU делаю нейросеть для прогноза на день вперед. На входе обучения батч вида [BatchNr, DayNr, DayFeatures]. После нескольких батчей на выходе из GRU слоя все время получаю одинаковые тензоры (на входе тензоры разные, хоть и очень похожие). Входные тензоры (1 день) примерно такие [0,0,1,-1,-1,1,0,0.5, ...... ,22,12,2022] (вход по большей части в диапазоне -1..1, но пара мест есть числа вида 2022). На выходе ожидаю что-то вида [0,0,0.2,0.9,1,1,1,1,0] (диапазон строго 0..1).

Перепробовал всё что смог: пробовал LSTM, менял loss, добавлял dropout, менял полносвязные слои (от 1 до 3), менял функции активации, менял число слоев GRU n_layers (от 1 до 3) и размер скрытого состояния hidden_dim (от 10 до 300), менял оптимизатор, добавлял штраф за одинаковые тензоры на выходе. Без fc0 немного хуже. В чем я ошибся?

class GRUNet_minimum(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim, n_layers, hyp_fc1, hyp_fc2, drop_prob=0.0):
        super(GRUNet_minimum, self).__init__()
        self.hidden_dim = hidden_dim
        self.n_layers = n_layers

        self.fc0 = nn.Linear(input_dim, input_dim)
        self.gru = nn.GRU(input_size=input_dim, hidden_size=hidden_dim, num_layers=n_layers, batch_first=True)
        self.fc1 = nn.Linear(hidden_dim, output_dim)
        self.prelu = nn.ELU()
        self.sigmoid = nn.Sigmoid()
        self.tanh = nn.Tanh()
        self.evaluate_mode = False
        self.loss_std = nn.MSELoss().to(device)

    def forward(self, x, h):
        x1=self.fc0(x)
        # создаю первоначальное состояние - всегда нули
        h = torch.zeros(self.n_layers, x.size(0), self.hidden_dim).to(device)
        out, h = self.gru( x1, h)
        out = out[:, -1, : ];
        out = self.fc1(out)
        out = self.sigmoid(out)
        return out, h

Кусок обучения

for X, y in tqdm(data_loader, total=num_batches, desc='Training'):
    h = model.init_hidden(batch_size)
    output, h = model(X, h)
    loss = get_loss(output, y, loss_function, mask)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

Если нужно дополню код (просто он довольно объемный).

По результатам комментариев:

  1. скрытое состояние обнуляется перед каждым батчем (пробовал не обнулять - никак не влияет в моем случае).

  2. В одном месте ошибся. Dataset length около 1000. Sequence len = пробовал от 10 до 400 (400 это в ущерб длине датасета).

  3. Не важно что я подаю на входе (точнее какие прошлые дни) - на выходе константа. Когда смотрю график предсказаний он одинаковый с точностью до 5 знака после запятой.

  4. Переобучение может давать одинаковый (константный) результат на тестовых данных, но у меня и на обучающих данных результат константный. Замеряю стандартное отклонение данных (по колонкам) перед GRU - получаю stddev около 0,26. Сразу после GRU stddev = 0,0 . (Ухищрениями с дополнительным loss дотягиваю его до 0,001-0,1 и даже больше, но финальный результат не меняется). Вместо GRU-RNN-LSTM ставлю полносвязную минисетку и обучение идет действительно как "ставим в прогноз последний день" (ну почти). Т.е. либо неправильно данные в пакете, либо еще что-то. Но постарался всё что можно перепроверить (насколько позволяет мой разум).

Разумеется приведенный код только костяк. Пробовал разное. В частности перед выходом несколько полносвязных слоев. Именно они выучивают характерные закономерности (типа: ночью -- холодно, днем -- тепло) и выдают на выходе изо дня в день с точностью до какого-то там знака после запятой.

Ответы

▲ 0

@nexoma пиши развернутый ответ про нормализацию (возможно с примерами из собственных опытов, где без нормализации всё хуже) - получишь баллы.

  1. Хоть 90% данных были в диапазоне [-1..1] были колонки, которые сильно выходили за этот диапазон. Простое выкидывание таких колонок уже помогает (потом там, разумеется, будет нормирование).

  2. batch_size вместо обычных 64 желательно ставить в моем случае 300 и даже 600 (а данных-то всего 800).

  3. Обязательный контроль градиентов при помощи чего-то такого

         # Gradient Norm Clipping
         grd_norms  = nn.utils.clip_grad_norm_(model.parameters(), max_norm=2, norm_type=2)
    
         if grd_norms > 1:
             logger.warning(f'Gradient problem grd_norms={grd_norms.item()}')
             nn.utils.clip_grad_value_(model.parameters(), clip_value=1.0)
    
  4. Увеличение числа эпох до нескольких тысяч (у постепенным уменьшением learning rate до 1e-6).

Применение всего комплекса позволило снизить train loss с 0,2 до 0,01 (дальше лень было ждать). Основной пункт первый, но только его мало (добавив только нормализацию loss падает примерно до 0,08).