Python实现FM算法解析


Posted in Python onJune 18, 2019

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算法前,我们先回顾一下最常见的线性表达式:

Python实现FM算法解析

其中w0为初始权值,或者理解为偏置项,wi为每个特征xi对应的权值。可以看到,这种线性表达式只描述了每个特征与输出的关系。

FM的表达式如下,可观察到,只是在线性表达式后面加入了新的交叉项特征及对应的权值。

Python实现FM算法解析

5. FM交叉项的展开

5.1 寻找交叉项

FM表达式的求解核心在于对交叉项的求解。下面是很多人用来求解交叉项的展开式,对于第一次接触FM算法的人来说可能会有疑惑,不知道公式怎么展开的,接下来笔者会手动推导一遍。

Python实现FM算法解析

设有3个变量(特征)x1 x2 x3,每一个特征的隐变量分别为v1=(1 2 3)、v2=(4 5 6)、v3=(1 2 1),即:

Python实现FM算法解析

设交叉项所组成的权矩阵W为对称矩阵,之所以设为对称矩阵是因为对称矩阵有可以用向量乘以向量转置替代的性质。
那么W=VVT,即

Python实现FM算法解析

所以:

Python实现FM算法解析

实际上,我们应该考虑的交叉项应该是排除自身组合的项,即对于x1x1、x2x2、x3x3不认为是交叉项,那么真正的交叉项为x1x2、x1x3、x2x1、x2x3、x3x1、x3x2。

去重后,交叉项即x1x2、x1x3、x2x3。这也是公式中1/2出现的原因。

5.2 交叉项权值转换

对交叉项有了基本了解后,下面将进行公式的分解,还是以n=3为例,

Python实现FM算法解析

所以:

Python实现FM算法解析

wij可记作Python实现FM算法解析Python实现FM算法解析,这取决于vi是1*3 还是3*1 向量。

5.3 交叉项展开式

上面的例子是对3个特征做的交叉项推导,因此对具有n个特征,FM的交叉项公式就可推广为:

Python实现FM算法解析

我们还可以进一步分解:

Python实现FM算法解析

所以FM算法的交叉项最终可展开为:

Python实现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):

Python实现FM算法解析

所以回归问题的损失函数对权值的梯度(导数)为:

Python实现FM算法解析

如果是二分类问题,损失函数一般是logit loss:

Python实现FM算法解析

其中,Python实现FM算法解析表示的是阶跃函数Sigmoid。

Python实现FM算法解析

所以分类问题的损失函数对权值的梯度(导数)为:

Python实现FM算法解析

Python实现FM算法解析

相应的,对于常数项、一次项、交叉项的导数分别为:

Python实现FM算法解析

7. FM算法的Python实现

FM算法的Python实现流程图如下:

Python实现FM算法解析

我们需要注意以下四点:

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算法代码片段

# 初始化参数
    w = zeros((n, 1)) # 其中n是特征的个数
    w_0 = 0.
    v = normalvariate(0, 0.2) * ones((n, k))
    for it in range(self.iter): # 迭代次数
      # 对每一个样本,优化
      for x in range(m):
        # 这边注意一个数学知识:对应点积的地方通常会有sum,对应位置积的地方通常都没有,详细参见矩阵运算规则,本处计算逻辑在:http://blog.csdn.net/google19890102/article/details/45532745
        # xi·vi,xi与vi的矩阵点积
        inter_1 = dataMatrix[x] * v
        # xi与xi的对应位置乘积  与  xi^2与vi^2对应位置的乘积  的点积
        inter_2 = multiply(dataMatrix[x], dataMatrix[x]) * multiply(v, v) # multiply对应元素相乘
        # 完成交叉项,xi*vi*xi*vi - xi^2*vi^2
        interaction = sum(multiply(inter_1, inter_1) - inter_2) / 2.
        # 计算预测的输出
        p = w_0 + dataMatrix[x] * w + interaction
        print('classLabels[x]:',classLabels[x])
        print('预测的输出p:', p)
        # 计算sigmoid(y*pred_y)-1准确的说不是loss,原作者这边理解的有问题,只是作为更新w的中间参数,这边算出来的是越大越好,而下面却用了梯度下降而不是梯度上升的算法在
        loss = self.sigmoid(classLabels[x] * p[0, 0]) - 1
        if loss >= -1:
          loss_res = '正方向 '
        else:
          loss_res = '反方向'
        # 更新参数
        w_0 = w_0 - self.alpha * loss * classLabels[x]
        for i in range(n):
          if dataMatrix[x, i] != 0:
            w[i, 0] = w[i, 0] - self.alpha * loss * classLabels[x] * dataMatrix[x, i]
            for j in range(k):
              v[i, j] = v[i, j] - self.alpha * loss * classLabels[x] * (
                  dataMatrix[x, i] * inter_1[0, j] - v[i, j] * dataMatrix[x, i] * dataMatrix[x, i])

FM算法完整实现

# -*- coding: utf-8 -*-

from __future__ import division
from math import exp
from numpy import *
from random import normalvariate # 正态分布
from sklearn import preprocessing
import numpy as np

'''
  data : 数据的路径
  feature_potenital : 潜在分解维度数
  alpha : 学习速率
  iter : 迭代次数
  _w,_w_0,_v : 拆分子矩阵的weight
  with_col : 是否带有columns_name
  first_col : 首列有价值的feature的index
'''


class fm(object):
  def __init__(self):
    self.data = None
    self.feature_potential = None
    self.alpha = None
    self.iter = None
    self._w = None
    self._w_0 = None
    self.v = None
    self.with_col = None
    self.first_col = None

  def min_max(self, data):
    self.data = data
    min_max_scaler = preprocessing.MinMaxScaler()
    return min_max_scaler.fit_transform(self.data)

  def loadDataSet(self, data, with_col=True, first_col=2):
    # 我就是闲的蛋疼,明明pd.read_table()可以直接度,非要搞这样的,显得代码很长,小数据下完全可以直接读嘛,唉~
    self.first_col = first_col
    dataMat = []
    labelMat = []
    fr = open(data)
    self.with_col = with_col
    if self.with_col:
      N = 0
      for line in fr.readlines():
        # N=1时干掉列表名
        if N > 0:
          currLine = line.strip().split()
          lineArr = []
          featureNum = len(currLine)
          for i in range(self.first_col, featureNum):
            lineArr.append(float(currLine[i]))
          dataMat.append(lineArr)
          labelMat.append(float(currLine[1]) * 2 - 1)
        N = N + 1
    else:
      for line in fr.readlines():
        currLine = line.strip().split()
        lineArr = []
        featureNum = len(currLine)
        for i in range(2, featureNum):
          lineArr.append(float(currLine[i]))
        dataMat.append(lineArr)
        labelMat.append(float(currLine[1]) * 2 - 1)
    return mat(self.min_max(dataMat)), labelMat

  def sigmoid(self, inx):
    # return 1.0/(1+exp(min(max(-inx,-10),10)))
    return 1.0 / (1 + exp(-inx))

  # 得到对应的特征weight的矩阵
  def fit(self, data, feature_potential=8, alpha=0.01, iter=100):
    # alpha是学习速率
    self.alpha = alpha
    self.feature_potential = feature_potential
    self.iter = iter
    # dataMatrix用的是mat, classLabels是列表
    dataMatrix, classLabels = self.loadDataSet(data)
    print('dataMatrix:',dataMatrix.shape)
    print('classLabels:',classLabels)
    k = self.feature_potential
    m, n = shape(dataMatrix)
    # 初始化参数
    w = zeros((n, 1)) # 其中n是特征的个数
    w_0 = 0.
    v = normalvariate(0, 0.2) * ones((n, k))
    for it in range(self.iter): # 迭代次数
      # 对每一个样本,优化
      for x in range(m):
        # 这边注意一个数学知识:对应点积的地方通常会有sum,对应位置积的地方通常都没有,详细参见矩阵运算规则,本处计算逻辑在:http://blog.csdn.net/google19890102/article/details/45532745
        # xi·vi,xi与vi的矩阵点积
        inter_1 = dataMatrix[x] * v
        # xi与xi的对应位置乘积  与  xi^2与vi^2对应位置的乘积  的点积
        inter_2 = multiply(dataMatrix[x], dataMatrix[x]) * multiply(v, v) # multiply对应元素相乘
        # 完成交叉项,xi*vi*xi*vi - xi^2*vi^2
        interaction = sum(multiply(inter_1, inter_1) - inter_2) / 2.
        # 计算预测的输出
        p = w_0 + dataMatrix[x] * w + interaction
        print('classLabels[x]:',classLabels[x])
        print('预测的输出p:', p)
        # 计算sigmoid(y*pred_y)-1
        loss = self.sigmoid(classLabels[x] * p[0, 0]) - 1
        if loss >= -1:
          loss_res = '正方向 '
        else:
          loss_res = '反方向'
        # 更新参数
        w_0 = w_0 - self.alpha * loss * classLabels[x]
        for i in range(n):
          if dataMatrix[x, i] != 0:
            w[i, 0] = w[i, 0] - self.alpha * loss * classLabels[x] * dataMatrix[x, i]
            for j in range(k):
              v[i, j] = v[i, j] - self.alpha * loss * classLabels[x] * (
                  dataMatrix[x, i] * inter_1[0, j] - v[i, j] * dataMatrix[x, i] * dataMatrix[x, i])
      print('the no %s times, the loss arrach %s' % (it, loss_res))
    self._w_0, self._w, self._v = w_0, w, v

  def predict(self, X):
    if (self._w_0 == None) or (self._w == None).any() or (self._v == None).any():
      raise NotFittedError("Estimator not fitted, call `fit` first")
    # 类型检查
    if isinstance(X, np.ndarray):
      pass
    else:
      try:
        X = np.array(X)
      except:
        raise TypeError("numpy.ndarray required for X")
    w_0 = self._w_0
    w = self._w
    v = self._v
    m, n = shape(X)
    result = []
    for x in range(m):
      inter_1 = mat(X[x]) * v
      inter_2 = mat(multiply(X[x], X[x])) * multiply(v, v) # multiply对应元素相乘
      # 完成交叉项
      interaction = sum(multiply(inter_1, inter_1) - inter_2) / 2.
      p = w_0 + X[x] * w + interaction # 计算预测的输出
      pre = self.sigmoid(p[0, 0])
      result.append(pre)
    return result

  def getAccuracy(self, data):
    dataMatrix, classLabels = self.loadDataSet(data)
    w_0 = self._w_0
    w = self._w
    v = self._v
    m, n = shape(dataMatrix)
    allItem = 0
    error = 0
    result = []
    for x in range(m):
      allItem += 1
      inter_1 = dataMatrix[x] * v
      inter_2 = multiply(dataMatrix[x], dataMatrix[x]) * multiply(v, v) # multiply对应元素相乘
      # 完成交叉项
      interaction = sum(multiply(inter_1, inter_1) - inter_2) / 2.
      p = w_0 + dataMatrix[x] * w + interaction # 计算预测的输出
      pre = self.sigmoid(p[0, 0])
      result.append(pre)
      if pre < 0.5 and classLabels[x] == 1.0:
        error += 1
      elif pre >= 0.5 and classLabels[x] == -1.0:
        error += 1
      else:
        continue
    # print(result)
    value = 1 - float(error) / allItem
    return value


class NotFittedError(Exception):
  """
  Exception class to raise if estimator is used before fitting
  """
  pass


if __name__ == '__main__':
  fm()

 以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
pymssql数据库操作MSSQL2005实例分析
May 25 Python
Python编程实现微信企业号文本消息推送功能示例
Aug 21 Python
Python编程之黑板上排列组合,你舍得解开吗
Oct 30 Python
详解Django中六个常用的自定义装饰器
Jul 04 Python
Python import与from import使用及区别介绍
Sep 06 Python
python 3.74 运行import numpy as np 报错lib\site-packages\numpy\__init__.py
Oct 06 Python
使用python实现微信小程序自动签到功能
Apr 27 Python
python如何调用字典的key
May 25 Python
简单了解Django项目应用创建过程
Jul 06 Python
搭建pypi私有仓库实现过程详解
Nov 25 Python
如何在向量化NumPy数组上进行移动窗口
May 18 Python
深入解析NumPy中的Broadcasting广播机制
May 30 Python
python pygame实现五子棋小游戏
Oct 26 #Python
PyQt 实现使窗口中的元素跟随窗口大小的变化而变化
Jun 18 #Python
python制作简单五子棋游戏
Jun 18 #Python
Python利用pandas处理Excel数据的应用详解
Jun 18 #Python
PyQt5固定窗口大小的方法
Jun 18 #Python
Python格式化字符串f-string概览(小结)
Jun 18 #Python
Python 安装第三方库 pip install 安装慢安装不上的解决办法
Jun 18 #Python
You might like
谈谈新手如何学习PHP
2006/12/14 PHP
PHP FATAL ERROR: CALL TO UNDEFINED FUNCTION BCMUL()解决办法
2014/05/04 PHP
使用Composer安装Yii框架的方法
2016/03/15 PHP
php分页原理 分页代码 分页类制作教程
2016/09/23 PHP
关于 byval 与 byref 的区别分析总结
2007/10/08 Javascript
基于jquery的button默认enter事件(回车事件)。
2011/05/18 Javascript
jquery事件与函数的使用介绍
2013/09/29 Javascript
基于JavaScript实现手机短信按钮倒计时(超简单)
2015/12/30 Javascript
JS使用ActiveXObject实现用户提交表单时屏蔽敏感词功能
2017/06/20 Javascript
解决vue里碰到 $refs 的问题的方法
2017/07/13 Javascript
vue中各选项及钩子函数执行顺序详解
2018/08/25 Javascript
js代码规范之Eslint安装与配置详解
2018/09/08 Javascript
vue中render函数的使用详解
2018/10/12 Javascript
vue基于element-ui的三级CheckBox复选框功能的实现代码
2018/10/15 Javascript
VUE脚手架的下载和配置步骤详解
2019/04/01 Javascript
JS面向对象之单选框实现
2020/01/17 Javascript
Nuxt.js的路由跳转操作(页面跳转nuxt-link)
2020/11/06 Javascript
python实现生命游戏的示例代码(Game of Life)
2018/01/24 Python
Python获取二维矩阵每列最大值的方法
2018/04/03 Python
python计算列表内各元素的个数实例
2018/06/29 Python
python的debug实用工具 pdb详解
2019/07/12 Python
python中下标和切片的使用方法解析
2019/08/27 Python
Python中的list与tuple集合区别解析
2019/10/12 Python
使用Pycharm(Python工具)新建项目及创建Python文件的教程
2020/04/26 Python
CSS3圆角和渐变2种常用功能详解
2016/01/06 HTML / CSS
新奥尔良珠宝:Mignon Faget
2020/11/23 全球购物
女儿十岁生日答谢词
2014/01/27 职场文书
工厂采购员岗位职责
2014/04/08 职场文书
常务副总经理任命书
2014/06/05 职场文书
一年级班主任工作总结2014
2014/11/08 职场文书
2014年后备干部工作总结
2014/12/08 职场文书
2015年毕业实习工作总结
2014/12/12 职场文书
七一表彰大会简报
2015/07/20 职场文书
2016幼儿园中班开学寄语
2015/12/03 职场文书
python opencv人脸识别考勤系统的完整源码
2021/04/26 Python
vue elementUI批量上传文件
2022/04/26 Vue.js