Tensorflow实现DeepFM(代码分析)
參考:
源碼:https://github.com/ChenglongChen/tensorflow-DeepFM
原文下載:https://arxiv.org/abs/1703.04247
參看原文我們可以發現,deepfm由兩部分組成:FM、deep,兩部分共享一套輸入,下面的介紹也從這兩個方面展開。
1.FM部分
fm部分我的上篇文章https://blog.csdn.net/guanbai4146/article/details/80139806已經介紹,這里簡單提一下。
從上面公式(這里提一下是兩套不同的參數,對應于下面的weights["feature_bias"]和weights["feature_embeddings"])可以看到,原始fm由兩部分組成,一階原始特征+二階特征組合+偏置項,強調的是特征組合的影響,有個點在于如何訓練參數。
1.1 稀疏特征輸入如何訓練參數
首先,數值型特征可以直接輸入模型(除必要的變換操作外),fm主要解決的是稀疏特征的問題,這里指的主要是類別型特征,比如性別。這類特征輸入模型的時候需要做特征展開,也就是一列展開成兩列了。具體可以參考上一篇博客的題外話。
這里要提一下這個點,因為這貫穿整個實現過程。這里性別就是一個field,而展開后的兩列就是feature(下面會用特征域(field)和特征(feature)表示)。
1.2 FM結構
上圖截自原文。
主要分四個部分:sparse feature層、dense embedding層、fm層、輸出層,圖中不同的符號和線段表示了不同的計算邏輯。
雖然在上一篇文章已經介紹了,但這里還需要提一下,fm的特點是引入了隱向量的概念:
假設有三個變量,那么他們交叉就有三個組合,其中是組合特征域的權重參數,這里我直接用特征域來表述,方便理解。如果這個這三個特征域都是類別特征,很容易出現什么現象?
比如:,向量內積為0那么使用梯度下降尋參的時候就沒辦法訓練得到的參數值,原本輸入特征就非常稀疏,導致問題更加嚴峻。所以這里引入隱向量(latent vector)的概念解決稀疏性問題,
具體來說,令(這里都是向量,維度為K,K的大小超參設定),那么即使正交也可以在后面中得到訓練,極大緩解了數據稀疏帶來的問題。
所以,這里的特征參數都會用一個K維的隱向量表示,了解了上述介紹后我們再看怎么實現(以下所有代碼均來自源碼DeepFM文件中,這里對每行代碼做注釋和個人理解的說明):
首先聲明兩個占位符,這些占位符會在訓練的時候由數據填充
# None * F, None * 特征域大小,特征索引占位符 self.feat_index = tf.placeholder(tf.int32, shape=[None, None],name="feat_index") # None * F, 特征值占位符(輸入的樣本數據) self.feat_value = tf.placeholder(tf.float32, shape=[None, None],name="feat_value")這里重點說明這兩個占位符,前面我們知道特征域(field)會被展開拉直成特征(features)集,比如現在有兩個特征域,年齡和性別,會被展開成【年齡、男、女】,對應類別特征值用01表示,看下面例子
| 25 | 男 |
| 26 | 女 |
| 24 | 男 |
?
那么這里field有2個【年齡、性別】,feature有三個值【年齡、男、女】,預處理部分會構建一個特征索引表【年齡:0,男:1,女:2】
先說feat_index數據形式,在預處理中會將原始樣本值處理成索引,所以每個樣本表的索引表就是:
| 年齡 | 性別 |
| 0 | 1 |
| 0 | 2 |
| 0 | 1 |
feat_value就是輸入的樣本值(類別特征數值化,對應的feature上取值設為1):
| 年齡 | 男 | 女 |
| 26 | 1 | ? |
| 26 | ? | 1 |
| 24 | 1 | ? |
注意:這里每行樣本都是2列(和域維度一樣),雖然類別型特征對應的feature不同,比如第一行樣本第二列表示男這個特征,而第二行第二列表示女這個特征。而他們的權重會通過feat_index的每行的索引去關聯。
接下來介紹各層代碼的開發。
1.2.1從sparse feature層到dense embedding層:
# None * F * K 樣本數 * 特征域個數 * 隱向量長,最后得到的embeddings維度(none, none, 8) self.embeddings = tf.nn.embedding_lookup(self.weights["feature_embeddings"], self.feat_index) # feat_value:(?, 39, 1)輸入特征值的維度 feat_value = tf.reshape(self.feat_value, shape=[-1, self.field_size, 1]) # (?, 39, 8) multiply兩個矩陣的對應元素各自相乘(參數權重和對應的特征值相乘)w * x self.embeddings = tf.multiply(self.embeddings, feat_value)注:解釋的時候都以一條樣本輸入說明,代碼注釋中的?和None都表示樣本數
第一行解釋:
self.weights["feature_embeddings"]是特征的權重參數,注意這里是特征的權重,參看之前那篇文章關于fm和ffm區別的敘述中,原始的fm模型對每個特征都是有一個特征權重的(注意是特征不是特征域)。具體定義如下 # feature_size:特征長度,embedding_size:隱向量長度 weights["feature_embeddings"] = tf.Variable(tf.random_normal([self.feature_size, self.embedding_size], 0.0, 0.01),name="feature_embeddings") embedding_lookup(params, ids)表示按后面ids從params里面選擇對應索引的值,也就是說從按照特征索引(feat_index)從特征權重集(weights[feature_embeddings])中選擇對應的特征權重。仍然以上面的例子說明,weights[feature_embeddings]就是【年齡(0)的權重,男(1)的權重,女(2)的權重】,以第一個樣本索引【0,1】為例,就是選出了【年齡(0)的權重,男(1)的權重】
第二到三行解釋:
第二行主要是二維向三維的轉換,變成:樣本數 * 特征域數 * 1,以一條樣本為例就是上面的feat_value的第一行【25,0】
第三行是矩陣對應元素相乘,也就是每個特征權重乘以樣本特征值,仍然以第一條樣本為例就是【25 * 年齡的權重,0 * 男的權重】
從圖中也能看出這兩層之間也就是權重連接的關系,維度大小并沒有變化。
1.2.2 FM層:
從結構圖中我們也可以看到fm的數據源來自前面兩層,sparse和dense都有數據輸入。
從前文和公式我們也能知道,fm分為一階項()和二階項()兩部分,
先說一階項:
# None * 特征域數 * 1 self.y_first_order = tf.nn.embedding_lookup(self.weights["feature_bias"], self.feat_index) # None * 特征域數 axis=2最內層元素維度的加和 self.y_first_order = tf.reduce_sum(tf.multiply(self.y_first_order, feat_value), 2) # None * 特征域數 # 第一層dropout self.y_first_order = tf.nn.dropout(self.y_first_order, self.dropout_keep_fm[0])第一行和上面類似,就是選擇對應輸入特征的權重值(中的),不做贅述,其中weights["feature_bias"]的定義如下:
weights["feature_bias"] = tf.Variable(tf.random_uniform([self.feature_size, 1], 0.0, 1.0), name="feature_bias")接下來第二行和第三行就是做對應的運算()和添加dropout層。
二階項:
# sum_square part 和平方,None * K 就是w_1 * x_1... + w_n * x_n... self.summed_features_emb = tf.reduce_sum(self.embeddings, 1) self.summed_features_emb_square = tf.square(self.summed_features_emb) # None * K 所有特征加和平方 (w_1 * x_1 + w_n * x_n...)^2# square_sum part 平方和 self.squared_features_emb = tf.square(self.embeddings) self.squared_sum_features_emb = tf.reduce_sum(self.squared_features_emb, 1) # None * K# second order (和平方-平方和) / 2對應元素相減,得到(x_1 * y_1 + ... + x_n * y_n)二階特征交叉,二階項本質是各向量交叉后求和的值,所以維度是K self.y_second_order = 0.5 * tf.subtract(self.summed_features_emb_square, self.squared_sum_features_emb) # None * K self.y_second_order = tf.nn.dropout(self.y_second_order, self.dropout_keep_fm[1]) # None * K fm第二層的dropout代碼就是實現下面的計算邏輯:
2. Deep部分
模型結構如下:
原始的deepfm深度部分使用的就是普通的dnn結構,如下代碼:
# None * (F*K) (樣本數, 特征域長度 * 隱向量長度) self.y_deep = tf.reshape(self.embeddings, shape=[-1, self.field_size * self.embedding_size]) self.y_deep = tf.nn.dropout(self.y_deep, self.dropout_keep_deep[0]) for i in range(0, len(self.deep_layers)):self.y_deep = tf.add(tf.matmul(self.y_deep, self.weights["layer_%d" %i]), self.weights["bias_%d"%i]) # None * layer[i] * 1 (wx+b)if self.batch_norm: # 對參數批正則化self.y_deep = self.batch_norm_layer(self.y_deep, train_phase=self.train_phase, scope_bn="bn_%d" %i) # None * layer[i] * 1self.y_deep = self.deep_layers_activation(self.y_deep) # 激活層# dropout at each Deep layer 每層都有一個dropout參數self.y_deep = tf.nn.dropout(self.y_deep, self.dropout_keep_deep[1+i])首先將dense層輸出進行reshape維度變換(將特征域對應的向量拉直平鋪),添加dropout層
深度結構一共兩層,循環部分分別添加以下步驟:、批正則化(可選)、激活層、dropout層,正常的dnn結構。
上面使用的權重參數(),聲明如下:
# num_layer:dnn層數 num_layer = len(self.deep_layers) # 特征域個數 * 隱向量長度 input_size = self.field_size * self.embedding_size # 標準差glorot設定參數的標準,標準差=sqrt(2/輸入維度+輸出維度) glorot = np.sqrt(2.0 / (input_size + self.deep_layers[0])) # layer_0是和dense層輸出做計算,所以參數維度是[特征域個數*隱向量長] weights["layer_0"] = tf.Variable(np.random.normal(loc=0, scale=glorot, size=(input_size, self.deep_layers[0])), dtype=np.float32) weights["bias_0"] = tf.Variable(np.random.normal(loc=0, scale=glorot, size=(1, self.deep_layers[0])), dtype=np.float32) # 1 * layers[0]for i in range(1, num_layer):glorot = np.sqrt(2.0 / (self.deep_layers[i-1] + self.deep_layers[i]))# layer_1第2層 32*32, layers[i-1] * layers[i]weights["layer_%d" % i] = tf.Variable(np.random.normal(loc=0, scale=glorot, size=(self.deep_layers[i-1], self.deep_layers[i])), dtype=np.float32)# 1 * layer[i]weights["bias_%d" % i] = tf.Variable(np.random.normal(loc=0, scale=glorot, size=(1, self.deep_layers[i])), dtype=np.float32)按空行分界,先說空行上半部分
deep_layer列表存儲的是兩層dnn的輸出維度,glorot是參數初始化的一種理論,這里不贅述。
深度部分的輸入就是dense embedding層的輸出,第一層計算的時候進行了reshape拉直處理,所以參數的輸入大小(input_size)=特征域個數 * 隱向量長度,這里就是分別聲明權重和偏置項。
再說空行下半部分,這里聲明的時候range是從1開始的,所以這里是從dnn的第二層開始聲明對應的參數變量(,每一層輸出維度的超參存儲在deep_layer中)。
以上,fm部分和dnn部分都已經解釋清楚了,接下來就是收尾。
3.FM和Deep的組合收尾
先看下整體的結構圖(以上結構圖均截自原文):
最后這部分就是將fm和deep部分組合到一起,也就是低階特征和高階特征的組合,最后計算輸出,先看下代碼:
# fm的一階項、二階項、深度輸出項拼接,變成 樣本數 * 79 concat_input = tf.concat([self.y_first_order, self.y_second_order, self.y_deep], axis=1) # 最后全連接層權重 * 輸出 + 偏置 self.out = tf.add(tf.matmul(concat_input, self.weights["concat_projection"]), self.weights["concat_bias"])第一行就是一階項+二階項(fm部分)、deep部分拼接(拉直鋪平)
第二行就是最后的全連接層,計算邏輯就是。至此,deepfm結構實現完畢。
4.總結
deepfm已經在工業界得到了普遍的應用,寫這篇分享的目的也是希望自己能夠從理論到實踐有一個更深入的理解,不當之處還望多多指正。
總結
以上是生活随笔為你收集整理的Tensorflow实现DeepFM(代码分析)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Armijo条件,Wolfe条件,Gol
- 下一篇: 个人信息保护相关的重要法规及规范性文件汇