用嵌套map(map[string]map[string]float64)替代二维数组存储稀疏评分矩阵;相似度计算前先求交集并过滤共同评分少于3个的用户对;初版推荐余弦相似度,需归一化共同物品向量并防除零;推荐时排除目标用户已有记录的item,按相似度加权平均聚合评分;并发采用分组策略(如每20用户一组),组内串行、组间并发,配合sync.Pool复用切片。

Go 里实现协同过滤,不靠第三方 ML 库也能跑通核心逻辑,关键在数据结构设计和相似度计算的取舍。
怎么用 map 和 slice 表达用户-物品评分矩阵
协同过滤最怕稀疏数据爆炸式增长,Go 没有内置的稀疏矩阵类型,硬套二维数组([1000][1000]float64)会浪费大量内存且遍历低效。实际做法是用嵌套 map:外层键是 userID,内层是 itemID → rating 映射。
- 避免预分配全量矩阵,只存真实交互(比如
map[string]map[string]float64) - 计算相似度时,必须先求两个用户的交集物品——用
for item := range userA再检查userB[item]是否存在 - 如果用户 A 和 B 共同评分的物品少于 3 个,直接跳过相似度计算(避免除零或噪声放大)
选余弦相似度还是皮尔逊?cosine 更适合冷启动场景
余弦相似度对绝对评分值不敏感,只看向量方向;皮尔逊则中心化处理(减去用户平均分),能缓解用户打分尺度差异。但 Go 里手动实现皮尔逊要额外算均值、方差,容易出错,且冷启动用户均值不稳定。
- 初版推荐直接用
cosine:分子是共同物品评分乘积之和,分母是各自评分向量模长乘积 - 注意归一化:用户 A 的向量只包含与用户 B 共同评分的物品,不能把所有物品都塞进去
- 如果某用户所有评分都是 5.0,余弦值恒为 1——这时应加一个极小扰动(如
+1e-9)避免浮点除零
推荐生成时为什么不能直接取 top-K 相似用户?
取 top-K 用户后,再聚合他们评过分但目标用户没接触过的物品,看似合理,但实际会漏掉高频热门物品(比如所有相似用户都买过 iPhone,但目标用户已买过,就不该再推)。更关键的是,没过滤掉用户明确跳过的物品。
立即学习“go语言免费学习笔记(深入)”;
- 必须排除目标用户已有记录的
itemID(哪怕评分是 0 或未评分,只要出现在其历史中就跳过) - 聚合时用加权平均而非简单计数:相似度 0.9 的用户给某物品评了 4.5 分,比相似度 0.3 的用户评 5.0 分影响更大
- 如果最终候选集不足 K 个,不要补随机热门项——宁可返回空,也别破坏推荐可信度
goroutine 并发加速相似度计算的边界在哪?
用户数超过 500 时,串行算两两相似度太慢,但盲目开 goroutine 也不行:每个 goroutine 都要读共享的评分数据,频繁锁竞争反而拖慢。
- 用
sync.Pool复用临时切片(比如存交集物品 ID),避免 GC 压力 - 按用户分组并发(例如每 20 个用户一组),组内串行计算,组间并发
- 当用户总数
真正难的不是算相似度,而是怎么定义“用户没听说过的物品”——日志里一次点击算听过?还是必须完成购买?这个业务语义必须在数据清洗阶段就固化,代码里硬编码只会让后续迭代越来越脆。


















