ប្រូបាប Temperature និង Sampling

មាតិកា

  1. ជំហានដែលបាត់រវាងពិន្ទុ និងពាក្យ
  2. Logit៖ ពិន្ទុដើម
  3. Softmax៖ ពីពិន្ទុទៅប្រូបាប
  4. Temperature៖ ធ្វើឲ្យមុត ឬរាបស្មើ
  5. Sampling៖ រមៀលគ្រាប់ឡុកឡាក់ដែលមានទម្ងន់
  6. Greedy៖ យកខាងលើបំផុតជានិច្ច
  7. Top-k និង Top-p៖ កាត់កន្ទុយ
  8. Prompt ដូចគ្នាត្រង់ Temperature 0, 0.7 និង 1.5
  9. តើ Temperature 0 ផ្តល់ចម្លើយដូចគ្នាជានិច្ចទេ?
  10. ការជ្រើសរើសការកំណត់ក្នុងគម្រោងពិត
  11. រូបមន្ត៖ Module sampling
  12. សាកល្បងដោយខ្លួនឯង
  13. សំណួរដែលអ្នកនឹងត្រូវគេសួរ

1. ជំហានដែលបាត់រវាងពិន្ទុ និងពាក្យ

នៅក្នុង បង្កើត Language Model តូចមួយ model របស់យើង រាប់គូពាក្យ ចែកនឹងចំនួនសរុប ហើយរមៀលគ្រាប់ឡុកឡាក់ដែលមានទម្ងន់។ វាដំណើរការបាន ព្រោះការរាប់ផ្តល់ប្រូបាបដល់យើងដោយផ្ទាល់។ Neural network ពិតមិនរាប់អ្វីទេពេលបង្កើតអត្ថបទ។ វាបញ្ចប់រាល់ជំហានដោយ ពិន្ទុ ដើមមួយសម្រាប់រាល់ token ដែលវាស្គាល់ ហើយពិន្ទុទាំងនោះមិនទាន់ជាប្រូបាបទេ។

ដូច្នេះរាល់ LLM ពិតមាន pipeline ខ្លីមួយនៅចុងបំផុតនៃរាល់ជំហាន៖

context  ->  model  ->  scores  ->  softmax  ->  probabilities
                                                      |
                              (temperature, top-k, top-p adjust)
                                                      |
                                              pick one token

ជំពូកនេះបង្កើតរាល់ដំណាក់កាលនៃ pipeline នេះ៖ softmax (ពិន្ទុទៅប្រូបាប) temperature (model ហ៊ានប្រថុយប៉ុណ្ណា) sampling (ការជ្រើស) និងតម្រងពីរហៅថា top-k និង top-p។ ទាំងនេះគឺជាប៊ូតុងបង្វិលដូចគ្នាដែលអ្នកនឹងឃើញក្នុងការកំណត់នៃឧបករណ៍ AI និងក្នុងជម្រើសនៃ LLM API នៅពេលក្រោយ។ ពេលអ្នកណាម្នាក់និយាយថា "បន្ថយ temperature" នេះគឺជាអ្វីដែលពួកគេមានន័យ។

អ្នកត្រូវការ src/bigram.js និង src/rng.js ពីជំពូកមុន។ កូដទាំងអស់ជា JavaScript ESM ធម្មតា រត់ demo នីមួយៗដោយ node។

2. Logit៖ ពិន្ទុដើម

ពិន្ទុដើមដែល model ផលិតហៅថា logit (ថា "LOH-jits")។ Logit អាចជាលេខណាក៏បាន៖ វិជ្ជមាន អវិជ្ជមាន ធំ តូច។ វាមានន័យតែថា "ធំជាង គឺទំនងជាង"។ សន្មតថា model តូចមួយកំពុងជ្រើសរវាងពាក្យបន្ទាប់បី ហើយផ្តល់ពិន្ទុទាំងនេះ៖

ពាក្យLogit
"coffee"2.0
"tea"1.0
"water"0.1

"coffee" មានពិន្ទុខ្ពស់បំផុត ដូច្នេះ model ចូលចិត្តវា។ ប៉ុន្តែវាចូលចិត្តវាប៉ុណ្ណា? ពិន្ទុ 2.0 ធៀបនឹង 1.0 តើនោះ "ទំនងជាពីរដង" ទេ? ពិន្ទុតែម្នាក់ឯងមិនអាចប្រាប់យើងបានទេ ហើយវាមិនអាចប្រើដើម្បីរមៀលគ្រាប់ឡុកឡាក់បានទេ ព្រោះប្រូបាបត្រូវតែវិជ្ជមាន ហើយបូកបាន 1។ យើងត្រូវការ function ដែលបំប្លែងបញ្ជីលេខណាក៏បានទៅជាប្រូបាបត្រឹមត្រូវ។

3. Softmax៖ ពីពិន្ទុទៅប្រូបាប

Function នោះគឺ softmax។ វាធ្វើពីរយ៉ាងសាមញ្ញ៖

  1. ធ្វើឲ្យគ្រប់ពិន្ទុវិជ្ជមាន ដោយលើក e (ប្រហែល 2.718) ទៅជាស្វ័យគុណនៃពិន្ទុ។ ក្នុង JavaScript នោះគឺ Math.exp(score)។ ពិន្ទុធំក្លាយជាលេខវិជ្ជមានធំ ហើយពិន្ទុអវិជ្ជមានក្លាយជាលេខវិជ្ជមានតូច។
  2. ចែកនីមួយៗនឹងផលបូក ដូច្នេះលទ្ធផលបូកបាន 1។
export function softmax(logits) {
  const max = Math.max(...logits); // subtract max so exp() cannot overflow
  const exps = logits.map((x) => Math.exp(x - max));
  const sum = exps.reduce((a, b) => a + b, 0);
  return exps.map((e) => e / sum);
}

បន្ទាត់ - max គឺជាល្បិចសុវត្ថិភាព។ Math.exp(1000) ធំពេកសម្រាប់លេខ JavaScript ហើយក្លាយជា Infinity ដែលនឹងធ្វើឲ្យអ្វីៗខូច។ ការដកពិន្ទុធំបំផុតមុន ធ្វើឲ្យគ្រប់ស្វ័យគុណ ជា 0 ឬអវិជ្ជមាន ហើយវាមិនប្តូរចម្លើយចុងក្រោយទេ ព្រោះកត្តាដដែលលុបគ្នាពេលអ្នកចែក។ រក្សាទុកនេះជា demo-softmax.js (បន្ទាប់ពីបង្កើត src/sampling.js ពីរូបមន្តនៅចុងបញ្ចប់ ឬចម្លង softmax ចូលទៅក្នុងវា)៖

import { softmax } from "./src/sampling.js";

const show = (list) => list.map((x) => x.toFixed(3)).join("  ");

const logits = [2.0, 1.0, 0.1];
const probs = softmax(logits);
console.log(show(probs));
console.log(probs.reduce((a, b) => a + b, 0));
console.log(show(softmax([102.0, 101.0, 100.1])));
console.log(show(softmax([1, 1, 1, 1])));
console.log(show(softmax([1000, 999])));
console.log(show(softmax([-3, 0, 3])));
// 0.659  0.242  0.099
// 1
// 0.659  0.242  0.099
// 0.250  0.250  0.250  0.250
// 0.731  0.269
// 0.002  0.047  0.950

អានលទ្ធផលម្តងមួយបន្ទាត់៖

  • បន្ទាត់ទី 1៖ ពិន្ទុទាំងបីរបស់យើងក្លាយជា 65.9%, 24.2% និង 9.9%។ "coffee" ត្រូវបានគេចូលចិត្ត ប៉ុន្តែ "tea" និង "water" នៅតែមានឱកាសពិតប្រាកដ។
  • បន្ទាត់ទី 2៖ ផលបូកគឺ 1 ដូចប្រូបាបត្រូវតែជា។ (Floating point ពេលខ្លះអាចបោះពុម្ព 0.9999999999999999 នៅទីនេះ ទាំងពីរមានន័យថា 1។)
  • បន្ទាត់ទី 3៖ យើងបានបន្ថែម 100 ទៅគ្រប់ពិន្ទុ ហើយចម្លើយមិនប្តូរទេ។ Softmax ខ្វល់តែអំពី ភាពខុសគ្នា រវាងពិន្ទុប៉ុណ្ណោះ។
  • បន្ទាត់ទី 4៖ ពិន្ទុស្មើគ្នាផ្តល់ប្រូបាបស្មើគ្នា។
  • បន្ទាត់ទី 5៖ ពិន្ទុ 1000 និង 999 ដំណើរការបានល្អ គ្មាន Infinity ទេ ដោយសារល្បិច - max។
  • បន្ទាត់ទី 6៖ គម្លាត 3 រវាងពិន្ទុធ្វើឲ្យមានភាពខុសគ្នាដ៏ធំ៖ 95% សម្រាប់ពិន្ទុខាងលើបំផុត។

អ្វីៗទាំងអស់ដែលបោះពុម្ពនៅទីនេះថេរ។ មិនទាន់មានភាពចៃដន្យនៅឡើយទេ។ គំនិត softmax ក៏ជាបេះដូងនៃជំហាន attention ក្នុង Embedding និង Attention។

4. Temperature៖ ធ្វើឲ្យមុត ឬរាបស្មើ

Temperature គឺជាលេខដែលអ្នកចែក logit នឹង មុន softmax។ វាជាការកំណត់ដ៏ល្បីបំផុតក្នុង AI ទាំងមូល ហើយវាគឺជាកូដតែមួយបន្ទាត់៖

export function applyTemperature(logits, t) {
  if (!(t > 0)) throw new Error("temperature must be > 0 (use greedy for 0)");
  return logits.map((x) => x / t);
}

គិតអំពីអ្វីដែលការចែកធ្វើ។ បើ t តិចជាង 1 ឧទាហរណ៍ 0.5 គ្រប់ពិន្ទុកើនទ្វេដង ដូច្នេះគម្លាតរវាងពិន្ទុកើនឡើង ហើយ softmax ផ្តល់ទៅអ្នកនាំមុខនូវចំណែកធំជាងនេះទៀត។ បើ t ច្រើនជាង 1 ពិន្ទុរួញចូលរកគ្នា គម្លាតបិទ ហើយប្រូបាបរាបស្មើទៅរកស្មើគ្នា។ សាកល្បងក្នុង demo-temperature.js៖

import { softmax, applyTemperature } from "./src/sampling.js";

const show = (list) => list.map((x) => x.toFixed(3)).join("  ");
const logits = [2.0, 1.0, 0.1];

for (const t of [0.2, 0.5, 1, 2, 5]) {
  console.log(`t=${String(t).padEnd(3)}`, show(softmax(applyTemperature(logits, t))));
}
try {
  applyTemperature(logits, 0);
} catch (error) {
  console.log(error.message);
}
// t=0.2 0.993  0.007  0.000
// t=0.5 0.864  0.117  0.019
// t=1   0.659  0.242  0.099
// t=2   0.502  0.304  0.194
// t=5   0.400  0.327  0.273
// temperature must be > 0 (use greedy for 0)

មើលលេខទីមួយក្នុងគ្រប់ជួរ គឺប្រូបាបនៃជម្រើសខាងលើបំផុត៖

  • នៅ t=0.2 វាគឺ 99.3%។ Model ស្ទើរតែប្រាកដ។ Temperature ទាបមានន័យថា ផ្តោត អាចទាយទុកបាន។
  • នៅ t=1 វាគឺ 65.9%។ នេះគឺជាយោបល់ផ្ទាល់ខ្លួនរបស់ model ដែលមិនបានប្តូរ។
  • នៅ t=5 វាគឺ 40% ហើយជម្រើសទាំងបីស្ទើរតែស្មើគ្នា។ Temperature ខ្ពស់មានន័យថា រាបស្មើ ហ៊ានប្រថុយ ពេលខ្លះល្ងង់ខ្លៅ។

ចំណាត់ថ្នាក់មិនដែលប្តូរទេ៖ "coffee" តែងតែលេខមួយ។ Temperature មិនធ្វើឲ្យ model ឆ្លាតជាង ឬល្ងង់ជាងទេ។ វាប្តូរ ថាតើយើងធ្វើតាមចំណាត់ថ្នាក់របស់វាយ៉ាងតឹងរ៉ឹងប៉ុណ្ណា។ នោះគឺជាចម្លើយមួយប្រយោគចំពោះសំណួរ "temperature គឺជាអ្វី?"។

អ្នកមិនអាចចែកនឹង 0 បានទេ ដូច្នេះ temperature ស្មើ 0 ពិតប្រាកដមិនអាចឆ្លងកាត់រូបមន្តនេះបានទេ។ Library ចាត់ទុក "temperature 0" ជាករណីពិសេសដែលមានន័យថា "ជ្រើសជម្រើសខាងលើបំផុតជានិច្ច" ដែលជា greedy decoding (ខាងក្រោម)។ អ្នកអាចគិតវាជាលីមីតពេល t កាន់តែជិត 0។

5. Sampling៖ រមៀលគ្រាប់ឡុកឡាក់ដែលមានទម្ងន់

ពេលអ្នកមានប្រូបាបហើយ អ្នក sample៖ ជ្រើសជម្រើសមួយដោយចៃដន្យ ដោយឱកាសរបស់ជម្រើសនីមួយៗស្មើនឹងប្រូបាបរបស់វា។ វាជាគំនិតបន្ទាត់ និងព្រួញដូចជំពូកមុន ឥឡូវនេះជា function ដែលប្រើឡើងវិញបាន៖

export function sampleIndex(probs, rng = Math.random) {
  const r = rng();
  let running = 0;
  for (let i = 0; i < probs.length; i++) {
    running += probs[i];
    if (r < running) return i;
  }
  return probs.length - 1; // guards against rounding error
}

វាត្រឡប់ index នៃជម្រើសដែលបានជ្រើស (0, 1, 2, ...) ជំនួសឲ្យពាក្យ ដូច្នេះវាដំណើរការសម្រាប់បញ្ជីណាក៏បាន៖ ពាក្យ token អ្វីក៏បាន។

ឥឡូវនេះភ្ជាប់វាទៅ bigram model របស់យើង។ Bigram model ផ្តល់ប្រូបាប ប៉ុន្តែ softmax និង temperature រំពឹងរក logit។ ជាសំណាងល្អ អ្នកអាចបំប្លែងប្រូបាប ទៅជា logit ដោយ logarithm៖ Math.log(prob)។ នេះគឺជាបញ្ច្រាសពិតប្រាកដនៃ softmax ដូច្នេះ softmax(logits) ផ្តល់ប្រូបាបដើមត្រឡប់មកវិញនៅ temperature 1។ បង្កើត demo-sampling.js៖

import { corpus, words } from "./src/corpus.js";
import { trainBigram, nextWordProbs } from "./src/bigram.js";
import { softmax, applyTemperature, sampleIndex, greedy } from "./src/sampling.js";
import { mulberry32 } from "./src/rng.js";

const model = trainBigram(words(corpus));
const options = nextWordProbs(model, "the");
const probs = options.map((o) => o.prob);
const labels = options.map((o) => o.word);

console.log(labels.join(" "));
console.log(probs.map((p) => p.toFixed(2)).join(" "));
console.log("greedy:", labels[greedy(probs)]);

const rng = mulberry32(1);
const tally = {};
for (let i = 0; i < 10000; i++) {
  const word = labels[sampleIndex(probs, rng)];
  tally[word] = (tally[word] ?? 0) + 1;
}
console.log(tally);

// Same table, three temperatures: the share of the top choice
const logits = probs.map(Math.log);
for (const t of [0.5, 1, 2]) {
  const p = softmax(applyTemperature(logits, t));
  console.log(`t=${t}: top ${p[0].toFixed(2)}, bottom ${p[p.length - 1].toFixed(2)}`);
}
// coffee students app river morning table window next small
// 0.21 0.21 0.13 0.13 0.08 0.08 0.08 0.04 0.04
// greedy: coffee
// {
//   river: 1267,
//   coffee: 2143,
//   app: 1238,
//   small: 390,
//   students: 2052,
//   morning: 828,
//   table: 841,
//   window: 788,
//   next: 453
// }
// t=0.5: top 0.30, bottom 0.01
// t=1: top 0.21, bottom 0.04
// t=2: top 0.16, bottom 0.07

តើយើងបានរៀនអ្វី?

  • បន្ទាប់ពី "the" model មានជម្រើសប្រាំបួន។ "coffee" និង "students" ស្មើគ្នានៅ 21%។
  • យើង sample 10,000 ដង។ "coffee" ត្រូវបានជ្រើស 2,143 ដង (21.4%) និង "students" 2,052 ដង (20.5%) "small" តែ 390 ដង (3.9%)។ ពេលរមៀលច្រើនដង ចំនួនរាប់ត្រូវនឹងប្រូបាប។ នោះគឺជាអត្ថន័យនៃ sampling។
  • Temperature 1 sample ពី distribution របស់ model ផ្ទាល់។ នៅ 0.5 ជម្រើសខាងលើបំផុតកើនដល់ 30% ហើយជម្រើសទំនងតិចបំផុតរួញទៅ 1%។ នៅ 2 គម្លាតបិទទៅ 16% ធៀបនឹង 7%។

ដោយសារយើងប្រើ mulberry32(1) លទ្ធផលទាំងមូលថេរ។ ជាមួយ Math.random ចំនួនរាប់នឹងខុសគ្នាបន្តិចក្នុងគ្រប់ការរត់ (ឧទាហរណ៍ "coffee" អាចជា 2,090 ឬ 2,190) ប៉ុន្តែវានឹងនៅជិតប្រូបាបជានិច្ច។

6. Greedy៖ យកខាងលើបំផុតជានិច្ច

វិធីសាមញ្ញបំផុតក្នុងការជ្រើសគ្មានភាពចៃដន្យទាល់តែសោះ៖ ជ្រើសជម្រើសដែលមានប្រូបាបខ្ពស់បំផុតជានិច្ច។ នេះហៅថា greedy decoding៖

export function greedy(probs) {
  let best = 0;
  for (let i = 1; i < probs.length; i++) {
    if (probs[i] > probs[best]) best = i;
  }
  return best;
}

Greedy តែងតែត្រឡប់ចម្លើយដូចគ្នាសម្រាប់ input ដូចគ្នា។ ស្តាប់ទៅល្អឥតខ្ចោះ ហើយសម្រាប់ការងារខ្លះវាពិតជាល្អ៖ ចាត់ថ្នាក់អត្ថបទ ដកស្រង់ field ថេរ ឆ្លើយការស្វែងរកការពិត។ ប៉ុន្តែសម្រាប់ការសរសេរ វាមានភាពខ្សោយដែលគេស្គាល់ ហើយអ្នកនឹងឃើញវាក្នុងផ្នែកទី 8៖ អត្ថបទពីការជ្រើស greedy ច្រើនតែធ្លាក់ចូល loop ដដែលៗ ព្រោះការបន្តទំនងបំផុតនៃឃ្លាមួយ ទំនោរនាំត្រឡប់ទៅឃ្លាដដែល។

(ពេលជម្រើសពីរស្មើគ្នា greedy ត្រឡប់មួយដែលវាឃើញមុន ដែលជាមូលហេតុដែលយើងតម្រៀបជម្រើស តាមលំដាប់ថេរក្នុង nextWordProbs។ ក្នុងលទ្ធផលខាងលើ "coffee" ឈ្នះ "students" ក្នុងការស្មើគ្នា ដោយសារតែលំដាប់នោះប៉ុណ្ណោះ។)

7. Top-k និង Top-p៖ កាត់កន្ទុយ

Temperature ខ្ពស់មានផលរំខានអាក្រក់មួយ។ វាធ្វើឲ្យពាក្យមិនទំនងកាន់តែទំនង រួមទាំងពាក្យដែល មិនទំនងពិតប្រាកដ គឺកន្ទុយគ្មានន័យនៃបញ្ជី។ ជាមួយវាក្យសព្ទដែលមាន token រាប់ម៉ឺន មានជម្រើសអាក្រក់រាប់ពាន់ ហើយសូម្បីតែឱកាសតូចៗនីមួយៗក៏បូកបញ្ចូលគ្នាដែរ។ តម្រងពីរជួសជុលរឿងនេះ ដោយកាត់កន្ទុយ មុន sampling។

Top-k៖ រក្សាតែ k ជម្រើសដែលទំនងបំផុត បោះបង់ដែលនៅសល់ ហើយធ្វើមាត្រដ្ឋានអ្នកនៅរស់ ដើម្បីឲ្យពួកគេបូកបាន 1 ម្តងទៀត។

Top-p (ហៅផងដែរថា nucleus sampling)៖ រក្សាក្រុមតូចបំផុតនៃជម្រើសខាងលើ ដែលប្រូបាបរបស់ពួកវាបូកបានយ៉ាងហោចណាស់ p ឧទាហរណ៍ 0.9។ ពេល model ប្រាកដ ក្រុមនោះតូច (ប្រហែលមួយពាក្យ)។ ពេលវាមិនប្រាកដ ក្រុមធំ។ ដូច្នេះ top-p សម្របតាមស្ថានភាព ខណៈ top-k រក្សាចំនួនថេរជានិច្ច។

export function topK(probs, k) {
  const keep = new Set(
    probs.map((p, i) => [p, i]).sort((a, b) => b[0] - a[0]).slice(0, k).map(([, i]) => i)
  );
  const kept = probs.map((p, i) => (keep.has(i) ? p : 0));
  const sum = kept.reduce((a, b) => a + b, 0);
  return kept.map((p) => p / sum);
}

export function topP(probs, p) {
  const order = probs.map((x, i) => [x, i]).sort((a, b) => b[0] - a[0]);
  const keep = new Set();
  let total = 0;
  for (const [x, i] of order) {
    keep.add(i);
    total += x;
    if (total >= p) break;
  }
  const kept = probs.map((x, i) => (keep.has(i) ? x : 0));
  const sum = kept.reduce((a, b) => a + b, 0);
  return kept.map((x) => x / sum);
}

Function ទាំងពីរត្រឡប់បញ្ជីប្រវែងដូចគ្នា ដោយមានសូន្យសម្រាប់ជម្រើសដែលត្រូវដកចេញ ដូច្នេះ index នៅតែតម្រឹមជាមួយពាក្យរបស់អ្នក។ រក្សាទុក demo-filters.js៖

import { topK, topP } from "./src/sampling.js";

const show = (list) => list.map((x) => x.toFixed(2)).join(" ");
const probs = [0.5, 0.25, 0.15, 0.07, 0.03];
console.log("original", show(probs));
console.log("topK 2  ", show(topK(probs, 2)));
console.log("topP 0.8", show(topP(probs, 0.8)));
console.log("topP 0.9", show(topP(probs, 0.9)));
console.log("topP 0.4", show(topP(probs, 0.4)));
// original 0.50 0.25 0.15 0.07 0.03
// topK 2   0.67 0.33 0.00 0.00 0.00
// topP 0.8 0.56 0.28 0.17 0.00 0.00
// topP 0.9 0.56 0.28 0.17 0.00 0.00
// topP 0.4 1.00 0.00 0.00 0.00 0.00

របៀបអាន៖

  • topK 2 រក្សា 0.50 និង 0.25 បន្ទាប់មកធ្វើមាត្រដ្ឋានឡើងវិញ៖ 0.50 / 0.75 = 0.67 និង 0.25 / 0.75 = 0.33។
  • topP 0.8៖ 0.50 គឺតែ 50% ការបន្ថែម 0.25 ផ្តល់ 75% នៅតិចជាង 80% ដូច្នេះ 0.15 ត្រូវបានបន្ថែមដែរ ហើយផលបូករហូតដល់ 90%។ ក្រុមគឺបីខាងលើបំផុត។
  • topP 0.9 ផ្តល់ក្រុមដូចគ្នា ព្រោះបីខាងលើបំផុតបូកបាន 90% ពិតប្រាកដ (នព្វន្ធ floating point ក្នុងឧទាហរណ៍ផ្សេងអាចធ្វើឲ្យករណីជិតព្រំដែនបែបនេះទៅម្ខាងណាក៏បាន)។
  • topP 0.4៖ ជម្រើសទីមួយតែម្នាក់ឯងមាន 50% រួចហើយ ដូច្នេះក្រុមមានសមាជិកម្នាក់ ហើយលទ្ធផលគឺ 100% លើវា។ Top-p ជាមួយ p តូចធ្វើតួដូច greedy។

លទ្ធផលទាំងប្រាំបន្ទាត់ថេរ។ ក្នុងប្រព័ន្ធពិត អ្នកជាទូទៅអាចផ្សំ temperature ជាមួយតម្រងមួយក្នុងចំណោមទាំងនេះ ហើយ API ជាច្រើនអនុវត្ត temperature មុន បន្ទាប់មកតម្រង បន្ទាប់មក sample។ នោះគឺជាលំដាប់ដែល function generateSampled របស់យើងប្រើបន្ទាប់។ (លំដាប់ពិតប្រាកដ និងតម្លៃលំនាំដើមខុសគ្នារវាងប្រព័ន្ធ ដូច្នេះអានឯកសាររបស់ប្រព័ន្ធដែលអ្នកប្រើ។)

8. Prompt ដូចគ្នាត្រង់ Temperature 0, 0.7 និង 1.5

ដល់ពេលមើលឥទ្ធិពលលើអត្ថបទពិត។ ក្នុងរូបមន្តអ្នកនឹងបន្ថែម generateSampled ដែលជាកំណែនៃ generate ដែលទទួលជម្រើស temperature, topK និង topP ដោយ temperature 0 មានន័យថា greedy។ "Prompt" របស់យើងគឺពាក្យចាប់ផ្តើម "the" ហើយយើងសុំ 12 ពាក្យ។ រក្សាទុក demo-generate-sampled.js៖

import { corpus, words } from "./src/corpus.js";
import { trainBigram } from "./src/bigram.js";
import { generateSampled } from "./src/sampling.js";
import { mulberry32 } from "./src/rng.js";

const model = trainBigram(words(corpus));

for (const temperature of [0, 0.7, 1.5]) {
  console.log(`temperature ${temperature}`);
  for (const seed of [1, 2, 3]) {
    const text = generateSampled(model, "the", 12, { temperature }, mulberry32(seed));
    console.log("  ", text);
  }
}

console.log("topK 2, temperature 1.5");
for (const seed of [1, 2, 3]) {
  console.log("  ", generateSampled(model, "the", 12, { temperature: 1.5, topK: 2 }, mulberry32(seed)));
}
console.log("topP 0.8, temperature 1.5");
for (const seed of [1, 2, 3]) {
  console.log("  ", generateSampled(model, "the", 12, { temperature: 1.5, topP: 0.8 }, mulberry32(seed)));
}
// temperature 0
//    the coffee shop is quiet and dara writes code and dara writes
//    the coffee shop is quiet and dara writes code and dara writes
//    the coffee shop is quiet and dara writes code and dara writes
// temperature 0.7
//    the river and the small coffee shop needs good app saves time
//    the river and dara is calm in the students laugh and a
//    the river and the coffee and the students order iced coffee shop
// temperature 1.5
//    the morning and a table dara drinks iced tea the small app
//    the table by the river is a phone sokha opens the table
//    the table by the coffee sokha makes iced coffee shop needs good
// topK 2, temperature 1.5
//    the students order more iced tea the students come to the coffee
//    the students order iced coffee and the coffee shop after lunch the
//    the students order iced coffee and dara writes code and the coffee
// topP 0.8, temperature 1.5
//    the river and the window dara writes code for a student and
//    the morning and the river is a phone sokha opens the table
//    the morning and the coffee after lunch sokha cleans the river and

ចំណាំ៖

  • Temperature 0 ផ្តល់អត្ថបទដូចគ្នាសម្រាប់គ្រប់ seed។ Greedy មិនខ្វល់ពី random generator ទេ។ វាក៏បង្ហាញបញ្ហាការដដែលៗ៖ "dara writes code and dara writes" កំពុងវិលជុំរួចហើយ ហើយបើយើងសុំ 50 ពាក្យ វានឹងបន្តវិលតាមពាក្យតិចតួចដដែល។
  • Temperature 0.7 និង 1.5 ផ្តល់អត្ថបទខុសគ្នាសម្រាប់គ្រប់ seed។ ភាពខុសគ្នារវាង 0.7 និង 1.5 ពិបាកមើលក្នុងឧទាហរណ៍បី ព្រោះតារាងកំប្លែងរបស់យើងតូច ប៉ុន្តែ 1.5 លោតច្រើនជាង ("the table by the coffee sokha makes iced coffee shop")។
  • Top-k និង top-p បង្ក្រាបអត្ថបទ temperature ខ្ពស់។ ជាមួយ topK: 2 គំរូទាំងបីនៅលើផ្លូវធម្មតា និងសមហេតុផល ("the students order iced coffee") ទោះបី temperature នៅតែ 1.5 ក៏ដោយ។

លទ្ធផលទាំងអស់នេះថេរ ព្រោះគ្រប់ការរត់ប្រើ generator ដែលមាន seed និង model ថេរ។ ក្នុង chat app ពិត អ្នកទទួលបានលទ្ធផលខុសគ្នាគ្រប់ពេលដែលអ្នកចុច "regenerate" ព្រោះ seed ត្រូវបានជ្រើសថ្មី។

តើ temperature "ត្រឹមត្រូវ" គឺប៉ុន្មាន? គ្មានតម្លៃតែមួយទេ។ វាអាស្រ័យលើការងារ ដែលយើងនិយាយក្នុង ផ្នែកទី 10។ តម្លៃនៅកណ្តាលដូចជា 0.7 គឺជាតម្លៃលំនាំដើមទូទៅក្នុងឧបករណ៍ជាច្រើន ប៉ុន្តែពិនិត្យតម្លៃលំនាំដើមនៃឧបករណ៍ដែលអ្នកប្រើ ព្រោះវាខុសគ្នា។

9. តើ Temperature 0 ផ្តល់ចម្លើយដូចគ្នាជានិច្ចទេ?

ក្នុងកំប្លែងរបស់យើង temperature 0 ផ្តល់លទ្ធផលដូចគ្នាគ្រប់ដងដោយធានា៖ វាជា pure function ដែលគ្មានភាពចៃដន្យ។ តើនោះក៏ពិតដែរទេ ពេលអ្នកហៅ model ធំhosted ជាមួយ temperature 0? ជាធម្មតាលទ្ធផលស្រដៀងគ្នាខ្លាំង ប៉ុន្តែមិនតែងតែដូចគ្នាបេះបិទទេ។ នេះជាសភាពការណ៍ដ៏ស្មោះត្រង់៖

  • Temperature 0 ដកការជ្រើសចៃដន្យចេញ។ Model គួរជ្រើស token ខាងលើបំផុតគ្រប់ដង។
  • ប៉ុន្តែពិន្ទុខ្លួនឯងអាចប្រែប្រួលបន្តិច។ Model ធំដំណើរការលើ chip ស្របគ្នាជាច្រើន ហើយការបូក floating point ផ្តល់លទ្ធផលខុសគ្នាបន្តិចអាស្រ័យលើ លំដាប់ ដែលលេខត្រូវបានបូក។ លំដាប់អាចប្តូរតាមរបៀបដែលសំណើត្រូវបានដាក់ជាក្រុមជាមួយគ្នានៅលើ server ឬ hardware ណាដែលដោះស្រាយសំណើរបស់អ្នក។ Token ពីរដែលមានពិន្ទុស្ទើរតែស្មើគ្នា អាចប្តូរកន្លែងគ្នា ហើយពេលពាក្យមួយខុសគ្នា អ្វីៗទាំងអស់បន្ទាប់អាចខុសគ្នា ព្រោះពាក្យថ្មីនីមួយៗអាស្រ័យលើពាក្យមុនៗ។
  • អ្នកផ្តល់សេវាក៏អាចប្តូរអ្វីៗនៅពីក្រោយឆាក៖ ការធ្វើបច្ចុប្បន្នភាព model ការរៀបចំ serving ផ្សេង ឬការកំណត់ផ្សេងទៀត។ API ខ្លះផ្តល់ជម្រើស seed ហើយពណ៌នាវាថា "best effort" សម្រាប់លទ្ធផលដែលអាចធ្វើឡើងវិញបាន មិនមែនជាការសន្យាទេ។

ដូច្នេះច្បាប់ជាក់ស្តែងគឺ៖ កុំសរសេរកម្មវិធីដែលត្រូវការចម្លើយរបស់ model hosted ឲ្យដូចគ្នា byte ទល់ byte។ បើកូដរបស់អ្នកប្រៀបធៀបលទ្ធផលជាមួយ string ថេរ ឬតេស្តរបស់អ្នករំពឹងប្រយោគដូចគ្នាបេះបិទគ្រប់ដង ត្រូវរំពឹងថាវានឹងខូចម្តងម្កាល។ ជំនួសមកវិញ ត្រូវសុំលទ្ធផលមានរចនាសម្ព័ន្ធ (មើល Prompt, Role និង Structured Output) ពិនិត្យ លក្ខណៈ ដែលអ្នកខ្វល់ ហើយរក្សាទុកលទ្ធផលបើអ្នកត្រូវការភាពអាចធ្វើឡើងវិញបាន។

llmlab របស់យើងទទួលបានភាពអាចធ្វើឡើងវិញបានពិតប្រាកដ ព្រោះវាតូច រត់លើម៉ាស៊ីនតែមួយ ធ្វើនព្វន្ធតាមលំដាប់ថេរ ហើយយើងគ្រប់គ្រង seed។ ប្រព័ន្ធពិតគ្មានភាពងាយស្រួលទាំងនោះទេ។

10. ការជ្រើសរើសការកំណត់ក្នុងគម្រោងពិត

គិតពី temperature និងតម្រងជាប៊ូតុងបង្វិលរវាង ទុកចិត្តបាន និង ច្នៃប្រឌិត។ ទាំងនេះជាច្បាប់មេដៃ មិនមែនច្បាប់ទេ។ សាកល្បងលើការងាររបស់អ្នកផ្ទាល់៖

ការងារការកំណត់ដែលត្រូវសាកល្បងមូលហេតុ
ដកស្រង់ទិន្នន័យ ចាត់ថ្នាក់ ឆ្លើយពីឯកសារថេរទាប ឬ 0អ្នកចង់បានចម្លើយទំនងបំផុត ដោយស្ថិរភាព
សរសេរកូដ ជួសជុល bugទាបSyntax មិនអត់ឱនទេ ការវាយខុសដោយច្នៃប្រឌិតធ្វើឲ្យវាខូច
Chat assistant សង្ខេបមធ្យមពាក្យសម្ដីធម្មជាតិ ហើយនៅតែនៅលើប្រធានបទ
គិតឈ្មោះ រឿង ពាក្យស្លោកខ្ពស់ជាងអ្នកចង់បានភាពចម្រុះ និងការភ្ញាក់ផ្អើល
ជម្រើសច្រើនដើម្បីប្រៀបធៀបមធ្យម ឬខ្ពស់ រត់ច្រើនដងទទួលបានបេក្ខជនខុសៗគ្នា

ដំបូន្មានជាក់ស្តែងខ្លះ៖

  1. ប្តូរការកំណត់ម្តងមួយ។ Temperature និង top-p ទាំងពីរប្តូរការរាលដាល ហើយមគ្គុទ្ទេសក៍ជាច្រើនណែនាំឲ្យកែមួយ មិនមែនទាំងពីរទេ។ ពិនិត្យអ្វីដែលអ្នកផ្តល់សេវារបស់អ្នកណែនាំ។
  2. ទាប មិនស្មើនឹងត្រឹមត្រូវទេ។ Temperature 0 ផ្តល់ចម្លើយ ទំនងបំផុត របស់ model ដែលមិនចាំបាច់ជាចម្លើយ ពិត ទេ។ ចម្លើយខុសដោយទំនុកចិត្តនៅតែខុសនៅគ្រប់ temperature។ យើងត្រឡប់មកវិញក្នុង ហេតុអ្វី Model ចេះ Hallucinate។

11. រូបមន្ត៖ Module sampling

បន្ថែមឧបករណ៍នៃជំពូកនេះទៅ llmlab។ បង្កើត src/sampling.js៖

// src/sampling.js
import { nextWordProbs } from "./bigram.js";

// Scores (any numbers) -> probabilities that are positive and sum to 1.
export function softmax(logits) {
  const max = Math.max(...logits); // subtract max so exp() cannot overflow
  const exps = logits.map((x) => Math.exp(x - max));
  const sum = exps.reduce((a, b) => a + b, 0);
  return exps.map((e) => e / sum);
}

// t < 1 sharpens, t > 1 flattens. t must be above 0.
export function applyTemperature(logits, t) {
  if (!(t > 0)) throw new Error("temperature must be > 0 (use greedy for 0)");
  return logits.map((x) => x / t);
}

// Pick an index using probabilities and a random number in [0, 1).
export function sampleIndex(probs, rng = Math.random) {
  const r = rng();
  let running = 0;
  for (let i = 0; i < probs.length; i++) {
    running += probs[i];
    if (r < running) return i;
  }
  return probs.length - 1; // guards against rounding error
}

// Index of the biggest probability. No randomness.
export function greedy(probs) {
  let best = 0;
  for (let i = 1; i < probs.length; i++) {
    if (probs[i] > probs[best]) best = i;
  }
  return best;
}

// Keep the k most likely options, zero the rest, renormalize.
export function topK(probs, k) {
  const keep = new Set(
    probs.map((p, i) => [p, i]).sort((a, b) => b[0] - a[0]).slice(0, k).map(([, i]) => i)
  );
  const kept = probs.map((p, i) => (keep.has(i) ? p : 0));
  const sum = kept.reduce((a, b) => a + b, 0);
  return kept.map((p) => p / sum);
}

// Keep the smallest group of top options whose total reaches p.
export function topP(probs, p) {
  const order = probs.map((x, i) => [x, i]).sort((a, b) => b[0] - a[0]);
  const keep = new Set();
  let total = 0;
  for (const [x, i] of order) {
    keep.add(i);
    total += x;
    if (total >= p) break;
  }
  const kept = probs.map((x, i) => (keep.has(i) ? x : 0));
  const sum = kept.reduce((a, b) => a + b, 0);
  return kept.map((x) => x / sum);
}

// Bigram generation with all the knobs. temperature 0 means greedy.
export function generateSampled(model, startWord, length, options = {}, rng = Math.random) {
  const { temperature = 1, topK: k, topP: p } = options;
  const out = [startWord];
  let current = startWord;
  for (let i = 1; i < length; i++) {
    const opts = nextWordProbs(model, current);
    if (opts.length === 0) break;
    let idx;
    if (temperature === 0) {
      idx = greedy(opts.map((o) => o.prob));
    } else {
      const logits = opts.map((o) => Math.log(o.prob));
      let probs = softmax(applyTemperature(logits, temperature));
      if (k) probs = topK(probs, k);
      if (p) probs = topP(probs, p);
      idx = sampleIndex(probs, rng);
    }
    current = opts[idx].word;
    out.push(current);
  }
  return out.join(" ");
}

Function ដែល export គឺ softmax(logits), applyTemperature(logits, t), sampleIndex(probs, rng), greedy(probs), topK(probs, k), topP(probs, p) និង generateSampled(model, startWord, length, options, rng)។ ចុងក្រោយគឺជា helper ជាក់លាក់សម្រាប់ bigram ដែលភ្ជាប់ផ្នែកនានាចូលគ្នា តាមរបៀបដូចគ្នានឹង decoding loop ពិត៖ ពិន្ទុ temperature softmax តម្រង sample បន្ថែម។ ឧបករណ៍ command line នៃជំពូកក្រោយ ប្រើវាឡើងវិញ ដូច្នេះរក្សា file ដូចអ្វីដែលបានបង្ហាញ។ បើអ្នករក្សា src/index.js ត្រូវបន្ថែម export * from "./sampling.js";។

12. សាកល្បងដោយខ្លួនឯង

  1. Softmax ដោយដៃ។ គណនា softmax([0, 0, 0]) និង softmax([0, 1]) ក្នុងក្បាល ឬលើក្រដាស បន្ទាប់មកពិនិត្យដោយកូដ។ ហេតុអ្វីលេខនៃចម្លើយទីពីរគឺប្រហែល 0.27 និង 0.73?
  2. រកចំណុចកាត់។ សម្រាប់ logit [2.0, 1.0, 0.1] ធ្វើ loop t ពី 0.1 ដល់ 10 ជាជំហាន 0.1 ហើយបោះពុម្ពប្រូបាបនៃជម្រើសខាងលើបំផុត។ នៅ t ប៉ុន្មានដែលវាជិត 50%? តើមានអ្វីកើតឡើងពេល t ធំខ្លាំងណាស់?
  3. តេស្ត sampler។ Sample 100,000 ដងពី [0.7, 0.2, 0.1] ដោយ generator ដែលមាន seed ហើយបោះពុម្ពចំណែកនៃ index នីមួយៗ។ តើវាជិត 70%, 20% និង 10% ទេ? សាកល្បងជាមួយ 10 sample។ តើមានអ្វីប្រែប្រួល ហើយហេតុអ្វី?
  4. ធ្វើឲ្យវាខូចដោយចេតនា។ ហៅ generateSampled ជាមួយ temperature 5 ហើយគ្មានតម្រង បន្ទាប់មកជាមួយ topK: 3។ ប្រៀបធៀបលទ្ធផល 5 នៃនីមួយៗ។ មួយណាសមហេតុផលជាង?
  5. សាកល្បងចំណុចចាប់ផ្តើមផ្សេង។ ប្រើ "sokha" និង "dara" ជាពាក្យចាប់ផ្តើម នៅ temperature 0។ ហេតុអ្វីលទ្ធផលនៃការបង្កើត greedy ប្តូរតាមពាក្យចាប់ផ្តើម ប៉ុន្តែមិនប្តូរតាម seed?

13. សំណួរដែលអ្នកនឹងត្រូវគេសួរ

តើ temperature ធ្វើអ្វីពិតប្រាកដ? វាប្តូរថាតើចំណាត់ថ្នាក់របស់ model ត្រូវបានធ្វើតាមយ៉ាងតឹងរ៉ឹងប៉ុណ្ណាពេលជ្រើស token បន្ទាប់។ Temperature ទាបធ្វើឲ្យជម្រើសខាងលើៗលេចធ្លោជាង ដូច្នេះចម្លើយផ្តោត និងអាចធ្វើឡើងវិញបាន។ Temperature ខ្ពស់បែងចែកប្រូបាបស្មើគ្នាជាង ដូច្នេះចម្លើយចម្រុះជាង ហើយពេលខ្លះចម្លែក។ ចំណាត់ថ្នាក់ខ្លួនឯងមិនប្តូរទេ។

តើ temperature ខ្ពស់ជាង "ច្នៃប្រឌិតជាង" ទេ? វា ចៃដន្យ ជាង ដែលអាចមើលទៅដូចជាច្នៃប្រឌិត ប៉ុន្តែភាពចៃដន្យមិនដូចគំនិតល្អទេ។ លើសពីចំណុចមួយ លទ្ធផលគ្រាន់តែកាន់តែអាក្រក់។ សម្រាប់ការងារច្នៃប្រឌិត ប្រសើរជាងប្រើ temperature កណ្តាល ហើយបង្កើតជម្រើសច្រើន ជាងកំណត់ប៊ូតុងបង្វិលដល់អតិបរមា។

ហេតុអ្វីសំណួរដូចគ្នាផ្តល់ចម្លើយខុសគ្នាគ្រប់ដង? ព្រោះ model sample៖ វាជ្រើសដោយចៃដន្យតាមសមាមាត្រនៃប្រូបាប ជំនួសឲ្យការយកជម្រើសខាងលើបំផុតជានិច្ច។ ការជ្រើសនីមួយៗអាចខុសគ្នា ហើយដោយសារពាក្យនីមួយៗអាស្រ័យលើពាក្យមុនៗ ភាពខុសគ្នាតូចមួយនៅដើមលូតលាស់ទៅជាចម្លើយខុសគ្នា។ ការបន្ថយ temperature កាត់បន្ថយការប្រែប្រួល។

បើខ្ញុំកំណត់ temperature ទៅ 0 តើខ្ញុំនឹងទទួលបានចម្លើយដូចគ្នាបេះបិទជានិច្ចទេ? ជាធម្មតាចម្លើយនឹងស្រដៀងគ្នាខ្លាំង ប៉ុន្តែមិនតែងតែដូចគ្នាបេះបិទនៅលើ model ធំhostedទេ។ ភាពខុសគ្នាតូចៗក្នុងនព្វន្ធ floating point ពីរបៀបដែលការងារត្រូវបានបែងចែកនៅលើ hardware អាចបញ្ច្រាសការសម្រេចជិតគ្នារវាង token ពីរ។ កុំសរសេរកូដដែលអាស្រ័យលើលទ្ធផលដូចគ្នាពាក្យតាមពាក្យ។

Top-k និង top-p គឺជាអ្វី ក្នុងពាក្យសាមញ្ញ? ពួកវាជាតម្រងដែលអនុវត្តមុនការជ្រើសចៃដន្យ។ Top-k រក្សាតែ k ជម្រើសដែលទំនងបំផុត។ Top-p រក្សាក្រុមតូចបំផុតនៃជម្រើសដែលប្រូបាបរបស់ពួកវាបូកបាន p។ ទាំងពីរដកកន្ទុយគ្មានន័យដែលមិនទំនងចេញ ដូច្នេះ temperature ខ្ពស់មិនបង្កើតសំរាមទេ។

តើ temperature ទាបអាចបញ្ឈប់ hallucination បានទេ? វាអាចកាត់បន្ថយភាពចៃដន្យខ្លះ ប៉ុន្តែវាមិនធ្វើឲ្យ model ដឹងច្រើនជាងទេ។ Model ដែលខុសដោយទំនុកចិត្តអំពីអ្វីមួយ ជាធម្មតានឹងខុសនៅ temperature 0 ដែរ ហើយចម្លើយគ្រាន់តែស្ថិរភាពជាង។ ការជួសជុល hallucination ត្រូវការព័ត៌មានល្អជាង និងការត្រួតពិនិត្យ ដែលជំពូកក្រោយៗគ្របដណ្តប់។

ទៅបន្តទៀត។ ឥឡូវនេះអ្នកអាចពន្យល់ជំហានចុងក្រោយនៃរបៀបដែល LLM សរសេរ៖ ពិន្ទុក្លាយជាប្រូបាបដោយ softmax temperature កែរូបរាងពួកវា តម្រងកាត់ពួកវា ហើយ sampling ជ្រើស។ ប៉ុន្តែរហូតមកដល់ពេលនេះ ពិន្ទុមកពីការរាប់គូពាក្យ។ តើលេខនៅខាងក្នុង model ពិតមកពីណា? បន្តជាមួយ របៀប Train Model មួយ ដែលអ្នកមើល model តូចមួយរៀនពិន្ទុផ្ទាល់ខ្លួនរបស់វាដោយ gradient descent។

ទិញកាហ្វេជូនខ្ញុំ

ចូលចិត្តមេរៀននេះមែនទេ? សូមពិចារណាទិញកាហ្វេជូនខ្ញុំមួយកែវ ដើម្បីគាំទ្រការបង្កើតមាតិកាឥតគិតថ្លៃបន្ថែមទៀត!