Языковая Модель без магии: Крошечная Language Model на чистом Node.js
- вторник, 28 июля 2026 г. в 00:00:04

English | Русский
Tokenization → embeddings → causal Transformer → LM head → softmax → loss → backpropagation. Без TensorFlow, без PyTorch, и без hidden autograd.
Учебная causal language model без TensorFlow, PyTorch и ML-библиотек. Каждый скаляр, нейрон, градиент, attention score и шаг обновления весов реализован обычным JavaScript и доступен для просмотра в отладчике.
В проекте остался один сценарий и одна команда:
node src/train.js --generalize --adaptive-teach
Достаточно Node.js 18.19 или новее. Устанавливать зависимости не нужно.
Реальный фрагмент из logs/training-log.txt: матрицы AFTER и DELTA одного FFN-слоя:

Сначала программа выводит ответы модели до обучения:
BEFORE TRAINING — random, usually wrong answers > can human read ? model: ? <unk> ... expected: human can read. [WRONG] > can cat read ? model: ? <unk> ... expected: cat cannot read. [WRONG]
Затем выполняются pre-training, SFT и adaptive SFT. В конце ответы становятся устойчивыми и правильными:
FINAL ANSWERS AFTER ADAPTIVE SFT > can human read ? model: human can read. [CORRECT] > can fish swim ? model: fish can swim. [CORRECT] > can bird fly ? model: bird can fly. [CORRECT] > can cat read ? model: cat cannot read. [CORRECT] Rehearsal controls preserved: 14/14. Stable criterion reached 11 times in a row.
Первоначальные ответы меняются, потому что веса инициализируются случайно. На тестовой машине полный запуск со скалярным autograd занимает около четырёх минут.
текст → word tokenization → token и position embeddings → два causal Transformer-блока → multi-head self-attention → FFN с двумя скрытыми слоями → LM head → softmax probabilities → следующий токен → cross-entropy loss → backpropagation → обновление весов Adam
Запуск состоит из трёх последовательных этапов.
Модель читает декларативные связи и предсказывает следующий токен:
human can read . fish can swim . bird can fly . dog cannot read .
Связь cat + read намеренно отсутствует.
SFT обучает формату вопроса и ответа на 42 парах:
question : can fish swim ? answer : fish can swim . question : is bird able to fly ? answer : bird can fly .
Loss считается только для токенов ответа. Каждая эпоха проходит каждую позицию ответа, поэтому примеры не пропускаются случайной выборкой.
Недостающая связь передаётся только как правильные target-токены:
['cat', 'cannot', 'read', '.']
Обучаются шесть связанных формулировок, например:
can cat read ? is cat able to read ? does the cat know how to read ?
Обучение останавливается, только когда:
все adaptive- и rehearsal-ответы правильны;
вероятность каждого правильного target-токена не ниже 95%;
полная проверка успешно проходит минимум 11 раз подряд.
Обучение только новым вопросам про кошку вызывало catastrophic forgetting: модель начинала возвращать ответ про кошку на посторонние вопросы. Rehearsal исправляет это, повторяя во время adaptive-этапа 14 ранее изученных связей can ... ?.
Центральная операция намеренно оставлена короткой:
function learnOneToken({ model, optimizer, input, targetId }) { const loss = model.loss(input, targetId); optimizer.zeroGrad(); loss.backward(); optimizer.step(); return loss.data; }
Её смысл:
loss = -log(P(правильный следующий токен | предыдущие токены))
backward() вычисляет dLoss/dWeight для всех участвовавших весов. Затем Adam изменяет веса так, чтобы вероятность правильного токена стала выше.
Каждый нейрон вычисляет знакомую формулу:
output = activation(sum(input[i] × weight[i]) + bias)
Linear — обычный массив таких нейронов. Каждый вес и bias является объектом Value, поэтому операции создают вычислительный граф для backpropagation.
Для каждого токена attention создаёт Query, Key и Value:
Q = X × Wq K = X × Wk V = X × Wv score = dot(Q, K) / sqrt(headSize) attention = softmax(score) output = attention × V
Causal-цикл рассматривает только текущую и предыдущие позиции, поэтому модель не видит будущий target. После attention каждый токен проходит через:
dModel → hidden ReLU → hidden ReLU → dModel
Оба подслоя окружены residual connections и LayerNorm.
Каждый запуск автоматически создаёт:
logs/training-log.txt
Лог представляет собой последовательную ASCII-схему, а не сырой JSON:
текст -> tokenizer -> embeddings -> Transformer blocks -> LM head -> softmax | +-> pre-training -> SFT -> adaptive SFT +-- TRANSFORMER BLOCK 0 / FFN / HIDDEN 1 | строка w[00] w[01] ... bias | neuron[0] +0.123456 -0.234567 ... +0.010000 | ... | LARGEST CHANGE: neuron[3] / weight[7] | before +0.120000 -> after +0.180000 -> delta +0.060000
В нём по порядку записано каждое событие обучения, начальные матрицы, все матрицы после pre-training/SFT и adaptive SFT, а также точная delta-матрица каждого слоя. Строки подписаны как token[n], position[n] или neuron[n], поэтому изменение можно проследить до слоя, нейрона и номера веса.
src/value.js — скалярный autograd и backpropagation.
src/nn.js — Neuron, Linear и LayerNorm.
src/model.js — embeddings, attention, Transformer-блоки, FFN и LM head.
src/tokenizer.js — word-level tokenization.
src/corpus.js — единственный набор для pre-training, SFT, adaptive и контроля.
src/optimizer.js — Adam и gradient clipping.
src/training-log.js — снимки модели и полная ASCII-трассировка матриц.
src/train.js — единый читаемый pipeline обучения.
test/model.test.js — проверки autograd, нейрона, токенизации и вероятностей.
Да по архитектуре и механике обучения, но нет по промышленному масштабу.
Этот проект | Production LLM |
|---|---|
Word-level словарь из 24 токенов | Десятки или сотни тысяч subword-токенов |
2 160 параметров | Миллионы или миллиарды параметров |
Два Transformer-блока | Десятки или сотни блоков |
Скалярный JavaScript autograd | Тензорный autograd на GPU/TPU |
Маленький структурированный dataset | Огромные подготовленные корпуса |
Узкое обученное поведение | Широкий язык, знания и reasoning |
Основной причинный путь настоящий:
токен → embedding → attention → FFN → logits → probability → loss → gradient → новый вес → изменившееся поведение
Слово «tiny» описывает масштаб, а не другой смысл обучения.
Репозиторий: tiny-language-model-neuro-js.