没评分不等于打了零分
一个按「相似用户喜欢什么」做推荐的原型:把用户对物品的评分放进一个矩阵,没评过的格子填 0,算余弦相似度,再加权平均。结果它预测的分数平均只有 0.41(满分 5 分,真实平均 3.07),误差比「所有人都猜 3 分」大一倍多,推荐出来的一多半是最热门的那 10 个物品。
问题不在相似度公式,也不在近邻的个数,而在填进去的那些 0。评分矩阵里绝大多数格子是空的,空的含义是「不知道」;填成 0,就变成了「这个用户给它打了最低分」。数据里九成的内容于是都在表达一个没有人说过的意思。
同样的事在后端工程里很常见,只是换了样子:把 NULL 当成 0 求平均、把没上报的指标画成 0、把没点击当成不感兴趣。本文用推荐里的评分矩阵把它的后果量出来。数据是合成的(固定随机种子,300 个用户、120 个物品、约 10% 的格子有评分),留出 20% 的评分做测试。实验见文末。
一、0 是怎么混进来的
用矩阵或二维数组存评分时,空格总得放点什么,0 是最顺手的。之后的每一步都把它当成了真实的评分:
相似度被扭曲。 两个用户的向量各有约 90% 是 0。余弦相似度此时主要取决于「两人评过的物品重合了多少」,而不是「对同一个物品的看法是否接近」。都评过热门物品的用户会显得彼此相似,无论他们打的分是高是低。
加权平均被拉低。 预测用户对某个物品的评分时,取最相似的 20 个邻居在这个物品上的评分做加权平均。邻居里没评过它的占大多数,每人贡献一个 0。实测预测值的平均是 0.41。
排序偏向热门。 被评分次数多的物品,邻居里评过它的人多,加权平均里非零项多,预测分就高。预测分的高低反映的是「多少人评过」,不是「评得多高」。
二、只用观测到的评分
改法是让缺失保持缺失,每一步只用确实存在的评分:
- 相似度只在共同评过的物品上算,并且先减去各自的平均分。有人习惯打高分,有人习惯打低分,减掉均值之后比较的是「相对自己平时的偏好」。共同评过的物品太少(实验里少于 3 个)时不计相似度,较少时打折。
- 预测时只让评过这个物品的邻居参与。 没评过的不是 0,是不参加。
- 加回目标用户自己的平均分。 邻居提供的是「比平时高还是低」,基准是用户自己的。
用 dict 或稀疏结构存「(用户,物品)→ 评分」,没有的键就是没有,比稠密矩阵加哨兵值更不容易出错。
三、结果
在留出的 821 个评分上:
| 预测方法 | RMSE |
|---|---|
| 全部预测为全局平均分 | 1.18 |
| 预测为用户自己的平均分 | 1.22 |
| 缺失当 0 的近邻加权 | 2.92 |
| 只用已观测评分的近邻加权 | 1.08 |
给每个用户推荐 5 个没评过的物品:
| 来自最热门 10 个物品的比例 | 命中测试集的物品,用户实际打分的平均 | |
|---|---|---|
| 缺失当 0 | 57% | 3.15 |
| 只用已观测评分 | 3% | 3.79 |
两点值得注意。
第一,缺失当 0 的方法比什么模型都不用还差。直接猜平均分的 RMSE 是 1.18,它是 2.92。如果没有和这个最简单的基线比较,只看「能跑出结果、推荐的东西看起来也挺受欢迎」,这个问题发现不了。
第二,修正之后的近邻方法只比猜平均分好 0.10。数据很稀疏,两个用户共同评过的物品很少,近邻方法能利用的信息有限。这个实验说明的是「填 0 是错的」,不是「近邻方法效果好」。第二张表里 3.79 对 3.15 的差别方向明确,但命中测试集的样本分别只有 38 和 143 个。
四、真实数据里还要多想一层
合成数据里,一个格子有没有评分只和物品的热门程度有关。真实情况更复杂:
- 缺失不是随机的。 人们更愿意给喜欢的东西打分,看都没看过的东西不会有评分。观测到的评分整体偏高,「没评分」本身带着一点「可能不感兴趣」的信息,但强度远远不到「0 分」。
- 隐式反馈没有负例。 点击、购买、播放这类数据只有「发生了」,没有「不喜欢」。没点击可能是不感兴趣,也可能是根本没看到。把没点击都当负例,模型学到的是「曝光位置」而不是「偏好」。常见的处理是只把「曝光了却没点击」当作弱负例,或者给未观测的项一个较低的权重,而不是一个确定的 0。
- 评估也会受污染。 用「有没有交互」来评估推荐,热门物品天然占优。离线指标要和简单基线(全局热门、用户均值)一起看,最终用线上对照实验确认,判断方法见 p 值 0.03,这个功能就该上线吗。
五、同一个问题的其他样子
「不知道」被悄悄换成一个具体的值,在服务端很多地方都有:
| 场景 | 被当成的值 | 后果 |
|---|---|---|
| 平均值的分母包含了没有数据的行 | 0 | 均值偏低,见 JOIN 之后,合计从 480 变成了 880 里的「人均」 |
| 监控里没上报的时间点 | 0 | 把「采集断了」画成「流量归零」,或者反过来把故障抹平 |
| 接口字段缺失时的默认值 | 0、空串、false | 分不清「用户选了否」和「用户没填」 |
| 机器学习特征的缺失 | 0 或均值 | 模型把填充值当成真实取值;训练流程见机器学习的一次完整流程 |
处理方式是一致的:让缺失在类型上可见(NULL、Optional、不存在的键),在每个聚合和比较的地方明确它该不该参与;确实需要填充时,把「这是填充的」作为单独的一列保留下来。
配套实验
- codesphere-labs/ai/missing-feedback-is-not-zero:Python 3.14.8 标准库、固定随机种子的合成评分数据上,四种预测方法在留出评分上的 RMSE 与两种近邻方法的推荐结果(验证记录)
参考资料
- Yifan Hu、Yehuda Koren、Chris Volinsky,Collaborative Filtering for Implicit Feedback Datasets,ICDM 2008(隐式反馈里给未观测项较低置信度的做法)
- Harald Steck,Training and Testing of Recommender Systems on Data Missing Not at Random,KDD 2010(评分并非随机缺失时的训练与评估)
- Charu C. Aggarwal,《Recommender Systems: The Textbook》,Springer,2016,第 2 章(基于近邻的协同过滤:均值中心化与共同评分)