跳轉到主要內容
部落格

LLM 後訓練入門

作者: 2025 年 8 月 26 日暫無評論

大型語言模型 (LLM) 徹底改變了我們撰寫和閱讀文件的方式。在過去一年左右的時間裡,我們發現它們的作用遠不止是文件的改寫:LLM 現在可以先思考再行動,它們可以規劃,可以呼叫瀏覽器等工具,可以編寫程式碼並檢查其是否有效,以及更多功能——事實上,這份清單還在迅速增長!

所有這些技能有什麼共同點?答案是它們都是在 LLM 訓練的後訓練階段開發的。儘管後訓練解鎖了我們幾年前看來如同魔法般的能力,但與 Transformer 架構和預訓練的基礎知識相比,它受到的關注卻出奇地少。 

本教程最初是為 Meta 基礎設施團隊編寫的,目標受眾是希望瞭解更多後訓練知識以貢獻力量的、沒有 LLM 建模專業知識的基礎設施工程師。我相信這涵蓋了很大一部分工程師:隨著強化學習成為主流,我們需要新的基礎設施才能提高生產力,因此彌合這一差距至關重要!現在我將其廣泛分享,希望 PyTorch Foundation 的更多人會有相似的背景和興趣,並且會像我們的團隊一樣,覺得這很有幫助。

後訓練入門

後訓練(有時也稱為“對齊”)是現代 LLM 的一個關鍵組成部分,也是“教導”模型如何以人類喜歡的方式回答問題以及如何進行推理的方法。

你可能會問,為什麼後訓練與預訓練不同?後訓練是為了讓模型能夠與使用者進行對話,而對話遵循一系列基本規則,例如:

  1. 在對話中,不止一位說話者,他們輪流發言
  2. 在發言前,你應該傾聽,才能說出相關內容

我們認為這些是顯而易見的,但預訓練只是在進行下一個詞預測,以讓模型瞭解世界,因此其中的資料是完全非結構化的,模型從未學習過這些基本規則。事實上,經過預訓練的模型通常不擅長理解它應該在一段時間後停止說話,會像 Google 自動完成框一樣喋喋不休。

此外,對模型施加一些絕對優先於其他一切的基本規則也很有用。這透過系統提示(和/或透過有監督微調(SFT)/獎勵塑造,詳見後文)在後訓練中完成。

後訓練資料格式

與這些模型聊天需要一些幕後底層連線。每次你在聊天視窗中與 ChatGPT 等服務交談時,都會看到這樣的使用者介面:

實際發生的情況是,後訓練結構已經為你連線好,模型將看到類似以下內容(使用Llama 3的資料格式):

<|begin_of_text|>
<|start_header_id|>system<|end_header_id|>
… <|eot_id|>

<|start_header_id|>user<|end_header_id|>
What is the capital of France?<|eot_id|>

<|start_header_id|>assistant<|end_header_id|>
The capital of France is Paris

<|start_header_id|>user<|end_header_id|>
How many people live there? Tell me just the number<|eot_id|>

<|start_header_id|>assistant<|end_header_id|>
START FILLING FROM HERE

請注意,LLM 的基本介面未改變:你提供一些文字,它將無限地繼續下去。

這種巧妙的底層連線確保模型收到所有元資料,知道之前的說話者已經說過話(它不應該模仿他們!!),並在助手說完話後停止。同樣,模型會愉快地繼續填充,但當我們看到<eot_id>令牌(“回合結束”)時,我們會阻止模型繼續,並將其返回給使用者。

請注意,模型不會做任何這些事情:儘管我們覺得它們很聰明,但它們仍然只是需要這種“手把手指導”的文字填充器。

這種格式有點難讀,但你基本上可以將其概念化為類似以下內容:

<system> You are a helpful assistant bla bla </system>
<user> What's the weather in Paris? </user>
<assistant> ANSWER HERE

趣聞:模型會愉快地扮演任何一個角色!你完全可以扮演助手,讓它扮演使用者,只需向其提供正確的文字結構——模型無論你提供什麼,都會簡單地接管並完成文字。嘗試使用本地模型或 API 進行操作(ChatGPT 等產品會為你處理這些底層連線,你無法覆蓋它們)。顯然,模型對其自己的格式非常敏感,因此請確保使用正確的格式。

後訓練技術

後訓練是一個快速變化的領域,不同的團隊將採用不同的技術。

讓我們看一下OLMo 2論文中描述的這個流程:

以下部分將逐一介紹每個方框。

SFT:有監督微調

SFT 的重點是模仿。其概念很簡單:你教模型一步一步地強制學習一個答案。 

如果這是下棋,你可以透過訓練馬格努斯·卡爾森的棋局來進行 SFT,你將強制教導模型在每一步都遵循馬格努斯的下法。你可以看到 SFT 的侷限性:你可以達到馬格努斯的能力,但他將是你的上限,你無法超越他(與強化學習(RL)不同,強化學習可以不斷嘗試直到你變得非常出色,詳見後文)。

在 LLM 的情況下,你逐字逐句地學習理想答案,因此你的損失函式就是針對輸出層的交叉熵,其中理想的“類別”是“正確”單詞的 ID。我們常被問到的一個問題是:這和預訓練不是一樣的嗎?它確實非常相似,只有一個關鍵區別:你只基於提示進行條件訓練;你不會學習提示。為什麼?因為你想要學習如何回答這個問題,而不是學習問題本身。 

記住:與預訓練不同,在後訓練中,我們有這種包含系統提示和使用者提示的結構(參見上面後訓練資料格式)。這些是輸入中我們希望作為條件但不想從中學習的部分。我們透過饋入整個序列(包括系統提示、使用者提示以及任何特殊字元)而不進行任何掩碼來完成此操作,但當我們計算損失時,我們會掩碼除實際響應之外的每個標記的損失。我們不會掩碼輸入中的提示,因為我們確實希望它對輸入條件產生貢獻。我們在反向傳播步驟中掩碼它,以防止它對損失產生貢獻)。

它們非常相似,以至於在實踐中,SFT 可以利用預訓練已經構建的所有基礎設施:事實上,像 Megatron 這樣的訓練平臺使用與預訓練步驟相同的資料載入器和訓練器類,只需設定一個引數來掩碼提示上的損失。

話雖如此,這些的規模遠不及預訓練——你最多隻能進行幾百萬個樣本的 SFT,所以只有幾 B 個 token,而預訓練會處理數萬億個 token。

誰來寫回復?

SFT 逐字逐句地學習,這在兩個重要方面限制了它:

  1. 你的上限由編寫答案的人決定(見上一段)
  2. 關鍵在於,你嚴重依賴資料質量。實際上,你的上限最終由你最差的答案而不是最好的答案決定。當你從人們那裡獲取答案時,你不能指望所有答案都具有相同的高質量。其中一些不會很出色,它們會對模型質量產生巨大影響。

那麼,我們該怎麼辦?我們無法對抗人性,所以想法是讓 LLM 生成它將用來訓練自己的回答。這乍一看似乎不合常理,但這種想法被稱為拒絕取樣(更多資訊請參見Llama 2 論文)。它之所以有效,是因為我們不只生成一個答案(那樣它就不會自我提升),而是生成許多(通常是 10 個)答案,這些答案來自多個不同的檢查點、隨機種子、系統提示等,以激發多樣性。然後,我們保留最佳答案(由流程排名,通常包括人類偏好獎勵模型等其他模型),並將其新增到資料庫中。如果你有機器學習(ML)背景,你可以將這種方法視為一種從某種程度上講,從某種程度上講,從整合模型中提煉出單個模型的方法(我知道這很籠統!)。

你可以透過多次迭代來迴圈執行此操作。如果操作得當,你可以攀登高峰,變得越來越好。

強化學習入門

強化學習(RL)是一個廣闊的領域,其中包含了著名的人類反饋強化學習(RLHF),但它並不僅限於此。

總的來說,RL 背後的核心思想是你現在是一個代理,可以針對環境採取行動,並觀察發生的事情,並從中獲得獎勵。你可以將這些獎勵視為生成一個“標籤”,然後你可以根據它進行訓練——儘管反向傳播會看起來有所不同(稍後會詳細介紹)。

如何訓練?

反向傳播確實發生在強化學習中,但與我們在監督學習中享受的整潔的前向-後向迴圈有關鍵區別。

關鍵的區別在於,在強化學習中,我們不幸沒有一個可微分的成本函式,例如交叉熵或均方誤差 (MSE)。獎勵(以及瀏覽器等工具)是不可微分的,因此你無法直接透過它們進行反向傳播——這非常不幸。

因此,我們採取的替代方法是一種非常粗糙的近似:我們只是對模型輸出中的對數機率進行操作,如果行動良好,就使其更大,如果不好,就使其更小——然後,我們反向傳播到模型中,調整所有前面的層以實現這一點。請注意,這遠不如最佳化像 MSE 和交叉熵這樣的監督成本函式高效:監督成本函式返回一個密集的梯度向量,而在這裡我們只從整個情節中得到一個標量,這效率低得多,因為每次互動我們獲得的“學習動力”更少。

強化學習過程還有更多的細枝末節,不同的演算法在如何解決主要問題(梯度消失/爆炸、樣本效率、基礎設施最佳化等)上做出了不同的選擇,但這就是大致的要點。附錄詳細地逐步推導了近端策略最佳化(PPO)。

讓我們從基礎設施的角度來看:與“標準”監督學習相比,你需要對模型進行大量的推理,這更昂貴(自迴歸、逐個 token 生成,而不是在單個前向傳遞中饋入整個現有序列),需要更多的基礎設施(KV 快取等),更難以批次處理等等。

儘管訓練目標如此粗糙,但在存在稀疏長期獎勵的情況下,它實際上執行得非常好,而 SFT 需要獎勵是密集的(你會知道每個 token 應該是什麼)。

延續國際象棋的類比

  • SFT 逐教模型複製
  • 在強化學習中,模型在贏得比賽時(可能在 20 步之後)會獲得獎勵。訓練演算法會傾向於那些能更頻繁地獲得更多獎勵的模型配置(給定一些探索/利用的權衡)。

雖然你一開始會弱得多,但最終,透過玩足夠多的遊戲,你可以達到馬格努斯的水平,甚至遠遠超越他——換句話說,你能夠達到的上限要高得多

從某種意義上說,你可以將強化學習視為一臺神奇的機器,它彌合了判斷與行動之間的差距:這是一個非常大的差距!我能認出 F1 賽車手何時失誤,但我卻無法做到即使是最差的 F1 賽車手也能做到的事情。強化學習可以將任何“鍵盤車神”變成真正的賽車手🏎️ 

如果你想用一句話來概括,“如果你能判斷它,你就能學會它!”。

獎勵駭客攻擊

注意一件事:你的上限仍然由你的判斷能力(藍色曲線)決定,而你的判斷能力又由你的獎勵有多好決定:對於遊戲之類的東西,獎勵非常清晰(你知道你何時以 100% 的精確度和召回率獲勝),所以天空才是極限。你確實可以將強化學習應用於任何事物,但如果你判斷得不好,你的代理將只會學習到噪音。

強化學習受限於你的判斷能力的一個推論是,它可能會學到你沒有意圖的行為:強化學習會竭盡所能最大化你給予它的獎勵,因此模型會完全按照你要求的去做,而不是你想要的。我們稱之為獎勵駭客攻擊,即使模型對此不負責,而是我們在設計激勵機制時造成的! 

舉個例子來證明這一點:強化學習發現《超級馬里奧 1》在 30 多年來一直存在一個 bug,即跳躍後轉身,你會有一個幀的無敵時間。鑑於其獎勵是最大化得分,並且如果你更快通關,你的得分會提高,它就會利用這個 bug 來獲得更高的得分(從而獲得更高的獎勵)。

開發者想要什麼

  • 最大化得分
  • 仍然像人類一樣玩
  • 不利用漏洞

開發者要求什麼

  • 最大化得分

注意:這在人類身上也會發生!我們只是稱之為反常激勵,但它們實際上是同一回事。英國政府擔心德里眼鏡蛇數量過多,懸賞每條死去的眼鏡蛇。最初,這是一個成功的策略;大量蛇被殺死以換取獎勵。然而,最終人們開始飼養眼鏡蛇以獲取收入。

LLM 的應用

與國際象棋的例子類似,強化學習可以比 SFT 具有更高的上限,因此它是教導模型如何以我們喜歡的方式與人類對話(RLHF)、如何推理等的首選方法。

正如我們剛剛看到的,你能夠達到的上限取決於你的獎勵有多好:如果你使用分類器來提供獎勵,你的上限將是該分類器的準確性。

RLHF

其中一個分類器是人類偏好。很難為如何以人類喜歡的方式寫作制定規則,因此我們採取的方法是訓練一個分類器來給這些寫作打分。我們透過準確率和 PR AUC 來監控其效能(如果你需要一個連續的分數進行排名,則只需要 PR AUC;否則,像 F1 或準確率這樣的點估計就足夠了)。一旦有了這個分類器,你所需要做的就是針對它執行強化學習並根據其反饋進行最佳化。

DPO:直接偏好最佳化

現在讓我們看看 LLM 後訓練流程中的第二個方框。DPO 是一種專門用於 LLM 的 RLHF 的演算法,它不是一種通用的強化學習演算法(與 PPO 及其同類不同,PPO 可以用於訓練機器人以及任何你想要的東西)。

事實上,嚴格來說,DPO 甚至不是一個真正的強化學習演算法;它只是假裝是!DPO 的核心思想是,如果你做一些合理的假設,你就能在仍然為 RLHF 進行訓練的同時擁有一個可微分的損失函式。

更準確地說,DPO 允許我們在某些假設下(詳見後文)為馬爾可夫決策過程提供一個監督學習解決方案,這是一件大事,因為通常解決 MDP 的唯一通用方法是透過強化學習(及其低效的成本函式)。 

DPO 的核心思想是:與其有一個單獨的獎勵模型,你可以將你的 LLM 回收利用,使其既是你的策略模型又是你的獎勵模型。為什麼?因為你的 LLM 會給出給定問題的一個答案的機率。因此,如果你有一個偏好對,你就可以簡單地說,對於同一個問題,你希望首選答案的機率高,而拒絕答案的機率低。換句話說,你希望最大化,這會匯出一個非常好的可微分函式!

與 PPO 和其他需要透過大量推理取樣多個答案的強化學習演算法相比,DPO 的執行成本極其低廉。缺點是DPO 不進行探索,因此你的表現也有一個上限。下面是對“真實”強化學習演算法(如 PPO,我們將在後面更詳細地介紹)的更詳細比較。

特性 DPO(直接偏好最佳化) PPO(近端策略最佳化)
最佳化 監督學習 強化學習 (RL)
所需資料 (提示,首選,拒絕) 對的固定資料集 Rollouts + 獎勵模型
損失函式 類似二分類的損失 裁剪策略梯度損失
探索 ❌ 否(固定資料集,無探索 - 完全離線) ✅ 是(策略可以探索新的響應 - 線上演算法)
同策略? ❌ 異策略(從固定資料中學習) ✅ 同策略(需要新的 rollouts)
計算成本 ✅ 低(每對只需一次前向傳播) ❌ 高(rollouts + PPO 訓練)
訓練穩定性 ✅ 穩定(類似微調) ❌ 不穩定(RL 方差)
收斂速度 ✅ 快 ❌ 慢(需要多次 rollouts)
效能受限於 ❌ 資料 ✅ 計算(更好的選擇)
最適用於 利用人類偏好進行廉價對齊 更靈活但昂貴的微調

線上強化學習

我們 LLM 後訓練流程中的第三個也是最後一個方框是線上強化學習。“標準”演算法是 PPO(近端策略梯度),由 OpenAI 於 2017 年建立。另一個被廣泛採用的演算法是GRPO(Group Relative Policy Optimization,由DeepSeek引入)。

關鍵概念:同策略與異策略

一個策略就是你正在訓練的 LLM。同策略意味著與環境的每次互動都直接來自正在訓練的模型。這最有意義,因為我們就是這樣向私人導師學習的:我們嘗試一些東西,犯一些錯誤,立即得到反饋,然後帶著這些知識再次嘗試。這比異策略學習要好得多,異策略學習是向你展示別人在這種情況下做了什麼,你可以從中學習——這個“別人”可以是(而且通常是)你過去的版本,所以通常會保留你過去所做的記憶在一個回放緩衝區中以備後用。

強化學習演算法屬於這兩個家族中的一個,PPO 屬於同策略一方,而 Q 學習(如 DQN)則屬於異策略陣營。

特性 同策略強化學習(例如 PPO) 異策略強化學習(例如 DQN、DPO)
定義 當前策略收集的資料中學習 先前收集的資料(即使來自舊策略)中學習
探索 ✅ 是(持續生成新的 rollouts,探索-利用權衡已經內建到策略網路的 logits 中——自然地開始大量探索,並逐漸轉向利用) ❌ 當離線執行時,你永遠不會探索,因為你只是重複使用你之前生成的舊資料(例如 DPO)。

你可以線上執行並探索,但探索策略由你定義(例如 epsilon-greedy)

基礎設施效率 ❌ 低(需要始終線上生成資料) ✅ 高(重用過去的生成)
訓練穩定性 ❌ 不穩定(策略不斷變化) ✅ 更穩定(固定資料集或重放緩衝區)
計算成本 ❌ 高(需要頻繁的 rollout)

成本主要由於訓練迴圈的同步性質(收集 → 訓練 → 收集 → 訓練)

✅ 低(在儲存資料上訓練)
演算法示例 PPO、A2C、TRPO DQN、DDPG、SAC、DPO
最適用於 需要持續探索的情況 當你能夠儲存和重用過去的經驗時

從基礎設施角度看:線上 vs 離線強化學習

同策略與異策略是從模型訓練動態的角度來看待事物。如果從基礎設施的角度來看待事物,我們應該考慮離線(使用靜態資料,我們在訓練時簡單地重新載入)與線上(我們即時生成資料)。這兩個概念與異策略和同策略很好地對應,因此有時可以互換使用,儘管嚴格來說它們仍然有點不同:

  • 如果你是離線學習,你只能進行異策略學習(因為資料是由另一個模型生成並儲存的)。拒絕取樣和 DPO 是異策略的離線演算法。這對於基礎設施來說是最簡單的事情。
  • 如果你是線上學習,那麼一旦你開始考慮使用多臺機器和同步,同策略還是異策略實際上更像是一個範圍。如果你想嚴格同策略,這意味著你以批次大小 = 1 進行訓練,然後從該模型中取樣,並在整個過程中引入障礙,以便模型不斷更新,並且在我們將所有權重重新分散到所有節點之前,不會取樣新的軌跡。

在程式碼中

# Idealized PPO training loop

collector = CollectorClass(model)

for i in range(num_collection):
    collector.sync_weights_() # align weights across all workers
    # resume collection and put trainer node on hold <- this is bad!
    data = next(collector) # collect data
    # Put collector nodes on hold <- this is bad!
    for j in range(num_epochs):
        for batch in split_data_randomly(data):
            loss_val = loss_fn(data)
            loss_val.backward()
            optim.step()
            optim.zero_grad()

因此,某種程度的“異策略性”是可取和可接受的。這伴隨著許多問題:這種情況在多大程度上成立?你應該多久更新一次收集權重?如何才能重疊權重同步、收集過程和模型訓練以最大化吞吐量?

我不希望你帶著異策略和離線演算法必然是糟糕且在所有情況下都應避免的想法離開本節。事實上,Llama 管道的前兩個部分(SFT 和 DPO)本質上是透過監督成本函式解決對齊馬爾可夫決策過程的一種方式。這些作為“某種”離線、異策略強化學習:

  1. 我們的 SFT 資料來自拒絕取樣,這意味著我們用模型本身生成它。雖然我們不使用真正的異策略 RL 演算法,如 DQN,但以這種方式進行的拒絕取樣是一種離線策略最佳化形式。
  2. 同樣,我們已經看到 DPO 也是一種離線策略最佳化形式,它也避免了進行實際的強化學習梯度更新,而這些更新是緩慢且不穩定的。

不同的團隊在如何利用所有這些技術以及如何組合它們方面找到了不同的方法,但並非所有團隊都公佈了這些資訊。

超越 RLHF:一種通用正規化

沒有什麼能限制我們只停留在人類反饋上——事實上,我們也沒有。如果你想學習如何寫好程式碼,你可以提供一個測試工具,並根據透過的測試數量給予相應的獎勵。如果你想學習求解積分,你可以使用 Wolfram 來檢查你的方程是否正確。

簡而言之,你可以透過混合 Software 1.0 和 Software 2.0 來構建獎勵管道。常見的模式是:

1. 獎勵模型。 一個分類器,給出從 0 到 1 的連續分數(有時甚至是無界的)。適用於對許多答案進行排序(只需按分數排序),尤其是在你無法輕易表達所需內容(人類偏好、寫作風格等)的領域。

a. 結果獎勵模型。 ORM 根據思想鏈的最終結果提供反饋,且僅根據最終結果。這些是最常見的——事實上,如果有人只說“獎勵模型”,指的就是這些。

b. 過程獎勵模型。 在上面的例子(國際象棋)中,獎勵是稀疏的:它只在遊戲獲勝或失敗時發生。直觀地說,擁有密集獎勵可以幫助代理獲得關於哪些行為是或不是可取的更精細的知識。這就是 PRM 所做的:它們不僅判斷最終輸出,還判斷整個步驟序列。例如,PRM 將判斷整個推理鏈條,並確保每個步驟都合理。這些並不常用(至少目前還沒有),因為它們往往非常嘈雜。

2. 基於規則的獎勵。你可以寫下一系列規則,併為每個規則(或整體遵守這些規則)設定獎勵。

a. 軟體管道。這些執行“普通”軟體 1.0,並根據其結果給你獎勵。例如,透過程式設計的測試用例或透過 linting。

b. 判官。當你無法透過傳統軟體管道檢查規則是否被遵守時,你可以使用 LLM 來為你驗證規則是否被遵守。例如,你可以寫下一組不得違反的安全規則,例如:“不得提及性相關內容”,判官可以問“所有規則都遵守了嗎?”。注意:這與連續評分獎勵模型不同,因為你只得到一個二元答案(規則遵守或不遵守)。你無法在此基礎上對許多答案進行排名,因此這被用於在某些規則被違反時提供負面獎勵。在實踐中,你通常甚至不會訓練這些模型,而是簡單地提示一個基礎模型為你進行判斷。

這些是構成非常複雜的獎勵塑造管道的組成部分。例如,你可以想象擁有一個由這些管道組成的複雜有向無環圖(DAG),以提供非常精細的獎勵。 

編碼的簡單示例:

這有基礎設施方面的影響

  • 測試工具和其他需要執行的二進位制檔案的沙盒
  • 我們把所有這些獎勵模型部署在哪裡?你可以有很多,而且它們可能很大(不要把它們想象成小的專用分類器!它們通常和你要訓練的模型一樣大!)。
  • 我們應該期待越來越多的工程師開發越來越好的獎勵管道(更精細,以塑造模型的行為)。這可以成為工程師貢獻的主要場所。這些管道本身就可以成為一個有用的 Hub,並可以眾包開發。

測試時計算和推理

測試時推理是過去一年由OpenAI提出的一個主要趨勢,DeepSeek 在其DeepSeek R1 論文中也成功復現。 

讓我們深入瞭解這是什麼。

簡而言之,這建立在之前的工作(如思維鏈ReAct迴圈)之上,這些工作發現讓模型在給出答案之前“自言自語”可以大大提高其答案的質量,特別是在數學等特定領域。這種能力是自發產生的,從未為此訓練過 LLM。因此,自然而然的下一步就是找出一種訓練 LLM 的方法,使其更好地進行這種“思考步驟”。 

我們不知道 OpenAI 採用了什麼技術,但 DeepSeek R1 論文最令人驚訝的發現是,你不需要一個超級巧妙的設定來誘導這種學習。事實上,他們表明,只需為模型提供思考空間(透過簡單地指示它在<think>標記和</think>標記之間填充文字,並且該文字不為空)。

這是他們使用的系統提示

一旦這些就緒,他們就進入了獎勵建模。

論文摘錄註釋

我強烈建議閱讀完整的DeepSeek R1 論文,因為它寫得非常好。在這裡,我們引用了論文的幾個部分,並附上我的評論,以便為非該領域專業讀者提供背景資訊。

第 2.2.2 節:獎勵建模

獎勵是訓練訊號的來源,它決定了強化學習的最佳化方向。為了訓練 DeepSeek-R1-Zero,我們採用了一種基於規則的獎勵系統,主要包括兩種獎勵:

  • 準確性獎勵:準確性獎勵模型評估響應是否正確。例如,對於具有確定性結果的數學問題,模型需要以指定格式(例如,在方框內)提供最終答案,從而能夠可靠地進行基於規則的正確性驗證。同樣,對於 LeetCode 問題,可以使用編譯器根據預定義的測試用例生成反饋。

評論:這都是我們已經見過的相當標準的東西。概念上很簡單。在實踐中,這需要機器學習工程師的技藝,才能製作出好的獎勵,這些獎勵既沒有噪音,又能推動強化學習過程朝著你想要的方向發展。

  • 格式獎勵:除了準確性獎勵模型,我們還採用了一個格式獎勵模型,強制模型將其思考過程放在“<think>”和“</think>”標籤之間。

評論:聰明!!!這是一種促使模型開始利用思考過程的簡單方法。否則,它可能不會始終如一地探索在這些<think>和</think>標籤之間新增思考。當它不這樣做時,施加強烈的負面獎勵可以使其端正,並將探索限制在使用這些標籤上。他們到 R1-Zero 就停止了,但實際上你可以繼續進行更復雜的獎勵建模。例如,R1-Zero 有時會混合使用英語和中文思考,所以你可以透過新增一個提示(然後是獎勵)來解決這個問題,讓它用英語思考。如果思考過程被判斷為在某種程度上“不好”(不一致等),你還可以新增中間負面獎勵,以進一步引導模型。你可以看到這是一個通用正規化……

  • 我們在開發 DeepSeek-R1-Zero 時不使用結果或過程神經獎勵模型,因為我們發現神經獎勵模型可能在大規模強化學習過程中遭受獎勵駭客攻擊,並且重新訓練獎勵模型需要額外的訓練資源,這使整個訓練流程複雜化。

評論:也許社群中有人最終會找出如何讓過程獎勵模型起作用的方法……

其他觀察

  • “啊哈”時刻

這對我來說非常有趣,因為強化學習本質上是自己學習回溯。這非常酷,老實說我沒想到會發生這種事。我想你可以透過搜尋來改進這一點:波束搜尋是最簡單的,從那裡你可以進入蒙特卡洛樹搜尋(MCTS)等樹搜尋演算法。既然我們有了一個有效的基礎,我認為我們將在所有這些更復雜的方法上看到更快的進展。在我看來,這是這項工作的一個被嚴重誤解的部分:DeepSeek 並沒有表明你不需要所有這些人工智慧計算,恰恰相反!他們只是表明你不需要複雜的方法來開始,簡單的規模方法就足夠了——這是我們在人工智慧中不斷學習的教訓。

自行增加思考時間(和測試時計算)

由於他們從未給模型任何思考時間過長的懲罰,強化學習發現思考更長時間沒有任何壞處,並且隨著每次迭代的進行,它只是不斷朝著更長的思考軌跡方向發展。這是意料之中的。我想象,如果他們繼續下去,模型也會自動學習在其最大序列長度處停止,因為那樣它就無法記住其整個推理軌跡,所以繼續下去將無濟於事(甚至可能有害)。

模型再次學習到測試時計算是好的,因此我們應該預期計算需求會上升。

附錄 A:深入瞭解 PPO

讓我們更深入地瞭解強化學習中的反向傳播。

讓我們從監督機器學習(無強化學習)中的“基本訓練迴圈”開始

loss_fn = nn.CrossEntropyLoss()
for batch in dataset:
        x, y = batch
        y_hat = model(x)  # One forward pass only
        loss = loss_fn(y, y_hat)
        loss.backward()  # One backward pass

回想一下,在強化學習中,我們沒有一個明確定義的成本函式,所以我們只是讓一個行動如果好就更有可能,如果壞就更不可能。我們如何做到這一點?

我們的模型已經輸出一個關於動作的機率分佈,所以最後一層將對所有動作進行 Softmax。我們要做的是對好的動作給予正梯度,對壞的動作給予負梯度。

所以,基本上,我們想做這樣的事情

model.weights.grad[good_actions] += delta
model.weights.grad[bad_actions] -= delta

自動梯度可以為我們做到這一點:為了獲得新增到梯度中的恆定增量,我們需要一個當被微分時能給我們這個增量的函式。答案是乘法。所以,我們的“成本函式”簡單地是log_probs * per_token_reward

讓我們保持高層次並逐步開發(否則 PPO 損失看起來相當可怕)

for batch in dataloader:  # Iterate over dataset (prompts)
    prompts = batch        # Get input prompts: (bsz, prompt_lens). Can be ragged, or packed.
    responses, log_probs = model.generate(prompts)  # Autoregressive 
generation! MANY forward calls, need KV cache etc. If this is not clear to
you, read this appendix.

# Also note: we NEED to return log_probs for all the intermediate
generations and note that we are not detaching them from the graph as we
are gonna need them later.

   # responses and log_probs are of size (bsz, response_lens). 
Also ragged/packed. 

    sequence_rewards = get_feedback(responses)  # Get rewards (e.g., human
preference or heuristic).

    # Note that rewards CAN be negative, in that case this sign will be
negative.

    # This is a tensor of size (bsz,). 

    per_token_reward = discount(sequence_rewards)  # This one is (bsz,
response_lens). A simple way to discount is to multiply tokens by a factor
gamma (eg 0.99).  So the last token gets reward of 1, the second-to-last
gets 1*gamma, then the previous gets 1*gamma*gamma and so on. You don't
want to maximize only your reward at time t but the sum of rewards till
the end of the episode 

    optimizer.zero_grad()  # Reset gradients

    # Manually nudge log-probs based on reward signal

    adjusted_log_probs = log_probs * per_token_reward # this is a
stochastic estimator for the gradient of the reward expectation given your
stochastic policy - in other words: on average, the gradient of that thing
points to where the policy is doing good!

    loss = -adjusted_log_probs.sum()  # Equivalent to maximizing
probability of good actions

    loss.backward()  # Still ONE backward call! PyTorch knows what to do.

    optimizer.step()  # Update model parameters

請注意,儘管我們進行了多次前向傳播(由於自迴歸生成),但我們只需要一次反向傳播(好吧,假設我們保留了圖。如果你使用 VLLM 或其他方式完成這些生成,那麼我們將需要在這裡多進行一次前向傳播來具現化這邊的圖,這應該不會太糟糕……)。

現在讓這更真實

以上是我們概念上所做的一切。但當你嘗試它時,一切都會發散 😀 

讓我們進行這些更改

  1. 自適應增量。使用靜態增量並非最優,因為更新的幅度應與選擇的優劣成比例。與其手動調整對數機率,我們可以讓反向傳播為我們完成工作。如果你想增加機率,你可以簡單地最大化對數機率。為了在梯度下降中做到這一點,我們最小化負對數機率。這就是策略梯度損失函式:
policy_gradient_loss = - (rewards * log_probs).sum(dim=-1).mean()

policy_gradient_loss.backward()
  1. 減少方差。如果我們僅用以上方法進行訓練,一些響應會獲得巨大獎勵,而另一些則會獲得獎勵,導致訓練不穩定。一種緩解方法是引入一個基線,即平均移動的預期累積獎勵:不太糟,但也不太好。這背後的直覺是,一個移動的好壞總是取決於可用的替代方案:例如,獲得 100 萬美元似乎很棒,直到你意識到你有機會獲得 1 億美元。 

這種相對於基線的移動的“優良性”被稱為優勢,這是強化學習中的一個核心概念。

我們如何預測基線分數(也稱為狀態的價值)?有兩種方法:

  1. 價值網路。訓練一個模型為你做這件事:你可以訓練一個價值網路來估計這個基線應該是什麼。
  2. 蒙特卡洛。簡單地執行一批生成(通常 4-5 次就足夠了),所有生成的平均累積獎勵就是你對價值的估計。

現在我們的程式碼會是這樣的

for batch in dataloader:
    prompts = batch
    responses, log_probs = model.generate(prompts)

    rewards = discount(get_feedback(responses))  # Already discounted

    for n in range(epochs):
        for _prompts, _log_probs, _rewards in make_minibatches(
            prompts,
            log_probs,
            rewards,
            ):
            values = value_network(_prompts)  # Predict baseline V(s)
            optimizer.zero_grad()
            # REINFORCE with baseline
            advantages = _rewards - values  # Compute advantage estimate
            loss = - (advantages * _log_probs).sum(dim=-1).mean()  
            loss += advantages.pow(2).mean()
            loss.backward()
            optimizer.step()

信不信由你,那仍然不穩定!強化學習就是極度不穩定且難以捉摸(儘管與其它領域相比,它對 LLM 來說表現出人意料地好)。一個原因是獎勵的分佈可能有一個很長的尾部,也就是說,你的梯度行為不佳。因此,在實踐中,我們確實需要確保更新受到良好約束,以便訓練表現良好。

PPO 基本上透過強制執行四個不同的約束來確保穩定性

1. 標準化優勢

上面的程式碼乘以原始優勢,儘管我們盡力進行基線化,但仍然可能導致巨大的更新,從而破壞訓練的穩定性。一種使其行為良好的方法是簡單地將其平移到零均值並縮放到單位方差。

advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)

2. 重要性取樣

這仍然不夠。為了進一步改進,我們將強制更新保持在信任區域內。PPO 透過防止更新相對於舊策略過大來做到這一點(所以是的,它需要保留 t-1 時刻的模型):

所以現在我們這樣做

old_log_probs = get_old_log_probs(prompts, responses).detach()  # Log
probs from previous policy

importance_sampling_ratio = torch.exp(log_probs - old_log_probs)

safe_advantages = importance_sampling_ratio * advantages

3. 裁剪大的更新

書中最古老的技巧。如果你面臨梯度過大的風險,只需裁剪它。

old_log_probs = get_old_log_probs(prompts, responses).detach()  # Log
probs from previous policy

importance_sampling_ratio = torch.exp(log_probs - old_log_probs)

clipped_sampling_ratio = torch.clamp(ratio, 1 - epsilon, 1 + epsilon)

even_safer_advantages = clipped_sampling_ratio * advantages

4. 取最小值()

這對我來說是最令人驚訝的一點。與其僅僅使用剪裁過的優勢,你實際上想要執行一個剪裁過的版本和未剪裁版本之間的min。原因很微妙:強化學習會試圖最大化其獎勵,如果你只提供剪裁過的獎勵,它會將獎勵推高到儘可能接近剪裁閾值,這仍然會破壞整個過程的穩定性(更多細節此處)。

old_log_probs = get_old_log_probs(prompts, responses).detach()  # Log
probs from previous policy
importance_sampling_ratio = torch.exp(log_probs - old_log_probs)

clipped_sampling_ratio = torch.clamp(ratio, 1 - epsilon, 1 + epsilon)

safe_advantages_final_final = torch.min(ratio * advantages,
clipped_sampling_ratio * advantages)

把它們組合在一起

epsilon = 0.2  # Clipping threshold

for batch in dataloader:
    prompts = batch
    # prev_log_probs are part of the loss - non-differentiable
    with torch.no_grad():
        responses, prev_log_probs = model.generate(prompts)

    rewards = discount(get_feedback(responses))  
    values = value_network(prompts)  

    advantages = rewards - values
    advantages = (advantages - advantages.mean()) / (advantages.std() + 
1e-8)  

    # ref_log_probs are used for regularization
    with torch.no_grad():
        ref_log_probs = get_old_log_probs(prompts, responses).detach()  

    for n in range(epochs):
        for batch in make_minibatches(...):
            log_probs = model(batch.prompts)[1]
            ratio = torch.exp(log_probs - batch.prev_log_probs) # 
Importance ratio
            clipped_ratio = torch.clamp(ratio, 1 - epsilon, 1 + epsilon)

            # The min() function prevents the model from "gaming" the 
clipped update
            loss = -torch.min(ratio * batch.advantages, clipped_ratio * 
batch.advantages).mean()
            # + add value_network loss, ref model regularization 
and entropy boost...

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

如果現在你看到它寫成一個方程式,希望它看起來不那麼可怕了!

在 PPO 中,還有兩個額外的損失。

在訓練策略網路的同時,價值損失用於協同訓練價值網路。簡單來說,你可以使用在各種生成中獲得的實際收益來持續訓練價值網路,所以這只是它們之間的 MSE 損失:

最後,作為又一道防護措施,我們還將阻止強化學習過多地改變模型的權重:畢竟,在預訓練中花費數百萬美元的計算來教導模型關於世界的資訊,我們不希望強化學習偏離這些太多。 

一個簡單的方法是簡單地新增一個 KL 散度損失項。

最終的 PPO 損失是這個

其中 c1 和 c2 是你透過實驗設定的超引數,用於平衡這些項(通常它們很小)。

DeepSeek 的 GRPO

現在你已經掌握了所有能夠開啟論文閱讀可怕公式的要素。這是 DeepSeek 的 GRPO 的公式(取自他們的論文)

基於蒙特卡洛的優勢估計

GRPO 的“創新”(對獎勵進行蒙特卡洛取樣)實際上是一個老技巧——人們在擁有價值網路之前就這麼做了!價值網路應該更穩定,但你可以想象,也昂貴得多。社群中關於如何處理這個問題有一些討論,批評網路不會很快從灰燼中崛起,這並非板上釘釘!

執行這個東西有多貴?

總結一下,在最壞的情況下,我們必須按順序執行以下模型(我只展示了精簡的網路操作)

1. 執行推理模型,獲取 token 和log-probs推理

1 次前向傳播

2. 給定生成的 token 執行參考模型,獲取 log-probs參考

1 次前向傳播

3. 給定生成的 token 執行獎勵模型,獲取獎勵

1 次前向傳播

4. (執行批評者以獲取價值 -> 優勢估計)

1 次前向傳播

5. 給定一批 token 執行圖內模型副本,獲取log-probs訓練

6. 計算
lp0 = f(log-probs訓練, log-probs推理, 優勢) + c1 L(log-probs訓練, log-probs參考)

7. 反向傳播 lp0,對模型權重進行 adam 步進

8. 計算
lp1 = c2 L(價值, 優勢

9. 反向傳播 lp1,對評論者網路權重進行 adam 步進

去除批評者(如 GRPO)可以消除第 4 步(1 次前向傳播)和第 8-9 步(1 次反向傳播 + 最佳化器步進)的成本,從而節省大量的記憶體和計算。

附錄 B:為什麼生成比處理提示更昂貴

如果你檢視 LLM 雲服務提供商的價格,你會注意到他們對輸出 token的收費總是比對輸入 token的收費高得多,例如:GPT5費用為 100 萬輸入 token 1.25 美元,但 100 萬輸出 token 10.00 美元。為什麼會這樣?

原因是自迴歸生成比處理已經寫好的文字昂貴得多!這是由於 Transformer 的工作方式。它們總是需要消耗整個序列,因此要處理一段文字,你只需要一次前向傳播。要生成新文字,你生成的每個 token 都需要執行一次前向傳播。然後,你獲取帶有剛剛生成的新單詞的序列,再次將其輸入以獲取下一個單詞,依此類推。這非常昂貴,並透過 KV Cache 得到緩解。

請注意,這與 RNN(如 LSTM)不同,在 RNN 中,如果你有一個長度為 L 的序列,並且你想處理一個額外的 token,那麼前向傳播將只攝取該單個 token 並重用它擁有的狀態。與 LSTM 不同,Transformer 是無狀態的,因此你需要攝取整個序列,並再進行一次 L+1 個 token 的前向傳播!

幸運的是,大部分計算可以回收,因此 KV 快取將大大緩解這個問題(否則,我們老實說將無法在生產中提供這些模型),但它仍然是一個問題。

舉個例子。

你有一個提示

<system>You are a nice LLM, be kind</system>

然後使用者寫道

<user>What's the capital of France?</user>

因此模型將收到此輸入以開始生成

"<system>You are a nice LLM, be kind</system><user>What's the capital of France?</user><agent>"

所有上述內容都可以透過一次前向傳播進行處理,因為所有 token 都已存在。

現在,你生成一個 token

The

要生成下一個 token,你需要再次對整個序列進行另一次前向傳播!!!現在,模型需要攝入

"<system>You are a nice LLM, be kind</system><user>What's the capital of France?</user><agent>The"

它將生成 capital(它實際上會生成一個空格,但是……你知道……讓我們加快速度)。現在,再次

"<system>You are a nice LLM, be kind</system><user>What's the capital of France?</user><agent>The capital"=

你可以看到這有多昂貴……