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 相关文章推荐
Python随机生成带特殊字符的密码
Mar 02 Python
Python编程之变量赋值操作实例分析
Jul 24 Python
简单学习Python多进程Multiprocessing
Aug 29 Python
python实现换位加密算法的示例
Oct 14 Python
python使用pygame框架实现推箱子游戏
Nov 20 Python
Python判断一个list中是否包含另一个list全部元素的方法分析
Dec 24 Python
python使用suds调用webservice接口的方法
Jan 03 Python
解决在Python编辑器pycharm中程序run正常debug错误的问题
Jan 17 Python
Python实现将HTML转成PDF的方法分析
May 04 Python
django 利用Q对象与F对象进行查询的实现
May 15 Python
Python+PyQt5+MySQL实现天气管理系统
Jun 16 Python
Python自动化xpath实现自动抢票抢货
Sep 19 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
那些年我们错过的魔术方法(Magic Methods)
2014/01/14 PHP
最新版本PHP 7 vs HHVM 多角度比较
2016/02/14 PHP
PHP打印输出函数汇总
2016/08/28 PHP
window.onload 加载完毕的问题及解决方案(下)
2009/07/09 Javascript
window.location.hash 使用说明
2010/11/08 Javascript
jQuery调用WebService的实现代码
2011/06/19 Javascript
php显示当前文件所在的文件以及文件夹所有文件以树形展开
2013/12/13 Javascript
js 异步操作回调函数如何控制执行顺序
2013/12/24 Javascript
javascript实现的简单的表单验证
2015/07/10 Javascript
深入理解jQuery layui分页控件的使用
2016/08/17 Javascript
总结十个Angular.js由浅入深的面试问题
2016/08/26 Javascript
微信小程序 wx.request(接口调用方式)详解及实例
2016/11/23 Javascript
JS实现滑动门效果的方法详解
2016/12/19 Javascript
关于前后端json数据的发送与接收详解
2017/07/30 Javascript
微信小程序表单弹窗实例
2018/07/19 Javascript
Vue cli3 库模式搭建组件库并发布到 npm的流程
2018/10/12 Javascript
微信小程序canvas绘制圆角base64图片的实现
2019/08/18 Javascript
vue cli3 配置proxy代理无效的解决
2019/10/30 Javascript
Vue结合路由配置递归实现菜单栏功能
2020/06/16 Javascript
vue 使用async写数字动态加载效果案例
2020/07/18 Javascript
js将日期格式转换为YYYY-MM-DD HH:MM:SS
2020/09/18 Javascript
[04:40]2016国际邀请赛中国区预选赛全程TOP10镜头集锦
2016/07/01 DOTA
[05:23]DOTA2-DPC中国联赛2月1日Recap集锦
2021/03/11 DOTA
浅谈Python使用Bottle来提供一个简单的web服务
2017/12/27 Python
浅谈Django学习migrate和makemigrations的差别
2018/01/18 Python
1分钟快速生成用于网页内容提取的xslt
2018/02/23 Python
python判断单向链表是否包括环,若包含则计算环入口的节点实例分析
2019/10/23 Python
如何用OpenCV -python3实现视频物体追踪
2019/12/04 Python
python字符串下标与切片及使用方法
2020/02/13 Python
基于python获取本地时间并转换时间戳和日期格式
2020/10/27 Python
英国第一独立滑雪板商店:The Snowboard Asylum
2020/01/16 全球购物
Structs界面控制层技术
2013/10/11 面试题
合作经营协议书范本
2014/04/17 职场文书
应届生简历自我评价
2015/03/11 职场文书
go 实现简易端口扫描的示例
2021/05/22 Golang
Java 语言中Object 类和System 类详解
2021/07/07 Java/Android