Bootstrap

迁移学习在CTR问题中的运用

在年初的IJCAI 阿里妈妈广告搜索转化率预估赛上,我接触了CTR问题。CTR即(click-through-rate)点击通过率,指的是投放广告实际被点击次数与广告实际显示量的比例。是衡量广告投放出去效果的重要指标。

此比赛的冠军所用的迁移学习的思想,让我眼前一亮。我在DF,CCF平台上的招行用户购买预测中用了此方法,并将auc值提升了一个千分点。实际上当时我和冠军成绩也就差几个千分点,可见此法带来的提升很可观。本文将从原理和实战思路上介绍此方法,并且给出我实际做的demo和详解。

何谓迁移学习

迁移学习的目标是从一个环境中学习到的知识用在新环境里。其意义在于在某一领域缺乏已标注的数据的情况下,我们利用类似任务的数据来辅助学习。

举个例子:我们拥有一万张不同种类的猫的图片,和一百张波斯猫的图片。现在想要训练一个可以分辨波斯猫的分类器。显然,如果我们直接使用那一万张图片的话,会由于源数据与目标的差异,导致分类器效果并不好。而如果我们放弃使用一万张图片,转而使用那一百张波斯猫的图片学习,则会导致数据量过小,分类器欠拟合。

为了解决这个问题,我们使用一万张图片来训练一个分辨是否为猫的分类器,而在此基础上,使用那一百张波斯猫的图片,来帮助我们在猫中进一步分辨是否为波斯猫。这样我们就可以利用大的数据,来为缺少数据的场景服务。此时就用到了迁移学习的基本思想。这一过程我想下面这张图可以很好的描述:

上图表示了,我们可以将两者的共性从源域迁移到目标域中。其中红色框框中表示共性,是从另一个类似任务中学习到的知识。

为何迁移学习可用在预测类问题

我们都知道,预测类问题在实际场景中很常见。

当然我们说的这种预测,并不是字面上形如“预测某人的肿瘤大小或者是否得了癌症”这种问题,因为这我还是想把他划分为普通监督学习式的回归和分类的问题。

我主要想指的是时序问题的预测,对未来的预测,才可以体现这个“预”字。我们对未来知之甚少,这正符合了我们迁移学习的主要目的--目标域缺乏有效数据。

我们将过去的发生过得某些属性,让其通过某种方式映射到未来,这就相当于给未来输送了知识,这样判断未来某一个特性,效果更加优良。

那我们从感性上也从理论上证明了此种方法可行。接下来我们开始用实际的例子来证明一下。

介绍此次的例子

为了突出重点,我进行了一系列预处理,预处理之后的数据我给传在百度云。我的预处理中添加了不少新特征,为了大家更好的理解我在做什么,建议大家戳比赛链接看看到底要做啥。

本次数据的内容主要有: original.csv

字段含义字段含义
V1-v30用户的固有属性,含义未知USRID用户编号,唯一标识
Flag标签,用户是否购买,预测目标next_time_{}四个字段,分别是用户不同点击行为时间间隔的均值,标准差,最小值,最大值
user_tch_cnt三个字段,用户在三个层次页面的点击次数user_evt1_cnt21个字段,层次一21个页面分别点击次数
xx_week_cnt第几周的点击次数xx_week_user_click_rate第几周点击次数占这四周总共点击次数比例
xx_week_max_click第几周点击某页面最多的次数love_LBl用户最喜欢的页面
last_click_day最后一次点击的日期first_click_day第一次点击的日期

总结:此份数据是过去一个月的用户点击页面的情况,目标是预测这类用户未来一周是否购买,对应数据中的FLAG字段。

思考:我们很容易构建一个监督学习,但是效果并不好。我们仔细分析数据可知,现有数据都是前一个月的,而对于要被预测的那一周,我们知之甚少。毕竟点击购买这类行为具有时效性,当天的点击判断当天是否购买最为准确。我们是否可以将被预测的那一周用户点击行为轮廓给描画出来呢?

开始动手

理清思路:我们要想构建一个监督学习,将前一个月的特征映射到未来一周。但是我们需要数据来作为交叉验证,来获得一个更好的监督学习的模型。于是我们采用这一个月的前三周作为训练数据,最后一周作为验证数据。训练好的模型再去将这一个月的后三周作为test数据来生成未来一周的用户特征。这有点儿类似我们常用的滑窗操作。前三周预测第四周,那么预测后三周输出的结果便是未来一周。

接下来我们一步步看看demo:

获取前三周与后三周的数据

first_three_week_data = data.drop(['last_week_cnt', 'last_week_user_click_rate', 'last_week_max_click'], axis=1).values
last_three_week_data = data.drop(['first_week_cnt', 'first_week_user_click_rate', 'first_week_max_click'], axis=1).values
复制代码

获取前三周特征我们是作减法,将所有的数据中减去第四周的几个特征,得到训练数据。相比于将前三周的数据从总数据中抽取出来作为训练数据,前者的优势在于训练特征多一些。

开始训练模型

last_week_cnt = data['last_week_cnt'].values
new_week_cnt = xgboost_for_feature(first_three_week_data, last_week_cnt, last_three_week_data)
复制代码

我们将前三周的数据作为训练集,第四周的作为test数据集,训练出一个模型之后进行预测。 这里我们用的是xgboost作为训练模型。下面是我写的xgboost_for_feature函数:

def xgboost_for_feature(X, y, test):
	x_train, x_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=1729)
	xlf = xgb.XGBRegressor(max_depth=10,
	                       learning_rate=0.01,
	                       n_estimators=500,
	                       silent=True,
	                       objective='reg:linear',
	                       nthread=-1,
	                       gamma=0,
	                       min_child_weight=1,
	                       max_delta_step=0,
	                       subsample=0.85,
	                       colsample_bytree=0.7,
	                       colsample_bylevel=1,
	                       reg_alpha=0,
	                       reg_lambda=1,
	                       scale_pos_weight=1,
	                       seed=1440,
	                       missing=None)
	xlf.fit(x_train, y_train, eval_metric='rmse', verbose=True, eval_set=[(x_test, y_test)], early_stopping_rounds=50)
	predictions = xlf.predict(test, ntree_limit=xlf.best_ntree_limit)
	return predictions

复制代码

我在这个函数里面还是对习惯性的训练数据做了一个拆分交叉验证的处理。对于参数方面,我们选择的是线性回归,因为我想得到一个更加精确的模型,选择最简单的线性回归原因是想要减少计算损失。实际上,我们换作其他方式的回归,性能差距也不大。我们将后三周的数据作为test数据集,前三周预测第四,那么第四周输出的结果便是未来一周。

data['new_week_cnt'] = list(new_week_cnt)
复制代码

那么这样训练完之后,预测之后的结果特征加到原数据的新一列,列名为new_week_cnt。至此我们便完成了“迁移”。

看看效果如何

我们经过最终实验,发现添加未来一周点击数据的模型最终auc结果为0.9046523660309632 相比之下,未添加未来一周点击数据的最终auc为0.866629382497686

当然,这个成绩是线下的,线上的仅仅有在千分位上的提升,看来有些过拟合。不过无论如何,在最后角逐阶段获得千分位上的提升还是很可观的。

下面是我这部分的demo


import pandas as pd
from sklearn.model_selection import StratifiedKFold
import lightgbm as lgb
from sklearn.metrics import roc_auc_score, mean_squared_error
from sklearn.cross_validation import train_test_split
import xgboost as xgb


def xgboost_for_feature(X, y, test):
	x_train, x_test, y_train, y_test = train_test_split(X, y, test_size=0.4, random_state=1729)
	xlf = xgb.XGBRegressor(max_depth=10,
	                       learning_rate=0.01,
	                       n_estimators=500,
	                       silent=True,
	                       objective='reg:linear',
	                       nthread=-1,
	                       gamma=0,
	                       min_child_weight=1,
	                       max_delta_step=0,
	                       subsample=0.85,
	                       colsample_bytree=0.7,
	                       colsample_bylevel=1,
	                       reg_alpha=0,
	                       reg_lambda=1,
	                       scale_pos_weight=1,
	                       seed=1440,
	                       missing=None)
	xlf.fit(x_train, y_train, eval_metric='rmse', verbose=True, eval_set=[(x_test, y_test)], early_stopping_rounds=50)
	predictions = xlf.predict(test, ntree_limit=xlf.best_ntree_limit)
	
	return predictions


def lgb_model(X, y, test):
	print('///', X.dtype)
	skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

	auc_score = []
	predictions = []
	
	params = {
	        'boosting_type': 'gbdt',
	        'objective': 'binary',
	        'metric': {'auc'},
	        'num_leaves': 15,
	        'learning_rate': 0.02,
	        'feature_fraction': 0.8,
	        'bagging_fraction': 0.9,
	        'bagging_freq': 8,
	        'verbose': 0,
	}

	for train_in, test_in in skf.split(X, y):
		X_train, X_test, y_train, y_test = X[train_in], X[test_in], y[train_in], y[test_in]
		lgb_train = lgb.Dataset(X_train, y_train)
		lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)

		print('Start training...')
		gbm = lgb.train(params, lgb_train, num_boost_round=40000,
						valid_sets=lgb_eval, verbose_eval=250, early_stopping_rounds=50)

		print('Start predicting...')
		y_pred = gbm.predict(X_test, num_iteration=gbm.best_iteration)
		auc_score.append(roc_auc_score(y_test,y_pred))
		predictions.append(gbm.predict(test, num_iteration=gbm.best_iteration))
	return auc_score, predictions


def create_pre_week_features(data):
	first_three_week_data = data.drop(['last_week_cnt', 'last_week_user_click_rate', 'last_week_max_click'], axis=1)
	last_three_week_data = data.drop(['first_week_cnt', 'first_week_user_click_rate', 'first_week_max_click'], axis=1)
	first_three_week_data = first_three_week_data.values
	last_three_week_data = last_three_week_data.values
	
	last_week_cnt = data['last_week_cnt'].values
	new_week_cnt = xgboost_for_feature(first_three_week_data, last_week_cnt, last_three_week_data)
	data['new_week_cnt'] = list(new_week_cnt)
	
	last_week_user_click_rate = data['last_week_user_click_rate'].values
	new_week_user_click_rate = xgboost_for_feature(first_three_week_data, last_week_user_click_rate, last_three_week_data)
	data['new_week_user_click_rate'] = list(new_week_user_click_rate)
	
	last_week_max_click = data['last_week_max_click'].values
	new_week_max_click = xgboost_for_feature(first_three_week_data, last_week_max_click, last_three_week_data)
	data['new_week_max_click'] = list(new_week_max_click)
	
	return data


if __name__ == "__main__":
	data = pd.read_csv('./data_transfer/data.csv')
	
	data = create_pre_week_features(data)   
	# 开始预测未来一周的点击数据,如果不使用未来一周的数据,则注释此句
	
	train = data[data['FLAG'] != -1]
	test = data[data['FLAG'] == -1]
	train_userid = train.pop('USRID')  # 这里将USRID 保存,以便提交结果时候要按照USRID排序
	
	y = train.pop('FLAG')
	col = train.columns
	X = train[col].values
	test_userid = test.pop('USRID')
	test_y = test.pop('FLAG')
	test = test[col].values    # 这部分是我将预测目标与s训练数据分离的代码
	
	auc_score, predictions = lgb_model(X, y, test)  # 获取到5折交叉验证的5个结果
	print(max(auc_score))
	
复制代码

全部的预处理以及模型建立完整的代码,我放在github上了,另外,原数据我放在百度云

;