點(diǎn)擊率預(yù)測(cè)實(shí)戰(zhàn):七種用戶行為建模與shared-bottom方案)
簡(jiǎn)介面向微信視頻號(hào)場(chǎng)景下的用戶互動(dòng)行為預(yù)測(cè)這份2021年微信大數(shù)據(jù)挑戰(zhàn)賽多目標(biāo)預(yù)測(cè)方案聚焦讀評(píng)論、點(diǎn)贊、點(diǎn)擊頭像、收藏、轉(zhuǎn)發(fā)、發(fā)表評(píng)論、關(guān)注七種行為構(gòu)建基于用戶行為數(shù)據(jù)的點(diǎn)擊率預(yù)測(cè)模型適合備賽學(xué)生、推薦系統(tǒng)學(xué)習(xí)者和算法工程師參考。壓縮包共19個(gè)文件主體為7個(gè)Python腳本和4個(gè)pyc編譯文件負(fù)責(zé)特征工程、模型訓(xùn)練與預(yù)測(cè)配套3個(gè)shell腳本用于初始化和一鍵運(yùn)行另含2個(gè)txt環(huán)境說(shuō)明、2個(gè)md文檔及1個(gè)docx附贈(zèng)資料包體僅84KB輕量清晰。已有86人學(xué)習(xí)瀏覽。通過(guò)這份代碼與說(shuō)明讀者可快速理解多目標(biāo)建模的整體流程掌握從原始行為數(shù)據(jù)到特征構(gòu)造、模型訓(xùn)練、推理銜接的實(shí)踐細(xì)節(jié)md與docx資料進(jìn)一步補(bǔ)充賽題解讀與方案思路便于在此基礎(chǔ)上擴(kuò)展特征或調(diào)優(yōu)參數(shù)是參加同類點(diǎn)擊率預(yù)測(cè)競(jìng)賽或業(yè)務(wù)建模的實(shí)用起點(diǎn)。1. 多目標(biāo)點(diǎn)擊率預(yù)測(cè)從一通亂調(diào)到一份能上項(xiàng)目的完整方案很多人拿到這類“多目標(biāo)預(yù)測(cè)”資源第一反應(yīng)是套個(gè)模型調(diào)參結(jié)果跑出來(lái)的分?jǐn)?shù)還不如單任務(wù)。問(wèn)題不在模型而在標(biāo)簽設(shè)計(jì)、特征窗口和驗(yàn)證切分從一開(kāi)始就沒(méi)對(duì)齊。這份 2021 微信大數(shù)據(jù)挑戰(zhàn)賽的實(shí)戰(zhàn)方案核心是把視頻號(hào)場(chǎng)景下的用戶互動(dòng)行為拆成讀評(píng)論、點(diǎn)贊、點(diǎn)擊頭像、收藏、轉(zhuǎn)發(fā)、發(fā)表評(píng)論、關(guān)注七個(gè)目標(biāo)用統(tǒng)一的用戶行為數(shù)據(jù)做多目標(biāo)點(diǎn)擊率預(yù)測(cè)。它覆蓋了特征構(gòu)造、shared-bottom 多任務(wù)模型、loss 加權(quán)和按時(shí)間切分的驗(yàn)證流程適合正在做推薦系統(tǒng)、廣告排序、內(nèi)容平臺(tái)互動(dòng)預(yù)測(cè)的從業(yè)者也適合拿大數(shù)據(jù)相關(guān)實(shí)戰(zhàn)項(xiàng)目練手的學(xué)習(xí)者照著完整跑一遍。2. 七種用戶行為先拆標(biāo)簽再談模型2.1 從曝光到七種行為標(biāo)簽到底在預(yù)測(cè)什么視頻號(hào)信息流的典型交互鏈路是用戶刷到一條視頻系統(tǒng)記錄這次曝光然后觀察用戶在后續(xù)時(shí)間窗口內(nèi)是否產(chǎn)生互動(dòng)。比賽把這層互動(dòng)拆成了七種獨(dú)立行為每一種都對(duì)應(yīng)一個(gè) 0/1 標(biāo)簽。這七種行為的業(yè)務(wù)含義差異很大。點(diǎn)贊和收藏是輕量級(jí)的興趣表達(dá)用戶不需要付出太多成本所以正樣本相對(duì)充足讀評(píng)論和發(fā)表評(píng)論屬于內(nèi)容消費(fèi)和社交參與前者是“看別人說(shuō)了什么”后者是“我親自下場(chǎng)說(shuō)點(diǎn)什么”兩者門檻明顯不同點(diǎn)擊頭像意味著用戶想進(jìn)一步了解創(chuàng)作者本人這是從內(nèi)容興趣轉(zhuǎn)向賬號(hào)興趣的信號(hào)轉(zhuǎn)發(fā)和關(guān)注則是強(qiáng)意圖行為轉(zhuǎn)發(fā)需要消耗社交信用關(guān)注代表長(zhǎng)期關(guān)系建立這兩類在數(shù)據(jù)里天然稀疏。我從這批方案里讀到的最關(guān)鍵設(shè)計(jì)就是它沒(méi)有把七種行為合并成一個(gè)“綜合互動(dòng)”標(biāo)簽去訓(xùn)練而是保留了每個(gè)任務(wù)的獨(dú)立輸出。原因很簡(jiǎn)單業(yè)務(wù)方要分別知道用戶會(huì)對(duì)一條視頻產(chǎn)生哪種互動(dòng)點(diǎn)贊概率高的人和關(guān)注概率高的人后續(xù)運(yùn)營(yíng)策略完全不同。如果合并成一個(gè)標(biāo)簽這些信息就全糊在一起了。這七種行為的稀疏度大致可以分成三檔我做了個(gè)表方便對(duì)齊后續(xù)的特征和 loss 設(shè)計(jì)思路。行為業(yè)務(wù)含義數(shù)據(jù)稀疏度點(diǎn)贊輕量興趣表達(dá)社交展示成本低低讀評(píng)論內(nèi)容消費(fèi)行為成本最低低收藏深度興趣表達(dá)用戶想留存內(nèi)容中發(fā)表評(píng)論社交參與需要文本輸入成本中點(diǎn)擊頭像從內(nèi)容興趣轉(zhuǎn)向賬號(hào)興趣中轉(zhuǎn)發(fā)強(qiáng)社交背書消耗社交信用高關(guān)注長(zhǎng)期關(guān)系建立意圖層級(jí)最高高2.2 多目標(biāo)為什么不能拆成七個(gè)獨(dú)立模型七種行為在用戶側(cè)高度相關(guān)。一個(gè)對(duì)籃球內(nèi)容有明顯興趣的用戶看到籃球視頻時(shí)點(diǎn)贊、收藏、轉(zhuǎn)發(fā)的概率會(huì)同時(shí)上升。這種相關(guān)性意味著它們共享同一個(gè)底層的用戶興趣表示。如果拆成七個(gè)獨(dú)立二分類模型每個(gè)模型都得從頭學(xué)習(xí)這份興趣表示數(shù)據(jù)利用效率很低。更現(xiàn)實(shí)的問(wèn)題是樣本量。轉(zhuǎn)發(fā)和關(guān)注的正樣本率通常只有千分位級(jí)別單拎出來(lái)訓(xùn)練模型很容易學(xué)成“永遠(yuǎn)預(yù)測(cè)負(fù)樣本”。但點(diǎn)贊和讀評(píng)論的正樣本充足模型能從這兩種高頻行為里學(xué)到用戶興趣的底層結(jié)構(gòu)這個(gè)結(jié)構(gòu)對(duì)轉(zhuǎn)發(fā)和關(guān)注任務(wù)同樣有效。我在實(shí)踐里體會(huì)最深的一點(diǎn)是多目標(biāo)模型不是把七個(gè)模型塞進(jìn)一個(gè)網(wǎng)絡(luò)里就完事而是要設(shè)計(jì)好共享和獨(dú)立的邊界。共享太多低頻任務(wù)會(huì)被高頻任務(wù)帶偏共享太少低頻任務(wù)又學(xué)不動(dòng)。接下來(lái)要寫的特征工程和模型結(jié)構(gòu)都是圍繞這個(gè)平衡點(diǎn)展開(kāi)的。2.3 數(shù)據(jù)長(zhǎng)什么樣字段、標(biāo)簽與樣本組織方式這類比賽的常見(jiàn)數(shù)據(jù)組織形式是一行一個(gè) user_id 和 feed_id 的曝光樣本特征是用戶側(cè)、內(nèi)容側(cè)、上下文側(cè)三類拼接而成標(biāo)簽列是七種行為的 0/1 值。我會(huì)建議拿到數(shù)據(jù)先做三件事確認(rèn)每列的缺失率、確認(rèn)七列標(biāo)簽的稀疏度、確認(rèn)日期范圍的跨度是否足以支撐時(shí)間切分驗(yàn)證。用戶側(cè)字段一般包含年齡、性別、關(guān)注數(shù)、粉絲數(shù)、歷史行為統(tǒng)計(jì)等靜態(tài)畫像。內(nèi)容側(cè)字段通常包括視頻的類別、時(shí)長(zhǎng)、作者信息、發(fā)布時(shí)間以及視頻自身的歷史互動(dòng)表現(xiàn)。上下文側(cè)字段則圍繞“這次曝光發(fā)生在什么場(chǎng)景下”展開(kāi)比如所處的信息流位置、用戶使用的終端、當(dāng)前時(shí)段等。有一點(diǎn)需要提前說(shuō)明這份方案里沒(méi)有把原始 ID 一股腦全塞進(jìn) embedding而是先用統(tǒng)計(jì)特征把行為信息做了一次壓縮。這樣做的直接好處是訓(xùn)練樣本的維度可控新用戶和新視頻出現(xiàn)時(shí)不會(huì)因?yàn)?ID 沒(méi)出現(xiàn)過(guò)就直接失效。下一章詳細(xì)講這些特征具體怎么從原始日志里構(gòu)造出來(lái)。3. 特征工程把行為日志擰成訓(xùn)練樣本3.1 用戶側(cè)統(tǒng)計(jì)特征把行為變成歷史概率多目標(biāo)點(diǎn)擊率預(yù)測(cè)任務(wù)里最有效的特征往往不是那些靜態(tài)畫像而是用戶過(guò)去一段時(shí)間在相似內(nèi)容上的表現(xiàn)。原因是行為預(yù)測(cè)本質(zhì)上是概率估計(jì)用戶過(guò)去 7 天的點(diǎn)贊率本身就是對(duì)“用戶現(xiàn)在有多大概率點(diǎn)贊”的一個(gè)平滑估計(jì)。我一般會(huì)把用戶側(cè)統(tǒng)計(jì)特征分成三個(gè)層次。第一層是用戶粒度的總體統(tǒng)計(jì)比如過(guò)去 N 天的總互動(dòng)次數(shù)、發(fā)表評(píng)論數(shù)、關(guān)注數(shù)增量。第二層是用戶與內(nèi)容類別的交叉統(tǒng)計(jì)比如用戶過(guò)去 N 天在體育類視頻上的點(diǎn)贊率這個(gè)特征對(duì)當(dāng)前體育視頻的點(diǎn)贊預(yù)測(cè)有很強(qiáng)的指向性。第三層是用戶與作者的交叉統(tǒng)計(jì)比如用戶過(guò)去 N 天是否看過(guò)該作者的視頻、看過(guò)之后的互動(dòng)率是多少這個(gè)特征對(duì)點(diǎn)擊頭像和關(guān)注兩個(gè)任務(wù)特別關(guān)鍵。構(gòu)造這些特征時(shí)要特別注意一點(diǎn)統(tǒng)計(jì)區(qū)間必須與預(yù)測(cè)目標(biāo)在時(shí)間上是嚴(yán)格隔離的。訓(xùn)練樣本的預(yù)測(cè)目標(biāo)是用戶在曝光后的行為那么特征只能使用曝光時(shí)刻之前已經(jīng)發(fā)生的行為數(shù)據(jù)。如果混入曝光之后的行為做統(tǒng)計(jì)就會(huì)把答案寫進(jìn)特征里造成嚴(yán)重的數(shù)據(jù)泄漏。3.2 窗口怎么選短期行為與長(zhǎng)期習(xí)慣單一時(shí)間窗口很難覆蓋用戶興趣的全部時(shí)間尺度。當(dāng)天或 3 天內(nèi)的行為反映的是瞬時(shí)興趣比如用戶剛看完一場(chǎng)球賽接下來(lái)幾個(gè)小時(shí)點(diǎn)籃球視頻的概率會(huì)明顯上升14 天和 30 天的窗口反映的則是穩(wěn)定習(xí)慣比如用戶長(zhǎng)期以來(lái)就是一個(gè)高頻運(yùn)動(dòng)內(nèi)容消費(fèi)者。不同任務(wù)對(duì)窗口的敏感度不一樣。點(diǎn)贊和讀評(píng)論這類低成本行為短期窗口的特征往往更有效因?yàn)樗鼈兊挠|發(fā)更依賴當(dāng)下的內(nèi)容吸引力轉(zhuǎn)發(fā)和關(guān)注這類強(qiáng)意圖行為樣本積累速度慢需要更長(zhǎng)窗口的統(tǒng)計(jì)才有足夠的穩(wěn)定性。方案里采用了多窗口并存的方式常見(jiàn)配置是 1、3、7、14 天四檔。短期窗口可以捕捉實(shí)時(shí)熱點(diǎn)對(duì)用戶行為的影響長(zhǎng)期窗口則負(fù)責(zé)刻畫穩(wěn)定的興趣底色模型在訓(xùn)練時(shí)自己學(xué)每個(gè)任務(wù)該側(cè)重哪檔窗口。3.3 行為序列特征把交互順序也喂給模型除了統(tǒng)計(jì)特征行為序列也是多目標(biāo)預(yù)測(cè)里常用的輸入。做法是取用戶最近交互過(guò)的 N 條 feed按時(shí)間排序每條記錄用 feed_id 和對(duì)應(yīng)行為類型組成序列截?cái)嚅L(zhǎng)度一般取 50。這樣模型能直接看到用戶近期走過(guò)的“內(nèi)容軌跡”而不只是壓成幾個(gè)統(tǒng)計(jì)值。序列特征的另一個(gè)作用是引入時(shí)間衰減。用戶 10 天前看過(guò)一個(gè)視頻和 10 分鐘前看過(guò)一個(gè)視頻對(duì)當(dāng)前預(yù)測(cè)的意義差別很大。處理方式通常分成兩種一種是在序列里拼接時(shí)間間隔字段讓模型自己學(xué)衰減關(guān)系另一種是在構(gòu)造統(tǒng)計(jì)特征時(shí)就按指數(shù)衰減權(quán)重計(jì)算加權(quán)均值越近的行為權(quán)重越高。我會(huì)優(yōu)先采用第二種方式做統(tǒng)計(jì)特征因?yàn)閷?shí)現(xiàn)簡(jiǎn)單且穩(wěn)定序列模型作為增強(qiáng)項(xiàng)在 baseline 跑通之后再考慮引入。3.4 特征構(gòu)造代碼從行為日志到訓(xùn)練樣本下面這段代碼是方案里用戶側(cè)統(tǒng)計(jì)特征的 pandas 實(shí)現(xiàn)核心邏輯是窗口聚合和按 act_type 展開(kāi)成多列特征。import pandas as pd import numpy as np def build_user_stats(behavior_log, last_days[1, 3, 7, 14]): 輸入: behavior_log 為行為日志表, 至少包含 user_id, feed_id, act_type, is_click, date act_type 取值: [read_comment,like,click_avatar, collect,forward,comment,follow] 輸出: 用戶側(cè)多窗口統(tǒng)計(jì)特征 DataFrame, 索引為 user_id feat_list [] max_date behavior_log[date].max() for win in last_days: start max_date - pd.Timedelta(dayswin - 1) sub behavior_log[behavior_log[date] start] # 按用戶聚合, 計(jì)算每個(gè)行為的總次數(shù) pivot ( sub.pivot_table( indexuser_id, columnsact_type, valuesis_click, aggfuncsum, fill_value0, ) .reset_index() ) # 把列名加上窗口后綴, 避免多窗口特征名沖突 rename_dict { col: f{col}_{win}d for col in pivot.columns if col ! user_id } pivot pivot.rename(columnsrename_dict) # 用戶總互動(dòng)次數(shù)與行為多樣性 act_cols [c for c in pivot.columns if c.endswith(f_{win}d)] pivot[ftotal_act_{win}d] pivot[act_cols].sum(axis1) pivot[funique_act_{win}d] (pivot[act_cols] 0).sum(axis1) feat_list.append(pivot) # 按 user_id 做橫向拼接, 形成寬表特征 result feat_list[0] for f in feat_list[1:]: result result.merge(f, onuser_id, howouter) return result.fillna(0)這段代碼有四個(gè)設(shè)計(jì)點(diǎn)需要展開(kāi)說(shuō)明。第一用 pivot_table 直接按 act_type 展開(kāi)多列一次性把七種行為的統(tǒng)計(jì)全部算完避免循環(huán)七次重復(fù)掃同一份數(shù)據(jù)。第二窗口后綴 _1d、_3d、_7d、_14d 是必須的因?yàn)槎啻翱谄唇訒r(shí)列名如果不區(qū)分就會(huì)互相覆蓋。第三total_act 和 unique_act 分別刻畫互動(dòng)總量和行為多樣性前者反映活躍度后者反映興趣面的寬窄這兩個(gè)特征對(duì)冷啟動(dòng)用戶的判斷有直接幫助。第四外層 fillna(0) 用來(lái)兜底那些沒(méi)有歷史行為的用戶這類用戶在很多任務(wù)下會(huì)天然落在低概率區(qū)間模型需要靠其他特征來(lái)區(qū)分他們。用這份資源做學(xué)習(xí)時(shí)建議跑完 baseline 之后再試一個(gè)變體把 is_click 換成每個(gè) act_type 的 0/1 標(biāo)簽分別做聚合也就是對(duì)每個(gè)目標(biāo)任務(wù)單獨(dú)構(gòu)造統(tǒng)計(jì)特征。這樣做會(huì)導(dǎo)致特征維度變成原來(lái)的七倍訓(xùn)練時(shí)間明顯變長(zhǎng)但某些任務(wù)的 AUC 會(huì)因此漲一截特別是關(guān)注和轉(zhuǎn)發(fā)這類稀疏任務(wù)。4. 多任務(wù)模型shared-bottom 結(jié)構(gòu)與七塔輸出4.1 多任務(wù)模型怎么選shared-bottom 是性價(jià)比最高的基線多目標(biāo)預(yù)測(cè)的建模方案在工業(yè)界已經(jīng)有不少選擇最常見(jiàn)的是 shared-bottom、MMoE、PLE 和 ESMM 這一類。ESMM 由于專為轉(zhuǎn)化鏈路設(shè)計(jì)在這里并不完全適用MMoE 和 PLE 效果通常更好但結(jié)構(gòu)復(fù)雜調(diào)參成本高。這份方案選用的 shared-bottom 是一個(gè)合理的起點(diǎn)底層共享用戶和內(nèi)容的興趣表示上層每個(gè)任務(wù)獨(dú)立出塔結(jié)構(gòu)簡(jiǎn)單容易復(fù)現(xiàn)也方便后續(xù)替換成更復(fù)雜的門控結(jié)構(gòu)。為什么共享底層對(duì)七目標(biāo)任務(wù)是可行的因?yàn)槠邆€(gè)任務(wù)共享同一個(gè)用戶興趣空間。點(diǎn)贊、收藏、轉(zhuǎn)發(fā)、關(guān)注都依賴用戶對(duì)內(nèi)容的整體喜好判斷只是表達(dá)強(qiáng)度不同。如果從第一天就強(qiáng)行拆成七個(gè)獨(dú)立模型每個(gè)模型的數(shù)據(jù)量都很有限尤其是轉(zhuǎn)發(fā)和關(guān)注這兩個(gè)稀疏任務(wù)基本學(xué)不出穩(wěn)定的興趣表示。共享底層讓這兩個(gè)低頻任務(wù)能從點(diǎn)贊、讀評(píng)論等高頻任務(wù)中借用學(xué)好的表示這是多任務(wù)建模最常見(jiàn)的收益來(lái)源。負(fù)遷移是 shared-bottom 的固有風(fēng)險(xiǎn)。某些任務(wù)的優(yōu)化方向如果與其他任務(wù)沖突共享層反而會(huì)互相拖累。但在這個(gè)場(chǎng)景里七種互動(dòng)行為在用戶側(cè)正相關(guān)負(fù)遷移并不嚴(yán)重這也是這個(gè)方案能直接跑通的底氣。4.2 七塔輸出與 loss 設(shè)計(jì)模型結(jié)構(gòu)上底層是一個(gè)多層全連接網(wǎng)絡(luò)輸入特征向量輸出一個(gè)維度為 hidden_dim 的共享表示。共享表示之上并行掛七個(gè)獨(dú)立塔網(wǎng)絡(luò)每個(gè)塔結(jié)構(gòu)相同各自輸出一個(gè)標(biāo)量經(jīng)過(guò) sigmoid 后得到該行為的預(yù)測(cè)概率。七個(gè)預(yù)測(cè)是獨(dú)立計(jì)算的不強(qiáng)制要求概率之和等于 1。用戶完全可能既點(diǎn)贊又轉(zhuǎn)發(fā)又關(guān)注所以每塔使用獨(dú)立的 BCE loss。整體 loss 是七個(gè) BCE 的加權(quán)和。loss sum(task_weight[i] * bce_loss(logits[i], labels[i]))task_weight 是這份方案里最值得反復(fù)調(diào)的一組超參數(shù)。初始值可以按每個(gè)任務(wù)正樣本率的倒數(shù)做歸一化先把低頻任務(wù)的 loss 放大到和高頻任務(wù)同一量級(jí)。后續(xù)再在 0.5 到 5 倍的范圍內(nèi)做小批量搜索。4.3 模型代碼shared-bottom 七塔結(jié)構(gòu)下面是方案里模型核心結(jié)構(gòu)的 PyTorch 實(shí)現(xiàn)關(guān)鍵點(diǎn)都標(biāo)了注釋。import torch import torch.nn as nn import torch.nn.functional as F class SharedBottomMultiTask(nn.Module): 多任務(wù)共享底層模型 num_features: 特征維度, 與特征工程的寬表列數(shù)一致 num_tasks: 任務(wù)數(shù), 本方案固定為 7 embed_dim: 底層共享表示的維度 def __init__(self, num_features, num_tasks7, embed_dim256): super().__init__() self.num_tasks num_tasks # 共享底層: 所有任務(wù)共用的興趣表示層 self.bottom nn.Sequential( nn.Linear(num_features, embed_dim), nn.BatchNorm1d(embed_dim), nn.ReLU(), nn.Dropout(0.3), nn.Linear(embed_dim, embed_dim // 2), nn.ReLU(), ) # 七塔: 每塔預(yù)測(cè)一個(gè)用戶行為概率 self.towers nn.ModuleList( [ nn.Sequential( nn.Linear(embed_dim // 2, 64), nn.ReLU(), nn.Dropout(0.2), nn.Linear(64, 1), ) for _ in range(num_tasks) ] ) def forward(self, x): # x: [batch_size, num_features] shared self.bottom(x) # 共享表示, 形狀 [batch, 128] logits [] for tower in self.towers: logits.append(tower(shared)) # 每塔輸出 [batch, 1] logits torch.cat(logits, dim1) # 拼接成 [batch, 7] return logits def predict_proba(self, x): logits self.forward(x) return torch.sigmoid(logits)forward 返回 logits 而不是 sigmoid 后的概率是有意的。torch 的 BCEWithLogitsLoss 內(nèi)部會(huì)把 sigmoid 和 BCE 合并計(jì)算數(shù)值上比先算 sigmoid 再算 BCE 更穩(wěn)定梯度也更平滑。predict_proba 單獨(dú)用作推理時(shí)輸出概率訓(xùn)練循環(huán)里統(tǒng)一走帶 logits 的 loss。訓(xùn)練時(shí) group 結(jié)構(gòu)也很重要不同任務(wù)缺失標(biāo)簽時(shí)需要 mask 掉對(duì)應(yīng)位置的 loss。比如數(shù)據(jù)里某條樣本沒(méi)有“讀評(píng)論”行為發(fā)生標(biāo)簽可以填 -1loss 計(jì)算時(shí)把 -1 位置的樣本排除否則零填充的標(biāo)簽會(huì)把負(fù)樣本的統(tǒng)計(jì)向錯(cuò)誤的方向推。criterion nn.BCEWithLogitsLoss(reductionnone) task_weights torch.tensor([1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0]) def multitask_loss(logits, labels, mask, task_weights): # logits: [batch, num_tasks] # labels: [batch, num_tasks], 缺失任務(wù)填 -1 # mask: [batch, num_tasks], 1 表示參與計(jì)算 loss_sum 0.0 for i in range(logits.size(1)): l criterion(logits[:, i], labels[:, i].clamp_min(0)) loss_task (l * mask[:, i]).sum() / (mask[:, i].sum() 1e-8) loss_sum task_weights[i] * loss_task return loss_sum / logits.size(1)有人認(rèn)為七個(gè)任務(wù)都重要所以權(quán)重統(tǒng)一為 1 最公平。實(shí)際跑下來(lái)不是這樣。點(diǎn)贊和讀評(píng)論的正樣本占比高每個(gè) batch 里這兩個(gè)任務(wù)貢獻(xiàn)的 loss 值天然就大權(quán)重全設(shè)為 1 等于邊緣化轉(zhuǎn)發(fā)和關(guān)注。task_weights 設(shè)置的邏輯是“把稀疏任務(wù)抬高到能參與訓(xùn)練的程度”而不是業(yè)務(wù)上誰(shuí)更重要誰(shuí)權(quán)重就大。4.4 loss 權(quán)重怎么調(diào)調(diào) loss 權(quán)重一直是多任務(wù)訓(xùn)練里最玄學(xué)的環(huán)節(jié)但也有一些可行的方法。先用每個(gè)任務(wù)的正樣本率 r_i 算出初始權(quán)重 w_i 1 / sqrt(r_i)然后再統(tǒng)一縮放。比如點(diǎn)贊正樣本率 5%轉(zhuǎn)發(fā)只有 0.3%那轉(zhuǎn)發(fā)的初始權(quán)重大約是點(diǎn)贊的 4 倍。跑一個(gè)完整 epoch 后打印每個(gè)任務(wù)單獨(dú)的 loss 值如果轉(zhuǎn)發(fā)任務(wù)的 loss 波動(dòng)太大就進(jìn)一步抬高它的權(quán)重或者給它的塔加一個(gè)更小的學(xué)習(xí)率。另一條經(jīng)驗(yàn)是不要一開(kāi)始就上 MMoE。把這份資源里 shared-bottom 的結(jié)構(gòu)先跑通、把七個(gè)任務(wù)的 AUC 都打印出來(lái)確認(rèn)每個(gè)任務(wù)都學(xué)到東西之后再考慮用 MMoE 替代 bottom 層。跳過(guò)驗(yàn)直接上復(fù)雜結(jié)構(gòu)很容易陷入“線下漲、線上掉”的怪圈連問(wèn)題出在模型還是數(shù)據(jù)都無(wú)法判斷。5. 訓(xùn)練避坑不均衡樣本、時(shí)間泄漏與指標(biāo)陷阱5.1 低頻行為全預(yù)測(cè)成近似常數(shù)第一次跑這份方案的訓(xùn)練腳本時(shí)現(xiàn)象是關(guān)注和轉(zhuǎn)發(fā)這兩個(gè)塔的輸出全部集中在 0.005 到 0.01 之間基本沒(méi)有區(qū)分度驗(yàn)證集上關(guān)注的 AUC 在 0.6 附近徘徊轉(zhuǎn)發(fā)更低。原因出在 loss 結(jié)構(gòu)上。點(diǎn)贊和讀評(píng)論的正樣本占比高每個(gè) batch 里這兩個(gè)任務(wù)貢獻(xiàn)的梯度和數(shù)值壓制了低頻任務(wù)。任務(wù)權(quán)重全為 1 時(shí)轉(zhuǎn)發(fā)任務(wù)的 loss 占總 loss 的比例只有個(gè)位數(shù)百分比反向傳播時(shí)它對(duì) shared-bottom 的更新信號(hào)相當(dāng)于被淹沒(méi)了。解決辦法是給 loss 加權(quán)初始權(quán)重按 1/sqrt(正樣本率) 計(jì)算。方案里提供的權(quán)重經(jīng)驗(yàn)值是轉(zhuǎn)發(fā)和關(guān)注設(shè)為其他任務(wù)的 2 到 3 倍跑完后轉(zhuǎn)發(fā)任務(wù)的 AUC 能明顯抬升。5.2 隨機(jī)切分驗(yàn)證集線下線上分?jǐn)?shù)分道揚(yáng)鑣很多人習(xí)慣拿到數(shù)據(jù)后直接 train_test_split按 8:2 比例隨機(jī)切分。這種做法在行為預(yù)測(cè)任務(wù)里會(huì)帶來(lái)嚴(yán)重的數(shù)據(jù)泄漏而且泄漏方式不容易察覺(jué)?,F(xiàn)象是本地 AUC 看著有 0.75提交到線上直接落到 0.67怎么調(diào)都補(bǔ)不回來(lái)。原因在于隨機(jī)切分會(huì)讓同一個(gè)用戶的同一天行為一部分進(jìn)訓(xùn)練集、一部分進(jìn)驗(yàn)證集。特征里已經(jīng)包含了截止到曝光時(shí)刻的行為統(tǒng)計(jì)驗(yàn)證樣本會(huì)間接看到未來(lái)信息導(dǎo)致線下評(píng)估過(guò)分樂(lè)觀。解決辦法是嚴(yán)格按時(shí)間切分訓(xùn)練集取截止日期之前的數(shù)據(jù)驗(yàn)證集取之后的數(shù)據(jù)。日期邊界的選擇要結(jié)合數(shù)據(jù)集的日期跨度預(yù)留出至少一個(gè)完整周期的數(shù)據(jù)做驗(yàn)證。5.3 ID 類特征不做頻次截?cái)鄀mbedding 學(xué)成了隨機(jī)向量有的實(shí)現(xiàn)會(huì)把 user_id 和 feed_id 直接做成 embedding 輸入模型?,F(xiàn)象是訓(xùn)練前期 loss 下降正常但上線后遇到新用戶、新視頻時(shí)預(yù)測(cè)概率幾乎失去區(qū)分度所有新內(nèi)容都給出差不多的分?jǐn)?shù)。原因是低頻 ID 的樣本太少embedding 在訓(xùn)練結(jié)束時(shí)沒(méi)有學(xué)出穩(wěn)定的語(yǔ)義而測(cè)試集里出現(xiàn)的新 ID 完全沒(méi)有訓(xùn)練樣本只能映射到隨機(jī)初始化的向量相當(dāng)于模型在朝一個(gè)隨機(jī)向量做預(yù)測(cè)。解決辦法是二選一要么對(duì) ID 頻次設(shè)閾值出現(xiàn)次數(shù)少于 10 次的 ID 統(tǒng)一映射到未知 ID 的 embedding要么干脆不用 ID改用用戶側(cè)統(tǒng)計(jì)特征內(nèi)容側(cè)統(tǒng)計(jì)特征代替這也是這份方案采取的策略。從復(fù)現(xiàn)角度講先把 ID 走統(tǒng)計(jì)特征跑通再逐步引入稀疏 ID embedding會(huì)更穩(wěn)。5.4 只看全局 AUC指標(biāo)會(huì)騙人全局 AUC 是所有樣本混在一起計(jì)算的它天然向高頻用戶傾斜?,F(xiàn)象是全局 AUC 從 0.70 漲到 0.74看起來(lái)一直在提升但拆開(kāi)細(xì)看粉絲數(shù)排前 20% 的用戶貢獻(xiàn)了其中絕大部分新用戶和低活躍用戶的 AUC 反而在掉。原因是這些頭部用戶行為密度高特征統(tǒng)計(jì)值穩(wěn)定預(yù)測(cè)本來(lái)就容易冷啟動(dòng)用戶行為稀少統(tǒng)計(jì)特征接近零向量模型沒(méi)有足夠信息做判斷。解決辦法是在驗(yàn)證階段同時(shí)打印三組指標(biāo)全局 AUC、按用戶分組的 GAUC、冷啟動(dòng)用戶子集的 AUC。三者一起看才能確定優(yōu)化方向。如果 GAUC 在漲而冷啟動(dòng) AUC 在下滑說(shuō)明模型在記憶頭部用戶的行為模式對(duì)沒(méi)有歷史行為的用戶沒(méi)有泛化能力這時(shí)就得回到特征側(cè)補(bǔ)冷啟動(dòng)特征而不是繼續(xù)調(diào)模型。5.5 標(biāo)簽缺失值直接用 0 填充梯度方向被帶偏如果訓(xùn)練數(shù)據(jù)里某些行為的標(biāo)簽因?yàn)椴杉蛉笔е苯犹?0 再算 BCE loss模型會(huì)把這些缺失樣本當(dāng)成明確的負(fù)樣本學(xué)習(xí)導(dǎo)致該行為的預(yù)測(cè)概率整體被壓低估。處理方式是給每個(gè)任務(wù)配一個(gè) mask 矩陣缺失的位置在 loss 計(jì)算中被排除。方案里填的是 -1 作為缺失標(biāo)記訓(xùn)練時(shí)通過(guò) clamp_min(0) 和 mask 結(jié)合使用確保缺失任務(wù)不參與梯度計(jì)算。6. 驗(yàn)證與進(jìn)階離線評(píng)估這樣切結(jié)果才接近線上6.1 驗(yàn)證集切分先按日期劃一道硬邊界我每次拿到新的行為數(shù)據(jù)集第一件事不是跑模型而是先把日期字段的分布打印出來(lái)確認(rèn)數(shù)據(jù)覆蓋的天數(shù)。然后按時(shí)間切分訓(xùn)練集截止在某一天驗(yàn)證集取其后完整周期的數(shù)據(jù)。import pandas as pd df pd.read_csv(train.csv, parse_dates[date]) split_date pd.Timestamp(2021-06-01) train df[df[date] split_date].copy() valid df[df[date] split_date].copy() # 校驗(yàn): 兩邊日期范圍不應(yīng)有重疊 assert train[date].max() valid[date].min() # 特征構(gòu)造必須只用 train 階段信息 # 統(tǒng)計(jì)特征在 valid 上計(jì)算時(shí), 只能使用 valid 樣本自身 date 之前的數(shù)據(jù)這個(gè)切法的關(guān)鍵點(diǎn)是統(tǒng)計(jì)特征不跨邊界計(jì)算。訓(xùn)練集里構(gòu)造的所有用戶統(tǒng)計(jì)特征都只能基于訓(xùn)練集本身的行為不可以用全量數(shù)據(jù)先算特征再切分那等于把驗(yàn)證集的信息提前放進(jìn)了特征里。6.2 離線評(píng)估GAUC 才是業(yè)務(wù)視角的指標(biāo)全局 AUC 衡量的是排序能力但用戶行為預(yù)測(cè)更關(guān)注“同一用戶內(nèi)部正樣本排名是否靠前”。GAUC 就是按用戶分組計(jì)算 AUC再用曝光數(shù)加權(quán)平均。from sklearn.metrics import roc_auc_score import pandas as pd def gauc(y_true, y_pred, user_ids): df pd.DataFrame({y: y_true, p: y_pred, uid: user_ids}) total_w 0.0 total_auc 0.0 for uid, group in df.groupby(uid): if len(group) 2 or group[y].nunique() 2: continue try: auc roc_auc_score(group[y], group[p]) except ValueError: continue w len(group) total_auc auc * w total_w w return total_auc / total_w if total_w 0 else 0.0驗(yàn)證集切分和 GAUC 這兩步是我拆這份資源時(shí)最想強(qiáng)調(diào)的收獲。之前的教訓(xùn)來(lái)自一次線上翻車本地全局 AUC 0.73上線后效果完全不對(duì)重新排查后發(fā)現(xiàn)驗(yàn)證集切分時(shí)用了隨機(jī)切分特征里混入了驗(yàn)證集信息。從那以后我每次做多目標(biāo)行為預(yù)測(cè)都會(huì)強(qiáng)制走一遍“按時(shí)間切分驗(yàn)證加 GAUC 和冷啟動(dòng)子集指標(biāo)雙榜對(duì)照”的流程再?zèng)Q定要不要?jiǎng)幽P徒Y(jié)構(gòu)。這份方案最大的價(jià)值也在這里——它不是只給你一個(gè)能跑的模型而是把數(shù)據(jù)劃界、特征隔離、指標(biāo)拆解這些容易被忽略但決定成敗的環(huán)節(jié)都串起來(lái)了。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取