Yelp餐厅数据集实战:从样本构建到TypeScript交付的推荐系统全链路
2026/9/17 20:16:17 网站建设 项目流程

简介:在真实业务场景中,推荐系统的落地远不止训练一个协同过滤模型那么简单。面对Yelp餐厅数据集这类包含评分、评论、签到等多源异构信息的真实数据,开发者往往需要先理解数据分布与业务约束,再设计训练样本与负采样策略,进而通过召回、排序、重排等模块构建完整链路。本文以餐厅推荐为切入点,讨论如何将隐式反馈转化为有效标签,如何平衡ItemCF、双塔模型与MIND、SDM等召回方案的适用性,以及如何利用特征工程提升排序效果。同时,针对工程交付环节,介绍了Jupyter Notebook用于算法实验、TypeScript用于服务化集成的分工模式,涵盖模型导出、线上推理与业务规则校验。无论你是想用真实数据集练手推荐系统,还是准备为客户搭建可感知、可解释的个性化推荐服务,这条从数据剖析到工程落地的实践路径都值得参考,让推荐系统真正从离线指标走向可用产品。 做这个项目的时候,我盯着标题里的三个关键词看了很久:Yelp餐厅数据集、推荐系统、还有那个有点突兀的TypeScript。前两个好理解,一看就是典型的数据科学任务;但TypeScript出现在这里,说明这不是一个"训练完模型写篇报告就完事"的课程作业,而是一个要真正交付、让客户能看到能用的系统。我最初也以为这就是个协同过滤的练习,实际动手才发现,Yelp餐厅数据集的真实复杂度远超市面上大部分推荐系统教程的数据,光是数据清洗和样本构造就能劝退不少人。这篇就按我实际做下来的完整链路来写,从数据剖析、样本构建、召回排序,到Jupyter Notebook做实验和TypeScript做交付的分工配合,最后是评估验收。想拿真实数据集练手推荐系统、或者是接了类似"给客户搭推荐"这种需求的朋友,可以直接照着这条路径走。

1. 标题背后的真实需求:这不是算法题,而是一套系统工程

先把这个任务的底细摸清楚。Yelp是美国最大的本地生活点评平台,它的开放数据集和很多比赛里用的干净数据集有一个本质区别:它更像真实业务里导出的数据,带着各种脏、乱、稀疏和偏差。说句实话,第一眼看到这个标题时,我的判断是:这是一道"全链路推荐系统"实战题,核心难点不在算法本身,而在怎么把一堆点评数据变成一套能运转、能解释、能交付的推荐服务。所以我在开始的规划阶段就定了三条主线:理解数据、构建模型、完成交付,三者缺一不可。

从客户视角想一下,他要的"推荐系统"到底长什么样?并不是一个离线算好的排行榜,而是用户在某个场景下打开页面,能在几百毫秒内看到"附近的餐厅,哪家更适合我"这样的结果。Yelp数据里有评分、评论文本、签到记录、商家属性、用户活跃行为,这些信息拆开看都能用,但合在一起时怎么取舍、怎么组合,才是真正的工程决策点。

还有个很关键的点容易被忽略:这是一个"餐厅"推荐,不是通用商品推荐。餐厅推荐有极强的本地属性和时间属性,用户不会为了吃一顿饭跨越半个城市,也不会在凌晨三点想吃早午餐。这种场景特征决定了数据和模型的构建方式,和电商推荐相比有大量细节差异。这些差异在公开数据集上尤其明显,但很多人没意识到它们的存在,最后做出来的模型指标还行,现场一用却感觉很别扭。

至于TypeScript出现在标题里,我后来理顺了:这类项目的典型交付路径是——Python侧用Jupyter Notebook完成数据分析、特征工程和模型训练,然后把模型导出成ONNX或通过API服务暴露出来,前端或服务端用TypeScript去接。所以这份项目资料里的TypeScript部分,大概率是模型服务或演示界面的工程代码。这一点很符合真实企业项目的技术栈组合,也决定了本文的实践路径:算法实验和工程交付分开,各用各的顺手工具。

如果你想复现或二次开发这个项目,我的建议是先把整条链路梳理成下面几个模块,再逐个击破,而不是一上来就调模型:

  • 数据模块:Yelp JSON文件的解析、字段梳理、数据质量探查
  • 特征模块:用户、商家、行为的特征构建,训练样本的生成
  • 召回模块:先把候选集从几十万缩到几百,保证计算可行性
  • 排序模块:在召回结果上精排,输出最终推荐列表
  • 评估模块:离线指标加必要的业务规则校验
  • 交付模块:模型导出、API封装、前端或服务端集成

这六个模块贯穿了"标题"这个简短的描述没写出来的所有工作。我强烈建议你拿到这类项目时,先按这个思路把范围定下来,否则很容易在数据清洗环节就陷入泥潭,或者反过来在模型调参上花太多时间,最后交付物拿不出手。

2. 数据解剖:Yelp的评分、评论与签到,到底哪个能当训练信号

2.1 先把JSON结构吃透

Yelp开放数据集提供的是JSON文件,我用的版本里包含五个核心文件:business.json、review.json、user.json、checkin.json、tip.json。别看只有五个文件,这里面的数据量是百万级的,review.json往往就有几百万条。很多人第一步用pandas直接pd.read_json去读,几百万条评论直接内存爆掉,这个坑我一开始也踩过。正确姿势是分块读,或者先抽样探查结构,再决定是全部加载还是走DuckDB之类的列式引擎。

来看一下business.json里最核心的字段。每个商家有business_id、name、categories、attributes、hours,还有latitude和longitude。注意,这里的categories是逗号分隔的字符串,比如"Restaurants, Pizza, Fast Food",这在特征工程时必须处理成多值。attributes是一个嵌套的JSON对象,里面装着各种布尔或文本属性,比如是否提供外卖、是否适合带孩子。这些字段对排序阶段非常有用,但对刚拿到数据的人来说,它们嵌套太深,解析起来很烦。

review.json是推荐系统最重要的行为数据来源。每条评论包含user_id、business_id、stars(1-5的整数评分)、还有很长一段评论文本。提醒一句:stars是显式反馈,但不同用户打分的尺度差异很大。有人给三星就是好评,有人给四星都嫌差。如果你直接把原始评分当回归目标,模型会被用户的打分行径带偏。更稳妥的做法是后面做评分校准,或者把评分映射成排序标签。

user.json和checkin.json有时候会被忽视,但它们在餐厅推荐里价值很高。user.json里除了用户ID,还有review_count、useful、funny、cool这类互动数据,还有friends、elite等字段,这些可以做用户活跃度和影响力的特征。checkin.json记录的是每条商家的签到时间分布,我后文会专门说它在餐厅推荐里的特殊价值。

2.2 评分、评论、签到:三种信号代表三种意图

很多推荐系统的教程只盯着评分和评论,因为这两个数据最直观。但在餐厅这个场景下,行为信号的优先级应该重新排:签到 > 评论 > 评分。听起来挺意外,我解释一下。

评分是用户消费后的态度表达,但它严重受选择偏差影响。用户只会去给他们愿意去的地方打分,压根不会去不喜欢的店。而且Yelp上的评分分布整体偏高,4星以上占了大多数。这个偏态分布会让模型学不到"什么是差"的信号。评论文本里有更丰富的信息,但做语义分析成本高,对冷启动帮助有限。

签到数据则代表了一次真实的到店行为,它比评分更接近"转化"。一个人在一家餐厅打了卡,比他在三年前给隔壁餐厅打五星更能说明当下的偏好。而且checkin.json里带时间戳,这个信息可以做时段偏好特征。我甚至用签到数据做过一个baseline,效果比单纯用评分构建的协同过滤好不少。原因很简单:餐厅消费频次低、随机性强,但签到是强意向行为,噪声小。

所以在构建标签策略时,我的做法是把三类信号叠加成一个多层标签体系。评分高且签到多的商家是强正样本,有签到但没评分的商家是中正样本,没有行为记录但同类别下曝光过却没去的商家(如果系统有曝光日志)是负样本。在Yelp开放数据集里没有曝光数据,所以负样本只能靠启发式采样,这点我后面展开讲。

2.3 餐厅推荐的两个隐藏维度:位置和时间

前面提到餐厅推荐有很强的LBS属性,这个在Yelp数据里是非常明确的。用户不太可能驱车50公里去一家餐厅,哪怕评分再高。所以我在做召回和排序时,把"地理距离"作为一个硬约束条件来处理,而不是单纯作为一个特征让模型自己学。硬约束的意思是:在召回阶段直接过滤掉超出一定距离的商家,排序阶段再对候选商家按距离衰减调整打分。这样做的好处是保证了推荐结果的基本可用性,坏处是可能丢掉一些数据里没有地理位置字段的样本,但整体收益远大于损失。

时间是另一个被公开数据集"误导"的点。Yelp官方在数据说明里会告诉你,这个数据集是某一段时间的快照,但很多项目直接把它当全量历史来用。实际上餐厅推荐里的时间信号非常重要,包括午餐和晚餐的时段偏好、周末和工作的区别、节假日场景等。我后面构造"周末晚餐"这样的候选场景时,会专门从checkin数据里提取每个商家各时段的签到热度分布,把"这家店晚上十点还开不开门"这类信息变成特征。这个特征在离线评估里带来明显的AUC提升。

3. 训练样本构造:隐性反馈、负采样与冷启动补全

数据理解透之后,接下来要解决的是"模型吃什么"的问题。我在做完第2章的数据解剖后,发现一个核心矛盾:显式评分数据虽然量大,但用来做推荐系统训练时,直接当回归或分类标签并不理想。所以训练样本构造这一环,在整个流程里是最耗时间、也最决定最终效果的环节,值得单独拆开讲。

3.1 把显式评分改造成隐式偏好,比直接用评分更稳

我的做法是构建一个"隐式反馈"样本体系,把真实的评分和签到行为映射成二分类的偏好标签。具体来说,评分高于某个阈值(我用的是4星及以上)的样本,配合签到记录,一起标记为正样本;评分低于阈值但没有明确负面行为的样本,标记为模糊样本,这类样本可以选择性丢弃或降权。为什么不直接把1星到5星当回归目标?因为用户打分尺度不一致,五档绝对分数不能代表相对偏好。一个人给所有去过的餐厅都打4-5星,另一个人平均只给3星,在绝对数值上他们永远没法对比。隐式反馈只关心"去还是没去"、"好评还是非好评",反而能消除这部分偏差。

映射完标签后要做降采样处理。Yelp数据里正样本占比不算低,但那是因为用户只给自己喜欢的店评论,这个偏好产生的样本天然存在"幸存者偏差"。如果训练集里全是用户喜欢的店,模型就学不到用户不喜欢什么。所以我统一做了负样本补充,后面这一节会说具体做法。

3.2 负采样策略:全局随机和类别约束的取舍

在Yelp这种只有正向行为、没有曝光日志的数据集里,负样本只能靠采样生成。我尝试过三种方案:

  • 方案A:完全随机采样用户没去过的高分商家,把它们当负样本。这个方案操作最简单,但问题是会把"用户可能喜欢但还没发现"的商家误判为负样本,这会带着模型学偏。
  • 方案B:只采样用户去过但评分很低的商家当负样本。这个更符合真实偏好,但数量太少,训练样本不够均衡。
  • 方案C:混合采样,按照一定的比例,随机负样本加低分负样本加同类别负样本混合构造。

我最后用的是方案C,而且把权重调整得比较讲究:负样本总量控制在正样本的1-3倍之间。这里注意,不是负样本越多越好,太多会让模型偏向"全预测为负",AUC反而下滑。我个人常用的配比是正:负=1:2,然后再加一部分与正样本同类别、但用户从未交互过的商家作难负样本,这个技巧能显著提高模型区分相似商家的能力。

3.3 冷启动补全:新店和新用户不能直接"没数据就放弃"

Yelp数据集里有个常见现象:一部分商家评论数极少,一部分用户只有一两条行为记录。如果冷启动问题不处理,推荐系统给这些人推出来的候选集基本都是全局热门餐厅,客户看了会觉得"这系统没有个性化"。为了缓解冷启动,我做了两层处理。

第一层是商家侧冷启动。商家评论少没关系,可以用它的categories、attributes、地理位置、周边相似商家的热度,构造基于内容特征的向量。也就是说,冷门商家虽然没有行为数据,但它的"画像"依然可以参与召回和排序。第二层是用户侧冷启动。用户行为少时,就用他的地理位置和最近一个行为去推断偏好。比如一个用户只有一条行为记录,在Yelp上给了一家日料店五星,那我会优先召回同一区域、同类别的候选商家。

这部分在模型上可以用经典的"热度回退"策略做兜底:当个性化信号不足时,把推荐结果退化成"附近热门+与你最近行为相似"的组合。这个策略简单但非常有效,尤其在给客户做演示的时候,它能保证第一批推荐结果看起来不那么"弱智"。

4. 从协同过滤到向量召回:Yelp场景下各方案的真实表现

样本就绪后,开始进入算法选型。我在这个项目里实施了三套召回方案,每一步都做了对比。直接说结论:在Yelp餐厅数据上,ItemCF的效果比UserCF更好,双塔向量召回能带来效果提升,但需要做很多工程细节优化,否则收益不明显。

4.1 为什么UserCF在餐厅场景打不过ItemCF

UserCF是找到和你有相似行为的一群用户,把这些用户喜欢的餐厅推荐给你。听上去逻辑正确,但在餐厅数据上有两个致命伤。第一个是用户的兴趣漂移很厉害,一个人可能这半年喜欢吃川菜,下半年因为健身改吃轻食,用历史行为找相似用户时会匹配到一堆兴趣已经迁移的邻居。第二个是用户行为稀疏,大多数用户只有几条评论记录,在极其稀疏的情况下计算用户相似度,得到的邻居集合噪声很大,推荐质量很不稳定。

ItemCF则稳定得多:先计算餐厅之间的相似度,再根据用户历史行为里的餐厅去推荐相似餐厅。相似餐厅的判定可以基于"被同一批用户光顾"这个行为共现,也可以基于类别、价格段、地理距离这些内容特征。在我做的离线评估里,ItemCF的Recall@50比UserCF高了将近10个百分点。这也是很多工业界推荐系统早期都从ItemCF起步的原因——它更抗稀疏、可解释性也强。

4.2 双塔模型:把用户和餐厅都"向量化"

单纯用ItemCF不够,因为它的候选集会偏向热门餐厅,缺乏个性化。为了改善这一点,我实现了标准的双塔模型:用户塔输入用户的历史行为序列、位置信息、活跃度特征;商家塔输入商家的类别、属性、评分、地理位置等特征;两个塔分别输出embedding,然后点积计算匹配分,用softmax或负采样交叉熵做训练。

这里有个我踩过的坑务必要说:双塔模型的效果极其依赖负采样策略,如果你直接把"全量商家随机采样"当负样本,模型会很容易学会区分"热不热"而不是"适不适合当前用户"。后来我参考了很多推荐系统实践,把负采样改成了"热门商家随机负采样 + 同类别负采样 + 全局随机负采样"的混合模式,模型效果上了一个台阶。

另外双塔模型还有一个经典问题,就是用户塔和商家塔的embedding会出现"塌缩",导致所有商家向量趋向一致。解决这个问题的方法是在训练时做梯度截断,或者加一层额外的正则化。我刚上手时没注意,导致线上召回结果里,前几个和后面几个候选看着几乎一模一样,排查了很久才发现是embedding空间坍缩。这个经验在Yelp数据上特别值得注意,因为餐厅属性维度很多,相似餐厅的向量天然容易靠得太近。

4.3 进阶召回:MIND与SDM这类序列召回思路,什么时候用得上

在热搜词里我看到了"推荐系统 sdm召回 mind召回",这两个是业界近几年的热门召回方案:MIND是阿里提出的多兴趣网络,核心思想是用动态路由从用户行为序列里提取多个兴趣向量,而不是把一个用户压缩成一个embedding;SDM是阿里提出的序列深度匹配模型,专门处理用户行为序列长、兴趣演进的场景。

放在Yelp餐厅数据上,MIND的思路比SDM更贴合。原因是SDM需要高质量的长行为序列,而Yelp数据里单个用户的签到和评论序列普遍偏短,SDM的优势发挥不出来;MIND能在较短序列中挖掘多个兴趣点,比如一个用户既喜欢日料又喜欢咖啡馆,MIND可以把这两种偏好拆成两个向量,召回时分别去检索匹配,效果明显比单向量双塔好。

如果你只是在做练习,不追求极致效果,我的建议是:先跑通双塔,再考虑MIND。MIND的训练复杂度高了不少,动态路由的迭代也会显著增加训练时间。但如果你要给客户展示"这个系统有前沿的召回算法",MIND是一个不错的加分项,而且它和双塔在代码结构上比较接近,改造起来不算伤筋动骨。SDM我建议放在后面再做,因为序列长度不足时它的收益很有限,投入产出比较低。

5. 排序层:为什么只看评分远远不够,特征工程才是关键

召回把候选集从几十万缩到了几百,接下来排序层决定这几百个候选里谁进Top 10。很多人做推荐系统,召回跑通就急着上LightGBM,结果特征全是"评分、评论数、距离",模型效果很难看。我在这个项目里做了大量特征工作,排序模型性能的提升主要来自特征,而不是模型结构。

5.1 特征体系框架:用户、商家、交叉、场景,一个都不能少

我按四个维度搭建特征体系:

  • 用户侧特征:用户历史平均评分、用户历史评论数、用户活跃天数、用户常去类别分布、用户地理中心点。
  • 商家侧特征:商家评分、评论量、价格带、类别数量、签到热度、营业时长、属性特征。
  • 交叉特征:用户与商家的距离、用户历史偏好类别与商家类别的匹配度、用户历史评分均值与商家评分的差值。
  • 场景特征:当前时段(午餐/晚餐/夜宵)、当天是否周末、商家在当前时段的热度。

特征不是越多越好,但上述几类特征基本能覆盖餐厅推荐的绝大部分决定性因素。我在实践时发现用户历史平均评分和商家评分之间的差值特别有用:如果差值大,往往意味着这个用户对商家不太满意;如果差值小,则说明商家的口味和用户的历史偏好高度匹配。这个交叉特征的AUC提升非常直观。

5.2 排序模型选型:GBDT是良好起点,深度模型再接再厉

排序模型我一开始用的LightGBM,原因是训练快、可解释性强、对特征缩放不敏感。这非常适合Yelp这种特征分布差异很大的数据:有的特征是长尾分布,有的是稀疏0-1特征。我用LightGBM调参后离线AUC能达到0.78左右,作为baseline已经能交付了。

为了追求更好的效果,我后面上了DeepFM结构,把类别特征embedding化,再和一阶特征拼接。DeepFM在这种场景下比纯粹的GBDT提升大约1到2个点的AUC。代价是训练时间变长、超参更多、调参成本增加。如果客户比较着急,我会建议用LightGBM先上线;如果客户明确说"效果优先",再上DeepFM。

这里有一个很重要的实践心得:在餐厅推荐里,排序模型一定要加入位置约束。我在Yelp数据上做过一个实验——同样的模型,把"距离"从特征改成一个乘法惩罚项,训练集和验证集的表现看起来差异不大,但结果反馈到前端演示时,用户明显更满意。原因是距离这个因素对餐厅选择来说是"硬约束"而不是"软偏好",模型自己学到的权重不够稳定,写成乘法惩罚项更可控。这个细节在推荐系统的课程里几乎不会讲,但在实际项目里非常关键。

5.3 重排层的业务规则:多样性、新鲜度和列表质量

排序模型输出分数后,还不能直接当最终结果。我额外加了一个重排层,主要做三件事:打散、去重、保底。

打散是为了避免某一类餐厅霸屏。假设用户历史行为里都是火锅,模型很可能会把前十家都推荐成火锅店。但真实用户到了现场,不一定顿顿都想吃火锅。我的做法是在最后的重排阶段,限制同一类别在Top 10中最多出现3家。去重是避免同一个连锁品牌的多个门店同时出现在列表里,这在Yelp数据里很常见(同一品牌有多家分店)。保底是保证每个候选餐厅都有自己的评论数和评分,避免推荐一些几乎没有用户验证过的店铺。

这三个约束在离线评估看甚至是"降分"的,因为打散会让Top 10的整体预估值不如纯排序高。但在实际客户验收时,这种看起来更"合理"的推荐列表,往往比纯模型输出的单调列表更具说服力。所以我一直强调:推荐系统不是纯离线指标的游戏,要结合用户的真实体验和业务方的接受度来判断好坏。

6. Jupyter Notebook负责实验,TypeScript负责交付:一套可行的协作分工

标题里同时出现了Jupyter Notebook和TypeScript,这确实让不少人疑惑。我在实际项目中体会到,这其实是一个非常合理的组合:Jupyter负责算法探索和建模验证,TypeScript负责把模型成果落地成可运行的服务或页面。两条链路各有各的工具链,分工清楚了,项目推进才会顺畅。

6.1 Jupyter Notebook的定位:不是生产环境,而是"科研记录本"

我见过很多人在Jupyter里写生产级代码,把Notebook跑完直接部署,这个习惯在正式项目中非常危险。Notebook适合做探索性数据分析、特征验证、模型对比,因为它的逐格执行模式让你能看到每个步骤的中间结果,很适合反复试错。但生产环境需要的是稳定、可测试、可重跑的代码,Notebook的全局变量状态和Cell间的隐式依赖很容易埋坑。

我的建议是:Notebook只用于三件事——数据探索(字段分布、缺失值、长尾分布)、baseline建模(快速跑一个协同过滤或LightGBM看效果)、特征评估(单特征AUC贡献)。任何超过200行的逻辑,都应该抽到独立的Python脚本或模块里,用版本管理维护。Notebook里只保留"调用、展示、记录"的内容,这样你复现实验结果时,不会被某个Cell的残留变量影响。

在这个项目里,我实际用Notebook做了大量EDA,比如分析评分分布、评论数的长尾效应、不同类别餐厅的热度差异、用户行为的地理分布。这些分析结果直接决定了特征工程的策略。比如数据里有一个非常明显的规律:评论数极少的餐厅评分分布极不均匀,这种餐厅在排序里如果权重过高,会经常推荐出"好评只有一条但评分五星"的"僵尸店"。这个洞察就是靠Notebook里的可视化分析得出来的。

6.2 TypeScript在推荐系统里的角色:API层和展示层的落地

为什么推荐系统项目会和TypeScript有关系?因为模型必须要有一个服务接口才能真正被"客户"使用。我推荐的技术栈是:Python侧负责训练和导出模型,导出的结果包括商家embedding表、用户embedding计算逻辑、特征处理管线;然后通过一个ONNX Runtime或者纯矩阵计算服务,把模型推理封装成HTTP接口;TypeScript在这时作为服务端语言,负责接收请求、调用推理、组装推荐结果、处理业务逻辑(比如过滤已关闭餐厅、按距离排序)。

同时,如果要在浏览器里给客户做一个演示Demo,TypeScript几乎是绕不开的。用Next.js或Vite搭一个前后端一体的项目,前端展示推荐列表,后端直接调用Python部署的模型API,这个组合非常顺手。TypeScript的类型系统在对接API响应时优势明显——推荐结果的数据结构可以通过interface严格定义,前端代码里很难出现"读错字段"这种低级bug。

我做这一层时的经验是:先和Python侧定好推荐结果的JSON Schema,比如返回的每个候选商家包含businessId、name、rating、distance、reason。这样前后端就能并行开发,不会互相等。TypeScript的强类型在这种多端协作场景下的确能减少大量沟通成本。

6.3 模型导出和线上推理:避免"Python能跑,上线就废"

模型导出是一个容易翻车的地方。在Notebook里跑得好好的LightGBM,导出成文件再加载,特征处理的顺序稍微不对,线上推理结果就会跟离线对不上。我建议所有特征处理逻辑必须封装成同一个函数或同一个类,离线训练和线上推理都调用这一套代码,而不是各写一份。这一点在Python侧就要做好,TypeScript侧只是负责调用,它不应该关心特征是怎么构造的。

如果用的是PyTorch训练的双塔模型,更要注意版本问题。Notebook里用的torch版本和线上服务用的版本如果不一致,可能会出现ONNX导出后算子不兼容的问题。我遇到过一次:Notebook里跑的GRU模块导出的ONNX,在CPU部署时比GPU慢了近10倍,最后排查发现是ONNX Runtime的算子优化开关没打开。这种问题在Jupyter里永远发现不了,只有真正做服务部署时才会暴露。

7. 评估与验收:别被离线指标骗了,推荐系统要这样验证

评估环节是整个项目最容易草草了事的部分,也是客户最看重的部分。很多做算法的人习惯只看AUC、Recall@K,但客户问"这个系统到底好不好用"时,你只报个数是不够的。我在这个项目里把评估拆成离线评估和业务规则校验两层,每一层都有针对Yelp场景的特殊处理。

7.1 离线评估:AUC、Recall@K、NDCG,怎么组合使用

离线评估我主要看三组指标:AUC衡量排序能力,Recall@K衡量召回覆盖度,NDCG衡量推荐序列的质量。在Yelp场景下,我更重视Recall@50和NDCG@10的组合。Recall@50看的是召回的候选集里能不能覆盖住用户实际消费过的餐厅,如果连真实行为都覆盖不住,后面排序再准也是白搭;NDCG@10则是看最终给用户展示的10个结果里,相关的结果是不是排在了前面。

我给一个我实测的数据参考:只用ItemCF做召回时,Recall@50大约在0.35左右;双塔模型做召回,Recall@50能到0.42;加上MIND多兴趣召回后,进一步提升到0.46。排序模型从0.78的AUC提升到0.80后,NDCG@10有约5%的相对提升。这些数字在不同版本的数据集上会有波动,但趋势是固定的。

7.2 业务规则校验:把推荐结果拉到真实场景里"体检"

离线指标好看还不够,推荐结果还要满足一系列业务常识。我把这些常识写成了一组校验脚本,每轮实验后自动跑一遍:

  • 距离合理性:推荐结果中90%以上商家应在用户附近某个半径内(我用的30公里)
  • 营业时间:如果模拟的是午餐时段,推荐结果不应包含大量仅晚餐营业的餐厅
  • 多样性:Top 10里同一类别不超过3家,同一品牌门店不超过2家
  • 质量底线:评分低于3.5星的商家不应大量出现在Top 10中
  • 新鲜度:新开业或近期评分高的餐厅应有一定的曝光机会

这些校验单独看都很朴素,但组合起来能抓住很多模型问题。比如我有一版双塔模型跑出来,AUC还行,但Top 10里有一半是同一品牌的连锁店,就是因为embedding把品牌相似性和用户偏好混淆了。这类问题如果只看AUC,几乎不可能发现。

7.3 交付时的验收清单:给客户一个可感知的"好用"

最后给客户验收时,我准备一个包含几个典型用户画像的演示用例:一位住在市中心、经常吃日料的用户;一位住在郊区、家庭聚餐为主、预算敏感的用户;一位刚注册、没有任何历史行为的新用户。分别展示推荐结果,并逐条解释为什么推荐某家餐厅,理由要可追溯,比如"离你近""和你过去光顾的餐厅风格相似""这个时段这家店很热门"。

推荐系统最怕变成黑盒,客户一旦发现推荐结果无法解释,就会怀疑整个系统的可靠性。所以在排序结果里我保留了一条"推荐理由"字段,让TypeScript前端展示时能明确告诉用户为什么推荐这家店。这一项在客户验收时的加分效果,超过模型中任何一个超参的调优。

8. 几个实操经验,最后再啰嗦一遍

写到这,整个项目的链路已经完整了。最后分享几个实操中沉淀下来的经验,不一定写在教科书里,但都很实用。

第一,Yelp数据的版本不同,字段细节会有差异,动手前先花半天时间做完整的数据概要统计,别急着写特征代码。甚至不用一直用全量数据,可以先抽一个10%-20%的子集验证管线流程,确认没问题后再放到全量上跑,能省下大量调试时间。我在做特征管线的初期,因为一直拿全量数据跑,每次调试都要等很久,后来改成抽样验证,整个开发效率提高了一倍。

第二,双塔模型训练时,我没有一开始就上大规模embedding维度,而是先用64维跑通验证流程,确认数据管道没有问题后,再逐步调到128或256,对比收益。这样既避免了大模型调参时的玄学问题,又能有效控制算力成本。Yelp商家数量在几十万量级,embedding维度设成128时模型文件已经不小,推理时也要考虑内存占用,一定要在效果和性能之间找好平衡点。

第三,给客户做演示的时候,一定要准备"地图视角"的推荐结果展示。餐厅推荐和商品推荐最大的不同在于地理属性,你可以在TypeScript前端的地图上把推荐餐厅标出来,用户一看就懂"这家店就在我家附近,评分还不错"。这种直观的视觉反馈,比任何精美的指标图表都更有说服力。

第四,如果遇到"某类商家在数据里占比特别高"的情况(比如Yelp里的美式餐厅和快餐非常多),在负采样时要注意去掉这些热门类别的主导效应。否则模型会形成严重的流行度偏置,把所有候选都推向热门类别,个性化程度会大打折扣。对付这个问题的常用办法是类别均衡采样,确保每个类别在训练样本中占比相对均衡。

第五,模型训练完不要急着删历史实验记录。我在这个项目里维护了一份实验日志,每个版本的训练数据范围、特征列表、负采样比例、模型参数、离线指标、上线表现都要登记。推荐系统的迭代非常频繁,没有这份日志,你过两周回头想调参时,会发现完全记不清之前哪版为什么效果好。这也是我在Jupyter Notebook之外坚持维护Python脚本和实验记录的原因,Notebook负责探索,脚本和文档负责沉淀。

整个项目做下来,我的最大感受是:推荐系统的难点并不是某个算法有多难,而是从杂乱的数据到可用的服务之间,隔着大量"无人替你趟雷"的细节。Yelp餐厅数据集因为真实、稀疏、带地理和时间属性,非常适合用来体会这些细节。希望这篇内容能帮你把整个链路走通,少踩一些我踩过的坑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询