Комментарии 7
После претрейна на базовых задачах модель начинает учить новую. Но параллельно пропускаем через старую версию сети (слепок) абсолютно случайный вектор (белый шум). Ответ старой сети становится целевым «ключом» для новой.
Несколько раз перечитал, но так и не понял, как конкретно это работает?.. Если мы возьмём мушку-дрозофиллу microgpt - как это можно реализовать на ней?
Надо смотреть. Сейчас немного занят. Попозже.
Если просто, то: учишь старое, делаешь замороженный слепок сети. затем учишь новое, параллельно гоняешь случайный шум через слепок и через живую сеть, штрафуешь живую сеть если её реакция на шум отличается от реакции слепка. или лучше сделать 100 случайных последовательностей и сохранить как эталон. тогда копия сети не нужна. Думаю ваша ллм легко с кодом справится. Можно выучить на именах, затем на городах, чтобы проверить насколько сохранится знание имен (вынужденное сохранение геометрии старых знаний).
На примере той же microgpt, доступной каждому буквально в кармане:
1) научили на именах, сохранили все матрицы
2) научили новую (на названиях населённых пунктов, к примеру)
3) как прогнать случайный шум? Запустить промпт "акщзе"?
4) "штрафуешь живую сеть если её реакция на шум отличается от реакции слепка..." - одна продолжила "акщземир", другая - "акщземол" - как рассчитать штраф и как его применить?
У вас же под рукой ЛЛМ... Уж это они делают на раз два. По акщзе - просто случайная последовательность токенов. Про штраф, берем внутреннее состояние x вектор 16 чисел внутри сети после последнего слоя, до lm_head. Это и есть реакция на шум. Штраф среднеквадратичное расстояние между двумя векторами
Скрытый текст
Шаг 1: после обучения на именах сохраняем слепок:
anchor = {k: [[Value(p.data) for p in row] for row in mat]
for k, mat in state_dict.items()}
Шаг 2: в training loop на городах добавляем:
# случайный шум — просто список случайных токенов
noise = [random.randint(0, vocab_size-1) for _ in range(8)]
# прогон через якорь — собираем x на каждой позиции
keys_a, vals_a = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)]
old_state = {k: v for k, v in state_dict.items()}
state_dict.update(anchor)
h_anchor = []
for pos_id, tok in enumerate(noise):
logits, x = gpt(tok, pos_id, keys_a, vals_a, return_hidden=True)
h_anchor.append([Value(xi.data) for xi in x]) # только числа, без графа
state_dict.update(old_state)
# прогон того же шума через живую сеть
keys_s, vals_s = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)]
h_student = []
for pos_id, tok in enumerate(noise):
logits, x = gpt(tok, pos_id, keys_s, vals_s, return_hidden=True)
h_student.append(x)
# MSE через Value — те же операции что он уже использует
sublim = Value(0)
n_total = len(noise) * n_embd
for ha, hs in zip(h_anchor, h_student):
for a, s in zip(ha, hs):
sublim = sublim + (s - a) ** 2
sublim = sublim * Value(1.0 / n_total)
# итоговый лосс — просто сложение двух Value
total_loss = loss + Value(2.0) * sublim
total_loss.backward()
# дальше его Adam step без изменений
И добавить return_hidden=True в gpt()
Я так понял, что:
1. Обучили сетку на именах
2. Скопировали её веса в новую сетку, и дообучили её на названиях населённых пунктов.
3. Подали на вход обеих случайный контекст "акщзе"
4. Старая сетка выдала "акщзем", дообученная выдала "акщзеп"
5. говорим дообученной, что надо было "м", а не "п" - для этого считаем вектор ошибки ["п" - "м"], и алгоритмом обратного распространения прогоняем её через сеть, обновляя веса.
6. повторяем, пока дообученная сетка не начнёт снова выдавать "акщзем"
шаги 4 и 5 точнее выглядят так - старая сеть на шуме "акщзе" дала внутренний вектор [0.31, -0.12, 0.84, ...]. Дообученная дала [0.33, -0.09, 0.79, ...]. Штраф = среднеквадратичное расстояние между этими двумя векторами. Не между токенами на выходе, а между внутренними состояниями.
И не на дообученной, а во время дообучения. Старая сеть заморожена, работаем со слепком, или вообще сняли внутренние вектора ответившие на шум и работаем с ними.

Семантический компьютер на 64 нейронах и обучение на шуме