KDD CUP 2020之Debiasing赛道方案 (Rush)
“?本文介紹了Rush團隊在KDD CUP 2020的Debiasing賽道中的解決方案,涵蓋了在召回,粗排以及精排階段的問題思考和總結,并針對核心算法模塊給出了清晰的代碼實現,全程干貨,推薦大家學習!”
團隊介紹:Rush團隊成員主要來自中科院,廈門大學和哈爾濱工業大學。團隊有多名非常優秀的2021應屆生,大佬們有工作機會的話,歡迎私信知乎"蘑菇先生"或添加微信"xtf_sir"。也歡迎大家點擊文末閱讀原文關注知乎專欄,蘑菇先生學習記。
此次比賽是典型的序列推薦場景中的糾偏問題,即:debiasing of next-item-prediction。模型構建的過程中要重點考慮行為序列和蘊含在序列中的時間信息,位置信息和長短期偏好等。為此,本文提出了一種融合傳統協同過濾方法和圖神經網絡方法的多路召回模型以及集成了GBDT和DIN的排序模型的方案。該方案遵循了推薦系統的主流架構,即召回+粗排+精排。召回方案主要包括了多種改進后的協同過濾方法,即:user-cf、item-cf、swing、bi-graph,以及改進的基于序列的圖神經網絡SR-GNN[1]方法。這些改進的召回方法能夠有效進行數據的糾偏。對每種召回方法,粗排階段會基于物品的流行度等因素對每個用戶的推薦結果進行初步重排,然后將不同方法的Recall的結果初步融合起來。精排方案主要采用了GBDT和DIN[4]方法,會重點挖掘召回特征,內容特征和ID類特征。最后通過集成GBDT和DIN產生排序結果。最終,我們團隊「Rush」的方案在Track B中,「full指標第3名,half指標第10名。」
目前代碼已開源:https://github.com/xuetf/KDD_CUP_2020_Debiasing_Rush
賽題解析
賽題介紹:KDD Cup 2020 Challenges for Modern E-Commerce Platform: Debiasing主要包括了4個數據集:
underexpose_user_feat.csv: 用戶的特征,uid, age, gender, city。缺失值非常多。
underexpose_item_feat.csv: 物品的特征,iid, 128維圖片向量+128維文字向量。
underexpose_train_click-T.csv: ?uid, iid, time, 訓練集,記錄了用戶歷史點擊行為。
underexpose_test_click-T.csv: uid, iid, time, 測試集,記錄了待預測用戶的歷史點擊行為。賽題方 ?還給出了要預測的用戶下一次發生點擊行為時的時間,即:underexpose_test_qtime-T.csv
目標是基于用戶歷史點擊行為,來預測下一次用戶會點擊的item,即「next-item prediction」。
根據賽題介紹和對數據集的觀察,可以推測主辦方是從全量數據里頭隨機采樣部分用戶,將這些用戶的點擊數據作為賽題的數據。在進行數據劃分的時候,選取了部分用戶的數據作為測試集test,其他用戶的數據作為訓練集train。對于測試集,將每個用戶行為序列的最后一次交互item作為線上測試answer,行為序列去除掉最后一個交互item以外的作為test用戶的歷史行為數據公開給我們,同時將answer中的user id和query time也公開給我們,即,test_q_time。具體如下圖所示:
線上數據劃分顯然,這是典型的序列推薦場景,即「next-item-prediction」。模型構建的過程中要「重點考慮行為序列和蘊含在序列中的時間信息,位置信息和長短期偏好等」。
為了保證線上線下數據分布的一致性,驗證集劃分思路可參考線上數據的劃分方式。即,利用線上train訓練集進行劃分,從train數據集中隨機采樣1600個用戶,將這1600個用戶的最后一次交互item作為驗證集answer,其它數據作為驗證集用戶的歷史行為數據。具體如下圖所示:
驗證集劃分這樣的劃分,保證了離線環境和線上環境的一致性。上述操作對每個phase都會進行這樣的劃分過程。
數據分析
幾個重要的數據分析觀察和結論如下:
經過統計分析,每個階段的時間范圍一致,不同階段按照時間推移,且不同階段的時間重疊部分占到了階段時間區間的3/4,因此會出現當前階段記錄不完全的情況,所以訓練模型時需要考慮使用聯合多個phase的全量數據訓練模型。「推測可能是線上打點日志系統的延遲上報,或者主辦方對每個階段的數據,都是從某個較大的時間區間內通過滑動窗口的方式隨機采樣得到的,因此樣本存在較大的時間重疊。」
經過驗證集上的統計,每個用戶的最后一次點擊有99%以上是在當前階段出現過的item,因此利用全量數據時需要將不屬于當前phase的item過濾掉,防止item的穿越。
一條相同的點擊數據可能會分布在各個階段之中,重復率占比非常高,因此需要對記錄進行「去重處理」。
item出現的次數呈現典型的長尾分布,在重排階段需要挖掘長尾物品,如結合物品出現的頻次進行糾偏。
其它的一些分析包括,最后一次點擊和倒二次點擊之間的內容相似性、基于w2v嵌入的行為相似性等分析。不一一列舉。
方案
我們的方案遵循了推薦系統的主流架構,即召回+粗排+精排。召回方案主要包括了多種改進后的協同過濾方法,即:「user-cf」、「item-cf」、「swing」、「bi-graph」。以及改進的基于序列的「圖神經網絡」SR-GNN方法。對每種召回方法,粗排階段會基于物品的流行度等因素對每個用戶的推薦結果進行初步重排,然后將不同方法的Recall的結果初步融合起來。精排方案主要采用了GBDT和DIN方法,會重點挖掘召回特征,內容特征和ID類特征。最終產生的結果是「GBDT」和「DIN」的集成。
召回方案
召回訓練集構造
經過數據分析,我們發現不同階段的數據存在明顯的交叉,說明了不同階段之間不存在明確的時間間隔。因此,我們希望充分利用所有階段的數據。但是直接利用所有階段的數據會造成「非常嚴重的數據穿越問題」。為了保證數據不穿越,我們對全量數據做了進一步的篩選。這是本方案的「key points」之一。具體包括兩點:
對每個用戶,根據測試集中的q-time,將q-time之后的數據過濾掉,防止user的行為穿越。
對1) 中過濾后的數據,進一步,把不在當前階段出現的item的行為數據過濾掉,防止item穿越。
def?get_whole_phase_click(all_click,?click_q_time):'''get?train?data?for?target?phase?from?whole?click:param?all_click:?the?click?data?of?target?phase:param?click_q_time:?the?infer?q_time?of?target?phase:return:?the?filtered?whole?click?data?for?target?phase'''whole_click?=?get_whole_click()phase_item_ids?=?set(all_click['item_id'].unique())pred_user_time_dict?=?dict(zip(click_q_time['user_id'],?click_q_time['time']))def?group_apply_func(group_df):u?=?group_df['user_id'].iloc[0]if?u?in?pred_user_time_dict:u_time?=?pred_user_time_dict[u]group_df?=?group_df[group_df['time']?<=?u_time]return?group_dfphase_whole_click?=?whole_click.groupby('user_id',??group_keys=False).apply(group_apply_func)print(phase_whole_click.head())print('group?done')#?filter-out?the?items?that?not?in?this?phasephase_whole_click?=?phase_whole_click[phase_whole_click['item_id'].isin(phase_item_ids)]return?phase_whole_click
參考item-cf [7, 8]的實現,考慮了交互時間信息,方向信息、物品流行度、用戶活躍度等因素對模型的影響對模型的影響。
其中,
考察了交互時間差距因素的影響,
考慮交互方向的影響,;正向時,即時,,否則,即,反向時,
考慮了物品流行度的影響,越流行的商品,協同信號越弱。即為交互過物品的用戶。
考慮了用戶活躍度的影響,越活躍的用戶,協同信號越弱,是用戶的交互過的物品。
用戶對目標item的分數,即多種recall方法融合并粗排后的分數;
目標item和歷史item的相似性,我們只選擇歷史交互序列中的「最后3個物品」的內容特征進行計算相似性。
待預測的目標物品原始的內容特征。
用戶歷史交互序列中的item的內容特征,根據交互時間和順序進行加權計算后的興趣向量。
用戶興趣向量和物品內容向量之間的內容相似性分數。
word2vec對訓練集中的用戶hist item sequences進行訓練,然后得到的每個物品的w2v向量。
每個待預測的目標物品的w2v向量和用戶歷史交互的item的w2v向量之間的相似性分數。
user id特征
item id特征
用戶歷史行為序列中的 item id特征 (和item id 特征共享嵌入)
GBDT實現:
DIN實現:
「召回訓練集的構造」,如何使用全量數據進行訓練,user側和item側都需要「防止穿越」。這個提高非常顯著,說明「數據」對于結果的影響非常大。
「CF中的改進點能夠有效進行糾偏」,包括,「交互時間、方向、內容相似性、物品流行度、用戶活躍度」等。這個提高也很顯著,和賽題Debiasing主題契合。
「SR-GNN」基于序列推薦的圖神經網絡模型,完美契合本次比賽序列推薦場景,捕捉item之間的「多階相似性」并兼顧用戶「長短期偏好」。另外,我們基于SR-GNN的改進點,「使用內容特征進行嵌入初始化」、根據頻次引入結點權重 (為了糾偏)、位置編碼 (強化短期交互互影響力)、嵌入歸一化、殘差連接、sequence-level embedding的構建等都帶來了提升。SR-GNN召回方案的提升效果達到「0.05+」。
「粗排考慮了頻次,提高低頻商品的曝光率,以消除召回方法對高頻商品的偏向性」,對half指標的提升很顯著。
「排序特征的構建」,包括召回特征、內容特征、歷史行為相關的特征、ID特征等。
「排序模型集成」,「LGB和DIN模型的融合」,對最終的指標有比較高的提升。
對每個階段,經過上述步驟后得到篩選后的針對該階段的全量訓練數據,會作為多路召回模型的輸入進行訓練和召回。
多路召回
多路召回包括了4種改進的協同過濾方法以及改進的圖神經網絡SR-GNN方法。
Item-CF
上述改進能夠有效進行糾偏。
def?get_time_dir_aware_sim_item(df):user_item_time_dict?=?get_user_item_time_dict(df)sim_item?=?{}item_cnt?=?defaultdict(int)for?user,?item_time_list?in?tqdm(user_item_time_dict.items()):for?loc_1,?(i,?i_time)?in?enumerate(item_time_list):item_cnt[i]?+=?1sim_item.setdefault(i,?{})for?loc_2,?(relate_item,?related_time)?in?enumerate(item_time_list):if?i?==?relate_item:continueloc_alpha?=?1.0?if?loc_2?>?loc_1?else?0.7loc_weight?=?loc_alpha?*?(0.8?**?(np.abs(loc_2?-?loc_1)?-?1))time_weight?=?np.exp(-15000?*?np.abs(i_time?-?related_time))sim_item[i].setdefault(relate_item,?0)sim_item[i][relate_item]?+=?loc_weight?*?time_weight?/?math.log(1?+?len(item_time_list))sim_item_corr?=?sim_item.copy()for?i,?related_items?in?tqdm(sim_item.items()):for?j,?cij?in?related_items.items():sim_item_corr[i][j]?=?cij?/?math.sqrt(item_cnt[i]?*?item_cnt[j])return?sim_item_corr,?user_item_time_dictUser-CF
在原始user-cf基礎上考慮了用戶活躍度、物品的流行度因素。
def?get_sim_user(df):#?user_min_time_dict?=?get_user_min_time_dict(df,?user_col,?item_col,?time_col)?#?user?first?time#?historyuser_item_time_dict?=?get_user_item_time_dict(df)#?item,?[u1,?u2,?...,]item_user_time_dict?=?get_item_user_time_dict(df)sim_user?=?{}user_cnt?=?defaultdict(int)for?item,?user_time_list?in?tqdm(item_user_time_dict.items()):num_users?=?len(user_time_list)for?u,?t?in?user_time_list:user_cnt[u]?+=?1sim_user.setdefault(u,?{})for?relate_user,?relate_t?in?user_time_list:#?time_diff_relate_u?=?1.0/(1.0+10000*abs(relate_t-t))if?u?==?relate_user:continuesim_user[u].setdefault(relate_user,?0)weight?=?1.0sim_user[u][relate_user]?+=?weight?/?math.log(1?+?num_users)sim_user_corr?=?sim_user.copy()for?u,?related_users?in?tqdm(sim_user.items()):for?v,?cuv?in?related_users.items():sim_user_corr[u][v]?=?cuv?/?math.sqrt(user_cnt[u]?*?user_cnt[v])return?sim_user_corr,?user_item_time_dictSwing
基于圖結構的推薦算法Swing [9],將物品的流行度因素也考慮進去。
def?swing(df,?user_col='user_id',?item_col='item_id',?time_col='time'):#?1.?item,?(u1,t1),?(u2,?t2).....item_user_df?=?df.sort_values(by=[item_col,?time_col])item_user_df?=?item_user_df.groupby(item_col).apply(lambda?group:?make_user_time_tuple(group,?user_col,?item_col,?time_col)).reset_index().rename(columns={0:?'user_id_time_list'})item_user_time_dict?=?dict(zip(item_user_df[item_col],?item_user_df['user_id_time_list']))user_item_time_dict?=?defaultdict(list)#?2.?((u1,?u2),?i1,?d12)u_u_cnt?=?defaultdict(list)item_cnt?=?defaultdict(int)for?item,?user_time_list?in?tqdm(item_user_time_dict.items()):for?u,?u_time?in?user_time_list:#?just?recorditem_cnt[item]?+=?1user_item_time_dict[u].append((item,?u_time))for?relate_u,?relate_u_time?in?user_time_list:if?relate_u?==?u:continuekey?=?(u,?relate_u)?if?u?<=?relate_u?else?(relate_u,?u)u_u_cnt[key].append((item,?np.abs(u_time?-?relate_u_time)))#?3.?(i1,i2),?simsim_item?=?{}alpha?=?5.0for?u_u,?co_item_times?in?u_u_cnt.items():num_co_items?=?len(co_item_times)for?i,?i_time_diff?in?co_item_times:sim_item.setdefault(i,?{})for?j,?j_time_diff?in?co_item_times:if?j?==?i:continueweight?=?1.0??#?np.exp(-15000*(i_time_diff?+?j_time_diff))sim_item[i][j]?=?sim_item[i].setdefault(j,?0.)?+?weight?/?(alpha?+?num_co_items)#?4.?norm?by?item?countsim_item_corr?=?sim_item.copy()for?i,?related_items?in?sim_item.items():for?j,?cij?in?related_items.items():sim_item_corr[i][j]?=?cij?/?math.sqrt(item_cnt[i]?*?item_cnt[j])return?sim_item_corr,?user_item_time_dictBi-Graph
Bi-Graph [3, 10] 核心思想是將user和item看做二分圖中的兩個集合,即:用戶集合和物品集合,通過不同集合的關系進行單模式投影得到item側的物品之間的相似性度量。改進方式:將時間因素、商品熱門度、用戶活躍度三因素考慮進去。
def?get_bi_sim_item(df):item_user_time_dict?=?get_item_user_time_dict(df,)user_item_time_dict?=?get_user_item_time_dict(df)item_cnt?=?defaultdict(int)for?user,?item_times?in?tqdm(user_item_time_dict.items()):for?i,?t?in?item_times:item_cnt[i]?+=?1sim_item?=?{}for?item,?user_time_lists?in?tqdm(item_user_time_dict.items()):sim_item.setdefault(item,?{})for?u,?item_time?in?user_time_lists:tmp_len?=?len(user_item_time_dict[u])for?relate_item,?related_time?in?user_item_time_dict[u]:sim_item[item].setdefault(relate_item,?0)weight?=?np.exp(-15000?*?np.abs(related_time?-?item_time))sim_item[item][relate_item]?+=?weight?/?(math.log(len(user_time_lists)?+?1)?*?math.log(tmp_len?+?1))return?sim_item,?user_item_time_dictSR-GNN
SR-GNN [1] 是將GNN用于序列推薦的一種模型,原論文的方法在多個數據集上都表現出較好的性能。SR-GNN通過GGNN能夠捕捉序列中不同item之間的多階關系,同時會綜合考慮序列的長短期偏好,尤其是短期的最后一次交互item,天然適用于該比賽的場景。但是直接使用原始論文開源的代碼[13],在我們的比賽場景中,召回效果不佳,還不如單個CF方法來的好,因此需要進行改進。
我們將用戶的行為記錄按時間戳排序,然后對用戶序列進行數據增強操作,得到增強后的行為序列后,使用改進的SR-GNN實施召回。具體改進如下:
「嵌入初始化」
由于訓練樣本較少,難以對物品嵌入矩陣進行充分的學習,因此不宜使用隨機初始化。考慮到比賽提供的數據中包含了物品特征,為此我們使用物品的文本描述和圖片描述向量(共256維)對嵌入矩陣進行初始化。這是「本方案的重要trick之一。」 這個方法能夠顯著解決某些長尾item的嵌入學習不充分的問題。
#?obtain?item?feat item_embed_np?=?np.zeros((item_cnt?+?1,?256)) for?raw_id,?idx?in?item_raw_id2_idx_dict.items():vec?=?item_content_vec_dict[int(raw_id)]item_embed_np[idx,?:]?=?vec np.save(open(sr_gnn_dir?+?'/item_embed_mat.npy',?'wb'),?item_embed_np)#?initialize?node?item?embedding if?kwargs.get('feature_init',?None)?is?not?None:init?=?tf.constant_initializer(np.load(kwargs['feature_init']))logger.info("Use?Feature?Init") else:init?=?tf.random_uniform_initializer(-self.var_init,?self.var_init)self.node_embedding?=?(tf.get_variable("node_embedding",?shape=[node_count,?self.hidden_size],?dtype=tf.float32,?initializer=init))「帶有節點權重的消息傳播」
在SR-GNN中,得到物品序列后,將序列中的物品作為圖節點,序列中相鄰的物品之間通過有向邊連接,最終分別得到入邊和出邊的鄰接矩陣并按行歸一化。例如,物品序列對應的有向圖及鄰接矩陣, ?如下所示:
得到序列的圖表示后,之后進行GNN處理的,「遵循GNN信息傳遞架構」 [12],即:「信息構造—傳播—更新」三個步驟:
1)、 「信息構造:」 針對全部物品設置嵌入矩陣,每個節點對應的物品可用嵌入矩陣的一個行向量表示。由于訓練集中物品呈長尾分布,「對于出現次數較多的物品,我們希望降低它的影響」,因此我們設置節點(即對應的物品)的初始權重,
為物品在訓練集中出現的次數,為全部物品出現次數的中位數,最終權重位于(0,1)之間,出現次數較多的物品權重較小,而出現次數較少的物品權重接近1。我們設置權值為可學習的參數,因此節點待傳播的信息為。
2)、 「傳播:」 按照連接矩陣進行傳播,
此處,為圖中全部節點的信息矩陣,分別表示入度矩陣和出度矩陣的行, 我們從入邊和出邊兩個方向傳播信息,為節點在第步時從鄰居節點匯聚得到的信息。為模型可學習的參數。
3)、 「更新:」 根據節點自身的信息和來自鄰居的信息,更新節點的信息。這里使用GRU進行結點信息的更新:,此處,我們采用了殘差連接。
以上過程可循環進行步,最終每個節點可獲取到它的階鄰居的信息。我們的方案中,。
「位置編碼」
用戶的行為受最后一次交互影響較大,為了強化交互順序的影響,我們增加了位置編碼矩陣,為位置數量,我們從后向前編碼,最后一次交互的物品位置為1,上一次為2,以此類推。通過GNN更新后的節點向量和位置編碼向量相加:
為節點的位置編碼向量。我們設置,對于倒數第5個物品之前的物品,它們的位置均為5。
「序列級別的嵌入表征」
這里需要匯聚圖中全部節點向量,得到一個圖級別的輸出作為序列的嵌入表征。考慮到最后一次行為的重要性,我們使用了加權平均池化的匯聚方式,即:
為序列最后一個item的嵌入表示,這里我們對序列中最后一個物品之前的物品向量進行平均池化,之后和最后一個物品向量按照權重進行加權,得到序列的表示。是可學習的參數。
「預測和損失函數」
我們對序列向量及物品向量進行L2歸一化:
之后通過點積對物品進行打分:
為超參數,我們設為10,來進一步拉大高意向item和低意向item之間的差距。這實際上是通過余弦相似度對物品進行打分,這些在參考文獻[2]中有具體描述。模型的損失為預測概率的多分類交叉熵損失。
產出多路召回結果
上述協同過濾方案實際上分為了Item-based,即:item-cf、swing、bi-graph和User-based,即user-cf。在具體進行推薦時,我們封裝了基于item的產生召回結果的流程和基于user的產生召回結果的流程。
Item-based
item-based的方法在進行推薦的時候,會利用用戶的歷史行為item,計算歷史行為item最相似的Top-K個item推薦給用戶。在計算相似性時,同樣會利用前文提到的策略,即:根據交互時間進行指數衰減;根據交互方向進行冪函數衰減。同時,我們還利用了物品的內容特征,即,利用Faiss [11] 計算了item-item之間的內容相似性權重,最后,每個item的得分=召回方法的分數 時間權重 方向權重 內容權重。每種方法產生Top-200個召回結果。
def?item_based_recommend(sim_item_corr,?user_item_time_dict,?user_id,?top_k,?item_num,?alpha=15000,item_cnt_dict=None,?user_cnt_dict=None,?adjust_type='v2'):item_content_sim_dict?=?get_glv('item_content_sim_dict')?#?get?global?variablesrank?=?{}if?user_id?not?in?user_item_time_dict:return?[]interacted_item_times?=?user_item_time_dict[user_id]min_time?=?min([time?for?item,?time?in?interacted_item_times])interacted_items?=?set([item?for?item,?time?in?interacted_item_times])for?loc,?(i,?time)?in?enumerate(interacted_item_times):if?i?not?in?sim_item_corr:continuefor?j,?wij?in?sorted(sim_item_corr[i].items(),?key=lambda?x:?x[1],?reverse=True)[0:top_k]:if?j?not?in?interacted_items:rank.setdefault(j,?0)content_weight?=?1.0if?item_content_sim_dict.get(i,?{}).get(j,?None)?is?not?None:content_weight?+=?item_content_sim_dict[i][j]if?item_content_sim_dict.get(j,?{}).get(i,?None)?is?not?None:content_weight?+=?item_content_sim_dict[j][i]time_weight?=?np.exp(alpha?*?(time?-?min_time))loc_weight?=?(0.9?**?(len(interacted_item_times)?-?loc))rank[j]?+=?loc_weight?*?time_weight?*?content_weight?*?wijif?item_cnt_dict?is?not?None:for?loc,?item?in?enumerate(rank):rank[item]?=?re_rank(rank[item],?item,?user_id,?item_cnt_dict,?user_cnt_dict,?adjust_type=adjust_type)sorted_rank_items?=?sorted(rank.items(),?key=lambda?d:?d[1],?reverse=True)return?sorted_rank_items[0:item_num]User-based
User-based進行推薦時,會將相似用戶的歷史感興趣item推薦給目標用戶。但是這里面的一個問題是,沒有利用到目標用戶本身的行為序列信息。我們做了改進,會計算相似用戶歷史感興趣item和目標用戶本身行為序列中的item之間的相似性,計算相似性時,同樣會利用時間權重和方向權重進行衰減。產生Top-200個召回結果。
def?user_based_recommend(sim_user_corr,?user_item_time_dict,?user_id,?top_k,?item_num,?alpha=15000,item_cnt_dict=None,?user_cnt_dict=None,?adjust_type='v2'):item_content_sim_dict?=?get_glv('item_content_sim_dict')rank?=?{}interacted_items?=?set([i?for?i,?t?in?user_item_time_dict[user_id]])interacted_item_time_list?=?user_item_time_dict[user_id]interacted_num?=?len(interacted_items)min_time?=?min([t?for?i,?t?in?interacted_item_time_list])time_weight_dict?=?{i:?np.exp(alpha?*?(t?-?min_time))?for?i,?t?in?interacted_item_time_list}loc_weight_dict?=?{i:?0.9?**?(interacted_num?-?loc)?for?loc,?(i,?t)?in?enumerate(interacted_item_time_list)}for?sim_v,?wuv?in?sorted(sim_user_corr[user_id].items(),?key=lambda?x:?x[1],?reverse=True)[0:top_k]:if?sim_v?not?in?user_item_time_dict:continuefor?j,?j_time?in?user_item_time_dict[sim_v]:if?j?not?in?interacted_items:rank.setdefault(j,?0)content_weight?=?1.0for?loc,?(i,?t)?in?enumerate(interacted_item_time_list):loc_weight?=?loc_weight_dict[i]time_weight?=?time_weight_dict[i]if?item_content_sim_dict.get(i,?{}).get(j,?None)?is?not?None:content_weight?+=?time_weight?*?loc_weight?*?item_content_sim_dict[i][j]#?weight?=?np.exp(-15000*abs(j_time-q_time))rank[j]?+=?content_weight?*?wuvif?item_cnt_dict?is?not?None:for?loc,?item?in?enumerate(rank):rank[item]?=?re_rank(rank[item],?item,?user_id,?item_cnt_dict,?user_cnt_dict,?adjust_type=adjust_type)rec_items?=?sorted(rank.items(),?key=lambda?d:?d[1],?reverse=True)return?rec_items[:item_num]SR-GNN
我們還對數據進行了增強操作。對每個用戶的交互序列進行截斷,變成多條的交互序列。然后使用模型進行訓練并產出結果。具體使用時,我們使用了兩套參數(原始論文實現+改進版實現)訓練SR-GNN,每套參數對應的模型根據公式(10)各產生Top-100個召回結果,共Top-200個召回結果。
#?Train python3?{sr_gnn_lib_dir}/main.py?--task?train?--node_count?{item_cnt}?\--checkpoint_path?{model_path}/session_id?--train_input?{file_path}/train_item_seq_enhanced.txt?\--test_input?{file_path}/test_item_seq.txt?--gru_step?2?--epochs?10?\--lr?0.001?--lr_dc?2?--dc_rate?0.1?--early_stop_epoch?3?--hidden_size?256?--batch_size?256?\--max_len?20?--has_uid?True?--feature_init?{file_path}/item_embed_mat.npy?--sigma?10?\--sq_max_len?5?--node_weight?True??--node_weight_trainable?True#?Output?Recommendations?????????? python3?{sr_gnn_lib_dir}/main.py?--task?recommend?--node_count?{item_cnt}?--checkpoint_path?{checkpoint_path}?\--item_lookup?{file_path}/item_lookup.txt?--recommend_output?{rec_path}?\--session_input?{file_path}/test_user_sess.txt?--gru_step?2?\--hidden_size?256?--batch_size?256?--rec_extra_count?50?--has_uid?True?\--feature_init?{file_path}/item_embed_mat.npy?\--max_len?10?--sigma?10?--sq_max_len?5?--node_weight?True?\--node_weight_trainable?True在A榜中,單模型的SR-GNN效果已超過4種改進后的CF融合后的效果。
最終,每個用戶產生了1000個召回結果。
粗排方案
粗排階段主要基于這樣的觀察,我們的模型Top 100的hit-rate指標遠高于Top 50,說明可能很多低流行度的物品被我們的模型召回了,但是排序較靠面,因此需要提高低頻商品的曝光率,以消除對高頻商品的偏向性。具體而言,對每個階段進行召回時,本方案會統計「該階段內」的物品出現的頻次,然后根據該頻次以及召回方法計算的item-item相似性分數,對相似性分數進行調整。這是「本方案的key points之一,能夠在基本不影響full的情況下,有效提高half」。不同于其他開源的方案,我們在召回后進行re-rank,而不是在精排后進行re-rank。
本方案考慮加入頻率因素,具體方法包括:
(1) ?首先將頻率作為一個單獨的考量標準,為了初步鑒定頻次的打壓效果并盡量排除其他權重對其干擾,直接將召回分數除以物品出現的頻次,初步鑒定對half有比較明顯的提升,但是會顯著降低full指標。
(2) ?進一步,經過對item頻次進行數據分析,item頻率的分布呈現長尾效應,因此對于這些高頻但極少數的item,考慮使用冪函數削弱打擊的效果。采用的打壓函數分段函數如下所示:
其中,為item出現在目標pahse中的頻次,則新的分數為,。
(3) 考慮到不同活躍度的用戶對于不同頻率的物品的傾向性不同,越活躍的用戶越傾向于點擊低頻的商品,因此對高活躍度的用戶,需要提高高頻item打壓程度;對低活躍度的用戶,提高對于低頻率物品的打壓程度。對于不同用戶進行區分的策略在幾乎不影響ndcg-full同時,有效提高了ndcg-half。
def?re_rank(sim,?i,?u,?item_cnt_dict,?user_cnt_dict,?adjust_type='v2'):''':param?sim:?recall?sim?value:param?i:?item:param?u:?user:param?item_cnt_dict:?item?frequency?map:param?user_cnt_dict:?user?frequency?map:param?adjust_type:?re-rank?strategy,?v0,?v1,?v2:return:'''if?adjust_type?is?None:return?simelif?adjust_type?==?'v1':#?Log,Linear,?3/4,?only?consider?item?frequencyif?item_cnt_dict.get(i,?1.0)?<?4:heat?=?np.log(item_cnt_dict.get(i,?1.0)?+?2)elif?item_cnt_dict.get(i,?1.0)?>=?4?and?item_cnt_dict.get(i,?1.0)?<?10:heat?=?item_cnt_dict.get(i,?1.0)else:heat?=?item_cnt_dict.get(i,?1.0)?**?0.75?+?5.0??#?3/4sim?*=?2.0?/?heatelif?adjust_type?==?'v2':#?Log,Linear,?3/4,?consider?user?activityuser_cnt?=?user_cnt_dict.get(u,?1.0)if?item_cnt_dict.get(i,?1.0)?<?4:heat?=?np.log(item_cnt_dict.get(i,?1.0)?+?2)#?對低活躍度的用戶,提高對于低頻率物品的打壓程度elif?item_cnt_dict.get(i,?1.0)?>=?4?and?item_cnt_dict.get(i,?1.0)?<?10:if?user_cnt?>?50:heat?=?item_cnt_dict.get(i,?1.0)?*?1elif?user_cnt?>?25:heat?=?item_cnt_dict.get(i,?1.0)?*?1.2else:heat?=?item_cnt_dict.get(i,?1.0)?*?1.6#?對高活躍度的用戶,需要提高高頻item打壓程度else:if?user_cnt?>?50:user_cnt_k?=?0.4elif?user_cnt?>?10:user_cnt_k?=?0.1else:user_cnt_k?=?0heat?=?item_cnt_dict.get(i,?1.0)?**?user_cnt_k?+?10?-?10?**?user_cnt_ksim?*=?2.0?/?heatelse:sim?+=?2.0?/?item_cnt_dict.get(i,?1.0)return?sim不同的召回方法得到的分數會經過上述步驟進行分數調整粗排,然后需要將不同召回模型初步融合在一起,我們的方法是,每種方法對「每個用戶」產生的推薦結果先進行打分的最小最大歸一化;然后求和合并不同方法對同一個用戶的打分結果。
「注」:實際上,我們臨近截止日期的時候對召回做了小修改,full指標上升了一些,half下降了一些,導致覆蓋了原本最好的half結果,沒來的及對改進后的召回重排策略進行精排。「最終導致目前線上最終的成績是僅通過上述召回方案得到的」。而在我們所有的提交記錄中,我們最好的half指標的成績是該召回方案和下文即將描述的排序方案產生的。筆者認為,如果對改進后的重排策略進行精排的話,我們的分數應該還會更高。
results精排方案
到目前為止,B榜的最終成績(full rank 3rd, half rank 10th)僅由上文提到的召回+粗排即可得到。精排方案在A榜的時候會有full會有0.05+的提升;half會有0.01+的提升。B榜由于時間問題沒來得及對改進后的召回方案做排序并提交。如果你有興趣可以接著往下閱讀。
精排方案主要由GBDT和DIN方法組成。這里面最重要的步驟來自于訓練樣本的構造和特征的構造。其中,訓練樣本的構造是重中之重,個人認為也是本次比賽「排序階段最大的難點所在」。
訓練樣本構造
排序方案的訓練樣本構造我們采用了序列推薦的典型構造方案,即:滑窗方式構造訓練樣本。為了保證訓練時和線上預測時的數據一致性,我們以行為序列中的1個item為滑窗步長,共滑動了10步。具體步驟即:對每個用戶的行為序列,從倒數第1個item開始,即:為ground truth item, 的通過我們的召回模型來計算item pair相似性,并為第個位置的next-item產生召回結果;滑動窗口往左滑動1步,即:為ground truth item, 的通過我們的召回模型來計算item pair相似性,并為第個位置的next-item產生召回結果;以此類推,共滑動10步。這種方式的缺點在于,計算復雜度非常高。因為每次滑動,都需要進行相似性的計算,并用訓練集中所有的用戶進行召回。目前筆者還不清楚這種方式是否是最優的構造方法(應該不是最優的),希望后面看看其他隊伍的開源開案,學習學習。
def?sliding_obtain_training_df(c,?is_silding_compute_sim=False):print('train_path={},?test_path={}'.format(train_path,?test_path))all_click,?click_q_time?=?get_phase_click(c)#?for?validationcompute_mode?=?'once'?if?not?is_silding_compute_sim?else?'multi'save_training_path?=?os.path.join(user_data_dir,?'training',?mode,?compute_mode,?str(c))click_history_df?=?all_clickrecall_methods?=?{'item-cf',?'bi-graph',?'user-cf',?'swing'}if?not?os.path.exists(save_training_path):?os.makedirs(save_training_path)total_step?=?10step?=?0full_sim_pair_dict?=?get_multi_source_sim_dict_results_multi_processing(click_history_df,recall_methods=recall_methods)pickle.dump(full_sim_pair_dict,?open(os.path.join(save_training_path,?'full_sim_pair_dict.pkl'),?'wb'))step_user_recall_item_dict?=?{}step_strategy_sim_pair_dict?=?{}while?step?<?total_step:print('step={}'.format(step))click_history_df,?click_last_df?=?get_history_and_last_click_df(click_history_df)??#?override?click_history_dfuser_item_time_dict?=?get_user_item_time_dict(click_history_df)if?is_silding_compute_sim:sim_pair_dict?=?get_multi_source_sim_dict_results_multi_processing(click_history_df,recall_methods=recall_methods)??#?re-computeelse:sim_pair_dict?=?full_sim_pair_dictuser_recall_item_dict?=?do_multi_recall_results_multi_processing(sim_pair_dict,?user_item_time_dict,ret_type='tuple',recall_methods=recall_methods)step_user_recall_item_dict[step]?=?user_recall_item_dictif?is_silding_compute_sim:step_strategy_sim_pair_dict[step]?=?sim_pair_dictstep?+=?1pickle.dump(step_user_recall_item_dict,open(os.path.join(save_training_path,?'step_user_recall_item_dict.pkl'),?'wb'))if?is_silding_compute_sim:pickle.dump(step_strategy_sim_pair_dict,open(os.path.join(save_training_path,?'step_strategy_sim_pair_dict.pkl'),?'wb'))#?validation/test?recall?results?based?on?full_sim_pair_dict#?user-cf?depend?on?sim-user?history,?so?use?all-click;?test?user?history?will?not?occur?in?train,?so?it's?okprint('obtain?validate/test?recall?data')if?mode?==?'offline':all_user_item_dict?=?get_user_item_time_dict(all_click)val_user_recall_item_dict?=?do_multi_recall_results_multi_processing(full_sim_pair_dict,all_user_item_dict,target_user_ids=click_q_time['user_id'].unique(),?ret_type='tuple',recall_methods=recall_methods)pickle.dump(val_user_recall_item_dict,open(os.path.join(save_training_path,?'val_user_recall_item_dict.pkl'),?'wb'))構造樣本標簽時,將召回結果中,命中了的用戶真實點擊的item作為正樣本(即:不包括召回結果中未命中,但是用戶真實點擊的item,好處是能夠把「召回分數特征等」送到模型中進行排序),然后隨機負采樣部分item作為負樣本,負樣本的策略以user和item側分別入手,按照比例進行負采樣,最終采樣到的負樣本: 正樣本比例 約等于 10:1。
具體實現時,我們會對每個階段的數據中的所有用戶,分別進行召回并構造樣本和標簽。上述得到的數據格式即:user id, item id, hist item sequence, label,即: 用戶id,目標物品id,用戶歷史交互item序列,標簽。
特征提取
重要的特征主要涉及「召回時的特征」以及「目標item和用戶歷史item之間的各種關聯性」,如內容關聯性、行為關聯性等。
召回特征
召回特征主要包括了:
內容特征
ID特征
這部分特征主要供深度學習模型DIN使用。包括:
比較遺憾的是,本次比賽user側的特征由于缺失值過多,我們沒有花太多時間在user側的特征提取,比如像item側的特征一樣,進行缺失值預測、補全等。
排序模型
排序模型包括了兩個,1個是GBDT,這里我們采用了LightGBM [5] 中的pair-wise方法LightGBMRanker進行排序。另一個是DIN,這里采用了DeepCTR [6] 庫中的DIN實現版本。對于DIN,我們利用了物品的內容特征對item的嵌入進行了初始化;利用用戶歷史行為序列中的item的加權后的興趣向量對user的嵌入進行了初始化。
模型集成
最后,我們將GBDT預測的分數和DIN預測的分數融合起來。具體而言,每個方法的預測概率會先進行user-wise的歸一化操作;然后兩個方法歸一化后預測的概率值進行相加融合。最后按照融合后的分數進行排序,并產生最終的Top 50結果。在A榜的時候,lgb對召回結果對full指標的提升效果大概都在0.02+;但是融合后的LGB+DIN,提升效果可達到0.05+。對half指標的提升略微少了一些,可能原因在于模型過于關注召回得到的sim等特征,對debiasing相關的特征挖掘比較少。
def?norm_sim(sim_df,?weight=0.0):#?print(sim_df.head())min_sim?=?sim_df.min()max_sim?=?sim_df.max()if?max_sim?==?min_sim:sim_df?=?sim_df.apply(lambda?sim:?1.0)else:sim_df?=?sim_df.apply(lambda?sim:?1.0?*?(sim?-?min_sim)?/?(max_sim?-?min_sim))sim_df?=?sim_df.apply(lambda?sim:?sim?+?weight)??#?plus?onereturn?sim_dfdef?ensemble(output_ranking_filename):#?ensemble?lgb+dinlgb_output_file?=?'ranker-'?+?output_ranking_filename?+?'-pkl'#?read?lgblgb_ranker_df?=?pickle.load(open('{}/{}'.format(output_path,?lgb_output_file),?'rb'))lgb_ranker_df['sim']?=?lgb_ranker_df.groupby('user_id')['sim'].transform(lambda?df:?norm_sim(df))#?read?dindin_output_file?=?'din-'?+?output_ranking_filename?+?'-pkl'din_df?=?pickle.load(open('{}/{}'.format(output_path,?din_output_file),?'rb'))din_df['sim']?=?din_df.groupby('user_id')['sim'].transform(lambda?df:?norm_sim(df))#?fuse?lgb?and?dindin_lgb_full_df?=?lgb_ranker_df.append(din_df)din_lgb_full_df?=?din_lgb_full_df.groupby(['user_id',?'item_id',?'phase'])['sim'].sum().reset_index()online_top50_click_np,?online_top50_click?=?obtain_top_k_click()res3?=?get_predict(din_lgb_full_df,?'sim',?online_top50_click)res3.to_csv(output_path?+?'/result.csv',?index=False,?header=None)總結
對本文方案的「key points」作一個總結:
參考文獻
[1] ?Wu S, Tang Y, Zhu Y, et al. Session-based recommendation with graph neural networks[C]//Proceedings of the AAAI Conference on Artificial Intelligence. 2019, 33: 346-353.
[2] ?Gupta P, Garg D, Malhotra P, et al. NISER: Normalized Item and Session Representations with Graph Neural Networks[J]. arXiv preprint arXiv:1909.04276, 2019.
[3] ?Zhou T, Ren J, Medo M, et al. Bipartite network projection and personal recommendation[J]. Physical review E, 2007, 76(4): 046115.
[4] Zhou G, Zhu X, Song C, et al. Deep interest network for click-through rate prediction[C]//Proceedings of the 24th ACM SIGKDD International Conference on Knowledge Discovery & Data Mining. 2018: 1059-1068.
[5] Ke G, Meng Q, Finley T, et al. Lightgbm: A highly efficient gradient boosting decision tree[C]//Advances in neural information processing systems. 2017: 3146-3154.
[6] DeepCTR, Easy-to-use,Modular and Extendible package of deep-learning based CTR models, https://github.com/shenweichen/DeepCTR
[7] A simple itemCF Baseline, score:0.1169, https://tianchi.aliyun.com/forum/postDetail?postId=103530
[8] 改進青禹小生baseline,phase3線上0.2, https://tianchi.aliyun.com/forum/postDetail?postId=105787
[9] 推薦系統算法調研, http://xtf615.com/2018/05/03/recommender-system-survey/
[10] A Simple Recall Method based on Network-based Inference, score:0.18 (phase0-3), https://tianchi.aliyun.com/forum/postDetail?postId=104936
[11] A library for efficient similarity search and clustering of dense vectors, https://github.com/facebookresearch/faiss
[12] CIKM 2019 tutorial: Learning and Reasoning on Graph for Recommendation, https://next-nus.github.io/
[13] Source code and datasets for the paper "Session-based Recommendation with Graph Neural Networks" (AAAI-19), https://github.com/CRIPAC-DIG/SR-GNN
往期精彩回顧適合初學者入門人工智能的路線及資料下載機器學習及深度學習筆記等資料打印機器學習在線手冊深度學習筆記專輯《統計學習方法》的代碼復現專輯 AI基礎下載機器學習的數學基礎專輯獲取一折本站知識星球優惠券,復制鏈接直接打開:https://t.zsxq.com/yFQV7am本站qq群1003271085。加入微信群請掃碼進群:總結
以上是生活随笔為你收集整理的KDD CUP 2020之Debiasing赛道方案 (Rush)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【机器学习基础】数学推导+纯Python
- 下一篇: 【机器学习基础】数学推导+纯Python