បង្កើត Language Model តូចមួយ
មាតិកា
- Language Model គឺជាតារាងនៃឱកាស
- រាប់ថាពាក្យណាមកក្រោយពាក្យណា
- ពីចំនួនរាប់ទៅជាប្រូបាប
- លេខចៃដន្យដែលអាចធ្វើឡើងវិញបាន
- រមៀលគ្រាប់ឡុកឡាក់ដែលមានទម្ងន់
- បង្កើតអត្ថបទ
- ហេតុអ្វីអត្ថបទស្តាប់ទៅចម្លែក
- Context កាន់តែច្រើន អត្ថបទកាន់តែល្អ
- តម្លៃនៃ Context កាន់តែច្រើន
- រឿងនេះខុសពី LLM ពិតយ៉ាងដូចម្តេច
- រូបមន្ត៖ Module bigram និង rng
- សាកល្បងដោយខ្លួនឯង
- សំណួរដែលអ្នកនឹងត្រូវគេសួរ
1. Language Model គឺជាតារាងនៃឱកាស
នៅក្នុង LLM គឺជាអ្វី? អ្នកបានឃើញ loop ដែលជាបេះដូងនៃ language model ទាំងអស់៖ ទាយបំណែកបន្ទាប់ បន្ថែមវា ធ្វើម្តងទៀត។ នៅក្នុង Token និង Tokenizer អ្នកបានឃើញថាអត្ថបទត្រូវបានកាត់ជាបំណែកតូចៗហៅថា token។
ឥឡូវនេះយើងបង្កើតផ្នែកកណ្តាល គឺផ្នែកដែលធ្វើការ ទាយ។ យើងនឹងបង្កើតកំណែតូចបំផុត ដែលស្មោះត្រង់នៃវា គឺ bigram model។ Bigram គឺជាគូនៃពាក្យជិតគ្នា។ ចំណេះដឹងទាំងអស់របស់ model គឺជាសំណួរតែមួយ៖
បើខ្ញុំទើបឃើញពាក្យមួយ តើពាក្យអ្វីជាធម្មតាមកបន្ទាប់ ហើយញឹកញាប់ប៉ុណ្ណា?
ប៉ុណ្ណឹងហើយ។ គ្មាន neural network គ្មានគណិតវិទ្យាអ្វីក្រៅពីការរាប់ និងការចែកទេ។ ប៉ុន្តែវាជា language model ពិតប្រាកដ៖ អ្វីដែលផ្តល់ប្រូបាបទៅឲ្យពាក្យបន្ទាប់ដែលអាចមានទាំងអស់។ Model ធំៗដែលអ្នកប្រើជារៀងរាល់ថ្ងៃធ្វើការងារដូចគ្នា។ គ្រាន់តែវាមើល context ច្រើនជាងឆ្ងាយ ប្រើយន្តការឆ្លាតជាងឆ្ងាយ ហើយរៀនពីអត្ថបទច្រើនជាងឆ្ងាយ។
ត្រូវប្រាកដថាអ្នកមាន folder llmlab ពីជំពូកមុនៗ។ ជំពូកនេះបន្ថែម src/rng.js និង
src/bigram.js (បូករួមទាំង src/ngram.js តូចមួយនៅចុងបញ្ចប់)។ កូដទាំងអស់ជា JavaScript
ESM ធម្មតា ដូច្នេះដំណើរការគ្រប់ file ដោយ node ឧទាហរណ៍ node demo-counts.js។
2. រាប់ថាពាក្យណាមកក្រោយពាក្យណា
ចាំ src/corpus.js ពីជំពូកទី ១ ទេ៖ វា export corpus (អត្ថបទខ្លីអំពីហាងកាហ្វេរបស់សុខា)
និង words(text) ដែលបំប្លែងអត្ថបទទៅជាអក្សរតូច ហើយបំបែកវាជាបញ្ជីពាក្យ។ បញ្ជីនោះគឺជា
training data របស់យើង។
ដើម្បី "train" bigram model ត្រូវដើរកាត់ពាក្យម្តង។ សម្រាប់គ្រប់គូ (ពាក្យបច្ចុប្បន្ន
ពាក្យបន្ទាប់) ត្រូវបន្ថែមមួយទៅ counter។ រក្សាទុក counter ក្នុង Map នៃ Map៖
key ខាងក្រៅគឺពាក្យបច្ចុប្បន្ន ហើយ map ខាងក្នុងផ្ទុកពាក្យបន្ទាប់នីមួយៗ និងចំនួនដងដែលវាមកតាមក្រោយ។
// src/bigram.js (first part)
export function trainBigram(tokens) {
const model = new Map();
for (let i = 0; i < tokens.length - 1; i++) {
const current = tokens[i];
const next = tokens[i + 1];
if (!model.has(current)) model.set(current, new Map());
const counts = model.get(current);
counts.set(next, (counts.get(next) ?? 0) + 1);
}
return model;
}
Loop បញ្ឈប់ត្រឹម length - 1 ព្រោះពាក្យចុងក្រោយគ្មានពាក្យបន្ទាប់ដើម្បីរៀនទេ។
?? 0 មានន័យថា "បើយើងមិនដែលឃើញគូនេះទេ ចាប់ផ្តើមពីសូន្យ"។ សាកល្បងមើល។
រក្សាទុកនេះជា demo-counts.js ក្នុង folder llmlab៖
import { corpus, words } from "./src/corpus.js";
import { trainBigram } from "./src/bigram.js";
const tokens = words(corpus);
const model = trainBigram(tokens);
console.log(tokens.length);
console.log(new Set(tokens).size);
console.log(model.get("coffee"));
console.log(model.get("sokha"));
// 182
// 67
// Map(5) {
// 'shop' => 7,
// 'with' => 1,
// 'and' => 2,
// 'sokha' => 1,
// 'after' => 1
// }
// Map(5) {
// 'opens' => 1,
// 'makes' => 1,
// 'likes' => 1,
// 'says' => 1,
// 'cleans' => 1
// }
Corpus របស់យើងមាន ១៨២ ពាក្យ ប៉ុន្តែមានតែ ៦៧ ពាក្យខុសគ្នា។ បន្ទាប់ពី "coffee" ពាក្យ "shop" មក ៧ ដង "and" ពីរដង ហើយពាក្យផ្សេងទៀតបីមួយដងម្នាក់។ បន្ទាប់ពី "sokha" មានកិរិយាសព្ទប្រាំផ្សេងគ្នាលេចឡើង ម្តងម្នាក់។ លទ្ធផលទាំងអស់នេះថេរ ព្រោះមិនទាន់មានភាពចៃដន្យនៅឡើយ។
Map នោះ គឺជា model។ បើអ្នកបោះពុម្ពវាទាំងអស់ អ្នកនឹងឃើញអ្វីៗទាំងអស់ដែល model ដឹង។
នេះជាមេរៀនធំដំបូង៖ language model គឺជា ស្ថិតិដែលបានរៀនអំពីអត្ថបទ រក្សាទុកក្នុងរចនាសម្ព័ន្ធទិន្នន័យមួយ។
ក្នុង bigram model រចនាសម្ព័ន្ធនោះជាតារាងដែលអ្នកអានដោយភ្នែកបាន។ ក្នុង LLM ពិតវាជាលេខរាប់ពាន់លាន
ដែលអ្នកអានមិនបាន ប៉ុន្តែការងារគឺដូចគ្នា។
3. ពីចំនួនរាប់ទៅជាប្រូបាប
ចំនួនរាប់មិនទាន់ជាប្រូបាបទេ។ "shop មក ៧ ដង" មិនសូវមានន័យទេ រហូតដល់អ្នកចែកនឹងចំនួនសរុប៖ បន្ទាប់ពី "coffee" corpus មាន 7 + 2 + 1 + 1 + 1 = 12 ពាក្យបន្ទាប់ ដូច្នេះ "shop" មានប្រូបាប 7 / 12 ប្រហែល 58%។
បន្ថែម function ដែលធ្វើការចែកនេះ ហើយតម្រៀបចម្លើយពីអាចកើតឡើងច្រើនបំផុតទៅតិចបំផុត៖
// src/bigram.js (second part)
export function nextWordProbs(model, word) {
const counts = model.get(word);
if (!counts) return [];
let total = 0;
for (const c of counts.values()) total += c;
return [...counts.entries()]
.map(([next, count]) => ({ word: next, count, prob: count / total }))
.sort((a, b) => b.count - a.count || a.word.localeCompare(b.word));
}
បើពាក្យមិនមានក្នុង model ទាល់តែសោះ យើងត្រឡប់បញ្ជីទទេ៖ model មិនដែលឃើញវាទេ ដូច្នេះវាគ្មានយោបល់។
ការតម្រៀបដាក់ចំនួនរាប់ខ្ពស់បំផុតមុន ហើយប្រើពាក្យខ្លួនឯងដើម្បីបំបែកករណីស្មើគ្នា ដូច្នេះលំដាប់គឺដូចគ្នា
នៅលើកុំព្យូទ័រគ្រប់គ្នា។ រក្សាទុក demo-probs.js៖
import { corpus, words } from "./src/corpus.js";
import { trainBigram, nextWordProbs } from "./src/bigram.js";
const model = trainBigram(words(corpus));
for (const word of ["coffee", "dara"]) {
console.log(`after "${word}":`);
for (const o of nextWordProbs(model, word)) {
const pct = (o.prob * 100).toFixed(0).padStart(3);
console.log(` ${o.word.padEnd(8)} ${o.count} time(s) ${pct}%`);
}
}
console.log(nextWordProbs(model, "banana"));
console.log(nextWordProbs(model, "app"));
// after "coffee":
// shop 7 time(s) 58%
// and 2 time(s) 17%
// after 1 time(s) 8%
// sokha 1 time(s) 8%
// with 1 time(s) 8%
// after "dara":
// writes 2 time(s) 29%
// drinks 1 time(s) 14%
// is 1 time(s) 14%
// says 1 time(s) 14%
// thinks 1 time(s) 14%
// walks 1 time(s) 14%
// []
// [
// { word: 'and', count: 1, prob: 0.2 },
// { word: 'because', count: 1, prob: 0.2 },
// { word: 'helps', count: 1, prob: 0.2 },
// { word: 'needs', count: 1, prob: 0.2 },
// { word: 'saves', count: 1, prob: 0.2 }
// ]
មើលអ្វីដែល model "ជឿ"។ បន្ទាប់ពី "coffee" វារំពឹង "shop" ច្រើនជាងពាក់កណ្តាលនៃពេលវេលា។ បន្ទាប់ពី "dara" វាគិតថា "writes" ទំនងបំផុត ប៉ុន្តែកិរិយាសព្ទប្រាំផ្សេងទៀតក៏អាចកើតឡើងដែរ។ ពាក្យដែលវាមិនដែលឃើញ ("banana") ទទួលបានបញ្ជីទទេ៖ នេះជាភាពខ្សោយពិតប្រាកដ ហើយជាតម្រុយដំបូងអំពីមូលហេតុដែល model ពិតប្រើ sub-word token (ពីជំពូក token) ដែលគ្មានអ្វីមិនស្គាល់ទាំងស្រុងឡើយ។
លទ្ធផលទាំងអស់នេះថេរ។ ភាគរយត្រូវបានបង្គត់ទៅជាចំនួនគត់ ដូច្នេះវាបូកបានប្រហែល 100 មិនតែងតែ 100 ពិតប្រាកដទេ។
បញ្ជីប្រូបាបនេះគឺជា លទ្ធផលរបស់ language model។ រាល់ LLM ចាប់ពី model កំប្លែងនេះ ដល់ model hostedធំបំផុត បញ្ចប់រាល់ជំហានដោយបញ្ជីបែបនេះ៖ សម្រាប់រាល់ token បន្ទាប់ដែលអាចមាន មានប្រូបាបមួយ។ ជំពូកបន្ទាប់ ប្រូបាប Temperature និង Sampling និយាយទាំងស្រុងអំពីអ្វីដែលអ្នកធ្វើជាមួយបញ្ជីនោះ។
4. លេខចៃដន្យដែលអាចធ្វើឡើងវិញបាន
ដើម្បីជ្រើសពាក្យបន្ទាប់ យើងត្រូវការភាពចៃដន្យ៖ បើ model ជ្រើស "shop" 58% នៃពេលវេលា
និង "and" 17% នៃពេលវេលា យើងត្រូវការលេខចៃដន្យដើម្បីសម្រេច។ Math.random() ប្រើបាន
ប៉ុន្តែគ្រប់ការរត់ផ្តល់អត្ថបទខុសគ្នា ហើយនោះធ្វើឲ្យមេរៀន ការធ្វើតេស្ត និងរបាយការណ៍ bug
លំបាក។ អ្នកមិនអាចនិយាយថា "មើលបន្ទាត់ទីបីនៃលទ្ធផល" បានទេ ពេលលទ្ធផលប្តូរគ្រប់ដង។
ដំណោះស្រាយគឺ random number generator ដែលមាន seed។ វាមើលទៅចៃដន្យ ប៉ុន្តែអ្នកផ្តល់លេខចាប់ផ្តើម (seed) ហើយ seed ដដែលតែងតែផលិតលំដាប់ដូចគ្នា។ យើងប្រើ generator តូច និងល្បីមួយឈ្មោះ mulberry32។ អ្នកមិនចាំបាច់យល់ល្បិច bit របស់វាទេ។ ចាត់ទុកវាដូចប្រអប់មួយ៖ ដាក់ seed ចូល ទទួលបាន function មួយដែលត្រឡប់លេខពី 0 ឡើងទៅ (ប៉ុន្តែមិនរួមបញ្ចូល) 1។
// src/rng.js
export function mulberry32(seed) {
let a = seed >>> 0;
return function () {
a = (a + 0x6d2b79f5) >>> 0;
let t = a;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
វាត្រឡប់ function មួយ ដូច្នេះរាល់ការហៅ function នោះផ្តល់លេខបន្ទាប់ក្នុងលំដាប់។
រក្សាទុក demo-rng.js៖
import { mulberry32 } from "./src/rng.js";
const a = mulberry32(42);
console.log(a(), a(), a());
const b = mulberry32(42);
console.log(b(), b(), b());
const c = mulberry32(43);
console.log(c());
// 0.6011037519201636 0.44829055899754167 0.8524657934904099
// 0.6011037519201636 0.44829055899754167 0.8524657934904099
// 0.9998110907617956
ពីរបន្ទាត់ដំបូងដូចគ្នា ព្រោះ generator ទាំងពីរចាប់ផ្តើមពី seed 42។ Seed 43 ផ្តល់លេខខុសគ្នាទាំងស្រុង។ រាល់បន្ទាត់នៃលទ្ធផលនៅទីនេះថេរ៖ នេះគឺជាចំណុចទាំងមូលនៃ seed។
សេវា LLM ពិតធ្វើការដូចគ្នាតាមស្មារតី។ ពួកវាប្រើ random number generator ដើម្បីជ្រើស token។ ខ្លះអនុញ្ញាតឲ្យអ្នកកំណត់ seed ដើម្បីឲ្យលទ្ធផលអាចធ្វើឡើងវិញបានច្រើនជាង ហើយយើងនឹងត្រឡប់មកវិញ នៅជំពូកបន្ទាប់។
5. រមៀលគ្រាប់ឡុកឡាក់ដែលមានទម្ងន់
ឥឡូវនេះយើងបំប្លែងលេខចៃដន្យទៅជាជម្រើស។ ស្រមៃមើលបន្ទាត់មួយប្រវែង 1 ឯកតាដែលកាត់ជាបំណែកៗ។ បន្ទាប់ពី "coffee" បន្ទាត់មានបំណែក 0.58 សម្រាប់ "shop" បំណែក 0.17 សម្រាប់ "and" និងបំណែក 0.08 បី។ បោះព្រួញទៅចំណុចចៃដន្យមួយចន្លោះ 0 និង 1 ហើយមើលថាវាត្រូវបំណែកណា។ បំណែកធំជាងត្រូវញឹកញាប់ជាង ដែលជាអត្ថន័យពិតប្រាកដនៃ "មានលទ្ធភាព 58%"។
ក្នុងកូដ ដើរកាត់ជម្រើសដោយបូកប្រូបាបរបស់វា (ផលបូកកំពុងរត់)។ លើកដំបូងដែលផលបូកកំពុងរត់ឡើង លើស លេខចៃដន្យ ជម្រើសនោះឈ្នះ៖
function pickWeighted(items, r) {
let running = 0;
for (const item of items) {
running += item.prob;
if (r < running) return item.word;
}
return items[items.length - 1].word; // guards against rounding error
}
បន្ទាត់ចុងក្រោយសំខាន់តែពេលការបង្គត់ធ្វើឲ្យប្រូបាបបូកបាន 0.9999999 ជំនួសឲ្យ 1
ហើយ r ចៃដន្យធ្លាក់ក្នុងចន្លោះតូចនោះ។ វាជាសំណាញ់សុវត្ថិភាព និងជាទម្លាប់ល្អជាមួយចំនួន
floating point។
Function តូចនេះ ("ការជ្រើសរើសចៃដន្យដែលមានទម្ងន់") ហៅថា sampling។ អ្នកនឹងជួបវាម្តងទៀតនៅជំពូកទី ៤ ក្រោមឈ្មោះនោះ។
6. បង្កើតអត្ថបទ
អ្វីៗរួចរាល់សម្រាប់ loop ពីជំពូកទី ១។ ចាប់ផ្តើមជាមួយពាក្យមួយ រកមើលប្រូបាបពាក្យបន្ទាប់របស់វា រមៀលគ្រាប់ឡុកឡាក់ដែលមានទម្ងន់ បន្ថែមអ្នកឈ្នះ ហើយធ្វើម្តងទៀតដោយយកពាក្យថ្មីជាពាក្យបច្ចុប្បន្ន៖
// src/bigram.js (third part)
export function generate(model, startWord, length, rng = Math.random) {
const out = [startWord];
let current = startWord;
for (let i = 1; i < length; i++) {
const options = nextWordProbs(model, current);
if (options.length === 0) break; // dead end: word never had a successor
current = pickWeighted(options, rng());
out.push(current);
}
return out.join(" ");
}
Parameter rng មានតម្លៃលំនាំដើម Math.random ដូច្នេះក្នុង app ពិតអ្នកទទួលបានអត្ថបទខុសគ្នា
គ្រប់ដង ប៉ុន្តែក្នុង demo របស់យើង យើងបញ្ជូន generator ដែលមាន seed។ រក្សាទុក
demo-generate.js៖
import { corpus, words } from "./src/corpus.js";
import { trainBigram, generate } from "./src/bigram.js";
import { mulberry32 } from "./src/rng.js";
const model = trainBigram(words(corpus));
for (const seed of [1, 2, 3]) {
console.log(seed, generate(model, "the", 14, mulberry32(seed)));
}
console.log(generate(model, "dara", 14, mulberry32(7)));
console.log(generate(model, "banana", 5, mulberry32(7)));
console.log(generate(model, "app", 5, mulberry32(7)));
// 1 the river and the small coffee shop needs good app saves time every morning
// 2 the morning and the app saves time every morning the coffee after lunch sokha
// 3 the morning and the coffee after lunch sokha cleans the river and dara says
// dara writes code for a phone sokha makes iced coffee and dara writes code
// banana
// app and dara walks home
អ្នកទើបតែបង្កើតកម្មវិធីបង្កើតអត្ថបទ។ Seed បីផ្តល់ប្រយោគបីខុសគ្នា ហើយនីមួយៗអាចធ្វើឡើងវិញបាន៖
រត់ file ម្តងទៀត អ្នកនឹងទទួលបានបន្ទាត់ដដែល។ បន្ទាត់ "banana" បង្ហាញផ្លូវទាល់៖
model មិនស្គាល់ពាក្យនោះ ដូច្នេះវាឈប់បន្ទាប់ពីពាក្យចាប់ផ្តើម។ បើអ្នកដក seed ចេញ
(ប្រើ Math.random) លទ្ធផលរបស់អ្នកនឹងប្រែប្រួលគ្រប់ការរត់ ប៉ុន្តែគ្រប់ប្រយោគនៅតែត្រូវបានបង្កើត
ពីតារាងដដែល។
7. ហេតុអ្វីអត្ថបទស្តាប់ទៅចម្លែក
អានលទ្ធផលដំបូងយឺតៗ៖ "the river and the small coffee shop needs good app saves time every morning"។ គ្រប់ គូ នៃពាក្យជិតគ្នាសុទ្ធតែល្អ៖ "the river", "small coffee", "needs good", "saves time"។ ប៉ុន្តែប្រយោគទាំងមូលគ្មានន័យ។ "Coffee shop needs good app saves time" មិនមែនជាប្រយោគអង់គ្លេសត្រឹមត្រូវតាមវេយ្យាករណ៍ទេ។
មូលហេតុគឺ model មាន ការចងចាំមួយពាក្យ។ ពេលវាជ្រើសពាក្យបន្ទាប់ពី "shop" វាមើលតែ "shop" ប៉ុណ្ណោះ។ វាភ្លេចទាំងស្រុងថាប្រយោគចាប់ផ្តើមដោយ "the river" ឬថាប្រយោគត្រូវការកិរិយាសព្ទ ហើយបន្ទាប់មក ចុងបញ្ចប់។ ជំហាននីមួយៗសមហេតុផលក្នុងតំបន់ ប៉ុន្តែគ្មានអ្វីរក្សាទាំងមូលឲ្យនៅលើផ្លូវទេ។ អត្ថបទរសាត់ទៅ ដូចអ្នកដែលកំពុងប្រាប់រឿងហើយភ្លេចអ្វីដែលខ្លួនបាននិយាយពីរពាក្យមុន។
បញ្ហាបីលេចឡើងក្នុងលទ្ធផល៖
- គ្មានរចនាសម្ព័ន្ធចម្ងាយឆ្ងាយ។ ប្រយោគវង្វេង ហើយមិនដែលចប់។
- ប្រធានបទរសាត់។ អត្ថបទលោតពីទន្លេទៅ app ទៅអាហារថ្ងៃត្រង់។
- ចម្លង និងភ្ជាប់។ អ្វីៗទាំងអស់មកពីអត្ថបទ training។ Model អាចត្រឹមតែផ្សំឡើងវិញនូវអ្វីដែលវាបានឃើញ។ វាមិនអាចបង្កើតពាក្យ ដែលវាមិនដែលជួបបានទេ។
ទាំងនេះគឺជាបញ្ហាដែល context ច្រើនជាងជួយបាន ដែលជាគំនិតបន្ទាប់។
8. Context កាន់តែច្រើន អត្ថបទកាន់តែល្អ
បើ model ចងចាំ ពីរ ពាក្យចុងក្រោយជំនួសឲ្យមួយ? នោះគឺជា trigram model។ ឬបីពាក្យ៖
4-gram។ ជាទូទៅ n-gram model មើល n ពាក្យចុងក្រោយ។ កូដប្តូរតិចតួចណាស់។
ជំនួសឲ្យការប្រើពាក្យមួយជា key ត្រូវភ្ជាប់ n ពាក្យចុងក្រោយជា string មួយ។ បង្កើត
src/ngram.js៖
// src/ngram.js
export function trainNgram(tokens, n) {
const model = new Map();
for (let i = 0; i + n < tokens.length; i++) {
const context = tokens.slice(i, i + n).join(" ");
const next = tokens[i + n];
if (!model.has(context)) model.set(context, new Map());
const counts = model.get(context);
counts.set(next, (counts.get(next) ?? 0) + 1);
}
return model;
}
export function generateNgram(model, n, startWords, length, rng = Math.random) {
const out = [...startWords];
while (out.length < length) {
const context = out.slice(-n).join(" ");
const counts = model.get(context);
if (!counts) break; // never seen this context
const entries = [...counts.entries()];
const total = entries.reduce((sum, [, c]) => sum + c, 0);
let r = rng() * total;
let chosen = entries[entries.length - 1][0];
for (const [word, count] of entries) {
r -= count;
if (r < 0) { chosen = word; break; }
}
out.push(chosen);
}
return out.join(" ");
}
នៅទីនេះ trainNgram(tokens, 1) គឺជា bigram model របស់យើងបេះបិទ គ្រាន់តែមាន key ជា string
ហើយ trainNgram(tokens, 2) គឺជា trigram model។ (កំណែនេះជ្រើសពាក្យដោយដកចំនួនរាប់ចេញពី
r * total ដែលជាគ្រាប់ឡុកឡាក់ដែលមានទម្ងន់ដូចមុន ក្នុងរូបរាងផ្សេង។) ប្រៀបធៀបពួកវាក្នុង
demo-ngram.js៖
import { corpus, words } from "./src/corpus.js";
import { trainNgram, generateNgram } from "./src/ngram.js";
import { mulberry32 } from "./src/rng.js";
const tokens = words(corpus);
for (const n of [1, 2, 3]) {
const model = trainNgram(tokens, n);
let options = 0;
for (const counts of model.values()) options += counts.size;
const avg = (options / model.size).toFixed(2);
console.log(`n=${n}: ${model.size} contexts, ${avg} choices per context`);
}
const one = trainNgram(tokens, 1);
const two = trainNgram(tokens, 2);
console.log(generateNgram(one, 1, ["the"], 16, mulberry32(11)));
console.log(generateNgram(two, 2, ["the", "coffee"], 16, mulberry32(11)));
console.log(generateNgram(two, 2, ["the", "students"], 16, mulberry32(3)));
console.log(generateNgram(two, 2, ["dara", "writes"], 16, mulberry32(3)));
// How many 4-word windows in the output appear word-for-word in the corpus?
const text = tokens.join(" ");
function copied(model, n, start) {
let hit = 0, total = 0;
for (let seed = 1; seed <= 20; seed++) {
const out = generateNgram(model, n, start, 20, mulberry32(seed)).split(" ");
for (let i = 0; i + 4 <= out.length; i++) {
total++;
if (text.includes(out.slice(i, i + 4).join(" "))) hit++;
}
}
return `${hit} of ${total}`;
}
console.log("bigram copied 4-word runs:", copied(one, 1, ["the"]));
console.log("trigram copied 4-word runs:", copied(two, 2, ["the", "coffee"]));
// n=1: 67 contexts, 1.94 choices per context
// n=2: 129 contexts, 1.25 choices per context
// n=3: 160 contexts, 1.07 choices per context
// the students order more iced tea the river is a phone sokha cleans the table by
// the coffee shop after school the students order iced coffee and the students laugh and the
// the students share a table dara says that a good table dara is a student and
// dara writes code at the table by the window dara writes code at the table by
// bigram copied 4-word runs: 130 of 340
// trigram copied 4-word runs: 294 of 340
អានលេខឲ្យយកចិត្តទុកដាក់ ព្រោះវាប្រាប់រឿងពិត៖
- ជាមួយ context ច្រើនជាង model មាន context ច្រើនជាង (67 បន្ទាប់មក 129 បន្ទាប់មក 160) និង ជម្រើសតិចជាង ក្នុងមួយៗ (1.94 បន្ទាប់មក 1.25 បន្ទាប់មក 1.07)។ Model កាន់តែប្រាកដ។
- អត្ថបទ trigram ស្ទាត់ជាង៖ "the coffee shop after school the students order iced coffee and the students laugh" អានស្ទើរតែដូចប្រយោគពិត ហើយ "dara writes code at the table by the window" ជាបន្ទាត់ស្អាត និងសមហេតុផល។
- ប៉ុន្តែពីរបន្ទាត់ចុងក្រោយបង្ហាញតម្លៃ។ ក្នុងចំណោមការរត់ ៤ ពាក្យដែល trigram ផលិត 294 ក្នុង 340 (ប្រហែល 86%) ត្រូវបានចម្លងពាក្យតាមពាក្យពីអត្ថបទ training។ Bigram ចម្លងបានតែ 130 ក្នុង 340 (ប្រហែល 38%)។ ម៉្យាងទៀត model ប្រើ context កាន់តែច្រើន វាកាន់តែ ធ្វើឡើងវិញនូវទិន្នន័យ training របស់វា។
បន្ទាត់ទាំងអស់ខាងលើថេរ ព្រោះគ្រប់ generator សុទ្ធតែមាន seed។
9. តម្លៃនៃ Context កាន់តែច្រើន
ហេតុអ្វីមិនប្រើ context 10 ពាក្យ? មើលតារាងម្តងទៀត៖ n=3 មាន 160 context
ហើយមានជម្រើសតែ 1.07 ក្នុងមួយៗ។ ស្ទើរតែគ្រប់ context ត្រូវបានឃើញតែម្តងប៉ុណ្ណោះ
ដែលមានន័យថា model អាចនិយាយបានតែរឿងមួយ៖ អ្វីដែលមកតាមក្រោយវាក្នុងអត្ថបទ training។
នោះគឺជាការទន្ទេញ មិនមែនការរៀនទេ។
នេះគឺជាការដោះដូរចម្បងនៃ language modelling៖
| ប្រវែង context | អ្វីដែលកើតឡើង |
|---|---|
| ខ្លី (1 ពាក្យ) | ជម្រើសច្រើន អត្ថបទមានភាពច្នៃប្រឌិតប៉ុន្តែគ្មានភាពស៊ីសង្វាក់ |
| មធ្យម (2-3 ពាក្យ) | ស្ទាត់ក្នុងតំបន់ ចាប់ផ្តើមចម្លងទិន្នន័យ training |
| វែង (10+ ពាក្យ) | ស្ទើរតែគ្រប់ context ថ្មី តារាងគ្មានចម្លើយ |
ជាមួយ context វែង មានបញ្ហាទីពីរ៖ context ភាគច្រើនមិនដែលលេចឡើងក្នុង training។
សួរតារាង context 10 ពាក្យអំពីប្រយោគដែលវាមិនដែលឃើញ ហើយ model.get(context)
ត្រឡប់ undefined។ n-gram model គ្មានវិធីនិយាយថា "នេះ ស្រដៀង នឹងអ្វីដែលខ្ញុំបានឃើញ" ទេ។
វាផ្គូផ្គងតែពាក្យពិតប្រាកដ ដូច្នេះតារាងធំសម្បើម ហើយជួរភាគច្រើនរបស់វាទទេ ឬប្រើតែម្តង។
កំផែងនេះគឺជាមូលហេតុដែល n-gram model ឈប់នៅ context តូច និងមូលហេតុដែលអ្នកស្រាវជ្រាវ ងាកទៅរក neural network។ Neural network មិនរក្សាទុកមួយជួរក្នុងមួយ context ទេ។ វារៀនសំណុំលេខតូចមួយដែលអនុញ្ញាតឲ្យវានិយាយថា "context នេះ ដូច context ទាំងនោះដែលខ្ញុំបានឃើញ" ទោះបីជាពាក្យពិតប្រាកដថ្មីក៏ដោយ។ នោះជារបៀបដែល model សម័យទំនើបអាន context រាប់ពាន់ពាក្យ ដោយមិនមានតារាងដែលពេញសកលលោក។ យើងបង្កើតការចាប់ផ្តើមនៃគំនិតនោះនៅក្នុង របៀប Train Model មួយ ហើយមើលរបៀបដែលពាក្យក្លាយជាលេខដែលអាចប្រៀបធៀបបាននៅក្នុង Embedding និង Attention។
10. រឿងនេះខុសពី LLM ពិតយ៉ាងដូចម្តេច
Model កំប្លែងរបស់យើង និង LLM ពិតមាន interface ដូចគ្នា៖ context ចូល ប្រូបាបសម្រាប់ token បន្ទាប់ដែលអាចមានគ្រប់ទាំងអស់ចេញ។ នៅខាងក្នុងពួកវាខុសគ្នាច្រើន។ នេះជាការប្រៀបធៀបដ៏ស្មោះត្រង់៖
| Bigram model របស់យើង | LLM ពិត | |
|---|---|---|
| ឯកតា | ពាក្យទាំងមូល | Sub-word token |
| Context | 1 ពាក្យ (ឬ n) | រាប់ពាន់ token ឬច្រើនជាងនេះ |
| ចំណេះដឹងរក្សាទុកក្នុង | តារាងនៃចំនួនរាប់ | លេខដែលបានរៀនរាប់ពាន់លាន |
| រៀនដោយ | រាប់ម្តង | ជុំជាច្រើននៃការកែលេខ (training) |
| Context ដែលមិនធ្លាប់ឃើញ | គ្មានចម្លើយ | ផ្តល់ចម្លើយផ្អែកលើ context ស្រដៀងគ្នា |
| អត្ថបទ training | 182 ពាក្យ | អត្ថបទច្រើនខ្លាំងណាស់ |
Model ពាក្យទាំងមូលដូចរបស់យើងគឺជាការធ្វើឲ្យសាមញ្ញ។ Model ពិតប្រើ sub-word token ដូច្នេះវាដោះស្រាយពាក្យណាក៏បាន សូម្បីតែពាក្យថ្មី។ Model ពិតក៏មិនមែនជាតារាង n-gram ពិតប្រាកដដែរ ហើយការហៅ Transformer ថា "bigram model ធំជាង" ជាការខុស។ ចំណុចនៃ model កំប្លែង គឺបង្ហាញ រូបរាង នៃការងារ (context ចូល បញ្ជីប្រូបាបចេញ បន្ទាប់មក sampling) ហើយឲ្យអ្នកឃើញរាល់លេខ។
11. រូបមន្ត៖ Module bigram និង rng
បន្ថែមឧបករណ៍នៃជំពូកនេះទៅ llmlab។ បង្កើត src/rng.js៖
// src/rng.js
// Seeded random number generator (mulberry32).
// Same seed -> same sequence, so demos are reproducible.
export function mulberry32(seed) {
let a = seed >>> 0;
return function () {
a = (a + 0x6d2b79f5) >>> 0;
let t = a;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
និង src/bigram.js ដែលបូកបញ្ចូលគ្នាបីផ្នែកពីជំពូកនេះ៖
// src/bigram.js
// A bigram language model: count which word follows which.
export function trainBigram(tokens) {
const model = new Map();
for (let i = 0; i < tokens.length - 1; i++) {
const current = tokens[i];
const next = tokens[i + 1];
if (!model.has(current)) model.set(current, new Map());
const counts = model.get(current);
counts.set(next, (counts.get(next) ?? 0) + 1);
}
return model;
}
// Turn the counts for one word into probabilities, biggest first.
export function nextWordProbs(model, word) {
const counts = model.get(word);
if (!counts) return [];
let total = 0;
for (const c of counts.values()) total += c;
return [...counts.entries()]
.map(([next, count]) => ({ word: next, count, prob: count / total }))
.sort((a, b) => b.count - a.count || a.word.localeCompare(b.word));
}
// Pick one item using probabilities and a number r in [0, 1).
function pickWeighted(items, r) {
let running = 0;
for (const item of items) {
running += item.prob;
if (r < running) return item.word;
}
return items[items.length - 1].word;
}
// Predict, append, repeat.
export function generate(model, startWord, length, rng = Math.random) {
const out = [startWord];
let current = startWord;
for (let i = 1; i < length; i++) {
const options = nextWordProbs(model, current);
if (options.length === 0) break; // dead end: word never had a successor
current = pickWeighted(options, rng());
out.push(current);
}
return out.join(" ");
}
Function សាធារណៈគឺ trainBigram(tokens), nextWordProbs(model, word) និង generate(model, startWord, length, rng)។ បើអ្នកមាន
src/index.js ត្រូវ re-export ពួកវា (export * from "./bigram.js"; និង
export * from "./rng.js";)។ ជំពូកទី ៤ import module ទាំងពីរ ដូច្នេះរក្សា
file ទាំងនេះឲ្យដូចអ្វីដែលបានបង្ហាញ។
12. សាកល្បងដោយខ្លួនឯង
- អាន model។ បោះពុម្ព
nextWordProbs(model, "iced")និងnextWordProbs(model, "good")។ តើពាក្យណាទំនងបំផុតក្នុងករណីនីមួយៗ? តើភាគរយបូកបាន 100 ទេ? - រកផ្លូវទាល់។ ពាក្យមួយជាផ្លូវទាល់ បើវាគ្មាន entry ក្នុង model។
ប្រៀបធៀបពាក្យចុងក្រោយនៃ
words(corpus)ជាមួយ key របស់ model។ តើវាជាផ្លូវទាល់ក្នុង corpus នេះទេ? តើអ្នកត្រូវប្តូរអ្វីក្នុងអត្ថបទដើម្បីបង្កើតមួយ ហើយgenerateធ្វើអ្វីពេលនោះ? - Seed ដូចគ្នា អត្ថបទដូចគ្នា។ ហៅ
generate(model, "the", 20, mulberry32(5))បីដងដោយប្រើ generator ថ្មីគ្រប់ដង។ បន្ទាប់មកហៅវាបីដង ដោយប្រើ generator តែមួយ ដែលចែករំលែក (const rng = mulberry32(5)) ហើយប្រៀបធៀប។ ពន្យល់ពីភាពខុសគ្នា។ - បន្ថែមអត្ថបទរបស់អ្នកផ្ទាល់។ ជំនួស corpus ដោយកថាខណ្ឌរបស់អ្នកផ្ទាល់ (ប្រមាណ 200 ពាក្យ) ហើយ train ម្តងទៀត។ តើអត្ថបទដែលបង្កើតស្តាប់ទៅដូចកថាខណ្ឌរបស់អ្នកទេ? តើមានអ្វីប្រែប្រួលបើអ្នកប្រើតែ 20 ពាក្យ?
- រកពាក្យចាប់ផ្តើម "ល្អបំផុត"។ សរសេរ loop ដែលបង្កើត 50 ប្រយោគនៃ 10 ពាក្យពីគ្រប់ពាក្យចាប់ផ្តើមក្នុង model ហើយរាប់ថាប៉ុន្មានប្រយោគឈប់មុនពេល។ តើពាក្យចាប់ផ្តើមណាមានហានិភ័យបំផុត?
- សាកល្បង 4-gram។ ប្រើ
trainNgram(tokens, 3)ហើយបង្កើតពី["the", "coffee", "shop"]។ តើវាបង្កើតលទ្ធផល 20 ពាក្យបានប៉ុន្មានមុនពេលវាជួប context ដែលវាមិនដែលឃើញ?
13. សំណួរដែលអ្នកនឹងត្រូវគេសួរ
នេះជា language model ពិតមែនទេ ឬគ្រាន់តែជាកំប្លែង? វាជា language model ពិតក្នុងន័យបច្ចេកទេស៖ វាផ្តល់ប្រូបាបទៅឲ្យពាក្យបន្ទាប់ដែលអាចមានគ្រប់ទាំងអស់ ដោយផ្អែកលើ context។ វាជាកំប្លែងក្នុងទំហំ និងអានុភាព។ Bigram model ដែលមានទិន្នន័យ training 182 ពាក្យ តូចណាស់ធៀបនឹង LLM ទំនើប ប៉ុន្តែសំណួរដែលទាំងពីរឆ្លើយគឺដូចគ្នា។
តើ model យល់អ្វីដែលវាសរសេរទេ? មិនមែនក្នុងន័យណាមួយដែលអ្នកហៅថាការយល់ទេ។ Model របស់យើងដឹងតែថាតើពាក្យមកតាមគ្នាញឹកញាប់ប៉ុណ្ណា។ ពេលវាសរសេរ "the coffee shop needs good app saves time" គ្មានអ្វីនៅខាងក្នុងវាពិនិត្យថានោះពិត ឬសូម្បីតែជាប្រយោគទេ។ ថាតើ model ធំៗ "យល់" ឬអត់ គឺជាសំណួរពិបាកជាងដែលយើងនឹងពិភាក្សាក្រោយក្នុងស៊េរីនេះ ហើយប្រាប់ត្រង់ៗ អ្នកវិទ្យាសាស្ត្រមិនយល់ស្របគ្នាទេ។
ហេតុអ្វីវាប្រើលេខចៃដន្យ? តើវាមិនអាចជ្រើសពាក្យល្អបំផុតបានទេ? វាអាចធ្វើបាន ហើយនោះហៅថា greedy choice។ បញ្ហាគឺការជ្រើសពាក្យទំនងបំផុតជានិច្ចផ្តល់អត្ថបទ គួរឲ្យធុញ និងដដែលៗ ហើយវាអាចជាប់ក្នុង loop។ ការជ្រើសចៃដន្យតាមសមាមាត្រនៃប្រូបាបផ្តល់ភាពចម្រុះ។ ជំពូកបន្ទាប់បង្ហាញរបៀបគ្រប់គ្រងរឿងនេះដោយ temperature។
តើអត្ថបទមកពីណា? តើវាត្រូវបានចម្លងទេ? អ្វីៗទាំងអស់ដែល model របស់យើងសរសេរត្រូវបានផ្គុំពីគូពាក្យក្នុងអត្ថបទ training របស់វា។ ជាមួយ context ច្រើនជាង វាចម្លងបំណែកវែងជាង៖ ក្នុងតេស្តរបស់យើង trigram បានចម្លងប្រហែល 86% នៃការរត់ 4 ពាក្យរបស់វា។ LLM ពិត generalize ច្រើនជាងតារាង n-gram ឆ្ងាយ ប៉ុន្តែពួកវាក៏អាចធ្វើឡើងវិញនូវអត្ថបទ training ដែរ ជាពិសេសអត្ថបទដែលលេចឡើងច្រើនដង។ នោះជាមូលហេតុមួយដែលសំណួរអំពីរក្សាសិទ្ធិ និងឯកជនភាពជុំវិញទិន្នន័យ training ត្រូវបានយកចិត្តទុកដាក់ខ្លាំង។
ហេតុអ្វីមិនប្រើ n ធំជាង ហើយទទួលបានអត្ថបទល្អឥតខ្ចោះ? ព្រោះ context វែងភាគច្រើនមិនដែលលេចឡើងក្នុងទិន្នន័យ training ដូច្នេះតារាងគ្មានចម្លើយសម្រាប់ពួកវា ហើយអ្វីដែលបានលេចឡើងត្រូវបានឃើញតែម្តងប៉ុណ្ណោះ ដែលមានន័យថា model គ្រាន់តែលេងឡើងវិញ។ Model ពិតដោះស្រាយរឿងនេះដោយរៀនលេខដែល generalize ជំនួសឲ្យការរក្សាទុកមួយជួរសម្រាប់គ្រប់ context ពិតប្រាកដ។
Seed គឺជាអ្វី ហើយហេតុអ្វីយើងប្រើវា? Seed គឺជាលេខចាប់ផ្តើមសម្រាប់ random number generator។ Seed ដូចគ្នា លេខ "ចៃដន្យ" ដូចគ្នា ដូច្នេះអ្នកទទួលបានលទ្ធផលដូចគ្នាគ្រប់ដង។ យើងប្រើវាដើម្បីឲ្យឧទាហរណ៍ក្នុងមេរៀននេះអាចធ្វើឡើងវិញបាន។ សេវា AI hostedក៏ប្រើ random sampling ដែរ ហើយខ្លះអនុញ្ញាតឲ្យអ្នកកំណត់ seed ប៉ុន្តែ seed តែមួយមិនតែងតែធានាលទ្ធផលដូចគ្នាពីប្រព័ន្ធធំhostedទេ។
តើ LLM ពិតធ្វើការបែបនេះទេ? ពួកវាចែករំលែករូបរាងដូចគ្នា៖ context ចូល បញ្ជីប្រូបាបសម្រាប់ token បន្ទាប់ចេញ បន្ទាប់មកជ្រើសមួយ ហើយធ្វើម្តងទៀត។ ខាងក្នុងខុសគ្នាខ្លាំង។ ពួកវាប្រើ sub-word token context រាប់ពាន់ token និង neural network ហៅថា Transformer ដែលរៀនលេខរបស់វាពីទិន្នន័យ ជំនួសឲ្យការរាប់គូក្នុងតារាង។
ទៅបន្តទៀត។ អ្នកមាន model ដែលបញ្ចេញបញ្ជីប្រូបាប ហើយអ្នកបានឃើញវាជ្រើសពីបញ្ជីនោះដោយគ្រាប់ឡុកឡាក់ដែលមានទម្ងន់។
ជំពូកបន្ទាប់បើកជំហាននោះឲ្យលម្អិត៖ របៀបដែលពិន្ទុក្លាយជាប្រូបាបដោយ softmax
អ្វីដែល temperature ធ្វើពិតប្រាកដ និងរបៀបដែល sampling top-k និង top-p កែរូបរាងលទ្ធផល។
បន្តជាមួយ ប្រូបាប Temperature និង
Sampling។
ទិញកាហ្វេជូនខ្ញុំ
ចូលចិត្តមេរៀននេះមែនទេ? សូមពិចារណាទិញកាហ្វេជូនខ្ញុំមួយកែវ ដើម្បីគាំទ្រការបង្កើតមាតិកាឥតគិតថ្លៃបន្ថែមទៀត!