FM算法解析及Python實現


1. 什么是FM?

FM即Factor Machine,因子分解機。

2. 為什么需要FM?

1、特征組合是許多機器學習建模過程中遇到的問題,如果對特征直接建模,很有可能會忽略掉特征與特征之間的關聯信息,因此,可以通過構建新的交叉特征這一特征組合方式提高模型的效果。

2、高維的稀疏矩陣是實際工程中常見的問題,並直接會導致計算量過大,特征權值更新緩慢。試想一個10000*100的表,每一列都有8種元素,經過one-hot獨熱編碼之后,會產生一個10000*800的表。因此表中每行元素只有100個值為1,700個值為0。

而FM的優勢就在於對這兩方面問題的處理。首先是特征組合,通過對兩兩特征組合,引入交叉項特征,提高模型得分;其次是高維災難,通過引入隱向量(對參數矩陣進行矩陣分解),完成對特征的參數估計。

3. FM用在哪?

我們已經知道了FM可以解決特征組合以及高維稀疏矩陣問題,而實際業務場景中,電商、豆瓣等推薦系統的場景是使用最廣的領域,打個比方,小王只在豆瓣上瀏覽過20部電影,而豆瓣上面有20000部電影,如果構建一個基於小王的電影矩陣,毫無疑問,里面將有199980個元素全為0。而類似於這樣的問題就可以通過FM來解決。

4. FM長什么樣?

在展示FM算法前,我們先回顧一下最常見的線性表達式:

其中w0 為初始權值,或者理解為偏置項,w為每個特征xi 對應的權值。可以看到,這種線性表達式只描述了每個特征與輸出的關系。

FM的表達式如下,可觀察到,只是在線性表達式后面加入了新的交叉項特征及對應的權值。

5. FM交叉項的展開

5.1 尋找交叉項

FM表達式的求解核心在於對交叉項的求解。下面是很多人用來求解交叉項的展開式,對於第一次接觸FM算法的人來說可能會有疑惑,不知道公式怎么展開的,接下來筆者會手動推導一遍。

設有3個變量(特征)x1 x2 x3,每一個特征的隱變量分別為v1=(1 2 3)、v2=(4 5 6)、v3=(1 2 1),即:

設交叉項所組成的權矩陣W為對稱矩陣,之所以設為對稱矩陣是因為對稱矩陣有可以用向量乘以向量轉置替代的性質。
那么W=VVT,即

所以:

實際上,我們應該考慮的交叉項應該是排除自身組合的項,即對於x1x1、x2x2、x3x3不認為是交叉項,那么真正的交叉項為x1x2、x1x3、x2x1、x2x3、x3x1、x3x2。
去重后,交叉項即x1x2、x1x3、x2x3。這也是公式中1/2出現的原因。

5.2 交叉項權值轉換

對交叉項有了基本了解后,下面將進行公式的分解,還是以n=3為例,

所以:

wij可記作,這取決於vi是1*3 還是3*1 向量。

 5.3 交叉項展開式

上面的例子是對3個特征做的交叉項推導,因此對具有n個特征,FM的交叉項公式就可推廣為:

 

我們還可以進一步分解:

所以FM算法的交叉項最終可展開為:

5.4 隱向量v就是embedding vector?

假設訓練數據集dataMatrix的shape為(20000,9),取其中一行數據作為一條樣本i,那么樣本i 的shape為(1,9),同時假設隱向量vi的shape為(9,8)(注:8為自定義值,代表embedding vector的長度)

所以5.3小節中的交叉項可以表示為:

sum((inter_1)^2 - (inter_2)^2)/2

其中:

inter_1 = i*v  shape為(1,8)

inter_2 = np.multiply(i)*np.multiply(v)  shape為(1,8)

可以看到,樣本i 經過交叉項中的計算后,得到向量shape為(1,8)的inter_1和 inter_2。

由於維度變低,所以此計算過程可以近似認為在交叉項中對樣本i 進行了embedding vector轉換。

故,我們需要對之前的理解進行修正:

  1. 我們口中的隱向量vi實際上是一個向量組,其形狀為(輸入特征One-hot后的長度,自定義長度);
  2. 隱向量vi代表的並不是embedding vector,而是在對輸入進行embedding vector的向量組,也可理解為是一個權矩陣;
  3. 由輸入i*vi得到的向量才是真正的embedding vector。

具體可以結合第7節點的代碼實現進行理解。

6. 權值求解

利用梯度下降法,通過求損失函數對特征(輸入項)的導數計算出梯度,從而更新權值。設m為樣本個數,θ為權值。

如果是回歸問題,損失函數一般是均方誤差(MSE):

所以回歸問題的損失函數對權值的梯度(導數)為:

如果是二分類問題,損失函數一般是logit loss:

其中,表示的是階躍函數Sigmoid。

所以分類問題的損失函數對權值的梯度(導數)為:

 相應的,對於常數項、一次項、交叉項的導數分別為:

7. FM算法的Python實現

 FM算法的Python實現流程圖如下:

 

我們需要注意以下四點:

1. 初始化參數,包括對偏置項權值w0、一次項權值w以及交叉項輔助向量的初始化;

2. 定義FM算法;

3. 損失函數梯度的定義;

4. 利用梯度下降更新參數。

下面的代碼片段是以上四點的描述,其中的loss並不是二分類的損失loss,而是分類loss的梯度中的一部分:

loss = self.sigmoid(classLabels[x] * p[0, 0]) -1

實際上,二分類的損失loss的梯度可以表示為:

gradient = (self.sigmoid(classLabels[x] * p[0, 0]) -1)*classLabels[x]*p_derivative

其中 p_derivative 代表常數項、一次項、交叉項的導數(詳見本文第6小節)。

FM算法代碼片段

 1 # 初始化參數
 2         w = zeros((n, 1))  # 其中n是特征的個數
 3         w_0 = 0.  4         v = normalvariate(0, 0.2) * ones((n, k))  5         for it in range(self.iter): # 迭代次數
 6             # 對每一個樣本,優化
 7             for x in range(m):  8                 # 這邊注意一個數學知識:對應點積的地方通常會有sum,對應位置積的地方通常都沒有,詳細參見矩陣運算規則,本處計算邏輯在:http://blog.csdn.net/google19890102/article/details/45532745
 9                 # xi·vi,xi與vi的矩陣點積
10                 inter_1 = dataMatrix[x] * v 11                 # xi與xi的對應位置乘積 與 xi^2與vi^2對應位置的乘積 的點積
12                 inter_2 = multiply(dataMatrix[x], dataMatrix[x]) * multiply(v, v)  # multiply對應元素相乘
13                 # 完成交叉項,xi*vi*xi*vi - xi^2*vi^2
14                 interaction = sum(multiply(inter_1, inter_1) - inter_2) / 2. 15                 # 計算預測的輸出
16                 p = w_0 + dataMatrix[x] * w + interaction 17                 print('classLabels[x]:',classLabels[x]) 18                 print('預測的輸出p:', p) 19                 # 計算sigmoid(y*pred_y)-1准確的說不是loss,原作者這邊理解的有問題,只是作為更新w的中間參數,這邊算出來的是越大越好,而下面卻用了梯度下降而不是梯度上升的算法在
20                 loss = self.sigmoid(classLabels[x] * p[0, 0]) - 1
21                 if loss >= -1: 22                     loss_res = '正方向 '
23                 else: 24                     loss_res = '反方向'
25                 # 更新參數
26                 w_0 = w_0 - self.alpha * loss * classLabels[x] 27                 for i in range(n): 28                     if dataMatrix[x, i] != 0: 29                         w[i, 0] = w[i, 0] - self.alpha * loss * classLabels[x] * dataMatrix[x, i] 30                         for j in range(k): 31                             v[i, j] = v[i, j] - self.alpha * loss * classLabels[x] * ( 32                                     dataMatrix[x, i] * inter_1[0, j] - v[i, j] * dataMatrix[x, i] * dataMatrix[x, i])

 

FM算法完整實現

  1 # -*- coding: utf-8 -*-
  2 
  3 from __future__ import division
  4 from math import exp
  5 from numpy import *
  6 from random import normalvariate  # 正態分布
  7 from sklearn import preprocessing
  8 import numpy as np
  9 
 10 '''
 11     data : 數據的路徑
 12     feature_potenital : 潛在分解維度數
 13     alpha : 學習速率
 14     iter : 迭代次數
 15     _w,_w_0,_v : 拆分子矩陣的weight
 16     with_col : 是否帶有columns_name
 17     first_col : 首列有價值的feature的index
 18 '''
 19 
 20 
 21 class fm(object):
 22     def __init__(self):
 23         self.data = None
 24         self.feature_potential = None
 25         self.alpha = None
 26         self.iter = None
 27         self._w = None
 28         self._w_0 = None
 29         self.v = None
 30         self.with_col = None
 31         self.first_col = None
 32 
 33     def min_max(self, data):
 34         self.data = data
 35         min_max_scaler = preprocessing.MinMaxScaler()
 36         return min_max_scaler.fit_transform(self.data)
 37 
 38     def loadDataSet(self, data, with_col=True, first_col=2):
 39         # 我就是閑的蛋疼,明明pd.read_table()可以直接度,非要搞這樣的,顯得代碼很長,小數據下完全可以直接讀嘛,唉~
 40         self.first_col = first_col
 41         dataMat = []
 42         labelMat = []
 43         fr = open(data)
 44         self.with_col = with_col
 45         if self.with_col:
 46             N = 0
 47             for line in fr.readlines():
 48                 # N=1時干掉列表名
 49                 if N > 0:
 50                     currLine = line.strip().split()
 51                     lineArr = []
 52                     featureNum = len(currLine)
 53                     for i in range(self.first_col, featureNum):
 54                         lineArr.append(float(currLine[i]))
 55                     dataMat.append(lineArr)
 56                     labelMat.append(float(currLine[1]) * 2 - 1)
 57                 N = N + 1
 58         else:
 59             for line in fr.readlines():
 60                 currLine = line.strip().split()
 61                 lineArr = []
 62                 featureNum = len(currLine)
 63                 for i in range(2, featureNum):
 64                     lineArr.append(float(currLine[i]))
 65                 dataMat.append(lineArr)
 66                 labelMat.append(float(currLine[1]) * 2 - 1)
 67         return mat(self.min_max(dataMat)), labelMat
 68 
 69     def sigmoid(self, inx):
 70         # return 1.0/(1+exp(min(max(-inx,-10),10)))
 71         return 1.0 / (1 + exp(-inx))
 72 
 73     # 得到對應的特征weight的矩陣
 74     def fit(self, data, feature_potential=8, alpha=0.01, iter=100):
 75         # alpha是學習速率
 76         self.alpha = alpha
 77         self.feature_potential = feature_potential
 78         self.iter = iter
 79         # dataMatrix用的是mat, classLabels是列表
 80         dataMatrix, classLabels = self.loadDataSet(data)
 81         print('dataMatrix:',dataMatrix.shape)
 82         print('classLabels:',classLabels)
 83         k = self.feature_potential
 84         m, n = shape(dataMatrix)
 85         # 初始化參數
 86         w = zeros((n, 1))  # 其中n是特征的個數
 87         w_0 = 0.
 88         v = normalvariate(0, 0.2) * ones((n, k))
 89         for it in range(self.iter): # 迭代次數
 90             # 對每一個樣本,優化
 91             for x in range(m):
 92                 # 這邊注意一個數學知識:對應點積的地方通常會有sum,對應位置積的地方通常都沒有,詳細參見矩陣運算規則,本處計算邏輯在:http://blog.csdn.net/google19890102/article/details/45532745
 93                 # xi·vi,xi與vi的矩陣點積
 94                 inter_1 = dataMatrix[x] * v
 95                 # xi與xi的對應位置乘積   與   xi^2與vi^2對應位置的乘積    的點積
 96                 inter_2 = multiply(dataMatrix[x], dataMatrix[x]) * multiply(v, v)  # multiply對應元素相乘
 97                 # 完成交叉項,xi*vi*xi*vi - xi^2*vi^2
 98                 interaction = sum(multiply(inter_1, inter_1) - inter_2) / 2.
 99                 # 計算預測的輸出
100                 p = w_0 + dataMatrix[x] * w + interaction
101                 print('classLabels[x]:',classLabels[x])
102                 print('預測的輸出p:', p)
103                 # 計算sigmoid(y*pred_y)-1
104                 loss = self.sigmoid(classLabels[x] * p[0, 0]) - 1
105                 if loss >= -1:
106                     loss_res = '正方向 '
107                 else:
108                     loss_res = '反方向'
109                 # 更新參數
110                 w_0 = w_0 - self.alpha * loss * classLabels[x]
111                 for i in range(n):
112                     if dataMatrix[x, i] != 0:
113                         w[i, 0] = w[i, 0] - self.alpha * loss * classLabels[x] * dataMatrix[x, i]
114                         for j in range(k):
115                             v[i, j] = v[i, j] - self.alpha * loss * classLabels[x] * (
116                                     dataMatrix[x, i] * inter_1[0, j] - v[i, j] * dataMatrix[x, i] * dataMatrix[x, i])
117             print('the no %s times, the loss arrach %s' % (it, loss_res))
118         self._w_0, self._w, self._v = w_0, w, v
119 
120     def predict(self, X):
121         if (self._w_0 == None) or (self._w == None).any() or (self._v == None).any():
122             raise NotFittedError("Estimator not fitted, call `fit` first")
123         # 類型檢查
124         if isinstance(X, np.ndarray):
125             pass
126         else:
127             try:
128                 X = np.array(X)
129             except:
130                 raise TypeError("numpy.ndarray required for X")
131         w_0 = self._w_0
132         w = self._w
133         v = self._v
134         m, n = shape(X)
135         result = []
136         for x in range(m):
137             inter_1 = mat(X[x]) * v
138             inter_2 = mat(multiply(X[x], X[x])) * multiply(v, v)  # multiply對應元素相乘
139             # 完成交叉項
140             interaction = sum(multiply(inter_1, inter_1) - inter_2) / 2.
141             p = w_0 + X[x] * w + interaction  # 計算預測的輸出
142             pre = self.sigmoid(p[0, 0])
143             result.append(pre)
144         return result
145 
146     def getAccuracy(self, data):
147         dataMatrix, classLabels = self.loadDataSet(data)
148         w_0 = self._w_0
149         w = self._w
150         v = self._v
151         m, n = shape(dataMatrix)
152         allItem = 0
153         error = 0
154         result = []
155         for x in range(m):
156             allItem += 1
157             inter_1 = dataMatrix[x] * v
158             inter_2 = multiply(dataMatrix[x], dataMatrix[x]) * multiply(v, v)  # multiply對應元素相乘
159             # 完成交叉項
160             interaction = sum(multiply(inter_1, inter_1) - inter_2) / 2.
161             p = w_0 + dataMatrix[x] * w + interaction  # 計算預測的輸出
162             pre = self.sigmoid(p[0, 0])
163             result.append(pre)
164             if pre < 0.5 and classLabels[x] == 1.0:
165                 error += 1
166             elif pre >= 0.5 and classLabels[x] == -1.0:
167                 error += 1
168             else:
169                 continue
170         # print(result)
171         value = 1 - float(error) / allItem
172         return value
173 
174 
175 class NotFittedError(Exception):
176     """
177     Exception class to raise if estimator is used before fitting
178     """
179     pass
180 
181 
182 if __name__ == '__main__':
183     fm()

 

=


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM