首页 > 系统服务 > 详细

FM(Factorization Machine)因式分解机 与 TensorFlow实现 详解

时间:2019-02-18 17:38:30      阅读:468      评论:0      收藏:0      [点我收藏+]

1,线性回归(Linear Regression)

线性回归,即使用多维空间中的一条直线拟合样本数据,如果样本特征为:

\[x = ({x_1},{x_2},...,{x_n})\]

模型假设函数如下:

\[\hat y = h(w,b) = {w^T}x + b,w = ({w_1},{w_2},...,{w_n})\]

以均方误差为模型损失,模型输入样本为(x(1),y(1)),(x(2),y(2)),...,(x(m),y(m)),损失函数如下:

\[l(w,b) = \sum\limits_{j = 1}^m {{{({{\hat y}^{(j)}} - {y^{(j)}})}^2}}  = \sum\limits_{j = 1}^m {(\sum\limits_{i = 1}^n {{w_i}x_i^{(j)} + b}  - {y^{(j)}})} \]

2,逻辑回归(Logistic Regression)

线性回归用于预测标记为连续的,尤其是线性或准线性的样本数据,但是有时样本的标记为离散的,最典型的情况莫过于标记为0或者1,此时的模型被称为分类模型。

为了扩展线性回归到分类任务,需要一个函数将(-∞,+∞)映射到(0,1)(或者(-1,1)等任意两个离散值),函数最好连续可导,并且在自变量趋向于-∞时无限趋近于因变量下限,在自变量趋向于+∞时无限趋近于因变量上限。符合条件的函数有不少,比如tanh函数,logistic函数。

如果映射函数采用logistic函数,设模型假设函数为样本预测分类概率,模型假设函数为:

\[\begin{array}{l}
P(y = 1) = h(w,b) = \frac{1}{{1 + {e^{ - {h_{linear}}(w,b)}}}} = \frac{1}{{1 + {e^{ - ({w^T}x + b)}}}}\\
P(y = 0) = 1 - P(y = 1)
\end{array}\]

另外,可以从另一个角度考虑从回归模型扩展为分类模型,令:

\[\ln (\frac{{P(y = 1)}}{{P(y = 0)}}) = \ln (\frac{{h(w,b)}}{{1 - h(w,b)}}) = {h_{linear}}(w,b) = {w^T}x + b\]

同样可以得到映射函数为logistic函数的假设函数:

\[h(w,b) = \frac{1}{{1 + {e^{ - ({w^T}x + b)}}}} = \frac{1}{{1 + {e^{ - (\sum\limits_{i = 1}^n {{w_i}{x_i}}  + b)}}}}\]

使用(负)对数似然函数作为损失函数:

\[\begin{array}{l}
l(w,b) = - \log (\prod\limits_{j = 1}^m {P{{({y^{(j)}} = 1)}^{{y^{(j)}}}}P{{({y^{(j)}} = 0)}^{(1 - {y^{(j)}})}}} )\\
= - \sum\limits_{j = 1}^m {({y^{(j)}}\log P({y^{(j)}} = 1) + (1 - {y^{(j)}})\log P({y^{(j)}} = 0))} \\
= - \sum\limits_{j = 1}^m {({y^{(j)}}\log (g(b + \sum\limits_{i = 1}^n {{w_i}x_i^{(j)}} )) + (1 - {y^{(j)}})\log (1 - g(b + \sum\limits_{i = 1}^n {{w_i}x_i^{(j)}} ))} ,g(z) = \frac{1}{{1 + {e^{ - z}}}}
\end{array}\]

很多场景下,样本包含一些离散的label特征,这些特征很难连续量化,最简单的处理方式就是one-hot,即将离散的每个数映射到多维空间中的一维。离散特征包含多少个可能值,转换后的特征就包含多少维。这样处理后样本特征的维数会变得非常巨大,另外大部分维度的值都是0。

一方面,逻辑回归核心是一个线性模型,因此计算规模随着样本特征数的增长而线性增长,相较其他机器学习模型来说计算量(随特征数增长)的增长率较小;另一方面,逻辑回归假设函数与损失函数中,各个特征对于结果是独立起作用的,因此在样本数足够的前提下,也不会受到大量值为0的特征的干扰。因此特别适合这类场景下的分类问题。

3,因式分解机(Factorization Machine)

逻辑回归最大的优势是简单,最大的劣势也是太简单,没有考虑特征之间的相互关系,需要手动对特征进行预处理。因此就有了包含特征两两交叉项的假设函数:

\[h(w,b) = g(b + \sum\limits_{i = 1}^n {{w_i}{x_i}}  + \sum\limits_{i = 1}^n {\sum\limits_{j = i + 1}^n {{\omega _{ij}}{x_i}} {x_j}} ),g(z) = \frac{1}{{1 + {e^{ - z}}}}\]

此时有一个严重的问题,由于特征维数有可能是非常巨大的,很有可能训练样本中有一些特征组合xixj(xi、xj都不为0)是完全不存在的,这样ωij就无法得到训练。实际使用模型时,如果出现特征组合xixj,模型就无法正常工作。

为了减小训练计算量,也为了规避上面说的这种情况,我们引入辅助矩阵v,n为特征总维数,k为超参数

 \[v \in {R^{n*k}}\]

然后使用vvT代替参数矩阵ω,可得

\[{\omega _{ij}} = \sum\limits_{r = 1}^k {{v_{ir}}v_{rj}^T}  = \sum\limits_{r = 1}^k {{v_{ir}}{v_{jr}}} \]

可得假设函数线性部分最后一项可化简为:

\[\begin{array}{l}
\sum\limits_{i = 1}^n {\sum\limits_{j = i + 1}^n {{\omega _{ij}}{x_i}} {x_j}} \\
= \sum\limits_{i = 1}^n {\sum\limits_{j = i + 1}^n {\sum\limits_{r = 1}^k {{v_{ir}}{v_{jr}}{x_i}{x_j}} } } \\
= \sum\limits_{r = 1}^k {(\sum\limits_{i = 1}^n {\sum\limits_{j = i + 1}^n {{v_{ir}}{v_{jr}}{x_i}{x_j})} } } \\
= \frac{1}{2}\sum\limits_{r = 1}^k {(\sum\limits_{i = 1}^n {\sum\limits_{j = 1}^n {{v_{ir}}{v_{jr}}{x_i}{x_j}} } - \sum\limits_{i = 1}^n {{v_{ir}}{v_{ir}}{x_i}{x_i}} )} \\
= \frac{1}{2}\sum\limits_{r = 1}^k {(\sum\limits_{i = 1}^n {{v_{ir}}{x_i}} \sum\limits_{j = 1}^n {{v_{jr}}{x_j}} - \sum\limits_{i = 1}^n {{v_{ir}}{v_{ir}}{x_i}{x_i}} )} \\
= \frac{1}{2}\sum\limits_{r = 1}^k {({{(\sum\limits_{i = 1}^n {{v_{ir}}{x_i}} )}^2} - \sum\limits_{i = 1}^n {{{({v_{ir}}{x_i})}^2}} )}
\end{array}\]

同逻辑回归一样,令模型假设函数为样本预测分类概率,还是使用(负)对数似然函数为损失函数:

\[\begin{array}{l}
l(b,w,v) = - \log (\prod\limits_{j = 1}^m {P{{({y^{(j)}} = 1)}^{{y^{(j)}}}}P{{({y^{(j)}} = 0)}^{(1 - {y^{(j)}})}}} ) = - \sum\limits_{j = 1}^m {({y^{(j)}}\log (h(w,b){|_{x = {x^{(j)}}}}) + (1 - {y^{(j)}})\log (1 - h(w,b){|_{x = {x^{(j)}}}}))} \\
h(w,b){|_{x = {x^{(j)}}}} = g(b + \sum\limits_{i = 1}^n {{w_i}x_i^{(j)}} + \frac{1}{2}\sum\limits_{r = 1}^k {({{(\sum\limits_{i = 1}^n {{v_{ir}}x_i^{(j)}} )}^2} - \sum\limits_{i = 1}^n {{{({v_{ir}}x_i^{(j)})}^2}} )} )\\
g(z) = \frac{1}{{1 + {e^{ - z}}}}
\end{array}\]

4,关于FM假设函数的讨论

仔细看FM假设函数线性部分最后一项的化简过程,会发现一个问题,这个“化简”过程仅仅是化简了式子的形式,其实并没有减少计算量,反倒是增加了计算量,如果假设函数不是:

\[h(w,b) = g(b + \sum\limits_{i = 1}^n {{w_i}{x_i}}  + \sum\limits_{i = 1}^n {\sum\limits_{j = i + 1}^n {{\omega _{ij}}{x_i}} {x_j}} )\]

而是:

\[h(w,b) = g(b + \sum\limits_{i = 1}^n {{w_i}{x_i}}  + \sum\limits_{i = 1}^n {\sum\limits_{j = 1}^n {{\omega _{ij}}{x_i}} {x_j}} )\]

表面上看假设函数增加了若干项, 但经过化简后,可得线性部分最后一项为:

\[\sum\limits_{i = 1}^n {\sum\limits_{j = 1}^n {{\omega _{ij}}{x_i}} {x_j}}  = \sum\limits_{i = 1}^n {\sum\limits_{j = i + 1}^n {\sum\limits_{r = 1}^k {{v_{ir}}{v_{jr}}{x_i}{x_j}} } }  = \sum\limits_{r = 1}^k {({{(\sum\limits_{i = 1}^n {{v_{ir}}{x_i}} )}^2})} \]

反倒是更简单了,为什么不用后面式子计算,而要用更复杂的前者呢?

这是由于ω的每一项并不是独立的,我们使用了vvT代替了ω

\[{\omega _{ij}} = \sum\limits_{r = 1}^k {{v_{ir}}v_{rj}^T}  = \sum\limits_{r = 1}^k {{v_{ir}}{v_{jr}}} \]

即可认为vi=(vi1,vi2,...,vik)代表了特征i与其他特征交叉关系的隐藏特征

如果不去掉交叉特征中的xixi项,则等同于vi不仅隐含了交叉关系,还隐含了xi的二次项,不符合我们的初衷

5,TensorFlow概述

TensorFlow即tensor+flow,tensor指数据,以任意维的类型化数组为具体形式,flow则指数据的流向

描述flow有三个层次的概念:

  • operation:是最基本的单元,每个operation获得0个或多个tensor,产生0个或多个tensor。operation可能为获取一个常量tf.constant(),获取一个变量tf.get_variable()或者进行一个操作,比如加法tf.add()、乘法tf.multiply()。
  • graph:定义了数据流,由tf.Graph()初始化(一般情况下省略)。graph由节点和节点间的连线构成,graph中的节点即operation。graph是对数据流向组成的网络结构的定义,本身并不进行任何计算。
  • session:定义了graph运行的环境,由tf.Session()初始化。session中可以运行graph中一个或多个operation,可以在运行前通过feed给operation输入数据,也可以通过返回值获取计算结果。

 

概念上讲,图中所有节点间的连线都是tensor,但总需要有一些数据作为数据来源,描述这些数据来源的组件有以下几种类型:

  • 常量:用tf.constant()定义,即运行过程中不会改变的数据,一般用来保存一些固有参数。常量可以被认为是一个operation,输入为空,输出为常数(数组)。
  • 变量:理论上广义的变量包含所有除常量以外的所有tensor,这里的变量专指tf.Variable()。值得注意的是,在TensorFlow中,小写开头的才是operation,大写开头的都是类,因此tf.Variable()不是一个operation,而是包含了一系列operation,如初始化、赋值的类成员。每个graph运行前必须初始化所有使用到的变量。
  • 占位符:用tf.placeholder()定义,是一种特殊类型的operation,不需要初始化,但需要在运行中通过feed接收数据。

6,LR的TensorFlow实现

首先需要定义输入样本的特征维度和接收输入样本特征和样本标签的占位符。

特征维度可以使用普通python变量指定,此时等同于使用tf.constant创建了一个常量

FEATURE_NUM = 8
#shape参数的列表长度代表占位符的维度
#如果shape不为None,则输入数据维度必须等同于shape参数列表长度
#每一维的大小可以不指定(为None),也可以指定,如果指定则输入数据该维度长度必须与shape指定的一致
x = tf.placeholder(tf.float32, shape=[None, FEATURE_NUM])
#等同于x = tf.placeholder(tf.float32, shape=[None, tf.constant(FEATURE_NUM, dtype=tf.int64)])
y = tf.placeholder(tf.float32, shape=[None])

不可以通过使用placeholder指定特征维度

m = tf.placeholder(tf.int64, shape=None)
x = tf.placeholder(tf.float32, shape=[None, m])  #无法执行
y = tf.placeholder(tf.float32, shape=[None])

虽然placeholder初始shape中不能包含其他tensor,但可以根据输入样本的列数动态指定

x = tf.placeholder(tf.float32, shape=[None, None])  #或者可以不指定维数,输入shape=None,此时可以输入任意维数,任意大小的数据
y = tf.placeholder(tf.float32, shape=[None])
m = tf.shape(x)[1]  #此处不能使用x.get_shape()[1].value,get_shape()获取的为静态大小,返回结果为[None,None]

接下来要定义放置LR假设函数中w与b的变量,在定义参数w前,需要定义一个w的初始值。对于LR来说,参数初始值为随机数或者全零都没关系,因为LR的损失函数一定是凸函数(求二阶导数可知4),但一般情况下还是习惯采用随机数初始化。

#常用的随机函数有random_uniform、random_normal和truncated_normal
#分别是均匀分布、正态分布、被截断的正态分布
weight_init = tf.truncated_normal(shape=[FEATURE_NUM, 1],mean=0.0,stddev=1.0)  #此处shape初始化只能使用常量,不支持使用tensor
weight = tf.Variable(weight_init)  
bais = tf.Variable([0.0])

为了保持习惯用法,将一维向量y扩展为二维列向量,TensorFlow中扩维可以使用tf.expand_dims()或者tf.reshape():

y_exband = tf.expand_dims(y, axis=1)  #方法1,axis表示在原若干个维度的间隔中第几个位置插入新维度
y_exband = tf.reshape(y, shape=[tf.shape(x)[0],1])  #方法2,shape为输出的维度值
y_exband = tf.reshape(y, shape=[-1,1])  #方法3,shape参数列表中最多只能有一个参数未知,写为-1

可知:

\[x \in {R^{m*n}},y \in {R^{m*1}},w \in {R^{n*1}},b \in {R^0}\]

回头看LR损失函数的定义

\[l(w,b) =  - \sum\limits_{j = 1}^m {({y^{(j)}}\log (g(b + \sum\limits_{i = 1}^n {{w_i}x_i^{(j)}} )) + (1 - {y^{(j)}})\log (1 - g(b + \sum\limits_{i = 1}^n {{w_i}x_i^{(j)}} ))} ,g(z) = \frac{1}{{1 + {e^{ - z}}}}\]

在TensorFlow中,“+”等同于tf.add(),“-”等同于tf.subtract(),“*”等同于tf.multiply(),“/”等同于tf.div(),所有这些操作是“逐项操作”:

  • 如果两个操作数都为标量,则结果为标量
  • 如果一个操作数为标量,另一个操作数为向量,则标量会分别与向量每一个元素逐个操作,得到结果
  • 如果两个操作数为相同维度,每个维度大小相同的向量,则结果为向量每两个对应位置上的元素的操作得到的结果
  • 如果两个操作数维度不相同,如果向量维度数为a、b,并且a>b,则b的各维度大小需要与a的高维度相对应,低维向量在高维向量的低维展开,操作得到结果
  • 除上面几种情况,调用不合法

因此,在逻辑回归的损失函数中,我们无需关心“b+”、“1-”等操作,这些标量自然会展开到正确的维度。同样,tf.log()、tf.sigmoid()函数也会对输入向量每一个元素逐个操作,不会改变向量的维度和对应值的位置。

对于式子中的求和Σ,在TensorFlow中有两种处理方式:

  • 一种是矩阵乘法tf.matmul(),如矩阵a、b、c关系为c=tf.matmul(a,b),则可得:

\[{c_{ij}} = \sum\limits_k {{a_{ik}}{b_{kj}}} \]

  • 另一种是矩阵压缩函数tf.reduce_sum()(求和)、tf.reduce_mean()(求平均)等函数。这些函数除了第一个参数为要操作的矩阵外,还有几个很有用的参数:reduction_indices是要压缩的维度。对于一个二维矩阵来说,压缩维度为0意味着压缩行,生成列向量,压缩维度为1意味着压缩列,生成行向量,reduction_indices=None意味着压缩成标量;keepdim指压缩后是否保持原有维度,如果keepdims=True,操作矩阵被压缩的维度长度会保持,但长度一定是1

在损失函数中,先看最外层求和项(以j求和),可以看做y与log函数的结果的逐项相乘求和,因此log函数的输出向量形状必须与y或者y的转置一致。而log函数的输出向量形状取决于内部求和项(以i求和)。可以通过求和式得到合适的运算形式为x*w,也可以通过形状来推理合适的运算形式,即:

\[tf.matmul(x,w) \in {R^{m*1}}\]

然后经过log函数得到的结果与y逐项相乘,再求压缩平均(此处之所以不是求和而是求平均,是为了避免样本数量对损失函数结果产生影响),可以得到假设函数、(负)似然函数、损失函数的运算式:

hypothesis = tf.sigmoid(tf.matmul(x, weight) + bais)
likelyhood = -(y_exband*tf.log(hypothesis) + (1.0-y_exband)*(tf.log(1.0-hypothesis)))
loss = tf.reduce_mean(likelyhood, reduction_indices=0)

然后利用TensorFlow的自动微分功能,即优化类之一来定义模型的优化过程:

LEARNING_RATE = 0.02
optimizer = tf.train.GradientDescentOptimizer(LEARNING_RATE)
training_op = optimizer.minimize(loss)

TensorFlow的优化类主要有以下几个:

  • GradientDescentOptimizer:最普通的批量梯度下降,令学习速率为η,t代表本次迭代,t+1代表下次迭代,则梯度迭代公式如下:

\[{\theta _{t + 1}} = {\theta _t} - \eta \frac{{\partial l(\theta )}}{{\partial \theta }}\]

  • AdagradOptimizer:进行参数迭代的同时记录了每个参数每次迭代的梯度的平方和,下次迭代时梯度与累积平方和的平方根成反比。这样会对低频的参数做较大的更新,对高频的参数做较小的更新,对于稀疏数据表现的更好;但是由于学习速率越来越小,有可能没有到达最低点学习速率就变得很慢了,难以收敛。令s为梯度累积平方和,ε为极小量,t代表本次迭代,t-1代表上次迭代,t+1代表下次迭代,梯度迭代公式如下:

\[{s_t} = {s_{t - 1}} + {(\frac{{\partial l(\theta )}}{{\partial \theta }})^2},{\theta _{t + 1}} = {\theta _t} - \frac{\eta }{{\sqrt {{s_t} + \varepsilon } }}\frac{{\partial l(\theta )}}{{\partial \theta }}\]

  • RMSPropOptimizer:为解决AdagradOptimizer后期更新速率过慢的问题,RMSprop使用加权累积平方和替换累积平方和。令m代表梯度加权累积平方和,ε为极小量,β为权重,t代表本次迭代,t-1代表上次迭代,t+1代表下次迭代,梯度迭代公式如下:

\[{m_t} = \beta {m_{t - 1}} + (1 - \beta ){(\frac{{\partial l(\theta )}}{{\partial \theta }})^2},{\theta _{t + 1}} = {\theta _t} - \frac{\eta }{{\sqrt {{m_t} + \varepsilon } }}\frac{{\partial l(\theta )}}{{\partial \theta }}\]

  • MomentumOptimizer:多一个必须参数“动量速率”,每次迭代时参考前一次迭代的“动量”,在迭代中方向不变的维度做较大的更新,迭代中方向反复改变的维度做较小的更新。适用于在不同维度梯度差距很大的情况,更新不会在小梯度方向反复震荡。令γ代表动量速率,t代表本次迭代,t-1代表上次迭代,t+1代表下次迭代,梯度迭代公式如下:

\[{v_t} = \gamma {v_{t - 1}} + \eta \frac{{\partial l(\theta )}}{{\partial \theta }},{\theta _{t + 1}} = {\theta _t} - {v_t}\]

  •  AdamOptimizer:综合了MomentumOptimizer和RMSPropOptimizer,既包含动量(一次项)部分也包含衰减(两次项)部分。令f代表动量,g代表衰减,β1、β2为权重,t代表本次迭代,t-1代表上次迭代,t+1代表下次迭代:

\[\begin{array}{l}
{f_t} = {\beta _2}{f_{t - 1}} + (1 - {\beta _2})\frac{{\partial l(\theta )}}{{\partial \theta }}\\
{g_t} = {\beta _1}{g_{t - 1}} + (1 - {\beta _1}){(\frac{{\partial l(\theta )}}{{\partial \theta }})^2}
\end{array}\]

        注意此处第一次迭代的梯度值和速率值偏小,为了校正偏差,计算校正后的动量f′和衰减g′,再计算迭代公式:

\[\begin{array}{l}
{{f‘}_t} = \frac{{{f_t}}}{{1 - {\beta _1}}}\\
{{g‘}_t} = \frac{{{g_t}}}{{1 - {\beta _2}}}\\
{\theta _{t + 1}} = {\theta _t} - \frac{\eta }{{\sqrt {{{g‘}_t} + \varepsilon } }}{{f‘}_t}
\end{array}\]

 最后还需要一个预测操作和准确率判断操作,使用TensorFlow搭建机器学习网络一般采用输出多维向量,使用tf.nn.softmax_cross_entropy_with_logits、

tf.nn.sparse_softmax_cross_entropy_with_logit或tf.nn.sigmoid_cross_entropy_with_logits预测样本label,但此处还严格按照LR原始定义来进行预测:

THRESHOLD = 0.5  
predictions = tf.sign(hypothesis-THRESHOLD)  #符号函数,判断向量对应位置的符号,输出对应位置为-1、0或1
labels = tf.sign(y_exband-THRESHOLD)
corrections = tf.equal(predictions, labels)  #比较向量对应位置的两个值,相等则输出对应位置为True
accuracy = tf.reduce_mean(tf.cast(corrections, tf.float32))

完整代码(包括测试运行代码)如下:

tf.reset_default_graph()  #清空Graph

FEATURE_NUM = 8  #特征数量
with tf.name_scope("lr"):
    x = tf.placeholder(tf.float32, shape=[None, FEATURE_NUM])
    y = tf.placeholder(tf.float32, shape=[None])

    weight_init = tf.truncated_normal(shape=[FEATURE_NUM, 1],mean=0.0,stddev=1.0)
    weight = tf.Variable(weight_init)  
    bais = tf.Variable([0.0])
    
    y_exband = tf.expand_dims(y, axis=1)
    
    hypothesis = tf.sigmoid(tf.matmul(x, weight) + bais)

with tf.name_scope("loss"):
    likelyhood = -(y_exband*tf.log(hypothesis) + (1.0-y_exband)*(tf.log(1.0-hypothesis)))
    loss = tf.reduce_mean(likelyhood, reduction_indices=0)

LEARNING_RATE = 0.02  #学习速率
with tf.name_scope("train"):
    optimizer = tf.train.GradientDescentOptimizer(LEARNING_RATE)
    training_op = optimizer.minimize(loss)
    
THRESHOLD = 0.5  #判断门限
with tf.name_scope("eval"):
    predictions = tf.sign(hypothesis-THRESHOLD)
    labels = tf.sign(y_exband-THRESHOLD)
    corrections = tf.equal(predictions, labels)
    accuracy = tf.reduce_mean(tf.cast(corrections, tf.float32))

init = tf.global_variables_initializer()  #初始化所有变量的operation

EPOCH = 10  #迭代次数
with tf.Session() as sess:
    sess.run(init)
    for i in range(EPOCH):
        _training_op, _loss = sess.run([training_op, loss], feed_dict={x: np.random.rand(10,8), y: np.random.rand(10)})
        _accuracy = sess.run([accuracy], feed_dict={x: np.random.rand(5,8), y: np.random.rand(5)})
        print "epoch:", i, _loss, _accuracy

7,FM的TensorFlow实现

待续 

 

 

 

 

参考文献:

 

FM(Factorization Machine)因式分解机 与 TensorFlow实现 详解

原文:https://www.cnblogs.com/jhc888007/p/10390282.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!