From 62d9503aaf82a1756b8dd8e3f199c2d6ea7efd60 Mon Sep 17 00:00:00 2001 From: Superjom Date: Thu, 25 May 2017 10:41:10 +0800 Subject: [PATCH] finish code --- ctr/README.md | 202 +++++++++++++++++++++++++++------------- ctr/README.org | 213 +++++++++++++++++++++++-------------------- ctr/data_provider.py | 44 +++++++-- ctr/train.py | 37 ++++---- 4 files changed, 303 insertions(+), 193 deletions(-) diff --git a/ctr/README.md b/ctr/README.md index 549f0ddd..411227d0 100644 --- a/ctr/README.md +++ b/ctr/README.md @@ -2,29 +2,29 @@

Table of Contents

- + + # 背景介绍 @@ -47,17 +47,17 @@ CTR(Click-through rate) 是用来表示用户点击一个特定链接的概率 - LR + DNN 特征 - DNN + 特征工程 -在发展早期是 LR 一统天下,但最近 DNN 模型由于其强大的学习能力和逐渐成熟的性能优化, +在发展早期时 LR 一统天下,但最近 DNN 模型由于其强大的学习能力和逐渐成熟的性能优化, 逐渐地接过 CTR 预估任务的大旗。 - + ## LR vs DNN 下图展示了 LR 和一个 \(3x2\) 的 NN 模型的结构: -![img](背景介绍/LR vs DNN_2017-05-22_10-09-02.jpg) +![img](背景介绍/lr-vs-dnn_2017-05-25_10-36-48.jpg) LR 部分和蓝色箭头部分可以直接类比到 NN 中的结构,可以看到 LR 和 NN 有一些共通之处(比如权重累加), 但前者的模型复杂度在相同输入维度下比后者可能第很多(从某方面讲,模型越复杂,越有潜力学习到更复杂的信息)。 @@ -68,10 +68,12 @@ LR 部分和蓝色箭头部分可以直接类比到 NN 中的结构,可以看 而 NN 模型具有自己学习新特征的能力,一定程度上能够提升特征使用的效率, 这使得 NN 模型在同样规模特征的情况下,更有可能达到更好的学习效果。 -本文会演示,如何使用 NN 模型来完成 CTR 预估的任务。 +LR 对于 NN 模型的优势是对大规模稀疏特征的容纳能力,包括内存和计算量等,工业界都有非常成熟的优化方法。 +本文后面的章节会演示如何使用 Paddle 编写一个结合两者优点的模型。 - + + # 数据和任务抽象 @@ -82,80 +84,150 @@ LR 部分和蓝色箭头部分可以直接类比到 NN 中的结构,可以看 这里,我们直接使用第一种方法做分类任务。 -我们使用 Kaggle 上 \`Click-through rate prediction\` 任务的数据集来演示模型。 +我们使用 Kaggle 上 \`Click-through rate prediction\` 任务的数据集[1] 来演示模型。 + +具体的特征处理方法参看 [data process](./dataset.md) + -各个字段内容如下: + -- 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 -- bannerpos -- siteid -- sitedomain -- sitecategory -- appid -- appdomain -- appcategory -- deviceid -- deviceip -- devicemodel -- devicetype -- deviceconntype -- C14-C21 – anonymized categorical variables +# Wide & Deep Learning Model +谷歌在 16 年提出了 Wide & Deep Learning 的模型框架,用于融合 适合学习抽象特征的 DNN 和 适用于大规模系数特征的 LR 两种模型的优点。 - -# 特征提取 + -下面我们会简单演示几种特征的提取方式。 +## 模型简介 -原始数据中的特征可以分为以下几类: +Wide & Deep Learning Model 可以作为一种相对成熟的模型框架使用, +在 CTR 预估的任务中工业界也有一定的应用,因此本文将演示使用此模型来完成 CTR 预估的任务。 -1. ID 类特征(稀疏,数量多) - - id - - siteid - - appid - - deviceid +模型结构如下: -2. 类别类特征(稀疏,但数量有限) - - C1 - - sitecategory - - devicetype - - C14-C21 +![img](Wide & Deep Learning Model/wide-deep_2017-05-25_10-24-26.png) -3. 数值型特征 - - hour (可以转化成数值,也可以按小时为单位转化为类别) +模型左边的 Wide 部分,可以容纳大规模系数特征,并且对一些特定的信息(比如 ID)有一定的记忆能力; +而模型右边的 Deep 部分,能够学习特征间的隐含关系,在相同数量的特征下有更好的学习和推导能力。 - + -## ID 类特征 +## 编写模型输入 -ID 类特征的特点是稀疏数据,但量比较大,直接使用 One-hot 表示时维度过大。 +模型只接受 3 个输入,分别是 -一般会作如下处理: +- \`dnninput\` ,也就是 Deep 部分的输入 +- \`lrinput\` ,也就是 Wide 部分的输入 +- \`click\` , 点击与否,作为二分类模型学习的标签 + 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)) - -# 模型实现 + +## 编写 Wide 部分 - + def build_lr_submodel(): + fc = layer.fc( + input=lr_merged_input, size=1, name='lr', act=paddle.activation.Relu()) + return fc -## DNN 简单模型 + - +## 编写 Deep 部分 -## long wide 复杂模型 + 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 - + + +## 两者融合 + + # 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 + + + + +## 训练任务的定义 + + 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) + + + # 写在最后 - +- [1] +- [2] Strategies for Training Large Scale Neural Network Language Models +- + +[1] diff --git a/ctr/README.org b/ctr/README.org index 2652ad3d..9773604b 100644 --- a/ctr/README.org +++ b/ctr/README.org @@ -1,3 +1,4 @@ +#+title: 使用 Wide & Deep neural model 进行 CTR 预估 * 背景介绍 CTR(Click-through rate) 是用来表示用户点击一个特定链接的概率, 通常被用来衡量一个在线广告系统的有效性。 @@ -24,8 +25,8 @@ CTR(Click-through rate) 是用来表示用户点击一个特定链接的概率 ** LR vs DNN 下图展示了 LR 和一个 \(3x2\) 的 NN 模型的结构: -#+DOWNLOADED: file:/Users/superjom/project/paddle_models/ctr/img/LR vs DNN.jpg @ 2017-05-22 10:09:02 -[[file:背景介绍/LR vs DNN_2017-05-22_10-09-02.jpg]] +#+DOWNLOADED: file:/Users/superjom/project/paddle_models/ctr/img/lr-vs-dnn.jpg @ 2017-05-25 10:36:48 +[[file:背景介绍/lr-vs-dnn_2017-05-25_10-36-48.jpg]] LR 部分和蓝色箭头部分可以直接类比到 NN 中的结构,可以看到 LR 和 NN 有一些共通之处(比如权重累加), 但前者的模型复杂度在相同输入维度下比后者可能第很多(从某方面讲,模型越复杂,越有潜力学习到更复杂的信息)。 @@ -48,121 +49,131 @@ LR 对于 NN 模型的优势是对大规模稀疏特征的容纳能力,包括 这里,我们直接使用第一种方法做分类任务。 -我们使用 Kaggle 上 `Click-through rate prediction` 任务的数据集来演示模型。 - -各个字段内容如下: - -- 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]。 - -** 数值型特征 -一般会做如下处理: - -- 归一化,直接作为特征输入模型 -- 用区间分割处理成类别类特征,稀疏化表示,模糊细微上的差别 - -** 处理方法实现 -接下来我们演示具体方法的实现,为了简洁,我们只对一下几个特征作处理: - -- site_category -- device_type -- id -- site_id -- hour - - -#+BEGIN_SRC python - import sys - - class CategoryFeatureGenerator(object): - ''' - Generator category features. - ''' - def __init__(self): - self.dic = {} - self.counter = 0 - - def register(self, key): - if key not in self.dic: - self.dic[key] = self.counter - self.counter += 1 - - def lookup(self, key): - return self.dic[key] - -#+END_SRC +我们使用 Kaggle 上 `Click-through rate prediction` 任务的数据集[1] 来演示模型。 +具体的特征处理方法参看 [[./dataset.md][data process]] * Wide & Deep Learning Model 谷歌在 16 年提出了 Wide & Deep Learning 的模型框架,用于融合 适合学习抽象特征的 DNN 和 适用于大规模系数特征的 LR 两种模型的优点。 - ** 模型简介 Wide & Deep Learning Model 可以作为一种相对成熟的模型框架使用, 在 CTR 预估的任务中工业界也有一定的应用,因此本文将演示使用此模型来完成 CTR 预估的任务。 模型结构如下: - [ pic ] +#+DOWNLOADED: file:/Users/superjom/project/paddle_models/ctr/img/wide-deep.png @ 2017-05-25 10:24:26 +[[file:Wide & Deep Learning Model/wide-deep_2017-05-25_10-24-26.png]] -** 编写 LR 部分 -** 编写 DNN 部分 +模型左边的 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 部分 + + #+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 部分 + + #+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 ** 两者融合 + #+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] Strategies for Training Large Scale Neural Network Language Models - https://www.kaggle.com/c/avazu-ctr-prediction/data +[1] https://www.kaggle.com/c/avazu-ctr-prediction/data diff --git a/ctr/data_provider.py b/ctr/data_provider.py index 28d4eb9f..596112d7 100644 --- a/ctr/data_provider.py +++ b/ctr/data_provider.py @@ -63,6 +63,9 @@ def get_all_field_names(mode=0): 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): @@ -70,6 +73,9 @@ class CategoryFeatureGenerator(object): self.counter = 1 def register(self, key): + ''' + Register record. + ''' if key not in self.dic: self.dic[key] = self.counter self.counter += 1 @@ -78,6 +84,9 @@ class CategoryFeatureGenerator(object): 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: @@ -90,11 +99,22 @@ class CategoryFeatureGenerator(object): 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 gen_cross_fea(self, fea1, fea2): + key = str(fea1) + str(fea2) + return self.gen(key) + def size(self): return self.max_dim @@ -152,7 +172,8 @@ def detect_dataset(path, topn, id_fea_space=10000): feature_dims['dnn_input'] = np.sum( feature_dims[key] for key in categorial_features + ['hour']) + 10 - feature_dims['lr_input'] = np.sum(feature_dims[key] for key in id_features) + 10 + feature_dims['lr_input'] = np.sum(feature_dims[key] + for key in id_features) + 10 return feature_dims @@ -180,21 +201,22 @@ class AvazuDataset(object): TRAIN_MODE = 0 TEST_MODE = 1 - def __init__(self, train_path, test_path=None): + def __init__(self, train_path, n_records_as_test=-1): self.train_path = train_path - self.test_path = test_path + self.n_records_as_test = n_records_as_test # task model: 0 train, 1 test self.mode = 0 def train(self): self.mode = self.TRAIN_MODE - return self._parse(self.train_path) + return self._parse( + self.train_path, skip_n_lines=self.n_records_as_test) def test(self): self.mode = self.TEST_MODE - return self._parse(self.test_path) + return self._parse(self.train_path, top_n_lines=self.n_records_as_test) - def _parse(self, path): + def _parse(self, path, skip_n_lines=-1, top_n_lines=-1): with open(path, 'rb') as csvfile: reader = csv.DictReader(csvfile) @@ -204,6 +226,11 @@ class AvazuDataset(object): id_dims = [feature_dims[key] for key in id_features] for row_id, row in enumerate(reader): + if skip_n_lines > 0 and row_id < skip_n_lines: + continue + if top_n_lines > 0 and row_id > top_n_lines: + break + record = [] for key in categorial_features: record.append(fields[key].gen(row[key])) @@ -218,10 +245,7 @@ class AvazuDataset(object): record = [dense_input, sparse_input] - if self.mode == self.TRAIN_MODE: - record.append(list((int(row['click']), ))) - - # print record + record.append(list((int(row['click']), ))) yield record diff --git a/ctr/train.py b/ctr/train.py index 671e8e66..9733f3b6 100644 --- a/ctr/train.py +++ b/ctr/train.py @@ -10,19 +10,20 @@ from data_provider import categorial_features, id_features, field_index, detect_ id_features_space = 10000 dnn_layer_dims = [128, 64, 32, 1] train_data_path = './train.txt' -data_meta_info = detect_dataset(train_data_path, 10000) +data_meta_info = detect_dataset(train_data_path, 500000) +batch_size = 1000 * 11 +test_set_size = 10000 logging.warning('detect categorical fields in dataset %s' % train_data_path) for key, item in data_meta_info.items(): logging.warning(' - {}\t{}'.format(key, item)) -paddle.init(use_gpu=False, trainer_count=1) +paddle.init(use_gpu=False, trainer_count=11) # ============================================================================== # input layers # ============================================================================== - dnn_merged_input = layer.data( name='dnn_input', type=paddle.data_type.sparse_binary_vector(data_meta_info['dnn_input'])) @@ -40,13 +41,6 @@ click = paddle.layer.data(name='click', type=dtype.dense_vector(1)) def build_dnn_submodel(dnn_layer_dims): dnn_embedding = layer.fc(input=dnn_merged_input, size=dnn_layer_dims[0]) - # dnn_embedding = layer.embedding( - # input=dnn_merged_input, - # size=128) - # average_layer = layer.pooling(input=dnn_embedding, - # pooling_type=paddle.pooling.Avg()) - - # _input_layer = average_layer _input_layer = dnn_embedding for no, dim in enumerate(dnn_layer_dims[1:]): fc = layer.fc( @@ -94,17 +88,26 @@ optimizer = paddle.optimizer.Momentum(momentum=0) trainer = paddle.trainer.SGD( cost=classification_cost, parameters=params, update_equation=optimizer) -dataset = AvazuDataset(train_data_path) - -step = 0 +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)) -def event_hander(event): - global step + 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=5), + paddle.reader.shuffle(dataset.train, buf_size=500), + batch_size=batch_size), feeding=field_index, - num_passes=1) + event_handler=event_handler, + num_passes=100) -- GitLab