【从零开始学推荐】基于物品的协同过滤

基于协同过滤算法进行文章推荐。

  1. 基于物品的协同过滤+热门补足进行topK推荐。
  2. 基于物品的协同过滤(时间衰减)+热门补足进行topK推荐。
  3. 基于物品的协同过滤(时间衰减)+embedding内容相关补足进行topK推荐。
import os
import gc
import time
import math
import pickle
import random
import collections
from datetime import datetime
from tqdm import tqdm_notebook
from collections import defaultdict, OrderedDict

import numpy as np
import pandas as pd

import warnings
warnings.filterwarnings('ignore')

def timmer(func):
    """ 装饰器,监控运行时间 """
    def wrapper(*args, **kwargs):
        before_time = time.time()
        f = func(*args, **kwargs)
        print("--> RUN TIME: <%s> : %s" % (func.__name__, time.time() - before_time))
        return f
    return wrapper

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

##### 数据准备
def get_all_click_df():
    """ 合并训练集和测试集的点击日志 """
    train_df = pd.read_csv('./inputs/train_click_log.csv')
    test_df = pd.read_csv('./inputs/testA_click_log.csv')
    all_click_df = train_df.append(test_df)
    all_click_df = all_click_df.drop_duplicates((['user_id', 'click_article_id', 'click_timestamp']))
    all_click_df = reduce_mem(all_click_df)
    return all_click_df

all_click_df = get_all_click_df()
all_click_df = all_click_df.sort_values(['user_id', 'click_timestamp'], ascending=[True, True])
all_click_df['click_article_id'] = all_click_df['click_article_id'].astype(str)
all_click_df['click_timestamp'] = all_click_df['click_timestamp'].astype(str)
data = all_click_df.groupby(['user_id'])[['click_article_id', 'click_timestamp']].agg({'click_article_id': lambda x: ','.join(list(x)), 'click_timestamp': lambda x: ','.join(list(x))}).reset_index()

def get_item_dict(x):
    return OrderedDict(zip(x.click_article_id.split(','), x.click_timestamp.split(',')))

data['item_dict'] = data.apply(get_item_dict, axis=1)
user_items_dict = dict(zip(data.user_id, data.item_dict))

基于物品的协同过滤是业界使用最多的算法。

物品之间的相似度公式为:

w_{ij} = \frac{|N(i)\bigcap N(j)|}{|N(i)|}|N(i)|是喜欢i物品的用户数,|N(i)\bigcap N(j)|是同时喜欢物品i和物品j的用户数。

当j很热门的时候,w_{ij}趋近于1,故提出改进公式:

w_{ij} = \frac{|N(i)\bigcap N(j)|}{|N(i)||N(j)|},惩罚热门物品j的权重。

基于物品的协同过滤,主要包括如下步骤:

1) 建立用户到消费物品列表。
2) 建立物品矩阵c[i][j],c[i][j]=k代表物品i和物品j共同消费用户k个。
    2.1 遍历每个用户的消费物品列表。
    2.2 每个用户对应的物品列表中,两两物品列表中c[i][j]+=1。
    2.3 得到物品之间不为0的共现矩阵C[i][j],近而根据余弦相似度得到W[i][j]。
3) 得到物品相似度矩阵之后,根据用户消费物品给他推荐最相似物品。
@timmer
def calucate_i2i_sim(i2i_sim_path):
    # 物品相关性矩阵
    if os.path.exists(i2i_sim_path):
        W = pickle.load(open(i2i_sim_path, 'rb'))
        return W
    
    C = defaultdict(int) # 物品矩阵,记录物品i和j相同用户数量
    N = defaultdict(int) # 记录每个物品的用户数量
    
    for user, item_time_dict in tqdm_notebook(user_items_dict.items()):
        for item_i, time_i in item_time_dict.items():
            N[item_i] += 1
            C.setdefault(item_i, {})
            for item_j, time_j in item_time_dict.items():
                if item_i == item_j:
                    continue
                C[item_i].setdefault(item_j, 0)
                C[item_i][item_j] += 1.0
    W = C.copy() # 物品相似度矩阵
    for i, related_items in C.items():
        for j, cij in related_items.items():
            W[i][j] = cij / math.sqrt(N[i] * N[j])
    pickle.dump(W, open(i2i_sim_path, 'wb'))
    return W

W = calucate_i2i_sim(i2i_sim_path='./outputs/i2i_sim.pkl')

def item_based_recommender(user_id, user_items_dict, hot_items, W, K=5, N=10):
    """ 根据用户消费物品最相似的k个物品进行推荐,推荐数量为N """
    # 用户历史上有交互的所有文章
    user_history_items = user_items_dict[user_id]
    
    rec_items_dict = {}
    for idx, (item_i, time_i) in enumerate(user_history_items.items()):
        for item_j, wij in sorted(W[item_i].items(), key=lambda x: x[1], reverse=True)[:K]:
            if item_j in user_history_items:
                continue
            rec_items_dict.setdefault(item_j, 0)
            rec_items_dict[item_j] += wij
    
    # 热门物品补足
    if len(rec_items_dict) < N:
        for i, hot_item in enumerate(hot_items):
            if hot_item in rec_items_dict.items():
                continue
            rec_items_dict[hot_item] = -i-100
            if len(rec_items_dict) == N:
                break
    rec_items_dict = sorted(rec_items_dict.items(), key=lambda x: x[1], reverse=True)[:N]
    return rec_items_dict

# 相似文章数量
K = 10
# 召回数量
N = 10
# 前100热门物品列表
hot_items = list(all_click_df.click_article_id.value_counts().index[:].values)[:100]
# i2i召回结果集
i2i_recall_dict = defaultdict(dict)
# 对所有用户进行推荐
for user_id in tqdm_notebook(all_click_df.user_id.unique()):
    i2i_recall_dict[user_id] = item_based_recommender(user_id, user_items_dict, hot_items, W, K, N)

res = []
for user_id, items in tqdm_notebook(i2i_recall_dict.items()):
    for item, score in items:
        res.append([user_id, item, score])
res_df = pd.DataFrame(res, columns=['user_id', 'item', 'score'])

# 每个用户取前5的文章
res_df.sort_values(by=['user_id', 'score'], axis=0, ascending=[True, False], inplace=True)
res_df['score_rank'] = res_df['score'].groupby([res_df['user_id']]).rank(ascending=False, method='first')

# 按提交格式定义dataframe
submit_df = res_df[res_df.score_rank<=5][['user_id', 'item', 'score_rank']].set_index(['user_id', 'score_rank']).unstack(-1).reset_index()
submit_df.columns = [int(col) if isinstance(col, int) else col for col in submit_df.columns.droplevel(0)]
submit_df = submit_df.rename(columns={'': 'user_id', 1: 'article_1', 2: 'article_2', 3: 'article_3', 4: 'article_4', 5: 'article_5'})

output_path = './outputs/{}.csv'.format('i2i_' + datetime.today().strftime('%m-%d'))
test_df = pd.read_csv('./inputs/testA_click_log.csv')
test_uids = set(test_df.user_id.values)
submit_df[submit_df.user_id.isin(test_uids)].to_csv(output_path, index=False, header=True)
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容