推荐系统实践 0x07 基于邻域的算法(2)

基于邻域的算法(2)

上一篇咱们讲了基于用户的协同过滤算法,基本流程就是寻找与目标用户兴趣类似的用户,按照他们对物品喜爱的对目标用户进行推荐,其中哪些类似用户的评分要带上目标用户与类似用户的类似度做为权重来计算。可是,基于用户的协同过滤算法存在一些弊端,如计算用户兴趣类似度矩阵将愈来愈困难,其运算时间复杂度和空间复杂度的增加和用户数的增加近似于平方关系,另外也很难对推荐结果进行解释。那么,这一篇咱们继续来了解一下基于物品的协同过滤算法。python

基于物品的协同过滤算法(ItemCF)

基于物品的协同过滤算法是大多数网站经常使用的推荐算法的基础。ItemCF不会利用物品的内容属性计算物品之间的类似度,而是分析用户的行为记录计算物品之间的类似度。那么,ItemCF主要分为两个步骤:git

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

物品类似度

咱们能够用以下公式定义物品的类似度:github

\[w_{ij}=\frac{|N(i)\cap N(j)|}{|N(i)|} \]

\(N(i)\)是指喜欢物品\(i\)的用户数量,分子部分表示既喜欢物品\(i\)又喜欢物品\(j\)的用户有多少,整个类似度公式表示的是喜欢物品\(i\)的用户中,同时喜欢物品\(j\)的用户比例是多少。可使用归一化以后的结果做为物品类似度。可是若是物品\(j\)很热门人人都喜欢,那么整个类似度就会变成1,这对于推荐冷门物品的推荐系统来讲并非好事情,因此咱们对物品类似度公式进行改进。算法

\[w_{ij}=\frac{|N(i)\cap N(j)|}{\sqrt{|N(i)||N(j)|}} \]

这个公式惩罚了热门物品\(j\)的权重,必定程度缓和了这个问题。一样的,咱们在计算物品类似度的时候能够先创建一个用户-物品的倒排表,伪代码以下:app

def ItemSimilarity(train):
    # calculate co-rated users between items
    C = dict()
    N = dict()
    for u, items in train.items():
        for i in users:
            N[i] += 1
            for j in users:
                if i == j:
                    continue
                C[i][j] += 1
    # finial similarity matrix W W = dict()
    for i, related_items in C.items():
        for j, cij in related_items.items():
            W[u][v] = cij / math.sqrt(N[i] * N[j])
    return W

用户对物品的兴趣

ItemCF虽然没有利用内容属性计算类似度,可是最后获得的结果仍然是内容上某种类似的,如同主演,同分类等等的电影。在获得物品类似度以后,咱们用以下公式计算用户对物品的兴趣:dom

\[p_{uj}=\sum_{i\in N(u)\cap S(j,K)}w_{ji}r_{ui} \]

这里\(N(u)\)是用户喜欢的物品的集合,\(S(j,K)\)是和物品\(j\)最类似的\(K\)个物品的集合,\(w_{ji}\)是物品\(j\)\(i\)的类似度,\(r_{ui}\)是用户\(u\)对物品\(i\)的兴趣。函数

def Recommendation(train, user_id, W, K):
    rank = dict()
    ru = train[user_id]
    for i, pi in ru.items():
        for j, wj in sorted(W[i].items(), key=itemgetter(1),
                            reverse=True)[0:K]:
            if j in ru:
                continue
            rank[j] += pi * wj
    return rank

另外加就是用户活跃度对物品类似度产生的影响。一个不活跃的用户含有大量的感兴趣的物品,那么会产生稠密的物品类似度大矩阵,因此活跃用户对物品类似度的贡献应该小于不活跃的用户。那么公式修正为:测试

\[w_{ij}=\frac{\sum_{u\in N(i)\cap N(j)}\frac{1}{\log 1+|N(u)|}}{\sqrt{|N(i)||N(j)|}} \]

跟基于用户的协同过滤的修正公式很像啊。网站

def ItemSimilarity(train):
    #calculate co-rated users between items C = dict()
    N = dict()
    for u, items in train.items():
        for i in users:
            N[i] += 1
            for j in users:
                if i == j:
                    continue
            C[i][j] += 1 / math.log(1 + len(items) * 1.0)
    #calculate finial similarity matrix W W = dict()
    for i,related_items in C.items():
        for j, cij in related_items.items()
            W[u][v] = cij / math.sqrt(N[i] * N[j])
    return W

仍是得感谢@Magic-Bubble分享在github上代码,清晰易懂,省去我重复造轮子的时间。那么给出在MovieLens数据集上的实验代码:ui

# 导入包
import random
import math
import time
from tqdm import tqdm


# 定义装饰器,监控运行时间
def timmer(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        res = func(*args, **kwargs)
        stop_time = time.time()
        print('Func %s, run time: %s' %
              (func.__name__, stop_time - start_time))
        return res

    return wrapper


class Dataset():
    def __init__(self, fp):
        # fp: data file path
        self.data = self.loadData(fp)

    @timmer
    def loadData(self, fp):
        data = []
        for l in open(fp):
            data.append(tuple(map(int, l.strip().split('::')[:2])))
        return data

    @timmer
    def splitData(self, M, k, seed=1):
        '''
        :params: data, 加载的全部(user, item)数据条目
        :params: M, 划分的数目,最后须要取M折的平均
        :params: k, 本次是第几回划分,k~[0, M)
        :params: seed, random的种子数,对于不一样的k应设置成同样的
        :return: train, test
        '''
        train, test = [], []
        random.seed(seed)
        for user, item in self.data:
            # 这里与书中的不一致,本人认为取M-1较为合理,因randint是左右都覆盖的
            if random.randint(0, M - 1) == k:
                test.append((user, item))
            else:
                train.append((user, item))

        # 处理成字典的形式,user->set(items)
        def convert_dict(data):
            data_dict = {}
            for user, item in data:
                if user not in data_dict:
                    data_dict[user] = set()
                data_dict[user].add(item)
            data_dict = {k: list(data_dict[k]) for k in data_dict}
            return data_dict

        return convert_dict(train), convert_dict(test)


class Metric():
    def __init__(self, train, test, GetRecommendation):
        '''
        :params: train, 训练数据
        :params: test, 测试数据
        :params: GetRecommendation, 为某个用户获取推荐物品的接口函数
        '''
        self.train = train
        self.test = test
        self.GetRecommendation = GetRecommendation
        self.recs = self.getRec()

    # 为test中的每一个用户进行推荐
    def getRec(self):
        recs = {}
        for user in self.test:
            rank = self.GetRecommendation(user)
            recs[user] = rank
        return recs

    # 定义精确率指标计算方式
    def precision(self):
        all, hit = 0, 0
        for user in self.test:
            test_items = set(self.test[user])
            rank = self.recs[user]
            for item, score in rank:
                if item in test_items:
                    hit += 1
            all += len(rank)
        return round(hit / all * 100, 2)

    # 定义召回率指标计算方式
    def recall(self):
        all, hit = 0, 0
        for user in self.test:
            test_items = set(self.test[user])
            rank = self.recs[user]
            for item, score in rank:
                if item in test_items:
                    hit += 1
            all += len(test_items)
        return round(hit / all * 100, 2)

    # 定义覆盖率指标计算方式
    def coverage(self):
        all_item, recom_item = set(), set()
        for user in self.test:
            for item in self.train[user]:
                all_item.add(item)
            rank = self.recs[user]
            for item, score in rank:
                recom_item.add(item)
        return round(len(recom_item) / len(all_item) * 100, 2)

    # 定义新颖度指标计算方式
    def popularity(self):
        # 计算物品的流行度
        item_pop = {}
        for user in self.train:
            for item in self.train[user]:
                if item not in item_pop:
                    item_pop[item] = 0
                item_pop[item] += 1

        num, pop = 0, 0
        for user in self.test:
            rank = self.recs[user]
            for item, score in rank:
                # 取对数,防止因长尾问题带来的被流行物品所主导
                pop += math.log(1 + item_pop[item])
                num += 1
        return round(pop / num, 6)

    def eval(self):
        metric = {
            'Precision': self.precision(),
            'Recall': self.recall(),
            'Coverage': self.coverage(),
            'Popularity': self.popularity()
        }
        print('Metric:', metric)
        return metric


# 1. 基于物品余弦类似度的推荐
def ItemCF(train, K, N):
    '''
    :params: train, 训练数据集
    :params: K, 超参数,设置取TopK类似物品数目
    :params: N, 超参数,设置取TopN推荐物品数目
    :return: GetRecommendation, 推荐接口函数
    '''
    # 计算物品类似度矩阵
    sim = {}
    num = {}
    for user in train:
        items = train[user]
        for i in range(len(items)):
            u = items[i]
            if u not in num:
                num[u] = 0
            num[u] += 1
            if u not in sim:
                sim[u] = {}
            for j in range(len(items)):
                if j == i: continue
                v = items[j]
                if v not in sim[u]:
                    sim[u][v] = 0
                sim[u][v] += 1
    for u in sim:
        for v in sim[u]:
            sim[u][v] /= math.sqrt(num[u] * num[v])

    # 按照类似度排序
    sorted_item_sim = {k: list(sorted(v.items(), \
                               key=lambda x: x[1], reverse=True)) \
                       for k, v in sim.items()}

    # 获取接口函数
    def GetRecommendation(user):
        items = {}
        seen_items = set(train[user])
        for item in train[user]:
            for u, _ in sorted_item_sim[item][:K]:
                if u not in seen_items:
                    if u not in items:
                        items[u] = 0
                    items[u] += sim[item][u]
        recs = list(sorted(items.items(), key=lambda x: x[1],
                           reverse=True))[:N]
        return recs

    return GetRecommendation


# 2. 基于改进的物品余弦类似度的推荐
def ItemIUF(train, K, N):
    '''
    :params: train, 训练数据集
    :params: K, 超参数,设置取TopK类似物品数目
    :params: N, 超参数,设置取TopN推荐物品数目
    :return: GetRecommendation, 推荐接口函数
    '''
    # 计算物品类似度矩阵
    sim = {}
    num = {}
    for user in train:
        items = train[user]
        for i in range(len(items)):
            u = items[i]
            if u not in num:
                num[u] = 0
            num[u] += 1
            if u not in sim:
                sim[u] = {}
            for j in range(len(items)):
                if j == i: continue
                v = items[j]
                if v not in sim[u]:
                    sim[u][v] = 0
                # 相比ItemCF,主要是改进了这里
                sim[u][v] += 1 / math.log(1 + len(items))
    for u in sim:
        for v in sim[u]:
            sim[u][v] /= math.sqrt(num[u] * num[v])

    # 按照类似度排序
    sorted_item_sim = {k: list(sorted(v.items(), \
                               key=lambda x: x[1], reverse=True)) \
                       for k, v in sim.items()}

    # 获取接口函数
    def GetRecommendation(user):
        items = {}
        seen_items = set(train[user])
        for item in train[user]:
            for u, _ in sorted_item_sim[item][:K]:
                # 要去掉用户见过的
                if u not in seen_items:
                    if u not in items:
                        items[u] = 0
                    items[u] += sim[item][u]
        recs = list(sorted(items.items(), key=lambda x: x[1],
                           reverse=True))[:N]
        return recs

    return GetRecommendation


# 3. 基于归一化的物品余弦类似度的推荐
def ItemCF_Norm(train, K, N):
    '''
    :params: train, 训练数据集
    :params: K, 超参数,设置取TopK类似物品数目
    :params: N, 超参数,设置取TopN推荐物品数目
    :return: GetRecommendation, 推荐接口函数
    '''
    # 计算物品类似度矩阵
    sim = {}
    num = {}
    for user in train:
        items = train[user]
        for i in range(len(items)):
            u = items[i]
            if u not in num:
                num[u] = 0
            num[u] += 1
            if u not in sim:
                sim[u] = {}
            for j in range(len(items)):
                if j == i: continue
                v = items[j]
                if v not in sim[u]:
                    sim[u][v] = 0
                sim[u][v] += 1
    for u in sim:
        for v in sim[u]:
            sim[u][v] /= math.sqrt(num[u] * num[v])

    # 对类似度矩阵进行按行归一化
    for u in sim:
        s = 0
        for v in sim[u]:
            s += sim[u][v]
        if s > 0:
            for v in sim[u]:
                sim[u][v] /= s

    # 按照类似度排序
    sorted_item_sim = {k: list(sorted(v.items(), \
                               key=lambda x: x[1], reverse=True)) \
                       for k, v in sim.items()}

    # 获取接口函数
    def GetRecommendation(user):
        items = {}
        seen_items = set(train[user])
        for item in train[user]:
            for u, _ in sorted_item_sim[item][:K]:
                if u not in seen_items:
                    if u not in items:
                        items[u] = 0
                    items[u] += sim[item][u]
        recs = list(sorted(items.items(), key=lambda x: x[1],
                           reverse=True))[:N]
        return recs

    return GetRecommendation


class Experiment():
    def __init__(self, M, K, N, fp='../dataset/ml-1m/ratings.dat',
                 rt='ItemCF'):
        '''
        :params: M, 进行多少次实验
        :params: K, TopK类似物品的个数
        :params: N, TopN推荐物品的个数
        :params: fp, 数据文件路径
        :params: rt, 推荐算法类型
        '''
        self.M = M
        self.K = K
        self.N = N
        self.fp = fp
        self.rt = rt
        self.alg = {
            'ItemCF': ItemCF,
            'ItemIUF': ItemIUF,
            'ItemCF-Norm': ItemCF_Norm
        }

    # 定义单次实验
    @timmer
    def worker(self, train, test):
        '''
        :params: train, 训练数据集
        :params: test, 测试数据集
        :return: 各指标的值
        '''
        getRecommendation = self.alg[self.rt](train, self.K, self.N)
        metric = Metric(train, test, getRecommendation)
        return metric.eval()

    # 屡次实验取平均
    @timmer
    def run(self):
        metrics = {'Precision': 0, 'Recall': 0, 'Coverage': 0, 'Popularity': 0}
        dataset = Dataset(self.fp)
        for ii in range(self.M):
            train, test = dataset.splitData(self.M, ii)
            print('Experiment {}:'.format(ii))
            metric = self.worker(train, test)
            metrics = {k: metrics[k] + metric[k] for k in metrics}
        metrics = {k: metrics[k] / self.M for k in metrics}
        print('Average Result (M={}, K={}, N={}): {}'.format(\
                              self.M, self.K, self.N, metrics))


# 1. ItemCF实验
M, N = 8, 10
for K in [5, 10, 20, 40, 80, 160]:
    cf_exp = Experiment(M, K, N, rt='ItemCF')
    cf_exp.run()

# 2. ItemIUF实验
M, N = 8, 10
K = 10  # 与书中保持一致
iuf_exp = Experiment(M, K, N, rt='ItemIUF')
iuf_exp.run()

# 3. ItemCF-Norm实验
M, N = 8, 10
K = 10  # 与书中保持一致
norm_exp = Experiment(M, K, N, rt='ItemCF-Norm')
norm_exp.run()

参考

《推荐系统实践》(项亮等著) —— 代码实现

相关文章
相关标签/搜索