取樣參數:
模型怎麼挑下一個字
上一課你把引擎架好了——Ollama 兩行指令能跑、vLLM 扛得住併發。 接下來不管你用哪一個,每一次呼叫都會遇到同一組參數: temperature、top_p、 frequency_penalty、presence_penalty。 它們看起來各管各的,其實全部在調同一顆不公平的骰子。
先看骰子長什麼樣。模型讀完「今天天氣真」之後,不會直接挑一個字—— 它給每個候選字一個機率,然後照這組機率擲一次骰子。 拉動 temperature,看同一組候選字的機率怎麼被壓尖、壓平:
右邊的實驗場是真的 Python(在你的瀏覽器裡跑,不用安裝任何東西)。 首次載入約需 30–60 秒,正好夠你讀完第 1 節。裡面每一根拉桿都在真的算 softmax、 真的跑生成迴圈——改壞了重新整理就復原,這是你的沙盒,盡量玩。
模型輸出的是機率分佈,不是一個字
這是整堂課唯一要先接受的事實:語言模型每一步吐出來的是一排分數(logit), 經過 softmax 變成所有候選字的機率。「今天天氣真」之後的五個候選長這樣:
| 候選字 | 機率 | 直覺 |
|---|---|---|
| 好 | 47.1% | 模型最想講的 |
| 不錯 | 26.2% | 第二順位,也很常出現 |
| 熱 | 15.7% | 合理但沒那麼通用 |
| 冷 | 10.5% | 合理但沒那麼通用 |
| 量子 | 0.52% | 長尾亂字——就是它讓模型偶爾講怪話 |
「取樣」就是照這組機率擲一顆不公平的骰子:好佔 47% 的面積、量子佔 0.52%。 右邊第 1 節可以真的擲——擲 200 次,量子很可能一次都不出現; 把次數拉到 2000,它就會冒出來十幾次。 這件事很重要:機率小不等於不會發生,只是要等。 一篇長文有幾千個 token,每一個都是一次擲骰——長尾遲早會被抽中。
所以接下來的四個參數,做的都是同一件事的三種變化: 改機率(temperature)、改名單(top_p)、改分數(penalty)。
temperature:T 在分母,這就是全部
公式只有這一條,而且只要記住一件事:T 在分母。
- T 小 ⇒ 除以小數 ⇒ logit 之間的差距被放大 ⇒ 分佈變尖 ⇒ 幾乎只選第一名
- T 大 ⇒ 除以大數 ⇒ 差距被抹平 ⇒ 分佈變平 ⇒ 冷門字也有機會
同一組 logit,只是換了分母,出來的骰子差這麼多:
| T | 好 | 量子 | 行為 | 適合 |
|---|---|---|---|---|
| 0.2 | 94.6% | ≈0% | 每次幾乎相同:「今天天氣真好」 | 抽取/分類/寫程式 |
| 1.0 | 47.1% | 0.52% | 模型原本的想法 | 一般對話 |
| 2.0 | 34.5% | 3.64% | 每次都不同,開始出現怪組合 | 創作/腦力激盪 |
盯著最後一欄:T 從 1.0 拉到 2.0,「量子」的機率從 0.52% 變成 3.64%——七倍。 這就是「調高 temperature 比較有創意」的真面目:不是模型變聰明了,是你把爛選項的中獎機率放大了。 創意和胡言亂語,在這條公式裡是同一件事。
實務上 temperature 是幾乎每個任務都要先設的那一顆。 要可重現(抽欄位、分類、產生程式碼)就設 0;要多樣性就 0.7~1.0。先動它,其餘遇到問題再說。
top_p:核採樣,把長尾直接砍掉
temperature 是把爛選項的機率壓小,但永遠壓不到 0。 top_p(核採樣 / nucleus sampling)換一個思路:改名單。
- 候選字照機率由大到小排序
- 一路累加,累積機率第一次跨過 p 的那個字為止都留著
- 名單外的字丟棄——機率變成 0,不是變小,是抽不到了
- 留下來的重新歸一化,只在這個「核」裡面擲骰子
T=1 的五個候選,累積起來是這樣(top_p = 0.9):
| 候選字 | 機率 | 累積 | 結果 |
|---|---|---|---|
| 好 | 0.471 | 0.471 | 留 |
| 不錯 | 0.262 | 0.733 | 留 |
| 熱 | 0.157 | 0.890 | 留(差 0.01 就跨過門檻) |
| 冷 | 0.105 | 0.995 | 留——累積跨過 0.9,到它為止 |
| 量子 | 0.005 | 1.000 | 丟棄,永遠抽不到 |
這比單調 temperature 更能避免離譜輸出:怪字直接出局,而不是「機率很小」。 注意「熱」累積到 0.890,只差 0.01 就跨過門檻——把 top_p 拉到 0.88, 連「冷」都會被砍掉。核的邊界比你想的還敏感,右邊的拉桿一動就看得到。
但兩個一起大改就完蛋了。temperature 和 top_p 都在控隨機性,方向還相反: T 拉高讓量子的機率變 7 倍,top_p 又立刻把它整個砍掉——兩個參數在互相抵銷, 結果難以預測。所以實務建議只有一句:固定一個、調另一個。 多數 API 的 top_p 預設 1.0(或 0.9~0.95), 一般不必動它,先調 temperature 就好。
penalty:唯一會回頭看你寫過什麼的參數
前兩個參數只看「這一步」。penalty 不一樣——它會回頭數已經寫出來的字, 在 logit 上直接扣分:
差別全在後面那一項。假設模型正在寫餐廳評論,已經寫出「很棒」三次:
| 參數 | 扣法 | 「很棒」的機率 | 專治 | 常用值 |
|---|---|---|---|---|
| frequency_penalty | 按次數累加:扣 3 × penalty | 42.0% → 16.6% | 複讀機、長文跳針 | 0.3 ~ 0.7 |
| presence_penalty | 出現過就扣一次:1 次和 10 次一樣 | 42.0% → 35.2% | 鼓勵換新詞、換話題 | 0.3 ~ 0.6(預設 0) |
同樣填 0.5,力道差一倍以上——因為 frequency 乘的是次數 3,presence 乘的永遠是 1。 再把 frequency 拉到 2.0 看看:「很棒」剩 0.3%, 那些本來冷門的詞硬被推上第一名。這就是「penalty 開太高會開始說怪話」的長相。
連續寫下去,差別才真的浮出來
上面只算了「下一步」。penalty 的威力要一直寫下去才看得出來,因為次數會累積、扣分會變重。 右邊第 5 節真的跑生成迴圈:每一步照扣分後的分佈抽一個詞、把次數加上去、再算下一步, 同時跑 200 條長度 20 的序列取平均。跑出來是這樣:
| 設定 | 最常出現字佔比 (越低越不跳針) | 相異用字數 (八個詞用到幾個) |
|---|---|---|
| 無 penalty | 0.43 | 6.2 / 8 |
| frequency 0.5 | 0.26 | 7.4 / 8 |
| presence 0.5 | 0.40(幾乎沒動) | 6.8 / 8 |
這張表就是那兩句口訣的實際長相:frequency 把最愛用的詞壓掉四成(0.43 → 0.26), presence 幾乎沒碰它(0.43 → 0.40),但把用字撐開了(6.2 → 6.8)。 它們壓的根本不是同一件事。
右邊還有一張掃描圖,把 penalty 從 0 掃到 1.5:frequency 一路俯衝、presence 幾乎是條水平線。 原因就寫在公式裡——presence 對每個出現過的字通通只扣 1 × penalty, 寫得夠長、大家都出現過之後,整排被扣一樣多等於沒扣(softmax 對整排平移免疫)。 所以它不是治跳針的藥,它是「換點沒講過的詞」的藥。
這幾個數字是模擬跑出來的:真的照公式扣分、真的抽樣 200 條序列取平均。 你在右邊改 seed 或序列長度,數字會小幅浮動,但方向不會變。
什麼任務調什麼
整堂課壓成一句話:模型輸出的是機率分佈,四個參數都只是在改這顆骰子的挑法。
| 參數 | 改的是 | 常用值 | 什麼時候調 |
|---|---|---|---|
| temperature | 機率(分佈壓尖/壓平) | 0:抽取/分類/程式 0.7~1.0:創作 |
幾乎每個任務都先設它 |
| top_p | 名單(砍掉長尾候選字) | 預設即可 0.9~1.0 | 與 temperature 擇一調,別同時大改 |
| frequency_penalty | 分數(按出現次數累加扣) | 0.3 ~ 0.7 | 長文複讀、跳針時再開 |
| presence_penalty | 分數(出現過就扣一次) | 0.3 ~ 0.6 | 希望話題/用詞更多元時 |
調參的順序也是這張表的順序:先動 temperature,其餘遇到問題再調。 如果四個都調過還是不對,那通常不是取樣參數的問題——換模型的差異會比調參大得多。
換你動手
挑戰在右邊 notebook 的第 6 節,由淺到深:
把實驗區的 my_T 改成 0.05 和 2.0,各記下「量子」的機率。差幾個數量級?
自己寫一個 nucleus(probs, p) 函式做 top_p 截斷+重新歸一化。驗證條件:top_p=0.9 時「量子」要恰好是 0,其餘四個相加為 1。
驗證「presence penalty 寫越長越無效」:把生成長度從 20 拉到 80,分別算前 20 步與後 60 步的最常出現字佔比。先猜,哪一種 penalty 的兩個數字會幾乎一樣?
卡住了?每一題在 notebook 末節都有折疊解答(含預期輸出)——先自己做,再打開對照。
下一課要拆的是速度的另一半:模型每吐一個字,前面所有字的計算怎麼被存下來重複用—— 那個東西叫 KV Cache,也是你的顯示卡記憶體最大的消耗者。
情境測驗
離開前試試看:下面的情境都真的會遇到。每題選一個你認為的最佳做法,選了馬上看得到解釋。
Q1 情境題
你要把幾千張發票的 OCR 文字批次抽成 JSON 欄位。同一張發票跑兩次結果不一樣,偶爾還多出不相干的字。最該先做什麼?
抽取/分類這類任務要的是可重現,而 temperature 正是「幾乎每個任務都先設它」的那一顆:T 往下壓,分佈被壓尖到幾乎只剩第一名(課裡實算:T=0.2 時第一名已佔 94.6%,T=0.05 時是 100.0%),輸出就穩定了。B 方向對但不是最佳做法——top_p 砍的是名單,砍完仍在核裡擲骰子;T=1 時 top_p=0.5 還留著兩個候選字,照樣會飄,而且官方建議是與 temperature 擇一調。A 是治複讀機的藥,跟「結果不穩定」無關,還可能把本來就該重複出現的欄位名扣掉。D 成本最高,而且沒解到隨機性的根本原因。
Q2 情境題
模型在寫一份 800 字的產品說明,讀起來一直跳針:「很棒」出現了十幾次。你只能改取樣參數,最對症的是?
logit′ = logit − freq_penalty × 出現次數:出現越多次扣越重,正好對付「同一個詞出現十幾次」。課裡的生成模擬(200 條長度 20 的序列)量到:無 penalty 時最常出現的詞佔 0.43,開 frequency 0.5 掉到 0.26;改成 presence 0.5 只掉到 0.40——presence 乘的永遠是 0/1,出現 1 次和 10 次扣一樣多,所以 A 幾乎沒效(它的專長是把用字撐開,同一份模擬裡相異用字 6.2 → 6.8)。C 提高的是整體隨機性,跳針沒解決還可能更離譜。D 方向相反:把候選名單砍小,可挑的詞更少,只會更容易重複。
Q3 錯誤診斷
有人想讓模型「更敢講怪話」,於是把 temperature 從 1.0 拉到 2.0,同時把 top_p 從 1.0 降到 0.9。結果跑了幾百次,「量子」這個冷門字一次都沒出現。最可能的原因是?
關鍵在 top_p 砍的是名單不是機率:被排除的字機率歸零,不是變小,所以「多跑幾次」永遠不會出現——C 因此不成立。實算確實如表:T=2.0 讓量子從 0.52% 升到 3.64%(七倍),但排序累積到「冷」就已經是 0.964、跨過 0.9 門檻,量子落在核外,重新歸一化後恰好是 0。這正是「temperature 與 top_p 擇一調」的理由:兩者都在控隨機性、方向相反,同時大改就互相抵銷。修法:想放行長尾就把 top_p 留在 1.0 只調 temperature;或固定 T=1 只調 top_p。A 錯——T 拉更高只是讓量子的機率再大一點,照樣被同一條門檻砍掉。B 是杜撰的規則,top_p 永遠作用在當下那組分佈上。