diff --git a/doc/custom_dataset_reader.md b/doc/custom_dataset_reader.md index 13f2e7752d49caf1e30b41f00ec5c9c98ed46c09..c6dba95100908d741437f4003119c83a072eba89 100644 --- a/doc/custom_dataset_reader.md +++ b/doc/custom_dataset_reader.md @@ -179,7 +179,9 @@ class TrainReader(Reader): 3. 在`init(self)`函数中声明一些在数据读取中会用到的变量,如示例代码中的`cont_min_`、`categorical_range_`等,必要时可以在`config.yaml`文件中配置变量,通过`env.get_global_env()`拿到。 4. 继承并实现基类中的`generate_sample(self, line)`函数,逐行读取数据。该函数应返回一个可以迭代的reader方法(带有yield的函数不再是一个普通的函数,而是一个生成器generator,成为了可以迭代的对象,等价于一个数组、链表、文件、字符串etc.) 5. 在这个可以迭代的函数中,如示例代码中的`def reader()`,我们定义数据读取的逻辑。以行为单位的数据进行截取,转换及预处理。 -6. 最后,我们需要将数据整理为特定的格式,才能够被dataset正确读取,并灌入的训练的网络中。简单来说,数据的输出顺序与我们在网络中创建的`inputs`必须是严格一一对应的,并转换为类似字典的形式。在示例代码中,我们使用`zip`的方法将参数名与数值构成的元组组成了一个list,并将其yield输出。如果展开来看,我们输出的数据形如`[('dense_feature',[value]),('C1',[value]),('C2',[value]),...,('C26',[value]),('label',[value])]` +6. 最后,我们需要将数据整理为特定的格式,才能够被dataset正确读取,并灌入的训练的网络中。简单来说,数据的输出顺序与我们在网络中创建的`inputs`必须是严格一一对应的,并转换为类似字典的形式。在示例代码中,我们使用`zip`的方法将参数名与数值构成的元组组成了一个list,并将其yield输出。如果展开来看,我们输出的数据形如 + + `[('dense_feature',[value]),('C1',[value]),('C2',[value]),...,('C26',[value]),('label',[value])]` ### 调试Reader diff --git a/doc/distributed_train.md b/doc/distributed_train.md index 22897d4a60957e97ecb3fa83e22bce662de05f62..c28425f7aa9a50fd165a4071b2d09a28694a4ab1 100644 --- a/doc/distributed_train.md +++ b/doc/distributed_train.md @@ -1,107 +1,4 @@ # PaddleRec 分布式训练 -## 分布式原理基本介绍 -> 占位 - -## 单机代码转分布式代码 - -> 占位 -### 训练代码准备 -参数服务器架构,有两个重要的组成部分:Server与Worker。为了启动训练,我们是否要准备两套代码分别运行呢?答案是不需要的。Paddle Fleet API将两者运行的逻辑进行了很好的统一,用户只需使用`fleet.init(role)`就可以判断当前启动的程序扮演server还是worker。使用如下的编程范式,只需10行,便可将单机代码转变为分布式代码: -``` python -role = role_maker.PaddleCloudRoleMaker() -fleet.init(role) - -# Define your network, choose your optimizer(SGD/Adam/Adagrad etc.) -strategy = StrategyFactory.create_sync_strategy() -optimizer = fleet.distributed_optimizer(optimizer, strategy) - -if fleet.is_server(): - fleet.init_server() - fleet.run_server() -if fleet.is_worker(): - fleet.init_worker() - # run training - fleet.stop_worker() -``` - -### 运行环境准备 -- Paddle参数服务器模式的训练,目前只支持在`Liunx`环境下运行,推荐使用`ubuntu`或`CentOS` -- Paddle参数服务器模式的前端代码支持`python 2.7`及`python 3.5+`,若使用`Dataset`模式的高性能IO,需使用`python 2.7` -- 使用多台机器进行分布式训练,请确保各自之间可以通过`ip:port`的方式访问`rpc`服务,使用`http/https`代理会导致通信失败 -- 各个机器之间的通信耗费应尽量少 - -假设我们有两台机器,想要在每台机器上分别启动一个`server`进程以及一个`worker`进程,完成2x2(2个参数服务器,2个训练节点)的参数服务器模式分布式训练,按照如下步骤操作。 - -### 启动server -机器A,IP地址是`10.89.176.11`,通信端口是`36000`,配置如下环境变量后,运行训练的入口程序: -```bash -export PADDLE_PSERVERS_IP_PORT_LIST="10.89.176.11:36000,10.89.176.12:36000" -export TRAINING_ROLE=PSERVER -export POD_IP=10.89.176.11 # node A:10.89.176.11 -export PADDLE_PORT=36000 -export PADDLE_TRAINERS_NUM=2 -python -u train.py --is_cloud=1 -``` -应能在日志中看到如下输出: - -> I0318 21:47:01.298220 188592128 grpc_server.cc:470] Server listening on 127.0.0.1:36000 selected port: 36000 - -查看系统进程 -> 8624 | ttys000 | 0:02.31 | python -u train.py --is_cloud=1 - -查看系统进程及端口占用: - -> python3.7 | 8624 | paddle | 8u | IPv6 | 0xe149b87d093872e5 | 0t0 | TCP | localhost:36000 (LISTEN) - -也可以看到我们的`server`进程8624的确在`36000`端口开始了监听,等待`worker`的通信。 - -机器B,IP地址是`10.89.176.12`,通信端口是`36000`,配置如下环境变量后,运行训练的入口程序: -```bash -export PADDLE_PSERVERS_IP_PORT_LIST="10.89.176.11:36000,10.89.176.12:36000" -export TRAINING_ROLE=PSERVER -export POD_IP=10.89.176.12 # node B: 10.89.176.12 -export PADDLE_PORT=36000 -export PADDLE_TRAINERS_NUM=2 -python -u train.py --is_cloud=1 -``` -也可以看到相似的日志输出与进程状况。(进行验证时,请务必确保IP与端口的正确性) - -### 启动worker - -接下来我们分别在机器A与B上开启训练进程。配置如下环境变量并开启训练进程: - -机器A: -```bash -export PADDLE_PSERVERS_IP_PORT_LIST="10.89.176.11:36000,10.89.176.12:36000" -export TRAINING_ROLE=TRAINER -export PADDLE_TRAINERS_NUM=2 -export PADDLE_TRAINER_ID=0 # node A:trainer_id = 0 -python -u train.py --is_cloud=1 -``` - -机器B: -```bash -export PADDLE_PSERVERS_IP_PORT_LIST="10.89.176.11:36000,10.89.176.12:36000" -export TRAINING_ROLE=TRAINER -export PADDLE_TRAINERS_NUM=2 -export PADDLE_TRAINER_ID=1 # node B: trainer_id = 1 -python -u train.py --is_cloud=1 -``` - -运行该命令时,若pserver还未就绪,可在日志输出中看到如下信息: -> server not ready, wait 3 sec to retry... -> -> not ready endpoints:['10.89.176.11:36000', '10.89.176.12:36000'] - -worker进程将持续等待,直到server开始监听,或等待超时。 - -当pserver都准备就绪后,可以在日志输出看到如下信息: -> I0317 11:38:48.099179 16719 communicator.cc:271] Communicator start -> -> I0317 11:38:49.838711 16719 rpc_client.h:107] init rpc client with trainer_id 0 - -至此,分布式训练启动完毕,将开始训练。 - ## PaddleRec分布式运行 > 占位 diff --git a/doc/imgs/fleet-ps.png b/doc/imgs/fleet-ps.png new file mode 100644 index 0000000000000000000000000000000000000000..c82141ea9ff43ed670927ed853fd4c766496a949 Binary files /dev/null and b/doc/imgs/fleet-ps.png differ diff --git a/doc/imgs/ps-overview.png b/doc/imgs/ps-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..8e9509cb3e2b63bec46b40dcf39d8876ac41500e Binary files /dev/null and b/doc/imgs/ps-overview.png differ diff --git a/doc/ps_background.md b/doc/ps_background.md new file mode 100644 index 0000000000000000000000000000000000000000..567c30a9672be8b3fa02da22e755555c68d38ea8 --- /dev/null +++ b/doc/ps_background.md @@ -0,0 +1,140 @@ +# 参数服务器训练简介 + + +如图1所示,参数服务器是分布式训练领域普遍采用的编程架构,主要包含Server和Worker两个部分,其中Server负责参数的存储和更新,而Worker负责训练。飞桨的参数服务器功能也是基于这种经典的架构进行设计和开发的,同时在这基础上进行了SGD(Stochastic Gradient Descent)算法的创新(Geometric Stochastic Gradient Descent)。当前经过大量的实验验证,最佳的方案是每台机器上启动Server和Worker两个进程,而一个Worker进程中可以包含多个用于训练的线程。 +

+ +

+ +参数服务器是主要解决两类问题: + +- 模型参数过大:单机内存空间不足,需要采用分布式存储。 +- 训练数据过多:单机训练太慢,需要加大训练节点,来提高并发训练速度。 设想,当训练数据过多,一个Worker训练太慢时,可以引入多个Worker同时训练,这时Worker之间需要同步模型参数。直观想法是,引入一个Server,Server充当Worker间参数交换的媒介。但当模型参数过大以至于单机存储空间不足时或Worker过多导致一个Server是瓶颈时,就需要引入多个Server。 + + +具体训练流程: + +- 将训练数据均匀的分配给不同的Worker。 +- 将模型参数分片,存储在不同的Server上。 +- Worker端:读取一个minibatch训练数据,从Server端拉取最新的参数,计算梯度,并根据分片上传给不同的Server。 +- Server端:接收Worker端上传的梯度,根据优化算法更新参数。根据Server端每次参数更新是否需要等待所有Worker端的梯度,分为同步训练和异步训练两种机制。 + + +飞桨的参数服务器框架也是基于这种经典的参数服务器模式进行设计和开发的,同时在这基础上进行了SGD(Stochastic Gradient Descent)算法的创新(GEO-SGD)。目前飞桨支持3种模式,分别是同步训练模式、异步训练模式、GEO异步训练模式,如图2所示。 + +

+ +

+ +## 同步训练 + +Worker在训练一个batch的数据后,会合并所有线程的梯度发给Server, Server在收到所有节点的梯度后,会统一进行梯度合并及参数更新。同步训练的优势在于Loss可以比较稳定的下降,缺点是整个训练速度较慢,这是典型的木桶原理,速度的快慢取决于最慢的那个线程的训练计算时间,因此在训练较为复杂的模型时,即模型训练过程中神经网络训练耗时远大于节点间通信耗时的场景下,推荐使用同步训练模式。 + +## 异步训练 + +在训练一个batch的数据后,Worker的每个线程会发送梯度给Server。而Server不会等待接收所有节点的梯度,而是直接基于已收到的梯度进行参数更新。异步训练去除了训练过程中的等待机制,训练速度得到了极大的提升,但是缺点也很明显,那就是Loss下降不稳定,容易发生抖动。建议在个性化推荐(召回、排序)、语义匹配等数据量大的场景使用。 尤其是推荐领域的点击率预估场景,该场景可能会出现千亿甚至万亿规模的稀疏特征,而稀疏参数也可以达到万亿数量级,且需要小时级或分钟级流式增量训练。如果使用异步训练模式,可以很好的满足该场景的online-learning需求。 + +## GEO异步训练 + +GEO(Geometric Stochastic Gradient Descent)异步训练是飞桨自研的异步训练模式,其最大的特点是将参数的更新从Server转移到Worker上。每个Worker在本地训练过程中会使用SGD优化算法更新本地模型参数,在训练若干个batch的数据后,Worker将发送参数更新信息给Server。Server在接收后会通过加和方式更新保存的参数信息。所以显而易见,在GEO异步训练模式下,Worker不用再等待Server发来新的参数即可执行训练,在训练效果和训练速度上有了极大的提升。但是此模式比较适合可以在单机内能完整保存的模型,在搜索、NLP等类型的业务上应用广泛,推荐在词向量、语义匹配等场景中使用。 + +> 运行策略的详细描述可以参考文档:[PaddlePaddle Fluid CPU分布式训练(Trainspiler)使用指南](https://www.paddlepaddle.org.cn/tutorials/projectdetail/454253) + +## 单机代码转分布式 + +### 训练代码准备 +参数服务器架构,有两个重要的组成部分:Server与Worker。为了启动训练,我们是否要准备两套代码分别运行呢?答案是不需要的。Paddle Fleet API将两者运行的逻辑进行了很好的统一,用户只需使用`fleet.init(role)`就可以判断当前启动的程序扮演server还是worker。使用如下的编程范式,只需10行,便可将单机代码转变为分布式代码: +``` python +role = role_maker.PaddleCloudRoleMaker() +fleet.init(role) + +# Define your network, choose your optimizer(SGD/Adam/Adagrad etc.) +strategy = StrategyFactory.create_sync_strategy() +optimizer = fleet.distributed_optimizer(optimizer, strategy) + +if fleet.is_server(): + fleet.init_server() + fleet.run_server() +if fleet.is_worker(): + fleet.init_worker() + # run training + fleet.stop_worker() +``` + +### 运行环境准备 +- Paddle参数服务器模式的训练,目前只支持在`Liunx`环境下运行,推荐使用`ubuntu`或`CentOS` +- Paddle参数服务器模式的前端代码支持`python 2.7`及`python 3.5+`,若使用`Dataset`模式的高性能IO,需使用`python 2.7` +- 使用多台机器进行分布式训练,请确保各自之间可以通过`ip:port`的方式访问`rpc`服务,使用`http/https`代理会导致通信失败 +- 各个机器之间的通信耗费应尽量少 + +假设我们有两台机器,想要在每台机器上分别启动一个`server`进程以及一个`worker`进程,完成2x2(2个参数服务器,2个训练节点)的参数服务器模式分布式训练,按照如下步骤操作。 + +### 启动server +机器A,IP地址是`10.89.176.11`,通信端口是`36000`,配置如下环境变量后,运行训练的入口程序: +```bash +export PADDLE_PSERVERS_IP_PORT_LIST="10.89.176.11:36000,10.89.176.12:36000" +export TRAINING_ROLE=PSERVER +export POD_IP=10.89.176.11 # node A:10.89.176.11 +export PADDLE_PORT=36000 +export PADDLE_TRAINERS_NUM=2 +python -u train.py --is_cloud=1 +``` +应能在日志中看到如下输出: + +> I0318 21:47:01.298220 188592128 grpc_server.cc:470] Server listening on 127.0.0.1:36000 selected port: 36000 + +查看系统进程 +> 8624 | ttys000 | 0:02.31 | python -u train.py --is_cloud=1 + +查看系统进程及端口占用: + +> python3.7 | 8624 | paddle | 8u | IPv6 | 0xe149b87d093872e5 | 0t0 | TCP | localhost:36000 (LISTEN) + +也可以看到我们的`server`进程8624的确在`36000`端口开始了监听,等待`worker`的通信。 + +机器B,IP地址是`10.89.176.12`,通信端口是`36000`,配置如下环境变量后,运行训练的入口程序: +```bash +export PADDLE_PSERVERS_IP_PORT_LIST="10.89.176.11:36000,10.89.176.12:36000" +export TRAINING_ROLE=PSERVER +export POD_IP=10.89.176.12 # node B: 10.89.176.12 +export PADDLE_PORT=36000 +export PADDLE_TRAINERS_NUM=2 +python -u train.py --is_cloud=1 +``` +也可以看到相似的日志输出与进程状况。(进行验证时,请务必确保IP与端口的正确性) + +### 启动worker + +接下来我们分别在机器A与B上开启训练进程。配置如下环境变量并开启训练进程: + +机器A: +```bash +export PADDLE_PSERVERS_IP_PORT_LIST="10.89.176.11:36000,10.89.176.12:36000" +export TRAINING_ROLE=TRAINER +export PADDLE_TRAINERS_NUM=2 +export PADDLE_TRAINER_ID=0 # node A:trainer_id = 0 +python -u train.py --is_cloud=1 +``` + +机器B: +```bash +export PADDLE_PSERVERS_IP_PORT_LIST="10.89.176.11:36000,10.89.176.12:36000" +export TRAINING_ROLE=TRAINER +export PADDLE_TRAINERS_NUM=2 +export PADDLE_TRAINER_ID=1 # node B: trainer_id = 1 +python -u train.py --is_cloud=1 +``` + +运行该命令时,若pserver还未就绪,可在日志输出中看到如下信息: +> server not ready, wait 3 sec to retry... +> +> not ready endpoints:['10.89.176.11:36000', '10.89.176.12:36000'] + +worker进程将持续等待,直到server开始监听,或等待超时。 + +当pserver都准备就绪后,可以在日志输出看到如下信息: +> I0317 11:38:48.099179 16719 communicator.cc:271] Communicator start +> +> I0317 11:38:49.838711 16719 rpc_client.h:107] init rpc client with trainer_id 0 + +至此,分布式训练启动完毕,将开始训练。 \ No newline at end of file diff --git a/doc/rec_background.md b/doc/rec_background.md index 7ea7cbbc897c64d68be516248880f927bcbde996..be3e2a8116e0bbcef811223b8d0feed8a0b192ef 100644 --- a/doc/rec_background.md +++ b/doc/rec_background.md @@ -18,21 +18,7 @@ - 基于内容过滤推荐[[4](#参考文献)](Content-based Filtering Recommendation):该方法利用商品的内容描述,抽象出有意义的特征,通过计算用户的兴趣和商品描述之间的相似度,来给用户做推荐。优点是简单直接,不需要依据其他用户对商品的评价,而是通过商品属性进行商品相似度度量,从而推荐给用户所感兴趣商品的相似商品;缺点是对于没有任何行为的新用户同样存在冷启动的问题。 - 组合推荐[[5](#参考文献)](Hybrid Recommendation):运用不同的输入和技术共同进行推荐,以弥补各自推荐技术的缺点。 -近些年来,深度学习在很多领域都取得了巨大的成功。学术界和工业界都在尝试将深度学习应用于个性化推荐系统领域中。深度学习具有优秀的自动提取特征的能力,能够学习多层次的抽象特征表示,并对异质或跨域的内容信息进行学习,可以一定程度上处理个性化推荐系统冷启动问题[[6](#参考文献)]。本教程主要介绍个性化推荐的深度学习模型,以及如何使用PaddlePaddle实现模型。 - -## 效果展示 - -我们使用包含用户信息、电影信息与电影评分的数据集作为个性化推荐的应用场景。当我们训练好模型后,只需要输入对应的用户ID和电影ID,就可以得出一个匹配的分数(范围[0,5],分数越高视为兴趣越大),然后根据所有电影的推荐得分排序,推荐给用户可能感兴趣的电影。 - -``` -Input movie_id: 1962 -Input user_id: 1 -Prediction Score is 4.25 -``` - -## 模型概览 - -本章中,我们首先介绍YouTube的视频个性化推荐系统[[7](#参考文献)],然后介绍我们实现的融合推荐模型。 +近些年来,深度学习在很多领域都取得了巨大的成功。学术界和工业界都在尝试将深度学习应用于个性化推荐系统领域中。深度学习具有优秀的自动提取特征的能力,能够学习多层次的抽象特征表示,并对异质或跨域的内容信息进行学习,可以一定程度上处理个性化推荐系统冷启动问题[[6](#参考文献)]。 ### YouTube的深度神经网络个性化推荐系统 @@ -47,22 +33,7 @@ YouTube是世界上最大的视频上传、分享和发现网站,YouTube个性 候选生成网络将推荐问题建模为一个类别数极大的多类分类问题:对于一个Youtube用户,使用其观看历史(视频ID)、搜索词记录(search tokens)、人口学信息(如地理位置、用户登录设备)、二值特征(如性别,是否登录)和连续特征(如用户年龄)等,对视频库中所有视频进行多分类,得到每一类别的分类结果(即每一个视频的推荐概率),最终输出概率较高的几百个视频。 -首先,将观看历史及搜索词记录这类历史信息,映射为向量后取平均值得到定长表示;同时,输入人口学特征以优化新用户的推荐效果,并将二值特征和连续特征归一化处理到[0, 1]范围。接下来,将所有特征表示拼接为一个向量,并输入给非线形多层感知器(MLP,详见[识别数字](https://github.com/PaddlePaddle/book/blob/develop/02.recognize_digits/README.cn.md)教程)处理。最后,训练时将MLP的输出给softmax做分类,预测时计算用户的综合特征(MLP的输出)与所有视频的相似度,取得分最高的$k$个作为候选生成网络的筛选结果。图2显示了候选生成网络结构。 - -

-
-图2. 候选生成网络结构 -

- -对于一个用户$U$,预测此刻用户要观看的视频$\omega$为视频$i$的概率公式为: - -

-
-

- -其中$u$为用户$U$的特征表示,$V$为视频库集合,$v_i$为视频库中第$i$个视频的特征表示。$u$和$v_i$为长度相等的向量,两者点积可以通过全连接层实现。 - -考虑到softmax分类的类别数非常多,为了保证一定的计算效率:1)训练阶段,使用负样本类别采样将实际计算的类别数缩小至数千;2)推荐(预测)阶段,忽略softmax的归一化计算(不影响结果),将类别打分问题简化为点积(dot product)空间中的最近邻(nearest neighbor)搜索问题,取与$u$最近的$k$个视频作为生成的候选。 +首先,将观看历史及搜索词记录这类历史信息,映射为向量后取平均值得到定长表示;同时,输入人口学特征以优化新用户的推荐效果,并将二值特征和连续特征归一化处理到[0, 1]范围。接下来,将所有特征表示拼接为一个向量,并输入给非线形多层感知器(MLP,详见[识别数字](https://github.com/PaddlePaddle/book/blob/develop/02.recognize_digits/README.cn.md)教程)处理。最后,训练时将MLP的输出给softmax做分类,预测时计算用户的综合特征(MLP的输出)与所有视频的相似度,取得分最高的K个作为候选生成网络的筛选结果。 #### 排序网络(Ranking Network) 排序网络的结构类似于候选生成网络,但是它的目标是对候选进行更细致的打分排序。和传统广告排序中的特征抽取方法类似,这里也构造了大量的用于视频排序的相关特征(如视频 ID、上次观看时间等)。这些特征的处理方式和候选生成网络类似,不同之处是排序网络的顶部是一个加权逻辑回归(weighted logistic regression),它对所有候选视频进行打分,从高到底排序后将分数较高的一些视频返回给用户。 @@ -74,510 +45,8 @@ YouTube是世界上最大的视频上传、分享和发现网站,YouTube个性 卷积神经网络经常用来处理具有类似网格拓扑结构(grid-like topology)的数据。例如,图像可以视为二维网格的像素点,自然语言可以视为一维的词序列。卷积神经网络可以提取多种局部特征,并对其进行组合抽象得到更高级的特征表示。实验表明,卷积神经网络能高效地对图像及文本问题进行建模处理。 -卷积神经网络主要由卷积(convolution)和池化(pooling)操作构成,其应用及组合方式灵活多变,种类繁多。本小结我们以如图3所示的网络进行讲解: - -

-
-图3. 卷积神经网络文本分类模型 -

- -假设待处理句子的长度为$n$,其中第$i$个词的词向量为$x_i\in\mathbb{R}^k$,$k$为维度大小。 - -首先,进行词向量的拼接操作:将每$h$个词拼接起来形成一个大小为$h$的词窗口,记为$x_{i:i+h-1}$,它表示词序列$x_{i},x_{i+1},\ldots,x_{i+h-1}$的拼接,其中,$i$表示词窗口中第一个词在整个句子中的位置,取值范围从$1$到$n-h+1$,$x_{i:i+h-1}\in\mathbb{R}^{hk}$。 - -其次,进行卷积操作:把卷积核(kernel)$w\in\mathbb{R}^{hk}$应用于包含$h$个词的窗口$x_{i:i+h-1}$,得到特征$c_i=f(w\cdot x_{i:i+h-1}+b)$,其中$b\in\mathbb{R}$为偏置项(bias),$f$为非线性激活函数,如$sigmoid$。将卷积核应用于句子中所有的词窗口${x_{1:h},x_{2:h+1},\ldots,x_{n-h+1:n}}$,产生一个特征图(feature map): - -

-
-

- -接下来,对特征图采用时间维度上的最大池化(max pooling over time)操作得到此卷积核对应的整句话的特征$\hat c$,它是特征图中所有元素的最大值: - -

-
-

- -#### 融合推荐模型概览 - -在融合推荐模型的电影个性化推荐系统中: - -1. 首先,使用用户特征和电影特征作为神经网络的输入,其中: - - - 用户特征融合了四个属性信息,分别是用户ID、性别、职业和年龄。 - - - 电影特征融合了三个属性信息,分别是电影ID、电影类型ID和电影名称。 - -2. 对用户特征,将用户ID映射为维度大小为256的向量表示,输入全连接层,并对其他三个属性也做类似的处理。然后将四个属性的特征表示分别全连接并相加。 - -3. 对电影特征,将电影ID以类似用户ID的方式进行处理,电影类型ID以向量的形式直接输入全连接层,电影名称用文本卷积神经网络得到其定长向量表示。然后将三个属性的特征表示分别全连接并相加。 - -4. 得到用户和电影的向量表示后,计算二者的余弦相似度作为个性化推荐系统的打分。最后,用该相似度打分和用户真实打分的差异的平方作为该回归模型的损失函数。 - -

-
-图4. 融合推荐模型 -

- -## 数据准备 - -### 数据介绍与下载 - -我们以 [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 实验室搜集整理。 - -Paddle在API中提供了自动加载数据的模块。数据模块为 `paddle.dataset.movielens` - - -```python -from __future__ import print_function -import paddle -movie_info = paddle.dataset.movielens.movie_info() -print(list(movie_info.values())[0]) -``` - - -```python -# Run this block to show dataset's documentation -# help(paddle.dataset.movielens) -``` - -在原始数据中包含电影的特征数据,用户的特征数据,和用户对电影的评分。 - -例如,其中某一个电影特征为: - - -```python -movie_info = paddle.dataset.movielens.movie_info() -print(list(movie_info.values())[0]) -``` - - - - -这表示,电影的id是1,标题是《Toy Story》,该电影被分为到三个类别中。这三个类别是动画,儿童,喜剧。 - - -```python -user_info = paddle.dataset.movielens.user_info() -print(list(user_info.values())[0]) -``` - - - - -这表示,该用户ID是1,女性,年龄比18岁还年轻。职业ID是10。 - - -其中,年龄使用下列分布 - -* 1: "Under 18" -* 18: "18-24" -* 25: "25-34" -* 35: "35-44" -* 45: "45-49" -* 50: "50-55" -* 56: "56+" - -职业是从下面几种选项里面选则得出: - -* 0: "other" or not specified -* 1: "academic/educator" -* 2: "artist" -* 3: "clerical/admin" -* 4: "college/grad student" -* 5: "customer service" -* 6: "doctor/health care" -* 7: "executive/managerial" -* 8: "farmer" -* 9: "homemaker" -* 10: "K-12 student" -* 11: "lawyer" -* 12: "programmer" -* 13: "retired" -* 14: "sales/marketing" -* 15: "scientist" -* 16: "self-employed" -* 17: "technician/engineer" -* 18: "tradesman/craftsman" -* 19: "unemployed" -* 20: "writer" - -而对于每一条训练/测试数据,均为 <用户特征> + <电影特征> + 评分。 - -例如,我们获得第一条训练数据: - - -```python -train_set_creator = paddle.dataset.movielens.train() -train_sample = next(train_set_creator()) -uid = train_sample[0] -mov_id = train_sample[len(user_info[uid].value())] -print ("User %s rates Movie %s with Score %s"%(user_info[uid], movie_info[mov_id], train_sample[-1])) -``` - - User rates Movie with Score [5.0] - - -即用户1对电影1193的评价为5分。 - -## 模型配置说明 - -下面我们开始根据输入数据的形式配置模型。首先引入所需的库函数以及定义全局变量。 -- IS_SPARSE: embedding中是否使用稀疏更新 -- PASS_NUM: epoch数量 - - -```python -import math -import sys -import numpy as np -import paddle -import paddle.fluid as fluid -import paddle.fluid.layers as layers -import paddle.fluid.nets as nets - -IS_SPARSE = True -BATCH_SIZE = 256 -PASS_NUM = 20 -``` - -然后为我们的用户特征综合模型定义模型配置 - -```python -def get_usr_combined_features(): - """network definition for user part""" - - USR_DICT_SIZE = paddle.dataset.movielens.max_user_id() + 1 - - uid = fluid.data(name='user_id', shape=[None], dtype='int64') - - usr_emb = fluid.embedding( - input=uid, - dtype='float32', - size=[USR_DICT_SIZE, 32], - param_attr='user_table', - is_sparse=IS_SPARSE) - - usr_fc = layers.fc(input=usr_emb, size=32) - - USR_GENDER_DICT_SIZE = 2 - - usr_gender_id = fluid.data(name='gender_id', shape=[None], dtype='int64') - - usr_gender_emb = fluid.embedding( - input=usr_gender_id, - size=[USR_GENDER_DICT_SIZE, 16], - param_attr='gender_table', - is_sparse=IS_SPARSE) - - usr_gender_fc = layers.fc(input=usr_gender_emb, size=16) - - USR_AGE_DICT_SIZE = len(paddle.dataset.movielens.age_table) - usr_age_id = fluid.data(name='age_id', shape=[None], dtype="int64") - - usr_age_emb = fluid.embedding( - input=usr_age_id, - size=[USR_AGE_DICT_SIZE, 16], - is_sparse=IS_SPARSE, - param_attr='age_table') - - usr_age_fc = layers.fc(input=usr_age_emb, size=16) - - USR_JOB_DICT_SIZE = paddle.dataset.movielens.max_job_id() + 1 - usr_job_id = fluid.data(name='job_id', shape=[None], dtype="int64") - - usr_job_emb = fluid.embedding( - input=usr_job_id, - size=[USR_JOB_DICT_SIZE, 16], - param_attr='job_table', - is_sparse=IS_SPARSE) - - usr_job_fc = layers.fc(input=usr_job_emb, size=16) - - concat_embed = layers.concat( - input=[usr_fc, usr_gender_fc, usr_age_fc, usr_job_fc], axis=1) - - usr_combined_features = layers.fc(input=concat_embed, size=200, act="tanh") - - return usr_combined_features -``` - -如上述代码所示,对于每个用户,我们输入4维特征。其中包括user_id,gender_id,age_id,job_id。这几维特征均是简单的整数值。为了后续神经网络处理这些特征方便,我们借鉴NLP中的语言模型,将这几维离散的整数值,变换成embedding取出。分别形成usr_emb, usr_gender_emb, usr_age_emb, usr_job_emb。 - -然后,我们对于所有的用户特征,均输入到一个全连接层(fc)中。将所有特征融合为一个200维度的特征。 - -进而,我们对每一个电影特征做类似的变换,网络配置为: - - -```python -def get_mov_combined_features(): - """network definition for item(movie) part""" - - MOV_DICT_SIZE = paddle.dataset.movielens.max_movie_id() + 1 - - mov_id = fluid.data(name='movie_id', shape=[None], dtype='int64') - - mov_emb = fluid.embedding( - input=mov_id, - dtype='float32', - size=[MOV_DICT_SIZE, 32], - param_attr='movie_table', - is_sparse=IS_SPARSE) - - mov_fc = layers.fc(input=mov_emb, size=32) - - CATEGORY_DICT_SIZE = len(paddle.dataset.movielens.movie_categories()) - - category_id = fluid.data( - name='category_id', shape=[None], dtype='int64', lod_level=1) - - mov_categories_emb = fluid.embedding( - input=category_id, size=[CATEGORY_DICT_SIZE, 32], is_sparse=IS_SPARSE) - - mov_categories_hidden = layers.sequence_pool( - input=mov_categories_emb, pool_type="sum") - - MOV_TITLE_DICT_SIZE = len(paddle.dataset.movielens.get_movie_title_dict()) - - mov_title_id = fluid.data( - name='movie_title', shape=[None], dtype='int64', lod_level=1) - - mov_title_emb = fluid.embedding( - input=mov_title_id, size=[MOV_TITLE_DICT_SIZE, 32], is_sparse=IS_SPARSE) - - mov_title_conv = nets.sequence_conv_pool( - input=mov_title_emb, - num_filters=32, - filter_size=3, - act="tanh", - pool_type="sum") - - concat_embed = layers.concat( - input=[mov_fc, mov_categories_hidden, mov_title_conv], axis=1) - - mov_combined_features = layers.fc(input=concat_embed, size=200, act="tanh") - - return mov_combined_features -``` - -电影标题名称(title)是一个序列的整数,整数代表的是这个词在索引序列中的下标。这个序列会被送入 `sequence_conv_pool` 层,这个层会在时间维度上使用卷积和池化。因为如此,所以输出会是固定长度,尽管输入的序列长度各不相同。 - -最后,我们定义一个`inference_program`来使用余弦相似度计算用户特征与电影特征的相似性。 - -```python -def inference_program(): - """the combined network""" - - usr_combined_features = get_usr_combined_features() - mov_combined_features = get_mov_combined_features() - - inference = layers.cos_sim(X=usr_combined_features, Y=mov_combined_features) - scale_infer = layers.scale(x=inference, scale=5.0) - - return scale_infer -``` - -进而,我们定义一个`train_program`来使用`inference_program`计算出的结果,在标记数据的帮助下来计算误差。我们还定义了一个`optimizer_func`来定义优化器。 - -```python -def train_program(): - """define the cost function""" - - scale_infer = inference_program() - - label = fluid.data(name='score', shape=[None, 1], dtype='float32') - square_cost = layers.square_error_cost(input=scale_infer, label=label) - avg_cost = layers.mean(square_cost) - - return [avg_cost, scale_infer] - - -def optimizer_func(): - return fluid.optimizer.SGD(learning_rate=0.2) -``` - - -## 训练模型 - -### 定义训练环境 -定义您的训练环境,可以指定训练是发生在CPU还是GPU上。 - -```python -use_cuda = False -place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace() -``` - -### 定义数据提供器 -下一步是为训练和测试定义数据提供器。提供器读入一个大小为 `BATCH_SIZE`的数据。`paddle.dataset.movielens.train` 每次会在乱序化后提供一个大小为`BATCH_SIZE`的数据,乱序化的大小为缓存大小`buf_size`。 - -```python -train_reader = fluid.io.batch( - fluid.io.shuffle( - paddle.dataset.movielens.train(), buf_size=8192), - batch_size=BATCH_SIZE) - -test_reader = fluid.io.batch( - paddle.dataset.movielens.test(), batch_size=BATCH_SIZE) -``` - -### 构造训练过程(trainer) -我们这里构造了一个训练过程,包括训练优化函数。 - -### 提供数据 - -`feed_order`用来定义每条产生的数据和`paddle.layer.data`之间的映射关系。比如,`movielens.train`产生的第一列的数据对应的是`user_id`这个特征。 - -```python -feed_order = [ - 'user_id', 'gender_id', 'age_id', 'job_id', 'movie_id', 'category_id', - 'movie_title', 'score' -] -``` - -### 构建训练程序以及测试程序 -分别构建训练程序和测试程序,并引入训练优化器。 - -```python -main_program = fluid.default_main_program() -star_program = fluid.default_startup_program() -[avg_cost, scale_infer] = train_program() - -test_program = main_program.clone(for_test=True) -sgd_optimizer = optimizer_func() -sgd_optimizer.minimize(avg_cost) -exe = fluid.Executor(place) - -def train_test(program, reader): - count = 0 - feed_var_list = [ - program.global_block().var(var_name) for var_name in feed_order - ] - feeder_test = fluid.DataFeeder( - feed_list=feed_var_list, place=place) - test_exe = fluid.Executor(place) - accumulated = 0 - for test_data in reader(): - avg_cost_np = test_exe.run(program=program, - feed=feeder_test.feed(test_data), - fetch_list=[avg_cost]) - accumulated += avg_cost_np[0] - count += 1 - return accumulated / count -``` - -### 构建训练主循环并开始训练 -我们根据上面定义的训练循环数(`PASS_NUM`)和一些别的参数,来进行训练循环,并且每次循环都进行一次测试,当测试结果足够好时退出训练并保存训练好的参数。 - -```python -# Specify the directory path to save the parameters -params_dirname = "recommender_system.inference.model" - -from paddle.utils.plot import Ploter -train_prompt = "Train cost" -test_prompt = "Test cost" - -plot_cost = Ploter(train_prompt, test_prompt) - -def train_loop(): - feed_list = [ - main_program.global_block().var(var_name) for var_name in feed_order - ] - feeder = fluid.DataFeeder(feed_list, place) - exe.run(star_program) - - for pass_id in range(PASS_NUM): - for batch_id, data in enumerate(train_reader()): - # train a mini-batch - outs = exe.run(program=main_program, - feed=feeder.feed(data), - fetch_list=[avg_cost]) - out = np.array(outs[0]) - - # get test avg_cost - test_avg_cost = train_test(test_program, test_reader) - - plot_cost.append(train_prompt, batch_id, outs[0]) - plot_cost.append(test_prompt, batch_id, test_avg_cost) - plot_cost.plot() - - if batch_id == 20: - if params_dirname is not None: - fluid.io.save_inference_model(params_dirname, [ - "user_id", "gender_id", "age_id", "job_id", - "movie_id", "category_id", "movie_title" - ], [scale_infer], exe) - return - print('EpochID {0}, BatchID {1}, Test Loss {2:0.2}'.format( - pass_id + 1, batch_id + 1, float(test_avg_cost))) - - if math.isnan(float(out[0])): - sys.exit("got NaN loss, training failed.") -``` -开始训练 -```python -train_loop() -``` - -## 应用模型 - -### 生成测试数据 -使用 create_lod_tensor(data, lod, place) 的API来生成细节层次的张量。`data`是一个序列,每个元素是一个索引号的序列。`lod`是细节层次的信息,对应于`data`。比如,data = [[10, 2, 3], [2, 3]] 意味着它包含两个序列,长度分别是3和2。于是相应地 lod = [[3, 2]],它表明其包含一层细节信息,意味着 `data` 有两个序列,长度分别是3和2。 - -在这个预测例子中,我们试着预测用户ID为1的用户对于电影'Hunchback of Notre Dame'的评分 - -```python -infer_movie_id = 783 -infer_movie_name = paddle.dataset.movielens.movie_info()[infer_movie_id].title -user_id = np.array([1]).astype("int64").reshape(-1) -gender_id = np.array([1]).astype("int64").reshape(-1) -age_id = np.array([0]).astype("int64").reshape(-1) -job_id = np.array([10]).astype("int64").reshape(-1) -movie_id = np.array([783]).astype("int64").reshape(-1) # Hunchback of Notre Dame -category_id = fluid.create_lod_tensor(np.array([10, 8, 9], dtype='int64'), [[3]], place) # Animation, Children's, Musical -movie_title = fluid.create_lod_tensor(np.array([1069, 4140, 2923, 710, 988], dtype='int64'), [[5]], - place) # 'hunchback','of','notre','dame','the' -``` - -### 构建预测过程并测试 -与训练过程类似,我们需要构建一个预测过程。其中, `params_dirname`是之前用来存放训练过程中的各个参数的地址。 - -```python -place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace() -exe = fluid.Executor(place) - -inference_scope = fluid.core.Scope() -``` - -### 测试 -现在我们可以进行预测了。我们要提供的`feed_order`应该和训练过程一致。 - - -```python -with fluid.scope_guard(inference_scope): - [inferencer, feed_target_names, - fetch_targets] = fluid.io.load_inference_model(params_dirname, exe) - - results = exe.run(inferencer, - feed={ - 'user_id': user_id, - 'gender_id': gender_id, - 'age_id': age_id, - 'job_id': job_id, - 'movie_id': movie_id, - 'category_id': category_id, - 'movie_title': movie_title - }, - fetch_list=fetch_targets, - return_numpy=False) - predict_rating = np.array(results[0]) - print("Predict Rating of user id 1 on movie \"" + infer_movie_name + - "\" is " + str(predict_rating[0][0])) - print("Actual Rating of user id 1 on movie \"" + infer_movie_name + - "\" is 4.") -``` - -## 总结 +卷积神经网络主要由卷积(convolution)和池化(pooling)操作构成,其应用及组合方式灵活多变,种类繁多。 -本文介绍了传统的个性化推荐系统方法和YouTube的深度神经网络个性化推荐系统,并以电影推荐为例,使用PaddlePaddle训练了一个个性化推荐神经网络模型。个性化推荐系统几乎涵盖了电商系统、社交网络、广告推荐、搜索引擎等领域的方方面面,而在图像处理、自然语言处理等领域已经发挥重要作用的深度学习技术,也将会在个性化推荐系统领域大放异彩。 ## 参考文献 @@ -588,7 +57,6 @@ with fluid.scope_guard(inference_scope): 4. [Peter Brusilovsky](https://en.wikipedia.org/wiki/Peter_Brusilovsky) (2007). *The Adaptive Web*. p. 325. 5. Robin Burke , [Hybrid Web Recommender Systems](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.435.7538&rep=rep1&type=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. 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.
diff --git a/readme.md b/readme.md index 69f2fbc997046a4b47101bde562bf71af2b6642f..4c63fc1210cedc3e4c70a21a48f49fed453872a0 100644 --- a/readme.md +++ b/readme.md @@ -57,7 +57,7 @@ python -m pip install paddlepaddle -i https://mirror.baidu.com/pypi/simple ``` - 2. 源码安装Fleet-Rec + 2. 源码安装PaddleRec ``` git clone https://github.com/PaddlePaddle/PaddleRec/ @@ -68,7 +68,7 @@

快速启动

-### 直接启动内置模型的默认配置 +### 启动内置模型的默认配置 目前框架内置了多个模型,简单的命令即可使用内置模型开始单机训练和本地1*1模拟训练,我们以`ctr-dnn`为例介绍PaddleRec的简单使用。 @@ -96,11 +96,11 @@ python -m paddlerec.run -m paddlerec.models.rank.dnn -e local_cluster python -m paddlerec.run -m paddlerec.models.rank.dnn -e cluster ``` -### 启动内置模型自定配置 +### 启动内置模型的自定配置 若您复用内置模型,对**yaml**配置文件进行了修改,如更改超参,重新配置数据后,可以直接使用paddlerec运行该yaml文件。 -例如在paddlerec代码目录下,修改了dnn模型yaml的配置后,运行`ctr-dnn`模型: +例如在paddlerec代码目录下,修改了dnn模型`config.yaml`的配置后,运行`ctr-dnn`模型: ```bash python -m paddlerec.run -m ./models/rank/dnn/config.yaml -e single ``` @@ -133,10 +133,8 @@ python -m paddlerec.run -m ./models/rank/dnn/config.yaml -e single

文档

### 新手教程 -* [环境要求](#环境要求) -* [安装命令](#安装命令) -* [快速开始](#一行命令启动训练) -* [推荐系统背景知识](doc/rec_background.md) +* [推荐系统背景介绍](doc/rec_background.md) +* [分布式-参数服务器背景介绍](doc/ps_background.md) ### 进阶教程 * [自定义数据集及Reader](doc/custom_dataset_reader.md)