驗回放破解稀疏獎勵難題)
先聊一個很微妙的心理現(xiàn)象事情明明搞砸了過一會兒回過頭看你總覺得“我早就知道會這樣”。心理學管這叫hindsight bias后見之明偏差放在日常里就是事后諸葛亮。但在我接觸過的強化學習項目里有一種做法把“事后之明”變成了實打實的學習信號那就是Hindsight Experience Replay中文常譯作“事后經(jīng)驗回放”縮寫HER。我第一次看OpenAI的機械臂抓取demo時被這個思路驚艷到——原來讓智能體從失敗里學到東西不需要給它更精細的獎勵函數(shù)只需要在經(jīng)驗回放時偷偷換一個“事后目標”。這篇文章我把HER從原理到實現(xiàn)、再到調參踩坑從頭到尾講一遍。適合正在跟稀疏獎勵較勁、跑過DQN或DDPG但效果不理想、或者想搞目標導向機器人策略的朋友。不需要太深的數(shù)學基礎但最好知道強化學習里的狀態(tài)、動作、獎勵和Q函數(shù)是怎么回事。1. 為什么需要Hindsight稀疏獎勵下的學習困境1.1 稀疏獎勵到底“難”在哪先從一個幾乎所有RL項目都會撞上的問題說起稀疏獎勵。拿機械臂推方塊到指定位置來說如果機械臂末端和目標的距離小于一個閾值比如0.05米就獎勵0距離超了就獎勵-1。這樣的話一個episode里絕大多數(shù)動作得到的獎勵都是-1。也就是說智能體在絕大部分時間里根本不知道自己在靠近目標因為在獎勵數(shù)值上“沒差”。這叫沒有梯度信號不是沒獎勵是獎勵就像一面白墻你撞上去不會被彈開也不會被引導。我見過不少人在這種環(huán)境里直接上PPO或DDPG結果訓練幾千個episode成功率仍然是0。原因不復雜隨機探索時機械臂恰好碰到目標的概率極低而且即使偶爾碰上了到目標的距離誤差也可能大得離譜或者由于隨機目標千變萬化同一個經(jīng)驗在下一次采樣里又變成無效樣本。說白了智能體連“成功長什么樣”都沒見過自然學不到通向成功的動作序列。很多人第一反應是設計reward shaping比如讓獎勵與“到目標的距離”連續(xù)相關。這樣做確實能讓訓練提速但代價也明顯手工設計的獎勵會注入人類偏見有可能誘導策略走捷徑、鉆漏洞。而且換一個環(huán)境距離函數(shù)又要重新設計維護成本很高。我希望找到一個更通用的辦法讓算法自己從“看似失敗”的經(jīng)驗里挖掘價值。HER就是為了這個目的出現(xiàn)的。1.2 經(jīng)驗回放為什么救不了場再說說經(jīng)驗回放Experience Replay。DQN和DDPG這類off-policy算法會把(狀態(tài), 動作, 獎勵, 下一狀態(tài))存進一個很大的buffer訓練時隨機采樣小批量打亂相關性。經(jīng)驗回放本身解決的是數(shù)據(jù)利用率和訓練穩(wěn)定性問題但請注意它只是“存儲和讀取經(jīng)驗”的機制并不會改變獎勵信號本身。在稀疏獎勵環(huán)境里buffer里塞滿了失敗經(jīng)驗獎勵全是-1。你采樣一萬條正樣本還是零。Q函數(shù)無法從全負樣本中學到一個有區(qū)分度的價值面。有人會說既然全負至少能讓網(wǎng)絡學會“別亂動”但問題在于目標環(huán)境里每個狀態(tài)s的價值并不相同如果所有狀態(tài)對應的目標g都被固定為原始目標而原始目標從未達成過那么Q網(wǎng)絡只能絕望地把所有狀態(tài)價值都壓向-1。這等于把目標當成了“海市蜃樓”看得見但永遠觸不到。HER的思路很直接既然原始目標達不成那我不要求你在當前軌跡中一定達成它。我可以把這條軌跡“重寫”成朝另一個目標成功的樣本。比如機械臂本來想去左上角結果跑到右上角那我就把“飛到右上角”當作這個樣本的目標重新算一遍獎勵這時候這條原本失敗的經(jīng)驗就成了一條漂亮的正樣本。聽起來有點像自欺欺人但實際效果驚人。這個“換目標”的操作就是HER的核心。1.3 HER的核心直覺換個目標就是成功用一個生活化比喻來解釋一個新手投籃目標是籃筐球沒進砸到了籃板左側。如果他只會按“籃筐”這個目標來訓練這次投球就是發(fā)球失誤沒任何收獲。但如果他換一個目標“下次爭取砸到籃板左側那個點”那么剛才那次投籃在“籃板左側”這個目標下其實是完美命中的。他可以把這個“命中了籃板左側”的經(jīng)驗記下來下次遇到“把球打到籃板左側”的新任務時就知道該用什么樣的力度和角度。強化學習里的HER正是這個流程每一條transition除了保留原始目標g下的樣本還會額外用episode中后續(xù)某個狀態(tài)的achieved_goal事后再看真正到達的位置作為新目標g把這條經(jīng)驗重新計算獎勵后也存進buffer。訓練過程中網(wǎng)絡會同時面對大量不同目標下的經(jīng)驗它的Q函數(shù)可以逐漸學會“給定任意目標我大概知道哪些動作值錢”。等到真正布置一個全新目標時它已經(jīng)具備了泛化能力不需要從頭再學。這種泛化依賴一個老概念通用價值函數(shù)近似器UVFA。意思很簡單價值函數(shù)不再只是Q(s, a)而是Q(s, a, g)把目標也當作輸入的一部分。HER大量生成“目標-狀態(tài)-轉移”三元組本質上就是在用數(shù)據(jù)迫使Q函數(shù)對目標維度泛化。1.4 為什么必須是off-policy算法有一個必須強調的點HER不能直接套在on-policy算法上使用。原因是重標注后的樣本其轉移是在“試圖達成原始目標”的行為策略下產(chǎn)生的。如果我們把經(jīng)驗里的目標換成g那么這條經(jīng)驗就不再符合當前策略對目標g的行為分布。換成Q學習和actor-critic這類off-policy算法它們用Q函數(shù)去估計價值經(jīng)驗來自不同策略也沒關系只要狀態(tài)、動作、獎勵、下一狀態(tài)對換目標后依然自洽即可。我在實際嘗試中踩過這個坑一開始圖省事想在某on-policy的policy gradient框架里硬加HER結果訓練完全飄掉loss亂跳。后來想明白policy gradient算法需要計算的是“當前策略路徑下的梯度”重標注目標后路徑和目標不匹配重要性采樣也救不回來。所以要玩HER安心選擇DQN、DDPG、TD3、SAC這類off-policy方法。2. Hindsight Experience Replay原理拆解2.1 數(shù)學描述與目標重標注先把符號理清楚。設在goal-conditioned MDP里狀態(tài)是由三部分構成的observation機器人的關節(jié)角、速度等原始觀測achieved_goal當前已到達的目標相關狀態(tài)desired_goal想要到達的目標。在Gym的Fetch系列環(huán)境里observation通常是一個15到25維向量achieved_goal是3維坐標desired_goal也是3維坐標。獎勵函數(shù)可以寫成r(s, a, g) 1 if ||achieved_goal(s) - g|| epsilon else 0或者是稀疏負獎勵r(s, a, g) 0 if ||achieved_goal(s) - g|| epsilon else -1其中s是動作執(zhí)行后的下一狀態(tài)achieved_goal(s)表示轉移后真正到達的位置。這一點非常重要判決是要看動作執(zhí)行之后的狀態(tài)離目標有多近而不是動作執(zhí)行之前。存儲一條經(jīng)驗時標準做法是保存(s_t, a_t, r_t, s_{t1}, g_t)其中s_t (obs_t, ag_t, g_t)。HER重標注時從同一個episode中挑一個未來時間點tk取s_{tk}的achieved_goal作為新的目標g然后代入獎勵函數(shù)重新計算r_t于是得到一條新經(jīng)驗(s_t, a_t, r_t, s_{t1}, g)注意動作a_t和轉移(s_t, s_{t1})都沒變變的只是目標和獎勵。正因為原始動作在“去往某個新目標”這個語境下被重新評估原本是無用的失敗樣本才有了正樣本的價值。實際代碼中我習慣直接保存obs_t、act_t、rew_t、next_obs_t、ag_t、next_ag_t和g_t做重標注時再用next_ag_t和新目標計算獎勵。2.2 采樣策略怎么選final / future / episode / randomHER論文里給出了四種從episode中選擇新目標的策略實際效果差異很大這里逐個講清楚。final策略直接用該episode最后一個狀態(tài)作為所有transition的新目標。比如這一輪機械臂最終停在坐標(1.2, 0.3, 0.8)那就把整條軌跡的目標全部替換成(1.2, 0.3, 0.8)。優(yōu)點是實現(xiàn)最簡單一條episode只生成一個額外目標計算量小。缺點是如果episode很長前面很多步驟距離最終位置很遠對價值函數(shù)來說是很大的挑戰(zhàn)學習起來可能不平穩(wěn)。future策略對第t步的transition從第t1步到episode結束的所有狀態(tài)中隨機挑未來狀態(tài)作為新目標。這是論文最推薦、也是我實測最穩(wěn)的策略。因為目標在時間上位于當前transition之后給網(wǎng)絡提供的信號具有天然因果性“我現(xiàn)在做的動作接下來會把我?guī)У竭@個位置所以這個位置應該成為我的目標”。正是這種順序關系讓Q函數(shù)更容易學到動作到結果的映射。episode策略從整個episode中任意狀態(tài)里隨機挑目標不要求未來。這樣做增加了目標多樣性但可能引入“先到A再到B但目標可以是B”這類混亂信號。如果episode比較長還是建議保留一些episode采樣的目標因為它能覆蓋到“達到過去某個狀態(tài)”的能力。random策略從buffer里所有episode中隨機挑狀態(tài)當目標。多樣性最大但和目標任務的關聯(lián)度也最弱。如果目標分布跨度很大random很容易把智能體往莫名其妙的地方帶。我在實際任務里基本不單獨用random頂多摻一小點作為正則化。論文中的標準做法是每條原始transition額外生成k個future目標k通常取4并把原始目標經(jīng)驗也保留下來。也就是說一條經(jīng)驗最終可能會有1原始k條目標變體進入buffer。k不小算力和存儲都會上漲后面我們專門說優(yōu)化。2.3 與DDPG等off-policy算法的融合HER不是一個獨立算法它更像經(jīng)驗回放模塊的一種增強插件。它需要一個基礎的off-policy強化學習算法最經(jīng)典的是DDPG。在DDPG里actor網(wǎng)絡μ(s, g)輸入狀態(tài)和目標輸出動作critic網(wǎng)絡Q(s, a, g)輸入狀態(tài)、動作和目標輸出估計的價值。訓練時的TD目標長這樣y r γ * Q_target(s, μ_target(s, g), g)注意公式里有兩個g一個是重標注后的新目標用于計算目標Q值另一個是critic輸入時要傳的目標。在實現(xiàn)里采集batch時我除了取obs和act還要把重標注后的g也作為target_goal保存起來否則直接在經(jīng)驗里改目標會污染原始樣本。在DDPG的actor更新中梯度通過Q(s, μ(s, g), g)對μ(s, g)的參數(shù)求導。因為Q函數(shù)學會了對任意目標g的價值評估actor才能學到“針對不同目標給出不同動作”的行為模式。如果Q只在一個目標上做過預測那actor也只能在一個目標上動作。HER生成了大量不同目標下的樣本恰好幫助Q面變得更光滑。2.4 數(shù)據(jù)流與存儲細節(jié)HER需要存儲episode結構不能只存一條條獨立的transition。因為重標注需要知道“這條transition之后同一episode里有哪些狀態(tài)”。所以我的buffer設計往往是兩層第一層是一個episodic臨時區(qū)記錄當前episode里每一步的obs、act、next_obs、ag、next_ag、g、done第二層是真正的經(jīng)驗池當episode結束時對整個episode做重標注產(chǎn)生額外transition再寫入經(jīng)驗池。這樣做有一個額外好處避免在每步step時都判斷“現(xiàn)在這條transition是否多采樣幾個未來目標”邏輯更清晰也方便批量向量化。缺點是該episodic臨時區(qū)的內存占用和一次episode長度成正比但通常幾百步以內完全可以接受。2.5 歸一化與動作范圍HER對歸一化很敏感。因為state和goal往往不在同一個尺度比如關節(jié)角度范圍可能是[-3, 3]物體坐標范圍可能是[0, 1]直接拼接后作為網(wǎng)絡輸入actor的探索噪聲如果按幅度加容易讓某些維度被噪聲淹沒。建議把每個維度做running mean/variance歸一化或者至少縮放到[-1, 1]。動作范圍同樣要注意。Fetch環(huán)境的action維度是2到4維范圍是[-1, 1]但物理模型要求機械臂位置變化很小。如果噪聲標準差設成0.3以上可能整個episode都是高頻抖動目標永遠靠不近。我一般初始設置標準差0.1并隨訓練衰減。這不是論文參數(shù)但實測下來穩(wěn)定得多。3. 從零實現(xiàn)在FetchReach上跑通HER3.1 環(huán)境與工具選型為了直觀驗證HER效果我推薦用Gym的FetchReach-v1。機械臂需要從初始位置把末端移動到三維空間中的隨機目標點episode長度50距離閾值0.05獎勵是稀疏的。環(huán)境里沒有物體抓取、沒有避障純粹看算法是否能學習目標導向的到達能力。對HER來說這是最容易診斷的起步環(huán)境。工具方面直接用PyTorch。OpenAI Baselines里雖然有HER的參考實現(xiàn)但基于TensorFlow 1.x舊接口太多復現(xiàn)成本高。我提供的版本會簡化掉一些和圖網(wǎng)絡相關的trick只保留DDPGHER核心讓改造成自己的任務更容易。GPU可有可無這種小環(huán)境CPU也能跑出結果。3.2 網(wǎng)絡與超參數(shù)設計網(wǎng)絡結構不需要花哨。我給出的配置Actor輸入維度原始obs維度FetchReach是10維有效狀態(tài)實際observation有25維其中包含噪聲關節(jié)信息goal維度3輸出維度等于action維度2。中間兩層全連接256激活ReLU輸出層用tanh再乘以動作上限。Critic輸入維度原始obs goal action中間層同樣256最終輸出一個標量Q值。超參數(shù)學習率actor和critic都取1e-3gamma0.98polyak滑動平均系數(shù)tau0.05batch_size256buffer_size1e5每輪episode結束后訓練50步探索噪聲標準差0.1。Actor噪聲在高斯基礎上可以加一個很小的帶通濾波但我直接用純高斯也很好。這里說下tau的選擇DDPG目標網(wǎng)絡更新公式是target - tau * target (1 - tau) * source如果你的tau寫成0.95那幾乎不更新嚴重拖慢訓練。常見框架里默認tau都是0.005或0.001用0.05已經(jīng)算很大了。3.3 核心代碼與注釋下面給出一個簡版但能跑的HERBuffer實現(xiàn)重點在重標注邏輯import numpy as np class HERBuffer: def __init__(self, buffer_size100000, k4): self.buffer_size buffer_size self.k k self.episodes [] # 每個元素是list of transition self.data { obs: [], act: [], rew: [], next_obs: [], ag: [], next_ag: [], g: [], g_her: [] } def push_episode(self, episode_transitions, env): # episode_transitions: list of dict # 先存原始樣本 for t_idx, trans in enumerate(episode_transitions): self._store( trans[obs], trans[act], trans[rew], trans[next_obs], trans[ag], trans[g], trans[g] ) # 再生成HER樣本 horizon len(episode_transitions) for t_idx, trans in enumerate(episode_transitions): # future策略從t_idx之后所有狀態(tài)中隨機選k個 if horizon - t_idx - 1 0: continue for _ in range(self.k): future_idx t_idx 1 np.random.randint(horizon - t_idx - 1) new_goal episode_transitions[future_idx][ag].copy() # 注意獎勵要基于 next_ag 而不是 ag new_rew env.compute_reward( trans[next_ag], new_goal, None ) self._store( trans[obs], trans[act], new_rew, trans[next_obs], trans[ag], new_goal, new_goal ) def _store(self, obs, act, rew, next_obs, ag, g, g_her): if len(self.data[obs]) self.buffer_size: # 簡單實現(xiàn)清空最老的10% drop int(self.buffer_size * 0.1) for key in self.data: self.data[key] self.data[key][drop:] self.data[obs].append(obs) self.data[act].append(act) self.data[rew].append(rew) self.data[next_obs].append(next_obs) self.data[ag].append(ag) self.data[g].append(g) self.data[g_her].append(g_her) def sample(self, batch_size): idx np.random.choice(len(self.data[obs]), batch_size, replaceFalse) batch {} for key in self.data: arr np.array(self.data[key], dtypenp.float32) batch[key] arr[idx] return batch注意代碼里env.compute_reward(trans[next_ag], new_goal, None)這里的第三個參數(shù)info在Gym某些版本里需要傳dict否則報錯。更穩(wěn)妥的做法是直接從環(huán)境定義里取出閾值自己算距離reward -1.0 if np.linalg.norm(next_ag - new_goal) 0.05 else 0.0。接下來是DDPG訓練主循環(huán)的關鍵部分env gym.make(FetchReach-v1) def select_action(actor, obs, goal, noise0.1): obs_t torch.FloatTensor(obs).unsqueeze(0) goal_t torch.FloatTensor(goal).unsqueeze(0) action actor.act(torch.cat([obs_t, goal_t], dim1)) action np.clip(action noise * np.random.randn(*action.shape), -1, 1) return action for episode in range(200): obs env.reset() ep_transitions [] done False score 0 while not done: action select_action(actor, obs[observation], obs[desired_goal]) next_obs, reward, done, info env.step(action) ep_transitions.append({ obs: obs[observation], act: action, rew: reward, next_obs: next_obs[observation], ag: obs[achieved_goal], next_ag: next_obs[achieved_goal], g: obs[desired_goal] }) obs next_obs score reward her_buffer.push_episode(ep_transitions, env) # 從buffer采樣訓練 for _ in range(50): batch her_buffer.sample(256) # 這里用batch[g_her]作為critic的目標goal而不是batch[g] # actor的監(jiān)督信號是Q(s, actor(s, g_her), g_her) # 具體更新代碼省略就是標準DDPG if episode % 20 0: print(fEpisode {episode}, avg_reward: {score})我在實際調試時把sample函數(shù)的batch[g_her]單獨挑出來確保critic在計算TD target時使用的是重標注后的目標。很多初學者容易在這里犯錯直接用batch里的原始goal那等于HER白做了。3.4 訓練觀察與效果分析在FetchReach上跑我的觀察是只用DDPG不加HER成功率幾乎在0到1%之間波動跑500個episode都不動。加上HER后通常100到150個episode之內rollout成功率就能漲到90%以上。訓練曲線不是平滑上升的而是一段平臺后突然跳變。原因是前期buffer里攢到了足夠數(shù)量的正樣本“候選”一旦Q函數(shù)開始對目標維度做出正確梯度策略會突然學會一項簡單技能比如“往某個坐標多推一點”。我建議同時畫兩條曲線一條是online success rate另一條是“平均達到距離”的變化。平均達到距離比成功率更平滑能幫你判斷是否在緩慢進步。如果平均距離在下降但成功率因為閾值的原因一直是0這說明方向是對了繼續(xù)訓練就行。如果平均距離紋絲不動那大概率HER沒生效回去檢查buffer里有沒有非-1的獎勵以及采樣時是否用了新的目標。4. 常見問題與排查技巧實錄4.1 不收斂的排查清單在實際跑HER時我遇到過的失敗模式整理成一個速查表癥狀可能原因排查動作成功率長時間為0平均距離也不降HER重標注邏輯沒生效buffer里仍全是-1打印一批buffer樣本檢查有沒有新目標下獎勵為0的transition訓練loss震蕩劇烈甚至出現(xiàn)NAN輸入和goal沒有歸一化數(shù)值爆炸對obs和goal做running normalization或用固定scale縮放到[-1,1]actor直接學到不動成功率某段突然掉探索噪聲太大動作一直飽和在邊界降低噪聲標準差檢查action是否在[-1,1]內critic的TD target不穩(wěn)定獎勵用舊achieved_goal計算了確保重標注時使用next_ag而不是ag收斂變慢后期上不去future策略采樣到太近的future狀態(tài)目標過于重復提高k值比如k8或混合episode策略增加多樣性4.2 目標采樣策略容易踩的坑future策略雖然好但有一個隱藏問題當episode長度很短比如FetchReach只有50步如果當前transition接近episode末尾剩余可選狀態(tài)很少重標注目標可能總是落在一個小范圍。目標是取自achieved_goal也就是機械臂實際經(jīng)過的位置。如果機械臂只在起點附近徘徊那么這些新目標都集中在起點附近網(wǎng)絡沒機會看到離起點遠的目標泛化就會差。解決方式是調整episode長度或增加動作噪聲讓機械臂多探索。如果任務確實很短可以用futureepisode混合一部分目標取自當前episode未來狀態(tài)一部分取自整個episode的任意狀態(tài)。注意不要全部用episode策略否則因果性過弱訓練穩(wěn)定性反而下降。我在機器人抓取任務里通常設置k4的future外加每個episode額外生成2條episode目標效果會比純future好一些。4.3 與PER、優(yōu)先回放的配合優(yōu)先經(jīng)驗回放PER和HER經(jīng)常被一起討論因為都能提升數(shù)據(jù)效率。我的建議是在HER剛開始投入時慎用PER。HER生成的額外樣本里有一類是“把當前狀態(tài)作為目標”的高獎勵樣本這類樣本TD誤差可能很大PER選中它的概率極高導致更新時被大量“這步本來就接近目標”的樣本刷屏反而壓制了對困難目標的探索。如果非要加PER可以把優(yōu)先級計算公式改溫和一點比如p abs(td_error) 1e-3采樣權重不要按論文那樣嚴格最好設置一個最小概率防止完全丟棄低TD誤差的樣本。說到底HER已經(jīng)把稀疏獎勵變得更密了PER的邊際收益不如在獎勵密集環(huán)境里那么高。4.4 資源與效率優(yōu)化實戰(zhàn)HER會放大經(jīng)驗量k4意味著buffer里的樣本數(shù)大約是原始樣本的5倍訓練一次需要sample的transition也變多了。內存還好但訓練吞吐量下降明顯。我常用的優(yōu)化手段有三個第一不要每步都往主buffer里寫重標注樣本。先緩存整個episode等episode結束后批量生成這樣能利用numpy向量化計算新目標和新獎勵。第二buffer容量不是越大越好。HER需要歷史episode足夠多來提供多樣的狀態(tài)目標但太大了也會讓訓練收斂慢因為樣本分布滯后于當前策略。我一般設成1e5到1e6之間具體看狀態(tài)維度。第三多進程采樣時讓每個worker持有自己的episodic buffer訓練主線程只負責采樣和更新能大幅提高數(shù)據(jù)吞吐。4.5 從仿真到真實機器人落地要記的筆記如果是把HER拿到真實機器人上有幾個細節(jié)跟仿真相差很大。真實機器人的末端坐標通常來自動捕或視覺估計achieved_goal本身帶噪聲而你還要把噪聲后的位置當新目標寫進buffer。這會讓策略學到“去一個不存在的點”或者“重復抖動”。建議在傳感器層先做濾波和標定讓achieved_goal的精度至少高于環(huán)境閾值一個量級。另外真實環(huán)境探索成本高一定要做domain randomization讓仿真里機械臂初始狀態(tài)、物體位置、摩擦系數(shù)都在一個區(qū)間里變化。HER重標注出的目標只是在仿真軌跡上“事后達到過”的位置如果仿真和真實之間的狀態(tài)分布偏移太大這些目標對真實環(huán)境的指導意義有限。安全方面真實機器人初始探索階段可以加一個夾爪保護和限位速度避免機械臂超出關節(jié)極限。5. 經(jīng)驗與擴展思考最后再說一個我在多個任務里反復驗證的經(jīng)驗HER特別適合“目標從狀態(tài)中直接提取”的任務尤其是機器人運動控制、路徑規(guī)劃、操作任務。但如果目標是從圖像之類的高維傳感器里提取挑戰(zhàn)就會大很多因為重標注出的新目標必須對應一個具體的像素塊或者潛在表征單純改標簽可能引入語義錯誤?,F(xiàn)在很多研究在做“表示空間里的HER”比如先學一個狀態(tài)編碼器然后在編碼空間里做目標重標注思路是一樣的。我自己在跑通HER后最大的體會是強化學習里“失敗”不是最可怕的“沒有替代目標”才是最可怕的。HER把失敗經(jīng)驗重新解釋成其他目標下的成功等于給智能體鋪了一條從探索到利用的緩坡。這種“事后聰明”式學習也讓我反思自己平時做項目復盤時的態(tài)度——與其盯著沒達成的原始目標懊悔不如把做出來的東西當作新的參照坐標提煉出它對其他問題有沒有價值。這可能就是hindsight這個詞給我的最大禮物。