推薦算法-AFM
推薦算法-AFM,這篇文章也是在FM的基礎上做工作。這篇文章是針對特徵之間組合時,不同的特徵都是用同樣的向量去做。即每一個特徵和其它的特徵進行組合時,都是採用同一個向量,缺乏不同特徵之間的關聯性不同,應該採用不同的向量。解決這個問題的一個思路就是FFM,即每一個特徵針對每一個的field生成一個特徵向量,即在進行特徵組合時,採用不同的向量表示去做。本文是解決這個問題的另一種思路,也就是對不同的特徵組合賦予不同的權值,而且這個權值是可學習的,體現了模型對不同的特徵組合的關注度不同。我的理解是對與最後的分類貢獻程度較高的特徵組合,會賦予較高的權值。
說到Attention,在cv、nlp、推薦這三個領域都有使用,可見Attention的作用之大。有人在cv領域使用Attention,可以找出深度模型作出最後判決所依據的區域,在nlp領域通過Attention可以找出關鍵詞。在推薦中,可以爲不同的特徵組合賦予權值。總體的思路就是模型對不同的特徵組合關注度是不同的。
AFM網絡結構
從上圖中可以看出,前半部分AFM的模型結構和NFM的基本一致,都是先把特徵映射到固定的長度,然後計算FM部分,即上面的Pair-wise Interaction Layer部分。但是後面的就不一樣了,NFM是直接在FM後面接入了一個NN層,而AFM是生成了一個Attention-based Pooling,即生成一組權重向量,然後再和Pair-wise部分對應相乘。AFM沒有繼續對這部分特徵進行深層神經網絡的學習,或許引入NN會進一步提升模型的精度。
FM的公式大家應該還記得,這裏先回歸一下:
attention就是對上面部分進行加權。最終AFM的公式表達爲:
相比於原始的FM公式,AFM部分多了一組權重係數和向量。我們只需要知道這兩個數據的表達式,就可以實現AFM了。
,
上面這兩個公式表達了的計算方式,第一個公式中又引入了參數,因爲兩個特徵相乘之後得到的特徵長度不變,仍爲embeddingSIze,w是要根據這部分特徵計算attention部分的權重,因此w的大小爲 K爲embedding部分的長度,A爲attention部分的長度,部分的長度也爲。第二個公式其實就是softmax表達式。其實最後就是對個特徵組合分別賦予權重,即上面的長度爲pairs。
AFM模型構建
模型構建部分就比較簡單了,其實就是先把embedding部分的權重構建出來,然後再把Attention的權重構建出來就可以了。
&emsp 權重構建,embedding部分就是[featureSIze, embeddingSize],以及一次項部分[featureSize,1]。接下來就是Attention部分的權重了,Attention部分的參數有,他們的shape分別爲:
然後我們可以構建權重了:
def _initWeights(self):
weights = dict()
# embedding
weights['feature_embedding'] = tf.Variable(tf.random_normal(shape=[self.featureSize, self.embeddingSize],
mean=0.0, stddev=1.0), name='feature_embedding')
weights['feature_bias'] = tf.Variable(tf.random_normal(shape=[self.featureSize, 1], mean=0.0, stddev=1.0),
name='feature_bias')
weights['bias'] = tf.Variable(tf.constant(0.1), name='bias')
# attention
# w: K * A
# b: A
# h: A
# p: K * 1
weights['attention_w'] = tf.Variable(tf.random_normal(shape=[self.embeddingSize, self.attentionSize], mean=0.0,
stddev=1.0), name='attention_w')
weights['attention_b'] = tf.Variable(tf.random_normal(shape=[self.attentionSize, ], mean=0.0, stddev=1.0),
name='attention_b')
weights['attention_h'] = tf.Variable(tf.random_normal(shape=[self.attentionSize, ], mean=0.0, stddev=1.0),
name='attention_h')
weights['attention_p'] = tf.Variable(tf.random_normal(shape=[self.embeddingSize, 1]))
return weights
計算圖的構建,首先是計算embedding部分,然後是計算attention部分的權重。
一些輸入的設置:
self.weights = self._initWeights()
self.featureIndex = tf.placeholder(shape=[None, None], dtype=tf.int32, name='featureIndex')
self.featureValue = tf.placeholder(shape=[None, None], dtype=tf.float32, name='featureValue')
self.label = tf.placeholder(shape=[None, 1], dtype=tf.float32, name='label')
# self.dropoutKeep = tf.placeholder(shape=[None, ], dtype=tf.float32, name='dropoutKeep')
self.trainPhrase = tf.placeholder(dtype=tf.bool, name='trainPhrase')
embedding部分:
# embedding
self.embedding = tf.nn.embedding_lookup(self.weights['feature_embedding'], self.featureIndex) # N * F * K
featureValue = tf.reshape(self.featureValue, shape=[-1, self.fieldSize, 1])
self.embedding = tf.multiply(self.embedding, featureValue) # N*F*K
接下來我們就要計算特徵之間的組合了,兩兩組合,這次是採用的for循環了,在PNN中我們是先找出來每一個特徵組合pair的索引,然後利用tf.gather來把這些向量找出來,最後實現相乘。特徵向量組合的的代碼:
得到的是一個長度爲embeddingSize的向量
elementWiseProduct = []
for i in range(0, self.fieldSize-1):
for j in range(i+1, self.fieldSize):
elementWiseProduct.append(tf.multiply(self.embedding[:, i, :], self.embedding[:, j, :]))
self.elementWiseProduct = tf.stack(elementWiseProduct) # f*(f-1)/2 * N * k
self.elementWiseProduct = tf.transpose(self.elementWiseProduct, [1, 0, 2], ) # N * (f*(f-1)/2) * K
此時self.elementWiseProduct的shape爲 f爲fieldSize,k爲embeddingSize。
然後就是計算,W的shape爲,因此首先要把self.elementWiseProduct的shape調整一下。
self.numPairs = int(self.fieldSize * (self.fieldSize-1)/2)
self.wxPlusB = tf.matmul(tf.reshape(self.elementWiseProduct, shape=[-1, self.embeddingSize]), # (N*pairs) * K
self.weights['attention_w']) + self.weights['attention_b'] # (N*pairs) * A
self.wxPlusB = tf.reshape(self.wxPlusB, shape=[-1, self.numPairs, self.attentionSize]) # N*pairs*A
因爲和相乘是矩陣相乘,所以要遵循矩陣相乘時尺寸的要求。計算完之後再對其進行shape的調整,調整爲。
然後就是計算,
self.attentionExp = tf.exp(tf.reduce_sum(tf.multiply(tf.nn.relu(self.wxPlusB), self.weights['attention_h']),
axis=2, keep_dims=True)) # N*pairs*1
self.attentionExpSum = tf.reduce_sum(self.attentionExp, axis=1, keep_dims=True) # N*1*1
第一行代碼是計算第二行代碼是對所有的求和,即爲後面的softmax計算做準備。上面這兩行的keep_dims還是要加的,如果不加的話,在某一維度上進行求和,求和之後得到的結果就會少一個維度。
計算,
self.attentionOut = tf.div(self.attentionExp, self.attentionExpSum) # N*pairs*1
接下來就是對每一對特徵組合進行權重分配了,
self.attention_x_product = tf.reduce_sum(tf.multiply(self.attentionOut, self.elementWiseProduct), axis=1) # N*K
self.attentionPartSum = tf.matmul(self.attention_x_product, self.weights['attention_p']) # N*1
最後把一次項以及偏置項計算出來加到一起就可以了,
# first order
self.yFirstOrder = tf.nn.embedding_lookup(self.weights['feature_bias'], self.featureIndex) # N*F
self.yFirstOrder = tf.multiply(self.yFirstOrder, featureValue) # N*F*1
self.yFirstOrder = tf.reduce_sum(self.yFirstOrder, axis=2) # N*F
# bias
self.yBias = self.weights['bias'] * tf.ones_like(self.label)
self.out = tf.add_n([tf.reduce_sum(self.yFirstOrder, axis=1, keep_dims=True),
self.attentionPartSum, self.yBias], name='out_afm')
然後就是模型的loss的設置、以及初始化等問題了。因爲該模型沒有全連接層,所以模型部分就是這樣。
這篇文章就是對FM的二次項部分進行了加權操作,沒有太大的改進,上一篇NFM是對FM的輸出向量進行了幾層全連接操作,思路都比較簡單。不知道這樣的模型在實際生產環境中是否有使用。
參考
https://www.comp.nus.edu.sg/~xiangnan/papers/ijcai17-afm.pdf
https://www.jianshu.com/p/83d3b2a1e55d