diff --git a/core/trainers/single_trainer.py b/core/trainers/single_trainer.py index c579bd377c02550f387644f5b4c7bda8f585bb44..888a93f3ddb7c1045505fc98eebc15d6dd19e950 100755 --- a/core/trainers/single_trainer.py +++ b/core/trainers/single_trainer.py @@ -57,7 +57,12 @@ class SingleTrainer(TranspileTrainer): if metrics: self.fetch_vars = metrics.values() self.fetch_alias = metrics.keys() - context['status'] = 'startup_pass' + evaluate_only = envs.get_global_env( + 'evaluate_only', False, namespace='evaluate') + if evaluate_only: + context['status'] = 'infer_pass' + else: + context['status'] = 'startup_pass' def startup(self, context): self._exe.run(fluid.default_startup_program()) diff --git a/core/trainers/transpiler_trainer.py b/core/trainers/transpiler_trainer.py index 775f0135fd5e5c54b0454711920a801608955f91..b50541241774ad771e7838925f9f3e7ad257d0ac 100755 --- a/core/trainers/transpiler_trainer.py +++ b/core/trainers/transpiler_trainer.py @@ -46,7 +46,7 @@ class TranspileTrainer(Trainer): namespace = "train.reader" class_name = "TrainReader" else: - readerdataloader = self.model._infer_data_loader + dataloader = self.model._infer_data_loader namespace = "evaluate.reader" class_name = "EvaluateReader" @@ -239,8 +239,17 @@ class TranspileTrainer(Trainer): metrics_format = ", ".join(metrics_format) self._exe.run(startup_program) - for (epoch, model_dir) in self.increment_models: - print("Begin to infer epoch {}, model_dir: {}".format(epoch, model_dir)) + model_list = self.increment_models + + evaluate_only = envs.get_global_env( + 'evaluate_only', False, namespace='evaluate') + if evaluate_only: + model_list = [(0, envs.get_global_env( + 'evaluate_model_path', "", namespace='evaluate'))] + + for (epoch, model_dir) in model_list: + print("Begin to infer No.{} model, model_dir: {}".format( + epoch, model_dir)) program = infer_program.clone() fluid.io.load_persistables(self._exe, model_dir, program) reader.start() diff --git a/models/rank/dnn/README.md b/models/rank/dnn/README.md index 2d335cdc8c8a5bcd1979d666cfb858c0acd8d94b..c307efcc255bc3d087de0cbcb2aaae39e65ce2f2 100644 --- a/models/rank/dnn/README.md +++ b/models/rank/dnn/README.md @@ -259,12 +259,3 @@ auc_var, batch_auc_var, auc_states = fluid.layers.auc( ``` 完成上述组网后,我们最终可以通过训练拿到`avg_cost`与`auc`两个重要指标。 - - -# 训练 -## 单机训练 -## 分布式训练 -### 本地模拟分布式 -### 百度云分布式训练 - -# 预测 diff --git a/models/recall/tdm/README.md b/models/recall/tdm/README.md deleted file mode 100644 index f87b93230ccb14d53d42b542969443b4bc74e8f0..0000000000000000000000000000000000000000 --- a/models/recall/tdm/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# 基于DNN模型的TDM召回模型 - -## 论文介绍 -## 目录 -* [数据准备](#数据准备) - * [数据来源](#数据来源) - * [数据预处理](#数据预处理) - * [一键下载训练及测试数据](#一键下载训练及测试数据) - * [数据读取](#数据读取) -* [模型组网](#模型组网) - * [输入定义](#输入定义) - * [模型组网](#模型组网) - * [Loss定义](#loss定义) -* [训练](#训练) - * [单机训练](#单机训练) - * [分布式训练](#分布式训练) - * [本地模拟分布式](#本地模拟分布式) - * [百度云分布式训练](#百度云分布式训练) -* [预测](#预测) - * [本地预测](#本地预测) - * [Benchmark](#benchmark) -* [调优](#调优) - * [效果调优](#效果调优) - * [性能调优](#性能调优) -* [模型产出与部署](#模型产出与部署) - -# -## 数据准备 -### 数据来源 -### 数据预处理 -### 一键下载训练及测试数据 -### 数据读取 - - -# -## 模型组网 -### 输入定义 -### 模型组网 -### Loss定义 - -# -## 训练 -### 单机训练 -### 分布式训练 -#### 本地模拟分布式 -#### 百度云分布式训练 - -# -## 预测 -### 预测组网 -### 本地预测 -### Benchmark - -# -## 调优 -### 效果调优 -### 性能调优 - -# -## 模型产出与部署 - diff --git a/models/recall/word2vec/config.yaml b/models/recall/word2vec/config.yaml index fd48ef056a1dad7bcb0b992a7f3619e86d9f1cfc..22c37226d9e459b5193830b4fea36e28b3bc0d1e 100755 --- a/models/recall/word2vec/config.yaml +++ b/models/recall/word2vec/config.yaml @@ -12,7 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. evaluate: - workspace: "paddlerec.models.recall.word2vec" + workspace: "paddlerec.models.recall.word2vec" + + evaluate_only: True + evaluate_model_path: "" + reader: batch_size: 50 class: "{workspace}/w2v_evaluate_reader.py" diff --git a/models/treebased/README.md b/models/treebased/README.md new file mode 100644 index 0000000000000000000000000000000000000000..84403b9ed8944898caf3e751ef16f370f6ad2efb --- /dev/null +++ b/models/treebased/README.md @@ -0,0 +1,30 @@ +# Paddle-TDM +本代码库提供了基于PaddlePaddle实现的TreeBased推荐搜索算法,主要包含以下组成: + +- 基于fake数据集,适用于快速调试的paddle-tdm模型。主要用于理解paddle-tdm的设计原理,高效上手设计适合您的使用场景的模型。 + +以上内容将随paddle版本迭代不断更新,欢迎您关注该代码库。 + +# +## TDM设计思路 + +### 基本概念 +TDM是为大规模推荐系统设计的、能承载任意先进模型来高效检索用户兴趣的推荐算法解决方案。TDM基于树结构,提出了一套对用户兴趣度量进行层次化建模与检索的方法论,使得系统能直接利高级深度学习模型在全库范围内检索用户兴趣。其基本原理是使用树结构对全库item进行索引,然后训练深度模型以支持树上的逐层检索,从而将大规模推荐中全库检索的复杂度由O(n)(n为所有item的量级)下降至O(log n)。 + +### 核心问题 + +1. 如何构建树结构? +2. 如何基于树结构做深度学习模型的训练? +3. 如何基于树及模型进行高效检索? + +### PaddlePaddle的TDM方案 + +1. 树结构的数据,来源于各个业务的实际场景,构造方式各有不同,paddle-TDM一期暂不提供统一的树的构造流程,但会统一树构造好之后,输入paddle网络的数据组织形式。业务方可以使用任意工具构造自己的树,生成指定的数据格式,参与tdm网络训练。 +2. 网络训练中,有三个核心问题: + + - 如何组网?答:paddle封装了大量的深度学习OP,用户可以根据需求设计自己的网络结构。 + - 训练数据如何组织?答:tdm的训练数据主要为:`user/query emb` 加 `item`的正样本,`item`需要映射到树的某个叶子节点。用户只需准备符合该构成的数据即可。负样本的生成,会基于用户提供的树结构,以及paddle提供的`tdm-sampler op`完成高效的负采样,并自动添加相应的label,参与tdm中深度学习模型的训练。 + - 大规模的数据与模型训练如何实现?答:基于paddle优秀的大规模参数服务器分布式能力,可以实现高效的分布式训练。基于paddle-fleet api,学习门槛极低,且可以灵活的支持增量训练,流式训练等业务需求。 +3. 训练好模型后,可以基于paddle,将检索与打分等流程都融入paddle的组网中,生成inference_model与参数文件,基于PaddlePaddle的预测库或者PaddleLite进行快速部署与高效检索。 + +# \ No newline at end of file diff --git a/models/recall/tdm/__init__.py b/models/treebased/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from models/recall/tdm/__init__.py rename to models/treebased/__init__.py diff --git a/models/treebased/tdm/README.md b/models/treebased/tdm/README.md new file mode 100644 index 0000000000000000000000000000000000000000..220b90481962a157aa1bb348c040d35fbcda6fa7 --- /dev/null +++ b/models/treebased/tdm/README.md @@ -0,0 +1,532 @@ +# Paddle-TDM-DEMO + +本代码仅作tdm组网示例,使用fake数据集,用于快速调研paddle-tdm。 + + +## 树结构的准备 +### 名词概念 + +为防止概念混淆,让我们明确tdm中名词的概念: + +
+ +
+ +- **item**:具有实际物理含义,是我们希望通过tdm检索最后得到的结果,如一个商品,一篇文章,一个关键词等等,在tdm模型中,item位于树的叶子节点。item有其自身的ID,我们姑且称之为 `item_id`。 +- **节点(node)**:tdm树的任意节点。我们完成聚类后,会生成一颗树,树的叶子节点对应着item。而非叶子节点,则是一种类别上的概括,从大方向的兴趣,不断细分到小方向的兴趣。我们希望这棵树的结构满足最大堆树的性质。同样,节点也有其自身的ID,我们称之为node_id。如上图,最左下方的节点,它的node_id是14,而对应的item_id是0. +- **Node-Embedding**:注意,此处的Embedding,并非我们已有的item-embedding,而是构建完成的树的节点对应的Embedding,由item-embedding通过规则生成,是我们的网络主要训练的目标。ID范围为所有0->节点数-1。我们同时也需准备一个映射表,来告诉模型,item_id到node_id的映射关系。 +- **Travel**:是指叶子节点从root开始直到其自身的遍历路径,如上图,14号节点的Travel:0->1->3->7->14 +- **Layer**:指树的层,如上图,共有4层。 + +> Paddle-TDM在训练时,不会改动树的结构,只会改动Node-Embedding。 + + +### 树的准备流程 + +让我们以上图给出的简单树结构为例,来介绍TDM的模型准备流程。假设我们已经完成了树的聚类,并得到了如上图所示的树结构: + +- 问题一:叶子节点的Embedding值是多少?答:叶子节点的node-embedding与对应的item的embedding值一致 +- 问题二:非叶子节点的Embedding值是多少?答:非叶子节点的Embedding值初始化目前有两种方案:1、随机初始化。2、使用其子节点的emb均值。 +- 问题三:树一定要求二叉树吗?答:没有这样的硬性要求。 +- 问题四:若有item不在最后一层,树不平衡怎么办?答:树尽量平衡,不在最后一层也没有关系,我们可以通过其他方式让网络正常训练。 +- 问题五:是每个用户都有一棵树,还是所有用户共享一颗树?答:只有一棵树,通过每一层的模型牵引节点的emb,使其尽量满足最大堆性质。 + +完成以上步骤,我们已经得到了树的结构,与每个节点的全部信息,现在让我们将其转换为Paddle-TDM训练所需的格式。我们需要产出四个数据: +#### Layer_list + +记录了每一层都有哪些节点。训练用 +```bash +# Layer list +1,2 +3,4,5,6 +7,8,9,10,11,12,13 +14,15,16,17,18,19,20,21,22,23,24,25 +``` +#### Travel_list + +记录每个叶子节点的Travel路径。训练用 +```bash +# Travel list +1,3,7,14 +1,3,7,15 +1,3,8,16 +... +2,5,12,25 +2,6,13,0 +``` + +#### Tree_Info + +记录了每个节点的信息,主要为:是否是item/item_id,所在层级,父节点,子节点。检索用 +```bash +# Tree info +0,0,0,1,2 +0,1,0,3,4 +0,1,0,5,6 +0,2,1,7,8 +... +10,4,12,0,0 +11,4,12,0,0 +``` + +#### Tree_Embedding + +记录所有节点的Embedding表,格式如正常表。训练及检索用 + +以上数据设计的初衷是为了高效,在paddle网络中以Tensor的形式参与训练,运行时,不再需要进行频繁的树的结构的遍历,直接根据已有信息进行快速查找与训练。以上数据可以明文保存,但最终都需要转成ndarray,参与网络的初始化。 +结合示例树,数据可以组织如右,下面介绍一些细节: + +- Layer list从第2(index=1)层开始即可,因为0号节点不参与训练也不参与检索; +- Travel list的按照item_id的顺序组织,如第一行对应着item_id=0的遍历信息,同样,也不需要包含0号节点; +- Travel_list每行的长度必须相等,遇到不在最后一层的item,需要padding 0 直至长度和其他item一致; +- Tree_info包含了0号节点的信息,主要考量是,当我们拿到node_id查找其信息时,可以根据id在该数据中寻找第id行; +- Tree_info各列的含义是:itme_id(若无则为0),层级Layer,父节点node_id(无则为0),子节点node_id(若无则为0,若子节点数量不满,则需要paddding 0) + +## 数据准备 +如前所述,若我们关心的是输入一个user emb,得到他所感兴趣的item id,那我们就准备user_emb + 正样本item的格式的数据,负采样会通过paddle的tdm_sampler op得到。数据的准备不涉及树的结构,因而可以快速复用其他任务的训练数据来验证TDM效果。 + +```bash +# emb(float) \t item_id (int) +-0.9480544328689575 0.8702829480171204 -0.5691063404083252 ...... -0.04391402751207352 -0.5352795124053955 -0.9972627758979797 0.9397293329238892 4690780 +``` + +## TDM网络设计 +假设输入数据是 Emb + item_id,下面让我们开始介绍一个最简单的网络设计。 + +
+ +
+ +上图给出了一个非常简单的TDM示例网络,没有添加任何复杂的逻辑,纯用DNN实现。 +TDM的组网,宏观上,可以概括为三个部分 +- 第一部分,输入侧的组网,如果想要对user/query进行一些预处理,或者添加Attention结构,通常都是在这一层次实现。 +- 第二部分,每层的输入与节点信息交互的组网,这一部分是将user/query的信息与node信息结合,在树的不同层下,进行不同粒度兴趣的学习。通常而言,第一部分与第二部分具有紧密的联系,可以统一为一个部分。 +- 第三部分,最终的判别组网,将每层交互得到的信息进行最终的概率判决。但这一层也不是必须的,并不要求所有层的信息都经过一个统一的分类器,可以各层拥有独立的概率判决器。为了逻辑划分更加清晰,我们在示例中添加了这个层次的组网,方便您更加直观的理解tdm网络。 +再次强调,该示例组网仅为展示tdm的基本运行逻辑,请基于这个框架,升级改进您自己的网络。 + +## TDM组网细节 + +### 训练组网 + +训练组网中需要重点关注五个部分: +1. 网络输入的定义 +2. 网络中输入侧的处理逻辑 +3. node的负采样组网 +4. input与node的交互网络 +5. 判别及loss计算组网 + + +#### 输入的定义 + +首先简要介绍输入的定义: + +**demo模型,假设输入为两个元素:** +> 一、user/query的emb表示,该emb应该来源于特征的组合在某个空间的映射(比如若干特征取emb后concat到一起),或其他预训练模型的处理结果(比如将明文query通过nlp预处理得到emb表示) + +> 二、item的正样本,是发生了实际点击/购买/浏览等行为的item_id,与输入的user/query emb强相关,是我们之后通过预测想得到的结果。 + +在paddle组网中,我们这样定义上面两个变量: +```python +def input_data(self): + """ + 指定tdm训练网络的输入变量 + """ + input_emb = fluid.data( + name="input_emb", + shape=[None, self.input_embed_size], + dtype="float32", + ) + + item_label = fluid.data( + name="item_label", + shape=[None, 1], + dtype="int64", + ) + + inputs = [input_emb] + [item_label] + return inputs +``` + +#### 输入侧的组网 + +**输入侧组网由FC层组成** +> 一、`input_fc`,主要功能是input_emb维度的压缩,只需一个fc即可。 + +> 二、`layer_fc`,主要功能是将input_emb映射到不同的兴趣层空间,和当层的node学习兴趣关系。有多少层,就添加多少个fc。 + +
+ +
+ +在paddle组网中,我们这样快速实现输入侧组网: +```python +def input_trans_layer(self, input_emb): + """ + 输入侧训练组网 + """ + # 将input压缩到与node相同的维度 + input_fc_out = fluid.layers.fc( + input=input_emb, + size=self.node_emb_size, + act=None, + param_attr=fluid.ParamAttr(name="trans.input_fc.weight"), + bias_attr=fluid.ParamAttr(name="trans.input_fc.bias"), + ) + + # 将input_emb映射到各个不同层次的向量表示空间 + input_layer_fc_out = [ + fluid.layers.fc( + input=input_fc_out, + size=self.node_emb_size, + act="tanh", + param_attr=fluid.ParamAttr( + name="trans.layer_fc.weight." + str(i)), + bias_attr=fluid.ParamAttr(name="trans.layer_fc.bias."+str(i)), + ) for i in range(self.max_layers) + ] + + return input_layer_fc_out +``` + +#### node的负采样组网 +**tdm 负采样的核心是tdm_sampler OP** + +tdm_sampler的运行逻辑如下: + +1. 输入item_id,读travel_list,查表,得到该item_id对应的遍历路径(从靠近根节点的第一层一直往下直到存放该item的node) +2. 读layer_list,查表,得到每层都有哪些node +3. 循环:i = 0, 从第i层开始进行负采样 + - 在item遍历路径上的node视为正样本,`positive_node_id`由`travel_list[item_id][i]`给出,其他同层的兄弟节点视为负样本,该层节点列表由`layer_list[i]`给出,如果`positive_node_id`不在`layer_list[i]`中,会提示错误。 + + - 在兄弟节点中进行随机采样,采样N个node,N由`neg_sampling_list[i]`的值决定,如果该值大于兄弟节点的数量,会提示错误。 采样结果不会重复,且不会采样到正样本。 + + - 如果`output_positive=True`,则会同时输出正负样本,否则只输出负采样的结果 + + - 生成该层`label`,shape与采样结果一致,正样本对应的label=1,负样本的label=0 + + - 生成该层`mask`,如果树是不平衡的,则有些item不会位于树的最后一层,所以遍历路径的实际长度会比其他item少,为了tensor维度一致,travel_list中padding了0。当遇到了padding的0时,tdm_sampler也会输出正常维度的采样结果,采样结果与label都为0。为了区分这部分虚拟的采样结果与真实采样结果,会给虚拟采样结果额外设置mask=0,如果是真实采样结果mask=1 + - i += 1, 若i > layer_nums, break +4. 对输出的采样结果、label、mask进行整理: + - 如果`output_list=False`,则会输出三个tensor(samping_result, label, mask),shape形如`[batch_size, all_layer_sampling_nums, 1]`; + - 若`output_list=True`,则会输出三个`list[tensor,...,tensor]`,`sampling_result_list/label_list/mask_list`,`len(list)`等于层数,将采样结果按照分属哪一层进行拆分,每个tensor的shape形如`[batch_size, layer_i_sampling_nums,1]` + +```python +# 根据输入的item的正样本在给定的树上进行负采样 +# sample_nodes 是采样的node_id的结果,包含正负样本 +# sample_label 是采样的node_id对应的正负标签 +# sample_mask 是为了保持tensor维度一致,padding部分的标签,若为0,则是padding的虚拟node_id +sample_nodes, sample_label, sample_mask = fluid.contrib.layers.tdm_sampler( + x=item_label, + neg_samples_num_list=self.neg_sampling_list, + layer_node_num_list=self.layer_node_num_list, + leaf_node_num=self.leaf_node_num, + tree_travel_attr=fluid.ParamAttr(name="TDM_Tree_Travel"), + tree_layer_attr=fluid.ParamAttr(name="TDM_Tree_Layer"), + output_positive=self.output_positive, + output_list=True, + seed=0, + tree_dtype='int64', + dtype='int64' +) + +# 查表得到每个节点的Embedding +sample_nodes_emb = [ + fluid.embedding( + input=sample_nodes[i], + is_sparse=True, + size=[self.node_nums, self.node_emb_size], + param_attr=fluid.ParamAttr( + name="TDM_Tree_Emb") + ) for i in range(self.max_layers) +] + +# 此处进行reshape是为了之后层次化的分类器训练 +sample_nodes_emb = [ + fluid.layers.reshape(sample_nodes_emb[i], + [-1, self.neg_sampling_list[i] + + self.output_positive, self.node_emb_size] + ) for i in range(self.max_layers) +] + +``` + +#### input与node的交互网络 +**交互网络由FC层组成** + +主要包含两个流程: +> 一、将输入进行维度上的`expand`,与采样得到的noed数量一致(当然也可以使用其他`broadcast`的网络结构) + +> 二、input_emb与node_emb进行`concat`,过FC,计算兴趣上的匹配关系 + +
+ +
+ +在paddle的组网中,我们这样实现这一部分的逻辑: + +```python +def _expand_layer(self, input_layer, node, layer_idx): + # 扩展input的输入,使数量与node一致, + # 也可以以其他broadcast的操作进行代替 + # 同时兼容了训练组网与预测组网 + input_layer_unsequeeze = fluid.layers.unsqueeze( + input=input_layer, axes=[1]) + if self.is_test: + input_layer_expand = fluid.layers.expand( + input_layer_unsequeeze, expand_times=[1, node.shape[1], 1]) + else: + input_layer_expand = fluid.layers.expand( + input_layer_unsequeeze, expand_times=[1, node[layer_idx].shape[1], 1]) + return input_layer_expand + +def classifier_layer(self, input, node): + # 扩展input,使维度与node匹配 + input_expand = [ + self._expand_layer(input[i], node, i) for i in range(self.max_layers) + ] + + # 将input_emb与node_emb concat到一起过分类器FC + input_node_concat = [ + fluid.layers.concat( + input=[input_expand[i], node[i]], + axis=2) for i in range(self.max_layers) + ] + + hidden_states_fc = [ + fluid.layers.fc( + input=input_node_concat[i], + size=self.node_emb_size, + num_flatten_dims=2, + act="tanh", + param_attr=fluid.ParamAttr( + name="cls.concat_fc.weight."+str(i)), + bias_attr=fluid.ParamAttr(name="cls.concat_fc.bias."+str(i)) + ) for i in range(self.max_layers) + ] + + # 如果将所有层次的node放到一起计算loss,则需要在此处concat + # 将分类器结果以batch为准绳concat到一起,而不是layer + # 维度形如[batch_size, total_node_num, node_emb_size] + hidden_states_concat = fluid.layers.concat(hidden_states_fc, axis=1) + return hidden_states_concat +``` + +#### 判别及loss计算组网 + +最终的判别组网会将所有层的输出打平放到一起,过`tdm.cls_fc`,再过`softmax_with_cross_entropy`层,计算cost,同时得到softmax的中间结果,计算acc或者auc。 + +```python +tdm_fc = fluid.layers.fc(input=layer_classifier_res, + size=self.label_nums, + act=None, + num_flatten_dims=2, + param_attr=fluid.ParamAttr( + name="tdm.cls_fc.weight"), + bias_attr=fluid.ParamAttr(name="tdm.cls_fc.bias")) + +# 将loss打平,放到一起计算整体网络的loss +tdm_fc_re = fluid.layers.reshape(tdm_fc, [-1, 2]) + +# 若想对各个层次的loss辅以不同的权重,则在此处无需concat +# 支持各个层次分别计算loss,再乘相应的权重 +sample_label = fluid.layers.concat(sample_label, axis=1) +sample_label.stop_gradient = True +labels_reshape = fluid.layers.reshape(sample_label, [-1, 1]) + +# 计算整体的loss并得到softmax的输出 +cost, softmax_prob = fluid.layers.softmax_with_cross_entropy( + logits=tdm_fc_re, label=labels_reshape, return_softmax=True) + +# 通过mask过滤掉虚拟节点的loss +sample_mask = fluid.layers.concat(sample_mask, axis=1) +sample_mask.stop_gradient = True +mask_reshape = fluid.layers.reshape(sample_mask, [-1, 1]) + +mask_index = fluid.layers.where(mask_reshape != 0) +mask_cost = fluid.layers.gather_nd(cost, mask_index) + +# 计算该batch的均值loss,同时计算acc, 亦可在这里计算auc +avg_cost = fluid.layers.reduce_mean(mask_cost) +acc = fluid.layers.accuracy(input=softmax_prob, label=labels_reshape) +``` + +### 预测组网 + +预测的整体流程类似于beamsearch。预测组网中需要重点关注三个部分: +1. 网络输入的定义,及初始检索层的确定 +2. 串行的通过各层分类器的流程 +3. 每层选出topK及最终选出topK的方法 + +#### 网络输入 + +首先考虑这样一个问题,假设树是一颗十层的完全二叉树,我们要取topK=1024的召回结果,那么我们需不需要从树的第一层开始逐层计算fc与topK?答案显然是否定的,我们只需要计算最后一层的1024个节点即可。在开发的实验中,我们得到的结论是,tdm检索时,计算复杂度并不高,时间耗费中OP的调度占据了主要矛盾,通过模型裁剪,树剪枝,模型量化等都可以加快预测速度。 + +因此infer组网中首先考虑了跳层与剪枝的实现:我们定义了三个变量,`input_emb`,`first_layer_node`,`first_layer_node_mask`作为网络的输入。 +- `input_emb`:预测时输入的user/query向量。 +- `first_layer_node`:检索时的起始node_id,是变长类型。这样设置的好处是:1、可以输入某一层所有节点node_id,从而在某一层开始进行检索。2、也可以设置为特定node_id,从指定的树分枝开始检索。输入的node_id应该位于同一层。 +- `first_layer_node_mask`:维度与`first_layer_node`相同,值一一对应。若node是叶子节点,则mask=1,否则设置mask=0。 + +在demo网络中,我们设置为从某一层的所有节点开始进行检索。paddle组网对输入定义的实现如下: +```python +def input_data(self): + input_emb = fluid.layers.data( + name="input_emb", + shape=[self.input_embed_size], + dtype="float32", + ) + + # first_layer 与 first_layer_mask 对应着infer起始的节点 + first_layer = fluid.layers.data( + name="first_layer_node", + shape=[1], + dtype="int64", + lod_level=1, #支持变长 + ) + + first_layer_mask = fluid.layers.data( + name="first_layer_node_mask", + shape=[1], + dtype="int64", + lod_level=1, + ) + + inputs = [input_emb] + [first_layer] + [first_layer_mask] + return inputs +``` + +确定起始层的方式比较简单,比较topK的大小与当层节点数,选取第一个节点数大于等于topK的层作为起始层,取它的节点作为起始节点。代码如下: +```python +def create_first_layer(self, args): + """decide which layer to start infer""" + first_layer_id = 0 + for idx, layer_node in args.layer_node_num_list: + if layer_node >= self.topK: + first_layer_id = idx + break + first_layer_node = self.layer_list[first_layer_id] + self.first_layer_idx = first_layer_id + return first_layer_node +``` + +#### 通过各层分类器的流程 + +tdm的检索逻辑类似beamsearch,简单来说:在每一层计算打分,得到topK的节点,将这些节点的孩子节点作为下一层的输入,如此循环,得到最终的topK。但仍然有一些需要注意的细节,下面将详细介绍。 + +- 问题一:怎么处理`input_emb`? + + - input_emb过`input_fc`,检索中,只需过一次即可: + ```python + nput_trans_emb = self.input_trans_net.input_fc_infer(input_emb) + ``` + - 在通过每一层的分类器之前,过`layer_fc`,指定`layer_idx`,加载对应层的分类器,将输入映射到不同的兴趣粒度空间 + ```python + input_fc_out = self.input_trans_net.layer_fc_infer( + input_trans_emb, layer_idx) + ``` + +- 问题二:怎样实现beamsearch? + + 我们通过在每一层计算打分,计算topK并拿到对应的孩子节点,for循环这个过程实现beamsearch。 + ```python + for layer_idx in range(self.first_layer_idx, self.max_layers): + # 确定当前层的需要计算的节点数 + if layer_idx == self.first_layer_idx: + current_layer_node_num = len(self.first_layer_node) + else: + current_layer_node_num = current_layer_node.shape[1] * \ + current_layer_node.shape[2] + + current_layer_node = fluid.layers.reshape( + current_layer_node, [self.batch_size, current_layer_node_num]) + current_layer_child_mask = fluid.layers.reshape( + current_layer_child_mask, [self.batch_size, current_layer_node_num]) + + # 查当前层node的emb + node_emb = fluid.embedding( + input=current_layer_node, + size=[self.node_nums, self.node_embed_size], + param_attr=fluid.ParamAttr(name="TDM_Tree_Emb")) + + input_fc_out = self.input_trans_net.layer_fc_infer( + input_trans_emb, layer_idx) + + # 过每一层的分类器 + layer_classifier_res = self.layer_classifier.classifier_layer_infer(input_fc_out, node_emb, layer_idx) + + # 过最终的判别分类器 + tdm_fc = fluid.layers.fc(input=layer_classifier_res, + size=self.label_nums, + act=None, + num_flatten_dims=2, + param_attr=fluid.ParamAttr( + name="tdm.cls_fc.weight"), + bias_attr=fluid.ParamAttr(name="tdm.cls_fc.bias")) + + prob = fluid.layers.softmax(tdm_fc) + positive_prob = fluid.layers.slice( + prob, axes=[2], starts=[1], ends=[2]) + prob_re = fluid.layers.reshape( + positive_prob, [self.batch_size, current_layer_node_num]) + + # 过滤掉padding产生的无效节点(node_id=0) + node_zero_mask = fluid.layers.cast(current_layer_node, 'bool') + node_zero_mask = fluid.layers.cast(node_zero_mask, 'float') + prob_re = prob_re * node_zero_mask + + # 在当前层的分类结果中取topK,并将对应的score及node_id保存下来 + k = self.topK + if current_layer_node_num < self.topK: + k = current_layer_node_num + _, topk_i = fluid.layers.topk(prob_re, k) + + # index_sample op根据下标索引tensor对应位置的值 + # 若paddle版本>2.0,调用方式为paddle.index_sample + top_node = fluid.contrib.layers.index_sample( + current_layer_node, topk_i) + prob_re_mask = prob_re * current_layer_child_mask # 过滤掉非叶子节点 + topk_value = fluid.contrib.layers.index_sample( + prob_re_mask, topk_i) + node_score.append(topk_value) + node_list.append(top_node) + + # 取当前层topK结果的孩子节点,作为下一层的输入 + if layer_idx < self.max_layers - 1: + # tdm_child op 根据输入返回其 child 及 child_mask + # 若child是叶子节点,则child_mask=1,否则为0 + current_layer_node, current_layer_child_mask = \ + fluid.contrib.layers.tdm_child(x=top_node, + node_nums=self.node_nums, + child_nums=self.child_nums, + param_attr=fluid.ParamAttr( + name="TDM_Tree_Info"), + dtype='int64') + ``` + +- 问题三:怎样得到最终的topK个叶子节点? + + 在过每层分类器的过程中,我们保存了每层的topk节点,并将非叶子节点的打分置为了0,保存在`node_score`与`node_list`中。显然,我们需要召回的是topk个叶子节点,对所有层的叶子节点打分再计算一次topk,拿到结果。 + + ```python + total_node_score = fluid.layers.concat(node_score, axis=1) + total_node = fluid.layers.concat(node_list, axis=1) + + # 考虑到树可能是不平衡的,计算所有层的叶子节点的topK + res_score, res_i = fluid.layers.topk(total_node_score, self.topK) + res_layer_node = fluid.contrib.layers.index_sample(total_node, res_i) + res_node = fluid.layers.reshape(res_layer_node, [-1, self.topK, 1]) + ``` + +- 问题四:现在拿到的是node_id,怎么转成item_id? + + 如果有额外的映射表,可以将node_id转为item_id,但也有更方便的方法。在生成`tree_info`时,我们保存了每个node_id相应的item_id信息,直接使用它,将node_id对应的tree_info行的数据的第零维的item_id切出来。 + + ```python + # 利用Tree_info信息,将node_id转换为item_id + tree_info = fluid.default_main_program().global_block().var("TDM_Tree_Info") + res_node_emb = fluid.layers.gather_nd(tree_info, res_node) + + res_item = fluid.layers.slice( + res_node_emb, axes=[2], starts=[0], ends=[1]) + res_item_re = fluid.layers.reshape(res_item, [-1, self.topK]) + ``` +以上,我们便完成了训练及预测组网的全部部分。 diff --git a/models/treebased/tdm/__init__.py b/models/treebased/tdm/__init__.py new file mode 100755 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/models/recall/tdm/config.yaml b/models/treebased/tdm/config.yaml similarity index 98% rename from models/recall/tdm/config.yaml rename to models/treebased/tdm/config.yaml index 2b2ec9f9ce83b728a1b3ce18fb0e01a6a06f4e11..ec79017496c971d40d62f9f8a64769f2f39ccd9e 100755 --- a/models/recall/tdm/config.yaml +++ b/models/treebased/tdm/config.yaml @@ -18,7 +18,7 @@ train: strategy: "async" epochs: 2 - workspace: "paddlerec.models.recall.tdm" + workspace: "paddlerec.models.treebased.tdm" reader: batch_size: 32 diff --git a/models/recall/tdm/data/test/demo_fake_test.txt b/models/treebased/tdm/data/test/demo_fake_test.txt similarity index 100% rename from models/recall/tdm/data/test/demo_fake_test.txt rename to models/treebased/tdm/data/test/demo_fake_test.txt diff --git a/models/recall/tdm/data/train/demo_fake_input.txt b/models/treebased/tdm/data/train/demo_fake_input.txt similarity index 100% rename from models/recall/tdm/data/train/demo_fake_input.txt rename to models/treebased/tdm/data/train/demo_fake_input.txt diff --git a/models/treebased/tdm/img/demo_network.png b/models/treebased/tdm/img/demo_network.png new file mode 100644 index 0000000000000000000000000000000000000000..3a9981f360ffaaaa337d0c87b2fd6135969bc2a3 Binary files /dev/null and b/models/treebased/tdm/img/demo_network.png differ diff --git a/models/treebased/tdm/img/demo_tree.png b/models/treebased/tdm/img/demo_tree.png new file mode 100644 index 0000000000000000000000000000000000000000..5e50d34ab7e53ce4bf505b5affc5c2e1d9ebfa73 Binary files /dev/null and b/models/treebased/tdm/img/demo_tree.png differ diff --git a/models/treebased/tdm/img/dnn-net.png b/models/treebased/tdm/img/dnn-net.png new file mode 100644 index 0000000000000000000000000000000000000000..3974bdc10e74ff80f738b4372947c7acc669a507 Binary files /dev/null and b/models/treebased/tdm/img/dnn-net.png differ diff --git a/models/treebased/tdm/img/input-net.png b/models/treebased/tdm/img/input-net.png new file mode 100644 index 0000000000000000000000000000000000000000..ca0633cc120367de1a7834a9987764ca4a092951 Binary files /dev/null and b/models/treebased/tdm/img/input-net.png differ diff --git a/models/recall/tdm/model.py b/models/treebased/tdm/model.py similarity index 100% rename from models/recall/tdm/model.py rename to models/treebased/tdm/model.py diff --git a/models/recall/tdm/tdm_evaluate_reader.py b/models/treebased/tdm/tdm_evaluate_reader.py similarity index 100% rename from models/recall/tdm/tdm_evaluate_reader.py rename to models/treebased/tdm/tdm_evaluate_reader.py diff --git a/models/recall/tdm/tdm_reader.py b/models/treebased/tdm/tdm_reader.py similarity index 100% rename from models/recall/tdm/tdm_reader.py rename to models/treebased/tdm/tdm_reader.py diff --git a/models/recall/tdm/tree/layer_list.txt b/models/treebased/tdm/tree/layer_list.txt similarity index 100% rename from models/recall/tdm/tree/layer_list.txt rename to models/treebased/tdm/tree/layer_list.txt diff --git a/models/recall/tdm/tree/travel_list.npy b/models/treebased/tdm/tree/travel_list.npy similarity index 100% rename from models/recall/tdm/tree/travel_list.npy rename to models/treebased/tdm/tree/travel_list.npy diff --git a/models/recall/tdm/tree/tree_emb.npy b/models/treebased/tdm/tree/tree_emb.npy similarity index 100% rename from models/recall/tdm/tree/tree_emb.npy rename to models/treebased/tdm/tree/tree_emb.npy diff --git a/models/recall/tdm/tree/tree_info.npy b/models/treebased/tdm/tree/tree_info.npy similarity index 100% rename from models/recall/tdm/tree/tree_info.npy rename to models/treebased/tdm/tree/tree_info.npy diff --git a/readme.md b/readme.md index 4c63fc1210cedc3e4c70a21a48f49fed453872a0..9a9417da172294369e0ed0a20780c44a448b62ef 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,4 @@ +
@@ -70,7 +71,7 @@ ### 启动内置模型的默认配置 -目前框架内置了多个模型,简单的命令即可使用内置模型开始单机训练和本地1*1模拟训练,我们以`ctr-dnn`为例介绍PaddleRec的简单使用。 +目前框架内置了多个模型,简单的命令即可使用内置模型开始单机训练和本地1*1模拟训练,我们以`dnn`为例介绍PaddleRec的简单使用。