乐趣区

关于推荐系统:推荐系统新闻推荐之推荐

特色工程

咱们制作特色和标签,将举荐问题转成监督学习问题。
咱们先回顾一下现有数据,有哪些特色能够间接利用:

  1. 文章的本身特色: category_id 示意这文章的类型,created_at_ts 示意文章建设的工夫,这个关系着文章的时效性,words_count 是文章的字数,个别字数太长咱们不太喜爱点击, 也不排除有人就喜爱读长文。
  2. 文章的内容 embedding 特色: 这个召回的时候用过,这里能够抉择应用,也能够抉择不必,也能够尝试其余类型的 embedding 特色,比方 W2V 等
  3. 用户的设施特色信息

下面这些间接能够用的特色,待做完特色工程之后,间接就能够依据 article_id 或者是 user_id 把这些特色退出进去。然而咱们须要先基于召回的后果,结构一些特色,而后制作标签,造成一个监督学习的数据集。

结构监督数据集的思路,依据召回后果,咱们会失去一个 {user_id: [可能点击的文章列表]} 模式的字典。那么咱们就能够对于每个用户,每篇可能点击的文章结构一个监督测试集,比方对于用户 user1,假如失去的他的召回列表 {user1: [item1, item2, item3]},咱们就能够失去三行数据(user1, item1), (user1, item2), (user1, item3) 的模式,这就是监督测试集时候的前两列特色。

结构特色的思路是这样,咱们晓得每个用户的点击文章是与其历史点击的文章信息是有很大关联的,比方同一个主题,类似等等。所以特色结构这块很重要的一系列特色 是要联合用户的历史点击文章信息。咱们曾经失去了每个用户及点击候选文章的两列的一个数据集,而咱们的目标是要预测最初一次点击的文章,比拟天然的一个思路就是和其最初几次点击的文章产生关系,这样既思考了其历史点击文章信息,又得离最初一次点击较近,因为新闻很大的一个特点就是重视时效性。往往用户的最初一次点击会和其最初几次点击有很大的关联。所以咱们就能够对于每个候选文章,做出与最初几次点击相干的特色如下:

  1. 候选 item 与最初几次点击的相似性特色(embedding 内积)— 这个间接关联用户历史行为
  2. 候选 item 与最初几次点击的相似性特色的统计特色 — 统计特色能够缩小一些稳定和异样
  3. 候选 item 与最初几次点击文章的字数差的特色 — 能够通过字数看用户偏好
  4. 候选 item 与最初几次点击的文章建设的时间差特色 — 时间差特色能够看出该用户对于文章的实时性的偏好

还须要考虑一下
5. 如果应用了 youtube 召回的话,咱们还能够制作用户与候选 item 的类似特色

当然,下面只是提供了一种基于用户历史行为做特色工程的思路,大家也能够思维风暴一下,尝试一些其余的特色。上面咱们就实现下面的这些特色的制作,上面的逻辑是这样:

  1. 咱们首先取得用户的最初一次点击操作和用户的历史点击,这个基于咱们的日志数据集做
  2. 基于用户的历史行为制作特色,这个会用到用户的历史点击表,最初的召回列表,文章的信息表和 embedding 向量
  3. 制作标签,造成最初的监督学习数据集

好了,废话不多说

导入库

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')

数据寄存地位以及后果输入地位

data_dir = './data'
save_dir = './results'

数据读取

训练和验证集的划分

划分训练和验证集的起因是为了在线下验证模型参数的好坏,为了齐全模仿测试集,咱们这里就在训练集中抽取局部用户的所有信息来作为验证集。提前做训练验证集划分的益处就是能够合成制作排序特色时的压力,一次性做整个数据集的排序特色可能工夫会比拟长。

# 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
获取历史点击和最初一次点击
# 获取以后数据的历史点击和最初一次点击
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
读取训练、验证及测试集
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(click_trn_data , 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
读取召回列表
# 返回多路召回列表或者单路召回
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 模型根底
  • word2vec 原理(二) 基于 Hierarchical Softmax 的模型
  • word2vec 原理(三) 基于 Negative Sampling 的模型
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
# 能够通过字典查问对应的 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
读取文章信息
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
读取数据
# 这里 offline 的 online 的区别就是验证集是否为空
click_trn, click_val, click_tst, val_ans = get_trn_val_tst_data(data_path, offline=False)

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. 做完负采样之后,更新此时新的用户召回文章列表,因为后续做特色的时候可能用到绝对地位的信息。

其实负采样也能够留在前面做完特色在进行,这里因为做排序特色太慢了,所以把负采样的环节提到后面了。

# 将召回列表转换成 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
# 负采样函数,这里能够管制负采样时的比例, 这里给了一个默认的值
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
# 召回数据打标签
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_
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
# 读取召回列表
recall_list_dict = get_recall_list(save_path, single_recall_model='i2i_itemcf') # 这里只抉择了单路召回的后果,也能够抉择多路召回后果
# 将召回数据转换成 df
recall_list_df = recall_dict_2_df(recall_list_dict)
# 给训练验证数据打标签,并负采样(这一部分工夫比拟久)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)
将召回数据转换成字典
# 将最终的召回的 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
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 次点击商品的类似度的和(最大,最小,均值),时间差特色,相似性特色,字数差特色,与该用户的相似性特色
# 上面基于 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
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)
# 获取训练验证及测试数据中召回列文章相干特色
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)
# 保留一份省的每次都要从新跑,每次跑的工夫都比拟长
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. 对于用户和商品还能够结构的特色:

    • 基于用户的点击文章次数和点击工夫结构能够体现用户活跃度的特色
    • 基于文章被点击次数和工夫结构能够反映文章热度的特色
    • 用户的工夫统计特色:依据其点击的历史文章列表的点击工夫和文章的创立工夫做统计特色,比方求均值,这个能够反映用户对于文章时效的偏好
    • 用户的主题喜好特色,对于用户点击的历史文章主题进行一个统计,而后对于以后文章看看是否属于用户曾经点击过的主题
    • 用户的字数喜好特色,对于用户点击的历史文章的字数统计,求一个均值
# 读取文章特色
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')

剖析一下点击工夫和点击文章的次数,辨别用户活跃度
如果某个用户点击文章之间的工夫距离比拟小,同时点击的文章次数很多的话,那么咱们认为这种用户个别就是沉闷用户, 当然掂量用户活跃度的形式可能多种多样,这里咱们只提供其中一种,咱们写一个函数,失去能够掂量用户活跃度的特色,逻辑如下:

首先依据用户 user_id 分组,对于每个用户,计算点击文章的次数,两两点击文章工夫距离的均值
把点击次数取倒数和工夫距离的均值对立归一化,而后两者相加合并,该值越小,阐明用户越沉闷
留神,下面两两点击文章的工夫距离均值,会呈现如果用户只点击了一次的状况,这时候工夫距离均值那里会呈现空值,对于这种状况最初特色那里给个大数进行辨别
这个的衡量标准就是先把点击的次数取到数而后归一化,而后点击的时间差归一化,而后两者相加进行合并,该值越小,阐明被点击的次数越多,且间隔时间短。

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
user_act_fea = active_level(all_data, ['user_id', 'click_article_id', 'click_timestamp'])
剖析一下点击工夫和被点击文章的次数,掂量文章热度特色

和下面同样的思路,如果一篇文章在很短的工夫距离之内被点击了很屡次,阐明文章比拟热门,实现的逻辑和下面的基本一致,只不过这里是依照点击的文章进行分组:

  1. 依据文章进行分组,对于每篇文章的用户,计算点击的工夫距离
  2. 将用户的数量取倒数,而后用户的数量和工夫距离归一化,而后相加失去热度特色,该值越小,阐明被点击的次数越大且工夫距离越短,文章比拟热
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
article_hot_fea = hot_level(all_data, ['user_id', 'click_article_id', 'click_timestamp'])    
用户的系列习惯

这个基于原来的日志表做一个相似于 article 的那种 DataFrame,寄存用户特有的信息, 次要包含点击习惯,喜好特色之类的

  • 用户的设施习惯,这里取最罕用的设施(众数)
  • 用户的工夫习惯:依据其点击过得历史文章的工夫来做一个统计(这个感觉最好是把工夫戳里的工夫特色的 h 特色提出来,看看用户习惯一天的啥时候点击文章),但这里先用转换的工夫吧,求个均值
  • 用户的喜好特色,对于用户点击的历史文章主题进行用户的喜好判断,更偏差于哪几个主题,这个最好是 multi-hot 进行编码,先试试行不
  • 用户文章的字数差特色,用户的喜好文章的字数习惯

这些就是对用户进行分组,而后统计即可

用户的设施习惯
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
    
# 设施特色(这里工夫会比拟长)
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)
用户的工夫习惯
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
    
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。

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
    
user_category_hob_cols = ['user_id', 'category_id']
user_cat_hob_info = user_cat_hob_fea(all_data, user_category_hob_cols)
用户的字数偏好特色
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)
用户的信息特色合并保留
# 所有表进行合并
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')
# 这样用户特色当前就能够间接读取了
user_info.to_csv(save_path + 'user_info.csv', index=False)  
用户特色间接读入

如果后面对于用户的特色工程曾经给做完了,前面能够间接读取

# 把用户信息间接读入进来
user_info = pd.read_csv(save_path + 'user_info.csv')
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
# 拼上用户特色
# 上面是线下验证的
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')
文章的特色间接读入
articles =  pd.read_csv(data_path+'articles.csv')
articles = reduce_mem(articles)
# 拼上文章特色
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') 
召回文章的主题是否在用户的喜好外面
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)
# 线下验证
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']
保留特色

特色工程和数据荡涤转换是较量中至关重要的一块,因为 数据和特色决定了机器学习的下限,而算法和模型只是迫近这个下限而已 ,所以特色工程的好坏往往决定着最初的后果, 特色工程 能够一步加强数据的表达能力,通过结构新特色,咱们能够挖掘出数据的更多信息,使得数据的表达能力进一步放大。在本节内容中,咱们次要是先通过制作特色和标签把预测问题转成了监督学习问题,而后围绕着用户画像和文章画像进行一系列特色的制作,此外,为了保障正负样本的数据平衡,咱们还学习了负采样就技术等。

退出移动版