推荐系统入门可以先用一张用户评分表完成物品协同过滤:若多个用户对两门课的评分相似,就把这两门课作为相邻物品。对已有评分的用户,使用他评过的物品给未评分物品估计分数,再生成候选。本教程会保留缺失评分、过滤已评分课程,并处理无共同评分、新用户与新物品。
评分、缺失和相似度的约定
本例评分范围是 1 到 5 分;NaN 表示尚未评分,不能解释成讨厌或 0 分。每行是一个用户,每列是一个课程物品。两门课的余弦相似度仅用共同评分的用户计算,至少两位共同评分用户才建立邻接;这个最小人数是教学规则,真实任务需要在验证数据上决定。

例如入门课与 Python课的共同评分向量是 [5,4,1] 和 [4,5,1],余弦相似度为向量点积除以两个向量的长度之积。它检查评分方向是否接近,不能消除用户“什么都给高分”的偏好。本例未做用户均值中心化或相似度收缩,少数共同评分得到的高相似度尤其需要谨慎。
环境与完整示例
下面示例在 Windows、Python 3.11.15、CPU 环境实际运行。先安装 Python 3.11,在新建的空文件夹中打开 PowerShell,把完整代码保存为 10-item-collaborative-recommendation.py。创建独立环境并执行:
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install numpy==2.4.6 pandas==3.0.6 scikit-learn==1.9.1
.\.venv\Scripts\python.exe 10-item-collaborative-recommendation.py
最后一行在保存代码后执行。macOS 或 Linux 把 Python 路径换成 ./.venv/bin/python。示例不需要 GPU、外部模型下载或付费 API;安装依赖需要联网。不同库版本可能带来数值末位差异,先核对数据行数、标签顺序和输出结构。
矩阵里共有六个用户和六门课程。“新课”完全没有评分;“实践项目”虽已有评分,却没有与“入门课”的共同评分用户。这样可以直接检查冷启动与无重叠路径。
把完整代码保存为 10-item-collaborative-recommendation.py 后运行。检查物品间的共同评分人数,再为 u1 和全新的用户生成候选;代码会断言结果不含已评分物品,也不把零评分的新课伪装成协同过滤推荐。
import numpy as np
import pandas as pd
from sklearn.metrics.pairwise import cosine_similarity
# 人工评分表,1–5分;NaN表示没有评分,不能解释成0分。
items = ['入门课', 'Python课', '机器学习课', '数据工程课', '实践项目', '新课']
R = pd.DataFrame([
[5, 4, np.nan, np.nan, np.nan, np.nan],
[4, 5, 5, 1, np.nan, np.nan],
[1, 1, 2, 5, np.nan, np.nan],
[np.nan, np.nan, 4, 4, 5, np.nan],
[np.nan, np.nan, 5, 3, 4, np.nan],
[np.nan, np.nan, np.nan, np.nan, 5, np.nan],
], index=['u1', 'u2', 'u3', 'u4', 'u5', 'u6'], columns=items)
observed = R.notna()
counts = observed.sum()
S = pd.DataFrame(0.0, index=items, columns=items)
overlap = pd.DataFrame(0, index=items, columns=items)
for i, a in enumerate(items):
for b in items[i+1:]:
common = observed[a] & observed[b]
overlap.loc[a, b] = overlap.loc[b, a] = int(common.sum())
# 教学规则:至少2位共同评分用户,且不推荐物品自身。
if common.sum() >= 2:
va, vb = R.loc[common, a].to_numpy(), R.loc[common, b].to_numpy()
similarity = float(cosine_similarity([va], [vb])[0, 0])
S.loc[a, b] = S.loc[b, a] = similarity
def recommend(user, n=3):
known = R.loc[user].dropna() if user in R.index else pd.Series(dtype=float)
candidates = [item for item in items if item not in known.index]
collaborative, fallback = [], []
for item in candidates:
weights = S.loc[item, known.index]
weights = weights[weights > 0]
if len(weights) and weights.sum() > 0:
score = float((weights*known[weights.index]).sum()/weights.sum())
collaborative.append((item, score, '协同过滤'))
elif counts[item] >= 2:
fallback.append((item, float(R[item].mean()), '评分均值兜底'))
# 无评分的新物品不假造相似度,留给内容特征或业务规则。
collaborative.sort(key=lambda x: (-x[1], x[0]))
fallback.sort(key=lambda x: (-x[1], x[0]))
return (collaborative + fallback)[:n]
print('co-rated users/入门课-Python课:', overlap.loc['入门课', 'Python课'])
print('similarity/入门课-Python课:', f"{S.loc['入门课','Python课']:.3f}")
print('no overlap/入门课-实践项目:', overlap.loc['入门课','实践项目'],
f"{S.loc['入门课','实践项目']:.3f}")
print('cold item/新课 ratings:', counts['新课'])
for user in ['u1', 'new_user']:
print('recommendations for', user)
result = recommend(user)
rated = set(R.loc[user].dropna().index) if user in R.index else set()
assert not rated.intersection(item for item, _, _ in result)
assert '新课' not in [item for item, _, _ in result]
for item, score, reason in result:
print(item, f'{score:.3f}', reason)
给已有用户计算未评分候选
对 u1,已评课程为入门课和 Python课。候选首先排除这两门课。每个候选课程只保留正相似度邻居,用“相似度×已评分数”的总和除以相似度总和计算估分;没有有效邻居时不做除法,也不随便填一个协同过滤分数。
可以获得协同过滤分数的课程先按估分排序;不足所需数量时,再补入评分人数至少为两人的课程均分兜底,并标清来源。这个排序政策有意优先协同过滤,因此兜底均分 4.667 仍会排在协同过滤估分 4.502 后面。两类分数的证据来源不同,不能只按一个混合数值表排序。
实际结果与三项检查
co-rated users/入门课-Python课: 3
similarity/入门课-Python课: 0.976
no overlap/入门课-实践项目: 0 0.000
cold item/新课 ratings: 0
recommendations for u1
数据工程课 4.527 协同过滤
机器学习课 4.502 协同过滤
实践项目 4.667 评分均值兜底
recommendations for new_user
实践项目 4.667 评分均值兜底
机器学习课 4.000 评分均值兜底
Python课 3.333 评分均值兜底
本例“入门课”和“Python课”有三位共同评分用户,余弦相似度约 0.976;“入门课”和“实践项目”没有共同评分,相似度为 0.000;“新课”评分人数为零,不进入结果。
u1 的两个协同过滤候选分别为数据工程课和机器学习课;实践项目来自评分均值兜底。结果中没有入门课或 Python课。对 new_user,因为没有任何个人评分,只能返回均分兜底;程序明确显示了这种结果来源,不能称为个性化推荐。
课程均分只在已有评分人数达到规则时使用;这同样会有热门偏差,也容易受极少评分影响。正式系统可加入人数门槛、评分时间、内容类目和用户选择条件,再做独立效果验证。这里没有把高分当成销量,也没有把未评分当成负反馈。
冷启动、无重叠与目录约束
- 两门课没有共同评分用户:相似度保持零。零表示当前规则无法提供邻接证据,不表示两门课内容相反。
- 只有一位共同评分用户:未达到本例最小人数,仍不给协同过滤邻接。
- 新用户没有评分:采用明确标记的均分候选;也可以请用户先选兴趣类目,但需要另建规则。
- 新物品没有评分:本例不返回它。要展示新物品,可以使用内容特征或受控的新物品曝光规则,再分别评估。
- 用户已评全部课程:候选为空,返回空列表;不能为凑数量重复推荐。
在真实目录里,还要先过滤下架、用户无权访问、已完成或不适用的课程,再做相似度候选排序。规则应来自目录和访问事实;修改课程名称不能绕过物品 ID 去重。
怎样评估和扩展这个基线
评分表由人工构造,没有进行推荐效果、点击率或购买率测试。得到一个候选分数不能证明用户会喜欢它;这个全物品两两比较示例也不适合直接扩展到海量目录。
下一步使用获准收集的真实评分,按时间封存后来的反馈,只用当时已知的评分建立相似度。在留出数据上检查评分估计误差,以及召回候选是否包含后来实际互动的物品;做排名评估时需要明确候选集、已见物品过滤和评价口径。若对所有评分建图后再评测其中一部分,目标评分可能已经参与了相似度计算。
物品数增加时,完整相似度矩阵的空间规模约随物品数平方增长。本例只用于理解公式;规模较大可保存每个物品最相关的少量邻居,用稀疏结构或候选召回来控制资源。正式推荐也常把召回、排序和业务约束分开,不把这段小脚本当成可直接上线的全流程。
相关官方资料
本文读取官方资料的日期为 2026 年 10 月 1 日;示例输出来自上面注明的本地环境。
- cosine_similarity API:余弦相似度的归一化点积定义与调用。
- Google 协同过滤基础:用户—物品反馈矩阵及基于用户和物品关系的推荐思路。
Ai菜鸟网。发布者:AI小管家,转载请注明出处:https://www.alyyhw.com/30590.html