修改第三、四章索引
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,664 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 赛题理解\n",
|
||||
"赛题理解是切入一道赛题的基础,会影响后续特征工程和模型构建等各种工作,也影响着后续发展工作的方向,正确了解赛题背后的思想以及赛题业务逻辑的清晰,有利于花费更少时间构建更为有效的特征模型, 在各种比赛中, 赛题理解都是极其重要且必须走好的第一步, 今天我们就从赛题的理解出发, 首先了解一下这次赛题的概况和数据,从中分析赛题以及大致的处理方式, 其次我们了解模型评测的指标,最后对赛题的理解整理一些经验。\n",
|
||||
"\n",
|
||||
"## 赛题简介\n",
|
||||
"此次比赛是新闻推荐场景下的用户行为预测挑战赛, 该赛题是以新闻APP中的新闻推荐为背景, 目的是**要求我们根据用户历史浏览点击新闻文章的数据信息预测用户未来的点击行为, 即用户的最后一次点击的新闻文章**, 这道赛题的设计初衷是引导大家了解推荐系统中的一些业务背景, 解决实际问题。 \n",
|
||||
"\n",
|
||||
"## 数据概况\n",
|
||||
"该数据来自某新闻APP平台的用户交互数据,包括30万用户,近300万次点击,共36万多篇不同的新闻文章,同时每篇新闻文章有对应的embedding向量表示。为了保证比赛的公平性,从中抽取20万用户的点击日志数据作为训练集,5万用户的点击日志数据作为测试集A,5万用户的点击日志数据作为测试集B。具体数据表和参数, 大家可以参考赛题说明。下面说一下拿到这样的数据如何进行理解, 来有效的开展下一步的工作。<br><br>\n",
|
||||
"## 评价方式理解\n",
|
||||
"理解评价方式, 我们需要结合着最后的提交文件来看, 根据sample.submit.csv, 我们最后提交的格式是针对每个用户, 我们都会给出五篇文章的推荐结果,按照点击概率从前往后排序。 而真实的每个用户最后一次点击的文章只会有一篇的真实答案, 所以我们就看我们推荐的这五篇里面是否有命中真实答案的。比如对于user1来说, 我们的提交会是:\n",
|
||||
">user1, article1, article2, article3, article4, article5.\n",
|
||||
"\n",
|
||||
"评价指标的公式如下:\n",
|
||||
"$$\n",
|
||||
"score(user) = \\sum_{k=1}^5 \\frac{s(user, k)}{k}\n",
|
||||
"$$\n",
|
||||
"\n",
|
||||
"假如article1就是真实的用户点击文章,也就是article1命中, 则s(user1,1)=1, s(user1,2-4)都是0, 如果article2是用户点击的文章, 则s(user,2)=1/2,s(user,1,3,4,5)都是0。也就是score(user)=命中第几条的倒数。如果都没中, 则score(user1)=0。 这个是合理的, 因为我们希望的就是命中的结果尽量靠前, 而此时分数正好比较高。\n",
|
||||
"\n",
|
||||
"## 赛题理解\n",
|
||||
"根据赛题简介,我们首先要明确我们此次比赛的目标: 根据用户历史浏览点击新闻的数据信息预测用户最后一次点击的新闻文章。从这个目标上看, 会发现此次比赛和我们之前遇到的普通的结构化比赛不太一样, 主要有两点:\n",
|
||||
" \n",
|
||||
"- 首先是目标上, 要预测最后一次点击的新闻文章,也就是我们给用户推荐的是新闻文章, 并不是像之前那种预测一个数或者预测数据哪一类那样的问题\n",
|
||||
"- 数据上, 通过给出的数据我们会发现, 这种数据也不是我们之前遇到的那种特征+标签的数据,而是基于了真实的业务场景, 拿到的用户的点击日志\n",
|
||||
"\n",
|
||||
"所以拿到这个题目,我们的思考方向就是结合我们的目标,**把该预测问题转成一个监督学习的问题(特征+标签),然后我们才能进行ML,DL等建模预测**。那么我们自然而然的就应该在心里会有这么几个问题:如何转成一个监督学习问题呢? 转成一个什么样的监督学习问题呢? 我们能利用的特征又有哪些呢? 又有哪些模型可以尝试呢? 此次面对数万级别的文章推荐,我们又有哪些策略呢? \n",
|
||||
"\n",
|
||||
"当然这些问题不会在我们刚看到赛题之后就一下出来答案, 但是只要有了问题之后, 我们就能想办法解决问题了, 比如上面的第二个问题,转成一个什么样的监督学习问题? 由于我们是预测用户最后一次点击的新闻文章,从36万篇文章中预测某一篇的话我们首先可能会想到这可能是一个多分类的问题(36万类里面选1), 但是如此庞大的分类问题, 我们做起来可能比较困难, 那么能不能转化一下? 既然是要预测最后一次点击的文章, 那么如果我们能预测出某个用户最后一次对于某一篇文章会进行点击的概率, 是不是就间接性的解决了这个问题呢?概率最大的那篇文章不就是用户最后一次可能点击的新闻文章吗? 这样就把原问题变成了一个点击率预测的问题(用户, 文章) --> 点击的概率(软分类), 而这个问题, 就是我们所熟悉的监督学习领域分类问题了, 这样我们后面建模的时候, 对于模型的选择就基本上有大致方向了,比如最简单的逻辑回归模型。<br><br>\n",
|
||||
"这样, 我们对于该赛题的解决方案应该有了一个大致的解决思路,要先转成一个分类问题来做, 而分类的标签就是用户是否会点击某篇文章,分类问题的特征中会有用户和文章,我们要训练一个分类模型, 对某用户最后一次点击某篇文章的概率进行预测。 那么又会有几个问题:如何转成监督学习问题? 训练集和测试集怎么制作? 我们又能利用哪些特征? 我们又可以尝试哪些模型? 面对36万篇文章, 20多万用户的推荐, 我们又有哪些策略来缩减问题的规模?如何进行最后的预测? "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Baseline"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 导包"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2020-11-16T07:46:49.678700Z",
|
||||
"start_time": "2020-11-16T07:46:49.673336Z"
|
||||
}
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# import packages\n",
|
||||
"import time, math, os\n",
|
||||
"from tqdm import tqdm\n",
|
||||
"import gc\n",
|
||||
"import pickle\n",
|
||||
"import random\n",
|
||||
"from datetime import datetime\n",
|
||||
"from operator import itemgetter\n",
|
||||
"import numpy as np\n",
|
||||
"import pandas as pd\n",
|
||||
"import warnings\n",
|
||||
"from collections import defaultdict\n",
|
||||
"import collections\n",
|
||||
"warnings.filterwarnings('ignore')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 25,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2020-11-16T07:48:34.240098Z",
|
||||
"start_time": "2020-11-16T07:48:34.236370Z"
|
||||
}
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# data_path = './data_raw/'\n",
|
||||
"data_path = '/home/admin/jupyter/data/' # 天池平台路径\n",
|
||||
"save_path = '/home/admin/jupyter/temp_results/' # 天池平台路径"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## df节省内存函数"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 节约内存的一个标配函数\n",
|
||||
"def reduce_mem(df):\n",
|
||||
" starttime = time.time()\n",
|
||||
" numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']\n",
|
||||
" start_mem = df.memory_usage().sum() / 1024**2\n",
|
||||
" for col in df.columns:\n",
|
||||
" col_type = df[col].dtypes\n",
|
||||
" if col_type in numerics:\n",
|
||||
" c_min = df[col].min()\n",
|
||||
" c_max = df[col].max()\n",
|
||||
" if pd.isnull(c_min) or pd.isnull(c_max):\n",
|
||||
" continue\n",
|
||||
" if str(col_type)[:3] == 'int':\n",
|
||||
" if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:\n",
|
||||
" df[col] = df[col].astype(np.int8)\n",
|
||||
" elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:\n",
|
||||
" df[col] = df[col].astype(np.int16)\n",
|
||||
" elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:\n",
|
||||
" df[col] = df[col].astype(np.int32)\n",
|
||||
" elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:\n",
|
||||
" df[col] = df[col].astype(np.int64)\n",
|
||||
" else:\n",
|
||||
" if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:\n",
|
||||
" df[col] = df[col].astype(np.float16)\n",
|
||||
" elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:\n",
|
||||
" df[col] = df[col].astype(np.float32)\n",
|
||||
" else:\n",
|
||||
" df[col] = df[col].astype(np.float64)\n",
|
||||
" end_mem = df.memory_usage().sum() / 1024**2\n",
|
||||
" print('-- Mem. usage decreased to {:5.2f} Mb ({:.1f}% reduction),time spend:{:2.2f} min'.format(end_mem,\n",
|
||||
" 100*(start_mem-end_mem)/start_mem,\n",
|
||||
" (time.time()-starttime)/60))\n",
|
||||
" return df"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 读取采样或全量数据"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2020-11-16T07:48:50.619963Z",
|
||||
"start_time": "2020-11-16T07:48:50.611667Z"
|
||||
}
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# debug模式:从训练集中划出一部分数据来调试代码\n",
|
||||
"def get_all_click_sample(data_path, sample_nums=10000):\n",
|
||||
" \"\"\"\n",
|
||||
" 训练集中采样一部分数据调试\n",
|
||||
" data_path: 原数据的存储路径\n",
|
||||
" sample_nums: 采样数目(这里由于机器的内存限制,可以采样用户做)\n",
|
||||
" \"\"\"\n",
|
||||
" all_click = pd.read_csv(data_path + 'train_click_log.csv')\n",
|
||||
" all_user_ids = all_click.user_id.unique()\n",
|
||||
"\n",
|
||||
" sample_user_ids = np.random.choice(all_user_ids, size=sample_nums, replace=False) \n",
|
||||
" all_click = all_click[all_click['user_id'].isin(sample_user_ids)]\n",
|
||||
" \n",
|
||||
" all_click = all_click.drop_duplicates((['user_id', 'click_article_id', 'click_timestamp']))\n",
|
||||
" return all_click\n",
|
||||
"\n",
|
||||
"# 读取点击数据,这里分成线上和线下,如果是为了获取线上提交结果应该讲测试集中的点击数据合并到总的数据中\n",
|
||||
"# 如果是为了线下验证模型的有效性或者特征的有效性,可以只使用训练集\n",
|
||||
"def get_all_click_df(data_path='./data_raw/', offline=True):\n",
|
||||
" if offline:\n",
|
||||
" all_click = pd.read_csv(data_path + 'train_click_log.csv')\n",
|
||||
" else:\n",
|
||||
" trn_click = pd.read_csv(data_path + 'train_click_log.csv')\n",
|
||||
" tst_click = pd.read_csv(data_path + 'testA_click_log.csv')\n",
|
||||
"\n",
|
||||
" all_click = trn_click.append(tst_click)\n",
|
||||
" \n",
|
||||
" all_click = all_click.drop_duplicates((['user_id', 'click_article_id', 'click_timestamp']))\n",
|
||||
" return all_click"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 全量训练集\n",
|
||||
"all_click_df = get_all_click_df(data_path, offline=False)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 获取 用户 - 文章 - 点击时间字典"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2020-11-16T07:56:39.800240Z",
|
||||
"start_time": "2020-11-16T07:56:39.793541Z"
|
||||
}
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 根据点击时间获取用户的点击文章序列 {user1: [(item1, time1), (item2, time2)..]...}\n",
|
||||
"def get_user_item_time(click_df):\n",
|
||||
" \n",
|
||||
" click_df = click_df.sort_values('click_timestamp')\n",
|
||||
" \n",
|
||||
" def make_item_time_pair(df):\n",
|
||||
" return list(zip(df['click_article_id'], df['click_timestamp']))\n",
|
||||
" \n",
|
||||
" user_item_time_df = click_df.groupby('user_id')['click_article_id', 'click_timestamp'].apply(lambda x: make_item_time_pair(x))\\\n",
|
||||
" .reset_index().rename(columns={0: 'item_time_list'})\n",
|
||||
" user_item_time_dict = dict(zip(user_item_time_df['user_id'], user_item_time_df['item_time_list']))\n",
|
||||
" \n",
|
||||
" return user_item_time_dict"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 获取点击最多的topk个文章"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 获取近期点击最多的文章\n",
|
||||
"def get_item_topk_click(click_df, k):\n",
|
||||
" topk_click = click_df['click_article_id'].value_counts().index[:k]\n",
|
||||
" return topk_click"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## itemcf的物品相似度计算"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2020-11-16T07:51:07.577037Z",
|
||||
"start_time": "2020-11-16T07:51:07.568098Z"
|
||||
}
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def itemcf_sim(df):\n",
|
||||
" \"\"\"\n",
|
||||
" 文章与文章之间的相似性矩阵计算\n",
|
||||
" :param df: 数据表\n",
|
||||
" :item_created_time_dict: 文章创建时间的字典\n",
|
||||
" return : 文章与文章的相似性矩阵\n",
|
||||
" 思路: 基于物品的协同过滤(详细请参考上一期推荐系统基础的组队学习), 在多路召回部分会加上关联规则的召回策略\n",
|
||||
" \"\"\"\n",
|
||||
" \n",
|
||||
" user_item_time_dict = get_user_item_time(df)\n",
|
||||
" \n",
|
||||
" # 计算物品相似度\n",
|
||||
" i2i_sim = {}\n",
|
||||
" item_cnt = defaultdict(int)\n",
|
||||
" for user, item_time_list in tqdm(user_item_time_dict.items()):\n",
|
||||
" # 在基于商品的协同过滤优化的时候可以考虑时间因素\n",
|
||||
" for i, i_click_time in item_time_list:\n",
|
||||
" item_cnt[i] += 1\n",
|
||||
" i2i_sim.setdefault(i, {})\n",
|
||||
" for j, j_click_time in item_time_list:\n",
|
||||
" if(i == j):\n",
|
||||
" continue\n",
|
||||
" i2i_sim[i].setdefault(j, 0)\n",
|
||||
" \n",
|
||||
" i2i_sim[i][j] += 1 / math.log(len(item_time_list) + 1)\n",
|
||||
" \n",
|
||||
" i2i_sim_ = i2i_sim.copy()\n",
|
||||
" for i, related_items in i2i_sim.items():\n",
|
||||
" for j, wij in related_items.items():\n",
|
||||
" i2i_sim_[i][j] = wij / math.sqrt(item_cnt[i] * item_cnt[j])\n",
|
||||
" \n",
|
||||
" # 将得到的相似性矩阵保存到本地\n",
|
||||
" pickle.dump(i2i_sim_, open(save_path + 'itemcf_i2i_sim.pkl', 'wb'))\n",
|
||||
" \n",
|
||||
" return i2i_sim_"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 17,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2020-11-16T07:53:10.038470Z",
|
||||
"start_time": "2020-11-16T07:51:11.281176Z"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"100%|██████████| 250000/250000 [00:23<00:00, 10802.38it/s]\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"i2i_sim = itemcf_sim(all_click_df)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## itemcf 的文章推荐"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 20,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2020-11-16T08:03:18.383215Z",
|
||||
"start_time": "2020-11-16T08:03:18.373432Z"
|
||||
}
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 基于商品的召回i2i\n",
|
||||
"def item_based_recommend(user_id, user_item_time_dict, i2i_sim, sim_item_topk, recall_item_num, item_topk_click):\n",
|
||||
" \"\"\"\n",
|
||||
" 基于文章协同过滤的召回\n",
|
||||
" :param user_id: 用户id\n",
|
||||
" :param user_item_time_dict: 字典, 根据点击时间获取用户的点击文章序列 {user1: [(item1, time1), (item2, time2)..]...}\n",
|
||||
" :param i2i_sim: 字典,文章相似性矩阵\n",
|
||||
" :param sim_item_topk: 整数, 选择与当前文章最相似的前k篇文章\n",
|
||||
" :param recall_item_num: 整数, 最后的召回文章数量\n",
|
||||
" :param item_topk_click: 列表,点击次数最多的文章列表,用户召回补全 \n",
|
||||
" return: 召回的文章列表 {item1:score1, item2: score2...}\n",
|
||||
" 注意: 基于物品的协同过滤(详细请参考上一期推荐系统基础的组队学习), 在多路召回部分会加上关联规则的召回策略\n",
|
||||
" \"\"\"\n",
|
||||
" \n",
|
||||
" # 获取用户历史交互的文章\n",
|
||||
" user_hist_items = user_item_time_dict[user_id]\n",
|
||||
" user_hist_items_ = {user_id for user_id, _ in user_hist_items}\n",
|
||||
" \n",
|
||||
" item_rank = {}\n",
|
||||
" for loc, (i, click_time) in enumerate(user_hist_items):\n",
|
||||
" for j, wij in sorted(i2i_sim[i].items(), key=lambda x: x[1], reverse=True)[:sim_item_topk]:\n",
|
||||
" if j in user_hist_items_:\n",
|
||||
" continue\n",
|
||||
" \n",
|
||||
" item_rank.setdefault(j, 0)\n",
|
||||
" item_rank[j] += wij\n",
|
||||
" \n",
|
||||
" # 不足10个,用热门商品补全\n",
|
||||
" if len(item_rank) < recall_item_num:\n",
|
||||
" for i, item in enumerate(item_topk_click):\n",
|
||||
" if item in item_rank.items(): # 填充的item应该不在原来的列表中\n",
|
||||
" continue\n",
|
||||
" item_rank[item] = - i - 100 # 随便给个负数就行\n",
|
||||
" if len(item_rank) == recall_item_num:\n",
|
||||
" break\n",
|
||||
" \n",
|
||||
" item_rank = sorted(item_rank.items(), key=lambda x: x[1], reverse=True)[:recall_item_num]\n",
|
||||
" \n",
|
||||
" return item_rank"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 给每个用户根据物品的协同过滤推荐文章"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 21,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2020-11-16T10:15:01.109798Z",
|
||||
"start_time": "2020-11-16T08:11:07.233787Z"
|
||||
},
|
||||
"scrolled": true
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"100%|██████████| 250000/250000 [43:19<00:00, 96.18it/s] \n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# 定义\n",
|
||||
"user_recall_items_dict = collections.defaultdict(dict)\n",
|
||||
"\n",
|
||||
"# 获取 用户 - 文章 - 点击时间的字典\n",
|
||||
"user_item_time_dict = get_user_item_time(all_click_df)\n",
|
||||
"\n",
|
||||
"# 去取文章相似度\n",
|
||||
"i2i_sim = pickle.load(open(save_path + 'itemcf_i2i_sim.pkl', 'rb'))\n",
|
||||
"\n",
|
||||
"# 相似文章的数量\n",
|
||||
"sim_item_topk = 10\n",
|
||||
"\n",
|
||||
"# 召回文章数量\n",
|
||||
"recall_item_num = 10\n",
|
||||
"\n",
|
||||
"# 用户热度补全\n",
|
||||
"item_topk_click = get_item_topk_click(all_click_df, k=50)\n",
|
||||
"\n",
|
||||
"for user in tqdm(all_click_df['user_id'].unique()):\n",
|
||||
" user_recall_items_dict[user] = item_based_recommend(user, user_item_time_dict, i2i_sim, \n",
|
||||
" sim_item_topk, recall_item_num, item_topk_click)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 召回字典转换成df"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 22,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2020-11-16T10:16:36.647466Z",
|
||||
"start_time": "2020-11-16T10:16:24.791219Z"
|
||||
},
|
||||
"scrolled": true
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"100%|██████████| 250000/250000 [00:04<00:00, 53319.08it/s]\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# 将字典的形式转换成df\n",
|
||||
"user_item_score_list = []\n",
|
||||
"\n",
|
||||
"for user, items in tqdm(user_recall_items_dict.items()):\n",
|
||||
" for item, score in items:\n",
|
||||
" user_item_score_list.append([user, item, score])\n",
|
||||
"\n",
|
||||
"recall_df = pd.DataFrame(user_item_score_list, columns=['user_id', 'click_article_id', 'pred_score'])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 生成提交文件"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 23,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2020-11-16T10:16:46.268341Z",
|
||||
"start_time": "2020-11-16T10:16:46.259293Z"
|
||||
}
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 生成提交文件\n",
|
||||
"def submit(recall_df, topk=5, model_name=None):\n",
|
||||
" recall_df = recall_df.sort_values(by=['user_id', 'pred_score'])\n",
|
||||
" recall_df['rank'] = recall_df.groupby(['user_id'])['pred_score'].rank(ascending=False, method='first')\n",
|
||||
" \n",
|
||||
" # 判断是不是每个用户都有5篇文章及以上\n",
|
||||
" tmp = recall_df.groupby('user_id').apply(lambda x: x['rank'].max())\n",
|
||||
" assert tmp.min() >= topk\n",
|
||||
" \n",
|
||||
" del recall_df['pred_score']\n",
|
||||
" submit = recall_df[recall_df['rank'] <= topk].set_index(['user_id', 'rank']).unstack(-1).reset_index()\n",
|
||||
" \n",
|
||||
" submit.columns = [int(col) if isinstance(col, int) else col for col in submit.columns.droplevel(0)]\n",
|
||||
" # 按照提交格式定义列名\n",
|
||||
" submit = submit.rename(columns={'': 'user_id', 1: 'article_1', 2: 'article_2', \n",
|
||||
" 3: 'article_3', 4: 'article_4', 5: 'article_5'})\n",
|
||||
" \n",
|
||||
" save_name = save_path + model_name + '_' + datetime.today().strftime('%m-%d') + '.csv'\n",
|
||||
" submit.to_csv(save_name, index=False, header=True)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 26,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2020-11-16T10:17:42.254328Z",
|
||||
"start_time": "2020-11-16T10:17:32.211862Z"
|
||||
}
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 获取测试集\n",
|
||||
"tst_click = pd.read_csv(data_path + 'testA_click_log.csv')\n",
|
||||
"tst_users = tst_click['user_id'].unique()\n",
|
||||
"\n",
|
||||
"# 从所有的召回数据中将测试集中的用户选出来\n",
|
||||
"tst_recall = recall_df[recall_df['user_id'].isin(tst_users)]\n",
|
||||
"\n",
|
||||
"# 生成提交文件\n",
|
||||
"submit(tst_recall, topk=5, model_name='itemcf_baseline')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 总结\n",
|
||||
"本节内容主要包括赛题简介,数据概况,评价方式以及对该赛题进行了一个总体上的思路分析,作为竞赛前的预热,旨在帮助学习者们能够更好切入该赛题,为后面的学习内容打下一个良好的基础。最后我们给出了关于本赛题的一个简易Baseline, 帮助学习者们先了解一下新闻推荐比赛的一个整理流程, 接下来我们就对于流程中的每个步骤进行详细的介绍。\n",
|
||||
"\n",
|
||||
"今天的学习比较简单,下面整理一下关于赛题理解的一些经验:\n",
|
||||
"\n",
|
||||
"* 赛题理解究竟是在理解什么? \n",
|
||||
"\n",
|
||||
">**理解赛题**:从直观上对问题进行梳理, 分析问题的目标,到底要让做什么事情, **这个非常重要**\n",
|
||||
">\n",
|
||||
">**理解数据**:对赛题数据有一个初步了解,知道和任务相关的数据字段和数据字段的类型, 数据之间的内在关联等,大体梳理一下哪些数据会对我们解决问题非常有用,方便后面我们的数据分析和特征工程。\n",
|
||||
">\n",
|
||||
">**理解评估指标**:评估指标是检验我们提出的方法,我们给出结果好坏的标准,只有正确的理解了评估指标,我们才能进行更好的训练模型,更好的进行预测。此外,很多情况下,线上验证是有一定的时间和次数限制的,**所以在比赛中构建一个合理的本地的验证集和验证的评价指标是很关键的步骤,能有效的节省很多时间**。 不同的指标对于同样的预测结果是具有误差敏感的差异性的所以不同的评价指标会影响后续一些预测的侧重点。\n",
|
||||
"\n",
|
||||
"* 有了赛题理解之后,我们该做什么?\n",
|
||||
"\n",
|
||||
" >在对于赛题有了一定的了解后,分析清楚了问题的类型性质和对于数据理解 的这一基础上,我们可以梳理一个解决赛题的一个大题思路和框架\n",
|
||||
" >\n",
|
||||
" >我们至少要有一些相应的理解分析,比如**这题的难点可能在哪里,关键点可能在哪里,哪些地方可以挖掘更好的特征**.\n",
|
||||
" >\n",
|
||||
" >用什么样得线下验证方式更为稳定,**出现了过拟合或者其他问题,估摸可以用什么方法去解决这些问题**\n",
|
||||
"\n",
|
||||
" 这时是在一个宏观的大体下分析的,有助于摸清整个题的思路脉络,以及后续的分析方向\n",
|
||||
"\n",
|
||||
"**关于Datawhale:** Datawhale是一个专注于数据科学与AI领域的开源组织,汇集了众多领域院校和知名企业的优秀学习者,聚合了一群有开源精神和探索精神的团队成员。Datawhale 以“for the learner,和学习者一起成长”为愿景,鼓励真实地展现自我、开放包容、互信互助、敢于试错和勇于担当。同时 Datawhale 用开源的理念去探索开源内容、开源学习和开源方案,赋能人才培养,助力人才成长,建立起人与人,人与知识,人与企业和人与未来的联结。 本次数据挖掘路径学习,专题知识将在天池分享,详情可关注Datawhale:\n",
|
||||
"\n",
|
||||
""
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.6.3"
|
||||
},
|
||||
"latex_envs": {
|
||||
"LaTeX_envs_menu_present": true,
|
||||
"autoclose": false,
|
||||
"autocomplete": true,
|
||||
"bibliofile": "biblio.bib",
|
||||
"cite_by": "apalike",
|
||||
"current_citInitial": 1,
|
||||
"eqLabelWithNumbers": true,
|
||||
"eqNumInitial": 1,
|
||||
"hotkeys": {
|
||||
"equation": "Ctrl-E",
|
||||
"itemize": "Ctrl-I"
|
||||
},
|
||||
"labels_anchors": false,
|
||||
"latex_user_defs": false,
|
||||
"report_style_numbering": false,
|
||||
"user_envs_cfg": false
|
||||
},
|
||||
"tianchi_metadata": {
|
||||
"competitions": [],
|
||||
"datasets": [],
|
||||
"description": "",
|
||||
"notebookId": "130006",
|
||||
"source": "dsw"
|
||||
},
|
||||
"toc": {
|
||||
"base_numbering": 1,
|
||||
"nav_menu": {},
|
||||
"number_sections": true,
|
||||
"sideBar": true,
|
||||
"skip_h1_title": false,
|
||||
"title_cell": "Table of Contents",
|
||||
"title_sidebar": "Contents",
|
||||
"toc_cell": false,
|
||||
"toc_position": {
|
||||
"height": "calc(100% - 180px)",
|
||||
"left": "10px",
|
||||
"top": "150px",
|
||||
"width": "170px"
|
||||
},
|
||||
"toc_section_display": true,
|
||||
"toc_window_display": true
|
||||
},
|
||||
"varInspector": {
|
||||
"cols": {
|
||||
"lenName": 16,
|
||||
"lenType": 16,
|
||||
"lenVar": 40
|
||||
},
|
||||
"kernels_config": {
|
||||
"python": {
|
||||
"delete_cmd_postfix": "",
|
||||
"delete_cmd_prefix": "del ",
|
||||
"library": "var_list.py",
|
||||
"varRefreshCmd": "print(var_dic_list())"
|
||||
},
|
||||
"r": {
|
||||
"delete_cmd_postfix": ") ",
|
||||
"delete_cmd_prefix": "rm(",
|
||||
"library": "var_list.r",
|
||||
"varRefreshCmd": "cat(var_dic_list()) "
|
||||
}
|
||||
},
|
||||
"types_to_exclude": [
|
||||
"module",
|
||||
"function",
|
||||
"builtin_function_or_method",
|
||||
"instance",
|
||||
"_Feature"
|
||||
],
|
||||
"window_display": false
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
# 赛题理解
|
||||
赛题理解是切入一道赛题的基础,会影响后续特征工程和模型构建等各种工作,也影响着后续发展工作的方向,正确了解赛题背后的思想以及赛题业务逻辑的清晰,有利于花费更少时间构建更为有效的特征模型, 在各种比赛中, 赛题理解都是极其重要且必须走好的第一步, 今天我们就从赛题的理解出发, 首先了解一下这次赛题的概况和数据,从中分析赛题以及大致的处理方式, 其次我们了解模型评测的指标,最后对赛题的理解整理一些经验。
|
||||
|
||||
## 赛题简介
|
||||
此次比赛是新闻推荐场景下的用户行为预测挑战赛, 该赛题是以新闻APP中的新闻推荐为背景, 目的是**要求我们根据用户历史浏览点击新闻文章的数据信息预测用户未来的点击行为, 即用户的最后一次点击的新闻文章**, 这道赛题的设计初衷是引导大家了解推荐系统中的一些业务背景, 解决实际问题。
|
||||
|
||||
## 数据概况
|
||||
该数据来自某新闻APP平台的用户交互数据,包括30万用户,近300万次点击,共36万多篇不同的新闻文章,同时每篇新闻文章有对应的embedding向量表示。为了保证比赛的公平性,从中抽取20万用户的点击日志数据作为训练集,5万用户的点击日志数据作为测试集A,5万用户的点击日志数据作为测试集B。具体数据表和参数, 大家可以参考赛题说明。下面说一下拿到这样的数据如何进行理解, 来有效的开展下一步的工作。
|
||||
## 评价方式理解
|
||||
理解评价方式, 我们需要结合着最后的提交文件来看, 根据sample.submit.csv, 我们最后提交的格式是针对每个用户, 我们都会给出五篇文章的推荐结果,按照点击概率从前往后排序。 而真实的每个用户最后一次点击的文章只会有一篇的真实答案, 所以我们就看我们推荐的这五篇里面是否有命中真实答案的。比如对于user1来说, 我们的提交会是:
|
||||
>user1, article1, article2, article3, article4, article5.
|
||||
|
||||
评价指标的公式如下:
|
||||
$$
|
||||
score(user) = \sum_{k=1}^5 \frac{s(user, k)}{k}
|
||||
$$
|
||||
|
||||
假如article1就是真实的用户点击文章,也就是article1命中, 则s(user1,1)=1, s(user1,2-4)都是0, 如果article2是用户点击的文章, 则s(user,2)=1/2,s(user,1,3,4,5)都是0。也就是score(user)=命中第几条的倒数。如果都没中, 则score(user1)=0。 这个是合理的, 因为我们希望的就是命中的结果尽量靠前, 而此时分数正好比较高。
|
||||
|
||||
## 赛题理解
|
||||
根据赛题简介,我们首先要明确我们此次比赛的目标: 根据用户历史浏览点击新闻的数据信息预测用户最后一次点击的新闻文章。从这个目标上看, 会发现此次比赛和我们之前遇到的普通的结构化比赛不太一样, 主要有两点:
|
||||
- 首先是目标上, 要预测最后一次点击的新闻文章,也就是我们给用户推荐的是新闻文章, 并不是像之前那种预测一个数或者预测数据哪一类那样的问题
|
||||
- 数据上, 通过给出的数据我们会发现, 这种数据也不是我们之前遇到的那种特征+标签的数据,而是基于了真实的业务场景, 拿到的用户的点击日志
|
||||
|
||||
所以拿到这个题目,我们的思考方向就是结合我们的目标,**把该预测问题转成一个监督学习的问题(特征+标签),然后我们才能进行ML,DL等建模预测**。那么我们自然而然的就应该在心里会有这么几个问题:如何转成一个监督学习问题呢? 转成一个什么样的监督学习问题呢? 我们能利用的特征又有哪些呢? 又有哪些模型可以尝试呢? 此次面对数万级别的文章推荐,我们又有哪些策略呢?
|
||||
|
||||
当然这些问题不会在我们刚看到赛题之后就一下出来答案, 但是只要有了问题之后, 我们就能想办法解决问题了, 比如上面的第二个问题,转成一个什么样的监督学习问题? 由于我们是预测用户最后一次点击的新闻文章,从36万篇文章中预测某一篇的话我们首先可能会想到这可能是一个多分类的问题(36万类里面选1), 但是如此庞大的分类问题, 我们做起来可能比较困难, 那么能不能转化一下? 既然是要预测最后一次点击的文章, 那么如果我们能预测出某个用户最后一次对于某一篇文章会进行点击的概率, 是不是就间接性的解决了这个问题呢?概率最大的那篇文章不就是用户最后一次可能点击的新闻文章吗? 这样就把原问题变成了一个点击率预测的问题(用户, 文章) --> 点击的概率(软分类), 而这个问题, 就是我们所熟悉的监督学习领域分类问题了, 这样我们后面建模的时候, 对于模型的选择就基本上有大致方向了,比如最简单的逻辑回归模型。
|
||||
|
||||
这样, 我们对于该赛题的解决方案应该有了一个大致的解决思路,要先转成一个分类问题来做, 而分类的标签就是用户是否会点击某篇文章,分类问题的特征中会有用户和文章,我们要训练一个分类模型, 对某用户最后一次点击某篇文章的概率进行预测。 那么又会有几个问题:如何转成监督学习问题? 训练集和测试集怎么制作? 我们又能利用哪些特征? 我们又可以尝试哪些模型? 面对36万篇文章, 20多万用户的推荐, 我们又有哪些策略来缩减问题的规模?如何进行最后的预测?
|
||||
|
||||
# Baseline
|
||||
|
||||
## 导包
|
||||
|
||||
|
||||
```python
|
||||
# import packages
|
||||
import time, math, os
|
||||
from tqdm import tqdm
|
||||
import gc
|
||||
import pickle
|
||||
import random
|
||||
from datetime import datetime
|
||||
from operator import itemgetter
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import warnings
|
||||
from collections import defaultdict
|
||||
warnings.filterwarnings('ignore')
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
data_path = './data_raw/'
|
||||
save_path = './tmp_results/'
|
||||
```
|
||||
|
||||
|
||||
|
||||
## df节省内存函数
|
||||
|
||||
|
||||
```python
|
||||
# 节约内存的一个标配函数
|
||||
def reduce_mem(df):
|
||||
starttime = time.time()
|
||||
numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']
|
||||
start_mem = df.memory_usage().sum() / 1024**2
|
||||
for col in df.columns:
|
||||
col_type = df[col].dtypes
|
||||
if col_type in numerics:
|
||||
c_min = df[col].min()
|
||||
c_max = df[col].max()
|
||||
if pd.isnull(c_min) or pd.isnull(c_max):
|
||||
continue
|
||||
if str(col_type)[:3] == 'int':
|
||||
if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
|
||||
df[col] = df[col].astype(np.int8)
|
||||
elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
|
||||
df[col] = df[col].astype(np.int16)
|
||||
elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
|
||||
df[col] = df[col].astype(np.int32)
|
||||
elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:
|
||||
df[col] = df[col].astype(np.int64)
|
||||
else:
|
||||
if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:
|
||||
df[col] = df[col].astype(np.float16)
|
||||
elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:
|
||||
df[col] = df[col].astype(np.float32)
|
||||
else:
|
||||
df[col] = df[col].astype(np.float64)
|
||||
end_mem = df.memory_usage().sum() / 1024**2
|
||||
print('-- Mem. usage decreased to {:5.2f} Mb ({:.1f}% reduction),time spend:{:2.2f} min'.format(end_mem,
|
||||
100*(start_mem-end_mem)/start_mem,
|
||||
(time.time()-starttime)/60))
|
||||
return df
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 读取采样或全量数据
|
||||
|
||||
|
||||
```python
|
||||
# debug模式:从训练集中划出一部分数据来调试代码
|
||||
def get_all_click_sample(data_path, sample_nums=10000):
|
||||
"""
|
||||
训练集中采样一部分数据调试
|
||||
data_path: 原数据的存储路径
|
||||
sample_nums: 采样数目(这里由于机器的内存限制,可以采样用户做)
|
||||
"""
|
||||
all_click = pd.read_csv(data_path + 'train_click_log.csv')
|
||||
all_user_ids = all_click.user_id.unique()
|
||||
|
||||
sample_user_ids = np.random.choice(all_user_ids, size=sample_nums, replace=False)
|
||||
all_click = all_click[all_click['user_id'].isin(sample_user_ids)]
|
||||
|
||||
all_click = all_click.drop_duplicates((['user_id', 'click_article_id', 'click_timestamp']))
|
||||
return all_click
|
||||
|
||||
# 读取点击数据,这里分成线上和线下,如果是为了获取线上提交结果应该讲测试集中的点击数据合并到总的数据中
|
||||
# 如果是为了线下验证模型的有效性或者特征的有效性,可以只使用训练集
|
||||
def get_all_click_df(data_path='./data_raw/', offline=True):
|
||||
if offline:
|
||||
all_click = pd.read_csv(data_path + 'train_click_log.csv')
|
||||
else:
|
||||
trn_click = pd.read_csv(data_path + 'train_click_log.csv')
|
||||
tst_click = pd.read_csv(data_path + 'testA_click_log.csv')
|
||||
|
||||
all_click = trn_click.append(tst_click)
|
||||
|
||||
all_click = all_click.drop_duplicates((['user_id', 'click_article_id', 'click_timestamp']))
|
||||
return all_click
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 全量训练集
|
||||
all_click_df = get_all_click_df(offline=False)
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 获取 用户 - 文章 - 点击时间字典
|
||||
|
||||
|
||||
```python
|
||||
# 根据点击时间获取用户的点击文章序列 {user1: {item1: time1, item2: time2..}...}
|
||||
def get_user_item_time(click_df):
|
||||
|
||||
click_df = click_df.sort_values('click_timestamp')
|
||||
|
||||
def make_item_time_pair(df):
|
||||
return list(zip(df['click_article_id'], df['click_timestamp']))
|
||||
|
||||
user_item_time_df = click_df.groupby('user_id')['click_article_id', 'click_timestamp'].apply(lambda x: make_item_time_pair(x))\
|
||||
.reset_index().rename(columns={0: 'item_time_list'})
|
||||
user_item_time_dict = dict(zip(user_item_time_df['user_id'], user_item_time_df['item_time_list']))
|
||||
|
||||
return user_item_time_dict
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 获取点击最多的Topk个文章
|
||||
|
||||
|
||||
```python
|
||||
# 获取近期点击最多的文章
|
||||
def get_item_topk_click(click_df, k):
|
||||
topk_click = click_df['click_article_id'].value_counts().index[:k]
|
||||
return topk_click
|
||||
```
|
||||
|
||||
|
||||
|
||||
## itemCF的物品相似度计算
|
||||
|
||||
|
||||
```python
|
||||
def itemcf_sim(df):
|
||||
"""
|
||||
文章与文章之间的相似性矩阵计算
|
||||
:param df: 数据表
|
||||
:item_created_time_dict: 文章创建时间的字典
|
||||
return : 文章与文章的相似性矩阵
|
||||
思路: 基于物品的协同过滤(详细请参考上一期推荐系统基础的组队学习), 在多路召回部分会加上关联规则的召回策略
|
||||
"""
|
||||
|
||||
user_item_time_dict = get_user_item_time(df)
|
||||
|
||||
# 计算物品相似度
|
||||
i2i_sim = {}
|
||||
item_cnt = defaultdict(int)
|
||||
for user, item_time_list in tqdm(user_item_time_dict.items()):
|
||||
# 在基于商品的协同过滤优化的时候可以考虑时间因素
|
||||
for i, i_click_time in item_time_list:
|
||||
item_cnt[i] += 1
|
||||
i2i_sim.setdefault(i, {})
|
||||
for j, j_click_time in item_time_list:
|
||||
if(i == j):
|
||||
continue
|
||||
i2i_sim[i].setdefault(j, 0)
|
||||
|
||||
i2i_sim[i][j] += 1 / math.log(len(item_time_list) + 1)
|
||||
|
||||
i2i_sim_ = i2i_sim.copy()
|
||||
for i, related_items in i2i_sim.items():
|
||||
for j, wij in related_items.items():
|
||||
i2i_sim_[i][j] = wij / math.sqrt(item_cnt[i] * item_cnt[j])
|
||||
|
||||
# 将得到的相似性矩阵保存到本地
|
||||
pickle.dump(i2i_sim_, open(save_path + 'itemcf_i2i_sim.pkl', 'wb'))
|
||||
|
||||
return i2i_sim_
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
i2i_sim = itemcf_sim(all_click_df)
|
||||
```
|
||||
|
||||
|
||||
|
||||
## itemCF 的文章推荐
|
||||
|
||||
|
||||
```python
|
||||
# 基于商品的召回i2i
|
||||
def item_based_recommend(user_id, user_item_time_dict, i2i_sim, sim_item_topk, recall_item_num, item_topk_click):
|
||||
"""
|
||||
基于文章协同过滤的召回
|
||||
:param user_id: 用户id
|
||||
:param user_item_time_dict: 字典, 根据点击时间获取用户的点击文章序列 {user1: {item1: time1, item2: time2..}...}
|
||||
:param i2i_sim: 字典,文章相似性矩阵
|
||||
:param sim_item_topk: 整数, 选择与当前文章最相似的前k篇文章
|
||||
:param recall_item_num: 整数, 最后的召回文章数量
|
||||
:param item_topk_click: 列表,点击次数最多的文章列表,用户召回补全
|
||||
return: 召回的文章列表 {item1:score1, item2: score2...}
|
||||
注意: 基于物品的协同过滤(详细请参考上一期推荐系统基础的组队学习), 在多路召回部分会加上关联规则的召回策略
|
||||
"""
|
||||
|
||||
# 获取用户历史交互的文章
|
||||
user_hist_items = user_item_time_dict[user_id]
|
||||
|
||||
item_rank = {}
|
||||
for loc, (i, click_time) in enumerate(user_hist_items):
|
||||
for j, wij in sorted(i2i_sim[i].items(), key=lambda x: x[1], reverse=True)[:sim_item_topk]:
|
||||
if j in user_hist_items:
|
||||
continue
|
||||
|
||||
item_rank.setdefault(j, 0)
|
||||
item_rank[j] += wij
|
||||
|
||||
# 不足10个,用热门商品补全
|
||||
if len(item_rank) < recall_item_num:
|
||||
for i, item in enumerate(item_topk_click):
|
||||
if item in item_rank.items(): # 填充的item应该不在原来的列表中
|
||||
continue
|
||||
item_rank[item] = - i - 100 # 随便给个负数就行
|
||||
if len(item_rank) == recall_item_num:
|
||||
break
|
||||
|
||||
item_rank = sorted(item_rank.items(), key=lambda x: x[1], reverse=True)[:recall_item_num]
|
||||
|
||||
return item_rank
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 给每个用户根据物品的协同过滤推荐文章
|
||||
|
||||
|
||||
```python
|
||||
# 定义
|
||||
user_recall_items_dict = collections.defaultdict(dict)
|
||||
|
||||
# 获取 用户 - 文章 - 点击时间的字典
|
||||
user_item_time_dict = get_user_item_time(all_click_df)
|
||||
|
||||
# 去取文章相似度
|
||||
i2i_sim = pickle.load(open(save_path + 'itemcf_i2i_sim.pkl', 'rb'))
|
||||
|
||||
# 相似文章的数量
|
||||
sim_item_topk = 10
|
||||
|
||||
# 召回文章数量
|
||||
recall_item_num = 10
|
||||
|
||||
# 用户热度补全
|
||||
item_topk_click = get_item_topk_click(all_click_df, k=50)
|
||||
|
||||
for user in tqdm(all_click_df['user_id'].unique()):
|
||||
user_recall_items_dict[user] = item_based_recommend(user, user_item_time_dict, i2i_sim,
|
||||
sim_item_topk, recall_item_num, item_topk_click)
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 召回字典转换成df
|
||||
|
||||
|
||||
```python
|
||||
# 将字典的形式转换成df
|
||||
user_item_score_list = []
|
||||
|
||||
for user, items in tqdm(user_recall_items_dict.items()):
|
||||
for item, score in items:
|
||||
user_item_score_list.append([user, item, score])
|
||||
|
||||
recall_df = pd.DataFrame(user_item_score_list, columns=['user_id', 'click_article_id', 'pred_score'])
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 生成提交文件
|
||||
|
||||
|
||||
```python
|
||||
# 生成提交文件
|
||||
def submit(recall_df, topk=5, model_name=None):
|
||||
recall_df = recall_df.sort_values(by=['user_id', 'pred_score'])
|
||||
recall_df['rank'] = recall_df.groupby(['user_id'])['pred_score'].rank(ascending=False, method='first')
|
||||
|
||||
# 判断是不是每个用户都有5篇文章及以上
|
||||
tmp = recall_df.groupby('user_id').apply(lambda x: x['rank'].max())
|
||||
assert tmp.min() >= topk
|
||||
|
||||
del recall_df['pred_score']
|
||||
submit = recall_df[recall_df['rank'] <= topk].set_index(['user_id', 'rank']).unstack(-1).reset_index()
|
||||
|
||||
submit.columns = [int(col) if isinstance(col, int) else col for col in submit.columns.droplevel(0)]
|
||||
# 按照提交格式定义列名
|
||||
submit = submit.rename(columns={'': 'user_id', 1: 'article_1', 2: 'article_2',
|
||||
3: 'article_3', 4: 'article_4', 5: 'article_5'})
|
||||
|
||||
save_name = save_path + model_name + '_' + datetime.today().strftime('%m-%d') + '.csv'
|
||||
submit.to_csv(save_name, index=False, header=True)
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 获取测试集
|
||||
tst_click = pd.read_csv(data_path + 'testA_click_log.csv')
|
||||
tst_users = tst_click['user_id'].unique()
|
||||
|
||||
# 从所有的召回数据中将测试集中的用户选出来
|
||||
tst_recall = recall_df[recall_df['user_id'].isin(tst_users)]
|
||||
|
||||
# 生成提交文件
|
||||
submit(tst_recall, topk=5, model_name='itemcf_baseline')
|
||||
```
|
||||
|
||||
# 总结
|
||||
|
||||
本节内容主要包括赛题简介,数据概况,评价方式以及对该赛题进行了一个总体上的思路分析,作为竞赛前的预热,旨在帮助学习者们能够更好切入该赛题,为后面的学习内容打下一个良好的基础。最后我们给出了关于本赛题的一个简易Baseline, 帮助学习者们先了解一下新闻推荐比赛的一个整理流程, 接下来我们就对于流程中的每个步骤进行详细的介绍。
|
||||
|
||||
今天的学习比较简单,下面整理一下关于赛题理解的一些经验:
|
||||
* 赛题理解究竟是在理解什么?
|
||||
|
||||
>**理解赛题**:从直观上对问题进行梳理, 分析问题的目标,到底要让做什么事情, **这个非常重要**
|
||||
>
|
||||
>**理解数据**:对赛题数据有一个初步了解,知道和任务相关的数据字段和数据字段的类型, 数据之间的内在关联等,大体梳理一下哪些数据会对我们解决问题非常有用,方便后面我们的数据分析和特征工程。
|
||||
>
|
||||
>**理解评估指标**:评估指标是检验我们提出的方法,我们给出结果好坏的标准,只有正确的理解了评估指标,我们才能进行更好的训练模型,更好的进行预测。此外,很多情况下,线上验证是有一定的时间和次数限制的,**所以在比赛中构建一个合理的本地的验证集和验证的评价指标是很关键的步骤,能有效的节省很多时间**。 不同的指标对于同样的预测结果是具有误差敏感的差异性的所以不同的评价指标会影响后续一些预测的侧重点。
|
||||
|
||||
* 有了赛题理解之后,我们该做什么?
|
||||
|
||||
>在对于赛题有了一定的了解后,分析清楚了问题的类型性质和对于数据理解 的这一基础上,我们可以梳理一个解决赛题的一个大题思路和框架
|
||||
>
|
||||
>我们至少要有一些相应的理解分析,比如**这题的难点可能在哪里,关键点可能在哪里,哪些地方可以挖掘更好的特征**.
|
||||
>
|
||||
>用什么样得线下验证方式更为稳定,**出现了过拟合或者其他问题,估摸可以用什么方法去解决这些问题**
|
||||
>
|
||||
|
||||
这时是在一个宏观的大体下分析的,有助于摸清整个题的思路脉络,以及后续的分析方向
|
||||
|
||||
**关于Datawhale:** Datawhale是一个专注于数据科学与AI领域的开源组织,汇集了众多领域院校和知名企业的优秀学习者,聚合了一群有开源精神和探索精神的团队成员。Datawhale 以“for the learner,和学习者一起成长”为愿景,鼓励真实地展现自我、开放包容、互信互助、敢于试错和勇于担当。同时 Datawhale 用开源的理念去探索开源内容、开源学习和开源方案,赋能人才培养,助力人才成长,建立起人与人,人与知识,人与企业和人与未来的联结。 本次数据挖掘路径学习,专题知识将在天池分享,详情可关注Datawhale:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,658 @@
|
||||
# 数据分析
|
||||
|
||||
数据分析的价值主要在于熟悉了解整个数据集的基本情况包括每个文件里有哪些数据,具体的文件中的每个字段表示什么实际含义,以及数据集中特征之间的相关性,在推荐场景下主要就是分析用户本身的基本属性,文章基本属性,以及用户和文章交互的一些分布,这些都有利于后面的召回策略的选择,以及特征工程。
|
||||
|
||||
**建议:当特征工程和模型调参已经很难继续上分了,可以回来在重新从新的角度去分析这些数据,或许可以找到上分的灵感**
|
||||
|
||||
## 导包
|
||||
|
||||
```python
|
||||
# 导入相关包
|
||||
%matplotlib inline
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import seaborn as sns
|
||||
plt.rc('font', family='SimHei', size=13)
|
||||
|
||||
import os,gc,re,warnings,sys
|
||||
warnings.filterwarnings("ignore")
|
||||
```
|
||||
|
||||
## 读取数据
|
||||
|
||||
|
||||
```python
|
||||
path = './data_raw/'
|
||||
|
||||
#####train
|
||||
trn_click = pd.read_csv(path+'train_click_log.csv')
|
||||
item_df = pd.read_csv(path+'articles.csv')
|
||||
item_df = item_df.rename(columns={'article_id': 'click_article_id'}) #重命名,方便后续match
|
||||
item_emb_df = pd.read_csv(path+'articles_emb.csv')
|
||||
|
||||
#####test
|
||||
tst_click = pd.read_csv(path+'testA_click_log.csv')
|
||||
```
|
||||
|
||||
## 数据预处理
|
||||
|
||||
计算用户点击rank和点击次数
|
||||
|
||||
|
||||
```python
|
||||
# 对每个用户的点击时间戳进行排序
|
||||
trn_click['rank'] = trn_click.groupby(['user_id'])['click_timestamp'].rank(ascending=False).astype(int)
|
||||
tst_click['rank'] = tst_click.groupby(['user_id'])['click_timestamp'].rank(ascending=False).astype(int)
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
#计算用户点击文章的次数,并添加新的一列count
|
||||
trn_click['click_cnts'] = trn_click.groupby(['user_id'])['click_timestamp'].transform('count')
|
||||
tst_click['click_cnts'] = tst_click.groupby(['user_id'])['click_timestamp'].transform('count')
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 数据浏览
|
||||
|
||||
### 用户点击日志文件_训练集
|
||||
|
||||
|
||||
```python
|
||||
trn_click = trn_click.merge(item_df, how='left', on=['click_article_id'])
|
||||
trn_click.head()
|
||||
```
|
||||
|
||||

|
||||
|
||||
**train_click_log.csv文件数据中每个字段的含义**
|
||||
|
||||
1. user_id: 用户的唯一标识
|
||||
2. click_article_id: 用户点击的文章唯一标识
|
||||
3. click_timestamp: 用户点击文章时的时间戳
|
||||
4. click_environment: 用户点击文章的环境
|
||||
5. click_deviceGroup: 用户点击文章的设备组
|
||||
6. click_os: 用户点击文章时的操作系统
|
||||
7. click_country: 用户点击文章时的所在的国家
|
||||
8. click_region: 用户点击文章时所在的区域
|
||||
9. click_referrer_type: 用户点击文章时,文章的来源
|
||||
|
||||
|
||||
```python
|
||||
#用户点击日志信息
|
||||
trn_click.info()
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
```python
|
||||
trn_click.describe()
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
```python
|
||||
#训练集中的用户数量为20w
|
||||
trn_click.user_id.nunique()
|
||||
```
|
||||
|
||||
|
||||
200000
|
||||
|
||||
|
||||
```python
|
||||
trn_click.groupby('user_id')['click_article_id'].count().min() # 训练集里面每个用户至少点击了两篇文章
|
||||
```
|
||||
|
||||
|
||||
2
|
||||
|
||||
**画直方图大体看一下基本的属性分布**
|
||||
|
||||
|
||||
```python
|
||||
plt.figure()
|
||||
plt.figure(figsize=(15, 20))
|
||||
i = 1
|
||||
for col in ['click_article_id', 'click_timestamp', 'click_environment', 'click_deviceGroup', 'click_os', 'click_country',
|
||||
'click_region', 'click_referrer_type', 'rank', 'click_cnts']:
|
||||
plot_envs = plt.subplot(5, 2, i)
|
||||
i += 1
|
||||
v = trn_click[col].value_counts().reset_index()[:10]
|
||||
fig = sns.barplot(x=v['index'], y=v[col])
|
||||
for item in fig.get_xticklabels():
|
||||
item.set_rotation(90)
|
||||
plt.title(col)
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
```
|
||||
|
||||

|
||||
|
||||
**从点击时间clik_timestamp来看,分布较为平均,可不做特殊处理。由于时间戳是13位的,后续将时间格式转换成10位方便计算。**
|
||||
|
||||
**从点击环境click_environment来看,仅有1922次(占0.1%)点击环境为1;仅有24617次(占2.3%)点击环境为2;剩余(占97.6%)点击环境为4。**
|
||||
|
||||
**从点击设备组click_deviceGroup来看,设备1占大部分(60.4%),设备3占36%。**
|
||||
|
||||
### 测试集用户点击日志
|
||||
|
||||
|
||||
```python
|
||||
tst_click = tst_click.merge(item_df, how='left', on=['click_article_id'])
|
||||
tst_click.head()
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
```python
|
||||
tst_click.describe()
|
||||
```
|
||||
|
||||

|
||||
|
||||
**我们可以看出训练集和测试集的用户是完全不一样的**
|
||||
|
||||
**训练集的用户ID由0 ~ 199999,而测试集A的用户ID由200000 ~ 249999。**
|
||||
|
||||
|
||||
```python
|
||||
#测试集中的用户数量为5w
|
||||
tst_click.user_id.nunique()
|
||||
```
|
||||
|
||||
|
||||
50000
|
||||
|
||||
|
||||
```python
|
||||
tst_click.groupby('user_id')['click_article_id'].count().min() # 注意测试集里面有只点击过一次文章的用户
|
||||
```
|
||||
|
||||
|
||||
1
|
||||
|
||||
### 新闻文章信息数据表
|
||||
|
||||
|
||||
```python
|
||||
#新闻文章数据集浏览
|
||||
item_df.head().append(item_df.tail())
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
```python
|
||||
item_df['words_count'].value_counts()
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
```python
|
||||
print(item_df['category_id'].nunique()) # 461个文章主题
|
||||
item_df['category_id'].hist()
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
```python
|
||||
item_df.shape # 364047篇文章
|
||||
```
|
||||
|
||||
|
||||
(364047, 4)
|
||||
|
||||
### 新闻文章embedding向量表示
|
||||
|
||||
|
||||
```python
|
||||
item_emb_df.head()
|
||||
```
|
||||
|
||||

|
||||
|
||||
```python
|
||||
item_emb_df.shape
|
||||
```
|
||||
|
||||
|
||||
(364047, 251)
|
||||
|
||||
## 数据分析
|
||||
|
||||
### 用户重复点击
|
||||
|
||||
|
||||
```python
|
||||
#####merge
|
||||
user_click_merge = trn_click.append(tst_click)
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
#用户重复点击
|
||||
user_click_count = user_click_merge.groupby(['user_id', 'click_article_id'])['click_timestamp'].agg({'count'}).reset_index()
|
||||
user_click_count[:10]
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
```python
|
||||
user_click_count[user_click_count['count']>7]
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
```python
|
||||
user_click_count['count'].unique()
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
```python
|
||||
#用户点击新闻次数
|
||||
user_click_count.loc[:,'count'].value_counts()
|
||||
```
|
||||
|
||||

|
||||
|
||||
**可以看出:有1605541(约占99.2%)的用户未重复阅读过文章,仅有极少数用户重复点击过某篇文章。 这个也可以单独制作成特征**
|
||||
|
||||
### 用户点击环境变化分析
|
||||
|
||||
|
||||
```python
|
||||
def plot_envs(df, cols, r, c):
|
||||
plt.figure()
|
||||
plt.figure(figsize=(10, 5))
|
||||
i = 1
|
||||
for col in cols:
|
||||
plt.subplot(r, c, i)
|
||||
i += 1
|
||||
v = df[col].value_counts().reset_index()
|
||||
fig = sns.barplot(x=v['index'], y=v[col])
|
||||
for item in fig.get_xticklabels():
|
||||
item.set_rotation(90)
|
||||
plt.title(col)
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 分析用户点击环境变化是否明显,这里随机采样10个用户分析这些用户的点击环境分布
|
||||
sample_user_ids = np.random.choice(tst_click['user_id'].unique(), size=5, replace=False)
|
||||
sample_users = user_click_merge[user_click_merge['user_id'].isin(sample_user_ids)]
|
||||
cols = ['click_environment','click_deviceGroup', 'click_os', 'click_country', 'click_region','click_referrer_type']
|
||||
for _, user_df in sample_users.groupby('user_id'):
|
||||
plot_envs(user_df, cols, 2, 3)
|
||||
```
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
**可以看出绝大多数数的用户的点击环境是比较固定的。思路:可以基于这些环境的统计特征来代表该用户本身的属性**
|
||||
|
||||
### 用户点击新闻数量的分布
|
||||
|
||||
|
||||
```python
|
||||
user_click_item_count = sorted(user_click_merge.groupby('user_id')['click_article_id'].count(), reverse=True)
|
||||
plt.plot(user_click_item_count)
|
||||
```
|
||||
|
||||
|
||||

|
||||
|
||||
**可以根据用户的点击文章次数看出用户的活跃度**
|
||||
|
||||
|
||||
```python
|
||||
#点击次数在前50的用户
|
||||
plt.plot(user_click_item_count[:50])
|
||||
```
|
||||
|
||||

|
||||
|
||||
**点击次数排前50的用户的点击次数都在100次以上。思路:我们可以定义点击次数大于等于100次的用户为活跃用户,这是一种简单的处理思路, 判断用户活跃度,更加全面的是再结合上点击时间,后面我们会基于点击次数和点击时间两个方面来判断用户活跃度。**
|
||||
|
||||
|
||||
```python
|
||||
#点击次数排名在[25000:50000]之间
|
||||
plt.plot(user_click_item_count[25000:50000])
|
||||
```
|
||||
|
||||

|
||||
|
||||
**可以看出点击次数小于等于两次的用户非常的多,这些用户可以认为是非活跃用户**
|
||||
|
||||
### 新闻点击次数分析
|
||||
|
||||
|
||||
```python
|
||||
item_click_count = sorted(user_click_merge.groupby('click_article_id')['user_id'].count(), reverse=True)
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
plt.plot(item_click_count)
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
```python
|
||||
plt.plot(item_click_count[:100])
|
||||
```
|
||||
|
||||

|
||||
|
||||
**可以看出点击次数最多的前100篇新闻,点击次数大于1000次**
|
||||
|
||||
|
||||
```python
|
||||
plt.plot(item_click_count[:20])
|
||||
```
|
||||
|
||||

|
||||
|
||||
**点击次数最多的前20篇新闻,点击次数大于2500。思路:可以定义这些新闻为热门新闻, 这个也是简单的处理方式,后面我们也是根据点击次数和时间进行文章热度的一个划分。**
|
||||
|
||||
|
||||
```python
|
||||
plt.plot(item_click_count[3500:])
|
||||
```
|
||||
|
||||

|
||||
|
||||
**可以发现很多新闻只被点击过一两次。思路:可以定义这些新闻是冷门新闻。**
|
||||
|
||||
### 新闻共现频次:两篇新闻连续出现的次数
|
||||
|
||||
|
||||
```python
|
||||
tmp = user_click_merge.sort_values('click_timestamp')
|
||||
tmp['next_item'] = tmp.groupby(['user_id'])['click_article_id'].transform(lambda x:x.shift(-1))
|
||||
union_item = tmp.groupby(['click_article_id','next_item'])['click_timestamp'].agg({'count'}).reset_index().sort_values('count', ascending=False)
|
||||
union_item[['count']].describe()
|
||||
```
|
||||
|
||||

|
||||
|
||||
**由统计数据可以看出,平均共现次数2.88,最高为1687。**
|
||||
|
||||
**说明用户看的新闻,相关性是比较强的。**
|
||||
|
||||
|
||||
```python
|
||||
#画个图直观地看一看
|
||||
x = union_item['click_article_id']
|
||||
y = union_item['count']
|
||||
plt.scatter(x, y)
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
```python
|
||||
plt.plot(union_item['count'].values[40000:])
|
||||
```
|
||||
|
||||

|
||||
|
||||
**大概有70000个pair至少共现一次。**
|
||||
|
||||
|
||||
|
||||
### 新闻文章信息
|
||||
|
||||
|
||||
```python
|
||||
#不同类型的新闻出现的次数
|
||||
plt.plot(user_click_merge['category_id'].value_counts().values)
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
```python
|
||||
#出现次数比较少的新闻类型, 有些新闻类型,基本上就出现过几次
|
||||
plt.plot(user_click_merge['category_id'].value_counts().values[150:])
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
```python
|
||||
#新闻字数的描述性统计
|
||||
user_click_merge['words_count'].describe()
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
```python
|
||||
plt.plot(user_click_merge['words_count'].values)
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
### 用户点击的新闻类型的偏好
|
||||
|
||||
此特征可以用于度量用户的兴趣是否广泛。
|
||||
|
||||
|
||||
```python
|
||||
plt.plot(sorted(user_click_merge.groupby('user_id')['category_id'].nunique(), reverse=True))
|
||||
```
|
||||
|
||||
|
||||

|
||||
|
||||
**从上图中可以看出有一小部分用户阅读类型是极其广泛的,大部分人都处在20个新闻类型以下。**
|
||||
|
||||
|
||||
```python
|
||||
user_click_merge.groupby('user_id')['category_id'].nunique().reset_index().describe()
|
||||
```
|
||||
|
||||

|
||||
|
||||
### 用户查看文章的长度的分布
|
||||
|
||||
通过统计不同用户点击新闻的平均字数,这个可以反映用户是对长文更感兴趣还是对短文更感兴趣。
|
||||
|
||||
|
||||
```python
|
||||
plt.plot(sorted(user_click_merge.groupby('user_id')['words_count'].mean(), reverse=True))
|
||||
```
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
**从上图中可以发现有一小部分人看的文章平均词数非常高,也有一小部分人看的平均文章次数非常低。**
|
||||
|
||||
**大多数人偏好于阅读字数在200-400字之间的新闻。**
|
||||
|
||||
|
||||
```python
|
||||
#挑出大多数人的区间仔细看看
|
||||
plt.plot(sorted(user_click_merge.groupby('user_id')['words_count'].mean(), reverse=True)[1000:45000])
|
||||
```
|
||||
|
||||

|
||||
|
||||
**可以发现大多数人都是看250字以下的文章**
|
||||
|
||||
|
||||
```python
|
||||
#更加详细的参数
|
||||
user_click_merge.groupby('user_id')['words_count'].mean().reset_index().describe()
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
### 用户点击新闻的时间分析
|
||||
|
||||
|
||||
```python
|
||||
#为了更好的可视化,这里把时间进行归一化操作
|
||||
from sklearn.preprocessing import MinMaxScaler
|
||||
mm = MinMaxScaler()
|
||||
user_click_merge['click_timestamp'] = mm.fit_transform(user_click_merge[['click_timestamp']])
|
||||
user_click_merge['created_at_ts'] = mm.fit_transform(user_click_merge[['created_at_ts']])
|
||||
|
||||
user_click_merge = user_click_merge.sort_values('click_timestamp')
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
user_click_merge.head()
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
```python
|
||||
def mean_diff_time_func(df, col):
|
||||
df = pd.DataFrame(df, columns={col})
|
||||
df['time_shift1'] = df[col].shift(1).fillna(0)
|
||||
df['diff_time'] = abs(df[col] - df['time_shift1'])
|
||||
return df['diff_time'].mean()
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 点击时间差的平均值
|
||||
mean_diff_click_time = user_click_merge.groupby('user_id')['click_timestamp', 'created_at_ts'].apply(lambda x: mean_diff_time_func(x, 'click_timestamp'))
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
plt.plot(sorted(mean_diff_click_time.values, reverse=True))
|
||||
```
|
||||
|
||||

|
||||
|
||||
**从上图可以发现不同用户点击文章的时间差是有差异的。**
|
||||
|
||||
|
||||
```python
|
||||
# 前后点击文章的创建时间差的平均值
|
||||
mean_diff_created_time = user_click_merge.groupby('user_id')['click_timestamp', 'created_at_ts'].apply(lambda x: mean_diff_time_func(x, 'created_at_ts'))
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
plt.plot(sorted(mean_diff_created_time.values, reverse=True))
|
||||
```
|
||||
|
||||

|
||||
|
||||
**从图中可以发现用户先后点击文章,文章的创建时间也是有差异的**
|
||||
|
||||
|
||||
```python
|
||||
# 用户前后点击文章的相似性分布
|
||||
item_idx_2_rawid_dict = dict(zip(item_emb_df['article_id'], item_emb_df.index))
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
del item_emb_df['article_id']
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
item_emb_np = np.ascontiguousarray(item_emb_df.values, dtype=np.float32)
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 随机选择5个用户,查看这些用户前后查看文章的相似性
|
||||
sub_user_ids = np.random.choice(user_click_merge.user_id.unique(), size=15, replace=False)
|
||||
sub_user_info = user_click_merge[user_click_merge['user_id'].isin(sub_user_ids)]
|
||||
|
||||
sub_user_info.head()
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
```python
|
||||
def get_item_sim_list(df):
|
||||
sim_list = []
|
||||
item_list = df['click_article_id'].values
|
||||
for i in range(0, len(item_list)-1):
|
||||
emb1 = item_emb_np[item_idx_2_rawid_dict[item_list[i]]]
|
||||
emb2 = item_emb_np[item_idx_2_rawid_dict[item_list[i+1]]]
|
||||
sim_list.append(np.dot(emb1,emb2)/(np.linalg.norm(emb1)*(np.linalg.norm(emb2))))
|
||||
sim_list.append(0)
|
||||
return sim_list
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
for _, user_df in sub_user_info.groupby('user_id'):
|
||||
item_sim_list = get_item_sim_list(user_df)
|
||||
plt.plot(item_sim_list)
|
||||
```
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
**从图中可以看出有些用户前后看的商品的相似度波动比较大,有些波动比较小,也是有一定的区分度的。**
|
||||
|
||||
|
||||
|
||||
## 总结
|
||||
|
||||
通过数据分析的过程, 我们目前可以得到以下几点重要的信息, 这个对于我们进行后面的特征制作和分析非常有帮助:
|
||||
|
||||
1. 训练集和测试集的用户id没有重复,也就是测试集里面的用户没有模型是没有见过的
|
||||
2. 训练集中用户最少的点击文章数是2, 而测试集里面用户最少的点击文章数是1
|
||||
3. 用户对于文章存在重复点击的情况, 但这个都存在于训练集里面
|
||||
4. 同一用户的点击环境存在不唯一的情况,后面做这部分特征的时候可以采用统计特征
|
||||
5. 用户点击文章的次数有很大的区分度,后面可以根据这个制作衡量用户活跃度的特征
|
||||
6. 文章被用户点击的次数也有很大的区分度,后面可以根据这个制作衡量文章热度的特征
|
||||
7. 用户看的新闻,相关性是比较强的,所以往往我们判断用户是否对某篇文章感兴趣的时候, 在很大程度上会和他历史点击过的文章有关
|
||||
8. 用户点击的文章字数有比较大的区别, 这个可以反映用户对于文章字数的区别
|
||||
9. 用户点击过的文章主题也有很大的区别, 这个可以反映用户的主题偏好
|
||||
10.不同用户点击文章的时间差也会有所区别, 这个可以反映用户对于文章时效性的偏好
|
||||
|
||||
所以根据上面的一些分析,可以更好的帮助我们后面做好特征工程, 充分挖掘数据的隐含信息。
|
||||
|
||||
|
||||
|
||||
**关于Datawhale:** Datawhale是一个专注于数据科学与AI领域的开源组织,汇集了众多领域院校和知名企业的优秀学习者,聚合了一群有开源精神和探索精神的团队成员。Datawhale 以“for the learner,和学习者一起成长”为愿景,鼓励真实地展现自我、开放包容、互信互助、敢于试错和勇于担当。同时 Datawhale 用开源的理念去探索开源内容、开源学习和开源方案,赋能人才培养,助力人才成长,建立起人与人,人与知识,人与企业和人与未来的联结。 本次数据挖掘路径学习,专题知识将在天池分享,详情可关注Datawhale:
|
||||
|
||||

|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,989 @@
|
||||
# 特征工程(制作特征和标签, 转成监督学习问题)
|
||||
|
||||
我们先捋一下基于原始的给定数据, 有哪些特征可以直接利用:
|
||||
|
||||
1. 文章的自身特征, category_id表示这文章的类型, created_at_ts表示文章建立的时间, 这个关系着文章的时效性, words_count是文章的字数, 一般字数太长我们不太喜欢点击, 也不排除有人就喜欢读长文。
|
||||
2. 文章的内容embedding特征, 这个召回的时候用过, 这里可以选择使用, 也可以选择不用, 也可以尝试其他类型的embedding特征, 比如W2V等
|
||||
3. 用户的设备特征信息
|
||||
|
||||
上面这些直接可以用的特征, 待做完特征工程之后, 直接就可以根据article_id或者是user_id把这些特征加入进去。 但是我们需要先基于召回的结果, 构造一些特征,然后制作标签,形成一个监督学习的数据集。<br><br>
|
||||
构造监督数据集的思路, 根据召回结果, 我们会得到一个{user_id: [可能点击的文章列表]}形式的字典。 那么我们就可以对于每个用户, 每篇可能点击的文章构造一个监督测试集, 比如对于用户user1, 假设得到的他的召回列表{user1: [item1, item2, item3]}, 我们就可以得到三行数据(user1, item1), (user1, item2), (user1, item3)的形式, 这就是监督测试集时候的前两列特征。<br><br>
|
||||
|
||||
构造特征的思路是这样, 我们知道每个用户的点击文章是与其历史点击的文章信息是有很大关联的, 比如同一个主题, 相似等等。 所以特征构造这块很重要的一系列特征**是要结合用户的历史点击文章信息**。我们已经得到了每个用户及点击候选文章的两列的一个数据集, 而我们的目的是要预测最后一次点击的文章, 比较自然的一个思路就是和其最后几次点击的文章产生关系, 这样既考虑了其历史点击文章信息, 又得离最后一次点击较近,因为新闻很大的一个特点就是注重时效性。 往往用户的最后一次点击会和其最后几次点击有很大的关联。 所以我们就可以对于每个候选文章, 做出与最后几次点击相关的特征如下:
|
||||
|
||||
1. 候选item与最后几次点击的相似性特征(embedding内积) --- 这个直接关联用户历史行为
|
||||
2. 候选item与最后几次点击的相似性特征的统计特征 --- 统计特征可以减少一些波动和异常
|
||||
3. 候选item与最后几次点击文章的字数差的特征 --- 可以通过字数看用户偏好
|
||||
4. 候选item与最后几次点击的文章建立的时间差特征 --- 时间差特征可以看出该用户对于文章的实时性的偏好
|
||||
|
||||
还需要考虑一下
|
||||
**5. 如果使用了youtube召回的话, 我们还可以制作用户与候选item的相似特征**
|
||||
|
||||
当然, 上面只是提供了一种基于用户历史行为做特征工程的思路, 大家也可以思维风暴一下,尝试一些其他的特征。 下面我们就实现上面的这些特征的制作, 下面的逻辑是这样:
|
||||
|
||||
1. 我们首先获得用户的最后一次点击操作和用户的历史点击, 这个基于我们的日志数据集做
|
||||
2. 基于用户的历史行为制作特征, 这个会用到用户的历史点击表, 最后的召回列表, 文章的信息表和embedding向量
|
||||
3. 制作标签, 形成最后的监督学习数据集
|
||||
|
||||
## 导包
|
||||
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pickle
|
||||
from tqdm import tqdm
|
||||
import gc, os
|
||||
import logging
|
||||
import time
|
||||
import lightgbm as lgb
|
||||
from gensim.models import Word2Vec
|
||||
from sklearn.preprocessing import MinMaxScaler
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
```
|
||||
|
||||
## df节省内存函数
|
||||
|
||||
|
||||
```python
|
||||
# 节省内存的一个函数
|
||||
# 减少内存
|
||||
def reduce_mem(df):
|
||||
starttime = time.time()
|
||||
numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']
|
||||
start_mem = df.memory_usage().sum() / 1024**2
|
||||
for col in df.columns:
|
||||
col_type = df[col].dtypes
|
||||
if col_type in numerics:
|
||||
c_min = df[col].min()
|
||||
c_max = df[col].max()
|
||||
if pd.isnull(c_min) or pd.isnull(c_max):
|
||||
continue
|
||||
if str(col_type)[:3] == 'int':
|
||||
if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
|
||||
df[col] = df[col].astype(np.int8)
|
||||
elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
|
||||
df[col] = df[col].astype(np.int16)
|
||||
elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
|
||||
df[col] = df[col].astype(np.int32)
|
||||
elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:
|
||||
df[col] = df[col].astype(np.int64)
|
||||
else:
|
||||
if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:
|
||||
df[col] = df[col].astype(np.float16)
|
||||
elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:
|
||||
df[col] = df[col].astype(np.float32)
|
||||
else:
|
||||
df[col] = df[col].astype(np.float64)
|
||||
end_mem = df.memory_usage().sum() / 1024**2
|
||||
print('-- Mem. usage decreased to {:5.2f} Mb ({:.1f}% reduction),time spend:{:2.2f} min'.format(end_mem,
|
||||
100*(start_mem-end_mem)/start_mem,
|
||||
(time.time()-starttime)/60))
|
||||
return df
|
||||
```
|
||||
|
||||
## 定义数据路径
|
||||
|
||||
|
||||
```python
|
||||
data_path = './data_raw/'
|
||||
save_path = './temp_results/'
|
||||
```
|
||||
|
||||
## 数据读取
|
||||
|
||||
### 训练和验证集的划分
|
||||
|
||||
划分训练和验证集的原因是为了在线下验证模型参数的好坏,为了完全模拟测试集,我们这里就在训练集中抽取部分用户的所有信息来作为验证集。提前做训练验证集划分的好处就是可以分解制作排序特征时的压力,一次性做整个数据集的排序特征可能时间会比较长。
|
||||
|
||||
|
||||
```python
|
||||
# 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
|
||||
```
|
||||
|
||||
### 获取历史点击和最后一次点击
|
||||
|
||||
|
||||
```python
|
||||
# 获取当前数据的历史点击和最后一次点击
|
||||
def get_hist_and_last_click(all_click):
|
||||
all_click = all_click.sort_values(by=['user_id', 'click_timestamp'])
|
||||
click_last_df = all_click.groupby('user_id').tail(1)
|
||||
|
||||
# 如果用户只有一个点击,hist为空了,会导致训练的时候这个用户不可见,此时默认泄露一下
|
||||
def hist_func(user_df):
|
||||
if len(user_df) == 1:
|
||||
return user_df
|
||||
else:
|
||||
return user_df[:-1]
|
||||
|
||||
click_hist_df = all_click.groupby('user_id').apply(hist_func).reset_index(drop=True)
|
||||
|
||||
return click_hist_df, click_last_df
|
||||
```
|
||||
|
||||
### 读取训练、验证及测试集
|
||||
|
||||
|
||||
```python
|
||||
def get_trn_val_tst_data(data_path, offline=True):
|
||||
if offline:
|
||||
click_trn_data = pd.read_csv(data_path+'train_click_log.csv') # 训练集用户点击日志
|
||||
click_trn_data = reduce_mem(click_trn_data)
|
||||
click_trn, click_val, val_ans = trn_val_split(all_click_df, sample_user_nums)
|
||||
else:
|
||||
click_trn = pd.read_csv(data_path+'train_click_log.csv')
|
||||
click_trn = reduce_mem(click_trn)
|
||||
click_val = None
|
||||
val_ans = None
|
||||
|
||||
click_tst = pd.read_csv(data_path+'testA_click_log.csv')
|
||||
|
||||
return click_trn, click_val, click_tst, val_ans
|
||||
```
|
||||
|
||||
### 读取召回列表
|
||||
|
||||
|
||||
```python
|
||||
# 返回多路召回列表或者单路召回
|
||||
def get_recall_list(save_path, single_recall_model=None, multi_recall=False):
|
||||
if multi_recall:
|
||||
return pickle.load(open(save_path + 'final_recall_items_dict.pkl', 'rb'))
|
||||
|
||||
if single_recall_model == 'i2i_itemcf':
|
||||
return pickle.load(open(save_path + 'itemcf_recall_dict.pkl', 'rb'))
|
||||
elif single_recall_model == 'i2i_emb_itemcf':
|
||||
return pickle.load(open(save_path + 'itemcf_emb_dict.pkl', 'rb'))
|
||||
elif single_recall_model == 'user_cf':
|
||||
return pickle.load(open(save_path + 'youtubednn_usercf_dict.pkl', 'rb'))
|
||||
elif single_recall_model == 'youtubednn':
|
||||
return pickle.load(open(save_path + 'youtube_u2i_dict.pkl', 'rb'))
|
||||
```
|
||||
|
||||
### 读取各种Embedding
|
||||
|
||||
#### Word2Vec训练及gensim的使用
|
||||
|
||||
Word2Vec主要思想是:一个词的上下文可以很好的表达出词的语义。通过无监督学习产生词向量的方式。word2vec中有两个非常经典的模型:skip-gram和cbow。
|
||||
|
||||
- skip-gram:已知中心词预测周围词。
|
||||
- cbow:已知周围词预测中心词。
|
||||

|
||||
|
||||
在使用gensim训练word2vec的时候,有几个比较重要的参数
|
||||
- size: 表示词向量的维度。
|
||||
- window:决定了目标词会与多远距离的上下文产生关系。
|
||||
- sg: 如果是0,则是CBOW模型,是1则是Skip-Gram模型。
|
||||
- workers: 表示训练时候的线程数量
|
||||
- min_count: 设置最小的
|
||||
- iter: 训练时遍历整个数据集的次数
|
||||
|
||||
**注意**
|
||||
1. 训练的时候输入的语料库一定要是字符组成的二维数组,如:[['北', '京', '你', '好'], ['上', '海', '你', '好']]
|
||||
2. 使用模型的时候有一些默认值,可以通过在Jupyter里面通过`Word2Vec??`查看
|
||||
|
||||
|
||||
下面是个简单的测试样例:
|
||||
```
|
||||
from gensim.models import Word2Vec
|
||||
doc = [['30760', '157507'],
|
||||
['289197', '63746'],
|
||||
['36162', '168401'],
|
||||
['50644', '36162']]
|
||||
w2v = Word2Vec(docs, size=12, sg=1, window=2, seed=2020, workers=2, min_count=1, iter=1)
|
||||
|
||||
# 查看'30760'表示的词向量
|
||||
w2v['30760']
|
||||
```
|
||||
|
||||
skip-gram和cbow的详细原理可以参考下面的博客:
|
||||
- [word2vec原理(一) CBOW与Skip-Gram模型基础](https://www.cnblogs.com/pinard/p/7160330.html)
|
||||
- [word2vec原理(二) 基于Hierarchical Softmax的模型](https://www.cnblogs.com/pinard/p/7160330.html)
|
||||
- [word2vec原理(三) 基于Negative Sampling的模型](https://www.cnblogs.com/pinard/p/7249903.html)
|
||||
|
||||
|
||||
```python
|
||||
def trian_item_word2vec(click_df, embed_size=64, save_name='item_w2v_emb.pkl', split_char=' '):
|
||||
click_df = click_df.sort_values('click_timestamp')
|
||||
# 只有转换成字符串才可以进行训练
|
||||
click_df['click_article_id'] = click_df['click_article_id'].astype(str)
|
||||
# 转换成句子的形式
|
||||
docs = click_df.groupby(['user_id'])['click_article_id'].apply(lambda x: list(x)).reset_index()
|
||||
docs = docs['click_article_id'].values.tolist()
|
||||
|
||||
# 为了方便查看训练的进度,这里设定一个log信息
|
||||
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', level=logging.INFO)
|
||||
|
||||
# 这里的参数对训练得到的向量影响也很大,默认负采样为5
|
||||
w2v = Word2Vec(docs, size=16, sg=1, window=5, seed=2020, workers=24, min_count=1, iter=1)
|
||||
|
||||
# 保存成字典的形式
|
||||
item_w2v_emb_dict = {k: w2v[k] for k in click_df['click_article_id']}
|
||||
pickle.dump(item_w2v_emb_dict, open(save_path + 'item_w2v_emb.pkl', 'wb'))
|
||||
|
||||
return item_w2v_emb_dict
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 可以通过字典查询对应的item的Embedding
|
||||
def get_embedding(save_path, all_click_df):
|
||||
if os.path.exists(save_path + 'item_content_emb.pkl'):
|
||||
item_content_emb_dict = pickle.load(open(save_path + 'item_content_emb.pkl', 'rb'))
|
||||
else:
|
||||
print('item_content_emb.pkl 文件不存在...')
|
||||
|
||||
# w2v Embedding是需要提前训练好的
|
||||
if os.path.exists(save_path + 'item_w2v_emb.pkl'):
|
||||
item_w2v_emb_dict = pickle.load(open(save_path + 'item_w2v_emb.pkl', 'rb'))
|
||||
else:
|
||||
item_w2v_emb_dict = trian_item_word2vec(all_click_df)
|
||||
|
||||
if os.path.exists(save_path + 'item_youtube_emb.pkl'):
|
||||
item_youtube_emb_dict = pickle.load(open(save_path + 'item_youtube_emb.pkl', 'rb'))
|
||||
else:
|
||||
print('item_youtube_emb.pkl 文件不存在...')
|
||||
|
||||
if os.path.exists(save_path + 'user_youtube_emb.pkl'):
|
||||
user_youtube_emb_dict = pickle.load(open(save_path + 'user_youtube_emb.pkl', 'rb'))
|
||||
else:
|
||||
print('user_youtube_emb.pkl 文件不存在...')
|
||||
|
||||
return item_content_emb_dict, item_w2v_emb_dict, item_youtube_emb_dict, user_youtube_emb_dict
|
||||
```
|
||||
|
||||
### 读取文章信息
|
||||
|
||||
|
||||
```python
|
||||
def get_article_info_df():
|
||||
article_info_df = pd.read_csv(data_path + 'articles.csv')
|
||||
article_info_df = reduce_mem(article_info_df)
|
||||
|
||||
return article_info_df
|
||||
```
|
||||
|
||||
### 读取数据
|
||||
|
||||
|
||||
```python
|
||||
# 这里offline的online的区别就是验证集是否为空
|
||||
click_trn, click_val, click_tst, val_ans = get_trn_val_tst_data(data_path, offline=False)
|
||||
```
|
||||
|
||||
-- Mem. usage decreased to 23.34 Mb (69.4% reduction),time spend:0.00 min
|
||||
|
||||
|
||||
|
||||
```python
|
||||
click_trn_hist, click_trn_last = get_hist_and_last_click(click_trn)
|
||||
|
||||
if click_val is not None:
|
||||
click_val_hist, click_val_last = click_val, val_ans
|
||||
else:
|
||||
click_val_hist, click_val_last = None, None
|
||||
|
||||
click_tst_hist = click_tst
|
||||
```
|
||||
|
||||
## 对训练数据做负采样
|
||||
|
||||
通过召回我们将数据转换成三元组的形式(user1, item1, label)的形式,观察发现正负样本差距极度不平衡,我们可以先对负样本进行下采样,下采样的目的一方面缓解了正负样本比例的问题,另一方面也减小了我们做排序特征的压力,我们在做负采样的时候又有哪些东西是需要注意的呢?
|
||||
|
||||
1. 只对负样本进行下采样(如果有比较好的正样本扩充的方法其实也是可以考虑的)
|
||||
2. 负采样之后,保证所有的用户和文章仍然出现在采样之后的数据中
|
||||
3. 下采样的比例可以根据实际情况人为的控制
|
||||
4. 做完负采样之后,更新此时新的用户召回文章列表,因为后续做特征的时候可能用到相对位置的信息。
|
||||
|
||||
其实负采样也可以留在后面做完特征在进行,这里由于做排序特征太慢了,所以把负采样的环节提到前面了。
|
||||
|
||||
|
||||
```python
|
||||
# 将召回列表转换成df的形式
|
||||
def recall_dict_2_df(recall_list_dict):
|
||||
df_row_list = [] # [user, item, score]
|
||||
for user, recall_list in tqdm(recall_list_dict.items()):
|
||||
for item, score in recall_list:
|
||||
df_row_list.append([user, item, score])
|
||||
|
||||
col_names = ['user_id', 'sim_item', 'score']
|
||||
recall_list_df = pd.DataFrame(df_row_list, columns=col_names)
|
||||
|
||||
return recall_list_df
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 负采样函数,这里可以控制负采样时的比例, 这里给了一个默认的值
|
||||
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
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 召回数据打标签
|
||||
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_
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
def get_user_recall_item_label_df(click_trn_hist, click_val_hist, click_tst_hist,click_trn_last, click_val_last, recall_list_df):
|
||||
# 获取训练数据的召回列表
|
||||
trn_user_items_df = recall_list_df[recall_list_df['user_id'].isin(click_trn_hist['user_id'].unique())]
|
||||
# 训练数据打标签
|
||||
trn_user_item_label_df = get_rank_label_df(trn_user_items_df, click_trn_last, is_test=False)
|
||||
# 训练数据负采样
|
||||
trn_user_item_label_df = neg_sample_recall_data(trn_user_item_label_df)
|
||||
|
||||
if click_val is not None:
|
||||
val_user_items_df = recall_list_df[recall_list_df['user_id'].isin(click_val_hist['user_id'].unique())]
|
||||
val_user_item_label_df = get_rank_label_df(val_user_items_df, click_val_last, is_test=False)
|
||||
val_user_item_label_df = neg_sample_recall_data(val_user_item_label_df)
|
||||
else:
|
||||
val_user_item_label_df = None
|
||||
|
||||
# 测试数据不需要进行负采样,直接对所有的召回商品进行打-1标签
|
||||
tst_user_items_df = recall_list_df[recall_list_df['user_id'].isin(click_tst_hist['user_id'].unique())]
|
||||
tst_user_item_label_df = get_rank_label_df(tst_user_items_df, None, is_test=True)
|
||||
|
||||
return trn_user_item_label_df, val_user_item_label_df, tst_user_item_label_df
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 读取召回列表
|
||||
recall_list_dict = get_recall_list(save_path, single_recall_model='i2i_itemcf') # 这里只选择了单路召回的结果,也可以选择多路召回结果
|
||||
# 将召回数据转换成df
|
||||
recall_list_df = recall_dict_2_df(recall_list_dict)
|
||||
```
|
||||
|
||||
100%|██████████| 250000/250000 [00:12<00:00, 20689.39it/s]
|
||||
|
||||
|
||||
|
||||
```python
|
||||
# 给训练验证数据打标签,并负采样(这一部分时间比较久)
|
||||
trn_user_item_label_df, val_user_item_label_df, tst_user_item_label_df = get_user_recall_item_label_df(click_trn_hist,
|
||||
click_val_hist,
|
||||
click_tst_hist,
|
||||
click_trn_last,
|
||||
click_val_last,
|
||||
recall_list_df)
|
||||
```
|
||||
|
||||
pos_data_num: 64190 neg_data_num: 1935810 pos/neg: 0.03315924600038227
|
||||
|
||||
|
||||
|
||||
```python
|
||||
trn_user_item_label_df.label
|
||||
```
|
||||
|
||||
## 将召回数据转换成字典
|
||||
|
||||
|
||||
```python
|
||||
# 将最终的召回的df数据转换成字典的形式做排序特征
|
||||
def make_tuple_func(group_df):
|
||||
row_data = []
|
||||
for name, row_df in group_df.iterrows():
|
||||
row_data.append((row_df['sim_item'], row_df['score'], row_df['label']))
|
||||
|
||||
return row_data
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
trn_user_item_label_tuples = trn_user_item_label_df.groupby('user_id').apply(make_tuple_func).reset_index()
|
||||
trn_user_item_label_tuples_dict = dict(zip(trn_user_item_label_tuples['user_id'], trn_user_item_label_tuples[0]))
|
||||
|
||||
if val_user_item_label_df is not None:
|
||||
val_user_item_label_tuples = val_user_item_label_df.groupby('user_id').apply(make_tuple_func).reset_index()
|
||||
val_user_item_label_tuples_dict = dict(zip(val_user_item_label_tuples['user_id'], val_user_item_label_tuples[0]))
|
||||
else:
|
||||
val_user_item_label_tuples_dict = None
|
||||
|
||||
tst_user_item_label_tuples = tst_user_item_label_df.groupby('user_id').apply(make_tuple_func).reset_index()
|
||||
tst_user_item_label_tuples_dict = dict(zip(tst_user_item_label_tuples['user_id'], tst_user_item_label_tuples[0]))
|
||||
```
|
||||
|
||||
## 用户历史行为相关特征
|
||||
|
||||
对于每个用户召回的每个商品, 做特征。 具体步骤如下:
|
||||
* 对于每个用户, 获取最后点击的N个商品的item_id,
|
||||
* 对于该用户的每个召回商品, 计算与上面最后N次点击商品的相似度的和(最大, 最小,均值), 时间差特征,相似性特征,字数差特征,与该用户的相似性特征
|
||||
|
||||
|
||||
```python
|
||||
# 下面基于data做历史相关的特征
|
||||
def create_feature(users_id, recall_list, click_hist_df, articles_info, articles_emb, user_emb=None, N=1):
|
||||
"""
|
||||
基于用户的历史行为做相关特征
|
||||
:param users_id: 用户id
|
||||
:param recall_list: 对于每个用户召回的候选文章列表
|
||||
:param click_hist_df: 用户的历史点击信息
|
||||
:param articles_info: 文章信息
|
||||
:param articles_emb: 文章的embedding向量, 这个可以用item_content_emb, item_w2v_emb, item_youtube_emb
|
||||
:param user_emb: 用户的embedding向量, 这个是user_youtube_emb, 如果没有也可以不用, 但要注意如果要用的话, articles_emb就要用item_youtube_emb的形式, 这样维度才一样
|
||||
:param N: 最近的N次点击 由于testA日志里面很多用户只存在一次历史点击, 所以为了不产生空值,默认是1
|
||||
"""
|
||||
|
||||
# 建立一个二维列表保存结果, 后面要转成DataFrame
|
||||
all_user_feas = []
|
||||
i = 0
|
||||
for user_id in tqdm(users_id):
|
||||
# 该用户的最后N次点击
|
||||
hist_user_items = click_hist_df[click_hist_df['user_id']==user_id]['click_article_id'][-N:]
|
||||
|
||||
# 遍历该用户的召回列表
|
||||
for rank, (article_id, score, label) in enumerate(recall_list[user_id]):
|
||||
# 该文章建立时间, 字数
|
||||
a_create_time = articles_info[articles_info['article_id']==article_id]['created_at_ts'].values[0]
|
||||
a_words_count = articles_info[articles_info['article_id']==article_id]['words_count'].values[0]
|
||||
single_user_fea = [user_id, article_id]
|
||||
# 计算与最后点击的商品的相似度的和, 最大值和最小值, 均值
|
||||
sim_fea = []
|
||||
time_fea = []
|
||||
word_fea = []
|
||||
# 遍历用户的最后N次点击文章
|
||||
for hist_item in hist_user_items:
|
||||
b_create_time = articles_info[articles_info['article_id']==hist_item]['created_at_ts'].values[0]
|
||||
b_words_count = articles_info[articles_info['article_id']==hist_item]['words_count'].values[0]
|
||||
|
||||
sim_fea.append(np.dot(articles_emb[hist_item], articles_emb[article_id]))
|
||||
time_fea.append(abs(a_create_time-b_create_time))
|
||||
word_fea.append(abs(a_words_count-b_words_count))
|
||||
|
||||
single_user_fea.extend(sim_fea) # 相似性特征
|
||||
single_user_fea.extend(time_fea) # 时间差特征
|
||||
single_user_fea.extend(word_fea) # 字数差特征
|
||||
single_user_fea.extend([max(sim_fea), min(sim_fea), sum(sim_fea), sum(sim_fea) / len(sim_fea)]) # 相似性的统计特征
|
||||
|
||||
if user_emb: # 如果用户向量有的话, 这里计算该召回文章与用户的相似性特征
|
||||
single_user_fea.append(np.dot(user_emb[user_id], articles_emb[article_id]))
|
||||
|
||||
single_user_fea.extend([score, rank, label])
|
||||
# 加入到总的表中
|
||||
all_user_feas.append(single_user_fea)
|
||||
|
||||
# 定义列名
|
||||
id_cols = ['user_id', 'click_article_id']
|
||||
sim_cols = ['sim' + str(i) for i in range(N)]
|
||||
time_cols = ['time_diff' + str(i) for i in range(N)]
|
||||
word_cols = ['word_diff' + str(i) for i in range(N)]
|
||||
sat_cols = ['sim_max', 'sim_min', 'sim_sum', 'sim_mean']
|
||||
user_item_sim_cols = ['user_item_sim'] if user_emb else []
|
||||
user_score_rank_label = ['score', 'rank', 'label']
|
||||
cols = id_cols + sim_cols + time_cols + word_cols + sat_cols + user_item_sim_cols + user_score_rank_label
|
||||
|
||||
# 转成DataFrame
|
||||
df = pd.DataFrame( all_user_feas, columns=cols)
|
||||
|
||||
return df
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
article_info_df = get_article_info_df()
|
||||
all_click = click_trn.append(click_tst)
|
||||
item_content_emb_dict, item_w2v_emb_dict, item_youtube_emb_dict, user_youtube_emb_dict = get_embedding(save_path, all_click)
|
||||
```
|
||||
|
||||
-- Mem. usage decreased to 5.56 Mb (50.0% reduction),time spend:0.00 min
|
||||
|
||||
|
||||
|
||||
```python
|
||||
# 获取训练验证及测试数据中召回列文章相关特征
|
||||
trn_user_item_feats_df = create_feature(trn_user_item_label_tuples_dict.keys(), trn_user_item_label_tuples_dict, \
|
||||
click_trn_hist, article_info_df, item_content_emb_dict)
|
||||
|
||||
if val_user_item_label_tuples_dict is not None:
|
||||
val_user_item_feats_df = create_feature(val_user_item_label_tuples_dict.keys(), val_user_item_label_tuples_dict, \
|
||||
click_val_hist, article_info_df, item_content_emb_dict)
|
||||
else:
|
||||
val_user_item_feats_df = None
|
||||
|
||||
tst_user_item_feats_df = create_feature(tst_user_item_label_tuples_dict.keys(), tst_user_item_label_tuples_dict, \
|
||||
click_tst_hist, article_info_df, item_content_emb_dict)
|
||||
```
|
||||
|
||||
100%|██████████| 200000/200000 [50:16<00:00, 66.31it/s]
|
||||
100%|██████████| 50000/50000 [1:07:21<00:00, 12.37it/s]
|
||||
|
||||
|
||||
|
||||
```python
|
||||
# 保存一份省的每次都要重新跑,每次跑的时间都比较长
|
||||
trn_user_item_feats_df.to_csv(save_path + 'trn_user_item_feats_df.csv', index=False)
|
||||
|
||||
if val_user_item_feats_df is not None:
|
||||
val_user_item_feats_df.to_csv(save_path + 'val_user_item_feats_df.csv', index=False)
|
||||
|
||||
tst_user_item_feats_df.to_csv(save_path + 'tst_user_item_feats_df.csv', index=False)
|
||||
```
|
||||
|
||||
## 用户和文章特征
|
||||
|
||||
### 用户相关特征
|
||||
|
||||
这一块,正式进行特征工程,既要拼接上已有的特征, 也会做更多的特征出来,我们来梳理一下已有的特征和可构造特征:
|
||||
1. 文章自身的特征, 文章字数,文章创建时间, 文章的embedding (articles表中)
|
||||
2. 用户点击环境特征, 那些设备的特征(这个在df中)
|
||||
3. 对于用户和商品还可以构造的特征:
|
||||
* 基于用户的点击文章次数和点击时间构造可以表现用户活跃度的特征
|
||||
* 基于文章被点击次数和时间构造可以反映文章热度的特征
|
||||
* 用户的时间统计特征: 根据其点击的历史文章列表的点击时间和文章的创建时间做统计特征,比如求均值, 这个可以反映用户对于文章时效的偏好
|
||||
* 用户的主题爱好特征, 对于用户点击的历史文章主题进行一个统计, 然后对于当前文章看看是否属于用户已经点击过的主题
|
||||
* 用户的字数爱好特征, 对于用户点击的历史文章的字数统计, 求一个均值
|
||||
|
||||
|
||||
```python
|
||||
click_tst.head()
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 读取文章特征
|
||||
articles = pd.read_csv(data_path+'articles.csv')
|
||||
articles = reduce_mem(articles)
|
||||
|
||||
# 日志数据,就是前面的所有数据
|
||||
if click_val is not None:
|
||||
all_data = click_trn.append(click_val)
|
||||
all_data = click_trn.append(click_tst)
|
||||
all_data = reduce_mem(all_data)
|
||||
|
||||
# 拼上文章信息
|
||||
all_data = all_data.merge(articles, left_on='click_article_id', right_on='article_id')
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
all_data.shape
|
||||
```
|
||||
|
||||
#### 分析一下点击时间和点击文章的次数,区分用户活跃度
|
||||
如果某个用户点击文章之间的时间间隔比较小, 同时点击的文章次数很多的话, 那么我们认为这种用户一般就是活跃用户, 当然衡量用户活跃度的方式可能多种多样, 这里我们只提供其中一种,我们写一个函数, 得到可以衡量用户活跃度的特征,逻辑如下:
|
||||
1. 首先根据用户user_id分组, 对于每个用户,计算点击文章的次数, 两两点击文章时间间隔的均值
|
||||
2. 把点击次数取倒数和时间间隔的均值统一归一化,然后两者相加合并,该值越小, 说明用户越活跃
|
||||
3. 注意, 上面两两点击文章的时间间隔均值, 会出现如果用户只点击了一次的情况,这时候时间间隔均值那里会出现空值, 对于这种情况最后特征那里给个大数进行区分
|
||||
|
||||
这个的衡量标准就是先把点击的次数取到数然后归一化, 然后点击的时间差归一化, 然后两者相加进行合并, 该值越小, 说明被点击的次数越多, 且间隔时间短。
|
||||
|
||||
|
||||
```python
|
||||
def active_level(all_data, cols):
|
||||
"""
|
||||
制作区分用户活跃度的特征
|
||||
:param all_data: 数据集
|
||||
:param cols: 用到的特征列
|
||||
"""
|
||||
data = all_data[cols]
|
||||
data.sort_values(['user_id', 'click_timestamp'], inplace=True)
|
||||
user_act = pd.DataFrame(data.groupby('user_id', as_index=False)[['click_article_id', 'click_timestamp']].\
|
||||
agg({'click_article_id':np.size, 'click_timestamp': {list}}).values, columns=['user_id', 'click_size', 'click_timestamp'])
|
||||
|
||||
# 计算时间间隔的均值
|
||||
def time_diff_mean(l):
|
||||
if len(l) == 1:
|
||||
return 1
|
||||
else:
|
||||
return np.mean([j-i for i, j in list(zip(l[:-1], l[1:]))])
|
||||
|
||||
user_act['time_diff_mean'] = user_act['click_timestamp'].apply(lambda x: time_diff_mean(x))
|
||||
|
||||
# 点击次数取倒数
|
||||
user_act['click_size'] = 1 / user_act['click_size']
|
||||
|
||||
# 两者归一化
|
||||
user_act['click_size'] = (user_act['click_size'] - user_act['click_size'].min()) / (user_act['click_size'].max() - user_act['click_size'].min())
|
||||
user_act['time_diff_mean'] = (user_act['time_diff_mean'] - user_act['time_diff_mean'].min()) / (user_act['time_diff_mean'].max() - user_act['time_diff_mean'].min())
|
||||
user_act['active_level'] = user_act['click_size'] + user_act['time_diff_mean']
|
||||
|
||||
user_act['user_id'] = user_act['user_id'].astype('int')
|
||||
del user_act['click_timestamp']
|
||||
|
||||
return user_act
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
user_act_fea = active_level(all_data, ['user_id', 'click_article_id', 'click_timestamp'])
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
user_act_fea.head()
|
||||
```
|
||||
|
||||
#### 分析一下点击时间和被点击文章的次数, 衡量文章热度特征
|
||||
和上面同样的思路, 如果一篇文章在很短的时间间隔之内被点击了很多次, 说明文章比较热门,实现的逻辑和上面的基本一致, 只不过这里是按照点击的文章进行分组:
|
||||
1. 根据文章进行分组, 对于每篇文章的用户, 计算点击的时间间隔
|
||||
2. 将用户的数量取倒数, 然后用户的数量和时间间隔归一化, 然后相加得到热度特征, 该值越小, 说明被点击的次数越大且时间间隔越短, 文章比较热
|
||||
|
||||
当然, 这只是给出一种判断文章热度的一种方法, 这里大家也可以头脑风暴一下
|
||||
|
||||
|
||||
```python
|
||||
def hot_level(all_data, cols):
|
||||
"""
|
||||
制作衡量文章热度的特征
|
||||
:param all_data: 数据集
|
||||
:param cols: 用到的特征列
|
||||
"""
|
||||
data = all_data[cols]
|
||||
data.sort_values(['click_article_id', 'click_timestamp'], inplace=True)
|
||||
article_hot = pd.DataFrame(data.groupby('click_article_id', as_index=False)[['user_id', 'click_timestamp']].\
|
||||
agg({'user_id':np.size, 'click_timestamp': {list}}).values, columns=['click_article_id', 'user_num', 'click_timestamp'])
|
||||
|
||||
# 计算被点击时间间隔的均值
|
||||
def time_diff_mean(l):
|
||||
if len(l) == 1:
|
||||
return 1
|
||||
else:
|
||||
return np.mean([j-i for i, j in list(zip(l[:-1], l[1:]))])
|
||||
|
||||
article_hot['time_diff_mean'] = article_hot['click_timestamp'].apply(lambda x: time_diff_mean(x))
|
||||
|
||||
# 点击次数取倒数
|
||||
article_hot['user_num'] = 1 / article_hot['user_num']
|
||||
|
||||
# 两者归一化
|
||||
article_hot['user_num'] = (article_hot['user_num'] - article_hot['user_num'].min()) / (article_hot['user_num'].max() - article_hot['user_num'].min())
|
||||
article_hot['time_diff_mean'] = (article_hot['time_diff_mean'] - article_hot['time_diff_mean'].min()) / (article_hot['time_diff_mean'].max() - article_hot['time_diff_mean'].min())
|
||||
article_hot['hot_level'] = article_hot['user_num'] + article_hot['time_diff_mean']
|
||||
|
||||
article_hot['click_article_id'] = article_hot['click_article_id'].astype('int')
|
||||
|
||||
del article_hot['click_timestamp']
|
||||
|
||||
return article_hot
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
article_hot_fea = hot_level(all_data, ['user_id', 'click_article_id', 'click_timestamp'])
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
article_hot_fea.head()
|
||||
```
|
||||
|
||||
#### 用户的系列习惯
|
||||
这个基于原来的日志表做一个类似于article的那种DataFrame, 存放用户特有的信息, 主要包括点击习惯, 爱好特征之类的
|
||||
* 用户的设备习惯, 这里取最常用的设备(众数)
|
||||
* 用户的时间习惯: 根据其点击过得历史文章的时间来做一个统计(这个感觉最好是把时间戳里的时间特征的h特征提出来,看看用户习惯一天的啥时候点击文章), 但这里先用转换的时间吧, 求个均值
|
||||
* 用户的爱好特征, 对于用户点击的历史文章主题进行用户的爱好判别, 更偏向于哪几个主题, 这个最好是multi-hot进行编码, 先试试行不
|
||||
* 用户文章的字数差特征, 用户的爱好文章的字数习惯
|
||||
|
||||
这些就是对用户进行分组, 然后统计即可
|
||||
|
||||
#### 用户的设备习惯
|
||||
|
||||
|
||||
```python
|
||||
def device_fea(all_data, cols):
|
||||
"""
|
||||
制作用户的设备特征
|
||||
:param all_data: 数据集
|
||||
:param cols: 用到的特征列
|
||||
"""
|
||||
user_device_info = all_data[cols]
|
||||
|
||||
# 用众数来表示每个用户的设备信息
|
||||
user_device_info = user_device_info.groupby('user_id').agg(lambda x: x.value_counts().index[0]).reset_index()
|
||||
|
||||
return user_device_info
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 设备特征(这里时间会比较长)
|
||||
device_cols = ['user_id', 'click_environment', 'click_deviceGroup', 'click_os', 'click_country', 'click_region', 'click_referrer_type']
|
||||
user_device_info = device_fea(all_data, device_cols)
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
user_device_info.head()
|
||||
```
|
||||
|
||||
#### 用户的时间习惯
|
||||
|
||||
|
||||
```python
|
||||
def user_time_hob_fea(all_data, cols):
|
||||
"""
|
||||
制作用户的时间习惯特征
|
||||
:param all_data: 数据集
|
||||
:param cols: 用到的特征列
|
||||
"""
|
||||
user_time_hob_info = all_data[cols]
|
||||
|
||||
# 先把时间戳进行归一化
|
||||
mm = MinMaxScaler()
|
||||
user_time_hob_info['click_timestamp'] = mm.fit_transform(user_time_hob_info[['click_timestamp']])
|
||||
user_time_hob_info['created_at_ts'] = mm.fit_transform(user_time_hob_info[['created_at_ts']])
|
||||
|
||||
user_time_hob_info = user_time_hob_info.groupby('user_id').agg('mean').reset_index()
|
||||
|
||||
user_time_hob_info.rename(columns={'click_timestamp': 'user_time_hob1', 'created_at_ts': 'user_time_hob2'}, inplace=True)
|
||||
return user_time_hob_info
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
user_time_hob_cols = ['user_id', 'click_timestamp', 'created_at_ts']
|
||||
user_time_hob_info = user_time_hob_fea(all_data, user_time_hob_cols)
|
||||
```
|
||||
|
||||
#### 用户的主题爱好
|
||||
这里先把用户点击的文章属于的主题转成一个列表, 后面再总的汇总的时候单独制作一个特征, 就是文章的主题如果属于这里面, 就是1, 否则就是0。
|
||||
|
||||
|
||||
```python
|
||||
def user_cat_hob_fea(all_data, cols):
|
||||
"""
|
||||
用户的主题爱好
|
||||
:param all_data: 数据集
|
||||
:param cols: 用到的特征列
|
||||
"""
|
||||
user_category_hob_info = all_data[cols]
|
||||
user_category_hob_info = user_category_hob_info.groupby('user_id').agg({list}).reset_index()
|
||||
|
||||
user_cat_hob_info = pd.DataFrame()
|
||||
user_cat_hob_info['user_id'] = user_category_hob_info['user_id']
|
||||
user_cat_hob_info['cate_list'] = user_category_hob_info['category_id']
|
||||
|
||||
return user_cat_hob_info
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
user_category_hob_cols = ['user_id', 'category_id']
|
||||
user_cat_hob_info = user_cat_hob_fea(all_data, user_category_hob_cols)
|
||||
```
|
||||
|
||||
#### 用户的字数偏好特征
|
||||
|
||||
|
||||
```python
|
||||
user_wcou_info = all_data.groupby('user_id')['words_count'].agg('mean').reset_index()
|
||||
user_wcou_info.rename(columns={'words_count': 'words_hbo'}, inplace=True)
|
||||
```
|
||||
|
||||
#### 用户的信息特征合并保存
|
||||
|
||||
|
||||
```python
|
||||
# 所有表进行合并
|
||||
user_info = pd.merge(user_act_fea, user_device_info, on='user_id')
|
||||
user_info = user_info.merge(user_time_hob_info, on='user_id')
|
||||
user_info = user_info.merge(user_cat_hob_info, on='user_id')
|
||||
user_info = user_info.merge(user_wcou_info, on='user_id')
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 这样用户特征以后就可以直接读取了
|
||||
user_info.to_csv(save_path + 'user_info.csv', index=False)
|
||||
```
|
||||
|
||||
### 用户特征直接读入
|
||||
如果前面关于用户的特征工程已经给做完了,后面可以直接读取
|
||||
|
||||
|
||||
```python
|
||||
# 把用户信息直接读入进来
|
||||
user_info = pd.read_csv(save_path + 'user_info.csv')
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
if os.path.exists(save_path + 'trn_user_item_feats_df.csv'):
|
||||
trn_user_item_feats_df = pd.read_csv(save_path + 'trn_user_item_feats_df.csv')
|
||||
|
||||
if os.path.exists(save_path + 'tst_user_item_feats_df.csv'):
|
||||
tst_user_item_feats_df = pd.read_csv(save_path + 'tst_user_item_feats_df.csv')
|
||||
|
||||
if os.path.exists(save_path + 'val_user_item_feats_df.csv'):
|
||||
val_user_item_feats_df = pd.read_csv(save_path + 'val_user_item_feats_df.csv')
|
||||
else:
|
||||
val_user_item_feats_df = None
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 拼上用户特征
|
||||
# 下面是线下验证的
|
||||
trn_user_item_feats_df = trn_user_item_feats_df.merge(user_info, on='user_id', how='left')
|
||||
|
||||
if val_user_item_feats_df is not None:
|
||||
val_user_item_feats_df = val_user_item_feats_df.merge(user_info, on='user_id', how='left')
|
||||
else:
|
||||
val_user_item_feats_df = None
|
||||
|
||||
tst_user_item_feats_df = tst_user_item_feats_df.merge(user_info, on='user_id',how='left')
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
trn_user_item_feats_df.columns
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
Index(['user_id', 'click_article_id', 'sim0', 'time_diff0', 'word_diff0',
|
||||
'sim_max', 'sim_min', 'sim_sum', 'sim_mean', 'score', 'rank', 'label',
|
||||
'click_size', 'time_diff_mean', 'active_level', 'click_environment',
|
||||
'click_deviceGroup', 'click_os', 'click_country', 'click_region',
|
||||
'click_referrer_type', 'user_time_hob1', 'user_time_hob2', 'cate_list',
|
||||
'words_hbo'],
|
||||
dtype='object')
|
||||
|
||||
|
||||
|
||||
### 文章的特征直接读入
|
||||
|
||||
|
||||
```python
|
||||
articles = pd.read_csv(data_path+'articles.csv')
|
||||
articles = reduce_mem(articles)
|
||||
```
|
||||
|
||||
-- Mem. usage decreased to 5.56 Mb (50.0% reduction),time spend:0.00 min
|
||||
|
||||
|
||||
|
||||
```python
|
||||
# 拼上文章特征
|
||||
trn_user_item_feats_df = trn_user_item_feats_df.merge(articles, left_on='click_article_id', right_on='article_id')
|
||||
|
||||
if val_user_item_feats_df is not None:
|
||||
val_user_item_feats_df = val_user_item_feats_df.merge(articles, left_on='click_article_id', right_on='article_id')
|
||||
else:
|
||||
val_user_item_feats_df = None
|
||||
|
||||
tst_user_item_feats_df = tst_user_item_feats_df.merge(articles, left_on='click_article_id', right_on='article_id')
|
||||
```
|
||||
|
||||
### 召回文章的主题是否在用户的爱好里面
|
||||
|
||||
|
||||
```python
|
||||
trn_user_item_feats_df['is_cat_hab'] = trn_user_item_feats_df.apply(lambda x: 1 if x.category_id in set(x.cate_list) else 0, axis=1)
|
||||
if val_user_item_feats_df is not None:
|
||||
val_user_item_feats_df['is_cat_hab'] = val_user_item_feats_df.apply(lambda x: 1 if x.category_id in set(x.cate_list) else 0, axis=1)
|
||||
else:
|
||||
val_user_item_feats_df = None
|
||||
tst_user_item_feats_df['is_cat_hab'] = tst_user_item_feats_df.apply(lambda x: 1 if x.category_id in set(x.cate_list) else 0, axis=1)
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 线下验证
|
||||
del trn_user_item_feats_df['cate_list']
|
||||
|
||||
if val_user_item_feats_df is not None:
|
||||
del val_user_item_feats_df['cate_list']
|
||||
else:
|
||||
val_user_item_feats_df = None
|
||||
|
||||
del tst_user_item_feats_df['cate_list']
|
||||
|
||||
del trn_user_item_feats_df['article_id']
|
||||
|
||||
if val_user_item_feats_df is not None:
|
||||
del val_user_item_feats_df['article_id']
|
||||
else:
|
||||
val_user_item_feats_df = None
|
||||
|
||||
del tst_user_item_feats_df['article_id']
|
||||
```
|
||||
|
||||
## 保存特征
|
||||
|
||||
|
||||
```python
|
||||
# 训练验证特征
|
||||
trn_user_item_feats_df.to_csv(save_path + 'trn_user_item_feats_df.csv', index=False)
|
||||
if val_user_item_feats_df is not None:
|
||||
val_user_item_feats_df.to_csv(save_path + 'val_user_item_feats_df.csv', index=False)
|
||||
tst_user_item_feats_df.to_csv(save_path + 'tst_user_item_feats_df.csv', index=False)
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 总结
|
||||
|
||||
特征工程和数据清洗转换是比赛中至关重要的一块, 因为**数据和特征决定了机器学习的上限,而算法和模型只是逼近这个上限而已**,所以特征工程的好坏往往决定着最后的结果,**特征工程**可以一步增强数据的表达能力,通过构造新特征,我们可以挖掘出数据的更多信息,使得数据的表达能力进一步放大。 在本节内容中,我们主要是先通过制作特征和标签把预测问题转成了监督学习问题,然后围绕着用户画像和文章画像进行一系列特征的制作, 此外,为了保证正负样本的数据均衡,我们还学习了负采样就技术等。当然本节内容只是对构造特征提供了一些思路,也请学习者们在学习过程中开启头脑风暴,尝试更多的构造特征的方法,也欢迎我们一块探讨和交流。
|
||||
|
||||
**关于Datawhale:** Datawhale是一个专注于数据科学与AI领域的开源组织,汇集了众多领域院校和知名企业的优秀学习者,聚合了一群有开源精神和探索精神的团队成员。Datawhale 以“for the learner,和学习者一起成长”为愿景,鼓励真实地展现自我、开放包容、互信互助、敢于试错和勇于担当。同时 Datawhale 用开源的理念去探索开源内容、开源学习和开源方案,赋能人才培养,助力人才成长,建立起人与人,人与知识,人与企业和人与未来的联结。 本次数据挖掘路径学习,专题知识将在天池分享,详情可关注Datawhale:
|
||||
|
||||

|
||||
|
||||
@@ -0,0 +1,952 @@
|
||||
# 排序模型
|
||||
|
||||
通过召回的操作, 我们已经进行了问题规模的缩减, 对于每个用户, 选择出了N篇文章作为了候选集,并基于召回的候选集构建了与用户历史相关的特征,以及用户本身的属性特征,文章本省的属性特征,以及用户与文章之间的特征,下面就是使用机器学习模型来对构造好的特征进行学习,然后对测试集进行预测,得到测试集中的每个候选集用户点击的概率,返回点击概率最大的topk个文章,作为最终的结果。
|
||||
|
||||
排序阶段选择了三个比较有代表性的排序模型,它们分别是:
|
||||
|
||||
1. LGB的排序模型
|
||||
2. LGB的分类模型
|
||||
3. 深度学习的分类模型DIN
|
||||
|
||||
得到了最终的排序模型输出的结果之后,还选择了两种比较经典的模型集成的方法:
|
||||
|
||||
1. 输出结果加权融合
|
||||
2. Staking(将模型的输出结果再使用一个简单模型进行预测)
|
||||
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pickle
|
||||
from tqdm import tqdm
|
||||
import gc, os
|
||||
import time
|
||||
from datetime import datetime
|
||||
import lightgbm as lgb
|
||||
from sklearn.preprocessing import MinMaxScaler
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
```
|
||||
|
||||
## 读取排序特征
|
||||
|
||||
|
||||
```python
|
||||
data_path = './data_raw/'
|
||||
save_path = './temp_results/'
|
||||
offline = False
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 重新读取数据的时候,发现click_article_id是一个浮点数,所以将其转换成int类型
|
||||
trn_user_item_feats_df = pd.read_csv(save_path + 'trn_user_item_feats_df.csv')
|
||||
trn_user_item_feats_df['click_article_id'] = trn_user_item_feats_df['click_article_id'].astype(int)
|
||||
|
||||
if offline:
|
||||
val_user_item_feats_df = pd.read_csv(save_path + 'val_user_item_feats_df.csv')
|
||||
val_user_item_feats_df['click_article_id'] = val_user_item_feats_df['click_article_id'].astype(int)
|
||||
else:
|
||||
val_user_item_feats_df = None
|
||||
|
||||
tst_user_item_feats_df = pd.read_csv(save_path + 'tst_user_item_feats_df.csv')
|
||||
tst_user_item_feats_df['click_article_id'] = tst_user_item_feats_df['click_article_id'].astype(int)
|
||||
|
||||
# 做特征的时候为了方便,给测试集也打上了一个无效的标签,这里直接删掉就行
|
||||
del tst_user_item_feats_df['label']
|
||||
```
|
||||
|
||||
## 返回排序后的结果
|
||||
|
||||
|
||||
```python
|
||||
def submit(recall_df, topk=5, model_name=None):
|
||||
recall_df = recall_df.sort_values(by=['user_id', 'pred_score'])
|
||||
recall_df['rank'] = recall_df.groupby(['user_id'])['pred_score'].rank(ascending=False, method='first')
|
||||
|
||||
# 判断是不是每个用户都有5篇文章及以上
|
||||
tmp = recall_df.groupby('user_id').apply(lambda x: x['rank'].max())
|
||||
assert tmp.min() >= topk
|
||||
|
||||
del recall_df['pred_score']
|
||||
submit = recall_df[recall_df['rank'] <= topk].set_index(['user_id', 'rank']).unstack(-1).reset_index()
|
||||
|
||||
submit.columns = [int(col) if isinstance(col, int) else col for col in submit.columns.droplevel(0)]
|
||||
# 按照提交格式定义列名
|
||||
submit = submit.rename(columns={'': 'user_id', 1: 'article_1', 2: 'article_2',
|
||||
3: 'article_3', 4: 'article_4', 5: 'article_5'})
|
||||
|
||||
save_name = save_path + model_name + '_' + datetime.today().strftime('%m-%d') + '.csv'
|
||||
submit.to_csv(save_name, index=False, header=True)
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 排序结果归一化
|
||||
def norm_sim(sim_df, weight=0.0):
|
||||
# print(sim_df.head())
|
||||
min_sim = sim_df.min()
|
||||
max_sim = sim_df.max()
|
||||
if max_sim == min_sim:
|
||||
sim_df = sim_df.apply(lambda sim: 1.0)
|
||||
else:
|
||||
sim_df = sim_df.apply(lambda sim: 1.0 * (sim - min_sim) / (max_sim - min_sim))
|
||||
|
||||
sim_df = sim_df.apply(lambda sim: sim + weight) # plus one
|
||||
return sim_df
|
||||
```
|
||||
|
||||
## LGB排序模型
|
||||
|
||||
|
||||
```python
|
||||
# 防止中间出错之后重新读取数据
|
||||
trn_user_item_feats_df_rank_model = trn_user_item_feats_df.copy()
|
||||
|
||||
if offline:
|
||||
val_user_item_feats_df_rank_model = val_user_item_feats_df.copy()
|
||||
|
||||
tst_user_item_feats_df_rank_model = tst_user_item_feats_df.copy()
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 定义特征列
|
||||
lgb_cols = ['sim0', 'time_diff0', 'word_diff0','sim_max', 'sim_min', 'sim_sum',
|
||||
'sim_mean', 'score','click_size', 'time_diff_mean', 'active_level',
|
||||
'click_environment','click_deviceGroup', 'click_os', 'click_country',
|
||||
'click_region','click_referrer_type', 'user_time_hob1', 'user_time_hob2',
|
||||
'words_hbo', 'category_id', 'created_at_ts','words_count']
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 排序模型分组
|
||||
trn_user_item_feats_df_rank_model.sort_values(by=['user_id'], inplace=True)
|
||||
g_train = trn_user_item_feats_df_rank_model.groupby(['user_id'], as_index=False).count()["label"].values
|
||||
|
||||
if offline:
|
||||
val_user_item_feats_df_rank_model.sort_values(by=['user_id'], inplace=True)
|
||||
g_val = val_user_item_feats_df_rank_model.groupby(['user_id'], as_index=False).count()["label"].values
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 排序模型定义
|
||||
lgb_ranker = lgb.LGBMRanker(boosting_type='gbdt', num_leaves=31, reg_alpha=0.0, reg_lambda=1,
|
||||
max_depth=-1, n_estimators=100, subsample=0.7, colsample_bytree=0.7, subsample_freq=1,
|
||||
learning_rate=0.01, min_child_weight=50, random_state=2018, n_jobs= 16)
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 排序模型训练
|
||||
if offline:
|
||||
lgb_ranker.fit(trn_user_item_feats_df_rank_model[lgb_cols], trn_user_item_feats_df_rank_model['label'], group=g_train,
|
||||
eval_set=[(val_user_item_feats_df_rank_model[lgb_cols], val_user_item_feats_df_rank_model['label'])],
|
||||
eval_group= [g_val], eval_at=[1, 2, 3, 4, 5], eval_metric=['ndcg', ], early_stopping_rounds=50, )
|
||||
else:
|
||||
lgb_ranker.fit(trn_user_item_feats_df[lgb_cols], trn_user_item_feats_df['label'], group=g_train)
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 模型预测
|
||||
tst_user_item_feats_df['pred_score'] = lgb_ranker.predict(tst_user_item_feats_df[lgb_cols], num_iteration=lgb_ranker.best_iteration_)
|
||||
|
||||
# 将这里的排序结果保存一份,用户后面的模型融合
|
||||
tst_user_item_feats_df[['user_id', 'click_article_id', 'pred_score']].to_csv(save_path + 'lgb_ranker_score.csv', index=False)
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 预测结果重新排序, 及生成提交结果
|
||||
rank_results = tst_user_item_feats_df[['user_id', 'click_article_id', 'pred_score']]
|
||||
rank_results['click_article_id'] = rank_results['click_article_id'].astype(int)
|
||||
submit(rank_results, topk=5, model_name='lgb_ranker')
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 五折交叉验证,这里的五折交叉是以用户为目标进行五折划分
|
||||
# 这一部分与前面的单独训练和验证是分开的
|
||||
def get_kfold_users(trn_df, n=5):
|
||||
user_ids = trn_df['user_id'].unique()
|
||||
user_set = [user_ids[i::n] for i in range(n)]
|
||||
return user_set
|
||||
|
||||
k_fold = 5
|
||||
trn_df = trn_user_item_feats_df_rank_model
|
||||
user_set = get_kfold_users(trn_df, n=k_fold)
|
||||
|
||||
score_list = []
|
||||
score_df = trn_df[['user_id', 'click_article_id','label']]
|
||||
sub_preds = np.zeros(tst_user_item_feats_df_rank_model.shape[0])
|
||||
|
||||
# 五折交叉验证,并将中间结果保存用于staking
|
||||
for n_fold, valid_user in enumerate(user_set):
|
||||
train_idx = trn_df[~trn_df['user_id'].isin(valid_user)] # add slide user
|
||||
valid_idx = trn_df[trn_df['user_id'].isin(valid_user)]
|
||||
|
||||
# 训练集与验证集的用户分组
|
||||
train_idx.sort_values(by=['user_id'], inplace=True)
|
||||
g_train = train_idx.groupby(['user_id'], as_index=False).count()["label"].values
|
||||
|
||||
valid_idx.sort_values(by=['user_id'], inplace=True)
|
||||
g_val = valid_idx.groupby(['user_id'], as_index=False).count()["label"].values
|
||||
|
||||
# 定义模型
|
||||
lgb_ranker = lgb.LGBMRanker(boosting_type='gbdt', num_leaves=31, reg_alpha=0.0, reg_lambda=1,
|
||||
max_depth=-1, n_estimators=100, subsample=0.7, colsample_bytree=0.7, subsample_freq=1,
|
||||
learning_rate=0.01, min_child_weight=50, random_state=2018, n_jobs= 16)
|
||||
# 训练模型
|
||||
lgb_ranker.fit(train_idx[lgb_cols], train_idx['label'], group=g_train,
|
||||
eval_set=[(valid_idx[lgb_cols], valid_idx['label'])], eval_group= [g_val],
|
||||
eval_at=[1, 2, 3, 4, 5], eval_metric=['ndcg', ], early_stopping_rounds=50, )
|
||||
|
||||
# 预测验证集结果
|
||||
valid_idx['pred_score'] = lgb_ranker.predict(valid_idx[lgb_cols], num_iteration=lgb_ranker.best_iteration_)
|
||||
|
||||
# 对输出结果进行归一化
|
||||
valid_idx['pred_score'] = valid_idx[['pred_score']].transform(lambda x: norm_sim(x))
|
||||
|
||||
valid_idx.sort_values(by=['user_id', 'pred_score'])
|
||||
valid_idx['pred_rank'] = valid_idx.groupby(['user_id'])['pred_score'].rank(ascending=False, method='first')
|
||||
|
||||
# 将验证集的预测结果放到一个列表中,后面进行拼接
|
||||
score_list.append(valid_idx[['user_id', 'click_article_id', 'pred_score', 'pred_rank']])
|
||||
|
||||
# 如果是线上测试,需要计算每次交叉验证的结果相加,最后求平均
|
||||
if not offline:
|
||||
sub_preds += lgb_ranker.predict(tst_user_item_feats_df_rank_model[lgb_cols], lgb_ranker.best_iteration_)
|
||||
|
||||
score_df_ = pd.concat(score_list, axis=0)
|
||||
score_df = score_df.merge(score_df_, how='left', on=['user_id', 'click_article_id'])
|
||||
# 保存训练集交叉验证产生的新特征
|
||||
score_df[['user_id', 'click_article_id', 'pred_score', 'pred_rank', 'label']].to_csv(save_path + 'trn_lgb_ranker_feats.csv', index=False)
|
||||
|
||||
# 测试集的预测结果,多次交叉验证求平均,将预测的score和对应的rank特征保存,可以用于后面的staking,这里还可以构造其他更多的特征
|
||||
tst_user_item_feats_df_rank_model['pred_score'] = sub_preds / k_fold
|
||||
tst_user_item_feats_df_rank_model['pred_score'] = tst_user_item_feats_df_rank_model['pred_score'].transform(lambda x: norm_sim(x))
|
||||
tst_user_item_feats_df_rank_model.sort_values(by=['user_id', 'pred_score'])
|
||||
tst_user_item_feats_df_rank_model['pred_rank'] = tst_user_item_feats_df_rank_model.groupby(['user_id'])['pred_score'].rank(ascending=False, method='first')
|
||||
|
||||
# 保存测试集交叉验证的新特征
|
||||
tst_user_item_feats_df_rank_model[['user_id', 'click_article_id', 'pred_score', 'pred_rank']].to_csv(save_path + 'tst_lgb_ranker_feats.csv', index=False)
|
||||
```
|
||||
|
||||
|
||||
|
||||
```python
|
||||
# 预测结果重新排序, 及生成提交结果
|
||||
# 单模型生成提交结果
|
||||
rank_results = tst_user_item_feats_df_rank_model[['user_id', 'click_article_id', 'pred_score']]
|
||||
rank_results['click_article_id'] = rank_results['click_article_id'].astype(int)
|
||||
submit(rank_results, topk=5, model_name='lgb_ranker')
|
||||
```
|
||||
|
||||
## LGB分类模型
|
||||
|
||||
|
||||
```python
|
||||
# 模型及参数的定义
|
||||
lgb_Classfication = lgb.LGBMClassifier(boosting_type='gbdt', num_leaves=31, reg_alpha=0.0, reg_lambda=1,
|
||||
max_depth=-1, n_estimators=500, subsample=0.7, colsample_bytree=0.7, subsample_freq=1,
|
||||
learning_rate=0.01, min_child_weight=50, random_state=2018, n_jobs= 16, verbose=10)
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 模型训练
|
||||
if offline:
|
||||
lgb_Classfication.fit(trn_user_item_feats_df_rank_model[lgb_cols], trn_user_item_feats_df_rank_model['label'],
|
||||
eval_set=[(val_user_item_feats_df_rank_model[lgb_cols], val_user_item_feats_df_rank_model['label'])],
|
||||
eval_metric=['auc', ],early_stopping_rounds=50, )
|
||||
else:
|
||||
lgb_Classfication.fit(trn_user_item_feats_df_rank_model[lgb_cols], trn_user_item_feats_df_rank_model['label'])
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 模型预测
|
||||
tst_user_item_feats_df['pred_score'] = lgb_Classfication.predict_proba(tst_user_item_feats_df[lgb_cols])[:,1]
|
||||
|
||||
# 将这里的排序结果保存一份,用户后面的模型融合
|
||||
tst_user_item_feats_df[['user_id', 'click_article_id', 'pred_score']].to_csv(save_path + 'lgb_cls_score.csv', index=False)
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 预测结果重新排序, 及生成提交结果
|
||||
rank_results = tst_user_item_feats_df[['user_id', 'click_article_id', 'pred_score']]
|
||||
rank_results['click_article_id'] = rank_results['click_article_id'].astype(int)
|
||||
submit(rank_results, topk=5, model_name='lgb_cls')
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 五折交叉验证,这里的五折交叉是以用户为目标进行五折划分
|
||||
# 这一部分与前面的单独训练和验证是分开的
|
||||
def get_kfold_users(trn_df, n=5):
|
||||
user_ids = trn_df['user_id'].unique()
|
||||
user_set = [user_ids[i::n] for i in range(n)]
|
||||
return user_set
|
||||
|
||||
k_fold = 5
|
||||
trn_df = trn_user_item_feats_df_rank_model
|
||||
user_set = get_kfold_users(trn_df, n=k_fold)
|
||||
|
||||
score_list = []
|
||||
score_df = trn_df[['user_id', 'click_article_id', 'label']]
|
||||
sub_preds = np.zeros(tst_user_item_feats_df_rank_model.shape[0])
|
||||
|
||||
# 五折交叉验证,并将中间结果保存用于staking
|
||||
for n_fold, valid_user in enumerate(user_set):
|
||||
train_idx = trn_df[~trn_df['user_id'].isin(valid_user)] # add slide user
|
||||
valid_idx = trn_df[trn_df['user_id'].isin(valid_user)]
|
||||
|
||||
# 模型及参数的定义
|
||||
lgb_Classfication = lgb.LGBMClassifier(boosting_type='gbdt', num_leaves=31, reg_alpha=0.0, reg_lambda=1,
|
||||
max_depth=-1, n_estimators=100, subsample=0.7, colsample_bytree=0.7, subsample_freq=1,
|
||||
learning_rate=0.01, min_child_weight=50, random_state=2018, n_jobs= 16, verbose=10)
|
||||
# 训练模型
|
||||
lgb_Classfication.fit(train_idx[lgb_cols], train_idx['label'],eval_set=[(valid_idx[lgb_cols], valid_idx['label'])],
|
||||
eval_metric=['auc', ],early_stopping_rounds=50, )
|
||||
|
||||
# 预测验证集结果
|
||||
valid_idx['pred_score'] = lgb_Classfication.predict_proba(valid_idx[lgb_cols],
|
||||
num_iteration=lgb_Classfication.best_iteration_)[:,1]
|
||||
|
||||
# 对输出结果进行归一化 分类模型输出的值本身就是一个概率值不需要进行归一化
|
||||
# valid_idx['pred_score'] = valid_idx[['pred_score']].transform(lambda x: norm_sim(x))
|
||||
|
||||
valid_idx.sort_values(by=['user_id', 'pred_score'])
|
||||
valid_idx['pred_rank'] = valid_idx.groupby(['user_id'])['pred_score'].rank(ascending=False, method='first')
|
||||
|
||||
# 将验证集的预测结果放到一个列表中,后面进行拼接
|
||||
score_list.append(valid_idx[['user_id', 'click_article_id', 'pred_score', 'pred_rank']])
|
||||
|
||||
# 如果是线上测试,需要计算每次交叉验证的结果相加,最后求平均
|
||||
if not offline:
|
||||
sub_preds += lgb_Classfication.predict_proba(tst_user_item_feats_df_rank_model[lgb_cols],
|
||||
num_iteration=lgb_Classfication.best_iteration_)[:,1]
|
||||
|
||||
score_df_ = pd.concat(score_list, axis=0)
|
||||
score_df = score_df.merge(score_df_, how='left', on=['user_id', 'click_article_id'])
|
||||
# 保存训练集交叉验证产生的新特征
|
||||
score_df[['user_id', 'click_article_id', 'pred_score', 'pred_rank', 'label']].to_csv(save_path + 'trn_lgb_cls_feats.csv', index=False)
|
||||
|
||||
# 测试集的预测结果,多次交叉验证求平均,将预测的score和对应的rank特征保存,可以用于后面的staking,这里还可以构造其他更多的特征
|
||||
tst_user_item_feats_df_rank_model['pred_score'] = sub_preds / k_fold
|
||||
tst_user_item_feats_df_rank_model['pred_score'] = tst_user_item_feats_df_rank_model['pred_score'].transform(lambda x: norm_sim(x))
|
||||
tst_user_item_feats_df_rank_model.sort_values(by=['user_id', 'pred_score'])
|
||||
tst_user_item_feats_df_rank_model['pred_rank'] = tst_user_item_feats_df_rank_model.groupby(['user_id'])['pred_score'].rank(ascending=False, method='first')
|
||||
|
||||
# 保存测试集交叉验证的新特征
|
||||
tst_user_item_feats_df_rank_model[['user_id', 'click_article_id', 'pred_score', 'pred_rank']].to_csv(save_path + 'tst_lgb_cls_feats.csv', index=False)
|
||||
```
|
||||
|
||||
|
||||
|
||||
```python
|
||||
# 预测结果重新排序, 及生成提交结果
|
||||
rank_results = tst_user_item_feats_df_rank_model[['user_id', 'click_article_id', 'pred_score']]
|
||||
rank_results['click_article_id'] = rank_results['click_article_id'].astype(int)
|
||||
submit(rank_results, topk=5, model_name='lgb_cls')
|
||||
```
|
||||
|
||||
## DIN模型
|
||||
|
||||
### 用户的历史点击行为列表
|
||||
|
||||
这个是为后面的DIN模型服务的
|
||||
|
||||
|
||||
```python
|
||||
if offline:
|
||||
all_data = pd.read_csv('./data_raw/train_click_log.csv')
|
||||
else:
|
||||
trn_data = pd.read_csv('./data_raw/train_click_log.csv')
|
||||
tst_data = pd.read_csv('./data_raw/testA_click_log.csv')
|
||||
all_data = trn_data.append(tst_data)
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
hist_click =all_data[['user_id', 'click_article_id']].groupby('user_id').agg({list}).reset_index()
|
||||
his_behavior_df = pd.DataFrame()
|
||||
his_behavior_df['user_id'] = hist_click['user_id']
|
||||
his_behavior_df['hist_click_article_id'] = hist_click['click_article_id']
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
trn_user_item_feats_df_din_model = trn_user_item_feats_df.copy()
|
||||
|
||||
if offline:
|
||||
val_user_item_feats_df_din_model = val_user_item_feats_df.copy()
|
||||
else:
|
||||
val_user_item_feats_df_din_model = None
|
||||
|
||||
tst_user_item_feats_df_din_model = tst_user_item_feats_df.copy()
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
trn_user_item_feats_df_din_model = trn_user_item_feats_df_din_model.merge(his_behavior_df, on='user_id')
|
||||
|
||||
if offline:
|
||||
val_user_item_feats_df_din_model = val_user_item_feats_df_din_model.merge(his_behavior_df, on='user_id')
|
||||
else:
|
||||
val_user_item_feats_df_din_model = None
|
||||
|
||||
tst_user_item_feats_df_din_model = tst_user_item_feats_df_din_model.merge(his_behavior_df, on='user_id')
|
||||
```
|
||||
|
||||
### DIN模型简介
|
||||
|
||||
我们下面尝试使用DIN模型, DIN的全称是Deep Interest Network, 这是阿里2018年基于前面的深度学习模型无法表达用户多样化的兴趣而提出的一个模型, 它可以通过考虑【给定的候选广告】和【用户的历史行为】的相关性,来计算用户兴趣的表示向量。具体来说就是通过引入局部激活单元,通过软搜索历史行为的相关部分来关注相关的用户兴趣,并采用加权和来获得有关候选广告的用户兴趣的表示。与候选广告相关性较高的行为会获得较高的激活权重,并支配着用户兴趣。该表示向量在不同广告上有所不同,大大提高了模型的表达能力。所以该模型对于此次新闻推荐的任务也比较适合, 我们在这里通过当前的候选文章与用户历史点击文章的相关性来计算用户对于文章的兴趣。 该模型的结构如下:
|
||||
|
||||

|
||||
|
||||
|
||||
我们这里直接调包来使用这个模型, 关于这个模型的详细细节部分我们会在下一期的推荐系统组队学习中给出。下面说一下该模型如何具体使用:deepctr的函数原型如下:
|
||||
> def DIN(dnn_feature_columns, history_feature_list, dnn_use_bn=False,
|
||||
> dnn_hidden_units=(200, 80), dnn_activation='relu', att_hidden_size=(80, 40), att_activation="dice",
|
||||
> att_weight_normalization=False, l2_reg_dnn=0, l2_reg_embedding=1e-6, dnn_dropout=0, seed=1024,
|
||||
> task='binary'):
|
||||
>
|
||||
> * dnn_feature_columns: 特征列, 包含数据所有特征的列表
|
||||
> * history_feature_list: 用户历史行为列, 反应用户历史行为的特征的列表
|
||||
> * dnn_use_bn: 是否使用BatchNormalization
|
||||
> * dnn_hidden_units: 全连接层网络的层数和每一层神经元的个数, 一个列表或者元组
|
||||
> * dnn_activation_relu: 全连接网络的激活单元类型
|
||||
> * att_hidden_size: 注意力层的全连接网络的层数和每一层神经元的个数
|
||||
> * att_activation: 注意力层的激活单元类型
|
||||
> * att_weight_normalization: 是否归一化注意力得分
|
||||
> * l2_reg_dnn: 全连接网络的正则化系数
|
||||
> * l2_reg_embedding: embedding向量的正则化稀疏
|
||||
> * dnn_dropout: 全连接网络的神经元的失活概率
|
||||
> * task: 任务, 可以是分类, 也可是是回归
|
||||
|
||||
在具体使用的时候, 我们必须要传入特征列和历史行为列, 但是再传入之前, 我们需要进行一下特征列的预处理。具体如下:
|
||||
|
||||
1. 首先,我们要处理数据集, 得到数据, 由于我们是基于用户过去的行为去预测用户是否点击当前文章, 所以我们需要把数据的特征列划分成数值型特征, 离散型特征和历史行为特征列三部分, 对于每一部分, DIN模型的处理会有不同
|
||||
1. 对于离散型特征, 在我们的数据集中就是那些类别型的特征, 比如user_id这种, 这种类别型特征, 我们首先要经过embedding处理得到每个特征的低维稠密型表示, 既然要经过embedding, 那么我们就需要为每一列的类别特征的取值建立一个字典,并指明embedding维度, 所以在使用deepctr的DIN模型准备数据的时候, 我们需要通过SparseFeat函数指明这些类别型特征, 这个函数的传入参数就是列名, 列的唯一取值(建立字典用)和embedding维度。
|
||||
2. 对于用户历史行为特征列, 比如文章id, 文章的类别等这种, 同样的我们需要先经过embedding处理, 只不过和上面不一样的地方是,对于这种特征, 我们在得到每个特征的embedding表示之后, 还需要通过一个Attention_layer计算用户的历史行为和当前候选文章的相关性以此得到当前用户的embedding向量, 这个向量就可以基于当前的候选文章与用户过去点击过得历史文章的相似性的程度来反应用户的兴趣, 并且随着用户的不同的历史点击来变化,去动态的模拟用户兴趣的变化过程。这类特征对于每个用户都是一个历史行为序列, 对于每个用户, 历史行为序列长度会不一样, 可能有的用户点击的历史文章多,有的点击的历史文章少, 所以我们还需要把这个长度统一起来, 在为DIN模型准备数据的时候, 我们首先要通过SparseFeat函数指明这些类别型特征, 然后还需要通过VarLenSparseFeat函数再进行序列填充, 使得每个用户的历史序列一样长, 所以这个函数参数中会有个maxlen,来指明序列的最大长度是多少。
|
||||
3. 对于连续型特征列, 我们只需要用DenseFeat函数来指明列名和维度即可。
|
||||
2. 处理完特征列之后, 我们把相应的数据与列进行对应,就得到了最后的数据。
|
||||
|
||||
下面根据具体的代码感受一下, 逻辑是这样, 首先我们需要写一个数据准备函数, 在这里面就是根据上面的具体步骤准备数据, 得到数据和特征列, 然后就是建立DIN模型并训练, 最后基于模型进行测试。
|
||||
|
||||
|
||||
```python
|
||||
# 导入deepctr
|
||||
from deepctr.models import DIN
|
||||
from deepctr.feature_column import SparseFeat, VarLenSparseFeat, DenseFeat, get_feature_names
|
||||
from tensorflow.keras.preprocessing.sequence import pad_sequences
|
||||
|
||||
from tensorflow.keras import backend as K
|
||||
from tensorflow.keras.layers import *
|
||||
from tensorflow.keras.models import *
|
||||
from tensorflow.keras.callbacks import *
|
||||
import tensorflow as tf
|
||||
|
||||
import os
|
||||
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
|
||||
os.environ["CUDA_VISIBLE_DEVICES"] = "2"
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 数据准备函数
|
||||
def get_din_feats_columns(df, dense_fea, sparse_fea, behavior_fea, his_behavior_fea, emb_dim=32, max_len=100):
|
||||
"""
|
||||
数据准备函数:
|
||||
df: 数据集
|
||||
dense_fea: 数值型特征列
|
||||
sparse_fea: 离散型特征列
|
||||
behavior_fea: 用户的候选行为特征列
|
||||
his_behavior_fea: 用户的历史行为特征列
|
||||
embedding_dim: embedding的维度, 这里为了简单, 统一把离散型特征列采用一样的隐向量维度
|
||||
max_len: 用户序列的最大长度
|
||||
"""
|
||||
|
||||
sparse_feature_columns = [SparseFeat(feat, vocabulary_size=df[feat].nunique() + 1, embedding_dim=emb_dim) for feat in sparse_fea]
|
||||
|
||||
dense_feature_columns = [DenseFeat(feat, 1, ) for feat in dense_fea]
|
||||
|
||||
var_feature_columns = [VarLenSparseFeat(SparseFeat(feat, vocabulary_size=df['click_article_id'].nunique() + 1,
|
||||
embedding_dim=emb_dim, embedding_name='click_article_id'), maxlen=max_len) for feat in hist_behavior_fea]
|
||||
|
||||
dnn_feature_columns = sparse_feature_columns + dense_feature_columns + var_feature_columns
|
||||
|
||||
# 建立x, x是一个字典的形式
|
||||
x = {}
|
||||
for name in get_feature_names(dnn_feature_columns):
|
||||
if name in his_behavior_fea:
|
||||
# 这是历史行为序列
|
||||
his_list = [l for l in df[name]]
|
||||
x[name] = pad_sequences(his_list, maxlen=max_len, padding='post') # 二维数组
|
||||
else:
|
||||
x[name] = df[name].values
|
||||
|
||||
return x, dnn_feature_columns
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 把特征分开
|
||||
sparse_fea = ['user_id', 'click_article_id', 'category_id', 'click_environment', 'click_deviceGroup',
|
||||
'click_os', 'click_country', 'click_region', 'click_referrer_type', 'is_cat_hab']
|
||||
|
||||
behavior_fea = ['click_article_id']
|
||||
|
||||
hist_behavior_fea = ['hist_click_article_id']
|
||||
|
||||
dense_fea = ['sim0', 'time_diff0', 'word_diff0', 'sim_max', 'sim_min', 'sim_sum', 'sim_mean', 'score',
|
||||
'rank','click_size','time_diff_mean','active_level','user_time_hob1','user_time_hob2',
|
||||
'words_hbo','words_count']
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# dense特征进行归一化, 神经网络训练都需要将数值进行归一化处理
|
||||
mm = MinMaxScaler()
|
||||
|
||||
# 下面是做一些特殊处理,当在其他的地方出现无效值的时候,不处理无法进行归一化,刚开始可以先把他注释掉,在运行了下面的代码
|
||||
# 之后如果发现报错,应该先去想办法处理如何不出现inf之类的值
|
||||
# trn_user_item_feats_df_din_model.replace([np.inf, -np.inf], 0, inplace=True)
|
||||
# tst_user_item_feats_df_din_model.replace([np.inf, -np.inf], 0, inplace=True)
|
||||
|
||||
for feat in dense_fea:
|
||||
trn_user_item_feats_df_din_model[feat] = mm.fit_transform(trn_user_item_feats_df_din_model[[feat]])
|
||||
|
||||
if val_user_item_feats_df_din_model is not None:
|
||||
val_user_item_feats_df_din_model[feat] = mm.fit_transform(val_user_item_feats_df_din_model[[feat]])
|
||||
|
||||
tst_user_item_feats_df_din_model[feat] = mm.fit_transform(tst_user_item_feats_df_din_model[[feat]])
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 准备训练数据
|
||||
x_trn, dnn_feature_columns = get_din_feats_columns(trn_user_item_feats_df_din_model, dense_fea,
|
||||
sparse_fea, behavior_fea, hist_behavior_fea, max_len=50)
|
||||
y_trn = trn_user_item_feats_df_din_model['label'].values
|
||||
|
||||
if offline:
|
||||
# 准备验证数据
|
||||
x_val, dnn_feature_columns = get_din_feats_columns(val_user_item_feats_df_din_model, dense_fea,
|
||||
sparse_fea, behavior_fea, hist_behavior_fea, max_len=50)
|
||||
y_val = val_user_item_feats_df_din_model['label'].values
|
||||
|
||||
dense_fea = [x for x in dense_fea if x != 'label']
|
||||
x_tst, dnn_feature_columns = get_din_feats_columns(tst_user_item_feats_df_din_model, dense_fea,
|
||||
sparse_fea, behavior_fea, hist_behavior_fea, max_len=50)
|
||||
```
|
||||
|
||||
WARNING:tensorflow:From /home/ryluo/anaconda3/lib/python3.6/site-packages/tensorflow/python/keras/initializers.py:143: calling RandomNormal.__init__ (from tensorflow.python.ops.init_ops) with dtype is deprecated and will be removed in a future version.
|
||||
Instructions for updating:
|
||||
Call initializer instance with the dtype argument instead of passing it to the constructor
|
||||
|
||||
|
||||
|
||||
```python
|
||||
# 建立模型
|
||||
model = DIN(dnn_feature_columns, behavior_fea)
|
||||
|
||||
# 查看模型结构
|
||||
model.summary()
|
||||
|
||||
# 模型编译
|
||||
model.compile('adam', 'binary_crossentropy',metrics=['binary_crossentropy', tf.keras.metrics.AUC()])
|
||||
```
|
||||
|
||||
WARNING:tensorflow:From /home/ryluo/anaconda3/lib/python3.6/site-packages/tensorflow/python/ops/init_ops.py:1288: calling VarianceScaling.__init__ (from tensorflow.python.ops.init_ops) with dtype is deprecated and will be removed in a future version.
|
||||
Instructions for updating:
|
||||
Call initializer instance with the dtype argument instead of passing it to the constructor
|
||||
WARNING:tensorflow:From /home/ryluo/anaconda3/lib/python3.6/site-packages/tensorflow/python/autograph/impl/api.py:255: add_dispatch_support.<locals>.wrapper (from tensorflow.python.ops.array_ops) is deprecated and will be removed in a future version.
|
||||
Instructions for updating:
|
||||
Use tf.where in 2.0, which has the same broadcast rule as np.where
|
||||
Model: "model"
|
||||
__________________________________________________________________________________________________
|
||||
Layer (type) Output Shape Param # Connected to
|
||||
==================================================================================================
|
||||
user_id (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
click_article_id (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
category_id (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
click_environment (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
click_deviceGroup (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
click_os (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
click_country (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
click_region (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
click_referrer_type (InputLayer [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
is_cat_hab (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
sparse_emb_user_id (Embedding) (None, 1, 32) 1600032 user_id[0][0]
|
||||
__________________________________________________________________________________________________
|
||||
sparse_seq_emb_hist_click_artic multiple 525664 click_article_id[0][0]
|
||||
hist_click_article_id[0][0]
|
||||
click_article_id[0][0]
|
||||
__________________________________________________________________________________________________
|
||||
sparse_emb_category_id (Embeddi (None, 1, 32) 7776 category_id[0][0]
|
||||
__________________________________________________________________________________________________
|
||||
sparse_emb_click_environment (E (None, 1, 32) 128 click_environment[0][0]
|
||||
__________________________________________________________________________________________________
|
||||
sparse_emb_click_deviceGroup (E (None, 1, 32) 160 click_deviceGroup[0][0]
|
||||
__________________________________________________________________________________________________
|
||||
sparse_emb_click_os (Embedding) (None, 1, 32) 288 click_os[0][0]
|
||||
__________________________________________________________________________________________________
|
||||
sparse_emb_click_country (Embed (None, 1, 32) 384 click_country[0][0]
|
||||
__________________________________________________________________________________________________
|
||||
sparse_emb_click_region (Embedd (None, 1, 32) 928 click_region[0][0]
|
||||
__________________________________________________________________________________________________
|
||||
sparse_emb_click_referrer_type (None, 1, 32) 256 click_referrer_type[0][0]
|
||||
__________________________________________________________________________________________________
|
||||
sparse_emb_is_cat_hab (Embeddin (None, 1, 32) 64 is_cat_hab[0][0]
|
||||
__________________________________________________________________________________________________
|
||||
no_mask (NoMask) (None, 1, 32) 0 sparse_emb_user_id[0][0]
|
||||
sparse_seq_emb_hist_click_article
|
||||
sparse_emb_category_id[0][0]
|
||||
sparse_emb_click_environment[0][0
|
||||
sparse_emb_click_deviceGroup[0][0
|
||||
sparse_emb_click_os[0][0]
|
||||
sparse_emb_click_country[0][0]
|
||||
sparse_emb_click_region[0][0]
|
||||
sparse_emb_click_referrer_type[0]
|
||||
sparse_emb_is_cat_hab[0][0]
|
||||
__________________________________________________________________________________________________
|
||||
hist_click_article_id (InputLay [(None, 50)] 0
|
||||
__________________________________________________________________________________________________
|
||||
concatenate (Concatenate) (None, 1, 320) 0 no_mask[0][0]
|
||||
no_mask[1][0]
|
||||
no_mask[2][0]
|
||||
no_mask[3][0]
|
||||
no_mask[4][0]
|
||||
no_mask[5][0]
|
||||
no_mask[6][0]
|
||||
no_mask[7][0]
|
||||
no_mask[8][0]
|
||||
no_mask[9][0]
|
||||
__________________________________________________________________________________________________
|
||||
no_mask_1 (NoMask) (None, 1, 320) 0 concatenate[0][0]
|
||||
__________________________________________________________________________________________________
|
||||
attention_sequence_pooling_laye (None, 1, 32) 13961 sparse_seq_emb_hist_click_article
|
||||
sparse_seq_emb_hist_click_article
|
||||
__________________________________________________________________________________________________
|
||||
concatenate_1 (Concatenate) (None, 1, 352) 0 no_mask_1[0][0]
|
||||
attention_sequence_pooling_layer[
|
||||
__________________________________________________________________________________________________
|
||||
sim0 (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
time_diff0 (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
word_diff0 (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
sim_max (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
sim_min (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
sim_sum (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
sim_mean (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
score (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
rank (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
click_size (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
time_diff_mean (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
active_level (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
user_time_hob1 (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
user_time_hob2 (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
words_hbo (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
words_count (InputLayer) [(None, 1)] 0
|
||||
__________________________________________________________________________________________________
|
||||
flatten (Flatten) (None, 352) 0 concatenate_1[0][0]
|
||||
__________________________________________________________________________________________________
|
||||
no_mask_3 (NoMask) (None, 1) 0 sim0[0][0]
|
||||
time_diff0[0][0]
|
||||
word_diff0[0][0]
|
||||
sim_max[0][0]
|
||||
sim_min[0][0]
|
||||
sim_sum[0][0]
|
||||
sim_mean[0][0]
|
||||
score[0][0]
|
||||
rank[0][0]
|
||||
click_size[0][0]
|
||||
time_diff_mean[0][0]
|
||||
active_level[0][0]
|
||||
user_time_hob1[0][0]
|
||||
user_time_hob2[0][0]
|
||||
words_hbo[0][0]
|
||||
words_count[0][0]
|
||||
__________________________________________________________________________________________________
|
||||
no_mask_2 (NoMask) (None, 352) 0 flatten[0][0]
|
||||
__________________________________________________________________________________________________
|
||||
concatenate_2 (Concatenate) (None, 16) 0 no_mask_3[0][0]
|
||||
no_mask_3[1][0]
|
||||
no_mask_3[2][0]
|
||||
no_mask_3[3][0]
|
||||
no_mask_3[4][0]
|
||||
no_mask_3[5][0]
|
||||
no_mask_3[6][0]
|
||||
no_mask_3[7][0]
|
||||
no_mask_3[8][0]
|
||||
no_mask_3[9][0]
|
||||
no_mask_3[10][0]
|
||||
no_mask_3[11][0]
|
||||
no_mask_3[12][0]
|
||||
no_mask_3[13][0]
|
||||
no_mask_3[14][0]
|
||||
no_mask_3[15][0]
|
||||
__________________________________________________________________________________________________
|
||||
flatten_1 (Flatten) (None, 352) 0 no_mask_2[0][0]
|
||||
__________________________________________________________________________________________________
|
||||
flatten_2 (Flatten) (None, 16) 0 concatenate_2[0][0]
|
||||
__________________________________________________________________________________________________
|
||||
no_mask_4 (NoMask) multiple 0 flatten_1[0][0]
|
||||
flatten_2[0][0]
|
||||
__________________________________________________________________________________________________
|
||||
concatenate_3 (Concatenate) (None, 368) 0 no_mask_4[0][0]
|
||||
no_mask_4[1][0]
|
||||
__________________________________________________________________________________________________
|
||||
dnn_1 (DNN) (None, 80) 89880 concatenate_3[0][0]
|
||||
__________________________________________________________________________________________________
|
||||
dense (Dense) (None, 1) 80 dnn_1[0][0]
|
||||
__________________________________________________________________________________________________
|
||||
prediction_layer (PredictionLay (None, 1) 1 dense[0][0]
|
||||
==================================================================================================
|
||||
Total params: 2,239,602
|
||||
Trainable params: 2,239,362
|
||||
Non-trainable params: 240
|
||||
__________________________________________________________________________________________________
|
||||
|
||||
|
||||
|
||||
```python
|
||||
# 模型训练
|
||||
if offline:
|
||||
history = model.fit(x_trn, y_trn, verbose=1, epochs=10, validation_data=(x_val, y_val) , batch_size=256)
|
||||
else:
|
||||
# 也可以使用上面的语句用自己采样出来的验证集
|
||||
# history = model.fit(x_trn, y_trn, verbose=1, epochs=3, validation_split=0.3, batch_size=256)
|
||||
history = model.fit(x_trn, y_trn, verbose=1, epochs=2, batch_size=256)
|
||||
```
|
||||
|
||||
Epoch 1/2
|
||||
290964/290964 [==============================] - 55s 189us/sample - loss: 0.4209 - binary_crossentropy: 0.4206 - auc: 0.7842
|
||||
Epoch 2/2
|
||||
290964/290964 [==============================] - 52s 178us/sample - loss: 0.3630 - binary_crossentropy: 0.3618 - auc: 0.8478
|
||||
|
||||
|
||||
|
||||
```python
|
||||
# 模型预测
|
||||
tst_user_item_feats_df_din_model['pred_score'] = model.predict(x_tst, verbose=1, batch_size=256)
|
||||
tst_user_item_feats_df_din_model[['user_id', 'click_article_id', 'pred_score']].to_csv(save_path + 'din_rank_score.csv', index=False)
|
||||
```
|
||||
|
||||
500000/500000 [==============================] - 20s 39us/sample
|
||||
|
||||
|
||||
|
||||
```python
|
||||
# 预测结果重新排序, 及生成提交结果
|
||||
rank_results = tst_user_item_feats_df_din_model[['user_id', 'click_article_id', 'pred_score']]
|
||||
submit(rank_results, topk=5, model_name='din')
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 五折交叉验证,这里的五折交叉是以用户为目标进行五折划分
|
||||
# 这一部分与前面的单独训练和验证是分开的
|
||||
def get_kfold_users(trn_df, n=5):
|
||||
user_ids = trn_df['user_id'].unique()
|
||||
user_set = [user_ids[i::n] for i in range(n)]
|
||||
return user_set
|
||||
|
||||
k_fold = 5
|
||||
trn_df = trn_user_item_feats_df_din_model
|
||||
user_set = get_kfold_users(trn_df, n=k_fold)
|
||||
|
||||
score_list = []
|
||||
score_df = trn_df[['user_id', 'click_article_id', 'label']]
|
||||
sub_preds = np.zeros(tst_user_item_feats_df_rank_model.shape[0])
|
||||
|
||||
dense_fea = [x for x in dense_fea if x != 'label']
|
||||
x_tst, dnn_feature_columns = get_din_feats_columns(tst_user_item_feats_df_din_model, dense_fea,
|
||||
sparse_fea, behavior_fea, hist_behavior_fea, max_len=50)
|
||||
|
||||
# 五折交叉验证,并将中间结果保存用于staking
|
||||
for n_fold, valid_user in enumerate(user_set):
|
||||
train_idx = trn_df[~trn_df['user_id'].isin(valid_user)] # add slide user
|
||||
valid_idx = trn_df[trn_df['user_id'].isin(valid_user)]
|
||||
|
||||
# 准备训练数据
|
||||
x_trn, dnn_feature_columns = get_din_feats_columns(train_idx, dense_fea,
|
||||
sparse_fea, behavior_fea, hist_behavior_fea, max_len=50)
|
||||
y_trn = train_idx['label'].values
|
||||
|
||||
# 准备验证数据
|
||||
x_val, dnn_feature_columns = get_din_feats_columns(valid_idx, dense_fea,
|
||||
sparse_fea, behavior_fea, hist_behavior_fea, max_len=50)
|
||||
y_val = valid_idx['label'].values
|
||||
|
||||
history = model.fit(x_trn, y_trn, verbose=1, epochs=2, validation_data=(x_val, y_val) , batch_size=256)
|
||||
|
||||
# 预测验证集结果
|
||||
valid_idx['pred_score'] = model.predict(x_val, verbose=1, batch_size=256)
|
||||
|
||||
valid_idx.sort_values(by=['user_id', 'pred_score'])
|
||||
valid_idx['pred_rank'] = valid_idx.groupby(['user_id'])['pred_score'].rank(ascending=False, method='first')
|
||||
|
||||
# 将验证集的预测结果放到一个列表中,后面进行拼接
|
||||
score_list.append(valid_idx[['user_id', 'click_article_id', 'pred_score', 'pred_rank']])
|
||||
|
||||
# 如果是线上测试,需要计算每次交叉验证的结果相加,最后求平均
|
||||
if not offline:
|
||||
sub_preds += model.predict(x_tst, verbose=1, batch_size=256)[:, 0]
|
||||
|
||||
score_df_ = pd.concat(score_list, axis=0)
|
||||
score_df = score_df.merge(score_df_, how='left', on=['user_id', 'click_article_id'])
|
||||
# 保存训练集交叉验证产生的新特征
|
||||
score_df[['user_id', 'click_article_id', 'pred_score', 'pred_rank', 'label']].to_csv(save_path + 'trn_din_cls_feats.csv', index=False)
|
||||
|
||||
# 测试集的预测结果,多次交叉验证求平均,将预测的score和对应的rank特征保存,可以用于后面的staking,这里还可以构造其他更多的特征
|
||||
tst_user_item_feats_df_din_model['pred_score'] = sub_preds / k_fold
|
||||
tst_user_item_feats_df_din_model['pred_score'] = tst_user_item_feats_df_din_model['pred_score'].transform(lambda x: norm_sim(x))
|
||||
tst_user_item_feats_df_din_model.sort_values(by=['user_id', 'pred_score'])
|
||||
tst_user_item_feats_df_din_model['pred_rank'] = tst_user_item_feats_df_din_model.groupby(['user_id'])['pred_score'].rank(ascending=False, method='first')
|
||||
|
||||
# 保存测试集交叉验证的新特征
|
||||
tst_user_item_feats_df_din_model[['user_id', 'click_article_id', 'pred_score', 'pred_rank']].to_csv(save_path + 'tst_din_cls_feats.csv', index=False)
|
||||
```
|
||||
|
||||
|
||||
|
||||
# 模型融合
|
||||
|
||||
## 加权融合
|
||||
|
||||
|
||||
```python
|
||||
# 读取多个模型的排序结果文件
|
||||
lgb_ranker = pd.read_csv(save_path + 'lgb_ranker_score.csv')
|
||||
lgb_cls = pd.read_csv(save_path + 'lgb_cls_score.csv')
|
||||
din_ranker = pd.read_csv(save_path + 'din_rank_score.csv')
|
||||
|
||||
# 这里也可以换成交叉验证输出的测试结果进行加权融合
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
rank_model = {'lgb_ranker': lgb_ranker,
|
||||
'lgb_cls': lgb_cls,
|
||||
'din_ranker': din_ranker}
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
def get_ensumble_predict_topk(rank_model, topk=5):
|
||||
final_recall = rank_model['lgb_cls'].append(rank_model['din_ranker'])
|
||||
rank_model['lgb_ranker']['pred_score'] = rank_model['lgb_ranker']['pred_score'].transform(lambda x: norm_sim(x))
|
||||
|
||||
final_recall = final_recall.append(rank_model['lgb_ranker'])
|
||||
final_recall = final_recall.groupby(['user_id', 'click_article_id'])['pred_score'].sum().reset_index()
|
||||
|
||||
submit(final_recall, topk=topk, model_name='ensemble_fuse')
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
get_ensumble_predict_topk(rank_model)
|
||||
```
|
||||
|
||||
## Staking
|
||||
|
||||
|
||||
```python
|
||||
# 读取多个模型的交叉验证生成的结果文件
|
||||
# 训练集
|
||||
trn_lgb_ranker_feats = pd.read_csv(save_path + 'trn_lgb_ranker_feats.csv')
|
||||
trn_lgb_cls_feats = pd.read_csv(save_path + 'trn_lgb_cls_feats.csv')
|
||||
trn_din_cls_feats = pd.read_csv(save_path + 'trn_din_cls_feats.csv')
|
||||
|
||||
# 测试集
|
||||
tst_lgb_ranker_feats = pd.read_csv(save_path + 'tst_lgb_ranker_feats.csv')
|
||||
tst_lgb_cls_feats = pd.read_csv(save_path + 'tst_lgb_cls_feats.csv')
|
||||
tst_din_cls_feats = pd.read_csv(save_path + 'tst_din_cls_feats.csv')
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 将多个模型输出的特征进行拼接
|
||||
|
||||
finall_trn_ranker_feats = trn_lgb_ranker_feats[['user_id', 'click_article_id', 'label']]
|
||||
finall_tst_ranker_feats = tst_lgb_ranker_feats[['user_id', 'click_article_id']]
|
||||
|
||||
for idx, trn_model in enumerate([trn_lgb_ranker_feats, trn_lgb_cls_feats, trn_din_cls_feats]):
|
||||
for feat in [ 'pred_score', 'pred_rank']:
|
||||
col_name = feat + '_' + str(idx)
|
||||
finall_trn_ranker_feats[col_name] = trn_model[feat]
|
||||
|
||||
for idx, tst_model in enumerate([tst_lgb_ranker_feats, tst_lgb_cls_feats, tst_din_cls_feats]):
|
||||
for feat in [ 'pred_score', 'pred_rank']:
|
||||
col_name = feat + '_' + str(idx)
|
||||
finall_tst_ranker_feats[col_name] = tst_model[feat]
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 定义一个逻辑回归模型再次拟合交叉验证产生的特征对测试集进行预测
|
||||
# 这里需要注意的是,在做交叉验证的时候可以构造多一些与输出预测值相关的特征,来丰富这里简单模型的特征
|
||||
from sklearn.linear_model import LogisticRegression
|
||||
|
||||
feat_cols = ['pred_score_0', 'pred_rank_0', 'pred_score_1', 'pred_rank_1', 'pred_score_2', 'pred_rank_2']
|
||||
|
||||
trn_x = finall_trn_ranker_feats[feat_cols]
|
||||
trn_y = finall_trn_ranker_feats['label']
|
||||
|
||||
tst_x = finall_tst_ranker_feats[feat_cols]
|
||||
|
||||
# 定义模型
|
||||
lr = LogisticRegression()
|
||||
|
||||
# 模型训练
|
||||
lr.fit(trn_x, trn_y)
|
||||
|
||||
# 模型预测
|
||||
finall_tst_ranker_feats['pred_score'] = lr.predict_proba(tst_x)[:, 1]
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# 预测结果重新排序, 及生成提交结果
|
||||
rank_results = finall_tst_ranker_feats[['user_id', 'click_article_id', 'pred_score']]
|
||||
submit(rank_results, topk=5, model_name='ensumble_staking')
|
||||
```
|
||||
|
||||
# 总结
|
||||
|
||||
本章主要学习了三个排序模型,包括LGB的Rank, LGB的Classifier还有深度学习的DIN模型, 当然,对于这三个模型的原理部分,我们并没有给出详细的介绍, 请大家课下自己探索原理,也欢迎大家把自己的探索与所学分享出来,我们一块学习和进步。最后,我们进行了简单的模型融合策略,包括简单的加权和Stacking。
|
||||
|
||||
关于Datawhale: Datawhale是一个专注于数据科学与AI领域的开源组织,汇集了众多领域院校和知名企业的优秀学习者,聚合了一群有开源精神和探索精神的团队成员。Datawhale 以“for the learner,和学习者一起成长”为愿景,鼓励真实地展现自我、开放包容、互信互助、敢于试错和勇于担当。同时 Datawhale 用开源的理念去探索开源内容、开源学习和开源方案,赋能人才培养,助力人才成长,建立起人与人,人与知识,人与企业和人与未来的联结。 本次数据挖掘路径学习,专题知识将在天池分享,详情可关注Datawhale:
|
||||
|
||||

|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,92 @@
|
||||
## 推荐系统实践(新闻推荐)
|
||||
|
||||
## 数据下载
|
||||
|
||||
> 链接:https://pan.baidu.com/s/15ReHXSM6VlQP-usia0DgYw
|
||||
> 提取码:135r
|
||||
|
||||
## 基本信息
|
||||
|
||||
- 学习周期:15天
|
||||
- 学习形式:理论学习+练习
|
||||
- 人群定位:有一定的数据分析基础,了解推荐系统基本算法,了解机器学习算法流程
|
||||
- 先修内容:[Python编程语言](https://github.com/datawhalechina/team-learning-program/tree/master/PythonLanguage);[编程实践(Pandas)](https://github.com/datawhalechina/team-learning-program/tree/master/IntroductionToPandas);[编程实践(Numpy)](https://github.com/datawhalechina/team-learning-program/tree/master/IntroductionToNumpy);[推荐系统基础](https://github.com/datawhalechina/team-learning-rs/tree/master/RecommendationSystemFundamentals)。
|
||||
- 难度系数:中
|
||||
|
||||
## 学习目标
|
||||
|
||||
### 熟悉推荐系统竞赛的基本流程
|
||||
|
||||
- 掌握数据分析方法
|
||||
- 了解多路召回策略
|
||||
- 了解冷启动策略
|
||||
- 了解排序特征的构造方法
|
||||
- 了解常见的排序模型
|
||||
- 了解模型融合
|
||||
|
||||
|
||||
### 新闻推荐入门赛学习内容汇总:
|
||||
|
||||

|
||||
|
||||
|
||||
## 任务安排
|
||||
|
||||
### Task00:熟悉规则(1天)
|
||||
|
||||
- 组队、修改群昵称
|
||||
- 熟悉打开规则
|
||||
|
||||
### Task01:赛题理解+Baseline(3天)
|
||||
|
||||
- 理解赛题数据和目标,理解评分指标,了解赛题的解题思路
|
||||
- 完成赛题报名和数据下载,跑通Baseline并成功提交结果
|
||||
|
||||
### Task02:数据分析(2天)
|
||||
|
||||
- 了解数据中不同文件所包含的信息,不同数据文件之间的关系
|
||||
- 分析点击数据中用户的点击环境、点击偏好,点击的文章属性等分布
|
||||
- 分析点击数据中文章的基本属性,文章的热门程度,文章的共现情况等
|
||||
- 分析文章属性文件中(embedding文件和属性特征文件),文章的基本信息
|
||||
|
||||
|
||||
### Task03:多路召回(3天)
|
||||
|
||||
- 熟悉常见的召回策略,如:itemcf, usercf以及深度模型召回等
|
||||
- 了解当前场景下的冷启动问题,及常见解决策略,了解如何将多路召回的结果进行合并
|
||||
- 完成多种策略的召回,冷启动及多路召回合并
|
||||
- 完成召回策略的调参和召回效果的评估
|
||||
|
||||
|
||||
### Task04:特征工程(3天)
|
||||
|
||||
- 了解排序数据标签的构建,训练数据的负采样,排序特征的常用构造思路
|
||||
- 完成用户召回文章与历史文章相关性的特征构造
|
||||
- 完成用户历史兴趣的相关特征的提取,文章本身属性特征的提取
|
||||
|
||||
### Task05:排序模型+模型融合(3天)
|
||||
|
||||
- 了解基本的排序模型,模型的训练和测试,常用的模型融合策略
|
||||
- 完成LGB分类模型,LGB排序模型,及深度模型中的DIN模型的训练、验证及调参
|
||||
- 完成加权融合与Staking融合两种融合策略
|
||||
|
||||
|
||||
|
||||
## 关于Datawhale
|
||||
|
||||
> Datawhale是一个专注于数据科学与AI领域的开源组织,汇集了众多领域院校和知名企业的优秀学习者,聚合了一群有开源精神和探索精神的团队成员。Datawhale 以“for the learner,和学习者一起成长”为愿景,鼓励真实地展现自我、开放包容、互信互助、敢于试错和勇于担当。同时 Datawhale 用开源的理念去探索开源内容、开源学习和开源方案,赋能人才培养,助力人才成长,建立起人与人,人与知识,人与企业和人与未来的联结。 本次数据挖掘路径学习,专题知识将在天池分享,详情可关注Datawhale(二维码在上面)
|
||||
|
||||
|
||||
|
||||
## 其他组队学习
|
||||
|
||||
有关组队学习的开源内容
|
||||
|
||||
- [team-learning](https://github.com/datawhalechina/team-learning):主要展示Datawhale的组队学习计划。
|
||||
- [team-learning-program](https://github.com/datawhalechina/team-learning-program):主要存储Datawhale组队学习中“编程、数据结构与算法”方向的资料。
|
||||
- [team-learning-data-mining](https://github.com/datawhalechina/team-learning-data-mining):主要存储Datawhale组队学习中“数据挖掘/机器学习”方向的资料。
|
||||
- [team-learning-nlp](https://github.com/datawhalechina/team-learning-nlp):主要存储Datawhale组队学习中“自然语言处理”方向的资料。
|
||||
- [team-learning-cv](https://github.com/datawhalechina/team-learning-cv):主要存储Datawhale组队学习中“计算机视觉”方向的资料。
|
||||
- [team-learning-rs](https://github.com/datawhalechina/team-learning-rs):主要存储Datawhale组队学习中“推荐系统”方向的资料。
|
||||
- [team-learning-rl](https://github.com/datawhalechina/team-learning-rl):主要存储Datawhale组队学习中“强化学习”方向的资料。
|
||||
|
||||
Reference in New Issue
Block a user