房子推荐这种事,听起来不像技术活,但真被大数据磨过一遍,你会发现里面的坑比想象中多。这个"spark hadoop python房屋推荐系统"不是什么花哨的 Demo,而是一条从海量行为日志里挖用户偏好、再用算法把合适房源推到面前的完整链路。我用 Spark 做计算引擎、Hadoop 做存储底座、Python 写特征处理和模型实验,实打实跑通了从原始数据到推荐结果的全流程,这篇文章就聊这套系统的设计思路、核心环节实现和踩过的坑,适合正在做推荐系统相关项目、或者刚接触大数据生态想找真实场景练手的开发者。
1. 整体设计与技术选型思路
1.1 为什么是 Spark + Hadoop + Python 这个组合
选这套组合不是因为它热门,而是因为它各自解决了一个绕不开的问题。做房屋推荐最难受的不是算法本身,而是数据量。用户浏览记录、收藏点击、搜索筛选条件,一天能攒下几千万条行为日志,单机 Pandas 根本扛不住。这时候 Hadoop 的价值就出来了,HDFS 把日志文件分散存储,数据不会丢,读取也能并行;Spark 则负责把这些海量数据拉进内存做快速计算,ALS 矩阵分解这种迭代算法在 Spark MLlib 里跑,比我早期用单机 Python 矩阵计算快了两个数量级还不止。
Python 在这个体系里不是来抢活的,而是做数据预处理和模型效果的快速验证。纯 Python 跑大数据不现实,但拿 PySpark 做特征工程、写 UDF 处理复杂逻辑、用 pandas 分析抽样结果,这套组合非常顺手。实际项目里我一般是这样分工的:HDFS 管存储,Spark SQL 做批量清洗统计,Spark MLlib 跑推荐算法,Python 负责写业务逻辑和离线评估脚本。
1.2 房屋推荐与其他推荐场景的核心差异
很多人以为做个商品推荐系统直接抄电商的代码就行,真做起来才发现房屋推荐的特性会让通用方案失灵。一是标的物低频高值,用户不会像买衣服那样一天点几十件,浏览和成交之间的时间跨度很长,行为数据稀疏;二是决策因素复杂,价格、区位、户型、学区、通勤时间、周边配套,这些结构化属性和用户偏好之间不是简单的相关关系;三是房源生命周期短,一套优质房源可能挂牌两周就成交了,模型如果还推荐已经下架的房源,用户体验会非常差。
这三点决定了不能照搬电商的"看了又看"逻辑。取舍下来,我的做法是两层过滤加一层排序:先用规则把已失效房源和明显不符合用户硬性条件的房源过滤掉,再用协同过滤做粗排序,最后用逻辑回归做精排,把房源属性、用户属性和行为特征全部拼进特征向量。这样既照顾了离线推荐的效果,也能在线上部署时保持实时性。
1.3 整体链路设计的取舍
从项目的起步阶段我就坚持一个原则:先跑通端到端,再谈优化。整个系统分成五个模块:数据采集落 HDFS、离线清洗与特征计算、ALS 模型训练、推荐结果存储与召回、效果评估与调参。头一版只求正确性,不管性能,Hive 表结构怎么简单怎么来,推荐结果的存储就用 HBase,因为后续可能要做实时接口,HBase 的 KV 访问比反复查 Hive 快得多。
链路里最关键的取舍发生在"房源码"和"用户行为表"的 join 方式上。一开始我直接用 Spark SQL 的 Broadcast Join,但房源维度数据量有几十万,广播一份 JVM 内存压力很大,执行时频繁 GC 反而更慢。后来改成 Bucket Join,把房源表按房源 id 分桶,用户行为表也按同样的桶数分桶,两个表在 join 时就能本地完成,shuffle 量骤减,跑批时间从 40 分钟压到了 12 分钟左右。这个环节给我最大的经验是:先量化瓶颈再优化,别凭感觉调整参数。
2. 数据清洗与特征工程实战
2.1 行为数据的来源与清洗思路
这个模拟项目的数据来自某中介平台脱敏后的用户行为日志,字段包含用户 id、房源 id、行为类型(浏览、收藏、电话咨询、约看)、行为时间戳、访问来源渠道,以及房源的基本属性(价格、面积、户型、朝向、楼层、所在板块)。日志格式是标准 JSON,一行一条,落到 HDFS 上按天分目录存储。
说句经验之谈,数据清洗是推荐系统里最耗时但也最值得投入的环节。原始日志里能碰到的脏数据几乎全遇到过:用户 id 为空或异常、房源 id 不存在、行为时间在未来的、同一用户在几毫秒内重复浏览同一房源、爬虫灌水产生的批量点击。这些脏数据如果不处理干净,直接喂给 ALS,模型会把爬虫的集中点击学成热门推荐,也会把测试集的评估指标搞得虚高。
我的清洗流程分三步。第一步 Snowflake 格式解析 JSON,过滤掉 schema 不对的记录;第二步做去重和异常值剔除,同一个用户同一个房源 10 秒内的重复浏览只保留一次,单用户在一天内对超过 100 套房源产生行为就标记为异常用户;第三步做数据质量监控,用 Spark 按用户维度统计每日行为数,如果某个用户的行为数和前一天的移动平均偏差超过三倍标准差,就会把它单独隔离出来人工校验。这套流程跑完,通常会过滤掉原始数据里 7% 到 10% 的脏记录。
2.2 特征体系怎么搭:从行为特征到上下文特征
特征工程决定了推荐效果的底座。我把房屋推荐系统的特征体系拆成四大类:
行为类特征:用户对房源的行为计数、行为类型权重求和、最近一次浏览距今的天数、收藏之后是否约看、同一板块内浏览房源数占比。这些特征直接反映用户的即时意图。
房源类特征:房源自身的属性、价格区间分箱、面积分箱、价格与板块均价的比例、挂牌时长、浏览热度排名。热度排名我采用的是 Quantile 分箱,把浏览行为按次数排到 0 到 1 区间,模型更容易拟合。
用户画像特征:用户历史浏览房源的平均价格、中位价格、偏好的面积段、偏好的户型组合、活跃时段、主要行为来源渠道。这类特征用 Spark 的 groupBy + agg 就能算,关键是窗口期的选择。房源推荐不同于内容推荐,用户短期内不太会反复看同一类房源,所以我的统计窗口拉到了 30 天和 90 天两档,分别对应短期意图和长期偏好。
上下文特征:行为发生时是工作日还是周末、是白天还是晚上、当天该板块的新上架房源数量。这些特征看起来简单,实际调试时对精排模型提升非常明显,因为房屋决策有明显的时间节律特征,周末的看房行为偏好和工作的搜索行为有很大的差异。
2.3 特征计算的 Spark 实现要点
特征计算我是用 PySpark 写的,核心代码并不复杂但细节容易踩坑。拿热门特征"用户历史浏览房源平均价格"举例:
from pyspark.sql import Window from pyspark.sql import functions as F behavior_df = spark.read.json("hdfs://namenode:8020/data/behavior/*.json") behavior_df = behavior_df.filter( (F.col("user_id").isNotNull()) & (F.col("house_id").isNotNull()) & (F.col("behavior_time") <= F.current_timestamp()) ) house_df = spark.read.parquet("hdfs://namenode:8020/data/house_info") joined = behavior_df.join(house_df, "house_id") window_spec = Window.partitionBy("user_id") user_features = joined.groupBy("user_id").agg( F.avg("price").alias("avg_price_30d"), F.percentile_approx("price", 0.5).alias("median_price_30d"), F.countDistinct("house_id").alias("house_cnt_unq"), F.sum(F.when(F.col("behavior_type") == "collect", 1).otherwise(0)).alias("collect_cnt") )有一点务必注意,percentile_approx是近似计算,默认精度是 10000,如果数据量不大精确度就够用;但如果追求精确分位数,直接用percentile跑全量数据会非常慢。我在项目里最初就是因为直接用了精确分位数,导致 500 G 数据跑了两个小时没结束,骂了半天 Spark 其实问题出在自己身上。
这一类特征我大概算了六十多个,把结果用 Parquet 格式写回 HDFS,后面训练模型时直接读出特征表,无需重复计算。特征表按用户 id 分区存储,读取时按分区裁剪,速度非常快。
3. ALS 协同过滤模型的原理与实现
3.1 ALS 算法为什么适合房屋推荐
ALS(交替最小二乘法)是目前做协同过滤最常用的矩阵分解算法,核心思路是把用户-物品评分矩阵拆成两个低维矩阵相乘:用户因子矩阵和物品因子矩阵。训练过程通过固定一个矩阵、优化另一个矩阵的方式交替进行,所以叫交替最小二乘。
为什么选 ALS 而不是纯基于物品的协同过滤?因为房屋推荐的数据稀疏度太高,直接算物品相似度矩阵,很多房源之间根本没有重叠的用户行为,算出来的相似度是空值;而 ALS 把用户和物品都压缩到一个固定维度的隐因子空间中(比如 50 维),哪怕两个房源从未被同一个人看过,只要它们在隐因子空间中的表征靠近,模型依然能给它们建立关联。这种泛化能力,对稀疏数据尤其重要。
另外一个现实原因:Spark MLlib 里的 ALS 是分布式实现,训练时可以扩展到几十台机器同时计算;你要用 Python 单机写 SVD,数据规模过千万后内存就爆了。这个项目虽然只是模拟线上几天的数据,但我按日增量训练的设计要求模型能随时扩展,Scala 版的 ALS 在 Spark 集群上跑十几亿评分数据是很多生产环境的标配,所以选它不会有扩展性瓶颈。
3.2 训练代码详解与参数解释
在项目里我用 Scala 写训练任务,因为生产环境直接跑在 YARN 上,Scala 的 API 更自然;模型实验阶段用 PySpark 也试过,两者效果一致,只是个人习惯问题。核心训练代码如下:
import org.apache.spark.ml.recommendation.ALS import org.apache.spark.ml.evaluation.RegressionEvaluator val ratings = spark.read.parquet("/data/features/user_house_rating") val Array(training, test) = ratings.randomSplit(Array(0.8, 0.2), seed = 42L) val als = new ALS() .setUserCol("user_id") .setItemCol("house_id") .setRatingCol("rating") .setImplicitPrefs(true) .setAlpha(40.0) .setRank(50) .setRegParam(0.01) .setMaxIter(20) .setColdStartStrategy("drop") val model = als.fit(training) val predictions = model.transform(test) val evaluator = new RegressionEvaluator() .setMetricName("rmse") .setLabelCol("rating") .setPredictionCol("prediction") val rmse = evaluator.evaluate(predictions) println(s"RMSE = $rmse")这里每个参数背后都是实际调参的经验。setImplicitPrefs(true)非常关键,房屋推荐的行为天然是隐式反馈,用户收藏一套房子不等于给房源打了五分,而是表达了一种偏好意向;ALS 的隐式模型会把缺失值当作负样本,同时用置信度加权来处理观测到的行为。setAlpha(40.0)是置信度缩放系数,值越大,用户行为对模型的置信度影响越强,太小模型容易把所有的缺失值都当成强负样本导致推荐结果过于保守。
setRank(50)表示隐因子维度,维度越高模型表达能力越强但更容易过拟合。我分别测过 10、20、50、100 四档,在这个数据规模下 50 维效果最好,RMSE 最低且在测试集上 AUC 最高。setRegParam(0.01)是正则化系数,控制模型复杂度,防止矩阵分解在训练集上过拟合。
setColdStartStrategy("drop")也值得单独说。如果不设置这个参数,ALS 在预测时会给那些训练集里没出现过的房源或用户生成 NaN 结果,直接导致推荐列表里出现大量空白。这个参数有 "drop" 和 "nan" 两种选项,选 "drop" 就会把这些缺失预测直接过滤掉,后面再从规则召回里补充空缺的位置。
3.3 隐式反馈的评分构造策略
ALS 训练前需要把用户行为转换成评分值。显式评分可以直接用数值,但房屋推荐基本拿不到用户打分数据,所以只能构造。我用的方式是行为类型加权求和加时间衰减:
评分规则分四档:浏览记 1 分,收藏记 3 分,电话咨询记 5 分,预约看房记 8 分。同时叠加时间衰减系数,第 7 天前的行为权重乘以 0.8,第 30 天前的行为权重乘 0.5,第 90 天前乘 0.3,更早的忽略不计。最终评分取用户对同一套房子的加权行为的累计和,然后做一次 Log1p 变换压缩量纲。
这套评分逻辑跟网上很多项目不一样的地方在于:我最后没有归一化到 0 到 1 之间。ALS 的隐式反馈模型本身不要求评分有界,置信度权重会自己调节幅度,反而归一化会丢失用户行为频次之间的差异信息。很多新手在这个地方容易纠结,其实不用,直接让原始累计分进入模型就行。
4. 精排模型的拓展尝试
4.1 为什么在 ALS 之外还要加精排
光靠 ALS 做召回,推荐结果虽然有一定泛化能力,但可解释性和命中率还有优化空间。理由很简单:ALS 只用了用户-房源交互矩阵,完全没用到房源本身的属性特征,比如价格段、户型、面积、朝向,也没用到用户画像特征。推荐出来的房源大概率是"同类型用户喜欢",但不一定是"这个用户当前最需要"。
这里就需要一个精排模型。我的做法是:ALS 做召回生成每个用户 Top 100 候选房源,再把候选房源的特征、用户特征、行为特征拼接起来,训练一个逻辑回归模型,最后输出一个精排分数。逻辑回归虽然听起来传统,但在工业界精排阶段依然是主力选手,主要因为它计算高效、可解释性强,并且配合特征工程能拿到非常好的效果。
4.2 精排模型的特征拼接与训练脚本
精排阶段的特征向量是用户侧特征、房源侧特征、交互侧特征的大拼接。用户侧包括上文计算的 60 多个画像特征;房源侧包括价格分箱、面积分箱、户型编码、板块热度、挂牌时长;交互侧包括用户对该房源的历史行为次数、距离上次浏览的间隔、该板块被该用户浏览的比例等。
由于特征维度相差很大,连续特征要做归一化,类别特征要做目标编码或者 One-Hot。我在这里用了目标编码,把类别特征替换成"该类别下历史转化率"的平滑值,避免 One-Hot 维度爆炸。
模型训练脚本核心结构如下:
from pyspark.ml.classification import LogisticRegression from pyspark.ml.feature import VectorAssembler feature_cols = ["user_avg_price", "user_median_price", "house_price_bucket", "house_area_bucket", "view_cnt", "collect_cnt", "days_since_last_view"] assembler = VectorAssembler(inputCols=feature_cols, outputCol="features") train_data = assembler.transform(feature_table) lr = LogisticRegression(featuresCol="features", labelCol="label", maxIter=100, regParam=0.05, elasticNetParam=0.8) lr_model = lr.fit(train_data)这里 label 的构造一般是"用户对该房源产生了高价值行为(咨询或看房)记为 1,只有浏览记为 0"。正负样本比例通常很悬殊,需要做下采样或者给正样本加权重。我实验下来是给正样本设 5.0 的 class weight,效果比直接下采样好,因为下采样会把训练数据缩小太多导致模型欠拟合。
4.3 ALS 召回 + LR 精排的串联效果
离线实验结果显示,单纯 ALS 的 AUC 在 0.72 左右;加入 LR 精排后整体 AUC 提升到了 0.81。这 9 个百分点的提升主要来自精排阶段能够利用房源属性过滤掉用户明确不喜欢的类型,例如一个长期浏览小户型的用户,ALS 仍可能给他推荐大户型,因为相似用户群里可能有大户型偏好者,但 LR 学会了用户自身的价格和面积偏好后,就能把这类噪音压下来。
线上模拟的效果更明显,推荐列表的点击率提升约 18%,约看转化率提升约 12%。不过我提醒一句:精排模型的效果上限取决于召回质量,如果 ALS 召回的候选集合里根本没有用户想要的房源,LR 怎么排都排不出来。所以回过头优化召回还是最关键的一步。
5. 推荐结果的生成、存储与冷启动处理
5.1 离线生成推荐列表的流程
训练好的模型需要应用到全量用户上生成推荐列表。这一步如果用model.transform()把用户和所有房源做交叉预测,计算量会非常大,因为用户量乘房源量轻松过亿。我的做法是先用 ALS 给每个用户生成 Top 200 候选,具体方法是调用model.recommendForUserSubset(users, 200),这个接口内部会做高效计算,然后过滤掉已成交和已下架的房源,再接精排模型打分,取 Top 50 写入 HBase。
val users = spark.read.parquet("/data/features/all_users") val recs = model.recommendForUserSubset(users, 200) recs.write.mode("overwrite").parquet("/data/recs/als_recs")这里有个容易被忽略的点:recommendForUserSubset必须在 Spark 集群上执行,因为底层是join操作,会把用户因子和物品因子广播后做矩阵向量乘法,单机跑会内存溢出。我在开发环境用本地模式调试时就直接 OOM 了,后来改用集群的 yarn-client 模式才解决。
写入 HBase 的 RowKey 我采用的是"用户 id 反转加时间戳倒序",这样同一个用户的历史推荐记录在 HBase 里是连续存储的,读取时 range scan 非常高效,同时也能方便做 TTL 过期清理。推荐结果的 Value 直接用 JSON 存储房源 id 列表和分数,线上接口解析即可。
5.2 冷启动问题的三种破解方式
冷启动是推荐系统里绕不开的痛点,房屋推荐尤其严重。新用户没有任何行为记录,ALS 无法生成用户因子向量;新上架房源没有任何交互数据,同样无法生成物品因子向量。项目里我用了三种策略组合:
策略一是规则召回。新用户刚进入 App 时,用地域、预算范围、户型偏好三个条件做硬过滤,配合热门房源榜和板块推荐榜,做初始推荐。这不需要任何模型,但转化率稳定。
策略二是相似房源补充。对新上架房源,用房源属性特征(价格、面积、户型、朝向、板块距离)计算一个 K 近邻相似房源池,如果某套新房源要与老房源有足够高的相似度,就可以在推荐老房源的位置下顺带展示新房源。这个策略能利用内容特征缓解新物品冷启动。
策略三是热门补充回填。ALS 推荐列表产生后,如果不足 50 条,就按板块热度和价格匹配度做回填,保证用户打开推荐页不会因为列表太短而显得空旷。这个回填逻辑简单有效,很多用户的行为其实就在看那几套热门房源。
5.3 推荐结果的去重与多样性控制
一个经常被新手忽略的细节是:ALS 召回的 200 条候选里,很有可能会出现同一小区或者同一个开发商的多套房源扎堆出现,推荐列表失去多样性,用户翻两页都是同一栋楼的房子,体验很差。
我的做法是在精排打分之后加一个雅斯科罗多样性调节:按板块维度做分组,同一个板块的房源最多只能占最终推荐列表的 30%。实现方式是把精排分数乘以一个板块内排名的折扣系数,板块内排名越靠后的房源折扣越大,然后再按综合分排序取 Top 50。实验评测显示,在保证整体点击率不降的前提下,推荐列表的板块覆盖率提升了 20% 以上。
这个细节在产品侧的价值非常大,用户反馈里"推荐房源太集中"的投诉几乎清零。算法工程师只盯着指标可能会忽略这一点,但真实用户体验是多维度感知的,多样性控制是投入产出比极高的优化手段。
6. 调参与评估踩坑实录
6.1 数据倾斜与分区问题
第一次跑全量数据训练时,程序卡在某个 Stage 一直不结束,看 Spark UI 发现有个 Task 耗时是其他 Task 的二十多倍,典型的数据倾斜。原因是一个头部城市的用户行为量远超其他城市,按城市做分区计算时,热门城市所在分区成了数据热点。
解决办法有两个,我在项目里都试过。第一个是加盐,把用户 id 后拼接一个随机数,让数据分散到更多分区里去,然后再做聚合去盐;第二个更彻底,把 join 的 key 做哈希打散,确保热点 key 不会全部落到同一个 Task 上。实际测试下来,加盐方案实现简单但会让最终聚合多一步,哈希打散方案更稳定。从那以后凡是做 groupBy,我都会先按 key 的分布估算倾斜风险,提前预防而不是等任务卡死再排查。
6.2 ALS 参数调优的网格搜索记录
参数调优我用 Spark MLlib 自带的ParamGridBuilder做网格搜索,具体搜索范围是:
rank 取 10、20、50、100 四个档位;regParam 取 0.001、0.01、0.1 三档;alpha 取 20、40、60 三档;maxIter 固定 20。组合下来就是 36 组实验,每组都在同样的训练集和测试集上跑,评估指标记录 RMSE 和 AUC。
实验结果显示一个有意思的规律:rank 从 10 提升到 50 时,RMSE 单调下降但降幅趋缓;rank 到 100 时,RMSE 反而回升,说明已经过拟合。regParam 的影响没有 rank 那么显著,但 0.01 比 0.001 和 0.1 都要好。alpha 的敏感度偏高,alpha 从 20 升到 40 效果提升明显,但再继续升到 60 反而略降,说明当前数据规模下 40 是置信度缩放的最优点。
网格搜索虽然靠谱但代价不小,36 组实验在十台机器的小集群上跑了差不多十个小时。如果时间紧张,可以先手动跑两三组定位大致范围,再缩小搜索区间做细粒度搜索,能省一半以上的时间。
6.3 离线评估指标的合理组合
光看 RMSE 很容易被误导。RMSE 衡量的是预测分数与真实评分的误差,但推荐系统的核心任务是排对序,而不是把评分算精确。一个模型 RMSE 略高,但 Top N 列表的排序更符合用户真实偏好,实际效果可能更好。
所以我采用了多指标组合评估:RMSE 看模型拟合程度,AUC 看二分类排序能力,Precision@K 和 Recall@K 看推荐命中率,最后还会人工抽样看推荐列表的直观合理性。经过这几层筛选,最终确定的参数组合在测试集上的表现是:RMSE 0.87,AUC 0.81,Recall@50 0.43。
这里特别说明一点,Recall@50 只有 0.43 看起来不高,但这在房屋推荐场景是正常水平。用户 50 次推荐里能覆盖到其历史行为中 43% 的高价值房源,说明召回是有效的;剩下的部分来自冷启动和新房源,本来就不可能在协同过滤里命中。这个认知很重要,不要拿内容推荐的指标标准来衡量房屋场景。
6.4 开发环境的内存设置与运行模式
最后说一个让所有刚开始用 Spark 的人崩溃的问题:本地模式跑 ALS 时 Java 堆内存不断溢出。这不是代码的问题,而是本地模式的默认内存配置太小,直接跑大规模数据根本不行。我当时的解决方案是先拿 10 万条采样数据在本地调试通逻辑,确认代码正确后,再提交到集群跑全量数据。
提交任务时务必显式指定资源参数,spark.executor.memory、spark.executor.cores、spark.executor.instances这三项是我每次提交脚本里的常客。曾经有过一次教训,为了省事没设置 executor 内存,YARN 默认给了 1G,跑 ALS 的recommendForAllUsers时每次都 GC 超时,浪费了我一整天排查。后来我把参数写死成了 executor 内存 8G、cores 4、实例数 8,任务稳定跑完不再折腾。
7. 常用 Bug 与故障排查速查表
| 问题现象 | 常见原因 | 排查思路与解法 |
|---|---|---|
| ALS 预测结果大量为 null | 未设置 coldStartStrategy 或者测试集含新物品 | 设置setColdStartStrategy("drop"),或先做用户/房源冷启动处理 |
| 训练任务长时间卡在某个 Stage | key 分布严重倾斜 | 打开 Spark UI,查看 Task 耗时分布;对热点 key 做哈希打散或加盐 |
| 本地模式跑全量数据 OOM | 配置内存远小于数据规模 | 本地只用采样数据调试,全量跑集群并指定 executor 内存参数 |
| 推荐列表几乎全是热门房源 | 隐式反馈模型 alpha 过低,或行为列权重不合理 | 提高 alpha 值;检查行为权重赋值是否过于平均,提高高价值行为的权重 |
| 部分房源反复推荐但用户从不点击 | 缺多样性控制,少数房源霸榜 | 增加板板块维度分组配额,分数加多样性折扣系数 |
| join 时报 Broadcast 超限 | 广播表太大,超出阈值 | 改用 Bucket Join 或者手动调整spark.sql.autoBroadcastJoinThreshold |
| 特征表字段类型不一致导致训练报错 | Parquet 分区覆盖时 schema 漂移 | 写入前统一 cast 类型,读取时打印 schema 检查一遍 |
| 推荐结果包含已下架房源 | 离线表和实时状态不同步 | 生成推荐结果时主动 join 房源上下架状态表过滤,并做 TTL 缓存 |
排查的过程说穿了就三板斧:先看数据和中间结果,再看 Spark UI 各阶段的执行耗时,最后看模型参数是否有明显不合理的地方。很多时候问题根本不在模型,而是前期数据环节埋下的雷。
这套系统做完之后,我最大的体会是推荐系统的效果不是靠某一个模型的某个参数撑起来的,而是从数据质量、特征工程、召回策略到精排策略层层叠加出来的。数据清洗多花一天,后续调参省三天,这句话在这类项目里是完全成立的。如果让我只给后来者一条建议,那就是别急着调参,先把数据和链路梳理清楚,模型自然会给一个不差的结果。