使用例子

使用经典的Movielens 100k dataset数据集作为推荐的例子. 在lightFM中, 可以直接调用函数获取整理成稀疏矩阵格式的数据:

from lightfm.datasets import fetch_movielens

data = fetch_movielens(min_rating=5.0)

但在这里, 我们将从原始的数据开始, 记录从原始数据到可供lightFM模型使用的数据, 即稀疏矩阵形式(coo, csr). 从而将数据处理的方法直接使用在各种推荐数据集上.

读取数据

下载好的数据集目录中包含很多文件, 其中的u.data包含了所有数据. 文件中每一行为一个交互样本, 格式为user id | item id | rating | timestamp, 列之间以tab符分隔. 使用pandas读取数据.

import codecs
import numpy as np
import pandas as pd

inter_file = "./data/ml-100k/u.data"

data = pd.read_csv(inter_file, delimiter="\t", header=None)
data.columns = ["user_id", "item_id", "rating", "timestamp"]
data.shape
(100000, 4)

共有100000条行为数据.

转为稀疏矩阵

lightFM模型中个各种方法只接受稀疏矩阵的形式, 因此需要把这种行形式的行为数据转换为(user, item)这种稀疏矩阵的形式. 固然可以使用scipy中的方法创建可以空的系数矩阵, 然后循环行为样本, 将对应位置填值, 但lightFM中提供了包装好的类来进行处理.

而且由于lightFM中使用useritem的特征, 使得整体数据情况比较复杂. 通过lightFM中的Dataset类就能够很好地进行管理. lightfm.data.Dataset能够做到:

  • useritem自身与整数索引之间的映射关系

  • useritem特征的管理, 包括特征的顺序等

  • 生成(user, item)对的稀疏矩阵

  • 生成useritem的特征数据结构

首先创建一个Dataset.

from lightfm.data import Dataset
dataset = Dataset()

Dataset__init__函数中有两个参数:

  • user_identity_features

  • item_identity_features

这两个参数默认值都为True. 以第一个参数为例, 作用是: Create a unique feature for every user in addition to other features. 即将每个用户本身作为一个特征, 追加到现有的user特征中, 其实就是用户的One-Hot特征.

如果没有任何useritem特征, 则这两个参数必须指定为True. 每个user, 每个item都是一个独特的特征, 只会被该useritem使用到, 其他的不会使用. 在这种情况下, lightFM算法也就等价于普通的FM算法.

在初始化完毕之后, 接下来需要将user列表item列表为模型指定, 这一步需要调用Datasetfit方法. 需要注意这里的fit方法投入的不是行为数据, 只是所有useritem分别组成的列表.

fit方法接收下面4个参数:

  • users: iterable of user ids, user列表

  • items: iterable of item ids, item列表

  • user_features: iterable of user features, optional, user feature的名称列表

  • item_features: iterable of item features, optional, item feature的名称列表

这一步的作用是对每个useritem指定一个整数ID, 这个id就是传入列表中元素的顺序. ID从0开始, 在lightFM中使用的这种整数索引, 而不是原始的user, item名称.

然后就可以通过函数看到useritem的交互矩阵的大小了.

num_users, num_items = dataset.interactions_shape()
num_users, num_items
(943, 1682)

共943个user, 1682个item.

如果后续需要增加user, item或user feature和item feature的内容, 只需要调用dataset.fit_partial, 参数形式与fit方法相同, 因为fit方法的内部也是调用了fit_partial完成的.

接下来就是生成行为矩阵(interactions matrix)了. 这就需要用到原始的行形式的行为数据. 使用dataset.build_interactions函数, 这个函数有以下的重要参数:

  • data: iterable of (user_id, item_id) or (user_id, item_id, weight).

    • 即是一个迭代器, 每个元素符合上面两种形式, 分别是行为数据与打分数据.

该函数的返回值有两个:

  • (interactions, weights)

    • 两个都是COO matrix的格式

    • 分别表示是否有交互行为, 和对应的权值. 其中weights在评分系统中有用, 在表示是否有交互行为时, 由于传入的data中每个元素的格式为(user_id, item_id), 此时生成的weights中的权值都为1, 因此与interactions是完全相同的

这一步耗时比较长.

interactions, weights = dataset.build_interactions([tuple(data[["user_id", "item_id", "rating"]].iloc[i, :]) for i in range(data.shape[0])])
interactions, weights
(<943x1682 sparse matrix of type '<class 'numpy.int32'>'
     with 100000 stored elements in COOrdinate format>,
 <943x1682 sparse matrix of type '<class 'numpy.float32'>'
     with 100000 stored elements in COOrdinate format>)
interactions.toarray(), weights.toarray()
(array([[1., 1., 1., ..., 0., 0., 0.],
        [1., 0., 0., ..., 0., 0., 0.],
        [0., 0., 0., ..., 0., 0., 0.],
        ...,
        [1., 0., 0., ..., 0., 0., 0.],
        [0., 0., 0., ..., 0., 0., 0.],
        [0., 1., 0., ..., 0., 0., 0.]], dtype=float32),
 array([[5., 3., 4., ..., 0., 0., 0.],
        [4., 0., 0., ..., 0., 0., 0.],
        [0., 0., 0., ..., 0., 0., 0.],
        ...,
        [5., 0., 0., ..., 0., 0., 0.],
        [0., 0., 0., ..., 0., 0., 0.],
        [0., 5., 0., ..., 0., 0., 0.]], dtype=float32))

拟合模型

在得到了稀疏矩阵形式的交互数据之后, 就可以创建模型, 并进行拟合操作了.

from lightfm import LightFM

LightFM是一种hybrid latent representation recommender model. 在初始化时, 需要注意一下的参数:

  • no_components: int, optional

    • 隐向量的长度, 默认为10. the dimensionality of the feature latent embeddings.

  • learning_schedule: string, optional

    • 学习率的变化策略, one of ('adagrad', 'adadelta'), 默认为adagrad

  • loss: string, optional

    • 训练时使用的损失函数, 这个参数非常重要, 不同的场景需要指定不同的损失函数

    • one of ('logistic', 'bpr', 'warp', 'warp-kos')

    • logistic: useful when both positive (1) and negative (-1) interactions are present

    • bpr: Bayesian Personalised Ranking. 目标是最大化正样本与随机一个负样本之间差值. 在以下的场景中使用:

      • only positive interactions are present

      • 需要最优的ROC, AUC

    • warp: Weighted Approximate-Rank Pairwise. 在以下的场景中使用:

      • only positive interactions are present

      • optimising the top of the recommendation list (precision@k) is desired

    • k-os warp: k-th order statistic loss. warp的一个变种

  • learning_rate: 初始学习率

  • item_alpha: L2 penalty on item features, 默认为0.0, 即不使用正则化. 开启正则化会降低训练的速度.

    • 使用时需检查最后输出的embedding weights是否都近似于0, 如果是说明这个值设定的太大了

  • user_alpha: L2 penalty on user features, 默认为0.0, 关闭

  • max_sampled: maximum number of negative samples used during WARP fitting.

  • random_state

然后是调用训练方法fit需要注意的参数:

fit(interactions, user_features=None, item_features=None, sample_weight=None, epochs=1, num_threads=1, verbose=False):
  • interactions: np.float32 coo_matrix of shape [n_users, n_items]

    • 交互稀疏矩阵. 如果只有行为, 矩阵内的非0处值都为1; 如果是评分系统, 非0处的值为任意数值.

  • user_features: np.float32 csr_matrix of shape [n_users, n_user_features], optional

    • 每行代表一个user, 行中的每个值代表这个user对于每个用户特征的权值

  • item_features: np.float32 csr_matrix of shape [n_items, n_item_features], optional

  • sample_weight: np.float32 coo_matrix of shape [n_users, n_items], optional

  • epochs: 训练的epoch次数

  • num_threads

  • verbose: bool, optional, 默认为False

训练好之后的权值可以通过以下四个属性得到:

  • item_embeddings: np.float32 array of shape [n_item_features, n_components]

  • user_embeddings: np.float32 array of shape [n_user_features, n_components]

  • item_biases: np.float32 array of shape [n_item_features,]

  • user_biases: np.float32 array of shape [n_user_features,]

通常来说, warp

model = LightFM(loss='warp')
model.fit(interactions)

查看item对应的embedding:

model.item_embeddings
array([[-0.09082373,  0.3682693 ,  0.39377818, ..., -0.5586541 ,
         0.75429815,  0.7312904 ],
       [-0.05811575,  0.4481644 , -0.14007325, ..., -0.35257035,
         0.29532075, -0.10209283],
       [-0.12324008,  0.14663236,  0.1538505 , ..., -0.09988073,
         0.20627348,  0.09036653],
       ...,
       [ 0.14181307, -0.40691146,  0.20176099, ...,  0.33941165,
        -0.38429636, -0.24518272],
       [ 0.21714945, -0.3173487 ,  0.04293495, ...,  0.13233031,
        -0.4103705 , -0.2907497 ],
       [ 0.20537567, -0.24591468,  0.14914612, ...,  0.23811479,
        -0.34415472, -0.13272695]], dtype=float32)

或者使用weights来训练模型:

model_weights = LightFM(loss='warp')
model_weights.fit(weights)
model_weights.item_embeddings
array([[-0.39075807,  0.47386566,  0.68777657, ..., -0.6056915 ,
         0.3733116 ,  0.60417   ],
       [-0.21503039,  0.30669904,  0.34501988, ..., -0.26187918,
         0.33937645,  0.44880536],
       [-0.04561909,  0.2656086 ,  0.06034704, ..., -0.04511829,
         0.2777305 , -0.00356603],
       ...,
       [ 0.23444146, -0.30501488, -0.31618324, ...,  0.36489612,
        -0.24902612, -0.29090258],
       [ 0.18201233, -0.10327398, -0.23778489, ...,  0.20811245,
        -0.07368308, -0.25726503],
       [ 0.18961608, -0.07346104, -0.17514575, ...,  0.34551695,
        -0.04013151, -0.15871607]], dtype=float32)

除了使用fit方法训练之外, 还可以使用fit_partial方法增量训练. 常用于多epoch训练的情景. fit_partial方法与fit方法传入的参数相同, 因为在fit方法中本身就调用了fit_partial方法.

预测结果

使用模型的predict方法对指定的user-item对进行预测(交互概率或预计评分). 需要特别注意数据的形式与转换过程.

predict(user_ids, item_ids, item_features=None, user_features=None, num_threads=1)
  • user_ids: integer or np.int32 array of shape [n_pairs,]

    • 需要注意这里传入的是整数形式的user, 即模型中对user的编号, 而不是原始的user

  • item_ids: np.int32 array of shape [n_pairs,]

    • 同理, 也是item对应的整数索引

  • user_features: np.float32 csr_matrix of shape [n_users, n_user_features], optional

  • item_features: np.float32 csr_matrix of shape [n_items, n_item_features], optional

  • num_threads

返回的形式为:

  • np.float32 array of shape [n_pairs,]

    • Numpy array containing the recommendation scores for pairs defined by the inputs

这里使用lightfm.datasets中的fetch_movielens函数获取数据, 原因是将数据分成了train和test, 即训练集和验证集. 注意这里的训练集与验证集是按行为划分的, 并不是按照user划分的.

当然也可以使用Dataset手动生成train和test. 生成的方法为:

  • 首先使用sklearn中的train_test_split函数将行为数据打乱后按比例划分成train和test

  • 然后分别使用Dataset中的build_interactions方法生成行为稀疏矩阵

from lightfm.datasets import fetch_movielens
movielens = fetch_movielens()
train, test = movielens['train'], movielens['test']
train, test
(<943x1682 sparse matrix of type '<class 'numpy.int32'>'
     with 90570 stored elements in COOrdinate format>,
 <943x1682 sparse matrix of type '<class 'numpy.int32'>'
     with 9430 stored elements in COOrdinate format>)
model = LightFM(no_components=10, loss="warp")
model.fit(train, epochs=10)

item_index0 = np.arange(test.shape[1])[np.not_equal(test.tocsr()[0, :].toarray(), 0).ravel()]
test.tocsr()[0, :].toarray().ravel()[np.not_equal(test.tocsr()[0, :].toarray(), 0).ravel()]

得到用于评测的test样本中的数值

array([4, 4, 4, 3, 2, 4, 5, 3, 5, 4], dtype=int32)

对相应的item进行评估:

model.predict(0, item_index0)
array([-4.54141998, -3.23370147, -4.54919338, -1.98084712, -3.43415737,
       -3.94253993, -4.17489243, -3.95391273, -1.67029822, -2.08529305])

整体还是比较接近的.

不清楚为什么数值前面有个负号, 猜测是为了使用np.argsort排序方便

预测时, 传入的user与item需满足以下的关系:

  • 因为预测的是user-item对, 因此两者的输入长度应相等

  • 如果是预测一个一个user对多个item或一个item对多个user, 一个的那项只需要传入一个整数标量

评估方法

lightFM还提供了便捷的用于评价拟合好的模型优劣性的方法.

from lightfm.evaluation import auc_score, precision_at_k

两个指标的意义分别为:

  • auc_score: Measure the ROC AUC metric for a model

    • the probability that a randomly chosen positive example has a higher score than a randomly chosen negative example

  • precision_at_k: Measure the precision at k metric for a model

    • the fraction of known positives in the first k positions of the ranked list of results. A perfect score is 1.0

其中precision_at_k方法配合warp损失函数, auc_score配合bpr损失函数使用更符合逻辑, 能取得更好的效果.

两个评价函数接收的参数形式是相同的:

precision_at_k(model, test_interactions, train_interactions=None, k=10, user_features=None, item_features=None, preserve_rows=False, num_threads=1, check_intersections=True)
  • model: LightFM模型, 已经训练好的

  • test_interactions: np.float32 csr_matrix of shape [n_users, n_items]

    • Non-zero entries representing known positives in the evaluation set

  • train_interactions: np.float32 csr_matrix of shape [n_users, n_items], optional

    • Non-zero entries representing known positives in the train set

    • These will be omitted from the score calculations to avoid re-recommending known positives

  • k: topK

  • user_features: np.float32 csr_matrix of shape [n_users, n_user_features], optional

  • item_features: np.float32 csr_matrix of shape [n_items, n_item_features], optional

  • num_threads

model_auc = LightFM(no_components=10, loss="bpr")
model_top = LightFM(no_components=10, loss="warp")
model_auc.fit(train, epochs=10)
model_top.fit(train, epochs=10)

auc_train_precision = auc_score(model_auc, train)
auc_train_precision
array([0.7889878 , 0.9512625 , 0.91909486, 0.88896024, 0.8801184 ,
       0.8458213 , 0.855748  , 0.8329105 , 0.7323852 , 0.8610401 ,
       0.75433517, 0.8580728 , 0.8176935 , 0.8583111 , 0.92405164,
       0.88807505, 0.95826656, 0.9128386 , 0.87326556, 0.9421181 ,
       0.7756875 , 0.8509254 , 0.8667348 , 0.8528218 , 0.8565037 ,
       0.9438616 , 0.9414917 , 0.88207227, 0.8531365 , 0.82404387,
       ...

返回的是每个user计算得到的auc的值, 因此整体的需要进行平均:

auc_train_precision = auc_score(model_auc, train).mean()
auc_test_precision = auc_score(model_auc, test).mean()
auc_train_precision, auc_test_precision
(0.8963242, 0.8576865)
top_train_precision = precision_at_k(model_top, train, k=10).mean()
top_test_precision = precision_at_k(model_top, test, k=10).mean()
top_train_precision, top_test_precision
(0.60010606, 0.11145282)

最后更新于

这有帮助吗?