推荐系统 - 基于FM算法的协同召回算法

详细理论参照此处:https://zhuanlan.zhihu.com/p/58160982

说明

1.FM是一种LR基础上扩展的线性模型,在特征上做了两两组合上的信息挖掘,因为其高效性,既可以作为召回模型,也可以作为排序模型,并且在数据量非常大时,其相对于lightGBM这样的模型,可以做流式训练,能承载更大数据量的训练,比如在2018腾讯广告算法大赛中,top选手在复赛大数据量情况下,多使用FFM、DeepFFM这种由FM扩展的模型。

2.FM主要在线性模型基础上加入了特征间的关联关系,其式子如下:

                                        

但是针对于上式,一个很大的问题,用户交互矩阵往往是比较稀疏的,这样就会导致对w的估算存在很大的问题,因此这里使用了矩阵分解的思想,对交叉项系数w进行 如下的矩阵分解

                                                            

我们称分解出的向量V为隐向量,V矩阵的第j列表示第j个特征的隐向量,从而可以将式子转换成如下的形式。

                                                 

其中<vi,vj>表示点乘,隐向量长度为k(k<<n),这样变换后,二次项的系数个数减少至kn个,隐向量矩阵对应的形式如下。

                                                      

经过V矩阵组合后,新的W可以表示为如下的形式。

                                             

2.算法主要分为两个步骤:

         (1)交互数据的embedding转换。根据用户和商品的出现情况做统计,构建出col_ix矩阵,利用data矩阵和row_ix来对col_ix做形式的包装,使用csr.csr_matrix做矩阵压缩获取根据 用户和商品交互 每行数据所对应的embedding形式。

          (2)将每行的embedding输入到FM模型中,进行下式所对应的交互。[FM的核心思想,两两xixj特征的交互]

                   

         然而从FM的原始数学公式看,因为在进行二阶(2-order)特征组合的时候,假设有n个不同的特征,那么二阶特征组合意味着任意两个特征都要进行交叉组合,所以可以直接推论得出:FM的时间复杂度是n的平方。但是如果故事仅仅讲到这里,FM模型是不太可能如此广泛地被工业界使用的。因为现实生活应用中的n往往是个非常巨大的特征数,如果FM是n平方的时间复杂度,那估计基本就没人带它玩了。

           因此我们对FM可以得到如下的公式改进,具体推导请参照https://zhuanlan.zhihu.com/p/58160982

                                         

           从而大大提升计算效率,通过输入embedding特征、进行分类预测,我们可以得到最终的推荐模型。

代码

数据集从此处下载:链接:https://pan.baidu.com/s/19dvx42ZJwSMUh_bXOAPNlw 
提取码:znhn 
 

代码如下,主要分为两个部分。   交互数据的embedding转化、 FM网络的训练两个过程。  这里并没有对不同特征进行不同的复杂处理。

# -*- encoding:utf-8 -*-
from itertools import count
from collections import defaultdict
from scipy.sparse import csr
import numpy as np
import pandas as pd
import numpy as np
from sklearn.feature_extraction import DictVectorizer
import tensorflow as tf
from tqdm import tqdm_notebook as tqdm


def vectorize_dic(dic,ix=None,p=None,n=0,g=0):
    """
    dic -- dictionary of feature lists. Keys are the name of features
    ix -- index generator (default None)
    p -- dimension of feature space (number of columns in the sparse matrix) (default None)
    把字典直接输入网络是非常不合适的,所以这里从id里面提取特征,做了embedding的一个向量化表示。

    这里把  项的字典  转换成了   embedding表示,至于怎么简单对id进行词向量转换的,还挺有意思的,字典的向量转换(这里可以看到进行压缩前,比较有意义的矩阵是 对出现次数统计的矩阵)
    其余还有全为0和全为1的参与到 稀疏矩阵的合成中。
                            csr.csr_matrix: [[0. 2. 0. ... 0. 0. 0.]
                         [0. 1. 1. ... 0. 0. 0.]
                         [0. 1. 0. ... 0. 0. 0.]
                         ...
                         [0. 0. 0. ... 0. 0. 0.]
                         [0. 0. 0. ... 0. 0. 0.]
                         [0. 0. 0. ... 0. 0. 0.]]
        向量转换的简单计算再这里,可以看得出是简单的  出现次数下的计算累加  (这里)
            for k,lis in dic.items():
                for t in range(len(lis)):
                    ix[str(lis[t]) + str(k)] = ix.get(str(lis[t]) + str(k),0) + 1
                    col_ix[i+t*g] = ix[str(lis[t]) + str(k)]
                i += 1

        常常需要把一个稀疏的np.array压缩,这时候就用到scipy库中的sparse.csr_matrix(csr:Compressed Sparse Row marix) 和sparse.csc_matric(csc:Compressed Sparse Column marix)

        现在明白了,这就是一个对特征 的字典形式存储转成 的二维矩阵 形式,外加矩阵的压缩。  从而实现对  特征的embedding转换,由id索引转成词向量。


        接下来就是将向量投入矩阵进行训练即可, 这就是一个线性模型,所以是比较简单的网络处理思路。  这里field也并不是分开处理的。


        这里是对
    """
    print('dic:',dic)
    if ix==None:
        ix = dict()

    nz = n * g

    col_ix = np.empty(nz,dtype = int)

    i = 0
    for k,lis in dic.items():
        for t in range(len(lis)):
            ix[str(lis[t]) + str(k)] = ix.get(str(lis[t]) + str(k),0) + 1
            col_ix[i+t*g] = ix[str(lis[t]) + str(k)]
        i += 1

    row_ix = np.repeat(np.arange(0,n),g)
    data = np.ones(nz)
    if p == None:
        p = len(ix)

    ixx = np.where(col_ix < p)    #这里  row_ix[ixx]和  col_ix[ixx]  都是一个通过不断累加统计下的二维矩阵,行是field,列是域列值的数量。
    print('csr.csr_matrix:', csr.csr_matrix((c[ixx],(row_ix[ixx],col_ix[ixx])),shape=(n,p)).todense())
    print('csr.csr_matrix shape:', csr.csr_matrix((data[ixx], (row_ix[ixx], col_ix[ixx])), shape=(n, p)).todense().shape)
    print('data[ixx] shape :',data)
    print('row_ix[ixx] shape :', row_ix)
    print('col_ix[ixx] shape :',col_ix)

    return csr.csr_matrix((data[ixx],(row_ix[ixx],col_ix[ixx])),shape=(n,p)),ix


def batcher(X_, y_=None, batch_size=-1):
    n_samples = X_.shape[0]

    if batch_size == -1:
        batch_size = n_samples
    if batch_size < 1:
       raise ValueError('Parameter batch_size={} is unsupported'.format(batch_size))

    for i in range(0, n_samples, batch_size):
        upper_bound = min(i + batch_size, n_samples)
        ret_x = X_[i:upper_bound]
        ret_y = None
        if y_ is not None:
            ret_y = y_[i:i + batch_size]
            yield (ret_x, ret_y)


cols = ['user','item','rating','timestamp']

train = pd.read_csv('data/ua.base',delimiter='\t',names = cols)
test = pd.read_csv('data/ua.test',delimiter='\t',names = cols)

x_train,ix = vectorize_dic({'users':train['user'].values,
                            'items':train['item'].values},n=len(train.index),g=2)
print('x_train:',x_train)

x_test,ix = vectorize_dic({'users':test['user'].values,
                           'items':test['item'].values},ix,x_train.shape[1],n=len(test.index),g=2)


print(x_train)
y_train = train['rating'].values
y_test = test['rating'].values

x_train = x_train.todense()
x_test = x_test.todense()

print('x_train  todense:', x_train)

print(x_train.shape)
print (x_test.shape)


n,p = x_train.shape

k = 10


x = tf.placeholder('float',[None,p])

y = tf.placeholder('float',[None,1])

w0 = tf.Variable(tf.zeros([1]))
w = tf.Variable(tf.zeros([p]))

v = tf.Variable(tf.random_normal([k,p],mean=0,stddev=0.01))

#y_hat = tf.Variable(tf.zeros([n,1]))

linear_terms = tf.add(w0,tf.reduce_sum(tf.multiply(w,x),1,keep_dims=True)) # n * 1
pair_interactions = 0.5 * tf.reduce_sum(
    tf.subtract(
        tf.pow(
            tf.matmul(x,tf.transpose(v)),2),
        tf.matmul(tf.pow(x,2),tf.transpose(tf.pow(v,2)))
    ),axis = 1 , keep_dims=True)

y_hat = tf.add(linear_terms,pair_interactions)

lambda_w = tf.constant(0.001,name='lambda_w')
lambda_v = tf.constant(0.001,name='lambda_v')

l2_norm = tf.reduce_sum(
    tf.add(
        tf.multiply(lambda_w,tf.pow(w,2)),
        tf.multiply(lambda_v,tf.pow(v,2))
    )
)

error = tf.reduce_mean(tf.square(y-y_hat))
loss = tf.add(error,l2_norm)


train_op = tf.train.GradientDescentOptimizer(learning_rate=0.01).minimize(loss)


epochs = 10
batch_size = 1000

# Launch the graph
init = tf.global_variables_initializer()
with tf.Session() as sess:
    sess.run(init)

    for epoch in tqdm(range(epochs), unit='epoch'):
        perm = np.random.permutation(x_train.shape[0]) #随机打乱,np.random.permutation(x):不在原数组上进行,返回新的数组,不改变自身数组。
        # iterate over batches
        for bX, bY in batcher(x_train[perm], y_train[perm], batch_size):
            _,t = sess.run([train_op,loss], feed_dict={x: bX.reshape(-1, p), y: bY.reshape(-1, 1)})
            print(t)


    errors = []
    for bX, bY in batcher(x_test, y_test):
        errors.append(sess.run(error, feed_dict={x: bX.reshape(-1, p), y: bY.reshape(-1, 1)}))
        print(errors)
    RMSE = np.sqrt(np.array(errors).mean())
    print (RMSE)






 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值