Продолжение исследования RNN

от автора

С прошлой статьи я внёс несколько изменений:
1. Планировщик был сломан и не изменял скорость. Починил.
2. Остаточное соединение через умножение.
3. WindowedDense для выходной проекции.
4. Добавил clipnorm 1, cutoff_rate 0.4

Как обычно это всё добавляет стабильности и 1% точности.

WindowedDense по неизвестной мне причине добавляет SMR стабильность.

class SMR(layers.Layer):   def __init__(self, units):     super().__init__()     self.state_size = units     self.s_l = layers.Dense(units, use_bias=False)    def get_in_proj(self):     return WindowedDense(self.state_size, 16)    def call(self, i, states):     s = states[0]     s = self.s_l(s)     o = i * (s + 0.1)     return o, [o]

Обновлённые тесты для «crime-and-punishment-2554» (1128 шагов):

Bigram: ~0.27  LSTM 0.4951, 0.5609, 0.5795, 0.5886  GRU 0.5036, 0.5654, 0.5834, 0.5932  SMR 0.5335, 0.5843, 0.6002, 0.6103  SLR(emb=3072, lr=0.001) 0.5267, 0.5799, 0.6021, 0.6151

В дальнейшем эксперименты будут на «Принцип неопределённости» (4356 шагов).
Русский фанфик по Звёздным Войнам. Намного более сложный текст.
Размер моделей будет ~500т.

Bigram: 0.2449  LSTM 0.5047, 0.5530, 0.5642, 0.5673  GRU 0.5006, 0.5458, 0.5611, 0.5652  SMR 0.5156, 0.5588, 0.5716, 0.5750  SLR(emb=3072, lr=0.001) 0.4849, 0.5279, 0.5398, 0.5423

LSTM и GRU опять «обделались». Посла множества экспериментов у меня появился список правил построения эффективных RNN:

  1. Для стека ячеек необходима входная матрица.

  2. Матрица для входа и/или состояния повышает стабильность.

  3. Остаточное соединение через умножение лучше сложения.
    Объясняю это тем что так легче масштабировать и инвертировать значения.

  4. Минимальная глубина вычислений ячейки. Для стабилизации градиентов.

  5. Использование активаторов-зажимов(TanH, Sigmoid, Softmax) приводит к угасанию градиентов. TanH наименее вредный.

  6. Внутренние нормализации — плохая практика.

  7. Отдельное состояние не обязательно.

  8. Нелинейности нужны только если задача требует.

  9. Языковое моделирование не требует активаторов и биасов.

  10. Проблемы градиентов первичны.

  11. Если состояние используется только в остаточном соединении то RNN можно распараллелить. Вот так: h = x + h-1;

  12. Количество обучаемых параметров и размер состояния должны быть сбалансированными.

  13. Обучение с сохранением состояния даёт мощную регуляризацию.

  14. Ценность эмбеддинга зависит от архитектуры: Есть матрица для состояния — минимальная. Есть матрица для входа — средняя. Матриц нет — максимальная.

  15. Нормализацию лучше делать перед слоем и остаточным соединением: x = norm(x); y = RNN(x); x = x * y;

  16. Ворота в разы повышают эффективность состояния. Но проигрывают из за ухудшения градиентов и баланса параметров/состояния. Возможно это нужно рассматривать как смесь экспертов.

  17. Чем больше опора на вход тем больше нужда в стеке или многослойности.

  18. Ортогональная инициализация не обязательна.

Список «ессесвенно» не исчерпывающий. Общий его посыл в том что SMR лучший.
Но я таки придумал архитектуру ещё лучше:

class MSMR(Layer):   def __init__(self, units, cells=3):     super().__init__()     self.units = units     self.state_size = [units, cells * units]     self.mem_shape = [-1, cells, units]     self.k_l = Dense(cells, use_bias=False)     self.d_l = Dense(units, use_bias=False)    def get_in_proj(self):     return Dense(self.units, use_bias=False)    def call(self, i, states):     s, m = states     m = tf.reshape(m, self.mem_shape)      k = tf.nn.softmax(self.k_l(i * s), axis=-1)     k = tf.expand_dims(k, -1)     d = tf.reduce_sum(m * k, axis=-2)      o = i * (self.d_l(d) + 0.1)      k = tf.tanh(self.k_l(o))     k = tf.expand_dims(k, -1)     w = tf.expand_dims(o, -2)     m = tf.tanh(m * k + w * (1 - k))      m = tf.reshape(m, [-1, self.state_size[1]])     return o, [o, m]    0.5187, 0.5705, 0.5860, 0.5902

По сути это универсальная обёртка для расширения памяти RNN.

ЗЫ:
Я пытался сравнивать с другими RNN и трансформерами. GPT2, Mamba, RWKV, Gemma2, …
Все они показали сомнительные результаты. С ними вообще сложно сравнивать. Это принципиально другие архитектуры. Похоже я близок к пределу точность/шаги для RNN и возможно всех других архитектур. За исключением семейств SSM и RWKV все RNN вертятся вокруг ворот LSTM/GRU и не предлагают ничего нового.

ЗЫЫ:
В моих экспериментах с трансформерами линейное кодирование позиций значительно превзошло синусоидальное.

p_emb = tf.cast(tf.range(0, 1, 1 / seq_len), t_emb.dtype) p_emb = tf.expand_dims(tf.expand_dims(p_emb, 0), -1) p_emb = tf.tile(p_emb, [batch_size, 1, 1]) x = tf.concat([t_emb, p_emb], -1)


ссылка на оригинал статьи https://habr.com/ru/articles/851182/