Аномалии обучения нейросетей (original) (raw)

Понадобилось мне как-то недавно обучить нейросеть для прогнозирования временных рядов. Дело для меня новое, но каких-то особых трудностей не предвиделось. После короткого поиска было решено за основу взять GRU.

Повозившись с импортом данных (данные были из нескольких БД), скомпоновав и отладив скрипт обучения был запущен процесс.

Тут нужно коротко сказать о данных. Это данные работы оборудования (плюс некие данные из внешнего мира). Данные разбиты по дням: одна строка = один день. Число колонок порядка 150 (параметров), число строк порядка 1000 (дней).

Задача нейросети предсказать ключевые параметры оборудования на день N+1, если известны данные для для N и ранее.

После первой эпохи обучения loss функция была около 0.6, а к концу 4-й эпохи опускалась до 0.2 и.... дальше процесс не шел. Многократные запуски с разными параметрами существенно ничего не меняли. Train loss около 0.2, test loss чуть больше (0.22).

Визуализация результатов прогноза, показала график в виде пульса. Но насторожило то, что пульс не меняется ни капельки!

Глянул на цифры: прогноз на разные дни был одинаковым с точностью до пяти знаков после запятой. Довольно быстро стало понятно, что на выходе из 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).

Структура нейросети примитивная: input — GRU слой — полносвязные слои (2 шт.) — output

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

  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 ставлю полносвязную минисетку (pytorch позволяет делать многое, в том числе цикл по дням) и обучение идет как "ставим в прогноз последний день" (ну почти, не идеально, но уже хоть как-то). Т.е. впечатление, что либо неправильно данные в пакете, либо еще что-то. Но постарался всё что можно перепроверить (насколько позволяет мой разум).

Сделал вопрос на ru.stackoverflow.com. Были комментарии, от которых я поначалу отмахнулся, как от несущественных: «проверьте сигналы, может идёт потеря до 0, добавьте нормализацию или что там для увеличения сигнала. и до кучи pytorch-forecasting.readthedocs.io/en/stable/index.html смотрели ли это pip install pytorch-forecasting». [Нет там потери в ноль.]

Методом многочисленных экспериментов был нащупан путь.

Типичное описание необходимости нормализации данных при использовании методов машинного обучения.

Как видим нормализация данных преподносится как желательное, но не обязательное действие (к тому же в моих данных 98% данных и так уже нормализованы) и все-таки это было ключевым местом.

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

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

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

grd_norms = nn.utils.clip_grad_norm_(model.parameters(), max_norm=2, norm_type=2)

if grd_norms > 1: 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).

На многочисленные эксперименты ушло больше недели, было прочитано пара десятков страниц по нейросетям из которых почерпнуты кое-какие новые знания и я в очередной раз убедился, что основами пренебрегать не стоит и что упорство будет вознаграждено.

Кстати, в прошлый раз Batch Normalization мне не помог, а в этот раз я вместо нормальной нормализации данных первым слоем поставил именно Batch Normalization (по крайней мере временно).