制作特征和标签, 转成监督学习问题
我们先捋一下基于原始的给定数据, 有哪些特征可以直接利用:
文章的自身特征, category_id表示这文章的类型, created_at_ts表示文章建立的时间, 这个关系着文章的时效性, words_count是文章的字数, 一般字数太长我们不太喜欢点击, 也不排除有人就喜欢读长文。
文章的内容embedding特征, 这个召回的时候用过, 这里可以选择使用, 也可以选择不用, 也可以尝试其他类型的embedding特征, 比如W2V等
用户的设备特征信息
上面这些直接可以用的特征, 待做完特征工程之后, 直接就可以根据article_id或者是user_id把这些特征加入进去。 但是我们需要先基于召回的结果, 构造一些特征,然后制作标签,形成一个监督学习的数据集。
构造监督数据集的思路, 根据召回结果, 我们会得到一个{user_id: [可能点击的文章列表]}形式的字典。 那么我们就可以对于每个用户, 每篇可能点击的文章构造一个监督测试集, 比如对于用户user1, 假设得到的他的召回列表{user1: [item1, item2, item3]}, 我们就可以得到三行数据(user1, item1), (user1, item2), (user1, item3)的形式, 这就是监督测试集时候的前两列特征。
构造特征的思路是这样, 我们知道每个用户的点击文章是与其历史点击的文章信息是有很大关联的, 比如同一个主题, 相似等等。 所以特征构造这块很重要的一系列特征是要结合用户的历史点击文章信息。我们已经得到了每个用户及点击候选文章的两列的一个数据集, 而我们的目的是要预测最后一次点击的文章, 比较自然的一个思路就是和其最后几次点击的文章产生关系, 这样既考虑了其历史点击文章信息, 又得离最后一次点击较近,因为新闻很大的一个特点就是注重时效性。 往往用户的最后一次点击会和其最后几次点击有很大的关联。 所以我们就可以对于每个候选文章, 做出与最后几次点击相关的特征如下:
候选item与最后几次点击的相似性特征(embedding内积) — 这个直接关联用户历史行为
候选item与最后几次点击的相似性特征的统计特征 — 统计特征可以减少一些波动和异常
候选item与最后几次点击文章的字数差的特征 — 可以通过字数看用户偏好
候选item与最后几次点击的文章建立的时间差特征 — 时间差特征可以看出该用户对于文章的实时性的偏好
还需要考虑一下 5. 如果使用了youtube召回的话, 我们还可以制作用户与候选item的相似特征
当然, 上面只是提供了一种基于用户历史行为做特征工程的思路, 大家也可以思维风暴一下,尝试一些其他的特征。 下面我们就实现上面的这些特征的制作, 下面的逻辑是这样:
我们首先获得用户的最后一次点击操作和用户的历史点击, 这个基于我们的日志数据集做
基于用户的历史行为制作特征, 这个会用到用户的历史点击表, 最后的召回列表, 文章的信息表和embedding向量
制作标签, 形成最后的监督学习数据集
划分训练集和验证集
划分训练和验证集的原因是为了在线下验证模型参数的好坏,为了完全模拟测试集,我们这里就在训练集中抽取部分用户的所有信息来作为验证集。提前做训练验证集划分的好处就是可以分解制作排序特征时的压力,一次性做整个数据集的排序特征可能时间会比较长。
# all_click_df指的是训练集
# sample_user_nums 采样作为验证集的用户数量
def trn_val_split(all_click_df, sample_user_nums):
all_click = all_click_df
all_user_ids = all_click.user_id.unique()
# replace=True表示可以重复抽样,反之不可以
sample_user_ids = np.random.choice(all_user_ids, size=sample_user_nums, replace=False)
click_val = all_click[all_click['user_id'].isin(sample_user_ids)]
click_trn = all_click[~all_click['user_id'].isin(sample_user_ids)]
# 将验证集中的最后一次点击给抽取出来作为答案
click_val = click_val.sort_values(['user_id', 'click_timestamp'])
val_ans = click_val.groupby('user_id').tail(1)
click_val = click_val.groupby('user_id').apply(lambda x: x[:-1]).reset_index(drop=True)
# 去除val_ans中某些用户只有一个点击数据的情况,如果该用户只有一个点击数据,又被分到ans中,
# 那么训练集中就没有这个用户的点击数据,出现用户冷启动问题,给自己模型验证带来麻烦
val_ans = val_ans[val_ans.user_id.isin(click_val.user_id.unique())] # 保证答案中出现的用户再验证集中还有
click_val = click_val[click_val.user_id.isin(val_ans.user_id.unique())]
return click_trn, click_val, val_ans
利用word2vec模型进行基于用户点击序列的文章embedding构造
本赛题提供了文章内容的embedding,这些embedding的构造方法可能是按照文章内容的相似等进行构造embedding的,而通过word2vec可以构造基于用户点击序列的文章embedding,这些得到的embedding更能反映序列信息。
对训练数据做负采样
通过召回我们将数据转换成三元组的形式(user1, item1, label)的形式,观察发现正负样本差距极度不平衡,我们可以先对负样本进行下采样,下采样的目的一方面缓解了正负样本比例的问题,另一方面也减小了我们做排序特征的压力,我们在做负采样的时候又有哪些东西是需要注意的呢?
只对负样本进行下采样(如果有比较好的正样本扩充的方法其实也是可以考虑的)
负采样之后,保证所有的用户和文章仍然出现在采样之后的数据中
下采样的比例可以根据实际情况人为的控制
做完负采样之后,更新此时新的用户召回文章列表,因为后续做特征的时候可能用到相对位置的信息。
因为召回的某一用户文章序列中对应此用户点击的最后一篇文章的情况,要么只有一篇,要么一篇都没有。这样对于所有用户的召回列表,为这些数据进行打标签,自然而然会产生少量的1(也就是召回了用户最后一次点击的文章),产生大量的0(负样本),这样产生了一个非常严重的问题,正负样本数量极其不均衡,这样通过模型训练的效果比较片面,效果往往不会很好,因此我们进行了减少负采样的操作。
# 负采样函数,这里可以控制负采样时的比例, 这里给了一个默认的值
def neg_sample_recall_data(recall_items_df, sample_rate=0.001):
pos_data = recall_items_df[recall_items_df['label'] == 1]
neg_data = recall_items_df[recall_items_df['label'] == 0]
print('pos_data_num:', len(pos_data), 'neg_data_num:', len(neg_data), 'pos/neg:', len(pos_data)/len(neg_data))
# 分组采样函数
def neg_sample_func(group_df):
neg_num = len(group_df)
sample_num = max(int(neg_num * sample_rate), 1) # 保证最少有一个
sample_num = min(sample_num, 5) # 保证最多不超过5个,这里可以根据实际情况进行选择
return group_df.sample(n=sample_num, replace=True)
# 对用户进行负采样,保证所有用户都在采样后的数据中
neg_data_user_sample = neg_data.groupby('user_id', group_keys=False).apply(neg_sample_func)
# 对文章进行负采样,保证所有文章都在采样后的数据中
neg_data_item_sample = neg_data.groupby('sim_item', group_keys=False).apply(neg_sample_func)
# 将上述两种情况下的采样数据合并
neg_data_new = neg_data_user_sample.append(neg_data_item_sample)
# 由于上述两个操作是分开的,可能将两个相同的数据给重复选择了,所以需要对合并后的数据进行去重
neg_data_new = neg_data_new.sort_values(['user_id', 'score']).drop_duplicates(['user_id', 'sim_item'], keep='last')
# 将正样本数据合并
data_new = pd.concat([pos_data, neg_data_new], ignore_index=True)
return data_new
根据用户进行分组负采样,对于某一用户最少存在一个负样本,最多存在5个负样本。
根据文章进行分组负采样,也会生成相应的负样本。
最后将两者的负采样进行结合然后去除重复的负采样,得到最后的正负样本。
给召回数据打标签
# 召回数据打标签
def get_rank_label_df(recall_list_df, label_df, is_test=False):
# 测试集是没有标签了,为了后面代码同一一些,这里直接给一个负数替代
if is_test:
recall_list_df['label'] = -1
return recall_list_df
label_df = label_df.rename(columns={'click_article_id': 'sim_item'})
recall_list_df_ = recall_list_df.merge(label_df[['user_id', 'sim_item', 'click_timestamp']], \
how='left', on=['user_id', 'sim_item'])
recall_list_df_['label'] = recall_list_df_['click_timestamp'].apply(lambda x: 0.0 if np.isnan(x) else 1.0)
del recall_list_df_['click_timestamp']
return recall_list_df_
上述代码的操作是将包含有【userid sim_item click_timestamp】属性的数据表与包含有【user_id sim_item】的召回样本表进行merge(整合)操作,整合之后,对于召回的文章中用户没有点击的自然不会有对应的click_timestamp,所以自然而然就为NAN(空),为空就是负样本,标签打为0,如果有对应的click_timestamp,说明这篇召回的文章用户点击过,自然而然就是正样本,标签记为1.
特征工程
1.用户历史行为特征
2.用户画像
3.文章画像
制作与用户历史行为相关特征
对于每个用户召回的每个商品, 做特征。 具体步骤如下:
对于每个用户, 获取最后点击的N个商品的item_id,
对于该用户的每个召回商品, 计算与上面最后N次点击商品的相似度的和(最大, 最小,均值), 时间差特征,相似性特征,字数差特征,与该用户的相似性特征
用户画像
用户相关特征
这一块,正式进行特征工程,既要拼接上已有的特征, 也会做更多的特征出来,我们来梳理一下已有的特征和可构造特征:
文章自身的特征, 文章字数,文章创建时间, 文章的embedding (articles表中)
用户点击环境特征, 那些设备的特征(这个在df中)
对于用户和商品还可以构造的特征:
基于用户的点击文章次数和点击时间构造可以表现用户活跃度的特征
基于文章被点击次数和时间构造可以反映文章热度的特征
用户的时间统计特征: 根据其点击的历史文章列表的点击时间和文章的创建时间做统计特征,比如求均值, 这个可以反映用户对于文章时效的偏好
用户的主题爱好特征, 对于用户点击的历史文章主题进行一个统计, 然后对于当前文章看看是否属于用户已经点击过的主题
用户的字数爱好特征, 对于用户点击的历史文章的字数统计, 求一个均值
分析一下点击时间和点击文章的次数,区分用户活跃度
如果某个用户点击文章之间的时间间隔比较小, 同时点击的文章次数很多的话, 那么我们认为这种用户一般就是活跃用户, 当然衡量用户活跃度的方式可能多种多样, 这里我们只提供其中一种,我们写一个函数, 得到可以衡量用户活跃度的特征,逻辑如下:
首先根据用户user_id分组, 对于每个用户,计算点击文章的次数, 两两点击文章时间间隔的均值
把点击次数取倒数和时间间隔的均值统一归一化,然后两者相加合并,该值越小, 说明用户越活跃
注意, 上面两两点击文章的时间间隔均值, 会出现如果用户只点击了一次的情况,这时候时间间隔均值那里会出现空值, 对于这种情况最后特征那里给个大数进行区分
这个的衡量标准就是先把点击的次数取到数然后归一化, 然后点击的时间差归一化, 然后两者相加进行合并, 该值越小, 说明被点击的次数越多, 且间隔时间短。
用户的系列习惯
这个基于原来的日志表做一个类似于article的那种DataFrame, 存放用户特有的信息, 主要包括点击习惯, 爱好特征之类的
用户的设备习惯, 这里取最常用的设备(众数)
用户的时间习惯: 根据其点击过得历史文章的时间来做一个统计(这个感觉最好是把时间戳里的时间特征的h特征提出来,看看用户习惯一天的啥时候点击文章), 但这里先用转换的时间吧, 求个均值
用户的爱好特征, 对于用户点击的历史文章主题进行用户的爱好判别, 更偏向于哪几个主题, 这个最好是multi-hot进行编码, 先试试行不
用户文章的字数差特征, 用户的爱好文章的字数习惯
这些就是对用户进行分组, 然后统计即可
文章画像
可以通过给定的article_csv中的信息直接读取其所包含的特征。