FFM代码实现

上一篇我们讲了《FFM原理及公式推导》,现在来编码实现一下。

把写代码所需要所有公式都列出来,各符号与原文《Field-aware Factorization Machines for CTR Prediction》中的保持一致。

符号约定:

nn:特征的维数

mm:域的个数

kk:隐向量的维度

jj:在特征中的下标

ff:在域中的下标

dd:在隐向量中的下标

ll:样本的总数

粗体字母表示向量或矩阵

特征组合

最基本的线性加权

ϕLM(w,x)=i=1nwixiϕLM(w,x)=∑i=1nwixi

任意特征两两组合

ϕpoly2(w,x)=j1=1nj2=j1+1nwj1,j2xj1xj2ϕpoly2(w,x)=∑j1=1n∑j2=j1+1nwj1,j2xj1xj2

ww是一个对称方阵,即wj1,j2=wj2,j1wj1,j2=wj2,j1,可以用矩阵分解法来拟合ww

wj1,j2=vj1vj2=vj2vj1=wj2,j1wj1,j2=vj1⋅vj2=vj2⋅vj1=wj2,j1

矩阵ww的规模是n×nn×n,矩阵vv的规模是n×kn×kknk≪n。实际上我们已经推导出了因子分解法。

因子分解法FM

ϕFM(w,x)=j1=1nj2=j1+1nwj1wj2xj1xj2ϕFM(w,x)=∑j1=1n∑j2=j1+1nwj1⋅wj2xj1xj2

这里的wjwj相当于上面的vjvj

域感知的因子分解法FFM

ϕFFM(w,x)=j1=1nj2=j1+1nwj1,f2wj2,f1xj1xj2ϕFFM(w,x)=∑j1=1n∑j2=j1+1nwj1,f2⋅wj2,f1xj1xj2

在FM中ww是规模为n×kFMn×kFM的二维矩阵,而在FFM中ww是规模为n×m×kFFMn×m×kFFM的三维矩阵,kFFMkFMkFFM≪kFM

逻辑回归二分类

决策函数

y^=11+exp(ϕFFM(w,x))y^=11+exp(ϕFFM(w,x))

带L2正则的目标函数

minwλ2w22+i=1llog(1+exp(yiϕFFM(w,xi)))minwλ2∥w∥22+∑i=1llog(1+exp(−yiϕFFM(w,xi)))

其中yi{1,1}yi∈{−1,1}

在SGD中每次只需要考虑一个样本的损失,此时目标函数为

minwλ2w22+log(1+exp(yϕFFM(w,x)))minwλ2∥w∥22+log(1+exp(−yϕFFM(w,x)))
 

梯度

gj1,f2=λwj1,f2+κwj2,f1xj1xj2gj1,f2=λ⋅wj1,f2+κ⋅wj2,f1xj1xj2
 

gj2,f1=λwj2,f1+κwj1,f2xj1xj2gj2,f1=λ⋅wj2,f1+κ⋅wj1,f2xj1xj2
 

梯度之所会这么简单,依赖一个很重要的前提:同一个域下的各个特征只有一个是非0值。

其中

κ=log(1+exp(yϕFFM(w,x)))ϕFFM(w,x)=y1+exp(yϕFFM(w,x))κ=∂log(1+exp(−yϕFFM(w,x)))∂ϕFFM(w,x)=−y1+exp(yϕFFM(w,x))

AdaGrad更新w

至于为什么要用AdaGrad替代传统的梯度下降法,请参见我之前写的《优化方法》。

(Gj1,f2)d(Gj1,f2)d+(gj1,f2)2d(Gj1,f2)d←(Gj1,f2)d+(gj1,f2)d2

(Gj2,f1)d(Gj2,f1)d+(gj2,f1)2d(Gj2,f1)d←(Gj2,f1)d+(gj2,f1)d2

(wj1,f2)d(wj1,f2)dη(Gj1,f2)d(gj1,f2)d(wj1,f2)d←(wj1,f2)d−η(Gj1,f2)d(gj1,f2)d

(wj2,f1)d(wj2,f1)dη(Gj2,f1)d(gj2,f1)d(wj2,f1)d←(wj2,f1)d−η(Gj2,f1)d(gj2,f1)d

初始化Gd=1Gd=1,这样在计算ηGdηGd时既可以防止分母为0,又可以避免该项太大或太小。

ηη是学习率,通常可取0.01。

初始的ww可以从均匀分布中抽样wU(0,1k)w∼U(0,1k)

实现发现将每个xx归一化,即模长为1,在测试集得到的准确率会稍微好一点且对参数不太敏感。

代码实现

只要把公式推导搞明白了,写代码就非常容易了,直接把公式直译成代码即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# -*- coding: utf-8 -*-
# @Date    : 3/2/18
# @Author  : zhangchaoyang
 
import  numpy as np
 
np.random.seed( 0 )
import  math
from  logistic  import  Logistic
 
 
class  FFM_Node( object ):
     '''
     通常x是高维稀疏向量,所以用链表来表示一个x,链表上的每个节点是个3元组(j,f,v)
     '''
     __slots__  =  [ 'j' 'f' 'v' ]   # 按元组(而不是字典)的方式来存储类的成员属性
 
     def  __init__( self , j, f, v):
         '''
         :param j: Feature index (0 to n-1)
         :param f: Field index (0 to m-1)
         :param v: value
         '''
         self .j  =  j
         self .f  =  f
         self .v  =  v
 
 
class  FFM( object ):
     def  __init__( self , m, n, k, eta, lambd):
         '''
         :param m: Number of fields
         :param n: Number of features
         :param k: Number of latent factors
         :param eta: learning rate
         :param lambd: regularization coefficient
         '''
         self .m  =  m
         self .n  =  n
         self .k  =  k
         # 超参数
         self .eta  =  eta
         self .lambd  =  lambd
         # 初始化三维权重矩阵w~U(0,1/sqrt(k))
         self .w  =  np.random.rand(n, m, k)  /  math.sqrt(k)
         # 初始化累积梯度平方和为,AdaGrad时要用到,防止除0异常
         self .G  =  np.ones(shape = (n, m, k), dtype = np.float64)
         self .log  =  Logistic()
 
     def  phi( self , node_list):
         '''
         特征组合式的线性加权求和
         :param node_list: 用链表存储x中的非0值
         :return:
         '''
         =  0.0
         for  in  xrange ( len (node_list)):
             node1  =  node_list[a]
             j1  =  node1.j
             f1  =  node1.f
             v1  =  node1.v
             for  in  xrange (a  +  1 len (node_list)):
                 node2  =  node_list[b]
                 j2  =  node2.j
                 f2  =  node2.f
                 v2  =  node2.v
                 w1  =  self .w[j1, f2]
                 w2  =  self .w[j2, f1]
                 + =  np.dot(w1, w2)  *  v1  *  v2
         return  z
 
     def  predict( self , node_list):
         '''
         输入x,预测y的值
         :param node_list: 用链表存储x中的非0值
         :return:
         '''
         =  self .phi(node_list)
         =  self .log.decide_by_tanh(z)
         return  y
 
     def  sgd( self , node_list, y):
         '''
         根据一个样本来更新模型参数
         :param node_list: 用链表存储x中的非0值
         :param y: 正样本1,负样本-1
         :return:
         '''
         kappa  =  - /  ( 1  +  math.exp(y  *  self .phi(node_list)))
         for  in  xrange ( len (node_list)):
             node1  =  node_list[a]
             j1  =  node1.j
             f1  =  node1.f
             v1  =  node1.v
             for  in  xrange (a  +  1 len (node_list)):
                 node2  =  node_list[b]
                 j2  =  node2.j
                 f2  =  node2.f
                 v2  =  node2.v
                 =  kappa  *  v1  *  v2
                 # self.w[j1,f2]和self.w[j2,f1]是向量,导致g_j1_f2和g_j2_f1也是向量
                 g_j1_f2  =  self .lambd  *  self .w[j1, f2]  +  *  self .w[j2, f1]
                 g_j2_f1  =  self .lambd  *  self .w[j2, f1]  +  *  self .w[j1, f2]
                 # 计算各个维度上的梯度累积平方和
                 self .G[j1, f2]  + =  g_j1_f2  * *  2   # 所有G肯定是大于0的正数,因为初始化时G都为1
                 self .G[j2, f1]  + =  g_j2_f1  * *  2
                 # AdaGrad
                 self .w[j1, f2]  - =  self .eta  /  np.sqrt( self .G[j1, f2])  *  g_j1_f2   # sqrt(G)作为分母,所以G必须是大于0的正数
                 self .w[j2, f1]  - =  self .eta  /  np.sqrt(
                     self .G[j2, f1])  *  g_j2_f1   # math.sqrt()只能接收一个数字作为参数,而numpy.sqrt()可以接收一个array作为参数,表示对array中的每个元素分别开方
 
     def  train( self , sample_generator, max_echo, max_r2):
         '''
         根据一堆样本训练模型
         :param sample_generator: 样本生成器,每次yield (node_list, y),node_list中存储的是x的非0值。通常x要事先做好归一化,即模长为1,这样精度会略微高一点
         :param max_echo: 最大迭代次数
         :param max_r2: 拟合系数r2达到阈值时即可终止学习
         :return:
         '''
         for  itr  in  xrange (max_echo):
             print  "echo" , itr
             y_sum  =  0.0
             y_square_sum  =  0.0
             err_square_sum  =  0.0   # 误差平方和
             population  =  0   # 样本总数
             for  node_list, y  in  sample_generator:
                 =  0.0  if  = =  - 1  else  y   # 真实的y取值为{-1,1},而预测的y位于(0,1),计算拟合效果时需要进行统一
                 self .sgd(node_list, y)
                 y_hat  =  self .predict(node_list)
                 y_sum  + =  y
                 y_square_sum  + =  * *  2
                 err_square_sum  + =  (y  -  y_hat)  * *  2
                 population  + =  1
             var_y  =  y_square_sum  -  y_sum  *  y_sum  /  population   # y的方差
             r2  =  1  -  err_square_sum  /  var_y
             print  "r2=" ,r2
             if  r2 > max_r2:   # r2值越大说明拟合得越好
                 print  'r2 have reach' , r2
                 break
 
     def  save_model( self , outfile):
         '''
         序列化模型
         :param outfile:
         :return:
         '''
         np.save(outfile,  self .w)
 
     def  load_model( self , infile):
         '''
         加载模型
         :param infile:
         :return:
         '''
         self .w  =  np.load(infile)

完整代码见 https://github.com/Orisun/ffm

 

原文来自:博客园(华夏35度)http://www.cnblogs.com/zhangchaoyang 作者:Orisun

猜你喜欢

转载自blog.csdn.net/yanhx1204/article/details/79718371
FFM
今日推荐