Python
【慕课】个性化推荐算法
11/16/2018
·
115
·
profile photo
这篇文章是我在慕课网学习 @david_beck 老师的《个性化推荐算法实战入门必修课》时根据老师所讲内容整理的笔记,使用 jupyter Notebook + python 3 整理形成,希望对你有所帮助。
现如今个性化推荐算法已经成为互联网产品不可缺少的一部分,本期课程的主要内容就是介绍一种最基础的 Collaborative filtering (协同过滤)

Itemcf 基于物品相似性的协同过滤

给用户推荐之前喜欢的物品相似物品;
  • 如何衡量相似?
  • 如何衡量喜欢?

基本公式

Item 相似性

分子部分表示物品 的重合程度,其中 表示对 有过行为的用户集合, 表示对 有过行为的用户集合;分母部分是对重合程度的归一化。

User 行为评分

其中 表示 对物品 的行为得分, 表示物品的相似度得分

公式升级

Item 相似性

  • 在实际的工程项目中,应当指出的是并不是所有的用户都应当具有相同的权值;例如批发商和普通用户的权重就不应该相等,如果用批发商的行为衡量用户喜好,那显然是不合适的;故应对以上公式进行如下改进:
  • 用户在不同时间对 的操作应给予时间衰减惩罚:
表示时间差异指标,其中 为一常数,当 越小, 取值与其接近于

Usercf 基于用户的协同过滤

给用户推荐相似兴趣用户感兴趣的物品:
  • 如何评价相似兴趣集合?
  • 如何找到集合用户感兴趣而目标用户没行为过的  ?

基本公式

User 行为相似度

User 行为评分

公式升级

User 行为相似度

  • 降低异常活跃物品对用户相似度的贡献:
  • 不同用户对同一 行为的时间段不同应该给予时间衰减惩罚:
表示时间衰减惩罚率,其中 表示用户 对物品 行为的时间, 表示用户 对物品 行为的时间,两个用户对同一物品操作时间的差越短,对同一物品行为的相似度也就越大,反之则越低。

Usercf vs Itemcf

优缺点比较

  • 推荐实时性
Usercf 基于用户行为相似度矩阵进行推荐,用户的新行为不一定能很快的影响推荐结果,具有一定的滞后性; Itemcf 则基于物品相似度矩阵,一旦用户对新的物品有了行为 (点击,加购,购买等),推荐结果会立即发生改变。
  • 新用户 / 新物品的推荐
Usercf 在对待新用户时,最初并不能给出较为准确的推荐结果,需要等用户有了一定的行为之后才能给出推荐结果; Itemcf 在对待新物品时,由于新物品没有与其他物品建立相似性矩阵,所以无法立即将新物品推荐给其他用户。
  • 推荐理由的可解释性
Usercf 基于用户行为进行推荐,可能出现一些特定用户用户并不感兴趣的推荐; Itemcf 基于物品相似性进行推荐,完全给予用户过往行为,推荐结果较为了信服。

适用场景

  • 性能层面
Usercf 适用于用户较少的场景;Itemcf 适用于用户量远大于物品量的场景; 在实际生产项目中,通常用户数都远远大于物品数,Itemcf 在更多时候成为首选。
  • 个性化层面
Usercf 适用于物品需要及时推荐下发而个性化不太强烈的领域; Itemcf 适用于常规物品丰富且需要高度个性化的领域。

示例代码

首先引入需要的模块:
from __future__ import division
import os
import sys
import math
import operator
读取 MovieLens 数据集:
  • 读取用户点击信息:
def get_user_click(rating_file):
    """
    get user click list
    Args:
        rating_file: input file
    Return:
        dict, key: userid, value: [itemid1, itemid2]
    """
    if not os.path.exists(rating_file):
        return {}, {}
    fp = open(rating_file)
    num = 0
    user_click = {}
    user_click_time = {}
    for line in fp:
        if num == 0:
            num += 1
            continue
        item = line.strip().split(',')
        if len(item) < 4:
            continue
        [userid, itemid, rating, timestamp] = item
        if userid + "_" + itemid not in user_click_time:
            user_click_time[userid + "_" + itemid] = int(timestamp)
        if float(rating) < 3.0:
            continue
        if userid not in user_click:
            user_click[userid] = []
        user_click[userid].append(itemid)
    fp.close()
    return user_click, user_click_time
  • 读取电影条目信息:
def get_item_info(item_file):
    """
    get item info[title, genres]
    Args:
        item_file: input item info file
    Return:
        a dict, key: itemid, value: [title, genres]
    """
    if not os.path.exists(item_file):
        return {}
    num = 0
    item_info = {}
    fp = open(item_file)
    for line in fp:
        if num == 0:
            num += 1
            continue
        item = line.strip().split(',')
        if len(item) < 3:
            continue
        if len(item) > 3:
            itemid = item[0]
            genres = item[-1]
            title = ",".join(item[1:-1])
        else:
            [itemid, title, genres] = item
        if itemid not in item_info:
            item_info[itemid] = [title, genres]
    fp.close()
    return item_info
公式算法:
  • 基础贡献分:
def base_contribute_score():
    """
    item cf / user cf base sim contribution by user
    """
    return 1
  • 升级 1: 对不同用户权重给予惩罚:
def enhance_one_contribute_score(user_total_click_num):
    """
    item cf enhance sim contribution score by user
    """
    return 1 / math.log10(1 + user_total_click_num)
  • 升级 2: 对用户行为给予时间衰减惩罚
def enhance_two_contribute_score(click_time_one, click_time_two):
    """
    itemcf update two sim contribution score by user
    """
    delta_time = abs(click_time_one - click_time_two)
    total_sec = 60 * 60 * 24
    delta_time = delta_time / total_sec
    return 1 / (1 + delta_time)

Itemcf

计算物品相似性:
def cal_item_sim(user_click, user_click_time):
    """
    Args:
        user_click: dict, ket userid, value [itemid1, itemid2]
    Return:
        dict, key: itemid_i, value dict, value_key itemid_j, value_value simscore
    """
    co_appear = {}
    item_user_click_time = {}
    for user, itemlist in user_click.items():
        for index_i in range(0, len(itemlist)):
            itemid_i = itemlist[index_i]
            item_user_click_time.setdefault(itemid_i, 0)
            item_user_click_time[itemid_i] += 1
            for index_j in range(index_i, len(itemlist)):
                itemid_j = itemlist[index_j]
                if user + "_" + itemid_i not in user_click_time:
                    click_time_one = 0
                else:
                    click_time_one = user_click_time[user + "_" + itemid_i]
                if user + "_" + itemid_j not in user_click_time:
                    click_time_two = 0
                else:
                    click_time_two = user_click_time[user + "_" + itemid_j]
                co_appear.setdefault(itemid_j, {})
                co_appear[itemid_i].setdefault(itemid_j, 0)
                # co_appear[itemid_i][itemid_j] += base_contribute_score()
                # co_appear[itemid_i][itemid_j] += enhance_one_contribute_score(len(itemlist))
                co_appear[itemid_i][itemid_j] += enhance_two_contribute_score(click_time_one, click_time_two)

                co_appear.setdefault(itemid_j, {})
                co_appear[itemid_j].setdefault(itemid_i, 0)
                # co_appear[itemid_j][itemid_i] += base_contribute_score()
                # co_appear[itemid_j][itemid_i] += enhance_one_contribute_score(len(itemlist))
                co_appear[itemid_j][itemid_i] += enhance_two_contribute_score(click_time_one, click_time_two)
    item_sim_score = {}
    item_sim_score_sorted = {}
    for itemid_i, relate_item in co_appear.items():
        for itemid_j, co_time in relate_item.items():
            sim_score = co_time / math.sqrt(item_user_click_time[itemid_i] * item_user_click_time[itemid_j])
            item_sim_score.setdefault(itemid_i, {})
            item_sim_score[itemid_i].setdefault(itemid_j, 0)
            item_sim_score[itemid_i][itemid_j] = sim_score
    for itemid in item_sim_score:
        item_sim_score_sorted[itemid] = sorted(item_sim_score[itemid].items(), key = \
                                              operator.itemgetter(1), reverse=True)

    return item_sim_score_sorted
计算推荐结果:
def itemcf_cal_recom_result(sim_info, user_click):
    """
    recom by itemcf
    Args:
        sim_info: item sim dict
        user_click: user click dict
    Return:
        dict, key: userid, value_key itemid, value_value recom score
    """
    recent_click_num = 3
    topk = 5
    recom_info = {}
    for user in user_click:
        click_list = user_click[user]
        recom_info.setdefault(user, {})
        for itemid in click_list[:recent_click_num]:
            if itemid not in sim_info:
                continue
            for item_sim_tuple in sim_info[itemid][:topk]:
                item_simid = item_sim_tuple[0]
                item_sim_score = item_sim_tuple[1]
                recom_info[user][item_simid] = item_sim_score
    return recom_info
辅助调试函数:
  • 调试物品相似性:
def debug_item_sim(item_info, sim_info):
    """
    show item sim info
    Args:
        item_info: dict, key itemid, value: [title, genres]
        sim_info: dict, key itemid, value dic, key [(itemid1, simscore), (itemid2, simscore)]
    """
    fixed_itemid = "1"
    if fixed_itemid not in item_info:
        print("invalid item id:", fixed_itemid)
        return
    [title_fix, genres_fix] = item_info[fixed_itemid]
    for item_tuple in sim_info[fixed_itemid][:5]:
        itemid_sim = item_tuple[0]
        sim_score = item_tuple[1]
        if itemid_sim not in item_info:
            continue
        [title, genres] = item_info[itemid_sim]
        print(title_fix + "\t" + genres_fix + "\tsim: " + title + "\t" + genres + "\t" + str(sim_score))
  • 调试推荐结果:
def debug_itemcf_recom_result(recom_result, item_info):
    """
    debug recom result
    Args:
        recom_result: key userid, value: dic, value_key: itemid, value_value: recom_score
        item_info: dict, key itemid, value: [title, genres]
    """
    user_id = "1"
    if user_id not in recom_result:
        print("invalid user id:", user_id)
        return
    for result_tuple in sorted(recom_result[user_id].items(), key=operator.itemgetter(1), reverse=True):
        item_id, score = result_tuple
        if item_id not in item_info:
            continue
        print(",".join(item_info[item_id]) + "\t" + str(score))
Itemcf 主流程:
def itemcf_main_flow():
    """
    main flow of itemcf
    """
    user_click, user_click_time = get_user_click('./data/ratings.csv')
    item_info = get_item_info('./data/movies.csv')
    sim_info = cal_item_sim(user_click, user_click_time)
    # debug_item_sim(item_info, sim_info)
    recom_result = itemcf_cal_recom_result(sim_info, user_click)
    # debug_itemcf_recom_result(recom_result, item_info)
    print(recom_result["1"])

if __name__ == "__main__":
    itemcf_main_flow()

Usercf

将 user_click 转化为 item_click_by_user:
def transfer_user_click(user_click):
    """
    get item by user click
    Args:
        user_click: a dict, key userid, value: [itemid1, itemid2]
    Return:
        dict, key: itemid, value: [userid1, userid2]
    """
    item_click_by_user = {}
    for user in user_click:
        item_list = user_click[user]
        for itemid in item_list:
            item_click_by_user.setdefault(itemid, [])
            item_click_by_user[itemid].append(user)
    return item_click_by_user

计算用户相似度:
def cal_user_sim(item_click_by_user, user_click_time):
    """
    get user sim info
    Args:
        item_click_by_user: dict, key: itemid, value: [itemid1, itemid2]
    Return:
        dict, key: itemid, value: dict, value_key: itemid_j, value_value: simscore
    """
    co_appear = {}
    user_click_count = {}
    for itemid, user_list in item_click_by_user.items():
        for index_i in range(0, len(user_list)):
            user_i = user_list[index_i]
            user_click_count.setdefault(user_i, 0)
            user_click_count[user_i] += 1
            if user_i + "_" + itemid not in user_click_time:
                click_time_one = 0
            else:
                click_time_one = user_click_time[user_i + "_" + itemid]
            for index_j in range(index_i + 1, len(user_list)):
                user_j = user_list[index_j]
                if user_j + "_" + itemid not in user_click_time:
                    click_time_two = 0
                else:
                    click_time_two = user_click_time[user_j + "_" + itemid]
                co_appear.setdefault(user_i, {})
                co_appear[user_i].setdefault(user_j, 0)
# co_appear[user_i][user_j] += base_contribute_score()# co_appear[user_i][user_j] += enhance_one_contribute_score(len(user_list))
                co_appear[user_i][user_j] += enhance_two_contribute_score(click_time_one, click_time_two)
                co_appear.setdefault(user_j, {})
                co_appear[user_j].setdefault(user_i, 0)
# co_appear[user_j][user_i] += base_contribute_score()# co_appear[user_j][user_i] += enhance_one_contribute_score(len(user_list))
                co_appear[user_j][user_i] += enhance_two_contribute_score(click_time_one, click_time_two)

    user_sim_info = {}
    user_sim_info_sorted = {}
    for user_i, relate_user in co_appear.items():
        user_sim_info.setdefault(user_i, {})
        for user_j, cotime in relate_user.items():
            user_sim_info[user_i].setdefault(user_j, 0)
            user_sim_info[user_i][user_j] = cotime / math.sqrt(user_click_count[user_i] * user_click_count[user_j])
    for user in user_sim_info:
        user_sim_info_sorted[user] = sorted(user_sim_info[user].items(), key = \
                                     operator.itemgetter(1), reverse=True)
    return user_sim_info_sorted

计算推荐结果:
def usercf_cal_recom_result(user_click, user_sim):
    """
    recom by usercf algo
    Args:
        user_click: dict, key userid, value [itemid1, itemid2]
        user_sim: key: userid, value:[(userid_j, score1), (userid_k, score2)]
    Return:
        dict, key userid, value: dict, value_key: itemid, value_value: recom_score
    """
    recom_result = {}
    topk_user = 3
    item_num = 5
    for user, item_list in user_click.items():
        tmp_dict = {}
        for itemid in item_list:
            tmp_dict.setdefault(itemid, 1)
        recom_result.setdefault(user, {})
        for user_tuple in user_sim[user][:topk_user]:
            userid_j, sim_score = user_tuple
            if userid_j not in user_click:
                continue
            for itemid_j in user_click[userid_j][:item_num]:
                recom_result[user].setdefault(itemid_j, sim_score)
    return recom_result

辅助调试函数:
  • 调试用户相似度:
def debug_user_sim(user_sim):
    """
    print user sim result
    Args:
        user_sim: key userid, value: [(userid1, score1), (userid2, score2)]
    """
    topk = 5
    fixed_user = "1"
    if fixed_user not in user_sim:
        print("invalid user id:", fixed_user)
        return
    for sim_tuple in user_sim[fixed_user][:topk]:
        userid, score = sim_tuple
        print(fixed_user + "\tsim_user: " + userid + "\t" + str(score))

  • 调试推荐结果:
def debug_usercf_recom_result(item_info, recom_result):
    """
    print recom result for usercf
    Args:
        item_info: key: itemid, value: [title, genres]
        recom_result: ket userid, value: dict, value_key: itemid, value_value: recom_score
    """
    fixed_user = "1"
    if fixed_user not in recom_result:
        print("invalid user id:", fixed_user)
        return
    for itemid in recom_result[fixed_user]:
        if itemid not in item_info:
            continue
        recom_score = recom_result[fixed_user][itemid]
        print("recom result: " + ",".join(item_info[itemid]) + "\t" + str(recom_score))

Usercf 主流程:
def usercf_main_flow():
    """
    main flow of usercf
    """
    user_click, user_click_time = get_user_click('./data/ratings.csv')
    item_info = get_item_info('./data/movies.csv')
    item_click_by_user = transfer_user_click(user_click)
    user_sim = cal_user_sim(item_click_by_user, user_click_time)
# debug_user_sim(user_sim)
    recom_result = usercf_cal_recom_result(user_click, user_sim)
# debug_usercf_recom_result(item_info, recom_result)
    print(recom_result["1"])

if __name__ == "__main__":
    usercf_main_flow()