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