> For the complete documentation index, see [llms.txt](https://blessbingo.gitbook.io/garnet/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://blessbingo.gitbook.io/garnet/tui-jian-suan-fa/matrix-factorization/lightfm/shi-yong-li-zi.md).

# 使用例子

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

```python
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`读取数据.

```python
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中使用`user`和`item`的特征, 使得整体数据情况比较复杂. 通过lightFM中的`Dataset`类就能够很好地进行管理. `lightfm.data.Dataset`能够做到:

* `user`和`item`自身与整数索引之间的映射关系
* `user`和`item`特征的管理, 包括特征的顺序等
* 生成`(user, item)`对的稀疏矩阵
* 生成`user`或`item`的特征数据结构

首先创建一个`Dataset`.

```python
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`特征.

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

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

`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`的名称列表

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

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

```python
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`是完全相同的

这一步耗时比较长.

```python
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))
```

## 拟合模型

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

```python
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`需要注意的参数:

```python
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**

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

查看`item`对应的embedding:

```python
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来训练模型:

```python
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`对进行预测(交互概率或预计评分). 需要特别注意数据的形式与转换过程.

```python
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**方法生成行为稀疏矩阵

```python
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>)
```

```python
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进行评估:

```python
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还提供了便捷的用于评价拟合好的模型优劣性的方法.

```python
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**损失函数使用更符合逻辑, 能取得更好的效果.

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

```python
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

```python
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`的值, 因此整体的需要进行平均:

```python
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)
```

```python
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)
```
