🧠 第五章 深度学习算法
📄 一句话概括

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 秒实验

  1. 把用户偏好里的「甜」从 0.1 改成 0.6,重跑,看推荐列表怎么变。
  2. 加一个「不吃香菜」的维度,验证它能不能把某道菜直接踢出前三。
  3. 想一想:为什么硬约束必须排在相似度之前?

常见误区

  1. 「推荐就是找最像的」——还要处理忌口、过敏、已购买。
  2. 「维度越多越准」——维度多了人标不准,噪声反而更大。
  3. 「准确率高就够了」——解释不了理由,用户不会信。
自测三问(点开对答案) 1. 为什么用余弦相似度而不是欧氏距离? —— 余弦只看方向,不受向量长度影响,正合「口味是否一致」这个语义。 2. 糖醋排骨为什么从 0.627 掉到 -0.173? —— 它甜度是 1.0,扣掉 0.8 的惩罚;硬约束覆盖了相似度。 3. 推荐系统里哪一类规则必须排在算法之前? —— 过敏原、忌口、宗教禁忌、已购买等一票否决类规则。

菜推荐完了,下一个实例安排行程——这次要同时满足时间和预算两个约束。

相关笔记