DeepCtr是一個簡易的CTR模型框架,集成了深度學習流行的所有模型,適合學推薦系統模型的人參考。我在參加比賽中用到了這個框架,但是效果一般,為了搞清楚原因從算法和框架兩方面入手。在讀代碼的過程中遇到一些不理解的問題,所以記錄在這里。
1. DeepFM模型的整體流程
preprocess_input_embedding:
create_singlefeat_inputdict:
搞成Feat是為了整體封裝好,然后輸入到Input的時候可以一一對應
dense和spare直接放入keras的Input,格式是dict{key是feat名字,value是Input層結果}
create_varlenfeat_inputdict
序列是直接用max len放入keras的Input
get_inputs_embedding
create_embedding_dict(embedding層)
稀疏特征:自動指定embeddingsize,6 * int(pow(feat.dimension, 0.25),否則按照指定的embeddingsize,使用L2正則化
序列特征和稀疏特征的流程一樣,封裝Embedding多了mask_zero
結果都是dict{key是feat名字,value是Embedding層結果},處理的是稀疏和序列特征
get_embedding_vec_list(是embedding值)
如果指定是hash,用自己寫的Hash函數將特征的索引(是Input層的結果,也就是原始是數據輸入)轉換成hash函數,如果不是就直接用原始的特征索引。用特征索引和對應的特征在Embedding獲取輸出。
這里只處理sparse
merge_sequence_input
這里處理序列
get_varlen_embedding_vec_dict
hash的時候全部填充0,和之前的區別是之前指定的才填充0
這里和處理的sparse的方式一樣,區別是用sequence_input_dict,但是這個和sparse都是用OrderedDict,區別是用最大長度,名字加seq
get_pooling_vec_list
如果沒有最長長度或者長度序列為空,不填充。在SequencePoolingLayer對序列特征進行pool
把結果加到之前sparse的結果上
返回全部結果
merge_dense_input
把原始的embedding拼接成列向量,加到之前的結果上
如果有線性:
稀疏向量的embedding長度是1,只做融合稀疏和序列,流程和之前一樣
inputs_list是稀疏、稠密、序列和序列長度
返回deep_emb_list, linear_emb_list, dense_input_dict, inputs_list
get_linear_logit
如果有linear embedding,就是dense input經過全連接層之后加linear embedding,否則是dense input經過全連接層之后直接輸出。默認創造linear embedding
embedding拉平放入FM和Deep中,然后是linear+FM+Deep,如果都有的話,有一個加一個
FM:和的平方-平方的和
DNN:默認兩層128*128
最后所有的結果放入PredictionLayer,也就是連一個softmax或者sigmoid
2. 框架優點
- 整體結構清晰靈活,linear返回logit,FM層返回logit,deep包含中間層結果,在每一種模型中打包deep的最后一層,判斷linear,fm和deep是否需要,最后接入全連接層。
- 主要用到的模塊和架構: keras的Concatenate(list轉tensor),Dense(最后的全連接層和dense),Embedding(sparse,dense,sequence),Input(sparse,dense,sequce)還有常規操作:優化器,正則化項
- 復用了重載了Layer層,重寫了build,call,compute_output_shape,compute_mask,get_config
3. 框架缺點
- 給定的參數都是論文提供的參數,實際使用存在問題,都需要自己修改!
- 好多參數沒有留接口,比如回歸問題的loss 是mean_squared_error,只能通過硬寫來修改參數
- 如果想實現自己的模型,復用這個框架,需要了解keras,同時改很多接口,時間代價較大。
運行模型,每次結果不一樣:
這個屬於正常現象,尤其是數據不夠充分的情況下,
主要原因是由於Tensorflow底層的多線程運行機制以及一些具有隨機性的op和random seed導致的。
如果想讓每次運行的結果盡量一致,可以考慮使用CPU運行程序,並且指定單線程運行,同時固定random seed,包括python自身的,Numpy的還有tensorflow的
4. 思考的問題
- 為什么獲取Embedding的時候要hash?為什么是在這個地方hash?
慢慢想! - 為什么linear需要全連接+Embedding,為什么默認為Linear加入Embeding操作?
慢慢想! - FM模型的連續特征是怎么處理的?
- 離散化后在輸入模型,事實上離散化后的模型更適合工業流水線環境。
- embedding一般是表示特征,embedding的話一般不能用來表示連續變量,拿年齡舉例來說就是不能讓10歲和40歲用同一個vector來表示。所以,還是要做one-hot處理的,也可以改動一下FM的結構,在一階部分保留原始的連續特征。
- w&d是將連續值特征轉換成累計分布形式,只針對離散特征去做fm和特征交叉;而它的連續值直接當作embedding向量和離散特征的embedding是拼接起來輸入到神經網絡里面去的。這里DeepFM模型也是這樣做的。
- 關於embedding降維的思考:
比如用戶是50個binary特征,廣告有100個binary特征,那預測用戶是否會點擊某條廣告: 用fm把這些特征都抽象成10維的embedding,而且只做用戶和廣告的特征交叉,那把用戶側的embedding對應元素相加,這就壓縮成10維了,廣告側也這么做,也變成10維了。這時候衡量用戶和廣告的相關性就直接拿這兩個10的向量內積一下。這樣實現降維的目的。 - 序列化embedding的方式是pooling的方式
這是參考youtobe的做法 - FM的embedding size通常設置的不大比如4或者8這個量級,但是在深度學習中一般設置的會相對大,比如32、64,128。這是為什么?
實驗決定embeddingsize,有專家說過embeddingsize對最后的結果影響不大,所以只是一般使用128. - 是否能實現有sparse的DeepFM
為了稀疏輸入,可以直接替換Tensor的類型
ids = tf.SparseTensor(sparse_index, sparse_ids, sparse_shape) values = tf.SparseTensor(sparse_index, sparse_values, sparse_shape)
- 在這個帖子中提到embedding look up性能非常低,沒有解決方案。
https://zhuanlan.zhihu.com/p/39774203 - concat_fun 這里是concat什么?fm的輸入為什么需要concat?
原來是list,每一行是一個tensor,concat之后是tensor,每一行是tensor - tf.keras.layers.Flatten()(fm_input)
原先的embedding輸入是[d,f,k],deep embedding是[d,f*k] - 這里的實現和我的實現不一樣:
我的linear+interact+deep接入全連接層,將所有的特征接入全連接層, 但是根據根據論文和多家的博客來看,我之前理解的是錯誤的,正確的應該是 fm logit+deep logit,最后接全連接層。 同時AFM等多個模型都是這么處理的。
5. 學習到的python知識
- cls:表示類本身,和靜態函數很像,但是比靜態函數多一個功能,就是調用一般的函數。
- namedtuple:表示可訪問屬性的tuple
- isinstance:判斷對象是否是一個已知類型
- 使用__import__ + list 導入需要的包
- new和init的區別:一個是創建,一個是初始化
這里寫框架閱讀寫的有點亂,下次可以按照以下三個要點來記錄:
- 輸入數據的方式
- 對不同數據的處理
- 最后的結合