From 9312f5b485a1f3fce2d66a0d44ab02d4224971f5 Mon Sep 17 00:00:00 2001 From: Superjom Date: Wed, 24 May 2017 19:02:50 +0800 Subject: [PATCH] init doc and model --- ctr/README.md | 161 +++++++++++++++++++++++++++++ ctr/README.org | 168 +++++++++++++++++++++++++++++++ ctr/data_provider.py | 235 +++++++++++++++++++++++++++++++++++++++++++ ctr/train.py | 110 ++++++++++++++++++++ 4 files changed, 674 insertions(+) create mode 100644 ctr/README.md create mode 100644 ctr/README.org create mode 100644 ctr/data_provider.py create mode 100644 ctr/train.py diff --git a/ctr/README.md b/ctr/README.md new file mode 100644 index 00000000..549f0ddd --- /dev/null +++ b/ctr/README.md @@ -0,0 +1,161 @@ +
+

Table of Contents

+ +
+ + + +# 背景介绍 + +CTR(Click-through rate) 是用来表示用户点击一个特定链接的概率, +通常被用来衡量一个在线广告系统的有效性。 + +当有多个广告位时,CTR 预估一般会作为排序的基准。 +比如在百度的搜索广告系统,当用户输入一个带商业价值的搜索词(query)时,系统大体上会执行下列步骤: + +1. 召回满足 query 的广告集合 +2. 业务规则和相关性过滤 +3. 根据拍卖机制和 CTR 排序 +4. 展出 + +可以看到,CTR 在最终排序中起到了很重要的作用。 + +在业内,CTR 模型经历了如下的发展阶段: + +- Logistic Regression(LR) + 特征工程 +- LR + DNN 特征 +- DNN + 特征工程 + +在发展早期是 LR 一统天下,但最近 DNN 模型由于其强大的学习能力和逐渐成熟的性能优化, +逐渐地接过 CTR 预估任务的大旗。 + + + + +## LR vs DNN + +下图展示了 LR 和一个 \(3x2\) 的 NN 模型的结构: + +![img](背景介绍/LR vs DNN_2017-05-22_10-09-02.jpg) + +LR 部分和蓝色箭头部分可以直接类比到 NN 中的结构,可以看到 LR 和 NN 有一些共通之处(比如权重累加), +但前者的模型复杂度在相同输入维度下比后者可能第很多(从某方面讲,模型越复杂,越有潜力学习到更复杂的信息)。 + +如果 LR 要达到匹敌 NN 的学习能力,必须增加输入的维度,也就是增加特征的数量(作为输入), +这也就是为何 LR 和大规模的特征工程必须绑定在一起的原因。 + +而 NN 模型具有自己学习新特征的能力,一定程度上能够提升特征使用的效率, +这使得 NN 模型在同样规模特征的情况下,更有可能达到更好的学习效果。 + +本文会演示,如何使用 NN 模型来完成 CTR 预估的任务。 + + + + +# 数据和任务抽象 + +我们可以将 \`click\` 作为学习目标,具体任务可以有以下几种方案: + +1. 直接学习 click,0,1 作二元分类,或 pairwise rank(标签 1>0) +2. 统计每个广告的点击率,将同一个 query 下的广告两两组合,点击率高的>点击率低的 + +这里,我们直接使用第一种方法做分类任务。 + +我们使用 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 +- bannerpos +- siteid +- sitedomain +- sitecategory +- appid +- appdomain +- appcategory +- deviceid +- deviceip +- devicemodel +- devicetype +- deviceconntype +- C14-C21 – anonymized categorical variables + + + + +# 特征提取 + +下面我们会简单演示几种特征的提取方式。 + +原始数据中的特征可以分为以下几类: + +1. ID 类特征(稀疏,数量多) + - id + - siteid + - appid + - deviceid + +2. 类别类特征(稀疏,但数量有限) + - C1 + - sitecategory + - devicetype + - C14-C21 + +3. 数值型特征 + - hour (可以转化成数值,也可以按小时为单位转化为类别) + + + + +## ID 类特征 + +ID 类特征的特点是稀疏数据,但量比较大,直接使用 One-hot 表示时维度过大。 + +一般会作如下处理: + + + + +# 模型实现 + + + + +## DNN 简单模型 + + + + +## long wide 复杂模型 + + + + +# 写在最后 + + + diff --git a/ctr/README.org b/ctr/README.org new file mode 100644 index 00000000..2652ad3d --- /dev/null +++ b/ctr/README.org @@ -0,0 +1,168 @@ +* 背景介绍 +CTR(Click-through rate) 是用来表示用户点击一个特定链接的概率, +通常被用来衡量一个在线广告系统的有效性。 + +当有多个广告位时,CTR 预估一般会作为排序的基准。 +比如在百度的搜索广告系统,当用户输入一个带商业价值的搜索词(query)时,系统大体上会执行下列步骤: + +1. 召回满足 query 的广告集合 +2. 业务规则和相关性过滤 +3. 根据拍卖机制和 CTR 排序 +4. 展出 + +可以看到,CTR 在最终排序中起到了很重要的作用。 + +在业内,CTR 模型经历了如下的发展阶段: + +- Logistic Regression(LR) + 特征工程 +- LR + DNN 特征 +- DNN + 特征工程 + +在发展早期时 LR 一统天下,但最近 DNN 模型由于其强大的学习能力和逐渐成熟的性能优化, +逐渐地接过 CTR 预估任务的大旗。 + +** 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]] + +LR 部分和蓝色箭头部分可以直接类比到 NN 中的结构,可以看到 LR 和 NN 有一些共通之处(比如权重累加), +但前者的模型复杂度在相同输入维度下比后者可能第很多(从某方面讲,模型越复杂,越有潜力学习到更复杂的信息)。 + +如果 LR 要达到匹敌 NN 的学习能力,必须增加输入的维度,也就是增加特征的数量(作为输入), +这也就是为何 LR 和大规模的特征工程必须绑定在一起的原因。 + +而 NN 模型具有自己学习新特征的能力,一定程度上能够提升特征使用的效率, +这使得 NN 模型在同样规模特征的情况下,更有可能达到更好的学习效果。 + +LR 对于 NN 模型的优势是对大规模稀疏特征的容纳能力,包括内存和计算量等,工业界都有非常成熟的优化方法。 + +本文后面的章节会演示如何使用 Paddle 编写一个结合两者优点的模型。 + +* 数据和任务抽象 +我们可以将 `click` 作为学习目标,具体任务可以有以下几种方案: + +1. 直接学习 click,0,1 作二元分类,或 pairwise rank(标签 1>0) +2. 统计每个广告的点击率,将同一个 query 下的广告两两组合,点击率高的>点击率低的 + +这里,我们直接使用第一种方法做分类任务。 + +我们使用 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 + + +* Wide & Deep Learning Model +谷歌在 16 年提出了 Wide & Deep Learning 的模型框架,用于融合 适合学习抽象特征的 DNN 和 适用于大规模系数特征的 LR 两种模型的优点。 + +** 模型简介 + Wide & Deep Learning Model 可以作为一种相对成熟的模型框架使用, + 在 CTR 预估的任务中工业界也有一定的应用,因此本文将演示使用此模型来完成 CTR 预估的任务。 + + 模型结构如下: + + [ pic ] + +** 编写 LR 部分 +** 编写 DNN 部分 +** 两者融合 + +* 写在最后 + +- [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 + diff --git a/ctr/data_provider.py b/ctr/data_provider.py new file mode 100644 index 00000000..28d4eb9f --- /dev/null +++ b/ctr/data_provider.py @@ -0,0 +1,235 @@ +import sys +import csv +import numpy as np +''' +The fields of the dataset are: + + 0. id: ad identifier + 1. click: 0/1 for non-click/click + 2. hour: format is YYMMDDHH, so 14091123 means 23:00 on Sept. 11, 2014 UTC. + 3. C1 -- anonymized categorical variable + 4. banner_pos + 5. site_id + 6. site_domain + 7. site_category + 8. app_id + 9. app_domain + 10. app_category + 11. device_id + 12. device_ip + 13. device_model + 14. device_type + 15. device_conn_type + 16. C14-C21 -- anonymized categorical variables + +We will treat following fields as categorical features: + + - C1 + - banner_pos + - site_category + - app_category + - device_type + - device_conn_type + +and some other features as id features: + + - id + - site_id + - app_id + - device_id + +The `hour` field will be treated as a continuous feature and will be transformed +to one-hot representation which has 24 bits. +''' + +feature_dims = {} + +categorial_features = ('C1 banner_pos site_category app_category ' + + 'device_type device_conn_type').split() + +id_features = 'id site_id app_id device_id'.split() + + +def get_all_field_names(mode=0): + ''' + @mode: int + 0 for train, 1 for test + @return: list of str + ''' + return categorial_features + ['hour'] + id_features + ['click'] \ + if mode == 0 else [] + + +class CategoryFeatureGenerator(object): + ''' + Generator category features. + ''' + + def __init__(self): + self.dic = {'unk': 0} + self.counter = 1 + + def register(self, key): + 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): + if key not in self.dic: + res = self.dic['unk'] + else: + res = self.dic[key] + return [res] + + def __repr__(self): + return '' % len(self.dic) + + +class IDfeatureGenerator(object): + def __init__(self, max_dim): + self.max_dim = max_dim + + def gen(self, key): + return [hash(key) % self.max_dim] + + def size(self): + return self.max_dim + + +class ContinuousFeatureGenerator(object): + def __init__(self, n_intervals): + self.min = sys.maxint + self.max = sys.minint + self.n_intervals = n_intervals + + def register(self, val): + self.min = min(self.minint, val) + self.max = max(self.maxint, val) + + def gen(self, val): + self.len_part = (self.max - self.min) / self.n_intervals + return (val - self.min) / self.len_part + + +fields = {} +for key in categorial_features: + fields[key] = CategoryFeatureGenerator() +for key in id_features: + fields[key] = IDfeatureGenerator(10000) + +field_index = dict( + (key, id) for id, key in enumerate(['dnn_input', 'lr_input', 'click'])) + + +def detect_dataset(path, topn, id_fea_space=10000): + ''' + Parse the first `topn` records to collect information of this dataset. + + NOTE the records should be randomly shuffled first. + ''' + # create categorical statis objects. + + with open(path, 'rb') as csvfile: + reader = csv.DictReader(csvfile) + for row_id, row in enumerate(reader): + if row_id > topn: + break + + for key in categorial_features: + fields[key].register(row[key]) + + for key, item in fields.items(): + feature_dims[key] = item.size() + + for key in id_features: + feature_dims[key] = id_fea_space + + feature_dims['hour'] = 24 + feature_dims['click'] = 1 + + 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 + + return feature_dims + + +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 + + +class AvazuDataset(object): + TRAIN_MODE = 0 + TEST_MODE = 1 + + def __init__(self, train_path, test_path=None): + self.train_path = train_path + self.test_path = test_path + # task model: 0 train, 1 test + self.mode = 0 + + def train(self): + self.mode = self.TRAIN_MODE + return self._parse(self.train_path) + + def test(self): + self.mode = self.TEST_MODE + return self._parse(self.test_path) + + def _parse(self, path): + with open(path, 'rb') as csvfile: + reader = csv.DictReader(csvfile) + + categorial_dims = [ + feature_dims[key] for key in categorial_features + ['hour'] + ] + id_dims = [feature_dims[key] for key in id_features] + + for row_id, row in enumerate(reader): + record = [] + for key in categorial_features: + record.append(fields[key].gen(row[key])) + record.append([int(row['hour'][-2:])]) + dense_input = concat_sparse_vectors(record, categorial_dims) + + record = [] + for key in id_features: + record.append(fields[key].gen(row[key])) + + sparse_input = concat_sparse_vectors(record, id_dims) + + record = [dense_input, sparse_input] + + if self.mode == self.TRAIN_MODE: + record.append(list((int(row['click']), ))) + + # print record + yield record + + +if __name__ == '__main__': + path = 'train.txt' + print detect_dataset(path, 400000) + + filereader = AvazuDataset(path) + for no, rcd in enumerate(filereader.train()): + print no, rcd + if no > 1000: break diff --git a/ctr/train.py b/ctr/train.py new file mode 100644 index 00000000..671e8e66 --- /dev/null +++ b/ctr/train.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import logging +import paddle.v2 as paddle +from paddle.v2 import layer +from paddle.v2 import data_type as dtype +from data_provider import categorial_features, id_features, field_index, detect_dataset, AvazuDataset, get_all_field_names + +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) + +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) + +# ============================================================================== +# input layers +# ============================================================================== + + +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)) + +# ============================================================================== +# network structure +# ============================================================================== + + +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( + input=_input_layer, + size=dim, + act=paddle.activation.Relu(), + name='dnn-fc-%d' % no) + _input_layer = fc + return _input_layer + + +# config LR submodel +def build_lr_submodel(): + fc = layer.fc( + input=lr_merged_input, size=1, name='lr', act=paddle.activation.Relu()) + return fc + + +# 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) + +step = 0 + + +def event_hander(event): + global step + + +trainer.train( + reader=paddle.batch( + paddle.reader.shuffle(dataset.train, buf_size=500), batch_size=5), + feeding=field_index, + num_passes=1) -- GitLab