Skip to content

CS234 作業三:Hopper 上的 reward engineering、RLHF、DPO 與 best arm identification

2026年9月30日1 分鐘
TL;DRCS234 Winter 2026 作業三共五題、94 分。前三題都在 MuJoCo Hopper 上:先用手寫 reward 跑 PPO(13),再從 1 萬筆偏好對學 reward model 後跑 PPO(19+8),最後用 SFT+DPO 直接從偏好學策略、完全不碰環境(6+19)。第四題換成純理論:用 Hoeffding 與 union bound 算出找 ε-最佳臂要幾次試驗(25),這題要等讀完下一篇 bandit 再做。第五題是新聞推薦的 stated vs revealed preference(4)。

🌏 English version

版本說明:本文依據 CS234 Winter 2026 的作業與投影片;公開錄影是 Spring 2024 版。所有事實都在 2026-09-30 打開 作業頁、A3 題目 PDF(8 頁)與作業頁連到的 Google Drive 起始碼(下載後解壓逐檔看過)核對。存取等級 A3(定義見 全球 AI/CS 課程地圖):題目、LaTeX 範本、起始碼與偏好資料都公開;拿不到的是 Gradescope 自動評分與官方解答。

系列位置:上一篇 從人類偏好學:Bradley-Terry、RLHF pipeline、DPO|下一篇 資料效率 I:bandit、regret、UCB|系列總覽

上一篇講完 L8:Bradley-Terry 怎麼把「A 比 B 好」變成 reward model,以及 DPO 怎麼跳過 reward model、直接用偏好更新策略。作業三把這兩條路放到同一個機器人任務上比:MuJoCo 的 Hopper,一隻要學會往前跳的單腳機器人。

題目 PDF 的開場講得很直白:RLHF 是讓 ChatGPT 表現出色的關鍵工具之一,但這個概念更早就出現在 Christiano et al.〈Deep reinforcement learning from human preferences〉。作業要你在機器人任務上親手做一次 RLHF,再跟 DPO 與 supervised learning(behavior cloning)比較。

本文只講題目結構、配分、起始碼長什麼樣,以及做題前該知道的事。不提供任何題目的解答。

先說一個順序問題:第四題 best arm identification 用的是 bandit 的概念,課堂上要到 L9 才教,本系列放在下一篇 order 13。建議 Q1–Q3、Q5 先做,Q4 等讀完下一篇再回來。

時程、繳交與配分

依 2026 課表,作業三在 Week 5(Feb 2–8,期中考那週)發下,Week 7 的 2 月 20 日下午 6 點截止,最多可用 2 個 late day。題目 PDF 抬頭寫的是「Feb 20, 2025」,跟 Winter 2026 的課表年份對不上,應該是沿用舊版抬頭;日期以作業頁為準。

成績比重分兩套:校內生作業三佔 7%,校外(沒有 tutorials)佔 15%。

Gradescope 上要交三樣東西:

  1. 寫作部分的 PDF
  2. LaTeX 的 zip,解開後直接是 main.tex 與 img/,裡面要有 hopper.png、hopper_rlhf.png、hopper_dpo.png 三張圖
  3. 程式的 zip,用起始碼裡的 collect_submission.sh 產生,只打包 run_dpo.py 與 run_rlhf.py
題目主題配分
Q1Reward engineering13(writeup)
Q2Learning from preferences19(writeup)+8(coding)
Q3Direct preference optimization6(writeup)+19(coding)
Q4Best Arm Identification in Multi-armed Bandit25(writeup)
Q5Stated vs. Revealed Preferences4(writeup)

合計 94 分。寫作佔 67 分,程式只佔 27 分;但程式要跑的時間遠比寫作長,下一節會講。

起始碼與資料:先看清楚手上有什麼

作業頁的「Starter code can be downloaded here」連到一個公開的 Google Drive 檔案。解壓後是 starter_code/ 資料夾,檔案日期是 2026-02-13,共 15 個項目:

檔案用途
ppo_hopper.pyQ1:用 stable-baselines3 的 PPO 在 Hopper-v4 上訓練,--early-termination 切換是否在不健康狀態結束 episode
run_rlhf.pyQ2:RewardModel 類別有四處 TODO(建網路、forward、compute_reward、update),其餘流程已寫好
run_dpo.pyQ3:ActionSequenceModel、SFT.update、DPO.update 是 TODO
data.py讀偏好資料、只保留嚴格偏好(去掉平手)的選項
render.py、plot.py、util.py算圖、畫學習曲線、共用工具
data/prefs-hopper.npz主要偏好資料,約 100 MB
data/long-prefs-hopper.npz給你用眼睛看的長片段偏好資料
data/pretrain.ptQ3 的預訓練 SFT 權重

兩份偏好資料的形狀,我用 numpy 打開看過:

檔案偏好對數每段步數觀測維度動作維度
prefs-hopper.npz10,00050113
long-prefs-hopper.npz10200113

每筆資料有 obs_1、obs_2、action_1、action_2 與 label。題目說明標籤的意思:0 代表第一段比較好,1 代表第二段,0.5 代表兩段一樣。

環境設定照 README 用 uv 建 Python 3.10.6 的虛擬環境。requirements.txt 釘了三個關鍵版本:gymnasium[mujoco]==0.29.1、stable-baselines3==2.3.0、torch==2.6.0。不要自己升版,Hopper-v4 這個環境名稱跟 gymnasium 版本綁在一起。

有一個缺口:題目 Q3 的註腳說課程準備了一份 notebook 示範 torch.distributions.Independent 的行為,但 Drive 上的 zip 裡沒有任何 .ipynb。校外讀者得自己查 PyTorch 文件。

Q1:手寫 reward 跑 PPO

這題在問一件事:你在作業二直接拿到 reward function,那個 reward 是誰、怎麼寫出來的?寫 reward 的過程叫 reward engineering,而它很難。

寫作題(a)–(c):

  • (a)為什麼 reward engineering 通常很難?寫錯 reward 有什麼風險?舉一個看起來合理、卻會帶來意外後果的 reward。
  • (b)讀 Hopper 環境說明,用自己的話講目標,以及 reward 的每一項怎麼推動代理人達成目標。
  • (c)預設情況下,代理人離開「healthy」狀態集合時 episode 就結束。healthy 是什麼意思?這種 early termination 有什麼優點與缺點?

(a)跟 作業一 Q2 的 reward hacking 是同一個問題換個場景:從交通匯流換到機器人。

程式題(d)–(f):

  • (d)用 ppo_hopper.py 跑 3 個 seed,有與沒有 early termination 各一組,共 6 次訓練。題目警告每個 seed 最多要跑 90 分鐘,要早點開始。畫出兩組的 episodic return 曲線,比較訓練 epoch 與 wall time,並評論平均 return 的 standard error 高不高、怎樣才能更準地估出 PPO 在 Hopper 上的平均表現。
  • (e)挑一個訓練好的策略算出評估影片:代理人完成任務了嗎?做法是你預期的,還是出乎意料?
  • (f)再算另一個策略的影片,比較兩者,你偏好哪一個?

起始碼的預設是 100 萬步,每 1 萬步評估一次、每次 10 個 episode。6 次訓練照最壞情況算是 9 小時;如果只有一台筆電,這一題就決定了整份作業的時程。

(e)(f)看起來像湊字數的觀察題,其實是在為 Q2 鋪路:你剛剛用眼睛比較了兩段 rollout 並說出偏好,這正是 RLHF 要人類標註者做的事。

Q2:從偏好學 reward model

這一題照 Christiano et al. 的框架:不手寫 reward,改從「兩段軌跡哪一段比較好」的標籤學出 reward,再拿它跑 PPO。

題目先定義偏好:若一段軌跡每一步的 reward 加總比另一段大,它就比較好。再用 Bradley-Terry 模型把「第一段比較好」的機率寫成兩段 reward 總和的 softmax:

P̂[σ¹ ≻ σ²] = exp(Σ r̂(o¹ₜ, a¹ₜ)) / ( exp(Σ r̂(o¹ₜ, a¹ₜ)) + exp(Σ r̂(o²ₜ, a²ₜ)) )

L(r̂) = − Σ over (σ¹, σ², μ) in D of [ μ log P̂[σ¹ ≻ σ²] + (1 − μ) log(1 − P̂[σ¹ ≻ σ²]) ]

也就是說,學 reward 變成一個分類問題:用交叉熵去擬合人類標籤 μ。

寫作題(a)(b):把 r̂ 用神經網路參數化成 r̂_θ,推導 log P̂ 與損失 L 對 θ 的梯度,寫成每一步 ∇_θ r̂_θ 的函數。題目給了提示:把 P̂ 改寫成 sigmoid(z_θ)。

程式與觀察題(c)–(g):

  • (c)用 render.py --dataset data/long-prefs-hopper.npz --idx IDX 看 5 組長片段。每段 200 步,算出來是 8 秒的影片。記下資料集標了哪一段比較好,問自己同不同意;估計你跟標註者的一致率,並判斷你敢不敢信任從這份資料學出來的 reward。
  • (d)實作 run_rlhf.py 裡的 RewardModel。
  • (e)用學到的 reward 跑 PPO,3 個 seed,畫出原始 reward 與學到的 reward下的平均 return,問兩者是否相關。
  • (f)偏好對夠多、而且真的照 Bradley-Terry 生成時,能不能還原出原本的 reward function?
  • (g)算出訓練後的行為影片,跟 Q1 用真實 reward 訓練的策略比,也跟資料集裡看到的示範比。

讀起始碼可以看出幾個設計:reward model 預設訓練 10 萬步、batch 64;輸出範圍預設夾在 0 到 1 之間;訓練好之後用 CustomRewardEnv 把 Hopper 的 reward 換掉,再交給同一個 stable-baselines3 PPO。評估時 callback 同時記錄 original_returns 與 learned_returns,(e)的兩條曲線就是從這裡來。

題目的註腳還點出一個近年的研究方向:人類對部分軌跡給的成對回饋,可能更貼近 regret,學出來的東西比較像 advantage function 而不是 reward,見 Knox et al. AAAI 2024。(f)想得深一點,會碰到同一個問題。

Q3:DPO,不學 reward model

Q2 的路線是「偏好 → reward model → PPO」。Q3 換一條路:手上有預訓練模型與偏好資料時,直接用偏好更新策略,完全跳過 reward model。題目給的 DPO 損失是:

L_DPO(π_θ; π_ref) = − E_(x, y_w, y_l) ~ D [ log σ( β log(π_θ(y_w|x) / π_ref(y_w|x)) − β log(π_θ(y_l|x) / π_ref(y_l|x)) ) ]

x 是情境(狀態),y_w 是被偏好的動作(LLM 裡就是回應),y_l 是另一個。

問題在於 DPO 原本是為 bandit 設計的:一個情境、一次回應。Hopper 是多步控制。題目的處理方式值得先想清楚:

  • 給定一個觀測,模型輸出接下來一段動作序列的分布,而不是單一動作。註腳解釋了原因:50 個動作只有 2 秒影片,要人類比較單一動作的效果幾乎不可能。
  • 如果動作序列跟整個 horizon 一樣長,就是 open-loop control;但它對擾動與累積誤差沒有反應。
  • 所以作業用 receding horizon control(也叫 MPC):每一步算出一段計畫,只執行第一個動作,下一步重算。

題目也提到,Contrastive Preference Learning(CPL)是直接處理多步偏好學習的後續方法,而且 CPL 證明了 DPO 是它在 bandit 設定下的特例。作業選 DPO,是因為它在 LLM 訓練裡用得廣,設定又夠簡單,能看出 RLHF 與直接從偏好學策略的差別。

程式題(a)–(c),只改 run_dpo.py:

  • (a)實作 ActionSequenceModel:每個動作用一個多變量常態分布,平均與標準差由神經網路預測。
  • (b)實作 SFT.update:在偏好資料上最大化被偏好動作的 log 機率,也就是 behavior cloning。
  • (c)實作 DPO.update:最小化上面的 DPO 損失。

實驗與寫作題(d)(e):SFT 與 DPO 各跑 3 個 seed,畫 return 曲線,跟 Q2 的 RLHF 比,評論兩種方法在這個例子上的優缺點;再挑最好的 DPO run,並排算出 SFT(左)與 DPO(右)的 10 個 episode 影片。

讀 main() 能看到實驗的幾個固定條件:

  • 兩種演算法都從 data/pretrain.pt 載入的 SFT 權重出發;DPO 用這個 SFT 模型初始化,並把它當 reference policy。
  • DPO 假設偏好是嚴格的,所以載入資料時丟掉 0.5 的平手對。
  • 預設 beta=0.1、lr=1e-5,SFT 與 DPO 各 2 萬步,每 2,000 步評估一次、每次 100 個 episode。
  • 環境用 terminate_when_unhealthy=False,跟 Q1 的預設不同。

題目自己先打了預防針:兩個策略的影片可能都不好看,因為它們只用少量離線資料訓練,從頭到尾沒跟環境互動過。這正是 Q3 跟 Q2 最根本的差別,寫(d)的優缺點時可以從這裡切入。

Q4:best arm identification

前三題都在 Hopper 上,Q4 突然換成純理論。情境是:在好幾種實驗藥物或網站設計裡,盡快找出最好的那一個,之後拿來用。這叫 pure exploration,不在乎過程中損失多少 reward。

題目把它寫成 reward 落在 [0, 1] 的 multi-armed bandit,並提醒 bandit 就是只有一個狀態、horizon 為 1 的有限期 MDP,所以一共只有 |A| 個確定性策略。提出的演算法極簡單:每隻手臂拉 n_e 次,回傳平均 reward 最高的那一隻。

題目給了 Hoeffding 不等式:n 個落在 [0, 1] 的 i.i.d. 變數,樣本平均偏離期望值超過 sqrt(log(2/δ) / 2n) 的機率小於 δ。三小題:

  • (a)從 Hoeffding 出發,證明「存在某隻手臂的估計偏離超過同一個寬度」的機率小於 |A|δ。能推出更緊的界也可以,但要說明為什麼更緊。
  • (b)要以至少 1 − δ′ 的機率回傳 ε-最佳的手臂,每隻手臂需要估多準?總共需要多少樣本?答案寫成手臂數、精度 ε 與失敗機率 δ′ 的函數。
  • (c)選做、不計分:改用中央極限定理假設平均值是常態分布,對兩隻手臂的情況重做一次,比較樣本數,並討論在實驗很貴的真實情境下,你會選哪一種假設。

(a)考的是 union bound,(b)考的是「估計誤差多小,才保證選出來的手臂不會差超過 ε」。這兩個工具在下一篇的 UCB 推導裡會再出現一次,而且更精細:order 13 會講到 L9 的 sub-Gaussian 信賴界與 union bound,以及 L10 依 Bandit Algorithms 第 7.1 節重寫的 UCB regret 證明。讀完再做 Q4,會順很多。

跟 UCB 對照也有助於理解題目:UCB 在乎過程中累積的 regret,Q4 只在乎最後選對。同一組集中不等式,換一個目標,就得到不同的演算法與界。

Q5:stated vs. revealed preferences

最後 4 分是一個新聞推薦 app 的情境。app 有兩類使用者資料:使用者自己填的興趣主題與偏好格式,以及互動資料(在哪篇文章停留、停多久、分享給誰)。另外還有地點、瀏覽器、使用頻率、付費與訂閱可能性等 metadata。

四個小問:哪些資料是 stated preference、哪些是 revealed preference;公司可能挑什麼 reward function;以 stated 或 revealed preference 為優先各有什麼倫理考量;怎麼加入探索,測試使用者的偏好會不會隨時間改變。

這題跟 L10 後半的價值對齊客座直接相關,那一講把「對齊使用者意圖、顯示偏好、最佳利益」拆成三種不同的目標。本系列放在 order 17 價值對齊。第 4 小問的「加入探索」則是 order 13–15 的主題。

自學怎麼做

  1. 先開跑 Q1 的 6 次訓練。 這是整份作業最花時間的部分,趁它在背景跑的時候寫 Q1 (a)–(c) 與 Q2 (a)(b) 的推導。
  2. Q2 (c) 用眼睛看完 5 組長片段再寫程式。 你怎麼評估標註品質,會影響你怎麼解讀 (e) 的曲線。
  3. Q2 的梯度推導先做,再寫 RewardModel.update。 推導出來的式子就是你 loss 的樣子;寫完可以用一個手算的小例子檢查 loss 值。
  4. Q3 先確定 ActionSequenceModel 輸出的分布形狀對。 log 機率要在動作序列的每一步與每一維上加總,torch.distributions.Independent 管的就是這件事。
  5. Q4 等讀完 order 13。 沒有 autograder,推導只能自己驗:把最後的樣本數公式代入幾組數字,看它隨手臂數與 ε 怎麼長。

今晚可以做的一件事:下載 Drive 上的 zip,建好環境,跑一次 render.py --dataset data/long-prefs-hopper.npz,看一組 8 秒的長片段,先不看標籤,自己判斷哪一段比較好,再去對答案。這一步不用 GPU,卻能讓你親身體會 RLHF 的資料是怎麼來的,以及它有多吵。

延伸閱讀

參考資料