diff --git a/ctr/README.org b/ctr/README.org deleted file mode 100644 index f4589e047de88250ae1c80b734a5bc739525d57c..0000000000000000000000000000000000000000 --- a/ctr/README.org +++ /dev/null @@ -1,178 +0,0 @@ -#+title: 使用 Wide & Deep neural model 进行 CTR 预估 -* 背景介绍 -CTR(Click-through rate) 是用来表示用户点击一个特定链接的概率, -通常被用来衡量一个在线广告系统的有效性。 - -当有多个广告位时,CTR 预估一般会作为排序的基准。 -比如在搜索引擎的广告系统里,当用户输入一个带商业价值的搜索词(query)时,系统大体上会执行下列步骤来展示广告: - -1. 召回满足 query 的广告集合 -2. 业务规则和相关性过滤 -3. 根据拍卖机制和 CTR 排序 -4. 展出广告 - -可以看到,CTR 在最终排序中起到了很重要的作用。 - -在业内,CTR 模型经历了如下的发展阶段: - -- Logistic Regression(LR) / GBDT + 特征工程 -- LR + DNN 特征 -- DNN + 特征工程 - -在发展早期时 LR 一统天下,但最近 DNN 模型由于其强大的学习能力和逐渐成熟的性能优化, -逐渐地接过 CTR 预估任务的大旗。 - -** LR vs DNN -下图展示了 LR 和一个 \(3x2\) 的 NN 模型的结构: - -[[./images/lr-vs-dnn.jpg]] - -LR 部分和蓝色箭头部分可以直接类比到 NN 中的结构,可以看到 LR 和 NN 有一些共通之处(比如权重累加), -但前者的模型复杂度在相同输入维度下比后者可能低很多(从某方面讲,模型越复杂,越有潜力学习到更复杂的信息)。 - -如果 LR 要达到匹敌 NN 的学习能力,必须增加输入的维度,也就是增加特征的数量, -这也就是为何 LR 和大规模的特征工程必须绑定在一起的原因。 - -LR 对于 NN 模型的优势是对大规模稀疏特征的容纳能力,包括内存和计算量等方面,工业界都有非常成熟的优化方法。 - -而 NN 模型具有自己学习新特征的能力,一定程度上能够提升特征使用的效率, -这使得 NN 模型在同样规模特征的情况下,更有可能达到更好的学习效果。 - - -本文后面的章节会演示如何使用 PaddlePaddle 编写一个结合两者优点的模型。 - -* 数据和任务抽象 -我们可以将 ~click~ 作为学习目标,具体任务可以有以下几种方案: - -1. 直接学习 click,0,1 作二元分类 -2. Learning to rank, 具体用 pairwise rank(标签 1>0)或者 list rank -2. 统计每个广告的点击率,将同一个 query 下的广告两两组合,点击率高的>点击率低的,做 rank 或者分类 - -我们直接使用第一种方法做分类任务。 - -我们使用 Kaggle 上 ~Click-through rate prediction~ 任务的数据集[3] 来演示模型。 - -具体的特征处理方法参看 [[./dataset.md][data process]] - -* Wide & Deep Learning Model -谷歌在 16 年提出了 Wide & Deep Learning 的模型框架,用于融合适合学习抽象特征的 DNN 和 适用于大规模稀疏特征的 LR 两种模型的优点。 -** 模型简介 - Wide & Deep Learning Model 可以作为一种相对成熟的模型框架使用, - 在 CTR 预估的任务中工业界也有一定的应用,因此本文将演示使用此模型来完成 CTR 预估的任务。 - - 模型结构如下: - -[[./images/wide-deep.png]] - -模型左边的 Wide 部分,可以容纳大规模系数特征,并且对一些特定的信息(比如 ID)有一定的记忆能力; -而模型右边的 Deep 部分,能够学习特征间的隐含关系,在相同数量的特征下有更好的学习和推导能力。 -** 编写模型输入 - -模型只接受 3 个输入,分别是 - -- ~dnn_input~ ,也就是 Deep 部分的输入 -- ~lr_input~ ,也就是 Wide 部分的输入 -- ~click~ , 点击与否,作为二分类模型学习的标签 - -#+BEGIN_SRC python - dnn_merged_input = layer.data( - name='dnn_input', - type=paddle.data_type.sparse_binary_vector(data_meta_info['dnn_input'])) - - lr_merged_input = layer.data( - name='lr_input', - type=paddle.data_type.sparse_binary_vector(data_meta_info['lr_input'])) - - click = paddle.layer.data(name='click', type=dtype.dense_vector(1)) -#+END_SRC - -** 编写 Wide 部分 - -Wide 部分直接使用了 LR 模型,但激活函数改成了 ~RELU~ 来加速 - #+BEGIN_SRC python - def build_lr_submodel(): - fc = layer.fc( - input=lr_merged_input, size=1, name='lr', act=paddle.activation.Relu()) - return fc - #+END_SRC - -** 编写 Deep 部分 -Deep 部分使用了标准的多层前向传导的 NN 模型 - #+BEGIN_SRC python - def build_dnn_submodel(dnn_layer_dims): - dnn_embedding = layer.fc(input=dnn_merged_input, size=dnn_layer_dims[0]) - _input_layer = dnn_embedding - for no, dim in enumerate(dnn_layer_dims[1:]): - fc = layer.fc( - input=_input_layer, - size=dim, - act=paddle.activation.Relu(), - name='dnn-fc-%d' % no) - _input_layer = fc - return _input_layer - #+END_SRC -** 两者融合 -两个 submodel 的最上层输出加权求和得到整个模型的输出,输出部分使用 ~sigmoid~ 作为激活函数,得到区间\((0,1)\) 的预测值, -来逼近训练数据中二元类别的分布,最终作为 CTR 预估的值使用。 - #+BEGIN_SRC python - # conbine DNN and LR submodels - def combine_submodels(dnn, lr): - merge_layer = layer.concat(input=[dnn, lr]) - fc = layer.fc( - input=merge_layer, - size=1, - name='output', - # use sigmoid function to approximate ctr rate, a float value between 0 and 1. - act=paddle.activation.Sigmoid()) - return fc - #+END_SRC - -** 训练任务的定义 - #+BEGIN_SRC python - dnn = build_dnn_submodel(dnn_layer_dims) - lr = build_lr_submodel() - output = combine_submodels(dnn, lr) - - # ============================================================================== - # cost and train period - # ============================================================================== - classification_cost = paddle.layer.multi_binary_label_cross_entropy_cost( - input=output, label=click) - - params = paddle.parameters.create(classification_cost) - - optimizer = paddle.optimizer.Momentum(momentum=0) - - trainer = paddle.trainer.SGD( - cost=classification_cost, parameters=params, update_equation=optimizer) - - dataset = AvazuDataset(train_data_path, n_records_as_test=test_set_size) - - def event_handler(event): - if isinstance(event, paddle.event.EndIteration): - if event.batch_id % 100 == 0: - logging.warning("Pass %d, Samples %d, Cost %f" % ( - event.pass_id, event.batch_id * batch_size, event.cost)) - - if event.batch_id % 1000 == 0: - result = trainer.test( - reader=paddle.batch(dataset.test, batch_size=1000), - feeding=field_index) - logging.warning("Test %d-%d, Cost %f" % (event.pass_id, event.batch_id, - result.cost)) - - - trainer.train( - reader=paddle.batch( - paddle.reader.shuffle(dataset.train, buf_size=500), - batch_size=batch_size), - feeding=field_index, - event_handler=event_handler, - num_passes=100) - #+END_SRC - -* 引用 -- [1] https://en.wikipedia.org/wiki/Click-through_rate -- [2] Mikolov, Tomáš, et al. "Strategies for training large scale neural network language models." Automatic Speech Recognition and Understanding (ASRU), 2011 IEEE Workshop on. IEEE, 2011. -- [3] https://www.kaggle.com/c/avazu-ctr-prediction/data -- [4] Cheng, Heng-Tze, et al. "Wide & deep learning for recommender systems." Proceedings of the 1st Workshop on Deep Learning for Recommender Systems. ACM, 2016. diff --git a/ctr/dataset.org b/ctr/dataset.org deleted file mode 100644 index d166064c3252cb0e3e7e7b8e8136abb30617e7b4..0000000000000000000000000000000000000000 --- a/ctr/dataset.org +++ /dev/null @@ -1,208 +0,0 @@ -#+title: 数据处理 -* 数据集介绍 -数据集使用 ~csv~ 格式存储,其中各个字段内容如下: - -- ~id~ : ad identifier -- ~click~ : 0/1 for non-click/click -- ~hour~ : format is YYMMDDHH, so 14091123 means 23:00 on Sept. 11, 2014 UTC. -- ~C1~ -- anonymized categorical variable -- ~banner_pos~ -- ~site_id~ -- ~site_domain~ -- ~site_category~ -- ~app_id~ -- ~app_domain~ -- ~app_category~ -- ~device_id~ -- ~device_ip~ -- ~device_model~ -- ~device_type~ -- ~device_conn_type~ -- ~C14-C21~ -- anonymized categorical variables - -* 特征提取 -下面我们会简单演示几种特征的提取方式。 - -原始数据中的特征可以分为以下几类: - -1. ID 类特征(稀疏,数量多) - - - ~id~ - - ~site_id~ - - ~app_id~ - - ~device_id~ - -2. 类别类特征(稀疏,但数量有限) - - - ~C1~ - - ~site_category~ - - ~device_type~ - - ~C14-C21~ - -3. 数值型特征转化为类别型特征 - - - hour (可以转化成数值,也可以按小时为单位转化为类别) - -** 类别类特征 -类别类特征的提取方法有以下两种: - -1. One-hot 表示作为特征 -2. 类似词向量,用一个 Embedding Table 将每个类别映射到对应的向量 - -** ID 类特征 -ID 类特征的特点是稀疏数据,但量比较大,直接使用 One-hot 表示时维度过大。 - -一般会作如下处理: - -1. 确定表示的最大维度 N -2. newid = id % N -3. 用 newid 作为类别类特征使用 - -上面的方法尽管存在一定的碰撞概率,但能够处理任意数量的 ID 特征,并保留一定的效果[2]。 - -** 数值型特征 -一般会做如下处理: - -- 归一化,直接作为特征输入模型 -- 用区间分割处理成类别类特征,稀疏化表示,模糊细微上的差别 - -* 特征处理 -** 类别型特征 - 类别型特征有有限多种值,在模型中,我们一般使用 embedding table 将每种值映射为连续值的向量。 - - 这种特征在输入到模型时,一般使用 One-hot 表示,相关处理方法如下: - - #+BEGIN_SRC python - class CategoryFeatureGenerator(object): - ''' - Generator category features. - - Register all records by calling ~register~ first, then call ~gen~ to generate - one-hot representation for a record. - ''' - - def __init__(self): - self.dic = {'unk': 0} - self.counter = 1 - - def register(self, key): - ''' - Register record. - ''' - if key not in self.dic: - self.dic[key] = self.counter - self.counter += 1 - - def size(self): - return len(self.dic) - - def gen(self, key): - ''' - Generate one-hot representation for a record. - ''' - if key not in self.dic: - res = self.dic['unk'] - else: - res = self.dic[key] - return [res] - - def __repr__(self): - return '' % len(self.dic) - #+END_SRC - - 本任务中,类别类特征会输入到 DNN 中使用。 - -** ID 类特征 - ID 类特征代稀疏值,且值的空间很大的情况,一般用模操作规约到一个有限空间, - 之后可以当成类别类特征使用,这里我们会将 ID 类特征输入到 LR 模型中使用。 - - #+BEGIN_SRC python - class IDfeatureGenerator(object): - def __init__(self, max_dim): - ''' - @max_dim: int - Size of the id elements' space - ''' - self.max_dim = max_dim - - def gen(self, key): - ''' - Generate one-hot representation for records - ''' - return [hash(key) % self.max_dim] - - def size(self): - return self.max_dim - #+END_SRC -** 交叉类特征 - LR 模型作为 Wide & Deep model 的 ~wide~ 部分,可以输入很 wide 的数据(特征空间的维度很大), - 为了充分利用这个优势,我们将演示交叉组合特征构建成更大维度特征的情况,之后塞入到模型中训练。 - - 这里我们依旧使用模操作来约束最终组合出的特征空间的大小,具体实现是直接在 ~IDfeatureGenerator~ 中添加一个 ~gen_cross_feature~ 的方法: - - #+BEGIN_SRC python - def gen_cross_fea(self, fea1, fea2): - key = str(fea1) + str(fea2) - return self.gen(key) - #+END_SRC - - 比如,我们觉得原始数据中, ~device_id~ 和 ~site_id~ 有一些关联(比如某个 device 倾向于浏览特定 site), - 我们通过组合出两者组合来捕捉这类信息。 -** 特征维度 -*** Deep submodel(DNN)特征 -|--------------------+-----------| -| feature | dimention | -|--------------------+-----------| -| ~app_category~ | 21 | -| ~site_category~ | 22 | -| ~device_conn_type~ | 5 | -| ~hour~ | 24 | -| ~banner_pos~ | 7 | -|--------------------+-----------| -| Total | 79 | -|--------------------+-----------| - -*** Wide submodel(LR)特征 -|-------------------------+------------| -| Feature | Dimention | -|-------------------------+------------| -| ~id~ | 100000 | -| ~site_id~ | 100000 | -| ~app_id~ | 100000 | -| ~device_id~ | 100000 | -| ~device_id~ X ~site_id~ | 10000000 | -|-------------------------+------------| -| Total | 10,400,000 | -|-------------------------+------------| -* 输入到 PaddlePaddle 中 -Deep 和 Wide 两部分均以 ~sparse_binary_vector~ 的格式[1]输入,输入前需要将相关特征拼合,模型最终只接受 3 个 input, -分别是 - -1. ~dnn input~ ,DNN 的输入 -2. ~lr input~ , LR 的输入 -3. ~click~ , 标签 - -拼合特征的方法: - -#+BEGIN_SRC python - def concat_sparse_vectors(inputs, dims): - ''' - concaterate sparse vectors into one - - @inputs: list - list of sparse vector - @dims: list of int - dimention of each sparse vector - ''' - res = [] - assert len(inputs) == len(dims) - start = 0 - for no, vec in enumerate(inputs): - for v in vec: - res.append(v + start) - start += dims[no] - return res -#+END_SRC - - -[1] https://github.com/PaddlePaddle/Paddle/blob/develop/doc/api/v1/data_provider/pydataprovider2_en.rst