12 道菜各 6 维口味画像,按「余弦相似度 + 甜度惩罚」打分:水煮鱼 0.953、 麻婆豆腐 0.861、红油抄手 0.857 排前三; 桂花糯米藕 -0.578 垫底——它余弦 0.222,被甜度惩罚拉成负分。
上一节做翻译,这一节做推荐。美食推荐是最容易上手的推荐场景:物品特征人能说清,不用担心冷启动。
一、把一道菜变成六个数
推荐的第一个动作永远是「把东西变成向量」。 一道菜可以用六个维度描述:辣、甜、酸、油、汤、荤,每个取 0 到 1。 这六个字不是随便挑的。它们要满足两个条件:人能判断(问一句就知道),而且能区分开不同的菜。 选维度是推荐系统里最费功夫的一步。 维度选得不对,后面算法再好也没用——这跟上一章「手工特征」是同一件事。 Demo 的 12 道菜覆盖了川、粤、江浙、家常几类,六个维度大致能把它们分开。
二、余弦相似度:看方向不看大小
余弦相似度算的是两个向量的夹角,不管长度。 为什么不用欧氏距离?因为「辣度高」和「整体口味都重」是两件事。余弦只关心方向是否一致,正合适。 Demo 里用户偏好是「辣 0.9、甜 0.1、酸 0.5、油 0.6、汤 0.4、荤 0.7」, 水煮鱼的余弦是 0.953——几乎是同一个方向。 但只看余弦会出问题:糖醋排骨的余弦有 0.627,不算低,可用户明确不吃甜。
三、硬约束不能被相似度盖过
所以 Demo 加了一个惩罚项:甜度乘 0.8,从余弦里减掉。 糖醋排骨从 0.627 掉到 -0.173,桂花糯米藕从 0.222 掉到 -0.578,直接排到末尾。 这个改动是「业务规则」,不是算法。 推荐系统里这一类规则必须排在算法之前。 过敏原、忌口、宗教禁忌、已购买过的商品,都属于「一票否决」,不能被相似度覆盖。 顺序反了会出事故:算法算出「这个人喜欢花生」,然后推给他一包花生,而他对花生过敏。 这种事在真实系统里发生过不止一次。
四、推荐结果怎么解释
排前三的是:水煮鱼 0.953、麻婆豆腐 0.861、红油抄手 0.857。 三个都是辣的、荤的、带点汤——和用户偏好完全对得上。 垫底两个是南瓜粥 -0.181 和桂花糯米藕 -0.578,都是甜的。 这套打分的好处是每个分数都能解释:余弦高是因为口味接近,负分是因为踩了忌口。 用户问「为什么推这个」,你能答上来。 深度学习推荐模型的准确率通常更高,但解释不了。所以很多系统用复杂模型排序、用简单规则生成推荐理由。
五、动手 Demo:按口味推菜:内容相似度加用户画像
给 12 道菜各标一个 6 维口味向量;设一个用户偏好向量和一个「明确不吃甜」的惩罚向量; 对每道菜算与偏好的余弦相似度,减去甜度惩罚乘 0.8,排序取前三, 并对比两道菜说明为什么不能只用辣度排序。
dims = [u'辣', u'甜', u'酸', u'油', u'汤', u'荤']
like = np.array([0.9, 0.1, 0.5, 0.6, 0.4, 0.7])
dislike = np.array([0.0, 1.0, 0.0, 0.0, 0.0, 0.0])
cos = float(v @ like / (np.linalg.norm(v) * np.linalg.norm(like)))
pen = float(v @ dislike) # 甜度
score = cos - 0.8 * pen
实跑输出:
--------------------------------------------------------------------
按口味推菜:内容相似度加用户画像
--------------------------------------------------------------------
菜品库 12 道,每道 6 维口味画像
维度 辣、甜、酸、油、汤、荤
用户偏好 辣 0.9、甜 0.1、酸 0.5、油 0.6、汤 0.4、荤 0.7
明确不吃 甜(权重 1.0 的反向)
菜品 和偏好余弦 甜度惩罚 最终得分
--------------------------------------------
麻婆豆腐 0.941 0.10 0.861
水煮鱼 0.953 0.00 0.953
糖醋排骨 0.627 1.00 -0.173
清炒时蔬 0.510 0.10 0.430
番茄蛋汤 0.571 0.30 0.331
回锅肉 0.894 0.20 0.734
酸辣粉 0.869 0.10 0.789
白切鸡 0.625 0.00 0.625
红油抄手 0.937 0.10 0.857
桂花糯米藕 0.222 1.00 -0.578
干锅花菜 0.830 0.00 0.830
南瓜粥 0.299 0.60 -0.181
推荐前 3 水煮鱼、麻婆豆腐、红油抄手
最不推荐 南瓜粥、桂花糯米藕
为什么不用「辣度排序」这么简单的规则?看两行对比:
水煮鱼 辣 1.0、荤 0.9、油 0.9 → 得分 0.953
桂花糯米藕 甜 1.0、几乎不辣 → 得分 -0.578
只看辣度会漏掉「有汤」和「荤」这两个偏好;
只看余弦又会把甜口的菜排上来——所以加了一个惩罚项。
「相似度 + 硬约束」是推荐系统的常规组合,
硬约束(不吃甜、过敏原、忌口)永远不能被相似度盖过。
余弦那一列最高的是水煮鱼 0.953,最低的是桂花糯米藕 0.222——这两个数就基本决定了排名。 惩罚列把排名彻底改了:糖醋排骨余弦 0.627 本来能进前六,扣掉 1.00 乘 0.8 之后变成 -0.173, 掉到最后。 最终前三:水煮鱼 0.953、麻婆豆腐 0.861、红油抄手 0.857。 三个都是辣、荤、带点汤,与偏好完全一致。 这个 Demo 只有 12 道菜、6 个维度,参数是我拍的。 它的价值在流程:先定维度、再算相似度、最后叠硬约束。
30 秒实验
- 把用户偏好里的「甜」从 0.1 改成 0.6,重跑,看推荐列表怎么变。
- 加一个「不吃香菜」的维度,验证它能不能把某道菜直接踢出前三。
- 想一想:为什么硬约束必须排在相似度之前?
常见误区
- 「推荐就是找最像的」——还要处理忌口、过敏、已购买。
- 「维度越多越准」——维度多了人标不准,噪声反而更大。
- 「准确率高就够了」——解释不了理由,用户不会信。
自测三问(点开对答案)
1. 为什么用余弦相似度而不是欧氏距离? —— 余弦只看方向,不受向量长度影响,正合「口味是否一致」这个语义。 2. 糖醋排骨为什么从 0.627 掉到 -0.173? —— 它甜度是 1.0,扣掉 0.8 的惩罚;硬约束覆盖了相似度。 3. 推荐系统里哪一类规则必须排在算法之前? —— 过敏原、忌口、宗教禁忌、已购买等一票否决类规则。菜推荐完了,下一个实例安排行程——这次要同时满足时间和预算两个约束。
- ☐ (本节完,下一节 5.2.4 实例4:使用AI充当旅游助手)