Token និង Tokenizer
មាតិកា
- Model មិនអានពាក្យទេ
- វិធីបីយ៉ាងដើម្បីកាត់អត្ថបទ
- តួអក្សរ និងពាក្យក្នុងកូដ
- គំនិតនៃ Byte-Pair Encoding
- Train BPE ដោយដៃ
- Encoding និង Decoding
- រូបមន្ត៖ Module Tokenizer
- ដំណើរការវាលើ Corpus
- ឥទ្ធិពលចម្លែករបស់ Tokenizer
- ភាសាខ្មែរ និងភាសាផ្សេងទៀត
- Token គឺជាលុយ
- សាកល្បងដោយខ្លួនឯង
- សំណួរដែលអ្នកនឹងត្រូវគេសួរ
1. Model មិនអានពាក្យទេ
ក្នុង ជំពូកទី 1 យើងបាននិយាយថា model "ទាយពាក្យបន្ទាប់"។ នោះគឺជាការធ្វើឲ្យសាមញ្ញ។ model ពិតមិនឃើញពាក្យទេ ហើយក៏មិនឃើញអក្សរដែរ។ វាឃើញ token។
token គឺជាបំណែកអត្ថបទមួយ ហើយ model នីមួយៗមានបញ្ជីថេរនៃបំណែកទាំងអស់ដែលវាស្គាល់។ បញ្ជីនោះគឺជា
vocabulary។ បំណែកមួយអាចជាពាក្យទាំងមូល ( the) ផ្នែកនៃពាក្យ (ing) តួអក្សរតែមួយ (z)
សញ្ញាវណ្ណយុត្តិ ឬសូម្បីតែចន្លោះបូកនឹងពាក្យមួយ។ បំណែកនីមួយៗមានលេខមួយ ហៅថា token ID។
model មិនដែលប៉ះអត្ថបទទេ វាអានបញ្ជី ID ហើយទាយ ID នៃ token បន្ទាប់។
កម្មវិធីដែលបំប្លែងអត្ថបទទៅជា ID និងបំប្លែងត្រឡប់មកវិញ គឺជា tokenizer៖
"Dara drinks iced tea"
| tokenizer (encode)
v
[ "Dar", "a", " drinks", " iced", " tea" ] <- pieces (illustrative)
|
v
[ 8501, 64, 20832, 39874, 8887 ] <- token IDs (made up)
|
v
the model
|
v
next token ID ---> tokenizer (decode) ---> " and"
បំណែក និង ID ខាងលើគ្រាន់តែជាការបង្ហាញ មិនមែនមកពី tokenizer ពិតទេ។ model ផ្សេងគ្នាកាត់ប្រយោគ ដូចគ្នាខុសគ្នា ព្រោះ tokenizer នីមួយៗត្រូវបានសាងសង់សម្រាប់ model ផ្ទាល់ខ្លួនរបស់វា។
ហេតុអ្វីរឿងនេះសំខាន់សម្រាប់អ្នកក្នុងនាមជាអ្នកសរសេរកម្មវិធី?
- តម្លៃ។ model ដែលបង្ហោះនៅលើ server ជាធម្មតាគិតលុយតាម token មិនមែនតាមពាក្យ ឬតួអក្សរទេ។
- ដែនកំណត់។ អង្គចងចាំរបស់ model (context window ជំពូកទី 7) ត្រូវបានរាប់ជា token។
- ល្បឿន។ model ផលិតម្តងមួយ token ដូច្នេះ token កាន់តែច្រើនមានន័យថាការរង់ចាំកាន់តែយូរ។
- ឥរិយាបថចម្លែក។ កំហុសចម្លែកៗរបស់ model ជាច្រើន ដូចជាការរាប់អក្សរក្នុងពាក្យខុស ចាប់ផ្តើមពី tokenization។
2. វិធីបីយ៉ាងដើម្បីកាត់អត្ថបទ
មានវិធីជាក់ស្តែងបីយ៉ាងដើម្បីបំបែកអត្ថបទជាបំណែក។
តួអក្សរ។ មួយ token ក្នុងមួយតួអក្សរ៖ c, o, f, f, e, e។
- vocabulary តូច៖ តួអក្សរពីរបីរយគ្របដណ្តប់អត្ថបទស្ទើរតែទាំងអស់។
- មិនដែលជួបពាក្យមិនស្គាល់។
- ប៉ុន្តែលំដាប់ក្លាយជាវែងណាស់។ ពាក្យ 5 អក្សរគឺ 5 token ដូច្នេះ model ឃើញតិចក្នុងជំហាននីមួយៗ ហើយត្រូវធ្វើការច្រើនជាង។
ពាក្យ។ មួយ token ក្នុងមួយពាក្យ៖ coffee។
- លំដាប់ខ្លី ហើយ token នីមួយៗមានអត្ថន័យ។
- ប៉ុន្តែ vocabulary ត្រូវមានគ្រប់ពាក្យ ក្នុងគ្រប់ទម្រង់៖
walk,walks,walked,walkingបូកនឹងឈ្មោះ ពាក្យវាយខុស និងពាក្យថ្មី។ vocabulary នឹងធំសម្បើម ហើយនៅតែមិនពេញលេញ។ ពាក្យដែលមិនមាននៅក្នុងបញ្ជីគឺជា ពាក្យមិនស្គាល់ (unknown word) ហើយ model មិនអាចដោះស្រាយវាបានទេ។
Sub-word។ បំណែកនៅចន្លោះទាំងពីរ៖ ពាក្យទូទៅគឺមួយ token ពាក្យកម្រគឺជាបំណែកច្រើន ហើយអត្ថបទ មិនធម្មតាណាក៏នៅតែអាចសាងសង់ពីបំណែកតូចៗបាន។
coffeeអាចជាមួយ token។unbreakableអាចជាun,break,able។ ពាក្យដែលប្រឌិតឡើង ត្រឡប់ទៅបំណែកតូចជាង រហូតដល់តួអក្សរតែមួយប្រសិនបើចាំបាច់។
| វិធី | ទំហំ vocabulary | ប្រវែងលំដាប់ | ពាក្យមិនស្គាល់ |
|---|---|---|---|
| តួអក្សរ | តូចខ្លាំង | វែងខ្លាំង | គ្មាន |
| ពាក្យ | ធំសម្បើម | ខ្លី | បញ្ហាជាប់ជានិច្ច |
| Sub-word | មធ្យម (រាប់ម៉ឺន ក្នុង model ជាច្រើន) | មធ្យម | ស្ទើរតែគ្មាន |
LLM ពិតប្រើ sub-word token។ នោះជាហេតុដែលយើងបាននិយាយក្នុងជំពូកទី 1 ថា model ទាយ "token បន្ទាប់"។ model តូចៗរបស់យើងក្នុងជំពូកទី 3 ដល់ 5 ប្រើតែពាក្យទាំងមូលប៉ុណ្ណោះ ព្រោះវាអានងាយជាង ហើយយើងនឹងសម្គាល់វាជាការធ្វើឲ្យសាមញ្ញនៅពេលវាលេចឡើង។
3. តួអក្សរ និងពាក្យក្នុងកូដ
យើងមានឧបករណ៍បំបែកពាក្យពីជំពូកទី 1 ហើយ (words ក្នុង src/corpus.js)។
token តួអក្សរសាមញ្ញជាងទៅទៀត៖ string អាចរត់តាមបាន (iterable) ដូច្នេះការ spread វាដោយ
[...text] ផ្តល់មួយធាតុក្នុងមួយតួអក្សរ។
នេះជាអ្វីដែលទាំងពីរមើលទៅលើឃ្លាខ្លីមួយ៖
import { words } from "./src/corpus.js";
const sentence = "the students order iced coffee";
console.log([...sentence].length);
console.log(words(sentence));
console.log(words(sentence).length);
30
[ 'the', 'students', 'order', 'iced', 'coffee' ]
5
សាមសិប token តួអក្សរ ប្រៀបនឹងប្រាំ token ពាក្យ សម្រាប់ប្រយោគដូចគ្នា ខុសគ្នាប្រាំមួយដង។ sub-word token នឹងធ្លាក់នៅចន្លោះ។ លទ្ធផលមានស្ថិរភាព។
ចំណុចលម្អិតមួយ៖ [...text] បំបែកតាម Unicode code point ដែលជិតនឹង "តួអក្សរ" ជាង
text.split("")។ សម្រាប់ភាសាអង់គ្លេសធម្មតា ទាំងពីរដូចគ្នា។ យើងនឹងត្រឡប់មកវិញថាហេតុអ្វីរឿងនេះសំខាន់
សម្រាប់ភាសាខ្មែរក្នុងផ្នែកទី 10។
4. គំនិតនៃ Byte-Pair Encoding
តើ tokenizer សម្រេចយ៉ាងដូចម្តេចថា sub-word ណាត្រូវរក្សា? វិធីពេញនិយមមួយ ដែលប្រើក្នុង tokenizer ពិតជាច្រើន គឺ byte-pair encoding (BPE)។ គំនិតសាមញ្ញគ្រប់គ្រាន់ដើម្បីដំណើរការលើក្រដាស៖
- ចាប់ផ្តើមជាមួយអត្ថបទជាតួអក្សរតែមួយៗ។
- រាប់គូនៃ token ជិតគ្នានីមួយៗ។
- យក គូដែលទូទៅបំផុត ហើយបញ្ចូលវាជា token ថ្មីមួយ។
- ជំនួសគូនោះគ្រប់កន្លែងក្នុងអត្ថបទ។
- ធ្វើជំហានទី 2 ដល់ 4 ម្តងទៀតតាមចំនួនដងដែលកំណត់។
ការធ្វើម្តងទៀតនីមួយៗហៅថា merge ហើយបញ្ជីតាមលំដាប់នៃ merge គឺជារូបមន្ត។ ក្រុមអក្សរទូទៅដូចជា
th និង ing ត្រូវបាន merge ដំបូង ហើយក្លាយជា token។ ពាក្យទូទៅទាំងមូលត្រូវបានសាងសង់ឡើង
តាមរយៈ merge ច្រើន។ ពាក្យកម្រមិនដែលត្រូវបាន merge ច្រើនទេ ដូច្នេះវានៅជាបំណែកតូចៗ។
ដូច្នេះ vocabulary គឺ៖ តួអក្សរតែមួយៗទាំងអស់ បូកនឹង token ថ្មីមួយសម្រាប់រាល់ merge។ ប្រសិនបើអ្នកជ្រើស 50,000 merge អ្នកទទួលបាន vocabulary ប្រហែល 50,000 បូកតួអក្សរចាប់ផ្តើម។ ចំនួន merge គឺជាជម្រើស នៃការរចនា។ merge កាន់តែច្រើនផ្តល់លំដាប់ខ្លីជាង ប៉ុន្តែ vocabulary ធំជាង។
tokenizer ពិតបន្ថែមព័ត៌មានលម្អិត៖ ពួកវាច្រើនតែចាប់ផ្តើមពី byte ជំនួសឲ្យតួអក្សរ (យើងពន្យល់ហេតុអ្វី ក្នុងផ្នែកទី 10) ពួកវាបំបែកអត្ថបទជា chunk មុន ហើយពួកវាត្រូវបាន train លើអត្ថបទដ៏ធំសម្បើម។ toy របស់យើង រក្សាគំនិតស្នូល ហើយដកអ្វីៗផ្សេងចេញ។ មុខងារពិតមួយដែលយើងរក្សា៖ យើងបំបែកអត្ថបទជា chunk មុន ដែល chunk នីមួយៗគឺជាពាក្យមួយជាមួយចន្លោះនៅពីមុខ។ merge មិនដែលឆ្លងព្រំដែន chunk ទេ។ បើគ្មានច្បាប់នេះ merge អាចបិទភ្ជាប់ចុងនៃពាក្យមួយទៅនឹងដើមនៃពាក្យបន្ទាប់។
5. Train BPE ដោយដៃ
មុនកូដ សូមដំណើរការគំនិតលើអត្ថបទតូចខ្លាំងមួយ។ យកអត្ថបទនេះ ដែល chunk នីមួយៗគឺជាពាក្យមួយជាមួយ ចន្លោះនាំមុខរបស់វា៖
"low", " lower", " lowest"
ចាប់ផ្តើមជាមួយតួអក្សរតែមួយៗ៖ l o w, space l o w e r, space l o w e s t។
នោះគឺ 16 token សរុប។
- Merge 1។ គូ
l+oលេចឡើងបីដង ដែលទូទៅបំផុត (ជាមួយo+wដែលក៏លេចឡើងបីដងដែរ ដែលរកឃើញមុនគេឈ្នះ)។ merge វាជាlo។ - Merge 2។
lo+wលេចឡើងបីដង។ merge ជាlow។ - Merge 3។
space+lowលេចឡើងពីរដង។ merge ជាlow។ - Merge 4។
low+eលេចឡើងពីរដង។ merge ជាlowe។ - បន្ទាប់ពីនោះ គ្មានគូណាលេចឡើងលើសពីម្តងទេ ដូច្នេះយើងឈប់។ គូដែលឃើញតែម្តងមិនមានតម្លៃជា token ទេ ព្រោះវាមិនដែលសន្សំចន្លោះ។
កម្មវិធីរបស់យើងនឹងធ្វើដូចនេះយ៉ាងពិតប្រាកដ។ បន្ទាប់ពីអ្នកសាងសង់ module ក្នុងផ្នែកទី 7 កូដខាងក្រោមបោះពុម្ពបញ្ជី token បន្ទាប់ពីរាល់ merge៖
import { trainBpe, encode } from "./src/tokenizer.js";
const text = "low lower lowest";
const merges = trainBpe(text, 10);
for (let k = 0; k <= merges.length; k++) {
const t = encode(text, merges.slice(0, k));
console.log(String(k).padStart(2), t.length, JSON.stringify(t));
}
លទ្ធផលមានស្ថិរភាព៖
0 16 ["l","o","w"," ","l","o","w","e","r"," ","l","o","w","e","s","t"]
1 13 ["lo","w"," ","lo","w","e","r"," ","lo","w","e","s","t"]
2 10 ["low"," ","low","e","r"," ","low","e","s","t"]
3 8 ["low"," low","e","r"," low","e","s","t"]
4 6 ["low"," lowe","r"," lowe","s","t"]
ជួរឈរទីមួយគឺចំនួន merge ដែលបានអនុវត្ត ជួរឈរទីពីរគឺចំនួន token។ ដប់ប្រាំមួយ token ថយមកប្រាំមួយ។ សូមកត់សម្គាល់ថារូបមន្តផ្អែកតែលើការរាប់ ដូច្នេះអត្ថបទ train ផ្សេងគ្នា ផ្តល់ vocabulary ផ្សេងគ្នា។
6. Encoding និង Decoding
ការ train បង្កើតបញ្ជី merge។ ការប្រើ tokenizer ក្រោយមកមានពីរផ្នែក។
Encoding បំប្លែងអត្ថបទថ្មីទៅជា token។ ចាប់ផ្តើមពីតួអក្សរតែមួយៗ ហើយអនុវត្ត merge ឡើងវិញ តាមលំដាប់ដូចគ្នា។ លំដាប់សំខាន់៖ merge ក្រោយអាចប្រើ token ដែល merge មុនបានបង្កើត។ ការ encode អត្ថបទដែល tokenizer មិនដែលឃើញក៏នៅតែដំណើរការ។ បំណែកដែលមិនដែល merge នៅជាតួអក្សរតែមួយៗ។
Decoding បំប្លែង token ត្រឡប់ទៅជាអត្ថបទ។ នេះងាយស្រួល៖ ព្រោះ token នីមួយៗគ្រាន់តែជា string
អ្នកបិទភ្ជាប់វាជាមួយគ្នាដោយ join("")។ tokenizer ល្អគឺ lossless៖ decode(encode(text))
ផ្តល់អត្ថបទដែលអ្នកចាប់ផ្តើមមកវិញយ៉ាងពិតប្រាកដ។ យើងនឹងសាកល្បងរឿងនោះ។
7. រូបមន្ត៖ Module Tokenizer
បង្កើត src/tokenizer.js។ នេះគឺជា module ដែលជំពូកក្រោយៗ import៖
import { words } from "./corpus.js";
// 1. Character tokens: every character is one token.
export function charTokens(text) {
return [...text];
}
// 2. Word tokens: the words() helper from corpus.js.
export function wordTokens(text) {
return words(text);
}
// 3. Toy byte-pair encoding (BPE).
// First split the text into chunks: a word together with the space in
// front of it. Merges never cross a chunk boundary, so " the" can become
// one token but "e t" cannot. (Real tokenizers also pre-split like this.)
function chunks(text) {
return (text.match(/ ?[^ ]+| +/g) ?? []).map((chunk) => [...chunk]);
}
// Training: start from single characters, then repeat numMerges times:
// find the most common neighbouring pair and glue it into one token.
export function trainBpe(text, numMerges) {
let pieces = chunks(text);
const merges = [];
for (let m = 0; m < numMerges; m++) {
const counts = new Map();
for (const chunk of pieces) {
for (let i = 0; i < chunk.length - 1; i++) {
const key = chunk[i] + "\u0000" + chunk[i + 1];
counts.set(key, (counts.get(key) ?? 0) + 1);
}
}
let best = null;
let bestCount = 1; // a pair seen only once is not worth merging
for (const [key, count] of counts) {
if (count > bestCount) {
best = key;
bestCount = count;
}
}
if (best === null) break;
const pair = best.split("\u0000");
merges.push(pair);
pieces = pieces.map((chunk) => applyMerge(chunk, pair));
}
return merges;
}
function applyMerge(tokens, [a, b]) {
const out = [];
for (let i = 0; i < tokens.length; i++) {
if (tokens[i] === a && tokens[i + 1] === b) {
out.push(a + b);
i++; // skip the second half of the pair
} else {
out.push(tokens[i]);
}
}
return out;
}
// Encoding: split into chunks, replay the merges in the same order.
export function encode(text, merges) {
return chunks(text).flatMap((chunk) => {
let tokens = chunk;
for (const pair of merges) tokens = applyMerge(tokens, pair);
return tokens;
});
}
// Decoding: just glue the pieces back together.
export function decode(tokens) {
return tokens.join("");
}
// Cost = tokens / 1,000,000 * price per million tokens.
export function estimateCost(tokenCount, pricePerMillion) {
return (tokenCount / 1_000_000) * pricePerMillion;
}
ដើរពិនិត្យតាមផ្នែកនីមួយៗ។
chunks(text)ប្រើ regular expression/ ?[^ ]+| +/g៖ "ចន្លោះស្រេចចិត្តមួយ តាមពីក្រោយដោយ តួអក្សរមិនមែនចន្លោះមួយ ឬច្រើន ឬមិនដូច្នោះ ជាចន្លោះជាប់គ្នា"។"low lower"ក្លាយជា chunklowនិងlower។?? []ដោះស្រាយអត្ថបទទទេ ដែលmatchត្រឡប់null។trainBpeរក្សាpiecesជាបញ្ជី chunk ដែល chunk នីមួយៗជាបញ្ជី token។ ក្នុងជុំនីមួយៗ វារាប់គូ ជិតគ្នាឆ្លងកាត់ chunk ទាំងអស់ ជ្រើសគូដែលមានចំនួនខ្ពស់បំផុត ចងចាំវាក្នុងmergesហើយអនុវត្តវាគ្រប់កន្លែង។ នៅពេលគូពីរស្មើគ្នា គូដែលរាប់មុនឈ្នះ ដែលជាហេតុដែលលទ្ធផលអាចផលិតឡើងវិញបាន។- គូត្រូវបានរក្សាទុកជា string ដែលមានតួអក្សរ null នៅកណ្តាល (
"\u0000") ដូច្នេះវាអាចជា key របស់Map។ តួអក្សរ null មិនដែលជាផ្នែកនៃអត្ថបទធម្មតាទេ ដូច្នេះវាមានសុវត្ថិភាពជាសញ្ញាបំបែក។ applyMergeដើរកាត់បញ្ជី។ នៅពេលវារកឃើញគូ វាច្រានតម្លៃដែលបិទភ្ជាប់ ហើយរំលងមុខមួយជំហានបន្ថែម។ ការរំលងការពារកុំឲ្យពាក់កណ្តាលទីពីរនៃគូត្រូវបានប្រើម្តងទៀត។encodeនិងdecodeគឺជាទិសពីរពីផ្នែកទី 6។estimateCostគឺជានព្វន្តធម្មតា ដែលប្រើក្នុងផ្នែកទី 11។
signature សម្រាប់ជំពូកក្រោយៗ៖
charTokens(text) -> string[]
wordTokens(text) -> string[]
trainBpe(text, numMerges) -> [string, string][] (ordered merges)
encode(text, merges) -> string[]
decode(tokens) -> string
estimateCost(tokenCount, pricePerMillion) -> number (in currency units)
8. ដំណើរការវាលើ Corpus
បង្កើត tokens-demo.js នៅ root នៃ llmlab៖
import { corpus, words } from "./src/corpus.js";
import { charTokens, wordTokens, trainBpe, encode, decode } from "./src/tokenizer.js";
const training = words(corpus).join(" ");
const merges = trainBpe(training, 60);
console.log("characters:", charTokens(training).length);
console.log("words:", wordTokens(training).length);
console.log("bpe (60 merges):", encode(training, merges).length);
const sentence = "dara drinks iced coffee and the students order iced tea";
const tokens = encode(sentence, merges);
console.log(JSON.stringify(tokens));
console.log(tokens.length, "tokens for", sentence.length, "characters");
console.log("round trip ok:", decode(tokens) === sentence);
ដំណើរការ node tokens-demo.js៖
characters: 934
words: 182
bpe (60 merges): 396
["d","ar","a"," d","ri","n","k","s"," iced"," coffee"," and"," the"," students"," ","or","de","r"," iced"," t","e","a"]
21 tokens for 55 characters
round trip ok: true
អ្វីៗនៅទីនេះមានស្ថិរភាព ព្រោះគ្មានភាពចៃដន្យ។ សិក្សាវា៖
- អត្ថបទដូចគ្នាគឺ 934 token ជាតួអក្សរ 182 ជាពាក្យ និង 396 ជាមួយ 60 BPE merge។ BPE ធ្លាក់នៅចន្លោះ ដូចដែលបានសន្យា។
- ពាក្យទូទៅពី corpus ដូចជា
iced,coffee,theនិងstudentsក្លាយជា token តែមួយ នីមួយៗមានចន្លោះនាំមុខ។ - ពាក្យដែល corpus ប្រើកម្រ ដូចជា
daraនៅដើមអត្ថបទ ឬorderត្រូវបានបំបែកជាបំណែកតូចៗច្រើន៖d,ar,a។ - វា decode ត្រឡប់ទៅជាអត្ថបទដើមយ៉ាងពិតប្រាកដ។
ឥឡូវ merge ច្រើនជាងនេះ។ ជាមួយ 200 merge ដែលបានស្នើសុំ ការ train ឈប់មុនកំណត់ នៅ 118 merge ព្រោះគ្មានគូណាលេចឡើងម្តងទៀតទេ (corpus របស់យើងមានតែ 934 តួអក្សរ)។ បន្ទាប់មកប្រយោគដូចគ្នាត្រូវការ តែ 15 token៖
const bigger = trainBpe(training, 200);
console.log(bigger.length);
console.log(JSON.stringify(encode(sentence, bigger)));
118
["d","ar","a"," d","ri","n","ks"," iced"," coffee"," and"," the"," students"," order"," iced"," tea"]
នោះគឺ 15 token ជំនួសឲ្យ 21។ merge កាន់តែច្រើនមានន័យថា token តិចជាង ប៉ុន្តែ vocabulary ធំជាង។ tokenizer ពិតជ្រើសទំហំដោយតុល្យភាពរឿងនោះយ៉ាងពិតប្រាកដ។ លេខរបស់អ្នកត្រូវនឹងរបស់យើង ព្រោះ algorithm និង corpus ថេរ។
ចុងក្រោយ អត្ថបទដែល tokenizer មិនដែលឃើញអំឡុងពេល train៖
console.log(JSON.stringify(encode("sokha drinks bubble tea", merges)));
["s","okha"," d","ri","n","k","s"," b","u","b","b","le"," t","e","a"]
bubble មិនដែលនៅក្នុង corpus របស់យើងទេ ប៉ុន្តែវាមិនធ្វើឲ្យអ្វីខូចទេ៖ វាត្រូវបានសាងសង់ពីតួអក្សរ
តែមួយៗ និងបំណែកតូច le។ នេះគឺជាអត្ថប្រយោជន៍ធំនៃ sub-word៖ គ្មានពាក្យមិនស្គាល់ មានតែពាក្យ
ដែលមិនសូវមានប្រសិទ្ធភាពប៉ុណ្ណោះ។
9. ឥទ្ធិពលចម្លែករបស់ Tokenizer
tokenizer ពិតត្រូវបាន train ម្តងលើអត្ថបទធំមួយ ហើយបន្ទាប់មកបង្កក។ ផលវិបាកបង្ហាញឡើងគ្រប់ទីកន្លែង។ toy របស់យើងបង្កើតឡើងវិញនូវពួកវាខ្លះ។
console.log(JSON.stringify(encode("coffee", merges)));
console.log(JSON.stringify(encode(" coffee", merges)));
console.log(JSON.stringify(encode("Coffee", merges)));
console.log(JSON.stringify(encode("2026", merges)));
console.log(JSON.stringify(encode("strawberry", merges)));
["c","o","f","f","ee"]
[" coffee"]
["C","o","f","f","ee"]
["2","0","2","6"]
["s","t","r","a","w","b","er","r","y"]
អ្វីដែលទាំងនេះបង្រៀន៖
- ចន្លោះជាកម្មសិទ្ធិរបស់ token។
coffeeនិងcoffeeគឺជា token ខុសគ្នា (ប្រាំបំណែក ប្រៀបនឹងមួយ)។ ក្នុង tokenizer ពិតក៏ដូចគ្នា៖ ពាក្យដូចគ្នានៅដើមបន្ទាត់ និងនៅកណ្តាលប្រយោគអាចមាន token ID ខុសគ្នា។ - អក្សរធំគឺជា token ផ្សេង។
Coffeeមិនមែនcoffeeទេ ហើយ corpus របស់យើងទាំងអស់ជាអក្សរតូច ដូច្នេះអក្សរធំCគឺជាតួអក្សរវង្វេង។ tokenizer ពិតមានបំណែកដាច់ដោយឡែកសម្រាប់Coffeeនិងcoffee។ - លេខបំបែកនៅកន្លែងចម្លែក។ toy របស់យើងមិនដែលឃើញខ្ទង់ទេ ដូច្នេះ
2026ក្លាយជាបួនតួអក្សរ។ tokenizer ពិតដោះស្រាយលេខបានល្អជាង ប៉ុន្តែជាធម្មតានៅតែជា chunk ដែលមិនត្រូវនឹងតម្លៃខ្ទង់។ នោះជាមូលហេតុមួយដែល model ធ្វើកំហុសនព្វន្តជាមួយលេខវែង។ - model មិនឃើញអក្សរទេ។ ប្រសិនបើ model ឃើញ
strawberryជាពីរ ឬបី token នោះ "តើមាន r ប៉ុន្មានក្នុង strawberry?" សួរអំពីអក្សរនៅក្នុង token ដែលវាស្គាល់តែជា ID តែមួយ។ វារៀនចម្លើយពីអត្ថបទ មិនមែនដោយមើលអក្ខរាវិរុទ្ធទេ។ នោះពន្យល់ថាហេតុអ្វី model ច្រើនតែមិនអាចទុកចិត្តបាន ក្នុងការរាប់អក្សរ ទោះបីកិច្ចការនេះមើលទៅសាមញ្ញក៏ដោយ។ នៅពេលអ្នកត្រូវការការងារត្រឹមត្រូវដូចជាការរាប់ សូមប្រើកូដ។
toy របស់យើងក៏ lossless លើអត្ថបទចម្លែក ដែរ៖
const odd = " two spaces";
console.log(decode(encode(odd, merges)) === odd);
true
ព្រោះចន្លោះជាផ្នែកនៃ token គ្មានអ្វីត្រូវបានបោះចោលទេ។
10. ភាសាខ្មែរ និងភាសាផ្សេងទៀត
tokenizer ត្រូវបាន train លើអត្ថបទមួយចំនួន។ ប្រសិនបើអត្ថបទភាគច្រើនជាភាសាអង់គ្លេស ភាសាអង់គ្លេសទទួលបាន merge វែង និងមានប្រសិទ្ធភាព ហើយភាសាផ្សេងទទួលបានតិចជាង។ នៅពេលភាសាមួយមាន merge តិចក្នុង tokenizer អត្ថបទរបស់វាត្រូវបានកាត់ជាបំណែកតូចៗច្រើន ដូច្នេះសារដូចគ្នាប្រើ token ច្រើនជាង គិតថ្លៃច្រើនជាង និងប្រើ context window អស់ច្រើនជាង។
យើងគួរប្រុងប្រយ័ត្នអំពីលេខនៅទីនេះ។ ចំនួន token បន្ថែមដែលប្រយោគខ្មែរត្រូវការ អាស្រ័យលើ tokenizer ជាក់លាក់ ហើយ tokenizer ផ្លាស់ប្តូរ។ កុំជឿលេខពីអត្ថបទ រួមទាំងអត្ថបទនេះផង។ សាកល្បងដោយខ្លួនឯង។ ស្វែងរកតាមអ៊ីនធឺណិតរកទំព័រ "tokenizer" ឬ "token counter" ដែលបោះពុម្ពផ្សាយដោយក្រុមហ៊ុនដែលមាន model ដែលអ្នកគ្រោងនឹងប្រើ បិទភ្ជាប់សារដូចគ្នាជាភាសាអង់គ្លេស និងភាសាខ្មែរ ហើយប្រៀបធៀបចំនួន។ ធ្វើដូចគ្នានៅពេលអ្នកប្តូរ model។ ក្នុង tokenizer ជាច្រើន អត្ថបទខ្មែរពិតជាចេញជា token ច្រើនជាង អត្ថបទអង់គ្លេសដែលមានអត្ថន័យដូចគ្នាយ៉ាងច្បាស់ ហើយអ្នកអាចគ្រោងផែនការជុំវិញវាបាន នៅពេលអ្នកបានវាស់វា។
រឿងមួយដែលយើង អាច វាស់ដោយកូដផ្ទាល់ខ្លួនគឺទំហំដើមនៃអត្ថបទជា byte ដែលជាកន្លែងដែល tokenizer ពិត
ច្រើនតែចាប់ផ្តើម។ បង្កើត bytes.js៖
const samples = { english: "coffee", khmer: "កាហ្វេ", greeting: "សួស្តី" };
for (const [name, text] of Object.entries(samples)) {
console.log(name.padEnd(9), [...text].length, "characters,", Buffer.byteLength(text, "utf8"), "UTF-8 bytes");
}
console.log(Buffer.from("coffee"));
console.log(Buffer.from("កា"));
console.log([..."កាហ្វេ"].map((c) => c.codePointAt(0).toString(16)));
english 6 characters, 6 UTF-8 bytes
khmer 6 characters, 18 UTF-8 bytes
greeting 6 characters, 18 UTF-8 bytes
<Buffer 63 6f 66 66 65 65>
<Buffer e1 9e 80 e1 9e b6>
[ '1780', '17b6', '17a0', '17d2', '179c', '17c1' ]
លទ្ធផលទាំងអស់នេះមានស្ថិរភាព។ នេះជារបៀបអានវា។
- ពាក្យអង់គ្លេស
coffeeគឺ 6 code point និង 6 byte៖ មួយ byte ក្នុងមួយអក្សរ។ - ពាក្យខ្មែរសម្រាប់កាហ្វេ
កាហ្វេក៏ជា 6 code point ដែរ ប៉ុន្តែ code point ខ្មែរនីមួយៗប្រើ 3 byte ក្នុង UTF-8 ដូច្នេះវាគឺ 18 byte។ code point ពីរដំបូងក(hex 1780) និងា(hex 17b6) ក្លាយជាប្រាំមួយ bytee1 9e 80 e1 9e b6។ - សូមចំណាំថា "6 characters" គឺជារបៀបដែល JavaScript រាប់ code point។ អ្នកអានខ្មែរអាចរាប់
ព្យាង្គ ឬក្រុមតួអក្សរខុសគ្នា ព្រោះភាសាខ្មែរប្រើស្រៈ និងសញ្ញាជើង
្(hex 17d2) ដែលជា code point ដាច់ដោយឡែក ប៉ុន្តែមិនមែនជាអក្សរដាច់ដោយឡែកសម្រាប់អ្នកអានទេ។
ហេតុអ្វីរឿងនេះសំខាន់? tokenizer ពិតជាច្រើនធ្វើការលើ byte ជំនួសឲ្យតួអក្សរ។ នោះធានាថាអត្ថបទណាក៏ដោយ ក្នុងភាសាណាក៏ដោយ ឬសូម្បីតែ emoji អាចត្រូវបាន encode៖ ករណីអាក្រក់បំផុតគឺមួយ token ក្នុងមួយ byte ដូច្នេះមិនដែលមានតួអក្សរមិនស្គាល់ទេ។ ប៉ុន្តែវាក៏មានន័យថា អត្ថបទដែលមានតិចក្នុងការ train របស់ tokenizer ចាប់ផ្តើមពីលំដាប់ byte វែងជាង ហើយមាន merge ដែលមានប្រយោជន៍តិចជាង។ លទ្ធផលមិនមែនជាច្បាប់ធម្មជាតិទេ។ វាជាផលវិបាកនៃអត្ថបទដែលអ្នករចនា tokenizer បានប្រើ ហើយ tokenizer ថ្មីៗច្រើនតែល្អជាង។ វាជាហេតុផលដើម្បីវាស់ ជាជាងសន្មត់។
tokenizer toy របស់យើងធ្វើការលើតួអក្សរ មិនមែន byte ទេ ដូច្នេះតួអក្សរខ្មែរដែលវាមិនដែលឃើញ គ្រាន់តែនៅ
ជាខ្លួនវា។ សាកល្បងដោយខ្លួនឯង៖ ដំណើរការ encode("កាហ្វេ", merges) ហើយអ្នកនឹងទទួលបានប្រាំមួយ token
តួអក្សរតែមួយៗ ព្រោះ corpus របស់យើងមិនមានភាសាខ្មែរដើម្បីរៀន merge ទេ។
ដំបូន្មានជាក់ស្តែងប្រសិនបើអ្នកសាងសង់កម្មវិធីសម្រាប់អ្នកប្រើខ្មែរ៖
- វាស់ចំនួន token ពិតជាមួយ tokenizer សម្រាប់ model ដែលអ្នកបានជ្រើស។
- ប៉ាន់ស្មាន token ច្រើនជាងនេះនៅពេលអ្នកប៉ាន់តម្លៃ និងទំហំ context។
- សាកល្បងគុណភាពនៃចម្លើយជាភាសាខ្មែរ មិនមែនគ្រាន់តែតម្លៃទេ។ merge តិចជាបញ្ហាមួយ ចំនួនអត្ថបទខ្មែរដែល model បានឃើញអំឡុងពេល train គឺជាបញ្ហាដាច់ដោយឡែកមួយទៀត។
11. Token គឺជាលុយ
model ដែលបង្ហោះនៅលើ server ភាគច្រើនគិតថ្លៃសម្រាប់ token ដែលអ្នកផ្ញើ និង token ដែល model សរសេរ ត្រឡប់មក ជារឿយៗក្នុងតម្លៃខុសគ្នា។ រូបមន្តសាមញ្ញ៖
cost = tokens / 1,000,000 * price per million tokens
នោះគឺជាអ្វីដែល estimateCost គណនា។ តម្លៃខាងក្រោមគឺជា ឧទាហរណ៍ដែលប្រឌិតឡើង
ដើម្បីបង្ហាញនព្វន្តប៉ុណ្ណោះ។ សូមអានទំព័រតម្លៃបច្ចុប្បន្នរបស់សេវាកម្មដែលអ្នកប្រើជានិច្ច។
import { estimateCost } from "./src/tokenizer.js";
const inputPrice = 1.0; // example price: $1.00 per million input tokens
const outputPrice = 4.0; // example price: $4.00 per million output tokens
// One chat: 2,000 tokens sent in, 500 tokens written back.
const perChat = estimateCost(2000, inputPrice) + estimateCost(500, outputPrice);
console.log(perChat.toFixed(4));
console.log((perChat * 10000).toFixed(2));
0.0040
40.00
ក្នុងតម្លៃឧទាហរណ៍ទាំងនេះ ការសន្ទនាមួយអស់ 0.004 ដុល្លារ តិចជាងកន្លះសេន។ ការសន្ទនាមួយម៉ឺនអស់ 40 ដុល្លារ។ លេខតូចក្លាយជាលុយពិតនៅខ្នាតធំ។ នោះក៏ជាហេតុដែលអ្នកសរសេរកម្មវិធីខ្វល់អំពី៖
- ផ្ញើតិចជាង។ កុំបិទភ្ជាប់ឯកសារទាំងមូល នៅពេលប្រាំកថាខណ្ឌគ្រប់គ្រាន់។
- សុំចម្លើយខ្លីជាង។ token លទ្ធផលច្រើនតែថ្លៃជាង token បញ្ចូល ដូច្នេះចម្លើយ 100 token ថោកជាង 1,000។
- ចងចាំថាប្រវត្តិត្រូវបានផ្ញើម្តងទៀត។ ក្នុង chat រាល់សារថ្មីក៏បង់ថ្លៃសម្រាប់សារមុនៗដែរ (ជំពូកទី 7)។
- ប្រើ model តូចជាងសម្រាប់ការងារងាយ។ តម្លៃខុសគ្នាច្រើនរវាង model។
ច្បាប់ទូទៅសម្រាប់ភាសាអង់គ្លេសគឺថា មួយ token តិចជាងមួយពាក្យបន្តិចជាមធ្យម ប៉ុន្តែវាប្រែប្រួលខ្លាំងតាម tokenizer ភាសា និងប្រភេទអត្ថបទ (កូដ លេខ និងពាក្យកម្រប្រើច្រើនជាង)។ ប្រើវាសម្រាប់ការប៉ាន់ស្មាន ដំបូងប៉ុណ្ណោះ ហើយប្រើ tokenizer ពិតនៅពេលលេខសំខាន់។
12. សាកល្បងដោយខ្លួនឯង
- Merge ដោយដៃ។ លើក្រដាស ដំណើរការ BPE លើ
"aa aab aab"សម្រាប់បី merge។ បន្ទាប់មកពិនិត្យចម្លើយរបស់អ្នកជាមួយtrainBpe("aa aab aab", 3)។ - មើលទំហំរួញ។ ដំណើរការ
trainBpeលើ corpus សម្រាប់ 0, 10, 30, 60, 100 និង 200 merge ហើយបោះពុម្ពencode(training, merges).lengthរាល់ដង។ តើចំនួនបន្តរួញទេ? តើវាឈប់នៅកន្លែងណា? - Train លើអត្ថបទផ្សេង។ train លើអត្ថបទផ្ទាល់ខ្លួនរបស់អ្នក (កថាខណ្ឌពីកំណត់ត្រារបស់អ្នក) បន្ទាប់មក encode ប្រយោគពី corpus កាហ្វេ។ ប្រៀបធៀបចំនួន token ជាមួយចំនួនពី tokenizer ដែលបាន train លើ corpus។ តើមួយណាមានប្រសិទ្ធភាពជាងលើអត្ថបទណា?
- បង្ហាញថាវា lossless។ សរសេរ loop ដែលពិនិត្យ
decode(encode(line, merges)) === lineសម្រាប់គ្រប់បន្ទាត់ក្នុងcorpusដែលបំបែកដោយcorpus.trim().split("\n")។ បោះពុម្ពចំនួនដែលជាប់។ - រកអ្វីដែលមិនស្គាល់។ encode ប្រយោគដែលមាន emoji ឬភាសាខ្មែរដោយ
encode។ តើបំណែកមើលទៅយ៉ាងណា? បន្ទាប់មកពន្យល់ក្នុងមួយប្រយោគថាហេតុអ្វី tokenizer ដែលផ្អែកលើ byte មិនដែលជួបតួអក្សរមិនស្គាល់។ - វាស់ពិតប្រាកដ។ ជ្រើសទំព័រ tokenizer ឬ token-counter សាធារណៈរបស់ model ណាមួយដែលបង្ហោះ នៅលើ server។ បិទភ្ជាប់សារខ្លីដូចគ្នាជាភាសាអង់គ្លេស និងភាសាខ្មែរ។ សរសេរចំនួនទាំងពីរ និងសមាមាត្រ។ ធ្វើម្តងទៀតជាមួយ tokenizer របស់ model ទីពីរប្រសិនបើអាច ហើយមើលថាតើសមាមាត្រដូចគ្នាទេ។
13. សំណួរដែលអ្នកនឹងត្រូវគេសួរ
token គឺជាអ្វី ក្នុងមួយប្រយោគ? វាគឺជាបំណែកអត្ថបទ ពាក្យខ្លី ផ្នែកនៃពាក្យវែង តួអក្សរតែមួយ ឬបន្តិចវណ្ណយុត្តិ ដែល model ចាត់ទុកជា ឯកតាមួយ ហើយផ្តល់លេខឲ្យវា។ model អាន និងសរសេរ token មិនដែលអក្សរដើមទេ។
ពាក្យមួយជាប៉ុន្មាន token? គ្មានចម្លើយថេរទេ។ ពាក្យខ្លីទូទៅច្រើនតែជាមួយ token ពាក្យកម្រ ឬវែងគឺជាច្រើន ហើយវាអាស្រ័យលើ tokenizer។ សម្រាប់ភាសាអង់គ្លេស token ច្រើនតែខ្លីជាងពាក្យបន្តិច ហើយសម្រាប់ភាសាផ្សេងវាអាចខ្លីជាងច្រើន។ ប្រើ tokenizer ពិតដើម្បីរាប់ មិនមែនច្បាប់ទូទៅទេ នៅពេលវាសំខាន់។
ហេតុអ្វីមិនប្រើពាក្យទាំងមូលតែម្តង? បញ្ជីពាក្យនឹងធំសម្បើម ហើយនៅតែខកខានពាក្យថ្មី ឈ្មោះ ពាក្យវាយខុស និងទម្រង់ពាក្យ។ Sub-word ផ្តល់ vocabulary ទំហំមធ្យមដែលអ្វីៗអាចសរសេរបាន ព្រោះពាក្យកម្រត្រូវបានសាងសង់ពីបំណែកតូចជាង។
ហេតុអ្វី model រាប់ចំនួនអក្សរខុស ឬបញ្ច្រាសពាក្យមិនល្អ? វាមិនឃើញអក្សរទេ។ វាឃើញ token ហើយ token ជាធម្មតាជាអក្សរច្រើនបញ្ចូលគ្នាជា ID តែមួយ។ កិច្ចការអក្ខរាវិរុទ្ធសួរអំពីរចនាសម្ព័ន្ធដែល model រៀនតែដោយប្រយោល។ សម្រាប់ការរាប់ ឬការងារ string ដែលត្រឹមត្រូវ សូមហៅកូដជំនួសឲ្យការសួរ model។
ហេតុអ្វីសារខ្មែររបស់ខ្ញុំអស់លុយច្រើនជាងសារអង់គ្លេសដូចគ្នា? ច្រើនតែដោយសារ tokenizer មានបំណែកដែលរួចជាស្រេចតិចជាងសម្រាប់ភាសាខ្មែរ ដូច្នេះអត្ថបទត្រូវបានបំបែក ជា token ច្រើនជាង ហើយអ្នកបង់តាម token។ វាអាស្រ័យលើ model ដូច្នេះសូមវាស់ជាមួយ tokenizer របស់ model នោះ។ វាមិនមែនជាការវិនិច្ឆ័យលើភាសានោះទេ។ វាជាលទ្ធផលនៃអត្ថបទដែល tokenizer ត្រូវបានសាងសង់ពី។
តើ token ដូចជា byte ដែរឬទេ? ទេ។ byte គឺជាឯកតានៃការផ្ទុក។ token គឺជាឯកតានៃអត្ថបទសម្រាប់ model ហើយអាចមានតួអក្សរច្រើន។ tokenizer ខ្លះប្រើ byte ជាបំណែកចាប់ផ្តើម ដែលធានាថាអត្ថបទទាំងអស់អាចត្រូវបាន encode។
តើ model ផ្សេងគ្នាប្រើ tokenizer ផ្សេងគ្នាទេ? បាទ/ចាស ជាធម្មតា។ អត្ថបទដូចគ្នាអាចជា 100 token ជាមួយ model មួយ និងចំនួនផ្សេងជាមួយមួយទៀត។ model ត្រូវតែប្រើជាមួយ tokenizer របស់ខ្លួនជានិច្ច ព្រោះ ID យល់បានតែចំពោះ model នោះប៉ុណ្ណោះ។
ទៅបន្តទៀត។ ឥឡូវអ្នកអាចបំប្លែងអត្ថបទទៅជាបំណែក ហើយរាប់វាបាន។ ជំហានបន្ទាប់គឺជាជំហានដែលធ្វើឲ្យ model បង្កើត៖ រៀនថាបំណែកណាច្រើនតែមកតាមបំណែកណា។ ក្នុង សាងសង់ Language Model តូចមួយ យើងរាប់គូពាក្យក្នុង corpus បំប្លែងចំនួនទៅជាប្រូបាប ហើយសរសេរអត្ថបទថ្មីដោយ model ដែលយើងសាងសង់ខ្លួនឯង។
ទិញកាហ្វេជូនខ្ញុំ
ចូលចិត្តមេរៀននេះមែនទេ? សូមពិចារណាទិញកាហ្វេជូនខ្ញុំមួយកែវ ដើម្បីគាំទ្រការបង្កើតមាតិកាឥតគិតថ្លៃបន្ថែមទៀត!