[深度学习] DeepFM 介绍与Pytorch代码解释
一. DeepFM算法的提出
由于DeepFM算法有效的結(jié)合了因子分解機與神經(jīng)網(wǎng)絡(luò)在特征學(xué)習(xí)中的優(yōu)點:同時提取到低階組合特征與高階組合特征,所以越來越被廣泛使用。
在DeepFM中
- FM算法負責(zé)對一階特征以及由一階特征兩兩組合而成的二階特征進行特征的提取
- DNN算法負責(zé)對由輸入的一階特征進行全連接等操作形成的高階特征進行特征的提取
具有以下特點:
二. DeepFM算法結(jié)構(gòu)圖
算法整體結(jié)構(gòu)圖如下所示:
DeepFM的輸入可由連續(xù)型變量和類別型變量共同組成,且類別型變量需要進行One-Hot編碼。而正由于One-Hot編碼,導(dǎo)致了輸入特征變得高維且稀疏。應(yīng)對的措施是:針對高維稀疏的輸入特征,采用Word2Vec的詞嵌入(WordEmbedding)思想,把高維稀疏的向量映射到相對低維且向量元素都不為零的空間向量中。
DeepFM包含兩部分:因子分解機部分與神經(jīng)網(wǎng)絡(luò)部分,分別負責(zé)低階特征的提取和高階特征的提取。這兩部分共享同樣的嵌入層輸入。DeepFM的預(yù)測結(jié)果可以寫為:
通過嵌入層,盡管不同field的長度不同(不同離散變量的取值個數(shù)可能不同),但是embedding之后向量的長度均為K(我們提前設(shè)定好的embedding-size)。
DeepFM的各模塊共享同一輸入,輸入是由各個field的Onehot編碼橫向拼接而成的高維稀疏向量。
首先,原始輸入的各個field經(jīng)過加權(quán)(實際上是Embedding為1維)后,求和可得一次項;
其次,原始輸入的各個field(不同長度)的Embedding(等長,k維latent vector),一方面兩兩內(nèi)積,然后求和可得二次項,另一方面作為輸入全連接到DNN。
算法整體結(jié)構(gòu)圖來源于paper原文,看起來比較復(fù)雜。那看一下網(wǎng)友的簡略圖:
FM部分的結(jié)構(gòu):
FM 部分的輸出如下:
這里需要注意三點:
DNN部分的結(jié)構(gòu)
這里DNN的作用是構(gòu)造高維特征,且有一個特點:DNN的輸入也是embedding vector。所謂的權(quán)值共享指的就是這里。
關(guān)于DNN網(wǎng)絡(luò)中的輸入a處理方式采用前向傳播,如下所示:
這里假設(shè)a(0)=(e1,e2,...em) 表示 embedding層的輸出,那么a(0)作為下一層 DNN隱藏層的輸入,其前饋過程如下。
1)不管每個Field的特征是否相同,但embedding后其特征向量維度均為k
2)利用FM中隱向量Vi來作為子網(wǎng)絡(luò)的初始化權(quán)重來獲得Field的隱向量。
三.代碼相關(guān)部分說明
1.對1個feature的稀疏向量進行緊湊化處理
首先是Sparse Vector到 Dense Vector的Embedding層。
前置知識是離散特征的向量化
#假設(shè)有特征gendervalues = (male,female,trans)#one-hot之后會得到長為3的vector#e.g. v = [0,1,0]#如果取值有1萬種,那么len(v)=10^4,而這么長的vector里面只有一個1#我們希望把它壓縮到一個比較親近人類的長度,e.g.300#于是有feature_size = 10^4embedding_size= 300#構(gòu)建embedding層import torchimport torch.nn as nnembd_layer = nn.Embedding(feature_size, embedding_size)
2.從1個feature擴展到n個features
#現(xiàn)在我們有n個features#每個feature的取值分別有feature_size種,存在一個list中。#偽代碼features = [age,gender,work]feature_sizes = []for feature in features:feature_size=len(feature)feature_sizes.append(feature_size)#于是我們可以求相關(guān)參數(shù)#在FM算法中,作者把一個字段作為一個field,于是我們可以根據(jù)傳入的參數(shù),知道有多少個fieldn_fields = len(feature_sizes)#再對每一個field都建立embeddingembedding_size = 300embd_layers = nn.ModuleList([nn.Embedding(feature_size, embedding_size) for feature_size in self.feature_sizes])#我們希望每一種feature,embd之后的size都是相同的,所以這里統(tǒng)一使用了300。
完成之后我們得到了更加緊湊的dense feature,注意這個dense_feature是embedding層的輸出的水平拼接。
所以可以算得其 dim = n個field * 每個300維? = 300*n 維?
3.Deep部分 把dense_feature傳入神經(jīng)網(wǎng)絡(luò)
#把300*n維的維度信息提出來
dense_dim = n_fields * embedding_size#設(shè)定兩個全連接層的維度,簡便起見假設(shè)它們維度相同
fc_dim = 100#現(xiàn)在我們要丟dense_feature進去self.fc_layer1 = nn.Sequential(nn.Linear(dense_dim,fc_dim),nn.BatchNorm2d(fc_dim),nn.LeakyReLU(0.2, inplace=True)
)self.fc_layer2 = nn.Sequential(nn.Linear(fc_dim,fc_dim),nn.BatchNorm2d(fc_dim), nn.LeakyReLU(0.2, inplace=True)
)
進行的是如下圖紅色這部分:
4.FM部分,根據(jù)sparse feature計算二階項的預(yù)測值
對每一個field,都有一個(feature_size, embedding_size)的參數(shù)矩陣。還是以gender為例,這個矩陣shape=(3,300)
默認你已經(jīng)自學(xué)完成了理論部分。
那么這就是參數(shù)共享了,因為形狀一樣,所以可以用embd層的參數(shù)矩陣,去代替FM部分隱因子的參數(shù)矩陣。
現(xiàn)在,我們的問題就是,如何用代碼來表達推導(dǎo)公式:
備注:原論文的版本,把小k換成小f,網(wǎng)上流傳的也多是遵循原版使用字符f。字母不影響達意。
我們把這個式子分為兩部分。
原式 = FM1-FM2
1. FM1計算方法
?容易知道,v'x的每一行都是 k=f 時的
例如v'x的第1行是k=1時的上式。而v'x的shape = (k,1),是一個k維的列向量,共含有k個元素。
顯然我們只需要對v'x的k個元素,逐個平方,再sum,接著乘1/2,即可得到FM1。
2. FM2計算方法
如果你用過matlab之類的東西,應(yīng)該知道點運算。 ^2表示逐元素取平方。
?顯然(v^2)'·(x^2) 的每一行的,都是k=f時的
我們只需要對(v^2)'·(x^2) 的每行sum加和,接著乘1/2,即可得到FM2。
這就是FM因子分解機理論上的計算方式
3. 優(yōu)化計算方法
然而這樣做的操作量很大,每個矩陣大小=n*k = feature_size * embedding_size
又考慮到,我們輸入的特征向量x是稀疏的!
雖然shape=(n,1),但是n個元素里面只有1個1,其他(n-1)個都是0
那么,假設(shè)我們知道那個1的下標值,例如我們知道xi=1,而其他的 xj = 0?, ( j=1,2,3,...n, j ≠?i )。
我們的整個計算過程,就可以只關(guān)注vi ,shape = (1,k),和 xi , shape = (1,1)
而xi=1,怎么乘都不影響vi的值,所以進一步的,我們只需要關(guān)注vi。
這樣計算值就極大地縮減了。
(當然,為了讓模型具備泛化能力,我們先不假定xi=1,而是假定xi等于某個不為0的數(shù)字。)
于是,最終我們需要的結(jié)果就轉(zhuǎn)變成了,vi·xi ,xi是一個很可能為1的數(shù)字,vi是(1,k)行向量。
這就是為什么在廣告CTR等領(lǐng)域你才能看到FM理論的大量應(yīng)用,因為這些地方的特征都是極為稀疏的。
【特征的特征決定了我們選取的處理特征的理論】
而上文說過,由于我們強行使用參數(shù)共享,所以 v=embedding矩陣,shape=(feature_size, embedding_size)。
import torch import torch.nn as nn# 先建立一個簡單的embd層, embedding_size = 5 feature_size = 3 embd_layer = nn.Embedding(feature_size, embedding_size)# 如果對第n個field,我們知道一個下標i,使得xi為當前field的唯一非0值。 idx = torch.LongTensor([1]) # 在pytorch中,我們可以這樣取出vi vi = embd_layer(idx) # shape = (1,embedding_size) =(1,k) ''' >>> vi.shape torch.Size([1, 5]) >>> vi tensor([[-1.1124, -0.7023, -0.8200, 1.6422, 0.2136]],grad_fn=<EmbeddingBackward>) >>> '''# ---------------- # 如果對第n個field,我們有m個樣本的下標構(gòu)成一個tensor batch_idx = torch.LongTensor([0, 1, 2]) # shape=(m) batch_vis = embd_layer(batch_idx) # shape=(m,k) ''' >>> batch_vis.shape torch.Size([3, 5]) >>> batch_vis tensor([[-0.9178, 1.1880, 0.9566, -0.9948, 0.9113],[-1.1124, -0.7023, -0.8200, 1.6422, 0.2136],[-0.4018, -0.0844, 0.9122, -1.7312, 1.5130]],grad_fn=<EmbeddingBackward>)'''# 再有m個樣本的對應(yīng)xi的值 batch_xis = torch.FloatTensor([1, 1, 1]) # shape=(m)# 我們讓每個樣本的vi與對應(yīng)的xi相乘 res = (batch_vis.t()) * batch_xis # (k,m)*(m)=(k,m) res = res.t() # shape=(m,k)# 由前面的討論知道,此時res的每行, # 都是第m個樣本在第n個field的(v'x)'=vixi,即長度為k的行向量 # [vi1xi,vi2xi,...,vikxi]我們解決了一個field內(nèi)m個sample的問題,現(xiàn)在可以把問題擴展到全部n個field了。
繼續(xù),我們要把n個field的vixi全部算出來。
import torchimport torch.nn as nn#對n個field搭建modeln_field = 4 #有4個字段 feature_sizes= [5,3,6,4] #每個字段向量化后的長度分別為5,3,6,4embedding_size = 30 #希望以k=30來表達這些向量n_embds = nn.ModuleList([nn.Embedding(feature_sizes[i], embedding_size) for i in range(n_fileds)])#從n_field個字段內(nèi)提取唯一的非零值對應(yīng)的vixi#假設(shè)有一個存儲idx的文件,m個樣本在n個field對應(yīng)的非零項的下標#shape=(m_samples,n_field)train_idx = read_from_file('xx.file') #再有m個樣本在n個field對應(yīng)的非零項xi的值,shape=(m_samples,n_field)train_values = read_from_file('xxxxx.file') results = []for i , module in enumerate(n_embds):#當前為 i_th_fieldbatch_idx = torch.Tensor(train_idx[:,i]) #shape=(m)batch_vis = module(batch_idx) #shape=(m,k)batch_xis = torch.FloatTensor(train_values[:,i]) #shape=(m)#我們讓每個樣本的vi與對應(yīng)的xi相乘res = (batch_vis.t())*train_values #(k,m)*(m)=(k,m)res = res.t() #shape=(m,k)results.append(res)#由前面的討論知道,此時res的每行,#都是第m個樣本在第n個field的(v'x)'=vixi,即長度為k的行向量#[vi1xi,vi2xi,...,vikxi]現(xiàn)在我們得到了一個results列表。
需要用它來算FM1和FM2。
觀察公式,對FM1,我們需要固定K不動,先sum一輪vikxi,然后逐項平方。最后逐K累加。
對FM2,我們需要固定K不動,先平方vikxi,再sum。最后逐個K累加。
先看FM1,以K為軸,先固定K不動。
(1)以n為維度sum一次。其實就是把results這個 list of tensor里面的每一個shape相同的tensor ,在相同位置上全部加起來。
fm1_step1 = sum(results) #sum(n*[m,k]) -> (m,k),此處必須使用python原生sum()(2)然后逐項平方。
fm1_step2 = fm1_step1*fm1_step1 #(m,k)*(m,k)=(m,k)(3)最后逐K累加。
fm1 = torch.sum(fm1_step2,dim=1) #sum([m,k] ,dim=1) -> (m)再看FM2,同樣以K為軸,先固定K不動。
(1)先進行逐項平方
fm2_step1= [item*item for item in results] #list of n*(m,k)(2)再以n為維度sum一次。
fm2_step2 = sum(fm2_step1) #sum(n*[m,k]) -> (m,k),此處必須使用python原生sum()(3)最后逐K累加。
fm2 = torch.sum(fm2_step2,dim=1) #sum([m,k],dim=1) -> (m)最后兩者相減,乘系數(shù)即可。
quadratic_term = (fm1-fm2)*0.5至此,我們得到了最后一個二次項。
但是還少了線性部分。
linear_part = nn.ModuleList( [ nn.Linear(feature_sizes[i]),1) for i in range(n_field)] )現(xiàn)在FM部分被徹底搞定了,我們看看進度條。
5.合并輸出,計算loss
import torch.nn.functional as F#設(shè)deep部分的輸出為 deep_out
total_out = deep_out + quadratic_term #shape=(m,1)#根據(jù)任務(wù)不同,這里的輸出可以做不同的處理。比如點擊率預(yù)測就是分類問題了。
#但是我當前的需求是數(shù)值預(yù)測,所以直接拿來用#隨便選一個損失函數(shù),或者自定義
loss_func = F.binary_cross_entropy_with_logits#優(yōu)化器
optimizer = torch.optim.SGD(self.parameters(), lr=self.learning_rate, weight_decay=self.weight_decay)
#optimizer = torch.optim.Adam(self.parameters(), lr=self.learning_rate, weight_decay=self.weight_decay)
#optimizer = torch.optim.RMSprop(self.parameters(), lr=self.learning_rate, weight_decay=self.weight_decay)
#optimizer = torch.optim.Adagrad(self.parameters(), lr=self.learning_rate, weight_decay=self.weight_decay)optimizer.zero_grad()
loss = loss_func(total_out,train_y)
loss.backward()
optimizer.step()
四 DeepFM中Field和特征的理解
論文中的特征就是one-hot編碼之后的特征,而Field就相當于一個列,舉個例子,比如在FM所展現(xiàn)的例子所示:
“中國機長”,“陳情令”,“大秦帝國”就是三個特征,但他們屬于同一個Field,就是“節(jié)目名”。
正如論文中所說,Field可以分為分類和連續(xù)兩種類型,分類類型要經(jīng)過one-hot編碼,而連續(xù)類型就是自己本身的數(shù)值value。這樣,每個樣本都可以表示為
其中如果field<j>是category類型的,那么通過one-hot編碼,就是個高維稀疏的矩陣,每一列都是一個特征;若是連續(xù)類型的Field就是它的特征,其value為原來的特征值,如上圖中的“喜愛程度”。
首先要明確數(shù)據(jù)的格式應(yīng)該是怎樣的。
我們假設(shè)現(xiàn)在要解決的問題是一個CTR預(yù)估問題,數(shù)據(jù)集是 (X,y),每一個樣本都是高度稀疏的高維向量。假設(shè)我們有兩種 field 的特征,連續(xù)型和離散型,連續(xù)型 field 一般不做處理沿用原值,離散型一般會做One-hot編碼。離散型又能進一步分為單值型和多值型,單值型在Onehot后的稀疏向量中,只有一個特征為1,其余都是0,而多值型在Onehot后,有多于1個特征為1,其余是0。
下面給出一個兩個樣本的例子,其中shop_score是連續(xù)型field,gender是單值離散型field,interest是多值離散型field。可以看到shop_score的取值是實數(shù),gender的取值是離散值,interest的取值是離散值序列。
對各field進行Onehot后,可見單值離散field對應(yīng)的獨熱向量只有一位取1,而多值離散field對應(yīng)的獨熱向量有多于一位取1,表示該field可以同時取多個特征值。
進一步,我們對每個field中的特征取值分別單獨編碼或聯(lián)合編碼,則確定了特征的index,這在libsvm數(shù)據(jù)格式中是需要
libsvm格式:
可見,連續(xù)field和單值field對樣本長度的貢獻恒定為1,但多值離散型field可能會導(dǎo)致樣本長度不一樣。對不定長樣本的處理方法自然是padding補零了
可以選擇對每個多值field分別進行padding,原因有二。
首先,若對樣本整體進行padding,萬一想要進行截斷,可能會截掉某些連續(xù)field和單值field,分別padding則可以分別截斷,而不影響其他的field。
第二,對每個field的不同特征單獨編碼互不影響,不需要維護一個全局的字典,每次只需要處理一個field的特征,甚至可以實現(xiàn)并行處理以及節(jié)省內(nèi)存的特征Encoding方案。
FM所需的數(shù)據(jù)格式正是libsvm格式,既需要數(shù)值本身(Value),也需要特征取值在字典中的index(ID)。
假如我們采用對每個field的不同特征取值單獨編碼的方式,則可以實現(xiàn)一些簡便性優(yōu)化。首先,數(shù)值型field的ID永遠是1,因此可以省略ID;第二,單值離散型field的Value永遠是1,因此可以省略Value;第三,多值離散型field可以用padding+masking的方式省略ID。
?
用Embedding實現(xiàn) FM一次項 ∑wixi
如上所述,我們的輸入數(shù)據(jù)有三種field,在One-hot處理后代入FM一次項的公式運算。每個field各有一個權(quán)值向量w,連續(xù)型field的w長度為1,離散型field的w長度為特征的取值個數(shù)。
??首先,連續(xù)型field對一次項的貢獻等于自身數(shù)值乘以權(quán)值w,可以用Dense(1)層實現(xiàn),任意個連續(xù)型field輸入到同一個Dense層即可,因此在數(shù)據(jù)處理時,可以先將所有連續(xù)型field拼成一個大矩陣,同時如上所述,ID可以省略。
??其次,單值離散型field根據(jù)樣本特征取值的index,從w中取出對應(yīng)權(quán)值(標量),由于離散型特征值為1,故它對一次項的貢獻即取出的權(quán)值本身。取出權(quán)值的過程稱為 table-lookup,可以用Embedding(n,1)層實現(xiàn)(n為該field特征取值個數(shù))。若將所有單值離散型field的特征值聯(lián)合編碼,則可使用同一個Embedding Table進行l(wèi)ookup,不需要對每個field單獨聲明Embedding層。因此在數(shù)據(jù)處理時,可以先將所有單值離散型field拼起來并聯(lián)合編碼,同時如上所述,Value可以省略,只關(guān)心lookup出來的權(quán)值w
即可。
??最后,多值離散型field可以同時取多個特征值,為了batch training,必須對樣本進行補零padding。相似地可用Embedding層實現(xiàn),Value并不是必要的,但Value可以作為mask來使用,當然也可以在Embedding中設(shè)置mask_zero=True。
??如下圖所示,假設(shè)我們有m個連續(xù)型field,n個單值離散型field,q個多值離散型field,每個多值離散型field的最長長度為Li(i=1,2,?,q)
用Embedding實現(xiàn)FM二次項 ∑∑(vi?vj)?xixj
?由于FM的二次項是不同特征之間的交叉(一般是不同field之間的交叉),不能分field實現(xiàn),必須將每個field輸入Embedding后拼接起來,再求二次項。
實現(xiàn)一個DNN
DNN從FM二次項倒數(shù)第二步生成的 None*F*K Embedding 張量開始,先用Flatten層平鋪,然后經(jīng)過若干層神經(jīng)網(wǎng)絡(luò),每一層后面可以加上dropout防止過擬合和BatchNormalization加速收斂。
Keras例子:
import numpy as np from keras.layers import * from keras.models import Model from keras import backend as K from keras import optimizers from keras.engine.topology import Layer# 樣本和標簽,這里需要對應(yīng)自己的樣本做處理 train_x = [np.array([0.5, 0.7, 0.9]),np.array([2, 4, 6]),np.array([[0, 1, 0, 0, 0, 1, 0, 1], [0, 1, 0, 0, 0, 1, 0, 1],[0, 1, 0, 0, 0, 1, 0, 1]]) ]label = np.array([0, 1, 0]) # 輸入定義 continuous = Input(shape=(1,), name='single_continuous') single_discrete = Input(shape=(1,), name='single_discrete') multi_discrete = Input(shape=(8,), name='multi_discrete')# FM 一次項部分 continuous_dense = Dense(1)(continuous) single_embedding = Reshape([1])(Embedding(10, 1)(single_discrete)) multi_dense = Dense(1)(multi_discrete) # 一次項求和 first_order_sum = Add()([continuous_dense, single_embedding, multi_dense])# FM 二次項部分 k=3 continuous_k = Dense(3)(continuous) single_k = Reshape([3])(Embedding(10, 3)(single_discrete)) multi_k = Dense(3)(multi_discrete)# 先相加后平方 sum_square_layer = Lambda(lambda x: x ** 2)(Add()([continuous_k, single_k, multi_k])) # 先平方后相加 continuous_square = Lambda(lambda x: x ** 2)(continuous_k) single_square = Lambda(lambda x: x ** 2)(single_k) multi_square = Lambda(lambda x: x ** 2)(multi_k) square_sum_layer = Add()([continuous_square, single_square, multi_square])substract_layer = Lambda(lambda x: x * 0.5)(Subtract()([sum_square_layer, square_sum_layer]))# 定義求和層 class SumLayer(Layer):def __init__(self, **kwargs):super(SumLayer, self).__init__(**kwargs)def call(self, inputs):inputs = K.expand_dims(inputs)return K.sum(inputs, axis=1)def compute_output_shape(self, input_shape):return tuple([input_shape[0], 1])# 二次項求和 second_order_sum = SumLayer()(substract_layer) # FM 部分輸出 fm_output = Add()([first_order_sum, second_order_sum]) # deep 部分 deep_input = Concatenate()([continuous_k, single_k, multi_k]) deep_layer_0 = Dropout(0.5)(Dense(64, activation='relu')(deep_input)) deep_layer_1 = Dropout(0.5)(Dense(64, activation='relu')(deep_layer_0)) deep_layer_2 = Dropout(0.5)(Dense(64, activation='relu')(deep_layer_1)) deep_output = Dropout(0.5)(Dense(1, activation='relu')(deep_layer_2)) concat_layer = Concatenate()([fm_output, deep_output]) y = Dense(1, activation='sigmoid')(concat_layer) model = Model(inputs=[continuous, single_discrete, multi_discrete], outputs=y) Opt = optimizers.Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=None, decay=0.0, amsgrad=False) model.compile(loss='binary_crossentropy',optimizer=Opt,metrics=['acc']) model.fit(train_x,label,shuffle=True,epochs=1,verbose=1,batch_size=1024,validation_split=None)參考資料
- 論文
- FFM及DeepFFM模型在推薦系統(tǒng)的探索
- 深度推薦模型之DeepFM
-
DeepFM算法解析及Python實現(xiàn)
-
deepFM in pytorch
-
用Keras實現(xiàn)一個DeepFM
總結(jié)
以上是生活随笔為你收集整理的[深度学习] DeepFM 介绍与Pytorch代码解释的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 造成服务器无法正常运行的原因有哪些
- 下一篇: [深度学习]CTR模型如何加入稠密连续型