Spark ALS餐饮推荐系统实战:从订单流水到Top-N推荐
发布时间:2026/9/28 22:45:29来源:尧图网络
简介基于Spark的餐饮平台菜品智能分析推荐系统是一份面向计算机、通信、人工智能、自动化等专业学生与从业者的完整项目源码与数据库包适合毕业设计、课程设计或项目实训参考。项目以Spark为核心完成菜品数据清洗、分析与智能推荐整体采用Java后端配合JSP、XML配置及CSS/JS前端资源构成可运行的前后端体系并包含SQL数据库脚本和用户菜品评分样例数据便于本地复现与二次开发。压缩包共49个文件约2.05MB主要包含17个Java源文件、8个XML配置、6个CSS样式、5个JS脚本、3个JSP页面及数据库、属性、说明文档等辅助文件目录结构清晰可作为学习Spark应用开发的入门与进阶案例。包中代码已经过调试测试项目整体曾获较高答辩评价具备较强学习借鉴价值基础较好的读者还可在此基础上扩展推荐逻辑或调整业务模块。目前已有254人学习下载。1. 为什么餐饮推荐系统要用 Spark这个高分项目的真实分量拿到一个标注“高分项目”的源码包里面是 Spark、推荐系统、数据库三样东西的组合很多人第一反应是“又要跑 LDA 或者 Word2Vec 练手”。但这个项目的核心其实不是算法炫技而是一条完整的离线推荐链路用 Spark 的 ALS 算法处理餐饮平台的订单流水把“谁在什么时间反复点了哪道菜”变成隐式评分训练出菜品向量和用户向量再把 Top-N 推荐结果写回 MySQL最后通过接口对外服务。换句话说它是一套能放进简历也能放进生产环境的最小可行推荐系统难点不在 ALS 本身而在于数据怎么清洗、评分怎么构造、结果怎么落库、冷启动怎么兜底。适合正在做大数据课程设计、准备 Spark 面试项目、或者想从“跑 demo”跨到“做系统”的开发者去拆解复现。2. 数据模型与预处理餐饮订单流水怎么变成 ALS 能吃的评分矩阵2.1 用户、菜品、订单三张核心表关系建模的取舍餐饮平台的数据和电商不完全一样电商有明确的加购、收藏、支付行为餐饮平台的主表是一张多行订单明细表每一行记录“某个用户在某一次订单里点了某一道菜点了 1 份价格多少”。常见做法是拆三张核心表user表存用户维度的注册时间、性别、常驻城市dish表存菜品维度的名称、分类、价格、辣度、上架状态order_detail表存订单流水包含order_id、user_id、dish_id、quantity、amount、create_time。这个建模里有一个容易被忽略的取舍要不要把订单头order 表和订单明细order_detail 表拆开。如果只做菜品推荐理论上明细表已经足够但真实业务里订单头有order_status、shop_id、pay_time拆开之后才能做“统计每单实付金额”“过滤未支付订单”这类操作。我一般会保留两张表而不是合成一张宽表因为后续做用户维度的特征工程时订单头的维度平均客单价、月下单频次和明细表的维度点菜种类数、辣度偏好是两类特征拆分存更干净。这个项目的数据库部分通常还会带几张辅助表比如user_rating用户对菜品的主动评分和dish_category菜品分类树。如果你拿到的源码里没有评分表不用慌ALS 本身就支持隐式反馈下文会讲怎么从订单流水构造出评分列。如果源码里连order_detail都没有那这个项目的数据库部分大概率只有结果表你需要自己写一套数据生成脚本这种情况后面也会给方向。2.2 从订单流水构造评分三元组隐式反馈的隐式玩法ALS 算法的输入是三元组(user_id, dish_id, rating)rating 可以是显式评分用户打了 4 分也可以是隐式置信度用户点了 3 次。餐饮平台的现实是绝大多数用户根本不会打分但每个人都在持续下单。所以最可靠的方案是走隐式反馈把行为频次映射成一个带权重的置信度分数。我常用的评分映射规则如下先按(user_id, dish_id)分组统计order_cnt下单次数、sum_quantity累计份数、以及最近一次下单距今天数recency_days。然后构造-- 把订单明细分组聚合成用户-菜品行为统计 SELECT user_id, dish_id, COUNT(*) AS order_cnt, SUM(quantity) AS total_quantity, DATEDIFF(CURDATE(), MAX(create_time)) AS recency_days FROM order_detail WHERE order_status paid GROUP BY user_id, dish_id这段 SQL 的逻辑很直接过滤未支付订单按用户和菜品聚合出频次和新鲜度。拿到这个结果后再用一个 SQL 把频次映射成评分。有一个业界常踩的坑直接用order_cnt当评分会导致热门菜品分数虚高训练出来的推荐列表全是“麻辣烫”“可乐”这类高频不痛不痒的菜品。正确做法是做一个非线性映射-- 隐式评分频次分 新鲜度加权 SELECT user_id, dish_id, ROUND( LOG2(order_cnt 1) * 2 CASE WHEN recency_days 7 THEN 1.5 WHEN recency_days 30 THEN 1.0 ELSE 0.5 END, 2 ) AS rating FROM user_dish_stats这样构造的评分有几个好处LOG2让点 10 次的人分数不会线性爆炸而是从 2 分涨到约 6.9 分recency_days权重反映“最近还在点”比“以前点过”更代表当前口味最终评分的取值范围落在 2.5 到 7.5 之间适合 ALS 的回归目标。如果只是照抄这段 SQL 而不理解为什么用LOG2最后调参阶段一定会被“推荐结果全是热门菜”的问题卡住。2.3 Spark 读取与清洗JSON 增量入库和空值战场拿到带数据库的项目一个容易翻车的地方是你以为要写 JDBC 直连 MySQL但实际的订单流水往往是每日以 JSON 文件同步到 HDFS 或对象存储的。这时候就要用 Spark 直接读 JSON 做清洗清洗完再落回 Hive 表或者临时表。from pyspark.sql import SparkSession from pyspark.sql.functions import col, when, lit spark SparkSession.builder \ .appName(dish_data_clean) \ .config(spark.sql.shuffle.partitions, 120) \ .getOrCreate() # 读取同步过来的原始订单 JSON raw spark.read.json(/data/dish/order_detail/2025-06-01/*.json) # 清洗过滤无效订单、去除重复行、修正字段类型 df raw.filter(col(order_status) paid) \ .dropDuplicates([order_id, dish_id]) \ .filter(col(quantity) 0) \ .withColumn(user_id, col(user_id).cast(long)) \ .withColumn(dish_id, col(dish_id).cast(long)) \ .withColumn(amount, col(amount).cast(double)) \ .select(order_id, user_id, dish_id, quantity, amount, create_time) df.createOrReplaceTempView(order_detail_clean) print(清洗后行数:, df.count())这里每一行都有背后原因dropDuplicates([order_id, dish_id])处理的是同一订单中同一道菜可能出现多条记录的系统 bugquantity 0过滤掉退菜产生的负数记录cast(long)是因为 ALS 要求userCol和itemCol必须是数值类型字符串 ID 在训练时会直接报错。spark.sql.shuffle.partitions设为 120 是因为这份数据量在百万级左右默认 200 个分区会产生大量小文件拖慢后续训练。清洗完的临时表可以复用上一小节的聚合 SQL也可以直接把order_detail_clean作为训练数据源。这里要特别提示一个排查技巧如果 ALS 训练时报“requirement failed: No moments”一类的错误八成是rating列有 NULL 或者user_id列混入了字符串回到这层清洗逻辑里加一个df.filter(col(rating).isNotNull())就好。3. 用 Spark ALS 训练菜品推荐模型核心参数与调参策略3.1 为什么必须是 ALS矩阵分解在餐饮场景的适用边界推荐系统可选的算法很多基于物品的协同过滤ItemCF、SVD、ALS 都能做菜品推荐。ItemCF 的解释性强“吃过这道菜的人也吃了那道菜”很容易向业务方交代但它在用户冷启动场景下表现尚可在菜品冷启动新菜上线没人点过时彻底失效。SVD 在稠密矩阵上效果好可餐饮订单矩阵的稀疏度通常在 95% 以上SVD 的收敛速度和稳定性都会变差。ALS 的独特优势在于两点第一它把用户的隐式反馈下单次数、浏览记录建模成置信度而不是非要一个显式评分这正好匹配餐饮数据只有订单流水没有评分的现实第二它的交替最小二乘优化天然适合并行化每个 worker 只需要算自己分片内的用户向量或物品向量不涉及全局梯度同步。在 Spark 生态里MLlib 提供的ALS实现专门优化过隐式反馈场景这是其他算法不具备的条件。所以这个项目选 ALS 不是因为它最新而是因为它在“高稀疏 隐式反馈 分布式训练”这个交集里最稳。3.2 rank、regParam、alpha 三个参数先理解再动手ALS 的参数看着不多真正影响结果的就是这三个外加一个coldStartStrategy和一个maxIter。很多新手把 rank 调到 50、alpha 调到 20跑完发现推荐列表完全没法看就是因为不了解这些参数在隐式反馈场景下的实际含义。rank 是隐因子维度。维度越高模型能捕捉的细粒度口味越丰富比如“喜欢重辣 喜欢川菜 喜欢内脏类”这些因素是独立的但维度高了之后训练时间变长、泛化能力反而下降。对于餐饮数据这种行为信号相对弱的场景我一般从 10 到 30 之间调用验证集 RMSE 选最优而不是无脑加高。regParam 是正则化系数控制向量模长不要过大。餐饮数据的特点是部分用户下单极多一个月点 50 次部分菜品销量极高工作餐爆款这两类对象容易学到模长很大的向量导致推荐结果被少数活跃用户和热门菜品绑架。regParam 往大调能压住这种情况常见区间是 0.01 到 0.2具体要看训练集的 RMSE 曲线是否平滑。alpha 只在implicitPrefsTrue时生效表示置信度权重。公式里置信度confidence 1 alpha * ratingalpha 越大评分高的行为对向量的牵引力越强。如果 alpha 设得太大用户点过 5 次的菜和点过 1 次的菜在向量空间里被拉开到不合理的距离推荐列表会偏向那些“用户反复点的那一两道菜”多样性明显下降。餐饮场景我习惯从 30 到 60 之间起调然后观察推荐列表的多样性和覆盖率。3.3 训练与预测的最小可运行代码下面这段代码是基于清洗后的临时表做训练的最小闭环可以直接替换路径跑通from pyspark.sql import SparkSession from pyspark.ml.recommendation import ALS from pyspark.ml.evaluation import RegressionEvaluator spark SparkSession.builder \ .appName(dish_als_train) \ .config(spark.executor.memory, 4g) \ .config(spark.sql.shuffle.partitions, 120) \ .getOrCreate() # 读取清洗后的用户-菜品-评分三元组 df spark.sql( SELECT user_id, dish_id, rating FROM rating_tmp WHERE rating IS NOT NULL ) # 按用户切分训练集和测试集保证同一用户不会同时出现在两边 train, test df.randomSplit([0.8, 0.2], seed42) als ALS( userColuser_id, itemColdish_id, ratingColrating, implicitPrefsTrue, rank20, regParam0.1, alpha40, coldStartStrategydrop, maxIter15, seed42 ) model als.fit(train) # 用测试集评估隐式反馈的拟合效果 evaluator RegressionEvaluator( metricNamermse, labelColrating, predictionColprediction ) rmse evaluator.evaluate(model.transform(test)) print(Test RMSE , rmse) # 为每一个用户生成 Top-10 推荐列表 user_recs model.recommendForAllUsers(10) user_recs.show(5, truncateFalse)这段代码有几个关键点需要说明。train, test df.randomSplit([0.8, 0.2], seed42)是最常见的切分方式但严格来说应该按时间切分把最后一周的订单作为测试集这样更贴近真实场景。coldStartStrategydrop的意思是如果测试集里出现训练集从未见过的用户就直接丢弃预测结果而不是输出一个全零向量。recommendForAllUsers(10)返回的字段结构是user_id和一个包含[dish_id, rating]数组的recommendations列后续落库时需要把数组展开成多行。关于 RMSE 的解读要特别小心隐式反馈的评分本来就是我们构造出来的不是用户真实打分RMSE 只能说明模型对训练数据的拟合程度不能直接等同于推荐质量。真正要看的指标是把推荐列表里的菜品回回到业务数据里看看是不是用户最近真的在点的菜。3.4 调参的实用顺序先定 rank再动 alpha最后碰 regParam拿到一个不熟悉的餐饮数据集我一般按三步调参不交叉验证只靠经验能省很多时间。第一步固定rank15, alpha40, regParam0.1跑一次拿到基线 RMSE。第二步把 rank 从 10 到 30 每隔 5 试一次画一条 RMSE 曲线找到拐点。第三步在最优 rank 附近同时扫 alpha20/30/40/60和 regParam0.01/0.05/0.1/0.2用网格搜索加早停机制控制在 20 次训练以内。需要提醒的是Spark MLlib 的 ALS 训练在数据量大时比较吃内存。如果executor.memory只有 2g 而数据集有上千万行大概率会报 OOM。这时候不用急着加内存先做一步数据裁剪过滤掉下单次数低于 2 的用户和销量低于 5 的菜品矩阵稀疏度下降后训练速度会显著提升推荐效果反而更稳。4. 推荐结果落库与查询接口数据库不是摆设4.1 结果表设计user_reco 和 item_sim 两张表的用途训练完模型只是第一步“推荐系统”四个字要落地必须有一个可以给前端或 App 查询的接口。很多课程设计项目在这里偷懒只把model.save()的结果放在 HDFS 上就结束了这恰恰是答辩时最容易暴露短板的地方。真实的业务链路是离线 Spark 训练出的模型输出推荐列表写进 MySQL 或者 Redis然后业务侧通过一个查询接口按用户 ID 实时取数。我推荐至少设计两张结果表。第一张是user_reco字段包括user_id、reco_dish_id、reco_rank、reco_score、update_date主键是(user_id, reco_rank)这张表供“猜你喜欢”模块查询。第二张是item_sim字段包括dish_id、sim_dish_id、sim_score主键是(dish_id, sim_dish_id)这张表供菜品详情页的“搭配推荐”模块查询。两张表的行数范围一般是user_reco表是用户数乘以 10取 Top-10item_sim表是菜品数乘以 20 左右。为什么用 MySQL 而不是 Redis因为离线推荐结果的总行数通常在百万级以内MySQL 完全扛得住而且对于课程设计和绝大多数中小餐饮平台来说MySQL 的运维成本最低。如果后续并发上来了再在前面加一层 Redis 缓存也不迟。表结构建好后记得给user_id和dish_id建索引千万级以下的表不需要加分区但索引必须加。4.2 把 Spark 训练结果批量写回 MySQLSpark 写 MySQL 有两个常用方案df.write.jdbc()和foreachPartition批量插入。前者简单但慢后者适合千万级数据。这个项目的用户量一般在几十万量级Top-10 推荐结果也就几百万行用write.jdbc()完全没问题。from pyspark.sql import SparkSession from pyspark.sql.functions import explode, col spark SparkSession.builder \ .appName(dish_reco_writeback) \ .getOrCreate() # 读取训练好的推荐结果 user_recs spark.sql(SELECT user_id, recommendations FROM tmp_user_recs) # 将数组展开为多行 reco_df user_recs \ .select( col(user_id), explode(recommendations).alias(reco) ) \ .select( col(user_id), col(reco.dish_id).alias(reco_dish_id), col(reco.rating).alias(reco_score) ) \ .withColumn(reco_rank, row_number().over( Window.partitionBy(user_id).orderBy(col(reco_score).desc()) )) reco_df.write \ .mode(overwrite) \ .option(truncate, true) \ .jdbc( urljdbc:mysql://localhost:3306/dish_reco, tableuser_reco, modeoverwrite, properties{ user: root, password: your_password, driver: com.mysql.cj.jdbc.Driver, rewriteBatchedStatements: true, batchsize: 1000 } )这段代码里explode(recommendations)是最关键的一步它把 ALS 输出的嵌套数组展开成一行一道菜。Window.partitionBy(user_id).orderBy(col(reco_score).desc())给每个用户的推荐列表重新编号保证reco_rank从 1 开始连续。jdbc写入时的mode(overwrite)配合option(truncate, true)效果是先清空表再写入避免残留上次的旧推荐结果。rewriteBatchedStatements和batchsize是两个容易被忽略的 JDBC 性能参数批量写入从几分钟降到几十秒就是靠它们。写回之后一定要做一个校验动作在 MySQL 客户端里执行SELECT COUNT(*) FROM user_reco和 Spark 端reco_df.count()对比。两个数字如果对不上优先检查 user_id 或 dish_id 是否有 NULL 值——jdbc写入遇到 NULL 主键不会报错但会静默丢行。4.3 查询接口一个 Flink 不必要Flask 加连接池足够推荐结果写进 MySQL 后对外查询接口的技术选型不需要太重。Spring Boot 可以Flask 也可以核心不是框架而是数据库连接池的配置方式。很多项目在并发查询时直接挂掉不是因为 SQL 复杂而是每次请求都新建数据库连接连接数一高 MySQL 就拒绝服务。from flask import Flask, jsonify from dbutils.pooled_db import PooledDB import pymysql app Flask(__name__) # 数据库连接池最小 2 个连接最大 10 个 pool PooledDB( creatorpymysql, host127.0.0.1, port3306, userroot, passwordyour_password, databasedish_reco, mincached2, maxcached10, maxconnections20, blockingTrue, charsetutf8mb4 ) app.route(/api/reco/int:user_id, methods[GET]) def get_reco(user_id): conn pool.connection() try: with conn.cursor() as cursor: cursor.execute( SELECT reco_dish_id, reco_score FROM user_reco WHERE user_id %s ORDER BY reco_rank LIMIT 10, (user_id,) ) result cursor.fetchall() return jsonify({user_id: user_id, dishes: result}) finally: conn.close() if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)这个接口的核心在connection.close()不是真的断开而是把连接还给连接池这样同一连接可以被多个请求复用。mincached2保持两个常驻连接maxconnections20限制峰值。产品上线前我一般会用ab -n 1000 -c 50压一下这个接口目标是平均响应时间在 200 毫秒以内。如果压测发现响应变慢先看是不是user_reco表没建索引再看不必要的 SQL 有没有做全表扫描。5. 避坑指南ALS 在餐饮数据上最常见的 5 个翻车现场5.1 现象冷启动用户查询推荐结果为空推荐接口上线后你会发现一部分用户请求返回的是空数组。这些用户有两个特征要么是刚注册的新用户在order_detail表里没有任何下单记录要么是历史用户被清洗过滤了——上一章提到“过滤掉下单次数低于 2 的用户”这个操作会误伤只下过一单的沉默用户。原因ALS 的矩阵分解原理决定了它只能对“至少出现在训练集里一次”的用户生成向量。训练集里完全没有的用户模型无法输出推荐向量recommendForAllUsers的结果里根本没有这个 user_id。解决在查询接口里做兜底策略。user_reco表查不到数据时回退到“全平台热门菜品 Top-20”SQL 写SELECT dish_id, COUNT(*) AS cnt FROM order_detail GROUP BY dish_id ORDER BY cnt DESC LIMIT 20。这个热门榜提前算好放 Redis冷启动用户秒回不卡顿。真实的餐饮业务里冷启动用户转化率依赖的是热门榜而不是个性化推荐所以兜底策略不是减分项而是刚需。5.2 现象训练时 OOMSpark 直接抛异常本地用master(local[*])跑小数据集一切正常把同一个脚本丢到集群上跑全量数据几分钟后 executor 报 OutOfMemoryError。这个翻车现场我见过太多次。原因ALS 的交替最小二乘需要在内存中保存用户矩阵和物品矩阵同时缓存评分矩阵的分区。如果数据量超过 executor 内存并且spark.memory.fraction没有调整训练过程会频繁 GC 直到崩溃。解决两步走。第一步数据裁剪在聚合评分时过滤掉低频用户和低频菜品前面 SQL 加一个HAVING COUNT(*) 2作为硬性门槛。第二步调内存参数spark.executor.memory建议按每 100 万行评分数据分配 2g 内存起步spark.memory.fraction默认 0.6可以调到 0.7 但不要超过 0.75留足 JVM 元空间。如果这两步都做了还在 OOM检查是不是df.repartition(executor_num * 2)没做默认分区数太少会导致单个分区数据量过大。5.3 现象推荐结果全是热门菜品用户看不到任何惊喜跑完模型打开user_reco表一看Top-10 里七八道菜都是全平台销量最高的“大众菜”每个用户的推荐列表高度雷同。这种结果在业务上是没有价值的——系统推荐的全是用户本身就知道的菜。原因隐式反馈的评分构造有问题。order_cnt直接作为评分时热门菜品天然获得大量高分。即使用了LOG2映射如果 alpha 设置过大置信度公式1 alpha * rating会让高频行为对向量的主导作用过强模型学到的主要是“大家都点热门菜”的统计规律而不是个性化区分。解决三个方向同时调整。第一把评分里的LOG2映射改成减权形式比如SQRT(order_cnt)让不同频次之间的差距更小。第二alpha 从 40 往下调试 20 或 10让低置信度行为也有参与感。第三训练前把每个菜品的下单用户数做一次标准化用rating / dish_popularity来削弱热门菜品的绝对优势。这一步属于玄学调参但方向一定是“降低热门权重、提升低频但高相关菜品的权重”。5.4 现象报错 “requirement failed: No moments”pyspark.ml.recommendation.ALS训练时报错requirement failed: No moments通常出现在上游数据刚接入新字段时。这个报错本身很奇怪因为“moments”是统计学里的一阶矩、二阶矩概念ALS 的实现里会统计评分列的均值、方差等信息数据无法计算时就抛这个错误。原因评分列rating存在空值、NaN或者列的数值类型不对。更多时候是评分列里全部是同一个值比如全为 1方差为 0 导致统计量计算出问题。解决在训练前执行df.select(rating).summary()看分布然后过滤空值df df.filter(col(rating).isNotNull() col(rating).isNaN() False)再加一个硬校验防止全等值df.select(rating).distinct().count()必须大于 1。这个错误属于典型的“黑匣子报错”不看训练数据只看报错信息完全猜不到原因。5.5 现象本地跑通、集群跑挂的 Spark 版本兼容问题本地用 Spark 3.1 调好的代码搬到集群上发现 Scala 版本不匹配、com.mysql.cj.jdbc.Driver找不到、JSON 路径读不到文件。这类问题不算算法问题但确实最烦人。原因本地用的是 Spark 3.x Jakarta 命名空间集群上的 Spark 还是 2.4或者 MySQL Connector/J 的版本是 8.0 写成com.mysql.cj.jdbc.Driver而集群环境的 MySQL Connector/J 是 5.x 只能识别com.mysql.jdbc.Driver。解决用spark-submit --jars显式提交驱动包不要依赖集群环境的默认包。MySQL 驱动和 JSON 文件路径全部在提交脚本里指定不能写死在代码里。打包之前先确认集群的 Spark 主版本如果是 2.4 就坚持用org.apache.spark:spark-mllib_2.11对应的依赖版本而不是在新版本环境里编完直接扔上去。这个坑最好通过写一份部署脚本来规避把版本号、jar 包路径、环境变量统一管理每次换环境只需改脚本头部几行。6. 验证与进阶用召回和一次真实订单回放检验推荐效果离线 RMSE 不能完全代表推荐质量这个前面提过。要验证这套 ALS 数据库落地的方案到底行不行我常用两个方法一个是离线召回评估另一个是时间回放评估。后者更贴近真实业务做法是把数据集按时间切分成训练集和测试集——比如用前 60 天的订单训练模型用后 7 天的订单做验证然后检查测试集里用户真实点过的菜有多少出现在推荐的 Top-10 里。# 统计测试集里每个用户实际下单的菜品 test_user_dishes test.groupBy(user_id).agg( collect_set(dish_id).alias(real_dishes) ) # 和推荐结果求交集计算 recall10 def recall_at_k(reco_df, real_df, k10): joined reco_df.join(real_df, user_id) matched joined.filter(col(reco_dish_id).isin(col(real_dishes))) total real_df.count() return matched.count() / total if total else 0.0 print(Recall10 , recall_at_k(reco_df, test_user_dishes))真实的餐饮数据上这个方案能达到多少召回如果评分构造得当Top-10 的 recall10 在 0.15 到 0.25 之间属于正常水平。低于 0.1 说明评分构造或者参数有问题需要回到第 2 章的评分映射和第 3 章的调参顺序重新过一遍。高于 0.3 反而要警惕过拟合——说明模型把高频下单行为记忆得太死了泛化到新订单的能力未必好。进阶方向上一个值得做的尝试是把时间衰减因子加进评分构造里比如评分乘以exp(-days_since_last_order / 30)让一个月没点的菜权重自然衰减。这个改进在离线评估里也许只提升 0.02 的召回但在线上的用户感知明显——推荐列表里不再出现半年前偶尔点过、现在已经不吃的菜。另一个方向是给user_reco表加一个update_date字段实现推荐结果的 T1 增量更新而不是每天都全量重算。我最初做这类项目时犯过一个低级错误拿到源码第一件事就是调参跑模型结果训练出来的推荐列表被千篇一律的热门菜淹没。后来花了一整天去检查数据清洗和评分构造才发现问题出在order_cnt直接当评分用而不是 ALS 本身。这个教训让我养成了一个习惯调任何模型之前先花两倍时间确认进入模型的数据长什么样。做推荐系统数据质量的优先级永远高于算法参数的优先级。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网