Datawhale新闻推荐竞赛学习总结:Baseline

赛题:天池大赛—零基础入门推荐系统—新闻推荐
天池新闻推荐入门赛之【赛题理解+Baseline】
推荐系统组队学习之协同过滤

推荐系统基本流程

赛题数据

数据表

  • train_click_log.csv:训练集用户点击日志(20W)
  • testA_click_log.csv:测试集用户点击日志(5W)
  • articles.csv:新闻文章信息数据表
  • articles_emb.csv:新闻文章embedding向量表示
  • sample_submit.csv:提交样例文件

字段表

FieldDescriptionFieldDescription
user_id用户idarticle_id文章id,与click_article_id相对应
click_article_id点击文章idcategory_id文章类型id
click_timestamp点击时间戳created_at_ts文章创建时间戳
click_environment点击环境words_count文章字数
click_deviceGroup点击设备组emb_1,emb_2,…,emb_249文章embedding向量表示
click_os点击操作系统
click_country点击城市
click_region点击地区
click_referrer_type点击来源类型

算法

Baseline使用基于物品的协同过滤(ItemCF)算法,仅使用到点击日志文件(train_click_log.csv、testA_click_log.csv)中的user_idclick_article_idclick_timestamp三个字段。

基于物品的协同过滤算法主要分为两步。

  1. 计算物品之间的相似度
  2. 根据物品之间相似度和用户的历史行为给用户生产推荐列表。

文章相似度

计算物品之间相似度——余弦相似度:
S i , j = ∣ N ( i ) ∩ N ( j ) ∣ ∣ N ( i ) ∣ ∣ N ( j ) ∣ S_{i,j}=\frac{\lvert N(i)\cap N(j)\rvert}{\sqrt{\lvert N(i)\rvert\lvert N(j)\rvert}} Si,j=N(i)N(j) N(i)N(j)

其中, N ( i ) N(i) N(i)表示的是喜欢物品 i i i 的用户集合, ∣ N ( i ) ∣ \lvert N(i)\rvert N(i)表示的是喜欢物品 i i i 的用户数量。

Code1

存在的问题:存在无用计算。计算了任意两个物品之间的余弦相似度,包括没有交互的物品。

'''
item_user_dict={
item1:{user1,user2,...},
item2:{user1,user2,...},
...
}
'''
i2i_sim={}
for i1 in item_user_dict.keys():
	for i2 in item_user_dict.keys():
	if i1==i2:
		continue
	i2i_sim[i1][i2]=len(item_user_dict[i1]&item_user_dict[i2])
	i2i_sim[i1][i2]/=math.sqrt(len(item_user_dict[i1])*len(item_user_dict[i2]))

Code2

文 章 A 和 文 章 B 的 相 似 度 = ∑ i ( 用 户 i 点 击 文 章 A 的 次 数 × 用 户 i 点 击 文 章 B 的 次 数 ) / log ⁡ ( 用 户 i 点 击 文 章 的 总 次 数 + 1 ) 文 章 A 被 点 击 的 总 次 数 × 文 章 B 被 点 击 的 总 次 数 文章A和文章B的相似度=\frac{\sum_{i}(用户i点击文章A的次数\times 用户i点击文章B的次数)/\log(用户i点击文章的总次数+1)}{\sqrt{文章A被点击的总次数\times 文章B被点击的总次数}} AB=A×B i(iA×iB)/log(i+1)

  • 用 户 i 点 击 文 章 A 的 次 数 × 用 户 i 点 击 文 章 B 的 次 数 用户i点击文章A的次数\times 用户i点击文章B的次数 iA×iB:同一用户不同的点击时间重复计算。
  • log ⁡ ( 用 户 i 点 击 文 章 的 总 次 数 + 1 ) \log(用户i点击文章的总次数+1) log(i+1):惩罚文章看得太多的用户,例如爬虫。
def itemcf_sim(df):
    """
        文章与文章之间的相似性矩阵计算
        :param df: 数据表
        :item_created_time_dict:  文章点击时间的字典
        return : 文章与文章的相似性矩阵
        思路: 基于物品的协同过滤, 在多路召回部分会加上关联规则的召回策略
    """
    
    user_item_time_dict = get_user_item_time(df) # {user1: [(item1, time1), (item2, time2)..]...}
    
    # 计算物品相似度
    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, {}) # 如果存在key=i,则不会置为空字典
            for j, j_click_time in item_time_list:
                if(i == j):
                    continue
                i2i_sim[i].setdefault(j, 0) # 如果存在key=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_

热门文章

使用点击次数衡量文章的热度:点击次数越多,文章热度越高。

# 获取近期点击最多的文章
def get_item_topk_click(click_df, k):
    topk_click = click_df['click_article_id'].value_counts().index[:k]
    return topk_click

为用户user_id推荐文章

1. 基于用户user_id的历史交互行为为每个文章打分

对用户的每一个交互记录,例如(文章i,点击时间),对与文章 i 相似度前 sim_item_topk 的每一篇文章 j,如果该用户没有点击过文章 j,则文章 j 的评分加 wij。

2. 热门文章补全

如果评分列表item_rank中的元素数量小于需要召回的文章数量,极端情形,对用户访问的每一篇文章 i,如果该用户访问过所有与文章 i 交互的相似度前 sim_item_topk 的文章,则评分列表item_rank是空的。此时可用热门文章补全。

# 基于商品的召回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] # [(item1,time1), (item2,time2)..]
    user_hist_items_={user_id for user_id,_ in user_hist_items} # 用户点击的文章集合{item1,item2,...}
    
    item_rank = {}
    for loc, (i, click_time) in enumerate(user_hist_items):
    	# 与文章i相似度前sim_item_topk的文章:文章j、相似度wij
        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

Code

1. 读取采样数据

# 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

2. DataFrame 内存优化

  因为训练数据集往往比较大,而内存会出现不够用的情况,可以通过修改特征的数据类型,从而达到优化压缩的目的。

# 节约内存的一个标配函数
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

代码注释

思路:如果数据没有那么大,就不要申请那么大的空间,应将其格式进行转换。

DataFrame.info()
DataFrame.info(verbose=None, buf=None, max_cols=None, memory_usage=None, null_counts=None)

在调用DataFrame的info()方法时,会打印出该DataFrame的内存使用情况。

memory_usage:bool, str, optional

  • 该参数用于指定是否显示DataFrame元素总的内存消耗。也可通过 pandas.options.display.memory_usage 来设置,True则总是显示内存消耗,False则从不显示内存消耗。
  • memory_usage='deep'将显示详细的内存使用报告。
import numpy as np
import pandas as pd
n=5000
dtypes = ['int64', 'float64', 'datetime64[ns]', 'timedelta64[ns]','complex128', 'object', 'bool']
data = {t: np.random.randint(100, size=n).astype(t) for t in dtypes}
df = pd.DataFrame(data)
df['categorical'] = df['object'].astype('category')
print(df.info())
print(df.info(memory_usage='deep'))


pandas.DataFrame.info
Frequently Asked Questions (FAQ)——DataFrame memory usage

DataFrame.memory_usage()
DataFrame.memory_usage(index=True, deep=False)

返回每一列的内存使用情况。参数index用于指定是否返回DataFrame的index列的内存使用情况。

# 每一列的内存使用情况
print(df.memory_usage())
# 总的内存使用情况
print('总的内存使用情况:{}'.format(df.memory_usage().sum()))

在这里插入图片描述

np.random.randint
numpy.random.randint(low, high=None, size=None, dtype='l')

返回一个随机整型数,范围 [ l o w , h i g h ) [low, high) [low,high)

  • low: int 生成的数值最低要大于等于low。(hign = None时,生成的数值要在[0, low)区间内)
  • high: int (可选) 如果使用这个值,则生成的数值在[low, high)区间。
  • size: int or tuple of ints(可选) 输出随机数的尺寸。默认是None的,仅仅返回满足要求的单一随机数。
  • dtype: dtype(可选):想要输出的格式。如int64、int等等。
np.iinfo(type)/np.finfo(type)

返回整数类型/浮点数类型的机器限制。

>>> ii32 = np.iinfo(np.int32)
>>> ii32.min
-2147483648
>>> ii32.max
2147483647

class numpy.iinfo(type)
class numpy.finfo(dtype)

更多方法

机器学习之内存优化

# 生成一个feather文件
your_df.to_feather(path)

# 读取feather文件
pd.read_feather(path)

3. tqdm库

  tqdm 是 Python 进度条库,可以在 Python 长循环中添加一个进度提示信息。

tqdm(iterable)

trange(i) 是 tqdm(range(i)) 的简单写法。
可以总结为三个方法:

方法一

# 方法1:
import time
from tqdm import tqdm  

for i in tqdm(range(100)):  
    time.sleep(0.01)

方法1+import time
from tqdm import trange

for i in trange(100):
    time.sleep(0.01)

方法二:为进度条设置描述

import time
from tqdm import tqdm

pbar = tqdm(["a", "b", "c", "d"])
for char in pbar:
    # 设置描述
    pbar.set_description("Processing %s" % char)
    time.sleep(0.2)

方法三:手动更新

import time
from tqdm import tqdm

# 一共200个,每次更新10,一共更新20次
with tqdm(total=200) as pbar:
  pbar.set_description("Processing")
  for i in range(20):
    pbar.update(10)
    time.sleep(0.1)

#方法2:
pbar = tqdm(total=200)
for i in range(20):
    pbar.update(10)
    time.sleep(0.1)
pbar.close()

tqdm模块
tqdm documentation

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值