提交 0784c22c 编写于 作者: L livc

Merge remote-tracking branch 'origin/recommender_sys' into recommender_sys

...@@ -2,98 +2,97 @@ ...@@ -2,98 +2,97 @@
## 背景介绍 ## 背景介绍
在网络技术不断发展和电子商务规模不断扩大的背景下,商品数量和种类快速增长,用户需要花费大量时间才能找到自己想买的商品,这就是信息超载问题。为了解决这个难题,个性化推荐系统(Recommender System)应运而生。 在网络技术不断发展和电子商务规模不断扩大的背景下,商品数量和种类快速增长,用户需要花费大量时间才能找到自己想买的商品,这就是信息超载问题。为了解决这个难题,推荐系统(Recommender System)应运而生。
可以想见,最简单的推荐策略是当今热点,比如某宝爆款。再比如希望加入个性化信息,就是浏览过的某宝爆款,或者浏览过的某条消息。 问题是,商品和新闻这么多,大多都是没有浏览过的,而且浏览过的可能已经买过或不再需要了。那么怎样推荐才合适呢? 个性化推荐系统是信息过滤系统(Information Filtering System)的子集,它可以用在很多领域,如电影、音乐、电商和 Feed 流推荐等。推荐系统通过分析、挖掘用户行为,发现用户的个性化需求与兴趣特点,将用户可能感兴趣的信息或商品推荐给用户。与搜索引擎不同,推荐系统不需要用户准确地描述出自己的需求,而是根据分析历史行为建模,主动提供满足用户兴趣和需求的信息。
个性化推荐系统是信息过滤系统(Information Filtering System)的子集,它通过分析、挖掘用户行为,发现用户的个性化需求与兴趣特点,将用户可能感兴趣的信息或商品推荐给用户。与传统的搜索引擎不同,推荐系统不需要用户准确地描述出自己的需求,而是根据分析历史行为建模,主动提供满足用户兴趣和需求的信息。一般来说,个性化推荐比一般化的推荐更能吸引用户点击或购买。个性化推荐可以用在很多领域,如电影、音乐、电商和 Feed 流推荐等。
传统的推荐系统方法主要有: 传统的推荐系统方法主要有:
- 协同过滤推荐[1](Collaborative Filtering Recommendation):协同过滤推荐技术是推荐系统中应用最广泛的技术之一。它一般使用 *K*近邻法(*k*-*NN*)收集并分析用户历史行为、活动、偏好,计算一个用户与其他用户的相似度或距离,利用目标用户的最近邻居用户对商品评价的加权评价值来预测目标用户对特定商品的喜好程度。协同过滤可以给用户推荐未购买过的新产品,缺点是对于没有任何行为的新用户存在冷启动的问题,其次是因为用户与商品之间的交互数据不够多造成的稀疏问题 - 协同过滤推荐(Collaborative Filtering Recommendation):该方法收集分析用户历史行为、活动、偏好,计算一个用户与其他用户的相似度,利用目标用户的相似用户对商品评价的加权评价值,来预测目标用户对特定商品的喜好程度。优点是可以给用户推荐未浏览过的新产品;缺点是对于没有任何行为的新用户存在冷启动的问题,同时也存在用户与商品之间的交互数据不够多造成的稀疏问题,会导致模型难以找到相近用户
- 基于内容过滤[2](Content-based Filtering):利用商品的内容描述,抽象出有意义的特征,计算用户的兴趣和商品描述之间的相似度,来给用户做推荐。基于内容的推荐简单直接,不需要依据其他用户对商品的评价,同时可以比较商品间的相似度,但它要求得到商品的显示属性,对于没有任何行为的新用户同样存在冷启动的问题。 - 基于内容过滤推荐[[1](#参考文献)](Content-based Filtering Recommendation):该方法利用商品的内容描述,抽象出有意义的特征,通过计算用户的兴趣和商品描述之间的相似度,来给用户做推荐。优点是简单直接,不需要依据其他用户对商品的评价,而是通过商品属性进行商品相似度度量,从而推荐给用户所感兴趣商品的相似商品;缺点是对于没有任何行为的新用户同样存在冷启动的问题。
- 组合推荐[3](Hybrid Recommendation):运用不同的输入和技术共同进行推荐,以弥补各自推荐技术的缺点。 - 组合推荐[[2](#参考文献)](Hybrid Recommendation):运用不同的输入和技术共同进行推荐,以弥补各自推荐技术的缺点。
深度学习具有优秀的自动提取特征的能力,能够学习多层次的抽象特征表示,并对异质或跨域的内容信息进行学习,解决推荐系统常见的冷启动问题[4] 其中协同过滤是应用最广泛的技术之一,它又可以分为多个子类:基于用户 (User-Based)的推荐[[3](#参考文献)] , 基于物品(Item-Based)的推荐[[4](#参考文献)],基于社交网络关系(Social-Based)的推荐[[5](#参考文献)],基于模型(Model-based的推荐等。1994年明尼苏达大学推出的GroupLens 系统[[3](#参考文献)]一般被认为是推荐系统成为一个相对独立的研究方向的标志。该系统首次提出了基于协同过滤来完成推荐任务的思想,此后,基于该模型的协同过滤推荐引领了推荐系统十几年的发展方向
## 效果展示 深度学习具有优秀的自动提取特征的能力,能够学习多层次的抽象特征表示,并对异质或跨域的内容信息进行学习,可以一定程度上处理推荐系统冷启动问题[[6](#参考文献)]。本教程主要介绍个性化推荐的深度学习模型,以及如何使用PaddlePaddle实现模型。
在电影推荐系统的场景中,我们可以根据所有电影的推荐得分排序,推荐给用户可能感兴趣的电影。 ## 效果展示
我们使用包含用户信息、电影信息与电影评分的数据集作为个性化推荐的应用场景。当我们训练好模型后,只需要输入对应的用户ID和电影ID,就可以得出一个匹配的分数,然后根据所有电影的推荐得分排序,推荐给用户可能感兴趣的电影。 我们使用包含用户信息、电影信息与电影评分的数据集作为个性化推荐的应用场景。当我们训练好模型后,只需要输入对应的用户ID和电影ID,就可以得出一个匹配的分数(范围[1,5],分数越高视为兴趣越大),然后根据所有电影的推荐得分排序,推荐给用户可能感兴趣的电影。
``` ```
Input movie_id: 9 Input movie_id: 1962
Input user_id: 4 Input user_id: 1
Prediction Score is 2.56 Prediction Score is 4.25
Input movie_id: 8
Input user_id: 2
Prediction Score is 3.13
``` ```
## 模型概览 ## 模型概览
在构造推荐系统之前,我们先来了解一些业内模型。 本章中,我们首先介绍YouTube的视频推荐系统[[7](#参考文献)],然后介绍我们实现的融合推荐模型。
### YouTube 的深度神经网络推荐系统 ### YouTube的深度神经网络推荐系统
YouTube 是世界上最大的视频上传、分享、发现的网站,YouTube 推荐系统为超过 10 亿用户从不断增长的视频库内个性化定制内容。系统由两个神经网络组成:一个用于生成候选视频,另一个用于排名。系统结构如下图所示[5] YouTube是世界上最大的视频上传、分享和发现网站,YouTube推荐系统为超过10亿用户从不断增长的视频库中推荐个性化的内容。整个系统由两个神经网络组成:候选生成网络和排序网络。候选生成网络从百万量级的视频库中生成上百个候选,排序网络对候选进行打分排序,输出排名最高的数十个结果。系统结构如图1所示
![](image/YouTube_Overview.png) <p align="center">
<img src="image/YouTube_Overview.png" width="75%" ><br/>
图1. YouTube 推荐系统结构
</p>
候选生成网络从用户的 YouTube 活动历史中提取信息,然后从视频库中检索出几百个与用户相关的视频输出。系统把推荐问题视为极多种类别的多分类问题。 #### 候选生成网络(Candidate Generation Network)
如下图所示,深度候选生成模型将嵌入的稀疏特征取平均后和一些稠密特征连接在一起,转换成适合隐藏层输入的固定宽度的向量。所有隐藏层是全连接的。在训练中,在取样的 softmax 输出上,使用梯度下降对交叉熵代价函数进行最小化。在服务中,使用近似最近邻(approximate nearest neighbor )查询来生成成百上千的候选视频推荐 候选生成网络将推荐问题建模为一个类别数极大的多类分类问题:对于一个Youtube用户,使用其观看历史(视频ID)、搜索词记录(search tokens)、人口学信息(如地理位置、用户登录设备)等特征,对视频库中所有视频进行多分类,得到每一类别的分类结果(即每一个视频的推荐概率),最终输出概率较高的几百个视频
![](image/Deep_candidate_generation_model_architecture.png) 对于用户$U$和历史信息$C$,预测$t$时刻用户要观看的视频$\omega_t$为视频$i$的概率为:
在排名模型中,特征根据贡献单个值还是多个值的集合被分为单价(univalent)特征和多价(multivalent)特征。例如,视频 ID 是单价特征,对应的多价特征就是用户最近看过的 N 个视频的 ID 集合。 $$P(\omega_t=i|U,C)=\frac{e^{v_{i}u}}{\sum_{j \in V}e^{v_{j}u}}$$
如下图所示,嵌入的分类特征(包括单价特征和多价特征)带有共享的嵌入和归一化后的连续特征的乘幂。所有层都是全连接的。最后,使用 logistic regression 为每一个视频评出分数,排序后返回给用户。在实践中,需要给网络输入几百个特征 其中,$V$表示视频库集合,大小在数百万量级;$u\in \mathbb{R}^N$是用户及相应历史信息的向量表示;$v_j\in \mathbb{R}^N$是ID为$j$的视频的向量表示
![](image/Deep_ranking_network_architecture.png) 将观看历史(视频ID)及搜索词记录这类历史信息映射为向量后取平均值得到定长表示,同时输入人口学特征(地理位置等)以优化新用户的推荐效果,将二值特征(如性别,是否登录)和连续特征(如用户年龄)归一化处理到[0, 1]。接下来将所有特征表示拼接为一个很长的向量并输入给多个非线形变换层(这里应用的是全连接的 ReLU)处理。最高层的非线形变换得到的即是$u$,最后将其输入给 softmax 做分类,如图2所示:
### 基于 RNN 的推荐系统模型 <p align="center">
<img src="image/Deep_candidate_generation_model_architecture.png" width="75%" ><br/>
图2. 候选生成网络结构
</p>
在这个场景中[6],我们先基于用户行为找到用户的最近邻用户群,然后将这些用户的商品评论文本转化为词向量作为训练数据。模型输出是用户喜欢某个商品的可能性大小 考虑到softmax分类的类别数非常多,为了保证一定的计算效率:1)训练阶段,使用负样本类别采样将实际计算的类别数缩小至数千;2)推荐(预测)阶段,忽略 softmax的归一化计算(不影响结果),将类别打分问题简化为点积(dot product)空间中的最近邻(nearest neighbor)搜索问题,取与$u$最近的$N$个视频作为生成的候选
#### 双向 RNN 模型 #### 排序网络(Ranking Network)
排序网络的结构类似于候选生成网络,但是它的目标是对候选进行更细致的打分排序。类似于传统的广告排序中的特征抽取方法,这里也构造了大量的用于视频排序的相关特征(如视频 ID、上次观看时间等)。离散特征和连续特征的处理方式类似于候选生成网络,不同之处是排序网络的顶部是一个加权逻辑回归(weighted logistic regression),它对所有候选视频进行打分,从高到底排序后将分数较高的一些视频返回给用户。
[双向 RNN]() 不仅可以访问上文信息,还可以访问下文信息。如下图所示,在单层模型中,前向 RNN 的输入为第一个词到最后一个词的顺序,后向 RNN 反之。对双向 RNN 输出的结果取平均值,经过线性转化后作为 softmax 的输入。在多层双向 RNN 中,每一层把前一层的记忆序列视为输入,然后计算这一层的记忆表示,最后取最后一层的输出做和单层一样的运算。 ### 融合推荐模型
![](image/BiRNN_with_GRU_Cell.png) 在下文的电影推荐系统中,我们使用电影特征和用户特征作为神经网络的输入。
#### 注意力机制模型 用户特征融合了用户ID,性别,职业,年龄四个属性信息。我们将用户 ID 映射为维度大小为256的向量表示,然后输入全连接层,同时对其他三个属性也做类似的处理。最后将四个属性的特征表示分别全连接并相加。
在上述双向 RNN 模型的基础上,我们引入了[注意力机制]()。每个前向和后向单元的状态被连接成一个输出向量,之后通过一个注意力权重向量的集合转化为标量。每个单元的标量再连接成一个新向量,这个向量输入到最后的预测层来生成最终结果 电影特征融合了电影ID,电影类型ID,电影名称三个属性信息。我们对电影 ID 以类似用户ID的方式进行处理,电影类型ID 将被以向量的形式直接输入全连接层。对于电影名称,我们用文本卷积神经网络(详见[第5章](https://github.com/PaddlePaddle/book/blob/develop/understand_sentiment/README.md))对其进行处理,得到其定长向量表示。最后将三个属性的特征表示分别全连接并相加
![](image/Attention_Based_BiRNN_with_GRU_cell.png) 得到电影和用户的向量表示后,计算二者的余弦相似度作为推荐系统的打分。最后,用该相似度打分和用户真实打分的差异的平方作为该回归模型的损失函数。
接下来,我们将使用神经网络构建自己的推荐系统。 <p align="center">
<img src="image/rec_regression_network.png" width="90%" ><br/>
图3. 融合推荐模型
</p>
## 数据准备 ## 数据准备
### 数据介绍与下载 ### 数据介绍与下载
此教程我们使用[MovieLens 数据集](http://grouplens.org/datasets/movielens/)。该数据集包含一些用户信息、电影信息以及电影评分,由 GroupLens Research 实验室搜集整理。 我们以 [MovieLens 百万数据集(ml-1m)](http://files.grouplens.org/datasets/movielens/ml-1m.zip)为例进行介绍。ml-1m 数据集包含了 6,000 位用户对 4,000 部电影的 1,000,000 条评价(评分范围 1~5 分,均为整数),由 GroupLens Research 实验室搜集整理。
根据数据规模的不同,该数据集也有很多不同的版本。这里我们用 [MovieLens 百万数据集(ml-1m)](http://files.grouplens.org/datasets/movielens/ml-1m.zip)作为示例,其中包含 6,000 位用户对 4,000 部电影的 1,000,000 条评价。该数据集于 2003 年 2 月发布。当一个新的用户进入 MovieLens ,他需对 15 部电影评分,评分范围为 1-5 分,评分间隔为 0.5 分。当用户查看意图电影时,MovieLens 的推荐系统将根据用户以往的评分预测其对该电影的评分[7]。
运行 `data/getdata.sh` 下载数据,`data/ml-1m` 的目录结构 您可以运行 `data/getdata.sh` 下载数据,如果数椐获取成功,您将在目录`data/ml-1m`中看到下面的文件
``` ```
+--ml-1m movies.dat ratings.dat users.dat README
+--- movies.dat # 电影特征
+--- ratings.dat # 评分
+--- users.dat # 用户特征
+--- README # 数据集描述
``` ```
ml-1m 中的数据文件使用 "::" 作为分隔符。数据格式为(更多细节请参阅 ml-1m 中的 README ): - movies.dat:电影特征数据,格式为`电影ID::电影名称::电影类型`
- ratings.dat:评分数据,格式为`用户ID::电影ID::评分::时间戳`
- 评分数据(ratings.dat):用户ID::电影ID::评分::时间戳 - users.dat:用户特征数据,格式为`用户ID::性别::年龄::职业::邮编`
- 电影特征数据(movies.dat):电影ID::电影名称::电影类型 - README:数据集的详细描述
- 用户特征数据(users.dat): 用户ID::性别::年龄::职业::邮编
### 数据预处理 ### 数据预处理
...@@ -103,93 +102,80 @@ ml-1m 中的数据文件使用 "::" 作为分隔符。数据格式为(更多 ...@@ -103,93 +102,80 @@ ml-1m 中的数据文件使用 "::" 作为分隔符。数据格式为(更多
pip install -r data/requirements.txt pip install -r data/requirements.txt
``` ```
整个预处理过程分为处理输入用户、电影特征(数据文件序列化)和将数据分为训练、测试集两部分。执行 `./preprocess.sh` 即可 其次,在预处理过程中,我们将字段配置文件`data/config.json`转化为meta配置文件`meta_config.json`,并生成对应的meta文件`meta.bin`,以完成数据文件的序列化。然后再将`ratings.dat`分为训练集、测试集两部分,把它们的地址写入`train.list``test.list`
在处理输入特征中,得到每个字段(movies/users)的字段配置文件,将其转化为可以解析数据集的 meta 文件,然后用 meta 文件解析数据集为 Python 对象并序列化。 ```bash
./preprocess.sh
```
在分割训练、测试集部分中,根据`ratings.dat`将数据分为两部分,分别用来进行模型训练和测试。 运行成功后目录`./data` 新增以下文件:
### 提供数据给 PaddlePaddle ```
meta_config.json meta.bin ratings.dat.train ratings.dat.test train.list test.list
```
- meta.bin: meta文件是Python的pickle对象, 存储着电影和用户信息。
- meta_config.json: meta配置文件,用来具体描述如何解析数据集中的每一个字段,由字段配置文件生成。
- ratings.dat.train和ratings.dat.test: 训练集和测试集,训练集已经随机打乱。
- train.list和test.list: 训练集和测试集的文件地址列表。
数据提供脚本 `dataprovider.py` 会读取 `meta.bin` 和评分文件,生成训练需要的样本。在这个脚本中,我们需要设置: ### 提供数据给 PaddlePaddle
- obj.slots: 特征的类型和维度。 我们使用 Python 接口传递数据给系统,下面 `dataprovider.py` 给出了完整示例。
- use_seq: `dataprovider.py` 中的数据是否为序列模式。
```python ```python
from paddle.trainer.PyDataProvider2 import * from paddle.trainer.PyDataProvider2 import *
import common_utils # 解析 from common_utils import meta_to_header
def __list_to_map__(lst): # 输出格式 def __list_to_map__(lst): # 将list转为map
ret_val = dict() ret_val = dict()
for each in lst: for each in lst:
k, v = each k, v = each
ret_val[k] = v ret_val[k] = v
return ret_val return ret_val
```
```python def hook(settings, meta, **kwargs): # 读取meta.bin
def hook(settings, meta, **kwargs): # 定义电影特征
""" movie_headers = list(meta_to_header(meta, 'movie'))
初始 hook 设置了 obj.slots 并存储 meta 数据。它将在处理数据前被唤起。
:参数对象: global object. It will passed to process routine.
:类型对象: object
:参数 meta: meta file 对象,通过 trainer_config 传递(记录了电影和用户的特征)
:参数 kwargs: 其他未用过的参数
"""
del kwargs
# Header 定义了 paddle 使用的 slots.
# 第一部分是电影特征
# 第二部分是用户特征
# 最后的部分是评分分数
# header 是一个 [USE_SEQ_OR_NOT?, SlotType] 的 list
movie_headers = list(common_utils.meta_to_header(meta, 'movie'))
settings.movie_names = [h[0] for h in movie_headers] settings.movie_names = [h[0] for h in movie_headers]
headers = movie_headers headers = movie_headers
user_headers = list(common_utils.meta_to_header(meta, 'user'))
# 定义用户特征
user_headers = list(meta_to_header(meta, 'user'))
settings.user_names = [h[0] for h in user_headers] settings.user_names = [h[0] for h in user_headers]
headers.extend(user_headers) headers.extend(user_headers)
headers.append(("rating", dense_vector(1))) # 分数
# 加载评分信息
# slot 类型 headers.append(("rating", dense_vector(1)))
settings.input_types = __list_to_map__(headers) settings.input_types = __list_to_map__(headers)
settings.meta = meta settings.meta = meta
```
接下来,在`process`函数中将数据逐一提供给 PaddlePaddle。
```python
@provider(init_hook=hook, cache=CacheType.CACHE_PASS_IN_MEM) @provider(init_hook=hook, cache=CacheType.CACHE_PASS_IN_MEM)
def process(settings, filename): def process(settings, filename):
with open(filename, 'r') as f: with open(filename, 'r') as f:
for line in f: for line in f:
# 读取评分 # 从评分文件中读取评分
user_id, movie_id, score = map(int, line.split('::')[:-1]) user_id, movie_id, score = map(int, line.split('::')[:-1])
# 将评分放缩到[-2, +2]范围内的整数
# 将分数范围放缩到 [-5, +5] score = float(score - 3)
score = float(score) * 2 - 5.0
# 读取 电影/用户 特征
movie_meta = settings.meta['movie'][movie_id] movie_meta = settings.meta['movie'][movie_id]
user_meta = settings.meta['user'][user_id] user_meta = settings.meta['user'][user_id]
# 添加电影ID与电影特征
outputs = [('movie_id', movie_id - 1)] outputs = [('movie_id', movie_id - 1)]
# 添加电影特征
for i, each_meta in enumerate(movie_meta): for i, each_meta in enumerate(movie_meta):
outputs.append((settings.movie_names[i + 1], each_meta)) outputs.append((settings.movie_names[i + 1], each_meta))
# 添加用户ID # 添加用户ID与用户特征
outputs.append(('user_id', user_id - 1)) outputs.append(('user_id', user_id - 1))
# 添加用户特征
for i, each_meta in enumerate(user_meta): for i, each_meta in enumerate(user_meta):
outputs.append((settings.user_names[i + 1], each_meta)) outputs.append((settings.user_names[i + 1], each_meta))
# 最后添加分数 # 添加评分
outputs.append(('rating', [score])) outputs.append(('rating', [score]))
# 将数据提供给 paddle # 将数据返回给 paddle
yield __list_to_map__(outputs) yield __list_to_map__(outputs)
``` ```
...@@ -197,20 +183,36 @@ def process(settings, filename): ...@@ -197,20 +183,36 @@ def process(settings, filename):
### 数据定义 ### 数据定义
定义通过 `define_py_data_sources2` 从 dataprovider 中读入数据: 加载`meta.bin`文件并定义通过`define_py_data_sources2`从dataprovider中读入数据:
```python ```python
define_py_data_sources2( from paddle.trainer_config_helpers import *
'data/train.list',
'data/test.list', try:
module='dataprovider', import cPickle as pickle
obj='process', except ImportError:
args={'meta': meta}) import pickle
is_predict = get_config_arg('is_predict', bool, False)
META_FILE = 'data/meta.bin'
with open(META_FILE, 'rb') as f:
# 加载 meta 文件
meta = pickle.load(f)
if not is_predict:
define_py_data_sources2(
'data/train.list',
'data/test.list',
module='dataprovider',
obj='process',
args={'meta': meta})
``` ```
### 算法配置 ### 算法配置
这里我们设置了 batch size、网络初始学习率,并设置RMSProp 优化方法为自适应学习率策略 这里我们设置了batch size、网络初始学习率和RMSProp自适应优化方法
```python ```python
settings( settings(
...@@ -219,65 +221,80 @@ settings( ...@@ -219,65 +221,80 @@ settings(
### 模型结构 ### 模型结构
网络结构如下图所示: 1. 定义数据输入和参数维度。
![](image/rec_regression_network.png) ```python
movie_meta = meta['movie']['__meta__']['raw_meta']
user_meta = meta['user']['__meta__']['raw_meta']
文件 `trainer_config.py` 中`construct_feature` 函数用来构建电影/用户特征,我们将每个特征种类映射到一个特征向量中: movie_id = data_layer('movie_id', size=movie_meta[0]['max']) # 电影ID
title = data_layer('title', size=len(movie_meta[1]['dict'])) # 电影名称
genres = data_layer('genres', size=len(movie_meta[2]['dict'])) # 电影类型
user_id = data_layer('user_id', size=user_meta[0]['max']) # 用户ID
gender = data_layer('gender', size=len(user_meta[1]['dict'])) # 用户性别
age = data_layer('age', size=len(user_meta[2]['dict'])) # 用户年龄
occupation = data_layer('occupation', size=len(user_meta[3]['dict'])) # 用户职业
```python embsize = 256 # 向量维度
def construct_feature(name): ```
__meta__ = meta[name]['__meta__']['raw_meta']
fusion = []
for each_meta in __meta__: # 读入数据
type_name = each_meta['type']
slot_name = each_meta.get('name', '%s_id' % name)
if type_name == 'id': # id:简单的嵌入,然后添加一个全连接层
slot_dim = each_meta['max']
embedding = embedding_layer(
input=data_layer(
slot_name, size=slot_dim), size=256)
fusion.append(fc_layer(input=embedding, size=256))
elif type_name == 'embedding': # embedding:如果是序列,则先做嵌入,然后再做一次文本卷积操作, 然后得到平均采样的结果。否则,则先做嵌入,然后添加一个全连接层。
is_seq = each_meta['seq'] == 'sequence'
slot_dim = len(each_meta['dict'])
din = data_layer(slot_name, slot_dim)
embedding = embedding_layer(input=din, size=256)
if is_seq:
fusion.append(
text_conv_pool(
input=embedding, context_len=5, hidden_size=256))
else:
fusion.append(fc_layer(input=embedding, size=256))
elif type_name == 'one_hot_dense': # one_hot_dense:两个全连接层。
slot_dim = len(each_meta['dict'])
hidden = fc_layer(input=data_layer(slot_name, slot_dim), size=256)
fusion.append(fc_layer(input=hidden, size=256))
# 聚集所有特征向量,使用全连接层连接它们并返回。
return fc_layer(name="%s_fusion" % name, input=fusion, size=256)
```
然后我们出求这两个特征的余弦相似度并输出 2. 构造“电影”特征
```python ```python
movie_feature = construct_feature("movie") # 电影 ID 和电影类型分别映射到其对应的特征隐层(256维)。
user_feature = construct_feature("user") movie_id_emb = embedding_layer(input=movie_id, size=embsize)
similarity = cos_sim(a=movie_feature, b=user_feature) movie_id_hidden = fc_layer(input=movie_id_emb, size=embsize)
if not is_predict:
outputs(
regression_cost(
input=similarity, label=data_layer(
'rating', size=1)))
define_py_data_sources2( genres_emb = fc_layer(input=genres, size=embsize)
'data/train.list',
'data/test.list', # 对于电影名称,一个ID序列表示的词语序列,在输入卷积层后,
module='dataprovider', # 将得到每个时间窗口的特征(序列特征),然后通过在时间维度
obj='process', # 降采样得到固定维度的特征,整个过程在text_conv_pool实现
args={'meta': meta}) title_emb = embedding_layer(input=title, size=embsize)
else: title_hidden = text_conv_pool(
outputs(similarity) input=title_emb, context_len=5, hidden_size=embsize)
```
# 将三个属性的特征表示分别全连接并相加,结果即是电影特征的最终表示
movie_feature = fc_layer(
input=[movie_id_hidden, title_hidden, genres_emb], size=embsize)
```
3. 构造“用户”特征。
```python
# 将用户ID,性别,职业,年龄四个属性分别映射到其特征隐层。
user_id_emb = embedding_layer(input=user_id, size=embsize)
user_id_hidden = fc_layer(input=user_id_emb, size=embsize)
gender_emb = embedding_layer(input=gender, size=embsize)
gender_hidden = fc_layer(input=gender_emb, size=embsize)
age_emb = embedding_layer(input=age, size=embsize)
age_hidden = fc_layer(input=age_emb, size=embsize)
occup_emb = embedding_layer(input=occupation, size=embsize)
occup_hidden = fc_layer(input=occup_emb, size=embsize)
# 同样将这四个属性分别全连接并相加形成用户特征的最终表示。
user_feature = fc_layer(
input=[user_id_hidden, gender_hidden, age_hidden, occup_hidden],
size=embsize)
```
4. 计算余弦相似度,定义损失函数和网络输出。
```python
similarity = cos_sim(a=movie_feature, b=user_feature, scale=2)
# 训练时,采用regression_cost作为损失函数计算回归误差代价,并作为网络的输出。
# 预测时,网络的输出即为余弦相似度。
if not is_predict:
lbl=data_layer('rating', size=1)
cost=regression_cost(input=similarity, label=lbl)
outputs(cost)
else:
outputs(similarity)
```
## 训练模型 ## 训练模型
...@@ -290,77 +307,70 @@ paddle train \ ...@@ -290,77 +307,70 @@ paddle train \
--save_dir=./output \ # 模型保存路径 --save_dir=./output \ # 模型保存路径
--use_gpu=false \ # 是否使用 GPU (默认不使用) --use_gpu=false \ # 是否使用 GPU (默认不使用)
--trainer_count=4\ # 一台机器上面的线程数量 --trainer_count=4\ # 一台机器上面的线程数量
--test_all_data_in_one_period=true \ # 每个训练周期训练一次所有数据。否则每个训练周期测试 batch_size 个 batch 的数据。 --test_all_data_in_one_period=true \ # 每个训练周期训练一次所有数据。否则每个训练周期测试batch_size个batch的数据。
--log_period=100 \ # 训练 log_period 个 batch 后打印日志 --log_period=100 \ # 训练 log_period 个 batch 后打印日志
--dot_period=1 \ # 每训练 dot_period 个 batch 后打印一个"." --dot_period=1 \ # 每训练 dot_period 个 batch 后打印一个"."
--num_passes=50 2>&1 | tee 'log.txt' --num_passes=50 2>&1 | tee 'log.txt'
``` ```
如果训练过程启动成功的话,输出应该类似如下: 成功的输出类似如下:
```shell ```bash
I0601 08:07:22.832059 10549 TrainerInternal.cpp:157] Batch=100 samples=160000 AvgCost=4.13494 CurrentCost=4.13494 Eval: CurrentEval: I0117 01:01:48.585651 9998 TrainerInternal.cpp:165] Batch=100 samples=160000 AvgCost=0.600042 CurrentCost=0.600042 Eval: CurrentEval:
...................................................................................................
I0601 08:07:50.672627 10549 TrainerInternal.cpp:157] Batch=200 samples=320000 AvgCost=3.80957 CurrentCost=3.48421 Eval: CurrentEval: I0117 01:02:53.821918 9998 TrainerInternal.cpp:165] Batch=200 samples=320000 AvgCost=0.602855 CurrentCost=0.605668 Eval: CurrentEval:
...................................................................................................
I0601 08:08:18.877369 10549 TrainerInternal.cpp:157] Batch=300 samples=480000 AvgCost=3.68145 CurrentCost=3.42519 Eval: CurrentEval: I0117 01:03:58.937922 9998 TrainerInternal.cpp:165] Batch=300 samples=480000 AvgCost=0.605199 CurrentCost=0.609887 Eval: CurrentEval:
...................................................................................................
I0601 08:08:46.863963 10549 TrainerInternal.cpp:157] Batch=400 samples=640000 AvgCost=3.6007 CurrentCost=3.35847 Eval: CurrentEval: I0117 01:05:04.083251 9998 TrainerInternal.cpp:165] Batch=400 samples=640000 AvgCost=0.608693 CurrentCost=0.619175 Eval: CurrentEval:
...................................................................................................
I0601 08:09:15.413025 10549 TrainerInternal.cpp:157] Batch=500 samples=800000 AvgCost=3.54811 CurrentCost=3.33773 Eval: CurrentEval: I0117 01:06:09.155859 9998 TrainerInternal.cpp:165] Batch=500 samples=800000 AvgCost=0.613273 CurrentCost=0.631591 Eval: CurrentEval:
I0601 08:09:36.058670 10549 TrainerInternal.cpp:181] Pass=0 Batch=565 samples=902826 AvgCost=3.52368 Eval: .................................................................I0117 01:06:51.109654 9998 TrainerInternal.cpp:181]
I0601 08:09:46.215489 10549 Tester.cpp:101] Test samples=97383 cost=3.32155 Eval: Pass=49 Batch=565 samples=902826 AvgCost=0.614772 Eval:
I0601 08:09:46.215966 10549 GradientMachine.cpp:132] Saving parameters to ./output/model/pass-00000 I0117 01:07:04.205142 9998 Tester.cpp:115] Test samples=97383 cost=0.721995 Eval:
I0601 08:09:46.233397 10549 ParamUtil.cpp:99] save dir ./output/model/pass-00000 I0117 01:07:04.205281 9998 GradientMachine.cpp:113] Saving parameters to ./output/pass-00049
I0601 08:09:46.233438 10549 Util.cpp:209] copy trainer_config.py to ./output/model/pass-00000
I0601 08:09:46.233541 10549 ParamUtil.cpp:147] fileName trainer_config.py
``` ```
## 应用模型 ## 应用模型
在训练了几轮以后,你可以对模型进行评估,通过选择最小训练误差的一轮参数得到最好轮次的模型。运行下面命令即可: 在训练了几轮以后,您可以对模型进行评估。运行以下命令,可以通过选择最小训练误差的一轮参数得到最好轮次的模型。
```shell ```shell
./evaluate.py log.txt ./evaluate.py log.txt
``` ```
将看到: 将看到:
```shell ```shell
Best pass is 00009, error is 3.06949, which means predict get error as 0.875998002281 Best pass is 00036, error is 0.719281, which means predict get error as 0.424052
evaluating from pass output/pass-00009 evaluating from pass output/pass-00036
``` ```
然后,你可以预测任何用户对于任何一部电影的评价,运行下面命令即可 预测任何用户对于任何一部电影评价的命令如下
```shell ```shell
python prediction.py 'output/pass-00009/' python prediction.py 'output/pass-00036/'
``` ```
预测程序将读取用户的输入,然后输出预测分数(注意这里的分数不是 dataprovider 归一化后的得分,而是预测的最终得分结果)。 预测程序将读取用户的输入,然后输出预测分数。您会看到如下命令行界面:
用户预测的命令行界面如下:
``` ```
Input movie_id: 9 Input movie_id: 1962
Input user_id: 4 Input user_id: 1
Prediction Score is 2.56 Prediction Score is 4.25
Input movie_id: 8
Input user_id: 2
Prediction Score is 3.13
``` ```
## 总结 ## 总结
本章介绍了传统的推荐系统方法、 YouTube 的深度神经网络推荐系统和基于 RNN 的推荐系统模型,并以电影推荐为例,使用神经网络训练了一个个性化推荐模型。推荐系统几乎涵盖了电商系统、社交网络,广告推荐,搜索引擎等领域的方方面面,而在图像处理、自然语言处理等领域已经发挥重要作用的深度学习技术,也将在推荐系统领域大放异彩。 本章介绍了传统的推荐系统方法和YouTube的深度神经网络推荐系统,并以电影推荐为例,使用PaddlePaddle训练了一个个性化推荐神经网络模型。推荐系统几乎涵盖了电商系统、社交网络、广告推荐、搜索引擎等领域的方方面面,而在图像处理、自然语言处理等领域已经发挥重要作用的深度学习技术,也将会在推荐系统领域大放异彩。
## 参考文献 ## 参考文献
1. Breese, John S., David Heckerman, and Carl Kadie. ["Empirical analysis of predictive algorithms for collaborative filtering."](https://arxiv.org/pdf/1301.7363v1.pdf) Proceedings of the Fourteenth conference on Uncertainty in artificial intelligence. Morgan Kaufmann Publishers Inc., 1998. APA 1. [Peter Brusilovsky](https://en.wikipedia.org/wiki/Peter_Brusilovsky) (2007). *The Adaptive Web*. p. 325.
2. [Peter Brusilovsky](https://en.wikipedia.org/wiki/Peter_Brusilovsky) (2007). *The Adaptive Web*. p. 325. [ISBN](https://en.wikipedia.org/wiki/International_Standard_Book_Number) [978-3-540-72078-2](https://en.wikipedia.org/wiki/Special:BookSources/978-3-540-72078-2). 2. Robin Burke , [Hybrid Web Recommender Systems](http://www.dcs.warwick.ac.uk/~acristea/courses/CS411/2010/Book%20-%20The%20Adaptive%20Web/HybridWebRecommenderSystems.pdf), pp. 377-408, The Adaptive Web, Peter Brusilovsky, Alfred Kobsa, Wolfgang Nejdl (Ed.), Lecture Notes in Computer Science, Springer-Verlag, Berlin, Germany, Lecture Notes in Computer Science, Vol. 4321, May 2007, 978-3-540-72078-2.
3. Robin Burke , [Hybrid Web Recommender Systems](http://www.dcs.warwick.ac.uk/~acristea/courses/CS411/2010/Book%20-%20The%20Adaptive%20Web/HybridWebRecommenderSystems.pdf), pp. 377-408, The Adaptive Web, Peter Brusilovsky, Alfred Kobsa, Wolfgang Nejdl (Ed.), Lecture Notes in Computer Science, Springer-Verlag, Berlin, Germany, Lecture Notes in Computer Science, Vol. 4321, May 2007, 978-3-540-72078-2. 3. P. Resnick, N. Iacovou, etc. “[GroupLens: An Open Architecture for Collaborative Filtering of Netnews](http://ccs.mit.edu/papers/CCSWP165.html)”, Proceedings of ACM Conference on Computer Supported Cooperative Work, CSCW 1994. pp.175-186.
4. Yuan, Jianbo, et al. ["Solving Cold-Start Problem in Large-scale Recommendation Engines: A Deep Learning Approach."](https://arxiv.org/pdf/1611.05480v1.pdf) *arXiv preprint arXiv:1611.05480* (2016). 4. Sarwar, Badrul, et al. "[Item-based collaborative filtering recommendation algorithms.](http://files.grouplens.org/papers/www10_sarwar.pdf)*Proceedings of the 10th international conference on World Wide Web*. ACM, 2001.
5. Covington P, Adams J, Sargin E. [Deep neural networks for youtube recommendations](http://delivery.acm.org/10.1145/2960000/2959190/p191-covington.pdf?ip=113.225.222.231&id=2959190&acc=OA&key=4D4702B0C3E38B35%2E4D4702B0C3E38B35%2E4D4702B0C3E38B35%2E5945DC2EABF3343C&CFID=713293170&CFTOKEN=33777789&__acm__=1483689091_3196ba42120e35d98a6adbf5feed64a0)[C]//Proceedings of the 10th ACM Conference on Recommender Systems. ACM, 2016: 191-198. 5. Kautz, Henry, Bart Selman, and Mehul Shah. "[Referral Web: combining social networks and collaborative filtering.](http://www.cs.cornell.edu/selman/papers/pdf/97.cacm.refweb.pdf)" Communications of the ACM 40.3 (1997): 63-65. APA
6. Yuan, Jianbo, et al. ["Solving Cold-Start Problem in Large-scale Recommendation Engines: A Deep Learning Approach."](https://arxiv.org/pdf/1611.05480v1.pdf) *arXiv preprint arXiv:1611.05480* (2016).
7. Covington P, Adams J, Sargin E. [Deep neural networks for youtube recommendations](https://static.googleusercontent.com/media/research.google.com/zh-CN//pubs/archive/45530.pdf)[C]//Proceedings of the 10th ACM Conference on Recommender Systems. ACM, 2016: 191-198.
MLA MLA
6. https://cs224d.stanford.edu/reports/LiuSingh.pdf
7. https://zh.wikipedia.org/wiki/MovieLens
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
# limitations under the License. # limitations under the License.
from paddle.trainer.PyDataProvider2 import * from paddle.trainer.PyDataProvider2 import *
import common_utils # parse from common_utils import meta_to_header
def __list_to_map__(lst): def __list_to_map__(lst):
...@@ -35,17 +35,16 @@ def hook(settings, meta, **kwargs): ...@@ -35,17 +35,16 @@ def hook(settings, meta, **kwargs):
file record movie/user features. file record movie/user features.
:param kwargs: unused other arguments. :param kwargs: unused other arguments.
""" """
del kwargs # unused kwargs
# Header define slots that used for paddle. # Header define slots that used for paddle.
# first part is movie features. # first part is movie features.
# second part is user features. # second part is user features.
# final part is rating score. # final part is rating score.
# header is a list of [USE_SEQ_OR_NOT?, SlotType] # header is a list of [USE_SEQ_OR_NOT?, SlotType]
movie_headers = list(common_utils.meta_to_header(meta, 'movie')) movie_headers = list(meta_to_header(meta, 'movie'))
settings.movie_names = [h[0] for h in movie_headers] settings.movie_names = [h[0] for h in movie_headers]
headers = movie_headers headers = movie_headers
user_headers = list(common_utils.meta_to_header(meta, 'user')) user_headers = list(meta_to_header(meta, 'user'))
settings.user_names = [h[0] for h in user_headers] settings.user_names = [h[0] for h in user_headers]
headers.extend(user_headers) headers.extend(user_headers)
headers.append(("rating", dense_vector(1))) # Score headers.append(("rating", dense_vector(1))) # Score
...@@ -62,8 +61,8 @@ def process(settings, filename): ...@@ -62,8 +61,8 @@ def process(settings, filename):
# Get a rating from file. # Get a rating from file.
user_id, movie_id, score = map(int, line.split('::')[:-1]) user_id, movie_id, score = map(int, line.split('::')[:-1])
# Scale score to [-5, +5] # Scale score to [-2, +2]
score = float(score) * 2 - 5.0 score = float(score - 3)
# Get movie/user features by movie_id, user_id # Get movie/user features by movie_id, user_id
movie_meta = settings.meta['movie'][movie_id] movie_meta = settings.meta['movie'][movie_id]
......
...@@ -47,5 +47,4 @@ if __name__ == '__main__': ...@@ -47,5 +47,4 @@ if __name__ == '__main__':
data.append(user_id - 1) data.append(user_id - 1)
data.extend(user_meta) data.extend(user_meta)
print "Prediction Score is %.2f" % ( print "Prediction Score is %.2f" % (
(network.forwardTest(cvt.convert([data]))[0]['value'][0][0] + 5) network.forwardTest(cvt.convert([data]))[0]['value'][0][0] + 3)
/ 2)
...@@ -27,75 +27,66 @@ with open(META_FILE, 'rb') as f: ...@@ -27,75 +27,66 @@ with open(META_FILE, 'rb') as f:
# load meta file # load meta file
meta = pickle.load(f) meta = pickle.load(f)
settings(
batch_size=1600, learning_rate=1e-3, learning_method=RMSPropOptimizer())
def construct_feature(name):
"""
Construct movie/user features.
This method read from meta data. Then convert feature to neural network due
to feature type. The map relation as follow.
* id: embedding => fc
* embedding:
is_sequence: embedding => context_projection => fc => pool
not sequence: embedding => fc
* one_hot_dense: fc => fc
Then gather all features vector, and use a fc layer to combined them as
return.
:param name: 'movie' or 'user'
:type name: basestring
:return: combined feature output
:rtype: LayerOutput
"""
__meta__ = meta[name]['__meta__']['raw_meta']
fusion = []
for each_meta in __meta__:
type_name = each_meta['type']
slot_name = each_meta.get('name', '%s_id' % name)
if type_name == 'id':
slot_dim = each_meta['max']
embedding = embedding_layer(
input=data_layer(
slot_name, size=slot_dim), size=256)
fusion.append(fc_layer(input=embedding, size=256))
elif type_name == 'embedding':
is_seq = each_meta['seq'] == 'sequence'
slot_dim = len(each_meta['dict'])
din = data_layer(slot_name, slot_dim)
embedding = embedding_layer(input=din, size=256)
if is_seq:
fusion.append(
text_conv_pool(
input=embedding, context_len=5, hidden_size=256))
else:
fusion.append(fc_layer(input=embedding, size=256))
elif type_name == 'one_hot_dense':
slot_dim = len(each_meta['dict'])
hidden = fc_layer(input=data_layer(slot_name, slot_dim), size=256)
fusion.append(fc_layer(input=hidden, size=256))
return fc_layer(name="%s_fusion" % name, input=fusion, size=256)
movie_feature = construct_feature("movie")
user_feature = construct_feature("user")
similarity = cos_sim(a=movie_feature, b=user_feature)
if not is_predict: if not is_predict:
outputs(
regression_cost(
input=similarity, label=data_layer(
'rating', size=1)))
define_py_data_sources2( define_py_data_sources2(
'data/train.list', 'data/train.list',
'data/test.list', 'data/test.list',
module='dataprovider', module='dataprovider',
obj='process', obj='process',
args={'meta': meta}) args={'meta': meta})
settings(
batch_size=1600, learning_rate=1e-3, learning_method=RMSPropOptimizer())
movie_meta = meta['movie']['__meta__']['raw_meta']
user_meta = meta['user']['__meta__']['raw_meta']
movie_id = data_layer('movie_id', size=movie_meta[0]['max'])
title = data_layer('title', size=len(movie_meta[1]['dict']))
genres = data_layer('genres', size=len(movie_meta[2]['dict']))
user_id = data_layer('user_id', size=user_meta[0]['max'])
gender = data_layer('gender', size=len(user_meta[1]['dict']))
age = data_layer('age', size=len(user_meta[2]['dict']))
occupation = data_layer('occupation', size=len(user_meta[3]['dict']))
embsize = 256
# construct movie feature
movie_id_emb = embedding_layer(input=movie_id, size=embsize)
movie_id_hidden = fc_layer(input=movie_id_emb, size=embsize)
genres_emb = fc_layer(input=genres, size=embsize)
title_emb = embedding_layer(input=title, size=embsize)
title_hidden = text_conv_pool(
input=title_emb, context_len=5, hidden_size=embsize)
movie_feature = fc_layer(
input=[movie_id_hidden, title_hidden, genres_emb], size=embsize)
# construct user feature
user_id_emb = embedding_layer(input=user_id, size=embsize)
user_id_hidden = fc_layer(input=user_id_emb, size=embsize)
gender_emb = embedding_layer(input=gender, size=embsize)
gender_hidden = fc_layer(input=gender_emb, size=embsize)
age_emb = embedding_layer(input=age, size=embsize)
age_hidden = fc_layer(input=age_emb, size=embsize)
occup_emb = embedding_layer(input=occupation, size=embsize)
occup_hidden = fc_layer(input=occup_emb, size=embsize)
user_feature = fc_layer(
input=[user_id_hidden, gender_hidden, age_hidden, occup_hidden],
size=embsize)
similarity = cos_sim(a=movie_feature, b=user_feature, scale=2)
if not is_predict:
lbl = data_layer('rating', size=1)
cost = regression_cost(input=similarity, label=lbl)
outputs(cost)
else: else:
outputs(similarity) outputs(similarity)
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册