☰
协同过滤入门:UserCF与ItemCF原理及实操详解
2026/10/15 22:16:21 网站建设 项目流程

1. 推荐系统入门:为什么协同过滤依然是绕不开的基石

做推荐系统这些年,我越来越觉得,很多新手一上来就盯着深度学习、大模型、双塔召回这些热词,反而把最经典的协同过滤给跳过了。结果就是模型跑起来了,但一遇到稀疏数据、冷启动、可解释性要求高的场景,整个人就懵了。其实协同过滤这套东西,尤其是基于邻域的方法,到今天依然是工业界推荐系统的底座之一。你去看很多中小型平台的推荐模块,或者大系统里的召回层,UserCF和ItemCF的影子无处不在。

这篇文章我想把协同过滤里基于邻域的方法彻底讲透,重点就是UserCF和ItemCF这两条路线。我会从设计思路、核心原理、相似度计算、完整实操流程,一直讲到常见问题和排查技巧。适合谁看?如果你刚接触推荐系统,想找一个能真正跑起来、能解释清楚、能落地的入门方案,那这篇就是给你写的。如果你已经做过一些推荐项目,但对相似度计算、邻居选择、评分预测这些细节还是一知半解,那这篇也能帮你把地基夯实。

先说清楚一个前提:协同过滤的核心思想特别朴素,就是“物以类聚,人以群分”。你喜欢的東西,跟你口味相似的人也可能喜欢;你喜欢过的某类东西,跟它相似的东西你大概率也会喜欢。UserCF走的是前一条路,ItemCF走的是后一条路。听起来简单,但真正落地的时候,相似度怎么算、邻居怎么选、评分怎么预测、冷启动怎么处理,每一步都有讲究。我见过太多人直接把余弦相似度一算就上线,结果推荐效果惨不忍睹,问题就出在细节上。

所以这篇文章不会只给你公式,我会把每个参数背后的逻辑、每个步骤的操作意图、每个坑的排查思路都讲清楚。你跟着走一遍,基本就能在自己的数据集上复现出一套可用的协同过滤推荐。

2. 基于邻域的协同过滤整体设计思路拆解

2.1 为什么选择基于邻域的方法而不是模型方法

协同过滤大致分两条技术路线:基于邻域的方法和基于模型的方法。基于模型的方法包括矩阵分解、隐语义模型、图神经网络等等,它们通过训练一个模型来学习用户和物品的隐向量表示。基于邻域的方法则不做全局训练,而是直接在用户-物品交互矩阵上做文章,通过相似度找邻居,再用邻居的评分来预测目标。

那为什么还要用基于邻域的方法?我总结下来有几个实打实的理由。第一,可解释性强。你给用户推荐一个东西,可以明确告诉他“因为你跟某类用户口味相似”或者“因为这个东西跟你之前喜欢的某样东西很像”。这种解释在电商、内容平台里非常重要,用户信任感直接拉满。第二,实现简单,不需要复杂的训练流程和调参。第三,增量更新方便。新来一个用户或者新上一个物品,不需要重新训练整个模型,直接算相似度就行。第四,在小规模数据上,基于邻域的方法往往能跟矩阵分解打得有来有回,甚至更好。

当然,它也有明显短板。数据稀疏的时候相似度计算不准,用户和物品数量大了之后相似度矩阵存储和计算成本高,泛化能力不如模型方法。但作为入门和基线,它绝对是第一选择。我的建议是,任何推荐项目,先跑一版UserCF和ItemCF作为baseline,后面再上复杂模型,这样你才知道复杂模型到底带来了多少提升。

2.2 UserCF和ItemCF的核心差异与适用场景

UserCF和ItemCF虽然都属于基于邻域的方法,但它们的出发点和适用场景差别很大。UserCF是“找相似的人”,ItemCF是“找相似的物”。这个差异直接决定了它们在工程上的表现。

UserCF更适合用户数量相对较少、物品数量多、用户兴趣比较稳定且社交属性强的场景。比如新闻推荐,用户看新闻的兴趣点相对集中,而且新闻更新极快,ItemCF维护物品相似度矩阵的成本会非常高,因为物品一直在变。这时候UserCF就更合适,因为用户群体相对稳定,算用户相似度更划算。

ItemCF则更适合物品数量相对稳定、用户数量庞大、物品更新不那么频繁的场景。比如电商、视频网站。电商里商品虽然多,但核心商品相对稳定,用户却是一直在增长的。而且ItemCF有个天然优势:用户的兴趣会漂移,但物品之间的关联相对稳定。你今天喜欢这个牌子的鞋,明天可能喜欢别的,但“买了鞋的人往往也买袜子”这个关联不会变。所以ItemCF在工业界用得更多,亚马逊早期的推荐就是ItemCF的经典案例。

还有一个关键差异:UserCF的推荐结果更偏向“热点”和“大众口味”,因为它是基于群体行为;ItemCF的推荐结果更偏向“个性化”和“长尾”,因为它基于物品关联。你如果想要推荐多样性,ItemCF通常表现更好。

2.3 相似度计算的选型逻辑

相似度计算是整个协同过滤的核心。常用的有余弦相似度、皮尔逊相关系数、修正余弦相似度、杰卡德相似度等。选哪个,直接决定推荐质量。

余弦相似度最常用,它衡量两个向量在方向上的接近程度,对绝对数值不敏感。在用户-物品评分矩阵里,两个用户的评分向量夹角越小,说明兴趣越接近。但它有个问题:不同用户的评分尺度不一样。有人打分普遍偏高,有人普遍偏低,余弦相似度会把这种尺度差异忽略掉,导致相似度失真。

皮尔逊相关系数就是来解决这个问题的。它先减去每个用户的平均分,再做相关计算,相当于做了中心化。这样就能消除用户评分尺度的影响。但皮尔逊也有缺点,如果一个用户只对很少物品打过分,他的评分向量方差很小,相关系数会不稳定。

修正余弦相似度则是从物品角度做中心化,减去物品的平均分。它在ItemCF里用得比较多,因为物品的评分分布差异也很大。

杰卡德相似度适合隐式反馈场景,比如用户有没有点击、有没有购买,只有0和1。它算的是交集除以并集,简单直接。

我的经验是:显式评分场景优先用皮尔逊或修正余弦,隐式反馈场景用余弦或杰卡德。具体选哪个,最好在验证集上跑一下对比,别拍脑袋。

3. 核心细节解析与实操要点

3.1 用户-物品交互矩阵的构建与处理

一切从数据开始。协同过滤的输入是一个用户-物品交互矩阵,行是用户,列是物品,值是评分或者交互强度。显式反馈就是1到5分的评分,隐式反馈就是点击次数、观看时长、购买次数等。

构建这个矩阵的时候,有几个坑必须注意。第一,缺失值处理。矩阵里大量位置是空的,表示用户没交互过。这些空值不能简单填0,因为0在评分场景里可能表示“非常不喜欢”,跟“没看过”完全是两回事。通常的做法是保持稀疏,只在有交互的位置计算。第二,数据归一化。不同用户的评分尺度差异很大,做UserCF之前最好对每个用户的评分做中心化,减去他的平均分。第三,隐式反馈的强度转换。点击一次和购买一次,权重肯定不一样。我一般会给不同行为赋不同权重,比如浏览1分、收藏3分、购买5分,然后再做归一化。

实操上,我习惯用稀疏矩阵来存储,Python里就是scipy的csr_matrix。稠密矩阵在用户和物品上万之后内存直接爆炸。构建的时候用字典记录(user, item, rating),然后转成稀疏矩阵。

import numpy as np from scipy.sparse import csr_matrix # 假设数据是 (user_id, item_id, rating) 的列表 rows, cols, data = [], [], [] for u, i, r in interactions: rows.append(u) cols.append(i) data.append(r) n_users = max(rows) + 1 n_items = max(cols) + 1 matrix = csr_matrix((data, (rows, cols)), shape=(n_users, n_items))

这段代码看起来简单,但有个细节:user_id和item_id必须是连续的整数,从0开始。如果你的原始ID是字符串或者不连续,必须先做映射。我见过有人直接拿原始ID当索引,结果矩阵维度爆炸,内存直接撑爆。

3.2 相似度矩阵的计算与优化

相似度矩阵是协同过滤的计算核心。UserCF要算用户-用户相似度矩阵,ItemCF要算物品-物品相似度矩阵。这个矩阵的大小是N乘N,N是用户数或物品数。如果用户有十万,这个矩阵就是一百亿个元素,根本存不下。

所以实际工程里,我们不会算完整的相似度矩阵,而是只保留每个用户或物品的Top-K个最近邻。这样存储量就从N方降到了N乘K。K一般取20到100之间,具体看数据规模。

计算相似度的时候,用稀疏矩阵的乘法可以大幅加速。比如余弦相似度,本质上是归一化后的矩阵乘自己的转置。用scipy的稀疏矩阵运算,几万用户几万物品的规模,几秒钟就能算完。

from sklearn.preprocessing import normalize # 对行做归一化,然后算余弦相似度 matrix_norm = normalize(matrix, axis=1, norm='l2') user_similarity = matrix_norm.dot(matrix_norm.T)

但这里有个问题:算出来的相似度矩阵是稠密的,还是存不下。所以算完之后要立刻做Top-K截断,只保留每个用户最相似的K个,其他的置零。然后再转成稀疏矩阵。

注意:做Top-K截断的时候,一定要把用户自己排除掉。自己跟自己的相似度是1,如果不排除,推荐的时候会把自己交互过的物品又推一遍,毫无意义。

另外,相似度计算之前,最好对矩阵做中心化。UserCF减去用户平均分,ItemCF减去物品平均分。这样算出来的相似度更准。中心化之后,矩阵会变稠密,因为原来缺失的位置减去平均分就不是零了。这时候要重新把缺失位置置零,只保留有交互的位置。

3.3 邻居选择与评分预测

有了相似度矩阵,下一步就是选邻居和预测评分。UserCF的思路是:对于目标用户u和候选物品i,找到跟u最相似的K个用户中,对i有过交互的那些人,用他们的评分加权平均来预测u对i的评分。权重就是相似度。

公式是这样的:预测评分等于相似度加权平均,分母是相似度绝对值之和。如果所有邻居都没交互过i,那就没法预测,直接跳过。

ItemCF反过来:对于目标用户u和候选物品i,找到跟i最相似的K个物品中,u交互过的那些,用u对这些物品的评分加权平均来预测。

这里有个关键细节:相似度可能是负数。皮尔逊相关系数范围是-1到1,负相似度表示负相关。加权平均的时候,如果直接用相似度做权重,负权重会导致预测值偏离。所以通常的做法是只保留正相似度的邻居,或者对相似度取绝对值。我一般只保留正相似度,因为负相关的邻居对推荐没有正向帮助。

还有一个细节:邻居数量K的选择。K太小,预测不稳定,容易受个别邻居影响;K太大,计算量大,而且会引入不相关的邻居,降低精度。我的经验是,K取20到50之间比较稳妥。数据稀疏的时候可以适当增大,数据稠密的时候可以减小。最好在验证集上做网格搜索。

评分预测完之后,还要做一件事:过滤掉用户已经交互过的物品。推荐系统不是让你重复消费,而是发现新东西。所以预测之前,先把用户已经看过的、买过的、评过分的物品从候选集里去掉。

4. 完整实操流程与核心环节实现

4.1 数据准备与预处理实操

我拿一个模拟数据集来演示完整流程。假设我们有一个小型电商平台的用户行为数据,包含用户ID、物品ID、评分。数据量大概几千个用户、几百个物品、几万条评分。

第一步,加载数据并做ID映射。把原始的用户ID和物品ID映射成从0开始的连续整数。

import pandas as pd df = pd.read_csv('ratings.csv') user_ids = df['user_id'].unique() item_ids = df['item_id'].unique() user_to_idx = {uid: idx for idx, uid in enumerate(user_ids)} item_to_idx = {iid: idx for idx, iid in enumerate(item_ids)} df['user_idx'] = df['user_id'].map(user_to_idx) df['item_idx'] = df['item_id'].map(item_to_idx)

第二步,划分训练集和测试集。通常按时间划分,用前面的数据训练,后面的数据测试。如果没有时间戳,就随机划分,比如80%训练,20%测试。但要注意,每个用户在测试集里至少要有一个交互,否则没法评估。

第三步,构建训练集的稀疏矩阵,并做中心化。UserCF减去用户平均分,ItemCF减去物品平均分。

from scipy.sparse import csr_matrix train_matrix = csr_matrix( (df_train['rating'], (df_train['user_idx'], df_train['item_idx'])), shape=(n_users, n_items) ) # UserCF中心化:减去每个用户的平均分 user_means = np.array(train_matrix.sum(axis=1)).flatten() / np.array((train_matrix != 0).sum(axis=1)).flatten() user_means = np.nan_to_num(user_means) # 对非零位置减去用户平均分 train_matrix_centered = train_matrix.copy().astype(float) for u in range(n_users): start, end = train_matrix.indptr[u], train_matrix.indptr[u+1] train_matrix_centered.data[start:end] -= user_means[u]

这段代码里有个细节:计算用户平均分的时候,分母是该用户有评分的物品数量,不是总物品数。如果某个用户一个评分都没有,平均分会是NaN,要处理掉。

4.2 UserCF相似度计算与推荐生成

中心化之后,算用户相似度。用归一化后的矩阵乘转置。

from sklearn.preprocessing import normalize train_norm = normalize(train_matrix_centered, axis=1, norm='l2') user_sim = train_norm.dot(train_norm.T).toarray() # 排除自身 np.fill_diagonal(user_sim, 0) # Top-K截断 K = 30 user_sim_topk = np.zeros_like(user_sim) for u in range(n_users): top_k_idx = np.argsort(user_sim[u])[-K:] user_sim_topk[u, top_k_idx] = user_sim[u, top_k_idx]

然后生成推荐。对于每个用户,遍历所有候选物品,找到K个邻居中有评分的,加权平均预测评分。

def predict_usercf(u, i, user_sim_topk, train_matrix, user_means, K=30): neighbors = np.nonzero(user_sim_topk[u])[0] if len(neighbors) == 0: return user_means[u] neighbor_ratings = train_matrix[neighbors, i].toarray().flatten() neighbor_sims = user_sim_topk[u, neighbors] mask = neighbor_ratings != 0 if mask.sum() == 0: return user_means[u] weighted_sum = np.sum(neighbor_sims[mask] * (neighbor_ratings[mask] - user_means[neighbors[mask]])) sim_sum = np.sum(np.abs(neighbor_sims[mask])) if sim_sum == 0: return user_means[u] return user_means[u] + weighted_sum / sim_sum

这个函数里,我用了中心化后的评分做加权,最后再加回用户平均分。这样预测更准。注意分母用的是相似度绝对值之和,防止负相似度导致分母为零或负数。

生成推荐列表的时候,对每个用户,把所有未交互物品的预测评分算出来,排序取Top-N。实际工程里不会全量算,而是先用召回策略缩小候选集,再算评分。但作为入门,全量算一遍理解原理就够了。

4.3 ItemCF相似度计算与推荐生成

ItemCF的流程类似,但相似度矩阵是物品-物品的。先对训练矩阵做物品中心化,减去每个物品的平均分。

item_means = np.array(train_matrix.sum(axis=0)).flatten() / np.array((train_matrix != 0).sum(axis=0)).flatten() item_means = np.nan_to_num(item_means) train_matrix_item_centered = train_matrix.copy().astype(float).T for i in range(n_items): start, end = train_matrix_item_centered.indptr[i], train_matrix_item_centered.indptr[i+1] train_matrix_item_centered.data[start:end] -= item_means[i] train_matrix_item_centered = train_matrix_item_centered.T

然后算物品相似度,同样做Top-K截断。

train_item_norm = normalize(train_matrix_item_centered.T, axis=1, norm='l2') item_sim = train_item_norm.dot(train_item_norm.T).toarray() np.fill_diagonal(item_sim, 0) K = 30 item_sim_topk = np.zeros_like(item_sim) for i in range(n_items): top_k_idx = np.argsort(item_sim[i])[-K:] item_sim_topk[i, top_k_idx] = item_sim[i, top_k_idx]

预测的时候,对于用户u和物品i,找到跟i最相似的K个物品中,u交互过的那些,加权平均。

def predict_itemcf(u, i, item_sim_topk, train_matrix, item_means, K=30): similar_items = np.nonzero(item_sim_topk[i])[0] if len(similar_items) == 0: return item_means[i] user_ratings = train_matrix[u, similar_items].toarray().flatten() item_sims = item_sim_topk[i, similar_items] mask = user_ratings != 0 if mask.sum() == 0: return item_means[i] weighted_sum = np.sum(item_sims[mask] * (user_ratings[mask] - item_means[similar_items[mask]])) sim_sum = np.sum(np.abs(item_sims[mask])) if sim_sum == 0: return item_means[i] return item_means[i] + weighted_sum / sim_sum

ItemCF有个额外的好处:可以预先算好物品相似度矩阵,线上服务的时候直接查表,响应速度很快。UserCF则需要实时算用户相似度,因为新用户不断进来,用户相似度矩阵更新频繁。

4.4 评估指标与效果验证

推荐系统常用的评估指标有RMSE、MAE、Precision@K、Recall@K、NDCG@K等。评分预测任务用RMSE和MAE,Top-N推荐任务用Precision、Recall、NDCG。

RMSE就是预测评分和真实评分的均方根误差,越小越好。MAE是平均绝对误差。这两个指标直观,但跟实际业务效果不完全对应。因为用户更关心推荐列表里有没有他喜欢的,而不是评分预测准不准。

所以我更推荐用Precision@K和Recall@K。Precision@K是推荐列表前K个里,用户实际交互过的比例。Recall@K是用户实际交互的物品中,被推荐列表覆盖的比例。NDCG@K则考虑了位置因素,越靠前的推荐权重越高。

实操上,我会把测试集里每个用户的交互物品作为ground truth,然后看推荐列表命中多少。K一般取10或20。

def precision_at_k(recommended, actual, k): rec_k = recommended[:k] hit = len(set(rec_k) & set(actual)) return hit / k def recall_at_k(recommended, actual, k): rec_k = recommended[:k] hit = len(set(rec_k) & set(actual)) return hit / len(actual)

跑完评估,你就能对比UserCF和ItemCF在你的数据上谁更好。我的经验是,物品数量远大于用户数量的时候,ItemCF通常更稳。用户数量少、物品更新快的时候,UserCF可能更好。

5. 常见问题与排查技巧实录

5.1 数据稀疏与冷启动问题排查

协同过滤最大的敌人就是数据稀疏。用户-物品矩阵里99%都是空的,相似度计算就会非常不准。我遇到过好几次,新用户进来,系统完全不知道该推荐什么,因为找不到相似邻居。

冷启动分两种:用户冷启动和物品冷启动。用户冷启动就是新用户没有历史行为,UserCF找不到相似用户,ItemCF也找不到他交互过的物品。物品冷启动就是新物品没人交互过,ItemCF算不出相似度。

解决思路有几个。第一,利用注册信息做粗粒度推荐。比如用户填了年龄、性别、地域,可以先按这些属性分组,推荐组内热门物品。第二,利用物品内容特征。新物品虽然没有交互,但有标题、分类、标签,可以用内容相似度做兜底。第三,引导用户做初始反馈。新用户进来先让他选几个感兴趣的标签,快速积累行为数据。

数据稀疏的另一个表现是相似度矩阵里大量零值。这时候可以降低相似度阈值,或者增大邻居数量K。但也不能无限增大,否则会引入噪声。我的做法是设一个最小共同交互数阈值,比如两个用户至少共同交互过3个物品,才算有效邻居。这样能过滤掉偶然相似。

实操心得:处理稀疏数据的时候,隐式反馈比显式评分更鲁棒。因为隐式反馈只有0和1,不存在评分尺度问题,而且数据量通常更大。如果显式评分太少,不妨把隐式行为也纳入进来,做加权融合。

5.2 相似度计算异常与数值稳定性

相似度计算有几个常见的数值问题。第一,除零错误。如果某个用户所有评分都一样,中心化之后全是零,归一化的时候分母为零。解决办法是加一个极小值epsilon,或者直接跳过这种用户。第二,相似度为NaN。如果两个用户没有共同交互物品,余弦相似度算出来是0,但皮尔逊相关系数会算成NaN。要提前判断共同交互数,不够就置零。第三,相似度矩阵不对称。理论上余弦相似度是对称的,但浮点运算误差可能导致微小不对称。一般不影响使用,但如果要严格对称,可以取平均值。

还有一个坑:Top-K截断的时候,如果相似度有并列,argsort的结果不稳定。可以用argsort的kind参数指定稳定排序,或者随机打乱后再取。

top_k_idx = np.argsort(user_sim[u], kind='stable')[-K:]

数值稳定性还体现在评分预测的加权平均上。如果相似度之和非常小,预测值会剧烈波动。我一般会设一个最小相似度和阈值,低于这个值就用全局平均分兜底。

5.3 推荐结果多样性与惊喜度不足

协同过滤有个通病:推荐结果容易集中在热门物品上,多样性差。UserCF尤其明显,因为它是基于群体行为的,大家都喜欢的东西会被反复推荐。ItemCF稍好,但也会陷入局部关联。

提升多样性的方法有几个。第一,在相似度计算里加入惩罚项,降低热门物品的权重。比如用TF-IDF的思想,物品越热门,它在相似度计算中的权重越低。第二,推荐列表做打散。同一类别的物品不要连续出现,可以按类别轮询。第三,引入随机性。在Top-N里随机替换一两个位置,给长尾物品曝光机会。第四,多路召回融合。UserCF和ItemCF各出一路,再加一路热门兜底,最后融合排序。

惊喜度是另一个维度。用户可能对某个物品没交互过,但潜在感兴趣。协同过滤很难发现这种惊喜,因为它只基于历史行为。要提升惊喜度,可以引入内容特征或者知识图谱,做跨域推荐。但这就超出协同过滤的范围了。

5.4 性能瓶颈与工程优化

协同过滤在工程上的瓶颈主要是相似度矩阵的计算和存储。用户或物品上万之后,N乘N的矩阵内存吃不消。优化手段有几个。

第一,用稀疏矩阵和Top-K截断,前面已经讲了。第二,用局部敏感哈希做近似最近邻搜索,避免全量计算。第三,用矩阵分块计算,每次只算一部分,减少内存峰值。第四,离线预计算。ItemCF的物品相似度可以离线算好,线上直接查。UserCF的用户相似度可以定时更新,比如每天凌晨跑一次。

线上服务的时候,推荐生成也要优化。不要全量算所有候选物品的评分,而是先用倒排索引召回。比如UserCF,先找到目标用户的K个邻居,然后把这K个邻居交互过的物品作为候选集,只算这些物品的评分。这样候选集从全量物品降到几千个,速度提升几个数量级。

# UserCF召回:邻居交互过的物品作为候选 candidates = set() for neighbor in neighbors: items = train_matrix[neighbor].nonzero()[1] candidates.update(items) candidates = candidates - set(train_matrix[u].nonzero()[1]) # 去掉已交互

这个召回策略简单有效,我实测下来,候选集能缩小到原来的百分之一甚至千分之一,而召回率损失很小。

5.5 常见问题速查表

问题现象可能原因排查思路解决方案
推荐结果全是热门物品相似度计算未惩罚热门检查物品流行度分布加入TF-IDF惩罚或降低热门权重
新用户无推荐用户冷启动检查新用户交互数用注册信息或热门兜底
相似度为NaN无共同交互或方差为零检查共同交互数设最小共同交互阈值
预测评分超出范围加权平均未截断检查预测值分布截断到评分区间
内存溢出相似度矩阵过大检查矩阵维度Top-K截断或分块计算
推荐重复已交互物品未过滤历史行为检查候选集预测前去掉已交互
线上响应慢全量计算评分检查候选集大小倒排索引召回
效果不稳定K值选择不当网格搜索K值验证集调参

这张表是我这些年踩坑总结出来的,基本覆盖了协同过滤落地时80%的问题。遇到问题先查表,能省不少时间。

6. UserCF与ItemCF的选型对比与融合策略

6.1 什么场景选UserCF,什么场景选ItemCF

选型没有绝对的对错,关键看场景。我总结了一个简单的判断逻辑。

用户数量少、物品数量多、物品更新快、需要强社交解释的场景,选UserCF。比如新闻、短视频、社交动态。这些场景里,用户兴趣相对稳定,物品生命周期短,UserCF的实时性优势明显。

物品数量相对稳定、用户数量庞大、物品更新慢、需要长尾推荐的场景,选ItemCF。比如电商、图书、电影。这些场景里,物品关联稳定,ItemCF的离线预计算优势明显,而且推荐多样性更好。

如果拿不准,就两个都跑,用A/B测试看数据。我做过的一个项目里,UserCF的点击率比ItemCF高5%,但ItemCF的转化率高8%。最后是两路融合,各取所长。

6.2 融合策略与加权方案

融合UserCF和ItemCF最简单的方式是加权平均。对每个候选物品,分别用UserCF和ItemCF预测评分,然后加权求和。

final_score = alpha * usercf_score + (1 - alpha) * itemcf_score

alpha是权重,0到1之间。alpha越大,越偏向UserCF。调alpha的时候,在验证集上做网格搜索,步长0.1就行。

更高级的融合是级联。先用一路召回,再用另一路排序。比如UserCF召回候选集,ItemCF对候选集重新打分排序。这样既利用了UserCF的召回能力,又利用了ItemCF的排序精度。

还有一种融合是特征拼接。把UserCF和ItemCF的预测分作为两个特征,再加上用户特征、物品特征,一起喂给一个逻辑回归或GBDT模型。这样模型能自动学习最优融合权重。工业界用得比较多的是这种。

注意:融合之前,两路分数最好做归一化,否则量纲不一致会导致加权失效。可以用Min-Max归一化或者Z-Score标准化。

6.3 从协同过滤到矩阵分解的平滑过渡

协同过滤跑通之后,下一步自然就是矩阵分解。矩阵分解可以看作协同过滤的升级版,它把用户和物品映射到低维隐空间,用隐向量内积来预测评分。相比基于邻域的方法,矩阵分解泛化能力更强,能处理更稀疏的数据。

但矩阵分解也有代价:可解释性变差,训练成本变高,增量更新麻烦。所以我的建议是,先用协同过滤把baseline跑出来,记录好评估指标。然后上矩阵分解,对比提升幅度。如果提升不明显,说明数据量或特征还不够,继续用协同过滤就行。如果提升明显,再考虑上线矩阵分解。

从协同过滤过渡到矩阵分解,代码改动其实不大。输入还是那个用户-物品矩阵,只是把相似度计算换成了梯度下降训练隐向量。评估指标也完全一样。所以协同过滤的经验完全可以复用。

7. 个人实操体会与后续扩展方向

协同过滤这套东西,我用了很多年,最大的体会就是:别小看经典方法。很多复杂模型在特定场景下未必打得过调好参的UserCF或ItemCF。尤其是数据量不大、可解释性要求高的时候,协同过滤就是最优解。

还有一个体会是,工程细节决定成败。相似度怎么算、邻居怎么选、K值取多少、怎么过滤已交互、怎么处理冷启动,这些细节比模型选择更重要。我见过太多人模型选得很 fancy,但数据预处理一塌糊涂,最后效果还不如一个调好参的ItemCF。

后续扩展的话,有几个方向值得尝试。第一,把协同过滤和内容特征结合,做混合推荐。第二,引入时间衰减,让近期行为权重更高。第三,用图算法做推荐,把用户和物品看成二部图,用随机游走生成推荐。第四,把协同过滤作为召回层,后面接排序模型,形成完整的推荐链路。

最后分享一个小技巧:调参的时候,先固定K值调相似度算法,再固定相似度算法调K值,最后调融合权重。不要三个一起调,否则你根本不知道哪个参数起了作用。这个顺序能帮你快速定位最优组合。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询