絡(luò):候選商品引導(dǎo)的注意力機制與推薦系統(tǒng)CTR模型實戰(zhàn)解析)
做推薦系統(tǒng)的同行應(yīng)該都有這種感覺用戶的歷史行為越長數(shù)據(jù)越豐富模型反而越難做。明明一個用戶點了不少裙子、看了不少口紅給他推下一件商品的時候這些歷史行為到底哪些有用、哪些是噪聲就成了一個很難回答的問題。早年大家習(xí)慣把用戶的歷史行為向量全部池化成一個固定向量然后拼上候選商品特征一起過MLP。這個方法很穩(wěn)定但它有一個先天缺陷——不管候選商品是什么池化出的用戶興趣向量都是同一份。仔細想想這是不合理的用戶在看一個籃球的時候他歷史里的口紅行為真的應(yīng)該和看一支口紅時貢獻相同的權(quán)重嗎DIN模型全稱Deep Interest Network就是沖著這個問題來的。它的核心思路特別直白用戶的歷史行為不應(yīng)該被一成不變地壓縮成固定向量而應(yīng)該根據(jù)當前候選商品去“喚醒”歷史行為中和這個候選商品最相關(guān)的部分。這個喚醒過程就是注意力機制只不過它在推薦系統(tǒng)里換了名字叫“局部激活單元”。這篇文章我會把這個模型的動機、網(wǎng)絡(luò)結(jié)構(gòu)、訓(xùn)練細節(jié)、復(fù)現(xiàn)要點和工程落地中的坑全部講透尤其會解釋很多論文里一筆帶過、但實踐中非常關(guān)鍵的地方。1. 為什么需要DIN從固定池化到局部激活1.1 固定向量池化的根本問題在DIN出來之前業(yè)內(nèi)主流的深度學(xué)習(xí)CTR模型基本長這樣把用戶的歷史點擊商品ID序列做Embedding得到一個向量序列然后做sum pooling或者mean pooling把序列壓成一個向量再把這個向量和候選商品Embedding、用戶畫像特征拼在一起送進MLP最后過一個sigmoid輸出點擊概率。這個結(jié)構(gòu)本身沒有問題問題出在pooling這個環(huán)節(jié)。sum pooling的本質(zhì)是把所有行為向量的信息全部“揉”進一個等維度的向量里。假設(shè)用戶的興趣維度是128他點了100個商品這100個商品的Embedding加起來除以100得到的平均向量已經(jīng)把每個商品自身的細節(jié)抹掉了一大半。更關(guān)鍵的是這個池化后的向量無論如何都不會變——候選商品是籃球還是口紅用戶歷史行為的權(quán)重分配都是那同一套。你可以把它理解成一位導(dǎo)購員不管顧客進來是想買運動鞋還是想買護膚品她都用同一套說辭推薦。這套說辭可能在某種場景下有效但面對興趣跨度極大的真實用戶它必然顧此失彼。用戶的興趣天生就是多樣的一個年輕用戶可能同時喜歡數(shù)碼產(chǎn)品和潮牌服飾這兩個興趣在池化時會互相干擾。結(jié)果就是模型為了平衡學(xué)出來的用戶表示變得“平平無奇”哪個興趣方向都表達得不夠充分。1.2 為什么要用候選商品來“指揮”注意力DIN的論文給了我一個非常關(guān)鍵的思想轉(zhuǎn)變用戶興趣表示不應(yīng)該是獨立計算出來的它應(yīng)該和候選商品有關(guān)。所謂局部激活就是當候選商品是籃球鞋的時候模型應(yīng)該去用戶的歷史行為序列里重點提取和籃球鞋相關(guān)的行為當候選商品是美白精華的時候模型應(yīng)該把注意力放到美妝類的歷史行為上。這個“重點提取”的過程就是讓每個歷史行為Embedding和候選商品Embedding做一個交互計算得出一個權(quán)重然后對所有歷史行為向量做加權(quán)和。這樣做的好處立竿見影。第一模型的有效表達能力變強了因為用戶興趣表示不是固定的而是針對每個候選商品“定制”的。第二噪聲干擾減少了歷史行為中與當前候選商品無關(guān)的部分會被自動壓權(quán)不會拖累預(yù)測。第三這個機制是端到端學(xué)出來的不需要人工去定義“籃球鞋和哪些歷史行為有關(guān)”這樣的規(guī)則完全靠數(shù)據(jù)自己擬合。我當時讀完論文的第一反應(yīng)是這個思路太自然了為什么之前沒人這么做其實是有人這么想的但把注意力機制穩(wěn)定地用在億級稀疏特征的CTR場景里并且把訓(xùn)練技巧、激活函數(shù)、正則方法都補齊到工程可用DIN是做得最完整的一個。2. DIN模型結(jié)構(gòu)逐層拆解2.1 候選商品、用戶行為與上下文特征的輸入設(shè)計DIN的輸入分為三大塊。第一塊是候選商品相關(guān)的特征這一塊直接決定預(yù)測目標通常包括商品ID、類目ID、店鋪ID等全部經(jīng)過Embedding后拼接成一個候選向量。第二塊是用戶歷史行為序列重點就是用戶在過去一段時間內(nèi)點擊過的商品ID序列每個商品ID同樣要做Embedding形成一個向量序列。第三塊是用戶畫像特征和上下文特征比如用戶的年齡、性別、城市等級、當前時間等。在特征工程層面有一個值得注意的設(shè)計候選商品和用戶歷史行為里的商品在Embedding層是共享參數(shù)的。候選商品ID是ID行為序列里的商品ID也是ID如果分開建兩套Embedding模型就學(xué)不到“候選商品和歷史點擊商品的關(guān)系”注意力權(quán)重也就失去了基礎(chǔ)。這一點在復(fù)現(xiàn)時特別容易漏掉一旦漏掉AUC會明顯下降。輸入長度方面歷史行為序列不能無限長。常見做法是固定截斷長度比如取最近點擊的50個或者100個商品。如果不足長度就做padding如果超過就按時間倒序截斷。實際場景中很多用戶的歷史行為可能只有十幾個固定長度會帶來大量padding需要在Embedding層把padding對應(yīng)的向量置為零防止它參與有效性計算。2.2 局部激活單元的計算邏輯局部激活單元是整個DIN的核心它的輸入是歷史行為序列里某一個商品對應(yīng)的Embedding向量以及候選商品對應(yīng)的Embedding向量。DIN的局部激活單元結(jié)構(gòu)與傳統(tǒng)的直接拼接做點乘不同它設(shè)計了一個小型的MLP來學(xué)習(xí)這兩個向量之間的交互關(guān)系。具體的計算方式是把兩個向量本身拼接在一起再讓它們做逐元素的外積得到一組交叉項然后把這三部分拼接起來送進一個兩層的全連接網(wǎng)絡(luò)最后輸出一個標量權(quán)重。這個權(quán)重再經(jīng)過指數(shù)映射就是那個歷史行為對候選商品的重要程度。很多初學(xué)者會問一個問題為什么不直接用softmax歸一化DIN有意沒有對全部行為序列做softmax因為用戶興趣的絕對強度本身是有意義的。如果一個用戶對某個歷史行為極其感興趣他對候選商品的外顯權(quán)重就應(yīng)該整體偏高如果做全序列的softmax就相當于把所有行為權(quán)重強制拉成相對比例這會丟失興趣強度的絕對值信息反而影響預(yù)測精度。計算出每個行為向量的權(quán)重之后對所有行為向量做加權(quán)求和得到最終的向量再與候選商品向量、用戶畫像向量拼接送入MLP。MLP部分就是標準的全連接網(wǎng)絡(luò)最后接sigmoid輸出點擊率。2.3 為什么不是先建模行為順序很多人會覺得歷史行為是序列數(shù)據(jù)應(yīng)該先用GRU、LSTM這些建模時序然后再接注意力。這個想法在邏輯上沒錯但在DIN提出的那個階段用序列模型把所有用戶歷史行為建模完整計算量非常大效果增益卻未必匹配。DIN的定位是盡量簡潔地解決“興趣表達與候選商品無關(guān)”的問題它沒有刻意去建模行為之間的先后依賴也沒有做長短期興趣的拆分就是一個注意力加權(quán)求和。論文后續(xù)確實有人在這條路上繼續(xù)延伸比如把行為序列換成Transformer結(jié)構(gòu)做建模但作為基石DIN把最有價值的一步先走通了——讓每個候選商品都能看到用戶歷史行為的“不同切面”。這一步走通之后后面的一切優(yōu)化都有了更牢固的地基。3. 訓(xùn)練中的兩個關(guān)鍵細節(jié)Dice激活與Mini-batch Aware正則3.1 Dice激活函數(shù)做了什么DIN論文里第一次看的時候大部分人都會被Dice激活函數(shù)這個小節(jié)卡住。它看起來只比PReLU多了一個自適應(yīng)開關(guān)但理解它的動機很重要。PReLU是帶可學(xué)習(xí)參數(shù)的ReLU變體公式很好理解當輸入大于零時保持原值當輸入小于等于零時乘以一個可學(xué)習(xí)的系數(shù)。這個系數(shù)是全局固定的一組標量在所有輸入樣本上共享。問題來了推薦系統(tǒng)里的特征輸入分布極其不穩(wěn)定有的特征均值可能在零附近有的特征整體是負數(shù)有的特征方差爆炸。用一個固定負斜率去處理所有分布特征很容易壓不住數(shù)據(jù)分布的變化。Dice的做法是對輸入做標準化用當前batch數(shù)據(jù)的均值和方差把輸入調(diào)整到零均值單位方差附近再用sigmoid函數(shù)動態(tài)控制這個激活門。當輸入分布不穩(wěn)定時這個sigmoid門會自適應(yīng)地調(diào)節(jié)激活點的位置相當于一個可以隨著數(shù)據(jù)分布流動的“軟開關(guān)”。復(fù)現(xiàn)的時候可以直接用PyTorch的batch normalization思路類比但要清楚Dice的標準化是不做仿射變換的重建的它只做標準化和門控。3.2 Mini-batch Aware正則的動機與簡化這個部分論文里給了比較復(fù)雜的形式很多復(fù)現(xiàn)教程會直接略過但工程上它非常重要。推薦系統(tǒng)的Embedding層參數(shù)動輒上億模型的權(quán)重數(shù)量遠超樣本數(shù)過擬合風(fēng)險極大。常用的L2正則理論上能壓制復(fù)雜度但它要求對全量參數(shù)更新時計算梯度這在工業(yè)級的Embedding規(guī)模下是不可行的——每個batch只碰到了極少一部分稀疏ID對全部Embedding做懲罰是巨大的浪費。Mini-batch Aware正則的思路是在計算L2正則時只對當前batch中出現(xiàn)過的稀疏特征對應(yīng)的Embedding做懲罰并且懲罰力度要依據(jù)該特征在當前batch中的出現(xiàn)頻率進行調(diào)整。更直白的解釋是如果一個參數(shù)在當前batch中出現(xiàn)了兩次它獲得的懲罰是出現(xiàn)一次樣本的兩倍如果一個參數(shù)在當前batch中一次都沒出現(xiàn)那就不懲罰它。論文里還做了一個頻次的p次方近似處理避免高頻特征被懲罰過重在實際復(fù)現(xiàn)時如果數(shù)據(jù)量不大不做p次方近似也不會差太多。我自己的復(fù)現(xiàn)經(jīng)驗是Mini-batch Aware正則這個細節(jié)最容易被省略如果不加結(jié)果往往是離線AUC有小幅上升但波動大線上反饋差距更明顯。做工程落地時這個正則一定要保留。4. 動手復(fù)現(xiàn)DINPyTorch代碼核心要點4.1 稀疏特征與行為序列的預(yù)處理在開始寫模型之前先把數(shù)據(jù)流想清楚。輸入樣本通常長這樣user_id、一個候選商品id、一個候選商品的類目id、一個歷史點擊商品id序列、一個歷史點擊商品類目序列、還有若干上下文特征。這里我建議把用戶歷史行為里的商品id和候選商品id放進同一個詞表序列長度固定為N。預(yù)處理階段把歷史行為序列倒序排列按時間最近優(yōu)先取前N個不足補零。class DINBase(nn.Module): def __init__(self, item_count, embedding_dim, sequence_length, hidden_units): super().__init__() self.item_embedding nn.Embedding(item_count, embedding_dim, padding_idx0) self.sequence_length sequence_length self.activation_unit ActivationUnit(embedding_dim, hidden_units) self.mlp nn.Sequential( nn.Linear(embedding_dim * 3 48, 200), Dice(200), nn.Linear(200, 80), Dice(80), nn.Linear(80, 1) )注意這里我拼接了用戶向量、候選商品向量、用戶畫像特征總計維度要算清楚。用戶向量就是注意力加權(quán)求和后的維度候選商品向量就是單個Embedding的維度。畫像特征通常是稠密數(shù)值特征加離散特征的Embedding拼接。4.2 局部激活單元的代碼實現(xiàn)局域激活單元的重點是計算交互特征。我給的實現(xiàn)是把候選商品向量復(fù)制成多個副本與每個歷史行為向量做外積再把三部分拼接送進MLP。class ActivationUnit(nn.Module): def __init__(self, embedding_dim, hidden_units): super().__init__() self.linear nn.Linear(embedding_dim * 3, hidden_units) self.dice Dice(hidden_units) self.linear_out nn.Linear(hidden_units, 1) def forward(self, candidate_item, behavior_sequence): # candidate_item: [batch, dim] # behavior_sequence: [batch, seq_len, dim] batch_size, seq_len, dim behavior_sequence.shape c candidate_item.unsqueeze(1).expand(batch_size, seq_len, dim) interaction c * behavior_sequence concat torch.cat([behavior_sequence, c, interaction], dim-1) hidden self.dice(self.linear(concat)) weight self.linear_out(hidden).squeeze(-1) weight torch.exp(weight) mask (behavior_sequence.sum(dim-1) ! 0).float() weight weight * mask weight weight / (weight.sum(dim-1, keepdimTrue) 1e-8) return weight大多數(shù)人第一次實現(xiàn)都會糾結(jié)要不要對權(quán)重做歸一化。剛才分析過論文思想是不強制做softmax但實際工程里如果序列長度差異很大不做歸一化長序列用戶注意力分數(shù)天然會大跨樣本比較就混亂了。我習(xí)慣在加權(quán)求和前對有效權(quán)重做一次求和歸一化保留興趣強度的相對差異但避免樣本間的序列長度偏差問題。當然如果有更貼近論文原始含義的復(fù)現(xiàn)需求也可以不做這里的歸一化而是直接加權(quán)求和。4.3 訓(xùn)練主流程與超參數(shù)配置DIN在訓(xùn)練時的超參數(shù)選擇直接影響最終效果。論文給我的經(jīng)驗是使用Adam優(yōu)化器初始學(xué)習(xí)率從0.001開始配合指數(shù)衰減大概每兩輪迭代衰減10%。Batch size在稠密ID規(guī)模不夸張時設(shè)為256到512比較合適。負樣本采樣也是一個重要環(huán)節(jié)。曝光未點擊樣本與點擊樣本的比例通常在5:1到20:1之間調(diào)整。比例高了模型會傾向預(yù)測低點擊率需要配合閾值校準比例低了正樣本噪聲會污染注意力權(quán)重的學(xué)習(xí)。訓(xùn)練過程中的Loss就是標準的二值交叉熵。我建議在訓(xùn)練日志里同時記錄訓(xùn)練集和驗證集AUC并且按用戶分組計算GAUC這一點和DIN論文的實驗設(shè)置保持一致因為全局AUC會被少數(shù)高頻用戶主導(dǎo)掩蓋模型在長尾用戶上的真實表現(xiàn)。5. 實驗評估、工程落地與常見問題排查5.1 評估指標為什么全局AUC不夠用DIN論文在實驗部分強調(diào)了評估指標的選擇問題。直接算全量樣本的AUC結(jié)果會被行為數(shù)量極大的頭部用戶“帶偏”這些用戶的興趣通常比較集中歷史行為很長模型對它們的預(yù)測本來就比較容易真正難預(yù)測的是那些歷史行為稀疏的長尾用戶他們的點擊行為跨領(lǐng)域跨度大候選商品的注意力計算會遇到更多挑戰(zhàn)。所以推薦的做法是先把測試樣本按用戶分組在每個用戶組內(nèi)計算AUC再按曝光數(shù)加權(quán)平均得到GAUC。我自己的復(fù)現(xiàn)對比中DIN的相對AUC提升大約在0.5到1個百分點但GAUC提升會更明顯說明模型的增益確實集中在了原有模型搞不定的樣本上。評估時千萬要兩個指標一起看。5.2 線上推理時的性能瓶頸與優(yōu)化工業(yè)落地中局部激活單元帶來的計算開銷不容忽視。在線推理時候選商品數(shù)量可能是幾千上萬個用戶歷史行為序列長度在50到100如果每個候選都和全部歷史行為做一次外積和MLP計算壓力不小。一種有效優(yōu)化是預(yù)先緩存用戶行為序列的Embedding結(jié)果因為用戶的動作頻率遠低于候選商品的變化頻率。行為向量的個數(shù)不變只是權(quán)重計算時需要和每個候選商品重新交互這部分無法完全預(yù)計算但可以把行為Embedding向量的查詢開銷省掉。如果延遲敏感還可以對行為序列做截斷或者只保留最高注意力權(quán)重的前K個行為實測下來精度損失很小。模型參數(shù)存儲方面Embedding矩陣是最大的內(nèi)存消耗點。如果商品量級是千萬級每個商品32維Embedding那么單是商品Embedding就有數(shù)億個參數(shù)必須利用參數(shù)服務(wù)器或者按分片方式存儲不要讓單機內(nèi)存成為瓶頸。5.3 復(fù)現(xiàn)與落地常見問題速查問題現(xiàn)象可能原因解決思路局部激活單元訓(xùn)練不收斂學(xué)習(xí)率過大、行為序列padding沒有遮罩調(diào)小初始學(xué)習(xí)率對padding位置強制置零權(quán)重AUC提升不明顯候選商品與歷史行為商品用了兩套Embedding檢查是否共享詞表與Embedding矩陣訓(xùn)練過程loss震蕩正負樣本比例失衡或batch size過小提高負樣本比例或增大batch size線上延遲超標注意力計算量過大截斷行為序列、只保留TopK高權(quán)重行為長尾用戶預(yù)測偏平行為序列太短、候選信息不足引入跨會話行為或補充用戶長期畫像特征還有一個隱蔽的坑值得單獨提注意力權(quán)重的飽和現(xiàn)象。當候選商品非常好猜時比如用戶歷史行為里有大量同款商品注意力網(wǎng)絡(luò)可能會把幾乎全部權(quán)重集中到一個行為上導(dǎo)致加權(quán)向量退化成單純的某個行為Embedding。這種情況雖然不報錯但會讓模型喪失泛化能力。我的經(jīng)驗是可以在激活單元輸出層加少量dropout或者對注意力權(quán)重logits做輕微的正則防止過度集中。迭代到后面你會發(fā)現(xiàn)DIN的真正價值不在模型結(jié)構(gòu)有多復(fù)雜而在于它把“候選商品引導(dǎo)歷史行為表示”這個思路做成了推薦系統(tǒng)深度模型的標配。后續(xù)很多模型比如用多頭注意力做多興趣抽取的MIND用膠囊網(wǎng)絡(luò)區(qū)分興趣的ComiRec本質(zhì)上都能看到DIN的影子。如果你想在推薦領(lǐng)域深入吃透DIN的每一處細節(jié)是最值得投入的一件事。最后分享一個我在實際復(fù)現(xiàn)中收獲最大的習(xí)慣不要只對著論文公式寫代碼把注意力網(wǎng)絡(luò)輸出的權(quán)重可視化出來。隨機挑幾個樣本看模型到底把注意力給了哪些歷史行為你會發(fā)現(xiàn)一個訓(xùn)練充分的DIN它的注意力分布和人的直覺高度吻合——這個觀察過程能幫你快速定位特征問題和模型問題比單純盯AUC數(shù)字有效得多。