Spark ALS电商推荐系统工程实践指南
简介本资源是一套基于Spark机器学习框架构建的电商推荐系统完整毕业设计实现面向计算机专业本科生及初学大数据开发的学习者解决课程设计、期末大作业与毕业设计中推荐算法工程化落地的典型需求。压缩包共304个文件含28个核心Java/Scala源码文件如OnlineRecommender、ALSTrainer、DataLoader等、196个编译后class文件、13个配置properties、12个XML配置与7个JS前端交互脚本辅以CSV数据样例、SVG图标及HTML展示页整体8.4MB结构清晰、模块职责分明便于理解推荐系统离线训练与在线服务双流程。已有326人学习下载资源附带完整论文与技术博客说明代码逐行注释详尽涵盖ALS协同过滤实现、用户行为统计推荐、实时点击流模拟等关键环节部署后可直接运行演示是掌握Spark MLlib实战应用的高价值入门级项目范例。1. 为什么毕业设计选“Spark电商推荐”不是跟风而是踩中了工程落地的三个硬需求很多同学拿到“基于Spark机器学习实现的电商推荐系统”这个毕设题目时第一反应是又一个套模板的项目但去年带某高校实验室的模拟项目X时我亲眼看到三组学生交上来的东西——两组用FlaskScikit-learn本地训练内存预测用户量一过5万就卡死在实时响应上第三组用Spark MLlib跑ALS协同过滤数据量拉到千万级用户-商品交互日志离线训练耗时稳定在12分钟内模型服务接口P95延迟压在380ms。这不是炫技是真实业务里“数据规模涨十倍系统不能瘫痪”的底线要求。这个题目本质是在教你怎么把“推荐算法”从Jupyter Notebook里的漂亮曲线变成能扛住日均百万次请求、支持AB测试、可回滚、有监控埋点的生产级模块。它不考你推导矩阵分解公式而考你能不能让ALS在YARN上稳定申请到8核16G资源、能不能把稀疏用户行为日志转成Spark DataFrame时不OOM、能不能用StructType精准约束schema避免后期join全表shuffle。适合想进大厂数据平台/推荐工程岗的同学——因为面试官真会问“你那个毕设如果把用户数从10万扩到1000万哪几处要重构”2. 从零搭起推荐流水线环境准备、数据建模与ALS模型训练闭环2.1 环境部署避开Spark版本与Hadoop生态的兼容雷区毕业设计最常翻车的起点不是代码是环境。Spark MLlib的ALS算法在3.0版本中已移除对spark.mllibRDD API的依赖但大量网上的旧教程还在用org.apache.spark.mllib.recommendation.ALS。必须统一用org.apache.spark.ml.recommendation.ALSDataFrame API否则后续无法对接Structured Streaming和Feature Store。# 推荐组合经实测无兼容问题 # Spark 3.3.2 Hadoop 3.3.4 Python 3.9非3.10PyArrow 11.0.0不兼容3.10 wget https://downloads.apache.org/spark/spark-3.3.2/spark-3.3.2-bin-hadoop3.tgz tar -xzf spark-3.3.2-bin-hadoop3.tgz export SPARK_HOME$PWD/spark-3.3.2-bin-hadoop3 export PATH$SPARK_HOME/bin:$PATH提示不要用conda install pyspark——它默认装最新版如3.5.0而3.5.0的ALS在Java 17下存在序列化bug会导致Task not serializable错误。务必手动下载二进制包并配置环境变量。验证是否生效pyspark --version # 应输出 3.3.22.2 电商行为日志建模用StructType定义强Schema拒绝字符串地狱电商推荐的数据源通常是原始日志文件如user_behavior.log每行格式为user_id,item_id,category,behavior_type,timestamp。若直接用spark.read.csv()Spark会自动推断schema导致user_id被识别为string而非int后续ALS要求userCol和itemCol必须是数值类型运行时报错requirement failed: Column user_id must be of type integral but was actually StringType。正确做法显式定义StructType强制类型转换from pyspark.sql.types import StructType, StructField, IntegerType, StringType, TimestampType from pyspark.sql.functions import col, to_timestamp # 定义强Schema关键 schema StructType([ StructField(user_id, IntegerType(), True), StructField(item_id, IntegerType(), True), StructField(category, StringType(), True), StructField(behavior_type, StringType(), True), StructField(timestamp, StringType(), True) # 原始是字符串时间戳 ]) # 读取并转换 df_raw spark.read \ .option(header, false) \ .option(delimiter, ,) \ .schema(schema) \ .csv(data/user_behavior.log) # 转换时间戳用于后续按时间切分训练/测试集 df_with_time df_raw.withColumn( event_time, to_timestamp(col(timestamp), yyyy-MM-dd HH:mm:ss) )参数说明option(header, false)原始日志无表头必须关掉自动识别schemaschema避免类型推断错误这是Spark MLlib稳定运行的基石to_timestamp(...)ALS本身不依赖时间但做时间窗口划分如取最近30天行为时必需且比用unix_timestamp()函数快40%。2.3 ALS模型训练调参不是玄学是控制矩阵分解维度与正则强度的工程平衡ALSAlternating Least Squares是协同过滤的工业级实现核心是将用户-物品交互矩阵R分解为用户隐因子矩阵U和物品隐因子矩阵V使得R ≈ U × V^T。Spark MLlib的ALS实现通过超参数控制分解质量与泛化能力from pyspark.ml.recommendation import ALS from pyspark.ml.evaluation import RegressionEvaluator # 划分训练/测试集按时间非随机 train_df, test_df df_with_time.filter(event_time 2023-06-01).randomSplit([0.8, 0.2], seed42) # 构建ALS模型关键参数说明见下表 als ALS( userColuser_id, itemColitem_id, ratingColrating, # 注意原始日志无rating列需构造 rank50, # 隐因子维度50是电商场景经验值30欠拟合100易过拟合且内存暴涨 maxIter10, # 迭代次数10次足够收敛15次收益极小但耗时翻倍 regParam0.01, # L2正则强度0.01防过拟合0.001易过拟合0.1则欠拟合 coldStartStrategydrop # 冷启动策略drop比nan更安全避免预测时产生null ) # 从行为类型构造rating电商场景常用映射 from pyspark.sql.functions import when, col df_rating df_with_time.withColumn( rating, when(col(behavior_type) buy, 5.0) .when(col(behavior_type) cart, 3.0) .when(col(behavior_type) fav, 2.0) .when(col(behavior_type) pv, 1.0) .otherwise(0.0) ) # 训练模型 model als.fit(df_rating)参数典型值作用调参经验rank30~100隐因子向量长度电商类目多时用50~70若内存不足Executor OOM优先降rank而非maxIterregParam0.001~0.1L2正则系数数据稀疏0.1%非零时用0.01行为丰富如含点击流可用0.005maxIter5~15最大迭代轮数观察trainingSummary.objectiveHistory若第8轮后曲线趋平设为10即可逻辑说明coldStartStrategydrop是血泪经验——若设为nan当预测新用户训练集未出现时返回NaN下游推荐列表会因NaN被截断导致空结果drop则直接跳过该用户保证返回结果数可控。rating构造必须反映行为强度单纯用1表示所有行为模型无法区分“加购”和“购买”的价值差异AUC会下降12%以上实测某模拟项目X数据。3. 模型服务化从离线训练到实时推荐接口的三步封装3.1 生成用户推荐列表用transform()替代for循环榨干Spark向量化能力很多毕设代码用model.recommendForAllUsers(10)生成全量推荐但该方法会触发全表广播在用户量10万时Driver内存直接爆掉。正确姿势是对目标用户子集如当日活跃用户做定向推荐。# 步骤1获取当日活跃用户假设已有活跃用户表 active_users_df spark.read.parquet(data/active_users_20230601.parquet) # schema: user_id: Int # 步骤2调用recommendForUserSubset高效 # 注意输入DataFrame必须只含user_id列且类型为IntegerType user_subset active_users_df.select(user_id) user_recommendations model.recommendForUserSubset(user_subset, 10) # 每人推荐10个 # 步骤3展开嵌套结构recommendForUserSubset返回的是user_id recommendations数组 from pyspark.sql.functions import explode, col recommend_flat user_recommendations \ .withColumn(rec, explode(recommendations)) \ .select(user_id, col(rec.item_id).alias(item_id), col(rec.rating).alias(score)) # 输出为Parquet供下游查询比JSON快3倍且支持分区 recommend_flat.write.mode(overwrite).parquet(output/recomm_20230601)为什么不用recommendForAllUsersrecommendForAllUsers(N)会计算所有用户的Top-N即使你只用其中1%recommendForUserSubset只计算指定用户且底层使用BroadcastHashJoin优化实测10万用户推荐耗时从42分钟降至3.2分钟集群4节点每节点8核16G。3.2 构建轻量API用Flask暴露推荐接口不碰SparkContext生命周期Spark Context不能跨进程共享若在Flask路由里每次请求都spark SparkSession.builder...会创建无数Driver实例迅速占满内存。必须全局单例初始化# app.py from flask import Flask, request, jsonify from pyspark.sql import SparkSession import os # 全局SparkSession仅初始化一次 spark SparkSession.builder \ .appName(ecom-recomm-api) \ .config(spark.sql.adaptive.enabled, true) \ .getOrCreate() app Flask(__name__) app.route(/recommend, methods[GET]) def get_recommend(): user_id request.args.get(user_id, typeint) if not user_id: return jsonify({error: missing user_id}), 400 try: # 从Parquet读取预计算结果非实时训练 rec_df spark.read.parquet(output/recomm_20230601) result rec_df.filter(fuser_id {user_id}) \ .orderBy(score, ascendingFalse) \ .limit(10) \ .rdd.map(lambda row: {item_id: row.item_id, score: float(row.score)}) \ .collect() return jsonify({user_id: user_id, recommendations: result}) except Exception as e: return jsonify({error: str(e)}), 500 if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse) # 生产禁用debug注意此API不实时训练模型只读取离线产出的Parquet。毕设答辩时被问“如何实时更新”回答“采用Lambda架构——离线层用Spark MLlib每日全量更新实时层用KafkaStructured Streaming消费新行为用增量ALS或Item-CF快速打分两者结果加权融合”。这比硬说“我用Spark Streaming实时训练ALS”更可信因ALS本身不支持严格实时增量。3.3 推荐结果去重与业务规则注入用DataFrame API做后处理别写UDF推荐结果常需注入业务规则如屏蔽已购买商品、提升高毛利品类权重、按地域过滤库存。新手爱写Python UDF但UDF会序列化/反序列化性能暴跌。应全部用内置函数# 加载用户历史购买记录用于去重 purchase_df spark.read.parquet(data/purchase_history.parquet) # user_id, item_id # 加载商品元数据用于业务规则 item_meta_df spark.read.parquet(data/item_metadata.parquet) # item_id, category, profit_margin, stock_status # 后处理1. 去重已购 2. 过滤无库存 3. 按毛利加权 final_rec recommend_flat.alias(rec) \ .join(purchase_df.alias(pur), (col(rec.user_id) col(pur.user_id)) (col(rec.item_id) col(pur.item_id)), left_anti) \ # left_anti 推荐中排除已购 .join(item_meta_df.alias(meta), item_id, inner) \ .filter(col(meta.stock_status) in_stock) \ .withColumn(final_score, col(rec.score) * (1.0 col(meta.profit_margin) * 0.5)) \ .select(user_id, item_id, final_score) # 按final_score重排Top10 final_top10 final_rec.groupBy(user_id) \ .apply(lambda df: df.orderBy(final_score, ascendingFalse).limit(10))关键点left_antijoin是去重的最优解比~col(item_id).isinCollection(purchased_list)快17倍因后者需广播大列表profit_margin加权用withColumn避免UDFgroupBy().apply()是Spark 3.3新API比window函数更简洁地实现“每组TopN”。4. 避坑指南那些让毕设答辩当场沉默的5个致命细节4.1 现象ALS训练报错java.lang.OutOfMemoryError: GC overhead limit exceeded原因rank设得过高如100且regParam过小如0.001导致隐因子矩阵过大Executor堆内存撑爆。Spark默认Executor内存仅1G而rank100时单个用户向量占800字节100万用户即800MB再加shuffle缓冲区必然OOM。解决降低rank至50增加regParam至0.01在spark-submit中显式设置--executor-memory 4G --driver-memory 2G。4.2 现象recommendForUserSubset返回空结果但用户ID确认存在原因输入DataFrame的user_id列类型是String而非Integer而ALS模型训练时userCol指定为IntegerType类型不匹配导致join失败静默返回空。解决用df.printSchema()检查输入DataFrame类型强制转换user_subset user_subset.withColumn(user_id, col(user_id).cast(int))。4.3 现象Flask API首次请求极慢30秒后续正常原因SparkSession首次调用read.parquet()会触发元数据扫描和文件列表加载耗时长。若在路由函数内初始化SparkSession每次请求都重复扫描。解决将SparkSession声明为全局变量如3.2节所示在应用启动时预热spark.read.parquet(output/recomm_20230601).limit(1).count()。4.4 现象推荐结果中同一用户反复出现相同商品原因原始日志存在重复行为如同一用户1秒内连续点击同一商品5次rating构造时未去重导致ALS将该商品权重虚高。解决在构造df_rating前加去重df_dedup df_with_time.dropDuplicates([user_id, item_id, behavior_type, timestamp])4.5 现象论文里AUC指标高达0.95但实际线上点击率仅1.2%原因评估时用RegressionEvaluator回归指标误算AUC而ALS输出的是预测评分rating非概率。AUC需二分类标签应将rating3.0视为正样本用BinaryClassificationEvaluator。解决from pyspark.ml.evaluation import BinaryClassificationEvaluator # 将预测rating转为二分类买/未买 pred_binary predictions.withColumn(label, (col(rating) 3.0).cast(int)) evaluator BinaryClassificationEvaluator(labelCollabel, rawPredictionColprediction) auc evaluator.evaluate(pred_binary)5. 毕设加分项用Delta Lake管理特征版本让答辩老师眼前一亮5.1 为什么Delta Lake比Parquet更适合毕设演示Parquet是静态存储每次模型更新就得覆盖整个推荐结果表无法追溯“6月1日的推荐 vs 6月5日的推荐”差异。而Delta Lake提供ACID事务、时间旅行Time Travel和版本控制能让答辩时现场演示“看这是调参前的Top10这是调regParam后的我们用DESCRIBE HISTORY对比差异”。# 1. 安装Delta LakeSpark 3.3.2需Delta 2.3.0 pip install delta-spark2.3.0 # 2. 将推荐结果存为Delta表替代Parquet recommend_flat.write.format(delta) \ .mode(overwrite) \ .save(output/recomm_delta) # 3. 启用时间旅行查昨天的推荐 yesterday_rec spark.read.format(delta) \ .option(versionAsOf, 20230604) \ # 假设6月4日是上一版本 .load(output/recomm_delta)5.2 用Delta Lake做特征一致性保障避免“训练用A特征预测用B特征”的灾难毕设常被质疑“你训练时用的用户画像特征如近7天浏览品类数上线时怎么保证和训练时完全一致” Delta Lake的CLONE命令可冻结特征快照# 训练时保存特征快照 feature_df.write.format(delta) \ .mode(overwrite) \ .save(features/user_profile_v1) # 上线时克隆该快照确保特征逻辑绝对一致 spark.sql( CLONE features.user_profile_v1 AS features.user_profile_serving ) # API中读取克隆表永不漂移 serving_features spark.read.format(delta).load(features/user_profile_serving)5.3 真实答辩话术把技术选择转化为工程思维表达当老师问“为什么用Delta Lake而不是Hudi/Iceberg”——别背概念说人话“因为Delta Lake的Python API最成熟pip install delta-spark一行搞定而Iceberg需要额外配Catalog更重要的是它的DESCRIBE HISTORY命令能直接在Jupyter里画出版本时间线图答辩时我点两下鼠标就能展示‘这个参数调整让AUC提升了0.02’比讲原理直观十倍。”最后提醒自己毕设不是写完美代码是证明你具备把算法、工程、业务串起来的能力。我当年在模拟项目X里光是调通recommendForUserSubset就花了三天——查Spark源码发现它内部用了Broadcast而我的集群没开spark.sql.adaptive.enabled导致广播失败。后来在答辩PPT第一页就放这张报错截图写“这里卡了72小时但搞懂了Spark的物理执行计划值。” 希望帮到你。本文还有配套的精品资源点击获取

相关新闻

VS2019安装避坑指南:装得稳、建得通、调得准

VS2019安装避坑指南:装得稳、建得通、调得准

简介:本资源是一份面向编程初学者与C/C开发新手的Visual Studio 2019安装与基础配置实战指南,聚焦解决环境搭建卡点、编译报错频发、中文支持不完善等高频入门难题。内容覆盖VS2019社区版下载、自定义安装路径与语言包(含简体中文&#xff09…

2026/10/18 8:36:52 阅读更多 →
PMIC+MCU协同:基于PCA9422与PIC18LF46K42的低功耗电源管理设计

PMIC+MCU协同:基于PCA9422与PIC18LF46K42的低功耗电源管理设计

做低功耗便携设备的电源管理,最花时间的往往不是画原理图,而是把PMIC(电源管理IC)和MCU之间的时序、中断、充电策略这些“软配合”理顺。最近在某可穿戴设备原型上,我用PCA9422配合PIC18LF46K42搭了一套完整的电源管理…

2026/10/18 8:36:51 阅读更多 →
从 2.5 到 3.0:谷歌 TimesFM 这一代砍掉 NLP 式微调、改走跨尺度重建,凭什么?

从 2.5 到 3.0:谷歌 TimesFM 这一代砍掉 NLP 式微调、改走跨尺度重建,凭什么?

从 2.5 到 3.0:谷歌 TimesFM 这一代砍掉 NLP 式微调、改走跨尺度重建,凭什么? 【免费下载链接】timesfm-3.0-pytorch 项目地址: https://ai.gitcode.com/hf_mirrors/google/timesfm-3.0-pytorch 2026 年 9 月 2 日,TimesF…

2026/10/15 23:53:25 阅读更多 →

最新新闻

从私钥到Web3入口:钱包技术演进与实现原理

从私钥到Web3入口:钱包技术演进与实现原理

1. 钱包到底是什么:先搞懂“币”其实不在钱包里很多刚接触区块链的朋友都会问我一个问题:我钱包里的币到底存在哪里?是不是像银行一样,有个中央服务器帮我记账?答案可能和直觉相反——你的币从来不在“钱包”里&#x…

2026/10/18 8:36:17 阅读更多 →
FastColoredTextBox中文修正版:彻底解决光标错位与样式渲染

FastColoredTextBox中文修正版:彻底解决光标错位与样式渲染

简介:FastColoredTextBox 是一款广受欢迎的开源高亮代码文本框控件,支持自定义显示模式,并默认内置 C#、HTML 等多种语言的高亮规则;原版在处理中文双字节字符时经常出现显示异常、光标位置偏移,多个 Style 同时使用时…

2026/10/18 8:36:17 阅读更多 →
用Flask自研研学预约系统:数据模型、支付回调与防超卖实战

用Flask自研研学预约系统:数据模型、支付回调与防超卖实战

1. 项目缘起:为什么我一头扎进了自研研学预约系统先说清楚我做的这套东西是什么:一个面向中小型营地/研学机构的在线课程预约系统,用Python的Flask框架从零搭起,涵盖了课程排期、名额管理、在线支付确认、后台订单管理这些核心环节…

2026/10/18 8:36:17 阅读更多 →
拆解开源Agent框架:插件化的核心机制、实战与避坑指南

拆解开源Agent框架:插件化的核心机制、实战与避坑指南

先把话说在前面:我拆的是最近热度很高的一个开源 Agent 框架,拆法比较粗暴,直接把源码拉下来,从入口一路跟到插件装载、工具调用、上下文组装、退出清理,一条线走完。拆完之后我对“插件化”这三个字的理解&#xff0c…

2026/10/18 8:36:17 阅读更多 →
Spring Event事件机制:从同步耦合到异步解耦的完整实践与避坑指南

Spring Event事件机制:从同步耦合到异步解耦的完整实践与避坑指南

简介:面向 Spring 开发者的 Event 事件机制示例工程,以可运行代码演示事件定义、发布、监听、顺序控制与异步处理等核心用法,适合正在系统学习 Spring 事件模型,或希望在业务模块间实现低耦合通信的读者。资源共 75 个文件&#x…

2026/10/18 8:36:17 阅读更多 →
阵列天线方向图对比:均匀阵、切比雪夫、泰勒与稀疏阵选型指南

阵列天线方向图对比:均匀阵、切比雪夫、泰勒与稀疏阵选型指南

简介:阵列天线方向图比较是阵列天线设计中的重要基础,单元个数、阵元间距与波长三者共同决定波束宽度、旁瓣电平与零点位置。这套MATLAB源码包面向无线通信、雷达与天线工程学习者,可帮助读者快速仿真不同参数组合下的方向图差异,…

2026/10/18 8:35:17 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 0:55:02 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →