1. 项目起因房源量上来之后推荐从可选变成了必需做这个系统之前我一直觉得推荐这词儿有点虚。房地产交易场景里用户买房是典型的低频高客单决策一套房子几百万谁会靠推荐来决定但实际做了两年之后我的看法完全变了——低频不代表不需要推荐恰恰因为低频用户对猜中我心意的要求高得离谱。我们当时面对的现状是平台上的商品房房源数量已经突破几十万量级分布在多个城市每个城市的板块、楼盘、户型组合起来光过滤条件就有十几层。用户进到列表页之后如果没有明确的目标比如三室两厅、总价500万以内、近地铁大部分人会翻两三页就流失。传统的综合排序——默认按更新时间倒序、或者按点击热度排序——根本撑不住。后来我们做过一次用户访谈得到一个很有代表性的反馈用户不是找不到房子而是找得太累了。几十页结果里真正符合他心意的可能只有三五套但要他自己筛选出来需要不断输入条件、翻页、对比。这个体验做到后面用户会直接放弃或者转向其他平台。所以个性化推荐在房产场景里解决的不是帮用户发现未知需求而是帮用户在海量SKU里减少决策成本。正是这个背景我们启动了商品房推荐系统项目。技术选型上团队当时已经具备比较成熟的Hadoop基础设施业务量级也撑得起分布式计算所以整体方案定了三条主线Hadoop做数据底座、Spark做离线特征与模型计算、Python做推荐逻辑与在线服务。我负责的部分是从数据接入到推荐结果产出全链路这篇文章记录的就是这套系统从0到1过程中最关键的设计决策和实操细节。如果你也在做电商、O2O、内容社区这类规模化推荐系统或者正准备用Spark跑机器学习任务这篇文章里讲的架构拆分、特征处理、调优经验都是可以直接迁移的。2. 技术选型为什么不直接用Python做所有事2.1 责任划分Hadoop存数、Spark算数、Python出结果很多朋友听到Python推荐系统第一反应是直接Pandas读数据算相似度矩阵再开个Flask接口不就行了吗几十万房源根本不是大数据单机完全扛得住。这话没毛病但放到真实业务环境里就走不通了。我们内部的原始数据远比几十万房源复杂。用户行为日志每天新增几千万条包含曝光、点击、收藏、咨询、对比、成交等多个动作房源主数据虽然量不大但是每天都在更新还有大量的图片、描述、配套设施等非结构化信息。再加上城市、板块、价格段、户型等维度拆分之后单机Pandas处理亿级行为日志光读数据就要十几分钟更别说join和聚合了。所以职责划分很明确层级技术组件负责内容数据底座Hadoop HDFS Hive存储原始日志、房源快照、清洗后的明细表离线计算Spark SQL Spark MLlibETL、特征拼接、画像构建、ALS模型训练算法逻辑Python脚本召回策略编排、规则过滤、排序融合在线服务Python Redis推荐结果读取、接口输出、埋点反馈Hadoop负责把数据存住Spark负责在合理时间内把数据算完Python负责把计算结果变成懂业务的推荐结果。这三层拆开之后每一层的问题都能独立优化不会互相拖累。2.2 为什么Spark在这套组合里不可替代如果只有几十万房源确实不用Spark。但当行为数据分析需要按天、按城市、按用户维度做累计聚合时单机方案的时间成本会呈指数上升。我们实际遇到过的情况是用Pandas处理两天的行为日志内存直接顶满用Spark跑同量级数据几十个executor并行三分钟出结果。这个差距在迭代调试阶段尤其明显——白天调代码、改特征晚上必须出结果验证计算时间太长根本没法形成快速实验闭环。另外Spark MLlib自带的ALS算法拿来做协同过滤非常省事分布式训练不用自己写并行逻辑。虽然业界有说法是MLlib的ALS效果不如深度模型但在房产推荐这个场景下行为数据稀疏程度极高深模型很容易过拟合ALS反而稳。后面我会细讲为什么ALS比深度学习更贴合这个场景。还有一点容易忽略Spark可以和Hive无缝集成直接用SQL操作HDFS上的表。我们团队里有不少同学只会写SQL不会写MapReduce或者Scala。有了Spark SQL这一层数据清洗和特征提取的代码门槛大幅降低很多临时分析任务用SQL就能搞定这对项目推进速度的帮助非常大。3. 数据接入把多源脏数据整理成能用的明细表3.1 数据来源梳理主数据、行为数据、外部数据推荐系统最怕的不是算法差而是数据乱。数据没理清楚之前谈算法都是白搭。我们当时接入的数据源主要分三类房源主数据从业务库同步过来的商品房信息包括楼盘名、户型、面积、朝向、楼层、装修状态、总价、单价、所在城市/板块/商圈、竣工年代、产权年限、物业类型等。这部分数据质量相对好但存在更新滞后和字段缺失的问题。用户行为数据来自App端和Web端的埋点日志记录了用户在房源列表页、详情页的每一次浏览、点击、收藏、咨询、对比、分享动作。这部分是最脏的字段缺失、时间戳时区不一致、用户ID和设备ID混用等问题都有。外部补充数据我们接了不少第三方数据比如地块周边的地铁站距离、学校划片信息、商超配套、二手房均价等。这些数据用来丰富房源画像中的位置价值维度。主数据通过Sqoop定期从业务库同步到HDFS落地为Hive分区表按天分区。行为日志通过Flume实时采集到Kafka再由Spark Streaming按小批次写入HDFS同样按天分区。这里有一个关键决策我们选择了离线批处理为主实时计算为辅。原因是推荐结果不需要实时到秒级——用户今天看过的房源明天再给他做个性化调整完全来得及。实时消费主要是为了监控和告警不是为了改推荐结果。3.2 清洗规则坐标漂移、价格异常、重复房源处理数据清洗大概是整个项目里最枯燥但又最重要的环节。我们遇到过几类典型脏数据处理方法可以给正在做类似项目的朋友参考坐标漂移。部分房源录入时经纬度坐标明显不准比如一个杭州的房子坐标落到了上海。我们用了Haversine公式计算坐标与城市行政中心的距离超过阈值就触发修正逻辑——优先用同楼盘的坐标均值如果整个楼盘的数据都缺失就用板块中心坐标兜底。价格异常。有些房源总价填的是首付金额有些填的是单价字段语义不统一。清洗规则里我们把总价和单价做了互检总价除以面积应该等于单价偏差超过5%就标记为异常并参考同小区均价修正。这一步看起来简单但对后面价格偏好的用户画像影响非常大——用户画像把价格区间分错推荐出来的房子不是买不起就是看不上。重复房源。商品房领域重复录房的场景比想象中多同一个楼盘同一户型的房子可能有多条记录只是楼栋号写错或者标题不同。我们用楼盘ID户型面积段总价段做组合去重保留信息最完整的那条。清洗之后的明细表设计为三层ODS层原样存储、DWD层清洗明细、DWS层汇总主题。推荐系统主要消费DWS层的房源主题表和用户行为主题表。实战提示清洗逻辑一定要留日志。每次清洗过程跑完把每个规则的命中条数、修正条数记下来否则后面排查特征异常的时候会非常痛苦——你不知道是清洗改了数据还是上游业务改了逻辑。4. 特征工程用Spark批量构造房源画像和用户画像4.1 房源画像把一套房子变成一串可计算的标签推荐系统的核心是把房子和用户都转化成计算机能比较的向量。房源画像我们设计成两个层次结构化标签。直接来自房源字段比如户型2室1厅、3室2厅、面积段70-90平、90-120平、总价段300-500万、500-800万、装修状态毛坯、简装、精装、朝向南、南北、东、西、楼层低、中、高、楼龄段5年内、5-10年、10年以上。这些标签直接映射为离散特征用One-Hot编码或者哈希编码都可以。派生标签。由原始字段计算出来的包括单价梯度总价/面积、位置价值分地铁距离、学校资源、商圈密度综合打分、户型方正度、楼层采光指数等。这些需要一些业务经验沉淀我们最初是跟运营侧反复确认后定了一套规则后面逐步迭代成模型打分。特征计算用Spark SQL写非常顺手。比如计算与地铁站距离这个特征核心逻辑是先读入地铁站点坐标表与房源坐标做笛卡尔积求距离再取最小值。这个操作在数据量小的时候用纯Python没问题但几十万房源乘以几百个地铁站就是上亿次距离计算Spark跑分布式求最小值的效率比单机强太多。SELECT house_id, MIN(haversine_distance(house_lon, house_lat, station_lon, station_lat)) AS nearest_subway_distance FROM dwd_house_info CROSS JOIN dim_subway_station GROUP BY house_idHaversine函数我们直接用Spark SQL的UDF实现注册一次全局复用。距离计算这个环节还有个加速技巧先做经纬度粗筛——两个点经纬度差值都小于0.5度的才精确计算距离这样能砍掉90%以上的无效计算。4.2 用户画像从稀疏行为里提炼可靠偏好用户画像比房源画像难做得多核心难点是行为数据稀疏。一个用户可能一个月只看了20套房但我们需要从这20次行为里判断他的真实偏好。我们按行为类型给不同权重打分行为类型权重说明曝光1列表页看到无法确认兴趣点击5主动点进详情有一定的兴趣收藏20强偏好信号咨询/约看50购买意向极强的信号对比15说明用户在多套之间纠结成交100最终付费历史最强信号用户对某个特征的偏好分 用户在该特征档位上所有行为加权和 / 用户该特征维度下所有档位的最大加权和。归一化到0到1保证不同用户之间的偏好强度可比。举个例子用户A看过10套房子其中8套是南向2套是东向。南向累计权重是8×5点击40东向累计权重是2×510归一化后南向偏好1.0东向偏好0.25。这个用户对朝向的要求就非常强。用户画像的计算同样在Spark里做。行为日志按用户聚合join房源标签表然后按特征维度做加权聚合。需要注意的是用户画像必须按时间衰减衰减——三个月前的行为和这周的行为权重不能一样否则用户已经换需求了系统还在推老房子。我们用了时间衰减系数行为时间越近权重越高半衰期大约是30天。4.3 特征存储画像结果怎么导出给线上用特征计算完需要落到线上能读到的存储里。我们分了两条路房源画像量相对固定几十万且更新不频繁直接落HBase按house_id查支持在线读取。用户画像量级大几百万且每天更新落HBase会占用大量存储空间。我们改用Redis的Hash结构存储key是user_idfield是feature_namevalue是feature_value设置7天过期。线上读取时如果Redis没命中再降级到HBase冷数据。Redis热数据 HBase冷数据这套组合在成本和安全之间平衡得不错。但要注意Redis不能存所有特征只存线上打分要用的那十几个核心特征不然内存开销太大。5. 推荐算法三种策略并行融合输出最终结果5.1 基于内容的推荐房产场景的主力基于内容的推荐Content-Based在房产推荐里是绝对主力。原因是房产这个品类的用户需求高度结构化——用户明确知道自己要什么朝向、什么面积段、什么总价。内容特征本身就能表达需求不太依赖和你像的人喜欢什么这种间接推断。核心实现逻辑将用户画像向量化取出用户对每个特征维度的偏好分组成用户向量U。将候选房源向量化每套房子的结构化标签组成房源向量H。计算余弦相似度 score cos(U, H)。加上一定的位置加权同城的房源额外加0.1的基准分因为跨城推荐毫无意义。这里有一个细节值得展开户型这个特征不能简单One-Hot之后算余弦。比如用户偏好3室2厅那2室1厅是不是一定不相关不一定也许用户只是预算范围内3室没货2室也接受。所以我们做了一层特征泛化在原始标签之上生成一组业务规则化的扩展标签。用户偏好3室2厅时同时给2室1厅一个0.5的衰减偏好分给4室以上一个0.3的衰减偏好分。这个概念和推荐系统里的标签扩展或query扩展类似就是为了缓解冷启动和稀疏问题。5.2 协同过滤ALS在隐式反馈上的应用协同过滤我们在Spark MLlib里用ALS实现。ALS适合处理用户-物品评分矩阵但我们的行为数据不是显式评分而是曝光、点击、收藏、咨询等多种隐式反馈信号。MLlib的ALS有个针对隐式反馈的变体训练时把行为次数组装成置信度矩阵confidence 1 alpha × ratingrating是行为权重归一化后的值alpha是一个放大系数我们调参后取40。alpha越大正样本有行为的交互对模型的影响越大。from pyspark.ml.recommendation import ALS als ALS( userColuser_id, itemColhouse_id, ratingColbehavior_score, implicitPrefsTrue, alpha40.0, rank50, regParam0.1, maxIter15 ) model als.fit(train_df) user_factors model.userFactors item_factors model.itemFactorsALS的产出一共有两个用途直接召回用model.recommendForAllUsers给每个用户推荐Top N房源。算相似房源用itemFactors算房源向量之间的余弦相似度产出类似房源池。用户看了一套房详情页可以推荐同小区或同户型的相似房源。关于rank隐因子维度的选择我们试过20、50、80三档。rank20时模型偏差大很多明显相关的房源召回不到rank80时过拟合严重测试集上的AUC反而下降rank50是效果和泛化都比较平衡的点。regParam我们选0.1主要为了抑制隐因子向量过大的问题。5.3 规则与排序融合让推荐结果可用、不越界算法打分只是个起点房源推荐跟商品推荐最大的不同在于决策成本高、试错成本高、地域属性极强。线上产品如果推了一个用户根本不可能买的房子用户不会像刷短视频那样划走就行而是会直接质疑平台的专业性。所以在最终排序前规则层做了如下过滤必需条件硬滤用户设定了城市、预算、户型那这个条件必须100%满足。用户画像里的偏好可以被算法参考但不能越过用户主动设定的筛选条件。冷门房源兜底如果一个房源历史曝光量极低说明用户不认可或房源本身有问题直接降权。连续30天无任何行为的房源从候选池剔除。行政合规滤除未达销售条件、证件不全的房源和相关项目信息从所有分桶排除确保为用户展示的内容均符合所在城市的销售管理要求。重复曝光抑制用户最近7天内看过超过3次的房源排序权重降50%。排序融合的公式我们最终定为最终得分 0.5 × 内容相似度 0.3 × ALS预测分 0.2 × 热度归一化分热度归一化分用房源近7天曝光到咨询的转化率计算这样好房源不会因为没有个性化匹配而被埋没。用户首次访问、无历史行为时内容相似度和ALS都退化为0全靠热度分兜底保证新用户也有人给推荐。6. 结果落地与在线服务模型产出怎么高效喂给用户6.1 离线产出流程一天三阶段的批处理推荐结果的产出不是一次性全量计算我们把它拆成了三个时段避免一次性算完导致数据新鲜度差每日凌晨02:00——跑全量任务。更新所有房源画像、用户画像、ALS模型训练、全量用户推荐结果落Redis。这个阶段处理的数据量最大占到全天计算量的80%。每两小时增量任务——跑增量数据。处理新上架的房源给它们找到第一批可能感兴趣的用户主要靠内容相似度召回插入推荐池。实时降级——用户请求推荐但Redis未命中时用简单的内容匹配规则在线兜底。这种三阶段设计的好处是用户每天早上打开App看到的是基于昨天行为数据的最新推荐白天新上架的房源最多延迟两小时进入推荐池不至于完全没曝光机会。6.2 线上接口设计缓存优先、批量读取推荐接口对外暴露一个简单POST接口传user_id、城市、分页参数返回推荐房源列表。内部逻辑先从Redis取该用户的推荐列表ID序列。如果Redis有且时间戳在4小时内直接返回。如果Redis没有或者过期触发降级逻辑用用户画像实时计算Top50候选再过滤规则后返回。返回前批量查询房源摘要信息拼接标题、图片、价格、户型等展示字段。线上性能方面最重要的优化是永远不要在线上一套房一查。几千个用户同时请求每套房都走一次数据库查询连接池必被拖死。我们的做法是推荐列表让Redis一次性返回候选ID序列再通过批量查询接口一次性取回所有房源摘要整个接口的p99延迟稳定在80ms以下。频道追踪用户在推荐列表上的点击、曝光等动作继续回流到Kafka进入第二天的离线训练数据。这个闭环是推荐系统持续进化的动力源。7. 踩过的坑和调优记录大数据推荐里绕不开的三个问题7.1 HDFS小文件问题我们最初把行为数据按天分区落地到HDFS每个分区下又按小时拆文件结果产生了大量小文件——有些文件只有几KB。HDFS适合大文件NameNode要管理每个文件块和目录项小文件一多内存直接告警。解决方式有两个在写入阶段做合并使用Spark的coalesce动态调整分区数控制每个文件大小在256MB左右。定期对ODS层做小文件合并用batch.compact方式跑一个纯重写任务把一天的日志合并成少量大文件。另外一个细节是下游读取Spark SQL读HDFS时文件数越多启动task数越多调度开销大执行慢。小文件治理后同样一天的日志任务跑批时间从25分钟降到8分钟收益非常明显。7.2 数据倾斜热门城市的计算任务效率瓶颈调试过程中最典型的任务是按城市统计行为数据并join房源信息。问题是某一线城市的房源和行为数据占了全量的40%以上Spark默认按key哈希分区之后大量数据涌入同一个executor任务严重倾斜——其他executor跑完了它还在跑。常用解法我们试过两轮第一轮用加盐方式处理把热点key加随机前缀打散join结束后再去掉前缀聚合。效果不错但代码复杂度高而且加盐粒度控制不好会带来数据正确性风险。第二轮改为Broadcast Join房源表几十万条记录完全可以用Broadcast变量广播到每个executor让小表和大表join时避免shuffle。这样热门城市key无论多大每个task都能本地读取房源信息不需要跨节点传输。最终方案就是Broadcast Join既简单又能根治倾斜。如果你也遇到类似问题建议先看看两个join的表能不能广播能广播就不要走Shuffle Join——这大概是Spark调优里性价比最高的操作。7.3 ALS训练内存与参数问题ALS调参最大的一次教训是一开始rank设成100maxIter设成30训练直接OOM。后来排查原因是隐因子向量过大模型参数数量远超样本量优化器需要的内存空间跟着膨胀。调参过程我们做了一个小规模实验组rankalpharegParam测试集AUC训练内存20400.10.682正常50400.10.713正常80400.10.705偏紧50200.10.698正常50500.10.702正常最后选择rank50、alpha40、regParam0.1。每秒处理用户量级约十万级别训练时间在40分钟内跑完全量数据。如果你在更大数据量上跑可以尝试用Spark的ALS.trainImplicit配合弹性分布式数据集特性做subsample训练效果差距不大但耗时能缩一半。8. 项目交付后的复盘这套系统真正改变了什么项目上线后大概跑了一个月我们做了一次效果回归。和旧的综合排序方案相比个性化推荐带来的业务变化有几个维度可以直接量化用户人均浏览房源数量下降了18%但详情页到咨询的转化率提升了22%。用户找到有意向房源的时间明显缩短咨询更高效。推荐列表的点击率比老版综合排序高了1.6倍。说明用户对推荐结果本身更感兴趣而不是把列表页当筛选器用。高意向用户30天内发生过咨询的次日留存率提升了9个百分点。这部分用户是平台的核心用户对个性化体验的感知最强。推荐系统最大的价值不一定是让用户多逛十分钟而是让用户更快找到自己要的东西。在低频率、高客单、强决策的品类里这一点对用户留存和平台口碑的作用比单纯拉升时长更重要。做完整套系统我个人的核心体会有两点。第一推荐系统是一个数据工程问题算法只是最后一公里。数据清洗、特征工程、存储链路、线上性能这四件事没做扎实任何算法都发挥不出来。很多团队一上来就调模型结果发现数据是脏的、特征对不上、接口延迟又高再好的模型也被糟糕的工程拖死。第二点是关于迭代节奏。这套系统真正稳定运行用了将近三个月前面两个月都在处理数据和踩坑真正的算法调优反而集中在最后两周。但前面的投入一点都不冤枉——数据越干净、画像越准确模型迭代的速度就越快。如果你正在启动一个类似的项目我的建议是把至少一半的时间留给数据层把你的特征体系先想清楚再谈用什么模型。
阅读完成 · 觉得有帮助?