Temperature, Top-p และ Determinism ที่คุณไม่มี
Temperature หาร logits ก่อน softmax ข้อเท็จจริงเดียวนี้ทำลายแนวคิดปุ่มปรับความสร้างสรรค์ พร้อมตัวอย่างคำตอบต่างกันจากคำสั่งเดิม
ในหน้านี้
นี่คือคำขอเดียวกันที่ส่งไปยังโมเดลเดียวกันห้าครั้ง weights เดิม, prompt เดิม, เครื่องเดิม, random seed เดิม สิ่งเดียวที่เปลี่ยนคือเลขหนึ่งตัว
prompt: "Q: What is the capital of France?\nA:"
T = 0.0 " Paris\nWhat is the question and does the answer answer it? The
question is: What is the capital of France?..."
T = 0.7 " Paris\nWhat is the question: Which city is the capital of
France?..."
T = 1.0 " Paris\nWhat is a good geographical qualifier for describing
Paris concerning its location?\nA: Near the Mediterranean Sea..."
T = 1.5 " Paris\nWhat clue from premise allows we to conclude that Godwin
was &, He chose Healing Crimson Colour No:white flour Pure..."
T = 2.0 "安全感金华.ITEMT]]];\naims assume parental.st-importe.valtermination
Screens قطر_Zeroหมายเลข-zA ('$ספטמבר..."ไม่มีอะไรพัง token ทุกตัวในบรรทัดสุดท้ายถูกสุ่มมาอย่างถูกต้องจากการแจกแจงความน่าจะเป็นของโมเดลเองเหนือ vocabulary 151,936 รายการ เลขที่เปลี่ยนเรียกว่า temperature เอกสารส่วนใหญ่อธิบายว่ามันเป็นปุ่มปรับความสร้างสรรค์ และคำอธิบายนั้นผิดในแบบที่บทนี้จะแสดงให้เห็น ไม่ใช่แค่กล่าวอ้าง
นี่ก็เป็นบทที่คำสัญญาสามข้อก่อนหน้านี้ถึงเวลาต้องชำระ บทที่ 4 นิยาม logit แล้วแทบไม่ได้ใช้มันจริง ๆ กล่องเรื่อง floating-point ของ บทที่ 2 จบด้วยคำสั่งว่า — จำเรื่องนี้ไว้เมื่อบทที่ 17 ถามว่าทำไม prompt, model และ seed เดียวกันจึงให้ tokens ต่างกันได้ และกล่อง mixture-of-experts ของ บทที่ 9 สัญญาว่าจะรวบรวมสาเหตุของ non-determinism สี่ข้อ ทั้งสามมาถึงด้านล่างนี้
บรรทัดเดียวที่ทั้งบทแขวนอยู่
ลิงก์ไปยังส่วน: บรรทัดเดียวที่ทั้งบทแขวนอยู่บทที่ 4 แนะนำ logit ว่าเป็นคะแนนค่าจริงที่ยังไม่ถูก normalize หนึ่งค่าต่อ class บทที่ 8 ทำให้ language model สร้างหนึ่งค่าต่อรายการใน vocabulary softmax แปลงเวกเตอร์นั้น ให้เป็นความน่าจะเป็น:
Temperature เข้ามาตรงนี้ — ชื่อนี้ยืมมาจากสถิติฟิสิกส์ ซึ่งพารามิเตอร์เดียวกันควบคุมว่า Boltzmann distribution จะกระจุกตัวบนสถานะพลังงานต่ำอย่างคมแค่ไหน1 — และ มันหาร logits ก่อน exponential:
ตำแหน่งนี้คือกลไกทั้งหมด และควรดูพีชคณิตสองบรรทัดเพื่อเห็นว่ามันอยู่ที่อื่นไม่ได้ สมมติว่าคุณพยายามใช้ temperature กับความน่าจะเป็นแทน — scale มันด้วย แล้ว renormalise คุณจะได้
ค่าคงที่ตัดกันหมด การ scale ความน่าจะเป็นไม่ทำอะไรเลย; การแจกแจงกลับมาเหมือนเดิม Temperature มีผลก็ต่อเมื่อมันกระทำกับ exponent เพราะการหารด้วย ก่อน exponentiating เท่ากับยกความน่าจะเป็นแต่ละตัวกำลัง — การปรับรูปแบบไม่เชิงเส้นที่เปลี่ยน อัตราส่วน ระหว่างรายการ แทนที่จะเปลี่ยน scale ร่วมของมัน
จากตำแหน่งนั้น ขีดจำกัดทั้งสองตามมาโดยไม่ต้องทำอะไรเพิ่ม เมื่อ logit ที่ใหญ่ที่สุดจะหนีห่างตัวที่เหลือ และ จะยุบลงบน token ที่ได้คะแนนสูงสุดเพียงตัวเดียว: greedy decoding เมื่อ โตขึ้น ทุกตัวจะมุ่งสู่ศูนย์ exponential ทุกตัวมุ่งสู่ 1 และการแจกแจงจะแบนลงจนเข้าใกล้ uniform เหนือ vocabulary ทั้งหมด ที่ พอดี สูตรจะหารด้วยศูนย์ ดังนั้น implementation ทุกตัวจึง special-case ให้เป็นค่าสูงสุดทางเลขคณิต — รวมถึง widget ด้านล่าง ซึ่งสลับไปใช้ argmax ที่
ขอเตือนหนึ่งอย่าง เพราะชื่อซ้ำกันทำให้สับสนจริง ๆ ใน machine learning ยังมีอีกสิ่งหนึ่งที่ไม่เกี่ยวกันแต่ชื่อ temperature เช่นกัน: temperature scaling วิธี calibration ที่ fit ค่าเดียวบน validation set เพื่อให้ confidence ของ classifier ตรงกับ accuracy ของมัน2 สูตรเดียวกัน แต่ไม่เกี่ยวกับ generation บทความที่พูดว่า “temperature” มักหมายถึงอันนั้น; บทนี้ไม่เคยหมายถึงมัน
นี่คือการแจกแจงนั้น พร้อมเลขคณิตให้เห็นตรงหน้า logits ถูกตรึงไว้และสมเหตุสมผล ดังนั้นตัวเลขในคำอธิบายด้านล่างตรวจเทียบกับสิ่งที่คุณเห็นได้:
Temperature ไม่ใช่ปุ่มปรับความสร้างสรรค์
ลิงก์ไปยังส่วน: Temperature ไม่ใช่ปุ่มปรับความสร้างสรรค์ตัวเลข ␣banana คือข้อโต้แย้งทั้งหมดในรูปย่อ: การเพิ่ม temperature ไม่สามารถให้ไอเดียที่โมเดลไม่มีอยู่แล้วแก่โมเดลได้ logits ถูกคำนวณแล้ว ranking ถูกกำหนดแล้ว และ temperature รักษา ranking นั้นไว้อย่างสมบูรณ์ — ความร้อนแค่ไหนก็ไม่เคยย้าย token ที่ได้คะแนนต่ำกว่าให้อยู่เหนือ token ที่ได้คะแนนสูงกว่าได้ สิ่งที่มันทำทั้งหมดคือกระจาย mass ลงไปตาม ranking ที่โมเดลสร้างเอง Temperature สูงไม่ได้ทำให้โมเดลคิดสร้างสรรค์กว่าเดิม; มันทำให้มีโอกาส emit tokens ที่มันให้คะแนนว่าแย่ มากขึ้น
บน vocabulary จริง เรื่องนี้หยุดเป็นความแปลกเล็ก ๆ และกลายเป็นเหตุผลที่ output จาก high-temperature ใช้งานไม่ได้ วัดบน Qwen/Qwen2.5-0.5B-Instruct, forward pass หนึ่งครั้ง, prompt ด้านบน, นับว่าต้องใช้กี่ tokens เพื่อสะสมสัดส่วนหนึ่งของ probability mass:
| temperature | top-1 probability | entropy | tokens holding 80 % | 90 % | 95 % | 99 % |
|---|---|---|---|---|---|---|
| 0.5 | 99.98 % | 0.00 nats | 1 | 1 | 1 | 1 |
| 0.7 | 99.65 % | 0.03 nats | 1 | 1 | 1 | 1 |
| 1.0 | 96.01 % | 0.30 nats | 1 | 1 | 1 | 14 |
| 1.2 | 88.20 % | 0.88 nats | 1 | 2 | 13 | 252 |
| 1.5 | 62.83 % | 3.07 nats | 29 | 353 | 2,672 | 26,787 |
| 2.0 | 16.62 % | 8.19 nats | 13,516 | 32,966 | 55,231 | 101,205 |
อ่านแถวล่างช้า ๆ ที่ บนคำถามที่มีคำตอบถูกต้องเพียงหนึ่งเดียว tokens ต่างกัน 32,966 ตัวแบ่งกันถือ top 90 % ของ probability mass นั่นไม่ใช่พื้นที่สร้างสรรค์ที่กว้างขึ้น แต่มันคือโมเดลที่ถูกเลขคณิตสั่งให้ปฏิบัติต่ออนุภาคภาษาเกาหลีกับ identifier ภาษา C++ เหมือนเป็นตัวเลือกที่ยังมีชีวิตสำหรับคำหลัง A: ขยะในบล็อกเปิดคือผลโดยตรง และมันไม่ใช่ bug ในโมเดลหรือ library — มันคือสิ่งที่คำขอนั้นขอไว้
ช่วงที่มีประโยชน์แคบและขึ้นกับงาน ไม่ใช่รสนิยม สำหรับคำถามเชิงข้อเท็จจริง คำตอบคือ token เดียว และความร้อนเหนือประมาณ 1.2 ใส่ error เข้ามาโดยไม่ได้ประโยชน์อะไร สำหรับงานปลายเปิด มี continuation ดี ๆ มากกว่าหนึ่งจริง และความร้อนบางส่วนซื้อ variety ที่ยังลื่นไหลได้:
"Write a two-sentence story about a lighthouse."
T = 0.0 "The lighthouse stood tall and proud, its beacon illuminating the
night sky above. A lone sailor, his eyes fixed on the distant
horizon..."
T = 0.7 "In the quiet, stormy waters of the sea, a lighthouse stood
sentinel over the horizon, its golden dome casting a warm glow
on the fog-shrouded streets below..."
T = 1.0 "In the quiet night, a lone lighthouse stood sentinel over the
sea, its shining beacon a beacon of hope and solace for sailors
and fishermen across the vast and endless ocean..."
T = 1.3 "In the gentle sunlight, now reflecting upon the opening of Jack's
lighthouse, Jim Trahan, a small-time individual difficult to
define in paperwork, wondered about a career where simplicity
reigns..."ที่ 1.3 โมเดลสร้างชื่อเฉพาะและประโยคที่ parse ไม่ได้ขึ้นมา ช่วงระหว่าง “เหมือนเดิมทุกครั้ง” กับ “ไม่รู้เรื่อง” อยู่ประมาณ 0.6 ถึง 1.1 สำหรับโมเดลนี้ในงานนี้ และคำแนะนำที่ซื่อตรงคือคุณต้องหาโดยวัดบนงาน ของคุณ ไม่ใช่คัดลอกเลขจากบล็อกโพสต์
ทำไมข้อความที่น่าจะเป็นที่สุดจึงเป็นข้อความที่แย่
ลิงก์ไปยังส่วน: ทำไมข้อความที่น่าจะเป็นที่สุดจึงเป็นข้อความที่แย่มีคำถามชัดเจนซ่อนอยู่ใต้ทั้งหมดนี้: ถ้าโมเดลมีการแจกแจงความน่าจะเป็นและ token หนึ่งน่าจะเป็นที่สุด ทำไมไม่เลือกมันเสมอ? Greedy decoding ฟรี ทำซ้ำได้ และไม่ต้องใช้พารามิเตอร์
เพราะผลลัพธ์เป็นแบบนี้:
prompt: "In a shocking finding, scientists discovered a herd of unicorns
living in a remote valley."
greedy: " The unicorns were so rare that they were not even recognized by
the local people. The unicorns were so rare that they were not
even recognized by the local people. The unicorns were so rare
that they were not even recognized by the local people. ..."
repeated 4-grams: 87.6 %แปดประโยค หนึ่งประโยค เกือบเก้าในสิบของหน้าต่างสี่-token เคยปรากฏก่อนหน้าใน output เดียวกันแล้ว นี่คือ neural text degeneration ซึ่ง Holtzman et al. ตั้งชื่อและอธิบายไว้ใน paper ที่แนะนำ top-p3 โมเดลไม่ได้พัง; การ maximize sequence probability เป็น objective ที่ผิดสำหรับข้อความปลายเปิด Human writing ไม่ใช่ลำดับคำที่น่าจะเป็นที่สุด — มันมี surprise, per-token probability ของมันลอยขึ้นลง ดิ่งแล้วฟื้น — ขณะที่เส้นทางความน่าจะเป็นสูงสุดคือ fixed point ที่เมื่อเข้าไปแล้วไม่มีเหตุผลให้หลุดออกมา
นั่นคือเหตุผลที่ sampling มีอยู่ตั้งแต่แรก และส่วนที่มักถูกละไว้คือ มันไม่ใช่กฎสากล บทที่ 12 วัดได้ 24 จาก 24 ข้อถูกในโจทย์คำสองขั้นด้วย plain greedy decoding และ sampling ที่ temperature 0.8 ทำให้ลดเหลือ 81 %; self-consistency จากนั้นใช้ tokens มากขึ้นหกเท่าเพื่อปีนกลับไปยังจุดที่ greedy อยู่แล้ว ข้อเท็จจริงทั้งสองจริงพร้อมกัน:
Open-ended generation. ไม่มี continuation ที่ถูกต้องเพียงหนึ่งเดียว ดังนั้นตัวที่น่าจะเป็นที่สุดจึงเป็นกับดัก — มันวนลูป และ 87.6 % ของมันคัดลอกจากตัวเอง Sample
งานที่มีคำตอบถูกต้องหนึ่งเดียว. มี continuation ที่ถูกต้องเพียงหนึ่งเดียวจริง ดังนั้นการดึงอย่างอื่นคือการดึง error 100 % ของบทที่ 12 กลายเป็น 81 % ด้วยเหตุผลนี้พอดี อย่า sample
prompt ใน production ส่วนใหญ่เป็นแบบที่สอง แต่ถูกตั้งค่าเหมือนแบบแรก เพราะ temperature ถูกปล่อยไว้ตามตัวอย่าง code ที่ใช้
วิธีตัดสองแบบ และมีเพียงแบบเดียวที่ปรับตัวได้
ลิงก์ไปยังส่วน: วิธีตัดสองแบบ และมีเพียงแบบเดียวที่ปรับตัวได้การ sample จากการแจกแจงเต็มไม่ใช่สิ่งที่ใครทำจริง เพราะ tail มหึมาและเต็มไปด้วยเรื่องไร้สาระ ต้องมีอะไรบางอย่างถูกตัดออก คำตอบแบบคลาสสิกมีสองแบบ และต่างกันในแง่เดียวที่ตัดสินทุกอย่าง
Top-k เก็บ candidates จำนวนคงที่ เรียงตามความน่าจะเป็น เก็บ ตัวแรก ทิ้งที่เหลือ แล้ว renormalise4 Top-p หรือที่เรียกว่า nucleus sampling เก็บ mass ปริมาณคงที่: หยิบ tokens ตามลำดับลดหลั่นจน cumulative probability ถึง แล้วหยุด3 อย่างเป็นทางการ nucleus คือเซตที่เล็กที่สุด ที่มี
ความต่างฟังดูเหมือนเครื่องสำอางแต่ไม่ใช่ เพราะ prompts สองอันที่คุณส่งในนาทีเดียวกันมีรูปทรงการแจกแจงต่างกันโดยสิ้นเชิง ทั้งสองรายการนี้คือโมเดลเดียวกันที่ temperature 1:
Q: What is the capital of France?\nA: | Once upon a time, | |
|---|---|---|
| top-1 probability | 96.01 % | 25.39 % |
| tokens holding 90 % of the mass | 1 | 467 |
| top-k = 40 keeps | 99.61 % of the mass | 78.87 % of the mass |
| mass in ranks 2 to 40 | 3.61 % | 53.48 % |
| token at rank 40 | ␣Av, 0.0093 % | ␣Dr, 0.128 % |
ค่า คงที่หนึ่งค่า ล้มเหลวสองทิศทางตรงข้ามกัน บน factual prompt, รับ tokens 39 ตัวที่รวมกันมีค่า 3.6 % — มันปล่อยขยะผ่านเข้ามา รวมถึง candidate ที่มีค่าเก้าในพันของเปอร์เซ็นต์ เพราะกฎนับ slots ไม่ใช่หลักฐาน บน story prompt, เดียวกันโยนทิ้ง mass 21 % ที่โมเดลให้ไว้จริง เพราะ nucleus จริงตรงนั้นกว้าง 467 tokens
Top-p ทำให้เลขเพียงตัวเดียวทำงานทั้งสองแบบได้ ตั้ง แล้วมันเก็บ 1 token ใน prompt แรกและ 467 ใน prompt ที่สอง เพราะมันถามคำถามเกี่ยวกับการแจกแจง แทนที่จะบังคับจำนวนลงไป ดูการปรับตัวนั้นตรง ๆ — cut เดียวกัน, temperature สี่ค่า:
widget นั้นยังปิดความเข้าใจผิดที่ควรระบุชื่อ เพราะมันทำให้ผู้คนเสียเงินจริง บนการแจกแจงที่มั่นใจ top_p = 0.9 ไม่ใช่ “variety นิดหน่อย” มันคือ greedy ที่ temperature 1 token นำตรงนี้ถือ 96.90 % ซึ่งเกิน 0.9 อยู่แล้ว ดังนั้น nucleus กว้างหนึ่ง token และไม่มีอย่างอื่นถูกดึงได้เลย ทีมตั้ง top_p เป็น 0.9 โดยเชื่อว่าคลายอะไรบางอย่างแล้วสงสัยว่าทำไมทุก response เหมือนกัน
ตั้ง top-k แทน แล้วความล้มเหลวตรงข้ามก็เห็นชัดไม่แพ้กัน:
penalties พร้อมสูตร เพราะคนสับสนกันเป็นโรคระบาด
ลิงก์ไปยังส่วน: penalties พร้อมสูตร เพราะคนสับสนกันเป็นโรคระบาดกลไกสามแบบเดินทางภายใต้ชื่อคล้ายกัน มันทำสิ่งต่างกัน และความต่างวัดได้ ให้ เป็นจำนวนครั้งที่ token ปรากฏไปแล้ว
Presence penalty
ลิงก์ไปยังส่วน: Presence penaltyลบ ค่าคงที่ จาก token ใด ๆ ที่เคยปรากฏเลย ปรากฏหนึ่งครั้งกับปรากฏสี่สิบครั้งถูกลงโทษเท่ากัน มันคือสวิตช์ ไม่ใช่ปุ่มหมุน
Frequency penalty
ลิงก์ไปยังส่วน: Frequency penaltyลบตาม สัดส่วนของจำนวนครั้ง token ที่ถูกใช้สี่ครั้งถูกลงโทษหนักกว่า token ที่ใช้ครั้งเดียวสี่เท่า และแรงกดทับซ้อนเมื่อข้อความยาวขึ้น
Repetition penalty (CTRL)
ลิงก์ไปยังส่วน: Repetition penalty (CTRL)ตัวดั้งเดิมจาก paper CTRL7 มัน หาร แทนที่จะลบ โดยต้องแยกกรณี sign เพราะการหาร logit ติดลบจะทำให้มัน ใหญ่ขึ้น ดังนั้นความแรงของมันขึ้นกับ magnitude ของ logit ซึ่งหมายความว่า เดียวกันกระทบต่างกันในจุดต่าง ๆ ของประโยคเดียวกัน
continuation ที่ degenerate เดิมจากก่อนหน้า เมื่อนำแต่ละแบบไปใช้ “Steps altered” นับว่าจาก 120 generation steps มีเท่าไรที่เลือก token ต่างจากโมเดลที่ไม่ถูกลงโทษ การ run ตรงนี้มี 120 steps เทียบกับ 140 ในบล็อกด้านบน ซึ่งเป็นเหตุผลที่ baseline แบบไม่ลงโทษอ่านได้ 85.5 % ไม่ใช่ 87.6 %:
| setting | repeated 4-grams | steps altered |
|---|---|---|
| nothing | 85.5 % | 0 / 120 |
| presence 0.5 | 65.0 % | 3 / 120 |
| presence 1.0 | 3.4 % | 11 / 120 |
| frequency 0.5 | 6.0 % | 12 / 120 |
| frequency 1.0 | 0.0 % | 20 / 120 |
| repetition 1.2 (CTRL) | 0.0 % | 35 / 120 |
มีสามอย่างตามมา Presence ที่ 0.5 เปลี่ยนการตัดสินใจสามครั้งจาก 120 และลด repetition ลงหนึ่งในสี่ — ลูปถูกยึดไว้ด้วย tokens เพียงหยิบมือ Frequency ที่ 0.5 เปลี่ยนการตัดสินใจมากกว่าสี่เท่าและให้ผลใหญ่กว่ามาก เพราะตัวคูณ count โตต่อไปเรื่อย ๆ ขณะที่ค่าคงที่ของ presence ไม่โต และ CTRL penalty ที่ค่าถูกคัดลอกกันแพร่หลาย 1.2 เขียน 35 จาก 120 การตัดสินใจ ใหม่ ซึ่งไม่ใช่การสะกิด; มันคือโมเดลคนละตัว
เลขสุดท้ายนั้นคือฉากตั้งต้นสำหรับความล้มเหลวที่ไม่มีใครเตือน
penalties ทำอะไรกับข้อความที่ควรต้องซ้ำ
ลิงก์ไปยังส่วน: penalties ทำอะไรกับข้อความที่ควรต้องซ้ำCode ซ้ำ Tables ซ้ำ Lists ซ้ำ Structured output ซ้ำโดยนิยาม — นั่นแหละคือ structure penalty แยกไม่ออกระหว่างโมเดลที่ติดลูปกับโมเดลที่กำลัง emit แถวที่สี่ของตารางอย่างถูกต้อง เพราะทั้งสองดูเหมือน token ปรากฏอีกครั้ง
สามงานเดิม สร้างสามวิธี:
| task | nothing | frequency 0.5 | repetition 1.2 |
|---|---|---|---|
| markdown table, 6 rows | 0 / 56 steps altered | 0 / 56 | 2 / 62 |
| Python function | 0 / 93 | 0 / 93 | 10 / 110 |
| bulleted list, 1 to 12 | 0 / 50 | 0 / 50 | 0 / 50 |
Frequency penalty ที่ 0.5 กลายเป็นว่าไม่เป็นอันตรายในทั้งสามงาน ซึ่งเป็นผลที่มีประโยชน์และน่าประหลาดใจเล็กน้อย และมันบอกอะไรที่แม่นยำ: เนื่องจากไม่มีการตัดสินใจเปลี่ยน structural tokens ต้องชนะตำแหน่งของตัวเองด้วย margin มากกว่าค่าที่ penalty ลบ แม้หลังจากปรากฏห้าและหกครั้งแล้ว CTRL penalty ซึ่ง หาร แทน กลับเขี่ยมันหลุด และนี่คือสิ่งที่มันสร้าง:
repetition 1.2, markdown table:
| n | 2^n |
| --- | --- |
| 0 | 1 |
| 1 | 2 |
| 2 | 4 |alignment พัง: ปริมาณ padding ภายในแต่ละ cell เปลี่ยนจากแถวหนึ่งไปอีกแถวหนึ่ง เพราะชุด spaces ก่อน closing pipe คือ repetition แบบที่ penalty ถูกสร้างมาเพื่อทำลายพอดี เป็นเรื่อง cosmetic และใช้ tokens เพิ่มอีกหกตัว เคส Python ไม่ใช่ cosmetic:
nothing / frequency 0.5:
total = 0
for i in range(1, n + 1):
total += i ** 2
return total
repetition 1.2:
# Initialize total_sum with 0
total_sum = 0
# Loop through numbers from 1 to n, incrementing by 2 each time
for i in range(1, n + 1,penalty ผลักโมเดลออกจาก total — ที่ใช้ไปแล้วใน docstring — ไปยัง total_sum, ยัด output ด้วย comments ที่แต่งขึ้นเพื่อใช้ budget กับ tokens ที่ยังไม่ถูกใช้ แล้วเดินเข้าสู่ range แบบสามอาร์กิวเมนต์พร้อม stride comment บอกว่า incrementing by 2 each time ซึ่งผิดสำหรับผลรวมกำลังสองจาก 1 ถึง repetition penalty สร้าง code ที่ไม่ถูกต้องจาก prompt ที่ตอบได้ถูกต้องเมื่อไม่มีมัน
กฎที่ตามมาสั้น ๆ: penalties มีไว้สำหรับ prose ปลายเปิด และควรปิดสำหรับ code, structured output, tabular data และสิ่งใดก็ตามที่มี schema บทที่ 18 ว่าด้วยหมวดที่สองนั้นพอดี
ลำดับการใช้ และทำไมมันเปลี่ยนคำตอบ
ลิงก์ไปยังส่วน: ลำดับการใช้ และทำไมมันเปลี่ยนคำตอบimplementation จริงทุกตัวใช้สิ่งเหล่านี้ในลำดับเฉพาะหนึ่งลำดับ:
penalties → temperature → top-k → top-p → sample
นี่ไม่ใช่งานบัญชีตามใจชอบ และการสลับสองขั้นทำให้การแจกแจงต่างกันจริง ๆ สองการวัด ทั้งคู่บน factual prompt
ตัดก่อนหรือหลัง temperature. nucleus ถูกคำนวณบนการแจกแจงที่มันได้รับ และ temperature เปลี่ยนการแจกแจงนั้นอย่างรุนแรง:
| top-p 0.9 after temperature | top-p 0.9 before temperature | |
|---|---|---|
| 1 token | 1 token | |
| 353 tokens | 1 token | |
| 32,966 tokens | 1 token |
ที่ setting nominal เดียวกันให้ candidate set ขนาด 32,966 หรือ 1 ขึ้นอยู่กับล้วน ๆ ว่าขั้นไหนทำก่อน ถ้าคุณเคยสงสัยว่าทำไมการเพิ่ม temperature “ไม่ทำอะไร” กับ provider หนึ่งแต่ทำลาย output กับอีก provider โดยใช้ตัวเลขสองตัวเดียวกัน ตารางนี้คือคำตอบที่เป็นไปได้
ลงโทษก่อนหรือหลัง temperature. ลบ penalty แล้วค่อยหารด้วย ให้ effective penalty เป็น ; หารก่อนแล้วค่อยลบให้ ด้วย presence penalty 1.0 ที่ใช้กับ leading token:
| temperature | penalise, then temper | temper, then penalise |
|---|---|---|
| 0.5 | 99.858 % | 99.948 % |
| 1.0 | 89.839 % | 89.839 % |
| 2.0 | 10.783 % | 6.830 % |
เหมือนกันที่ อย่างที่ต้องเป็น ต่างกันด้วย factor 1.58 ที่ “Presence penalty 1.0” ไม่ใช่ปริมาณ penalty ที่นิยามชัดเจน เว้นแต่คุณรู้ด้วยว่า temperature ถูกใช้ตรงไหน และไม่มี API ไหน document เรื่องนี้
แสดงรายละเอียด
ทางเลือก: pipeline ทั้งหมด ตามลำดับด้านบน
สิบหกบรรทัด และทุกอย่างในบทนี้อยู่ในนั้น มันคือ computation เดียวกับที่ widget ทำ บนเวกเตอร์ logit จริงแทนที่จะเป็นตัวเลขคงที่สิบตัว
def sample(logits, counts, presence=0.0, frequency=0.0,
temperature=1.0, top_k=0, top_p=1.0, generator=None):
z = logits.clone()
idx = torch.tensor(list(counts)) # 1. penalties
if len(idx):
z[idx] -= presence
z[idx] -= frequency * torch.tensor([float(c) for c in counts.values()])
if temperature <= 0: # 2. temperature
return int(z.argmax()) # T=0 is argmax
p = torch.softmax(z / temperature, -1)
p, order = p.sort(descending=True)
if top_k: # 3. top-k
p[top_k:] = 0
p = p * ((p.cumsum(0) - p) < top_p) # 4. top-p
p = p / p.sum() # 5. renormalise
return int(order[torch.multinomial(p, 1, generator=generator)])cumsum(0) - p ในบรรทัด top-p คือ cumulative mass ที่ไม่รวม token ปัจจุบัน ซึ่งเป็นสิ่งที่ทำให้ nucleus รวม token ที่ข้าม threshold เข้าไป แทนที่จะหยุดก่อนหน้ามันพอดี ทำ off by one ตรงนี้ แล้ว top_p = 0.9 จะกลายเป็น cut ที่แน่นกว่า implementation อื่นเล็กน้อยอย่างเงียบ ๆ
นี่เป็นหนึ่งในไม่กี่จุดในครึ่งหลังของคอร์สที่ Python เป็นภาษาที่ถูกต้อง และเหตุผลเป็นเชิงโครงสร้าง ไม่ใช่สไตล์: ทุกบรรทัดด้านบนต้องมีเวกเตอร์ logits เต็มอยู่ในมือ และเมื่อผ่าน HTTP API เวกเตอร์นั้นไม่มีอยู่ คุณส่ง temperature และ top_p ไปให้ provider ได้; คุณ implement มันเองไม่ได้ และคุณมองไม่เห็นว่ามันทำอะไร
ไม่มี sampling API สากล
ลิงก์ไปยังส่วน: ไม่มี sampling API สากลprovider ทุกเจ้ารับ subset ต่างกันของ controls เหล่านี้ ด้วยช่วงค่าต่างกัน และเพิกเฉยส่วนที่เหลืออย่างเงียบ ๆ นี่ไม่ใช่คำบ่นเชิงนามธรรม แอปพลิเคชันใดก็ตามที่มีตัวเลือก model ต้องเขียนความต่างเหล่านี้ไว้ที่ไหนสักแห่ง และไฟล์ที่ทำแบบนั้นคือแผนที่ของความเข้ากันไม่ได้ นี่คือสิ่งที่ catalogue หนึ่งในลักษณะนี้ประกาศสำหรับพารามิเตอร์เดียวเหนือ text sources เก้าแหล่งที่มันรองรับ:
| declared temperature range | sources |
|---|---|
| 0 to 1 | Anthropic, Google, Meta, Cerebras, PaLM |
| 0 to 1.5 | Mistral |
| 0 to 2 | OpenAI, DeepSeek, xAI |
คำเดียวกัน แต่ scale ไม่เหมือนกัน “temperature of 1” คือการแจกแจงที่ไม่ถูกแก้ไขสำหรับที่หนึ่ง และเป็นความร้อนสูงสุดที่อนุญาตในอีกที่หนึ่ง และครึ่งหนึ่งของ catalogue ไม่สามารถแสดงค่าที่อีกครึ่งถือว่า neutral-plus-a-bit ได้ knobs ที่เหลือไม่สม่ำเสมอพอกัน: entries ของ OpenAI, DeepSeek และ xAI รับ presence และ frequency penalties และไม่มี topK; entries ของ Google, Meta, Cerebras และ PaLM รับ topK และไม่มี penalties; Anthropic รับ topK, topP และ stop sequences และไม่มี penalties; และ มีเพียงหนึ่งจากเก้า — Mistral — ที่รับ seed การส่งพารามิเตอร์ที่ provider ไม่ implement โดยทั่วไปไม่ให้ error เลย: request สำเร็จ, knob ไม่ทำอะไร, แล้วคุณสรุปว่า setting ไม่มีผล
และสังเกตว่าไฟล์แบบนี้คืออะไร: claim เกี่ยวกับ API ของคนอื่น เขียนขึ้นในวันหนึ่งวันใด และไม่มีอะไรตรวจสอบหลังจากนั้น catalogue ที่บอก 0 ถึง 1 สำหรับ provider ที่ตอนนี้รับ 0 ถึง 2 จะ cap ทุก request อย่างเงียบ ๆ
controls อีกสองตัวอยู่ในตระกูลเดียวกัน logprobs เมื่อมีให้ใช้ จะคืน log-probabilities ของ token ที่เลือก และมักคืน alternatives อันดับต้น ๆ ไม่กี่ตัว — หน้าต่างเดียวที่คุณมีสู่การแจกแจงที่บทนี้พูดถึง และเป็นฐานของ confidence heuristic ทุกแบบที่สร้างบน closed model ส่วน maximum tokens บวก stop sequences จบ generation โดยไม่อ้างอิงความน่าจะเป็นเลย: hard cap และ string match ทั้งคู่ปรากฏเป็น finish_reason จาก บทที่ 14 ซึ่ง length หมายความว่าคำตอบของคุณถูกตัดกลางประโยคด้วย budget ไม่ใช่โมเดลจบเอง
seed และ determinism ที่คุณไม่มี
ลิงก์ไปยังส่วน: seed และ determinism ที่คุณไม่มีตั้ง seed แล้ว sampling จะทำซ้ำได้ ส่วนนั้นจริง และตรวจสอบง่าย:
seed = 1234 " Paris\nWhat is a good geographical qualifier for describing
Paris concerning its location?\nA: Near the Mediterranean Sea"
seed = 1234 " Paris\nWhat is a good geographical qualifier for describing
Paris concerning its location?\nA: Near the Mediterranean Sea"
seed = 7 " Paris is the capital of France. The appellation of Paris is
\"Île de Paris\"."
seed = 7 " Paris is the capital of France. The appellation of Paris is
\"Île de Paris\"."เหมือนกันระดับ byte ภายใน seed เดียวกัน ต่างกันข้าม seeds เป็นไปตามที่โฆษณาไว้พอดี ดังนั้นสิ่งที่ seed fix คือ random draw ในบรรทัดสุดท้ายของฟังก์ชัน sample นั้น — token ใดถูกเลือกเมื่อมีการแจกแจงหนึ่ง
สิ่งที่มันไม่ fix คือการแจกแจง และตรงนั้นคือปัญหา เพราะเวกเตอร์ logits ที่โมเดลของคุณสร้างไม่ใช่วัตถุทางคณิตศาสตร์; มันคือผลลัพธ์ของการบวก floating-point หลายพันล้านครั้ง และการบวกเหล่านั้นมีลำดับ
บทที่ 2 เตรียมการทดลองนี้ไว้แล้ว ตัวเลข float32 หนึ่งล้านตัวเดียวกัน บวกด้วยการจัดกลุ่มต่างกัน:
sequential 998.564270020 error vs float64: 6.393e-03
pairwise (numpy) 998.570556641 error vs float64: 1.061e-04
in 4 chunks 998.570495605 error vs float64: 1.672e-04
in 8 chunks 998.570556641 error vs float64: 1.061e-04
in 16 chunks 998.570678711 error vs float64: 1.594e-05
sequential == pairwise? False
4 chunks == 8 chunks? Falseดูบรรทัดสุดท้าย จำนวน chunks เปลี่ยนคำตอบ นั่นไม่ใช่เรื่องแปลกเกี่ยวกับ numpy; มันคือกลไก เพราะเมื่อ inference server แบ่ง reduction ไปยังหน่วย parallel มากขึ้นหรือน้อยลง มันกำลังทำสิ่งนี้พอดี และ server แบ่งตามจำนวน requests ที่มันกำลังให้บริการ
นี่คือผลนั้นบนตัวโมเดลเอง prompt เดียวกัน, forward pass เดียวกัน, ความต่างเดียวคือมี requests อื่นเกิดขึ้นใน batch กี่รายการ:
20 identical forward passes, batch of 1: 20 / 20 bit-for-bit identical
the same prompt inside a batch of 2: 147,321 of 151,936 logits differ
the same prompt inside a batch of 4: 146,515 of 151,936 logits differ
the same prompt inside a batch of 8: 146,515 of 151,936 logits differ
the same prompt inside a batch of 16: 147,321 of 151,936 logits differ
largest change to any logit: 2.5e-05run ลำพัง โมเดล deterministic สมบูรณ์ — ยี่สิบ pass เหมือนกันระดับ bit ใส่ prompt เดียวกันเป๊ะเข้าไปใน batch พร้อม requests ที่ไม่เกี่ยวข้อง แล้ว 97 % ของ logits เปลี่ยน ไม่มีอะไรเกี่ยวกับ request ของคุณเปลี่ยน มี request ของคนอื่นมาถึง
ทีนี้ส่วนที่ซื่อตรง เพราะเรื่องนี้มักถูกเล่าเหมือนจบแค่นั้น การเปลี่ยน จะเปลี่ยน output ก็ต่อเมื่อ candidate tokens สองตัวอยู่ห่างกันภายในค่านั้น จาก 717 generation steps ในสิบสอง prompts ช่องว่างที่เล็กที่สุดระหว่าง top two logits คือ — ใหญ่กว่า perturbation ร้อยเท่า — และ ไม่มี step ใดใกล้พอที่จะ flip ดังนั้นบนโมเดลนี้, ใน float32, บน laptop, batching ขยับ logit ทุกตัวแต่ไม่เปลี่ยน token ใดเลย
นั่นคือคำอธิบายของเงื่อนไขที่เอื้ออำนวย ไม่ใช่คำปลอบใจ และเปลี่ยนเงื่อนไขเพียงอย่างเดียวก็พอ:
same weights, same prompts, greedy decoding, no seed involved
float32 vs bfloat16: 6 of 8 answers diverge
first divergence at step 23, on average
float32: "...it is scattered and dispersed into different colors,
including blue. The blue light is scattered more than other
colors, so it appears to come from the sky."
bfloat16: "...it is scattered and scattered, causing the colors of the
sun to be scattered and scattered, creating the appearance
of a blue color."หกจากแปดคำตอบแยกทาง และหนึ่งคำตอบแย่ลงมาก ตารางของบทที่ 2 บอกว่าทำไม: bfloat16 เก็บ mantissa 7 bits ดังนั้นใกล้ magnitude logit 16 ค่าที่ represent ได้ห่างกัน 0.125 — 16.0, แล้ว 16.125, แล้ว 16.25 — และ rounding สามารถขยับ logit ได้ถึง 0.0625 ขณะเดียวกัน 4.7 % ของ generation steps ที่วัดด้านบนมี top-two gap ต่ำกว่า 0.1 นั่นคือความต่างทั้งหมดระหว่างสองการทดลอง: ใน float32 perturbation เล็กกว่าการตัดสินใจที่ใกล้ที่สุดร้อยเท่า และใน bfloat16 มันมีขนาดเท่ากัน Production inference run ใน 16-bit บน hardware ที่มี fused kernels และ reduction orders ที่ไม่มีใครสัญญาว่าจะคงไว้ คำถามว่า “numerical noise เล็กน้อยจนละเลยได้ไหม” เป็นคำถามเกี่ยวกับ precision และ hardware ไม่ใช่เกี่ยวกับโมเดล
ดังนั้น สาเหตุสี่ข้อ ตาม catalogue ที่บทที่ 9 สัญญาไว้:
Floating-point addition ไม่ associative
ลิงก์ไปยังส่วน: Floating-point addition ไม่ associativeกล่องของบทที่ 2 ลำดับของผลรวมเปลี่ยนค่าของมัน ดังนั้นการเปลี่ยนวิธีแบ่ง reduction ใด ๆ จะเปลี่ยน logits นี่คือ substrate; อีกสามข้อคือวิธีเปลี่ยนลำดับ
Dynamic batching จัดกลุ่ม request ของคุณกับคนแปลกหน้า
ลิงก์ไปยังส่วน: Dynamic batching จัดกลุ่ม request ของคุณกับคนแปลกหน้าContinuous batching จาก บทที่ 13 คือเหตุผลที่ inference ราคาเอื้อมถึง — และหมายความว่ารูปร่างของ matrices ที่ tokens ของคุณไหลผ่านขึ้นกับ traffic วัดไว้ด้านบน: logits 147,321 ตัวขยับเพราะ batch size เปลี่ยน
Mixture-of-experts routing ขึ้นกับ batch
ลิงก์ไปยังส่วน: Mixture-of-experts routing ขึ้นกับ batchกล่องของบทที่ 9 พูดไว้แล้ว router เลือกแบบ discrete ต่อ token ต่อ layer ภายใต้ capacity limits ต่อ expert ที่คำนวณเหนือ batch token ที่หากอยู่ลำพังจะไป expert 7 เมื่อมีเพื่อนร่วมทางจะไป expert 12 นี่ไม่ใช่ความต่างจาก rounding; มันคือชุด weights คนละชุด
โมเดลหลังชื่อเปลี่ยน
ลิงก์ไปยังส่วน: โมเดลหลังชื่อเปลี่ยนversion string อย่าง -latest คือ pointer และ pointers ถูก repoint ได้ Providers ยัง update serving stack ภายใต้ version identifier คงที่ด้วย ทั้งสองอย่างไม่ได้ถูกประกาศใน granularity ที่ให้คุณ correlate กับ output ของคุณที่เปลี่ยนได้
พารามิเตอร์ seed ของ OpenAI ซื่อตรงเรื่องนี้ในทางเดียวที่มันทำได้: มันมาพร้อม field system_fingerprint ที่ระบุ backend configuration และ documentation ระบุว่า determinism เป็น best-effort และ fingerprint ที่เปลี่ยนหมายความว่าผลลัพธ์อาจต่าง อ่านมันตามที่มันเป็น — provider กำลังบอกคุณว่ามันควบคุมสาเหตุทั้งสี่ข้อข้างบน คุณควบคุมไม่ได้เลย และสิ่งเดียวที่มันเสนอได้คือบอกคุณ หลังจากข้อเท็จจริง ว่าบางอย่างขยับ
ต่อจากนี้ไปไหน
ลิงก์ไปยังส่วน: ต่อจากนี้ไปไหนทุกอย่างที่นี่ว่าด้วย knob และผลที่ตามมา ถอยออกมาหนึ่งระดับ แล้วปัญหาที่ยากกว่าจะปรากฏ: วัตถุที่เราปรับจูนกันอยู่คือการแจกแจงความน่าจะเป็น และการแจกแจงความน่าจะเป็นไม่มี interface
function call มี interface database row มี interface handler POST ที่คาดหวัง JSON body พร้อม required fields สามช่องมี interface และมันจะ reject อย่างอื่นทั้งหมด ระหว่างโมเดลกับ component อื่นทุกตัวในระบบของคุณมี contract ที่ฝ่ายหนึ่งให้คำสัญญาไม่ได้: โมเดลจะสร้าง บางอย่าง ที่ดึงมาจากการแจกแจงซึ่งคุณปรับรูปทรงแล้วแต่ไม่ได้ตรึงไว้ และ code อีกฝั่งต้องการค่าชนิดที่รู้จัก ไม่เช่นนั้นก็ throw
สะพานระหว่างสองโลกนั้นสร้างจากวัสดุของบทนี้ ไม่ใช่จาก parsing และ retries ถ้า token หนึ่งจะทำลาย structure ที่ต้องการ คุณไม่ sample มันแล้วหวัง — คุณตั้ง logit ของมันเป็น ก่อนที่ softmax จะเห็นมันด้วยซ้ำ Constrained decoding คือ mask เหนือเวกเตอร์เดียวกับที่เราใช้ทั้งบทปรับรูปทรง และมันเปลี่ยน “please reply in JSON” จากคำขอให้เป็นการรับประกัน
บทที่ 18 คือ contract นั้น: tool calling, JSON Schema, structured outputs และสิ่งที่ต้องใช้เพื่อทำให้ deterministic system ปลอดภัยพอจะสร้างทับบน probabilistic system
แหล่งที่มาและวิธีการ
ลิงก์ไปยังส่วน: แหล่งที่มาและวิธีการการวัดทั้งหมดในบทนี้มาจาก Qwen/Qwen2.5-0.5B-Instruct บน CPU, float32 เว้นแต่ระบุไว้ โดย implement sampling ตามที่เขียนใน section ทางเลือก แทนที่จะ delegate ให้ library มันเป็นโมเดลขนาดเล็ก และค่าจำเพาะเป็นของมัน; กลไกไม่ใช่ Von Platen, How to generate text with different decoding methods (Hugging Face, 2020) คือบทความที่บทนี้วัดเทียบ และยังเป็นบทนำสั้นที่ดีที่สุดสู่เนื้อหาเดียวกัน สำหรับ section determinism: reproducibility notes ของ PyTorch อธิบายว่า seed fix และไม่ fix อะไรบนเครื่องเดียว, documentation ของ OpenAI สำหรับ seed และ system_fingerprint อธิบายว่า provider สัญญาอะไรได้และไม่ได้ และบทสนทนาในปี 2025 ของ Thinking Machines เรื่อง batch-invariant kernels เป็นคำอธิบายสาธารณะที่ชัดที่สุดว่าทำไมการแก้สิ่งนี้ที่ระดับ inference-server เป็นไปได้แต่ไม่ฟรี
รายการอ้างอิง
ลิงก์ไปยังส่วน: รายการอ้างอิง-
Ackley, D. H., Hinton, G. E. and Sejnowski, T. J. A Learning Algorithm for Boltzmann Machines. Cognitive Science 9(1), pp. 147–169 (1985), ซึ่ง temperature ใน softmax มาจาก statistical physics Hinton, G., Vinyals, O. and Dean, J., Distilling the Knowledge in a Neural Network, arXiv:1503.02531 (2015), section 2, คือที่ที่พารามิเตอร์เดียวกันกลับมาปรากฏใน deep learning สมัยใหม่ — ในฐานะวิธีเผยการแจกแจงเต็มของ teacher ซึ่งเป็น soft labels ของบทที่ 13 ไม่ใช่ sampling ของบทนี้ ↩
-
Guo, C., Pleiss, G., Sun, Y. and Weinberger, K. Q. On Calibration of Modern Neural Networks. arXiv:1706.04599 (2017). อย่าสับสนสิ่งนี้กับ temperature ในบทนี้ Temperature scaling fit ค่าเดียวบน validation set เพื่อให้ confidence ของโมเดลตรงกับ accuracy ของมัน; มันเป็น post-hoc calibration method ที่ใช้กับ outputs ของ classifier Temperature sampling คือ runtime control เหนือวิธีที่ generator ดึง tokens สูตรเดียวกัน จุดประสงค์ต่างกัน และไม่มีค่าที่ใช้ร่วมกัน ↩
-
Holtzman, A., Buys, J., Du, L., Forbes, M. and Choi, Y. The Curious Case of Neural Text Degeneration. arXiv:1904.09751 (2019). แนะนำ nucleus sampling และการวัดที่ว่า maximisation-based decoding สร้างข้อความที่ probability profile ไม่เหมือน human text เลย ↩ ↩2
-
Fan, A., Lewis, M. and Dauphin, Y. Hierarchical Neural Story Generation. arXiv:1805.04833 (2018). paper ที่ทำให้ top-k sampling เป็นที่นิยม ↩
-
Nguyen, M. et al. Turning Up the Heat: Min-p Sampling for Creative and Coherent LLM Outputs. arXiv:2407.01082 (2024). ↩
-
Meister, C., Pimentel, T., Wiher, G. and Cotterell, R. Locally Typical Sampling. arXiv:2202.00666 (2022). ↩
-
Keskar, N. S., McCann, B., Varshney, L. R., Xiong, C. and Socher, R. CTRL: A Conditional Transformer Language Model for Controllable Generation. arXiv:1909.05858 (2019). Section 4.1 คือ repetition penalty ดั้งเดิม — ตัวที่หาร ↩