2018科大讯飞AI营销算法大赛总结(冠军)

作者介绍:王贺,武汉大学,计算机专业,研二


写在前面

首先很幸运能够拿到这次冠军,有两位大佬队友是这次获胜的关键,再次感谢鹏哥和阿水。

同时希望我的分享与总结能给大家带来些许帮助,并且一起交流学习。

接下来将会呈现ppt内容和部分代码

  1. 赛题分析

  2. 探索性分析

  3. 数据预处理

  4. 特征工程

  5. 算法模型

  6. 思考总结


1. 赛题分析

本次大赛提供了讯飞AI营销云的海量广告投放数据,参赛选手通过人工智能技术构建预测模型预估用户的广告点击概率,即给定广告点击相关的广告、媒体、用户、上下文内容等信息的条件下预测广告点击概率。

这又是一道关于CTR的问题,对于CTR问题而言,广告是否被点击的主导因素是用户,其次是广告信息。所以我们要做的是充分挖掘用户及用户行为信息,然后才是广告主、广告等信息。

赛题特征:广告信息、媒体信息、用户信息、上下文信息 

提供数据:共1001650初赛数据 和 1998350条复赛数据(复赛训练数据为初赛数据和复赛数据)

评估指标:通过logloss评估模型效果(越小越好),公式如下:

640?wx_fmt=jpeg

其中N表示测试集样本数量,yi表示测试集中第i个样本的真实标签,pi表示第i个样本的预估转化率。这类评估函数常用logloss和AUC,简单的说logloss更关注和观察数据的吻合程度,AUC更关注rank order。如果是按照概率期望来进行收费投放的话就用logloss,如果定投一定量就用AUC,主要还是和业务相关。


2. 探索性分析

这一部分将会对部分数据进行分析,另外获取每个类别特征的转化率分布情况判断特征效果,看分布可以有一个很好的初步验证作用。

不同时刻的曝光量和点击率变化,将一天分成4个时段的曝光量和点击率情况

640?wx_fmt=jpeg

640?wx_fmt=jpeg

训练集正负样本比例,大约为1:4,应该经过了降采样。

640?wx_fmt=jpeg

广告的长宽是很重要的特征,正负样本中关于这两个特征的分布存在较为明显的区别。

640?wx_fmt=jpeg
640?wx_fmt=jpeg

在训练集中的部分热门广告,adid为1537089的广告在训练集中曝光次数超过了12万次。

640?wx_fmt=jpeg

在训练集中的部分热门广告,adid为1537080的广告在训练集中点击率最高,接近0.5。

640?wx_fmt=jpeg OPPO和 vivo的用户最多,而这两种机型的用户点击率也高于其他手机的用户
640?wx_fmt=jpeg
640?wx_fmt=jpeg


3. 数据预处理

由于数据噪音比较多,所以细致的预处理能够是模型更具泛化性,同时挖掘更多特征。

1.初复赛训练数据合并后去重(7361条)

2.提取广告投放时间信息,日期、小时以及早中晚时间段

0-6>--1 | 7-12>--2 | 13-18>--3 | 19-24>--4

3. 细分广告主行业与媒体广告位,去除只有一个取值的字段

102400_102401>--102400 102401

4. 清洗手机品牌和机型字段,对同类进行合并

iphone>--apple | redmi>--xiaomi | honor>--huawei

5. 对操作系统及其版本、名称进行更细粒度的刻画

5.1.1>--5 1 1 | 6.0.1>--6 0 1

6. 构造虚拟用户组别,对用户标签和其他类别特征进行编码

7. 对city特征进行切分,如

640?wx_fmt=jpeg

框内为身份证前六位,51代表广东省,04代表广州市,10代表白云区

8. 缺失值填充,对不同类型的数据填充不同类型的值


4. 特征工程

4.1 特征构造

1)基础特征:原始特征(广告信息 媒体信息 用户信息 上下文信息)

2)One-hot:将类别特征离散化

由于最后融合三套代码的结果,所以有的代码进行了one-hot,有的没有这样做,而是直接labelencoder。

3)user_tags多值特征:因为包含用户的属性信息,所以完美的表达user_tags至关重要,提取有效属性,减少冗余

640?wx_fmt=jpeg


从下图可以看出user_tags中标签属性的重要性分布情况

640?wx_fmt=jpeg

4)统计特征

统计特征我们用的都是常规操作,如count、ratio、nunique和ctr相关特征。

count:一维+二维count计数特征(如广告主id共计投放次数)

# 对交叉特征的求count

# add cross feature

first_feature = ['app_cate_id', 'f_channel', 'app_id']

second_feature = ["make", "model", "osv1", "osv2", "osv3", "adid", "advert_name", "campaign_id", "creative_id","carrier", "nnt", "devtype", "os"]

cross_feature = []

for feat_1 in first_feature:

    for feat_2 in second_feature:

        col_name = "cross_" + feat_1 + "_and_" + feat_2

        cross_feature.append(col_name)

        data[col_name] = data[feat_1].astype(str).values + '_' + data[feat_2].astype(str).values

# 求count计数特征


ratio:类别偏好的ratio比例特征(如广告主id的某个广告id投放比例)

# 这里会考虑所有的组合,当然也可以考虑进行一波特征选择

label_feature = ['advert_id','advert_industry_inner','advert_name','campaign_id', 

'creative_height', 'creative_tp_dnf', 'creative_width', 'province', 'f_channel', 'car

rier', 'creative_type', 'devtype', 'nnt', 'adid', 'app_id', 'app_cate_id', 'city', 'os', 'orderid', 'inner_slot_id', 'make', 'osv','os_name', 'creative_has_deeplink', 'creative_is_download', 'hour', 'creative_id', 'model']


mean:用户标签与其他字段的组合mean特征(如广告id对用户性别的投放比例)

640?wx_fmt=jpeg


nunique: 类别变量的nunique特征(如广告主id有多少个不同的广告id)

# 此处参考@有风的冬

## 广告

adid_nuq = ['model', 'make', 'os', 'city', 'province', 'user_tags', 'f_channel', 

'app_id', 'carrier', 'nnt', 'devtype', 'app_cate_id', 'inner_slot_id']

for feat in adid_nuq:

    gp1 = data.groupby('adid')[feat].nunique().reset_index().rename(columns=

{feat:"adid_%s_nuq_num" % feat})

    gp2 = data.groupby(feat)['adid'].nunique().reset_index().rename(columns=

{'adid': "%s_adid_nuq_num" % feat})

    data = pd.merge(data, gp1, how='left', on=['adid'])

    data = pd.merge(data, gp2, how='left', on=[feat])

## 广告主

advert_id_nuq = ['model', 'make', 'os', 'city', 'province', 'user_tags', 'f_channel', 

'app_id', 'carrier', 'nnt', 'devtype','app_cate_id', 'inner_slot_id']

for fea in advert_id_nuq:

    gp1 = data.groupby('advert_id')[fea].nunique().reset_index().rename(columns=

{fea: "advert_id_%s_nuq_num" % fea})

    gp2 = data.groupby(fea)['advert_id'].nunique().reset_index().rename(

        columns={'advert_id': "%s_advert_id_nuq_num" % fea})

    data = pd.merge(data, gp1, how='left', on=['advert_id'])

    data = pd.merge(data, gp2, how='left', on=[fea])

## app_id

app_id_nuq = ['model', 'make', 'os', 'city', 'province', 'user_tags', 'f_channel', 

'carrier', 'nnt', 'devtype','app_cate_id', 'inner_slot_id']

for fea in app_id_nuq:

    gp1 = data.groupby('app_id')[fea].nunique().reset_index().rename(columns=

{fea: "app_id_%s_nuq_num" % fea})

    gp2 = data.groupby(fea)['app_id'].nunique().reset_index().rename(columns=

{'app_id': "%s_app_id_nuq_num" % fea})

    data = pd.merge(data, gp1, how='left', on=['app_id'])

    data = pd.merge(data, gp2, how='left', on=[fea])


点击率:这里使用的历史点击率,来挖掘历史点击信息,同时防止过拟合

# 和当初baseline所用一样

# add ctr feature

data['period'] = data['day']

data['period'][data['period'] < 27] = data['period'][data['period'] < 27] + 31

for feat_1 in ['advert_id', 'advert_industry_inner', 'advert_name', 'campaign_id', 

'creative_height','creative_tp_dnf', 'creative_width', 'province', 'f_channel']:

    res = pd.DataFrame()

    temp = data[[feat_1, 'period', 'click']]

    for period in range(27, 35):

        if period == 27:

            count = temp.groupby([feat_1]).apply(

                lambda x: x['click'][(x['period'] <= period).values].count()).reset_index(name=feat_1 + '_all')

            count1 = temp.groupby([feat_1]).apply(

                lambda x: x['click'][(x['period'] <= period).values].sum()).reset_index(name=feat_1 + '_1')

        else:

            count = temp.groupby([feat_1]).apply(

                lambda x: x['click'][(x['period'] < period).values].count()).reset_index(name=feat_1 + '_all')

            count1 = temp.groupby([feat_1]).apply(

                lambda x: x['click'][(x['period'] < period).values].sum()).reset_index(name=feat_1 + '_1')

        count[feat_1 + '_1'] = count1[feat_1 + '_1']

        count.fillna(value=0, inplace=True)

        count[feat_1 + '_rate'] = round(count[feat_1 + '_1'] / count[feat_1 + '_all'], 5)

        count['period'] = period

        count.drop([feat_1 + '_all', feat_1 + '_1'], axis=1, inplace=True)

        count.fillna(value=0, inplace=True)

        res = res.append(count, ignore_index=True)

    print(feat_1, ' over')

    data = pd.merge(data, res, how='left', on=[feat_1, 'period'])

可以看出这些都是常规操作,如果能够顺利的完成这些就能得到不错的分数


4.2 特征选择

这里我们主要用了卡方检验和特征重要性,由于三套代码,所有使用的方法并不相同。

user_tags特征我们分别用了卡方检验和特征重要性。

train_new = pd.DataFrame()

test_new = pd.DataFrame()

cntv = CountVectorizer()

cntv.fit(data['user_tags'])

train_a = cntv.transform(train['user_tags'])

test_a = cntv.transform(test['user_tags'])

train_new = sparse.hstack((train_new, train_a), 'csr', 'bool')

test_new = sparse.hstack((test_new, test_a), 'csr', 'bool')

# 卡方检验

SKB = SelectPercentile(chi2, percentile=95).fit(train_new, train_y)

train_new = SKB.transform(train_new)

test_new = SKB.transform(test_new)


5. 算法模型

GBDT模型记忆性更强,记忆特征和标签相关特征组合能力强,因此在小数据集上有很好的结果 。

FFM和DeepFFM初期尝试并未得到很好的效果

最终我们选择了XGBoost和LightGBM,得到的结果并做了最终的加权融合。

640?wx_fmt=jpeg


# 模型参数及五折构造结果

lgb_clf = lgb.LGBMClassifier(boosting_type='gbdt', num_leaves=48, max_depth=-1, learning_rate=0.02, n_estimators=6000, max_bin=425, subsam

ple_for_bin=50000, objective='binary', min_split_gain=0,min_child_weight=5, min_child_samples=10, subsample=0.8, subsample_freq=1,colsample_bytree=0.8, 

reg_alpha=3, reg_lambda=0.1, seed=1000, n_jobs=-1, silent=True)

skf = list(StratifiedKFold(y_loc_train, n_folds=5, shuffle=True, random_state=1024))

baseloss = []

loss = 0

for i, (train_index, test_index) in enumerate(skf):

    print("Fold", i)

    lgb_model = lgb_clf.fit(X_loc_train[train_index], y_loc_train[train_index],

                            eval_names=['train', 'valid'],

                            eval_metric='logloss',

                            eval_set=[(X_loc_train[train_index], y_loc_train[train_index]),

                                      (X_loc_train[test_index], y_loc_train[test_index])], early_stopping_rounds=100)

    baseloss.append(lgb_model.best_score_['valid']['binary_logloss'])

    loss += lgb_model.best_score_['valid']['binary_logloss']

    test_pred = lgb_model.predict_proba(X_loc_test, num_iteration=lgb_mod

el.best_iteration_)[:, 1]

    print('test mean:', test_pred.mean())

    res['prob_%s' % str(i)] = test_pred

print('logloss:', baseloss, loss / 5)

640?wx_fmt=jpeg


6. 思考总结

  • 由于本次比赛数据中缺乏用户id这一关键信息,用户画像难以得到清晰地建立,因此如何充分挖掘用户标签中所包含的信息至关重要。

  • 即使是同样的业务场景,在不同的数据收集背景下,同样的特征完全可能会起到完全相反的效果,这也是一种数据陷阱。

  • 匿名化数据需要对数据进行充分理解分析,甚至可以尝试根据业务理解进行反编码,这样能够为特征工程指明方向。

  • 建模过程中充分考虑了用户标签与其他信息的交互作用,并采用Stacking抽取特征信息的方式减少维度与内存的使用,对广告与用户交互信息的充分挖掘,也使得模型在AB榜测试相对稳定。

  • 模型缺乏差异性和创新性,最开始尝试过deepffm,由于效果一般而没有坚持改进,大部分精力放在了数据理解与特征挖掘上。


写在最后

以上就是此次比赛主要内容,总的来看操作还是很常规的,如有疑问欢迎提出。

路漫漫其修远兮,吾将上下而求索。


温馨提示:点击阅读原文,即可关注作者知乎噢!


640?wx_fmt=jpeg

猜你喜欢

转载自blog.csdn.net/kMD8d5R/article/details/83552360