协同过滤(Collaborative Filtering)是推荐系统最经典的方法,核心假设:过去偏好相似的用户,未来也会偏好相似的物品。它的优势在于无需领域知识,仅依赖用户行为数据即可完成推荐。
| 维度 | UserCF | ItemCF |
|---|---|---|
| 核心思想 | 找相似用户推荐他们喜欢的 | 找相似物品推荐与历史相似的 |
| 相似度 | 用户间评分向量 | 物品间评分向量 |
| 适用场景 | 新闻/社交(兴趣迁移快) | 电商/视频(物品稳定) |
| 计算量 | 用户量大时昂贵 | 物品量适中时高效 |
| 冷启动 | 新用户无相似用户 | 新物品无相似物品 |
| 可解释性 | "和你相似的人也喜欢" | "买了X还买了Y" |
| 代表系统 | 网易云、Digg | 亚马逊、淘宝 |
协同过滤起源于1992年Xerox的Tapestry系统,1994年GroupLens将其应用于新闻推荐。Amazon在2001年提出ItemCF方法,成为电商推荐的标配。Netflix Prize竞赛(2006-2009)进一步推动了协同过滤方法的发展。
r̂(u,i) = Σ sim(u,v)·r(v,i) / Σ |sim(u,v)|
N(u)为用户u的K个最相似邻居
r̂(u,i) = Σ sim(i,j)·r(u,j) / Σ |sim(i,j)|
I(u)为用户u已评分的物品集合
cos(a,b) = (a·b) / (||a||·||b||)
pearson(a,b) = Σ(ai-ā)(bi-b̄) / √(Σ(ai-ā)²·Σ(bi-b̄)²)
J(A,B) = |A∩B| / |A∪B|
import numpy as np
from collections import defaultdict
np.random.seed(42)
users=[f"U{i}"for i in range(1,16)];items=[f"I{j}"for j in range(1,21)]
ratings={}
for u in users:
n=np.random.poisson(5)+2
for item in np.random.choice(items,size=min(n,len(items)),replace=False):
ratings[(u,item)]=np.random.choice([1,2,3,4,5],p=[.05,.1,.2,.35,.3])
print("="*60+"\n协同过滤推荐系统\n"+"="*60)
print(f"用户:{len(users)} 物品:{len(items)} 交互:{len(ratings)} 密度:{len(ratings)/(len(users)*len(items)):.4f}")
def cosine_sim(a,b):
ck=set(a.keys())&set(b.keys())
if not ck:return 0.0
va=np.array([a[k]for k in ck]);vb=np.array([b[k]for k in ck])
d=np.linalg.norm(va)*np.linalg.norm(vb)
return np.dot(va,vb)/d if d>0 else 0.0
def pearson_sim(a,b):
ck=set(a.keys())&set(b.keys())
if len(ck)<2:return 0.0
va=np.array([a[k]for k in ck]);vb=np.array([b[k]for k in ck])
va_c,vb_c=va-va.mean(),vb-vb.mean()
d=np.linalg.norm(va_c)*np.linalg.norm(vb_c)
return np.dot(va_c,vb_c)/d if d>0 else 0.0
uv=defaultdict(dict);iv=defaultdict(dict)
for(u,i),r in ratings.items():uv[u][i]=r;iv[i][u]=r
# UserCF
target="U1";K=5
sims={u:pearson_sim(uv[target],uv[u])for u in users if u!=target}
top_k=sorted(sims.items(),key=lambda x:-x[1])[:K]
print(f"\n与{target}最相似的{K}个用户(Pearson):")
for u,s in top_k:print(f" {u}:{s:.4f}")
ti=set(items)-set(uv[target].keys());preds={}
for item in ti:
num=den=0
for u,sim in top_k:
if item in uv[u]:num+=sim*uv[u][item];den+=abs(sim)
if den>0:preds[item]=num/den
urec=sorted(preds.items(),key=lambda x:-x[1])[:5]
print(f"UserCF推荐Top-5:");[print(f" {i}:{s:.3f}")for i,s in urec]
# ItemCF
ti2="I1";isims={i:cosine_sim(iv[ti2],iv[i])for i in items if i!=ti2}
top_ki=sorted(isims.items(),key=lambda x:-x[1])[:K]
print(f"\n与{ti2}最相似的{K}个物品:")
for i,s in top_ki:print(f" {i}:{s:.4f}")
ipreds={}
for item in ti:
num=den=0
for ri,rv in uv[target].items():
s=cosine_sim(iv.get(item,{}),iv.get(ri,{}));num+=s*rv;den+=abs(s)
if den>0:ipreds[item]=num/den
irec=sorted(ipreds.items(),key=lambda x:-x[1])[:5]
print(f"ItemCF推荐Top-5:");[print(f" {i}:{s:.3f}")for i,s in irec]
# 评估
test_data={};train_data=dict(ratings)
for u in users:
ui=[(i,r)for(uu,i),r in ratings.items()if uu==u]
if ui:h=ui[np.random.randint(len(ui))];test_data[(u,h[0])]=h[1];del train_data[(u,h[0])]
def eval_cf(data,method,Kn=5,Kr=5):
u2=defaultdict(dict);i2=defaultdict(dict)
for(u,i),r in data.items():u2[u][i]=r;i2[i][u]=r
hits=0;ndcgs=[]
for(u,ti3),tr in test_data.items():
if u not in u2:continue
ts=set(items)-set(u2[u].keys())
if ti3 not in ts:ts.add(ti3)
ps={}
for item in ts:
num=den=0
if method=='user':
sl=[(o,pearson_sim(u2[u],u2[o]))for o in users if o!=u and o in u2]
for o,s in sorted(sl,key=lambda x:-x[1])[:Kn]:
if item in u2[o]:num+=s*u2[o][item];den+=abs(s)
else:
for ri,rv in u2[u].items():
s=cosine_sim(i2.get(item,{}),i2.get(ri,{}));num+=s*rv;den+=abs(s)
if den>0:ps[item]=num/den
rec=[i for i,_ in sorted(ps.items(),key=lambda x:-x[1])[:Kr]]
if ti3 in rec:hits+=1;ndcgs.append(1.0/np.log2(rec.index(ti3)+2))
else:ndcgs.append(0)
n=len(test_data);return hits/n if n else 0,np.mean(ndcgs)
u_hr,u_ndcg=eval_cf(train_data,'user');i_hr,i_ndcg=eval_cf(train_data,'item')
print(f"\n{'方法':<10}{'HitRate@5':<12}{'NDCG@5':<10}")
print("-"*32);print(f"{'UserCF':<10}{u_hr:<12.4f}{u_ndcg:<10.4f}");print(f"{'ItemCF':<10}{i_hr:<12.4f}{i_ndcg:<10.4f}")
# Jaccard对比
u1,u2_id="U1","U2"
if u1 in uv and u2_id in uv:
cs=cosine_sim(uv[u1],uv[u2_id]);ps=pearson_sim(uv[u1],uv[u2_id])
sa=set(uv[u1].keys());sb=set(uv[u2_id].keys())
js=len(sa&sb)/len(sa|sb)if sa|sb else 0
print(f"\n{u1}与{u2_id}相似度:Cosine={cs:.4f} Pearson={ps:.4f} Jaccard={js:.4f}")
✅ 验证通过
本课代码涵盖了从数据生成、模型训练到效果评估的完整流程。以下是关键步骤的详细解读:
代码设计原则:简洁可读、关键步骤有注释、结果可复现(固定随机种子)。
| 组件 | 版本 | 说明 |
|---|---|---|
| Python | 3.8+ | 推荐3.10版本 |
| NumPy | 1.21+ | 核心数值计算库 |
| SciPy | 1.7+ | 统计检验(部分课程) |
本课是第2课:协同过滤(UserCF / ItemCF),属于基础方法阶段。我们系统学习了本课的核心概念、数学原理和代码实现,并通过实机运行验证了算法的正确性。
练习1:调整K邻居数从5→3/10,观察HitRate变化趋势
练习2:实现Jaccard相似度替换余弦,对比推荐效果
练习3:预计算ItemCF相似度矩阵,对比在线计算性能
练习4:添加用户均值中心化,观察Pearson系数变化
| 公司 | 场景 | 核心技术 |
|---|---|---|
| 字节跳动 | 抖音/TikTok | 多目标排序+实时特征+双塔召回 |
| 阿里巴巴 | 淘宝推荐 | DIN/DIEN序列模型+MIND多兴趣 |
| 腾讯 | 微信看一看 | DeepFM+图神经网络召回 |
| 美团 | 本地生活推荐 | 多场景多目标+时空特征 |
| Netflix | 视频推荐 | 矩阵分解+深度学习混合 |
✅ 掌握UserCF与ItemCF的算法原理与Python实现
✅ 理解余弦/Pearson/Jaccard三种相似度度量
✅ 完成留一法离线评估对比
✅ 理解两种方法的适用场景与选择策略