2020 KDD Cup:Debiasing phase4 baseline 0.25

2020-05-06 23:32:00 浏览数 (1)

写在前面

"该赛道的数据集强调电商推荐系统的公平性,尤其是流量较少的广大中小商家所面临的“有好货缺无人问津”的困境。数据横跨十余天,中间还穿插了某次全网促销活动,涵盖了一些商品从上新时无人问津、到逐渐成为高潜力爆款的历程。欲获胜的队伍需格外关注曝光不足的商品上的推荐准确度,需探索“如何抵消掉历史点击数据的选择性偏差以便避免只推爆款”、“如何注意数据分布随时间的变化以便及时发现高潜力冷门好货”、“如何利用多模态图文商品信息来辅助商品冷启动”等重要课题。"--解题关键

首先感谢青禹小生的开源,本文是在其基础上进行更多的细化, 也将对更多优化方向和建模思路进行简单介绍。

关联商品打分

代码语言:javascript复制
def get_sim_item(df_, user_col, item_col, use_iif=False):

    df = df_.copy()
    user_item_ = df.groupby(user_col)[item_col].agg(list).reset_index()
    user_item_dict = dict(zip(user_item_[user_col], user_item_[item_col]))

    user_time_ = df.groupby(user_col)['time'].agg(list).reset_index() # 引入时间因素
    user_time_dict = dict(zip(user_time_[user_col], user_time_['time']))

    sim_item = {}  
    item_cnt = defaultdict(int)  # 商品被点击次数
    for user, items in tqdm(user_item_dict.items()):  
        for loc1, item in enumerate(items):  
            item_cnt[item]  = 1  
            sim_item.setdefault(item, {})  
            for loc2, relate_item in enumerate(items):  
                if item == relate_item:  
                    continue  
                t1 = user_time_dict[user][loc1] # 点击时间提取
                t2 = user_time_dict[user][loc2]
                sim_item[item].setdefault(relate_item, 0)  
                if not use_iif:  
                    if loc1-loc2>0:
                        sim_item[item][relate_item]  = 1 * 0.7 * (0.8**(loc1-loc2-1)) * (1 - (t1 - t2) * 10000) / math.log(1   len(items)) # 逆向
                    else:
                        sim_item[item][relate_item]  = 1 * 1.0 * (0.8**(loc2-loc1-1)) * (1 - (t2 - t1) * 10000) / math.log(1   len(items)) # 正向
                else:  
                    sim_item[item][relate_item]  = 1 / math.log(1   len(items))

    sim_item_corr = sim_item.copy() # 引入AB的各种被点击次数  
    for i, related_items in tqdm(sim_item.items()):  
        for j, cij in related_items.items():  
            sim_item_corr[i][j] = cij / ((item_cnt[i] * item_cnt[j]) ** 0.2)

    return sim_item_corr, user_item_dict

这里在原有基础上考虑了两点因素,关联位置因素和关联时间因素。强关联的发生是有向的、有位置的和有时间的。

  • 比如我们先买了手机,那下一次买手机壳的关联,和先买手机壳再买手机的关联,这两种很明显,A到B大于B到A,这是有向性;
  • 我先买了手机,然后买了手机壳,又买了耳机,很明显,手机和手机壳的关联性大于手机与耳机的关联性,这是位置性;
  • 那么如果再加上时间这层因素,时间相隔越远的关联性肯定是不高的。

这三点因素就可以组成我们的优化思路,有向性打分*位置打分*时间打分,得到最终关联打分。

交互行为打分

代码语言:javascript复制
def recommend(sim_item_corr, user_item_dict, user_id, top_k, item_num):

    rank = {}  
    interacted_items = user_item_dict[user_id] 
    interacted_items = interacted_items[::-1]
    for loc, i in enumerate(interacted_items):  
        for j, wij in sorted(sim_item_corr[i].items(), reverse=True)[0:top_k]:  
            if j not in interacted_items:  
                rank.setdefault(j, 0)  
                rank[j]  = wij * (0.7**loc) 
     
     return sorted(rank.items(), key=lambda d: d[1], reverse=True)[:item_num]

这里的优化也很符合我们的主观认识,距离下次点击月近的行为,相关性越接近,所有可以根据位置远近考虑重要性,添加权重因子。当然还可以添加时间权重因子。

优化方向

切勿陷入思维定势,也许我的优化方向和baseline会使大家产生一个误区。就像之前安泰杯的比赛,决赛中评委也说到了baseline,很大程度影响的大家的思维方向。没有说baseline不好,而这只能作为无数解题思路中的一小部分,一个分支。不是沿着这个分支走下去,而是去创造更多分支。这就如同多路召回,之将其当作融合的一部分罢了。

目前只是从关联的角度解决问题,这个角度可以做的更细。也可以考虑其它方向,目前大家做的还都是起点,而不是终点。向量召回、模型召回,以及后面的排序都需要进一步尝试。

建模思路

没有相关经验的同学可能会问正负样本怎么来,那其实很简单,需要我们去构造label。这里分为两步:

  • 样本提取。我们线下验证的时候,一般是用户最后一次点击进行验证,会进行召回50个商品,然后观察召回率。这样的50个商品及对应的用户就是样本数据。
  • 样本打标。召回的50个商品中是最后一次点击的商品labael是1,反之为0。

这样我们就能得到训练集,测试集构造方式一样,只不过需要去预测其label,最后将label的概率进行排序,top50就是最终建模得到的结果。

为了防止数据泄露,构造特征时一定要用历史的点击行为进行构造。最后我们就可以像一般的二分类问题进行解题了。

完整代码

代码语言:javascript复制
import pandas as pd
from tqdm import tqdm
from collections import defaultdict
import math

# fill user to 50 items  
def get_predict(df, pred_col, top_fill):  
    top_fill = [int(t) for t in top_fill.split(',')]  
    scores = [-1 * i for i in range(1, len(top_fill)   1)]  
    ids = list(df['user_id'].unique())  
    fill_df = pd.DataFrame(ids * len(top_fill), columns=['user_id'])  
    fill_df.sort_values('user_id', inplace=True)  
    fill_df['item_id'] = top_fill * len(ids)  
    fill_df[pred_col] = scores * len(ids)  
    df = df.append(fill_df)  
    df.sort_values(pred_col, ascending=False, inplace=True)  
    df = df.drop_duplicates(subset=['user_id', 'item_id'], keep='first')  
    df['rank'] = df.groupby('user_id')[pred_col].rank(method='first', ascending=False)  
    df = df[df['rank'] <= 50]  
    df = df.groupby('user_id')['item_id'].apply(lambda x: ','.join([str(i) for i in x])).str.split(',', expand=True).reset_index()  
    return df  

now_phase = 4
train_path = './data/underexpose_train'  
test_path = './data/underexpose_test'  
recom_item = []  

whole_click = pd.DataFrame()  
for c in range(now_phase   1):  
    print('phase:', c)  
    click_train = pd.read_csv(train_path   '/underexpose_train_click-{}.csv'.format(c), header=None,  names=['user_id', 'item_id', 'time'])  
    click_test = pd.read_csv(test_path   '/underexpose_test_click-{}.csv'.format(c,c), header=None,  names=['user_id', 'item_id', 'time'])  

    all_click = click_train.append(click_test)  
    whole_click = whole_click.append(all_click)  
    whole_click = whole_click.drop_duplicates(subset=['user_id','item_id','time'],keep='last')
    whole_click = whole_click.sort_values('time')

    item_sim_list, user_item = get_sim_item(whole_click, 'user_id', 'item_id', use_iif=False)  

    for i in tqdm(click_test['user_id'].unique()):  
        rank_item = recommend(item_sim_list, user_item, i, 500, 500)  
        for j in rank_item:  
            recom_item.append([i, j[0], j[1]])  
            
# find most popular items  
top50_click = whole_click['item_id'].value_counts().index[:50].values  
top50_click = ','.join([str(i) for i in top50_click])  

recom_df = pd.DataFrame(recom_item, columns=['user_id', 'item_id', 'sim'])  
result = get_predict(recom_df, 'sim', top50_click)  
result.to_csv('baseline.csv', index=False, header=None)

0 人点赞