diff --git a/ctr/README.md b/ctr/README.md
index 549f0dddc381df0068335cf112e68fdb00436199..411227d060715a581630797aa0ea185bf71c6ce6 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 模型的结构:
-
+
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
+
-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 2652ad3d00ebe7e6592d990da175fda837ab522c..9773604b54ca4f62e915dd38a116afee7236104e 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 28d4eb9fe48a109233dd40aed7cf94325a6aef06..596112d7c7d53d3852656b0b6c40376ce4df7d2f 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 671e8e6669c0fe4137d96c6e1f9721143c0ac265..9733f3b69dd0fa936fb4b15270a719e1fd407e53 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)