С прошлой статьи я внёс несколько изменений:
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:
-
Для стека ячеек необходима входная матрица.
-
Матрица для входа и/или состояния повышает стабильность.
-
Остаточное соединение через умножение лучше сложения.
Объясняю это тем что так легче масштабировать и инвертировать значения. -
Минимальная глубина вычислений ячейки. Для стабилизации градиентов.
-
Использование активаторов-зажимов(TanH, Sigmoid, Softmax) приводит к угасанию градиентов. TanH наименее вредный.
-
Внутренние нормализации — плохая практика.
-
Отдельное состояние не обязательно.
-
Нелинейности нужны только если задача требует.
-
Языковое моделирование не требует активаторов и биасов.
-
Проблемы градиентов первичны.
-
Если состояние используется только в остаточном соединении то RNN можно распараллелить. Вот так: h = x + h-1;
-
Количество обучаемых параметров и размер состояния должны быть сбалансированными.
-
Обучение с сохранением состояния даёт мощную регуляризацию.
-
Ценность эмбеддинга зависит от архитектуры: Есть матрица для состояния — минимальная. Есть матрица для входа — средняя. Матриц нет — максимальная.
-
Нормализацию лучше делать перед слоем и остаточным соединением: x = norm(x); y = RNN(x); x = x * y;
-
Ворота в разы повышают эффективность состояния. Но проигрывают из за ухудшения градиентов и баланса параметров/состояния. Возможно это нужно рассматривать как смесь экспертов.
-
Чем больше опора на вход тем больше нужда в стеке или многослойности.
-
Ортогональная инициализация не обязательна.
Список «ессесвенно» не исчерпывающий. Общий его посыл в том что 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/
Добавить комментарий