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 为初始权值,或者理解为偏置项,wi 为每个特征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转换。
故,我们需要对之前的理解进行修正:
- 我们口中的隐向量vi实际上是一个向量组,其形状为(输入特征One-hot后的长度,自定义长度);
- 隐向量vi代表的并不是embedding vector,而是在对输入进行embedding vector的向量组,也可理解为是一个权矩阵;
- 由输入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 # 初始化参数
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()
∂lossR(y,y)∂θ
=(y,y)∂y∂θ
FM算法解析及Python实现的更多相关文章
- DeepFM算法解析及Python实现
1. DeepFM算法的提出 由于DeepFM算法有效的结合了因子分解机与神经网络在特征学习中的优点:同时提取到低阶组合特征与高阶组合特征,所以越来越被广泛使用. 在DeepFM中,FM算法负责对一阶 ...
- GBDT+LR算法解析及Python实现
1. GBDT + LR 是什么 本质上GBDT+LR是一种具有stacking思想的二分类器模型,所以可以用来解决二分类问题.这个方法出自于Facebook 2014年的论文 Practical L ...
- FFM算法解析及Python实现
1. 什么是FFM? 通过引入field的概念,FFM把相同性质的特征归于同一个field,相当于把FM中已经细分的feature再次进行拆分从而进行特征组合的二分类模型. 2. 为什么需要FFM? ...
- 名人问题 算法解析与Python 实现 O(n) 复杂度 (以Leetcode 277. Find the Celebrity为例)
1. 题目描述 Problem Description Leetcode 277. Find the Celebrity Suppose you are at a party with n peopl ...
- Bayesian Personalized Ranking 算法解析及Python实现
1. Learning to Rank 1.1 什么是排序算法 为什么google搜索 ”idiot“ 后,会出现特朗普的照片? “我们已经爬取和存储了数十亿的网页拷贝在我们相应的索引位置.因此,你输 ...
- python常见排序算法解析
python——常见排序算法解析 算法是程序员的灵魂. 下面的博文是我整理的感觉还不错的算法实现 原理的理解是最重要的,我会常回来看看,并坚持每天刷leetcode 本篇主要实现九(八)大排序算法 ...
- 计量经济与时间序列_ACF自相关与PACF偏自相关算法解析(Python,TB(交易开拓者))
1 在时间序列中ACF图和PACF图是非常重要的两个概念,如果运用时间序列做建模.交易或者预测的话.这两个概念是必须的. 2 ACF和PACF分别为:自相关函数(系数)和偏自相关函数(系数). ...
- 【Python五篇慢慢弹(5)】类的继承案例解析,python相关知识延伸
类的继承案例解析,python相关知识延伸 作者:白宁超 2016年10月10日22:36:57 摘要:继<快速上手学python>一文之后,笔者又将python官方文档认真学习下.官方给 ...
- 地理围栏算法解析(Geo-fencing)
地理围栏算法解析 http://www.cnblogs.com/LBSer/p/4471742.html 地理围栏(Geo-fencing)是LBS的一种应用,就是用一个虚拟的栅栏围出一个虚拟地理边界 ...
随机推荐
- 通过 Ansible 安装 Docker
本文的演示环境为 ubuntu 16.04. 先在 Ansible Galaxy 搜索 docker,由 geerlingguy 贡献的 docker role 是目前最受欢迎的: 通过 ansibl ...
- -1-5 java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁 sleep()和wait()方法的区别 为什么wait(),notify(),notifyAll()等方法都定义在Object类中
本文关键词: java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁 sleep()和wait()方法的区别 为什么wait( ...
- webpack4.0各个击破(9)—— karma篇
webpack作为前端最火的构建工具,是前端自动化工具链最重要的部分,使用门槛较高.本系列是笔者自己的学习记录,比较基础,希望通过问题 + 解决方式的模式,以前端构建中遇到的具体需求为出发点,学习we ...
- C# 判断网卡类型以及其他网卡信息
NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces(); foreach (NetworkInterfac ...
- 配置多个 git 账号的 ssh密钥
背景 在工作中,我们通常会以 ssh 的方式配置公司的 git 账号,但是平时也会使用 github 管理自己的项目.因此,我们需要为自己的 github 创建一个新的 git 账号,这就需要生成新的 ...
- java数组及数组的插入,删除,冒泡算法
1.数组的定义 数组为相同类型的若干个数据,在一个数组里面,不能存放多种不同类型的数据,其中每个数据为该数组的一个元素,可以通过下标对改元素进行访问. 1.1 数组的特点 (1)数组被创建后,长度就已 ...
- php设计模式--简单介绍
鉴于最近有几个小伙伴总问一些设计模式相关的东西,本人借鉴了一些东西,准备将常见的几种php的设计模式总结整理出来. 平时我们用到的设计模式很多,建议大家多多读一些php开源框架,当深入的阅读了一些ph ...
- You are what you write——沈向洋
title: You are what you write--沈向洋 date: 2018-02-21 13:18:28 tags: [随想,write] categories: 个人文章 --- A ...
- mybatis基础(下)
mybatis和spring整合 需要spring通过单例方式管理SqlSessionFactory spring和mybatis整合生成代理对象,使用SqlSessionFactory创建SqlSe ...
- 关系型数据库中主键(primary key)和外键(foreign key)的概念。
刚接触关系型数据库的同学,会听过主键和外键的概念.这是关系型数据库的基本概念,需要清楚理解.今天我就以简洁的语言总结一下这个概念. 主键.一句话概括:一张表中,可以用于唯一标识一条记录的字段组(或者说 ...