1. 程式人生 > >2018科大訊飛AI營銷演算法大賽總結(冠軍)

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