Skip to content

没评分不等于打了零分 ​

一个按「相似用户喜欢什么」做推荐的原型:把用户对物品的评分放进一个矩阵,没评过的格子填 0,算余弦相似度,再加权平均。结果它预测的分数平均只有 0.41(满分 5 分,真实平均 3.07),误差比「所有人都猜 3 分」大一倍多,推荐出来的一多半是最热门的那 10 个物品。

问题不在相似度公式,也不在近邻的个数,而在填进去的那些 0。评分矩阵里绝大多数格子是空的,空的含义是「不知道」;填成 0,就变成了「这个用户给它打了最低分」。数据里九成的内容于是都在表达一个没有人说过的意思。

同样的事在后端工程里很常见,只是换了样子:把 NULL 当成 0 求平均、把没上报的指标画成 0、把没点击当成不感兴趣。本文用推荐里的评分矩阵把它的后果量出来。数据是合成的(固定随机种子,300 个用户、120 个物品、约 10% 的格子有评分),留出 20% 的评分做测试。实验见文末。

一、0 是怎么混进来的 ​

填 0:空格变成「0 分」用户 150400用户 203005用户 340020保留缺失:空格是「不知道」用户 15?4??用户 2?3??5用户 34??2?邻居没评过 → 当作给了 0 分,拉低预测只让评过这个物品的邻居参与留出评分上的 RMSE(越小越好)猜全局平均分1.18只用已观测评分1.08缺失当 02.92
图 1 · 评分矩阵里约九成的格子是空的。填成 0 之后,每个用户的向量主要由 0 组成:相似度变成了「评过的物品重合多少」,加权平均时没评过该物品的邻居都贡献一个 0 分。合成数据上的结果:缺失当 0 的预测平均只有 0.41 分(真实平均 3.07),在留出评分上的误差是直接猜平均分的两倍多

用矩阵或二维数组存评分时,空格总得放点什么,0 是最顺手的。之后的每一步都把它当成了真实的评分:

相似度被扭曲。 两个用户的向量各有约 90% 是 0。余弦相似度此时主要取决于「两人评过的物品重合了多少」,而不是「对同一个物品的看法是否接近」。都评过热门物品的用户会显得彼此相似,无论他们打的分是高是低。

加权平均被拉低。 预测用户对某个物品的评分时,取最相似的 20 个邻居在这个物品上的评分做加权平均。邻居里没评过它的占大多数,每人贡献一个 0。实测预测值的平均是 0.41。

排序偏向热门。 被评分次数多的物品,邻居里评过它的人多,加权平均里非零项多,预测分就高。预测分的高低反映的是「多少人评过」,不是「评得多高」。

二、只用观测到的评分 ​

改法是让缺失保持缺失,每一步只用确实存在的评分:

  1. 相似度只在共同评过的物品上算,并且先减去各自的平均分。有人习惯打高分,有人习惯打低分,减掉均值之后比较的是「相对自己平时的偏好」。共同评过的物品太少(实验里少于 3 个)时不计相似度,较少时打折。
  2. 预测时只让评过这个物品的邻居参与。 没评过的不是 0,是不参加。
  3. 加回目标用户自己的平均分。 邻居提供的是「比平时高还是低」,基准是用户自己的。

用 dict 或稀疏结构存「(用户,物品)→ 评分」,没有的键就是没有,比稠密矩阵加哨兵值更不容易出错。

三、结果 ​

在留出的 821 个评分上:

预测方法RMSE
全部预测为全局平均分1.18
预测为用户自己的平均分1.22
缺失当 0 的近邻加权2.92
只用已观测评分的近邻加权1.08

给每个用户推荐 5 个没评过的物品:

来自最热门 10 个物品的比例命中测试集的物品,用户实际打分的平均
缺失当 057%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、不存在的键),在每个聚合和比较的地方明确它该不该参与;确实需要填充时,把「这是填充的」作为单独的一列保留下来。


配套实验

参考资料

文章以 CC BY-NC-SA 4.0 授权 · 代码片段以 MIT 授权