简介:这是一份天池“津南数字制造算法挑战赛”赛场一初赛第十七名队伍的完整代码包,适合正在备战数据挖掘竞赛、希望提升工业制造场景建模能力的算法学习者和从业者。压缩包共十三个文件,以七个Notebook为主,覆盖数据清洗、特征工程、模型选择与调优等核心环节;辅以两个Python脚本和一个CSV数据文件,便于直接运行和复现;另有三个Markdown文档记录方案思路与迭代过程。整套代码约2.98MB,结构精简,适合快速拆解一支Top20队伍的实战策略。目前已有140人学习下载。通过这份资源,读者可以观察到真实竞赛中的数据处理技巧、特征构造思路,以及集成模型在制造数据上的调参与验证方法,还可借鉴其项目组织与代码注释规范,对完整走通一次数据竞赛流程很有帮助。
1. 赛场一和 17 名代码包:一份 17 名 zip 到底值不值得细看
初赛第 17 名这个名次很微妙。它不在领奖台上,但足够说明这套代码在成百上千支队伍里跑通了完整链路:数据清洗、特征工程、模型训练、验证、推理提交,每一环都经历过高分与低分的反复试错。对后来者来说,这比看冠军方案更接近真实比赛状态——冠军代码往往做了大量针对性调参和资源堆叠,而 17 名代码通常保留了朴素的骨架和可迁移的工程习惯。天池的津南数字制造算法挑战赛【赛场一】本质上是制造业场景下的数据挖掘问题,赛题数据多与生产参数、质量检测指标相关,赛场上拼的是对业务字段的理解深度和特征构造的细腻程度,这也正是这份 zip 值得拆开看的原因:你能从中找到一套可以直接套用的比赛代码组织方式,而不用从零开始搭项目结构。
如果你现在正要参加下一场天池或其他平台的算法赛,或者想把比赛代码改造成工业项目的基线系统,这份代码包能给你三样东西:一份可复现的本地实验目录、一组经过验证的特征处理方法、一个不会在提交前夜崩掉的模型集成套路。下面我带你按自己的工程习惯把这个 zip 里的思路完整过一遍,你可以照着搭自己的版本。
2. 解包与工程还原:先把 zip 变成能跑的本地项目
拿到津南数字制造算法挑战赛【赛场一】初赛 17 名 Drop 队代码.zip,第一件事不是看模型,而是把压缩包还原成可复现的工程目录。比赛代码的压缩包通常没有统一的目录规范,有的带requirements.txt,有的直接把数据文件和代码混在一起,有的缺 README——这都不是问题,关键是你在 10 分钟内建立起自己对这份代码的「地图」,知道哪个文件是入口、哪个是特征脚本、哪个是模型训练主程序。
2.1 用命令行快速解包并核查文件完整性
先别双击解压,用终端操作更可控。Linux 和 macOS 下常见做法是:
# 创建独立目录,避免解压时文件散落当前文件夹 mkdir -p tianchi_jiangnan && cd tianchi_jiangnan # 解压 zip,-d 指定目标目录 unzip ../津南数字制造算法挑战赛【赛场一】初赛\ 17\ 名\ Drop\ 队代码.zip -d ./drop17 # 查看解压后的顶层结构,只显示两层 find drop17 -maxdepth 2 -type f | head -50 # 统计文件数量与总体积,判断是否包含数据文件 du -sh drop17 find drop17 -type f | wc -l参数说明:-d指定解压目标目录,避免文件直接倾倒进当前目录造成混乱;find ... -maxdepth 2只列出两层以内的文件,如果代码里有嵌套的数据文件夹,这个命令会直接暴露出来;du -sh看总大小,如果体积超过 1GB,基本可以判断 zip 里带着原始数据或中间特征缓存。Windows 用户也可以用tar -xf命令直接解压 zip,Git Bash 和 PowerShell 7 都内置了这个支持。解压后检查一下有没有隐藏的.ipynb_checkpoints或__pycache__目录,这些通常是迭代过程中的残留,不是代码主体,读代码时可以直接跳过。
2.2 建立代码地图:入口文件、特征文件、模型文件三级索引
解包完成后,不要立刻打开每个文件通读,而是先建立一份索引。我一般会执行一条 find 命令,把所有.py文件按修改时间排序,再看看有没有 README 或.ipynb文件:
# 按修改时间倒序排列所有 Python 文件 find drop17 -name "*.py" -exec ls -lt {} + | head -30 # 查找是否有 notebook 和说明文档 find drop17 -name "*.ipynb" -o -name "README*" -o -name "*.md" | head -20 # 用 grep 快速定位 train 函数 / model 定义 / 特征处理关键字 grep -rn "def train\|lgb\.train\|xgb\.train\|train_test_split" drop17 --include="*.py" | head -20grep命令在定位代码入口时非常高效。def train能帮你找到自定义的训练函数,lgb.train能确认模型库是 LightGBM 还是 XGBoost,train_test_split能告诉你验证集是随机切分还是按时间切分。天池比赛里,时间序列类问题通常用TimeSeriesSplit或按时间点切分,如果这里出现train_test_split且shuffle=True,说明队伍用的是随机验证策略——这个细节决定了你复现时能不能得到和原来一样的分数,值得先记下来。
# 这段代码用于快速扫描代码包里的依赖库,判断环境要求 import ast from pathlib import Path from collections import Counter deps = Counter() for py in Path("drop17").rglob("*.py"): try: tree = ast.parse(py.read_text(encoding="utf-8")) except Exception: continue for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: deps[alias.name.split(".")[0]] += 1 elif isinstance(node, ast.ImportFrom): deps[node.module.split(".")[0] if node.module else ""] += 1 for lib, cnt in deps.most_common(20): print(f"{lib:20s} {cnt:3d}")这段 Python 扫描脚本会在不执行任何代码的前提下,把项目里所有 import 语句统计出来,让你在装环境之前就知道需要哪些依赖。ast.parse只做语法解析不执行代码,所以即使代码里有文件读写或网络请求也不会触发。node.module.split(".")[0]是为了把from sklearn.model_selection import KFold这种深引用简化成顶层包名sklearn。这个脚本对任何比赛代码包都适用,建议存成scan_deps.py放在工具目录里。扫描结果里如果同时出现lightgbm、xgboost、catboost,说明这支队伍做了多模型融合;如果只有一个,说明是单模型路线。
2.3 运行前先复现离线验证,拒绝盲跑全量训练
直接跑完整训练脚本是比赛代码复现里最容易踩的坑。天池这种比赛的数据量通常在几万到几十万行,完整训练可能要跑几个小时,如果代码里有隐藏 bug 或者路径依赖,等你发现时已经浪费了大量时间。我的做法是构造一个最小子集做冒烟测试。
# smoke_test.py —— 用 2% 数据验证代码链路是否通畅 import pandas as pd from pathlib import Path # 假设原代码的数据路径是 ./data/train.csv df = pd.read_csv("drop17/data/train.csv", nrows=2000) # 保留原特征列结构,把标签列也带上 target_col = "label" # 按实际代码包里的列名调整 assert target_col in df.columns, f"数据中找不到标签列: {df.columns[:10]}" # 关键:保存一个 mini 版本,不修改原代码的数据加载逻辑 mini_dir = Path("drop17/data_mini") mini_dir.mkdir(exist_ok=True) df.to_csv(mini_dir / "train.csv", index=False) valid = pd.read_csv("drop17/data/train.csv", nrows=500) valid.to_csv(mini_dir / "valid.csv", index=False) print(f"mini train: {df.shape}, mini valid: {valid.shape}")把这段脚本跑完后,手动修改主训练脚本里的数据路径指向data_mini目录,然后运行主程序。注意nrows=2000只取前 2000 行,如果原数据不是随机打乱的,这个子集的分布会与全量数据有偏差,所以冒烟测试的目的只是验证 IO、特征处理、模型训练、预测输出这一整条链路能跑通,分数没有参考意义。冒烟测试通过之后,把所有路径改回原值,再用nohup或tmux在后台跑全量训练,同时定期看一眼训练日志的 loss 或log_loss是否在正常下降。
3. 赛场一的代码骨架拆解:特征、模型与验证三件套
天池津南数字制造算法挑战赛【赛场一】的赛题设置通常是回归或二分类问题,数据特征多为设备参数、工序指标和质检结果。17 名队伍的代码骨架一定包含三个核心模块:特征工程、模型训练、交叉验证。这三块是比赛代码的通用主干,不管具体赛题是什么,拆开看都有大量可复用套路。
3.1 特征工程的常见做法:统计量、离散化与业务比率
特征工程是数字制造赛题拉开差距的关键环节。设备采集的数据往往包含大量连续型传感器值和离散型工序编号,常见做法是围绕这两类数据分别构造特征。下面是一段典型的特征构造代码,代表了这类赛题里最通用的特征组合方式:
# feat_engineering.py —— 基于原始字段构造统计特征 import pandas as pd import numpy as np def build_features(df: pd.DataFrame) -> pd.DataFrame: """从原始数据中构造特征,保持 DataFrame 列名可追溯""" feat = df.copy() # 数值列统一做缺失值填充,-999 是比赛代码里常见的分箱标记 num_cols = feat.select_dtypes(include=[np.number]).columns feat[num_cols] = feat[num_cols].fillna(-999) # 连续值离散化:把传感器读数切成 5 档,捕捉非线性关系 if "sensor_value" in feat.columns: feat["sensor_bin"] = pd.cut( feat["sensor_value"], bins=5, labels=[0, 1, 2, 3, 4] ).astype(int) # 类别列频次编码:替代 label encoding,增强泛化性 for col in feat.select_dtypes(include=["object"]).columns: freq_map = feat[col].value_counts().to_dict() feat[f"{col}_freq"] = feat[col].map(freq_map) # 业务比率特征:两道工序的测量值之比往往比绝对值更有区分度 if {"process_a", "process_b"}.issubset(feat.columns): feat["ratio_ab"] = feat["process_a"] / ( feat["process_b"] + 1e-6 # 防止除零 ) return feat这段代码有三个设计要点。fillna(-999)把缺失值替换成极值,比中位数填充更符合树模型的分裂逻辑——决策树可以学出「缺失值单独走一条分支」的模式;pd.cut离散化的bins=5参数需要根据数据分布调整,如果传感器值集中在很窄的区间,5 档会分布不均,可以改成bins=np.percentile分位数切分;频次编码对高基数类别特征特别有效,比如设备编号或操作员 ID,如果用 one-hot 会把特征矩阵撑爆,而频次编码把类别映射成出现次数,保留了「稀有类别」的信息。比率特征ratio_ab是制造场景里最实用的组合特征,比如温度与转速的比值可能直接关联到良率,这类特征树的首次分裂就会用到,信息增益通常不低。
特征构造完成后,需要做一个耗时较长的离线评估:分别跑「只有原始特征」和「原始特征+构造特征」两组实验,对比验证集分数。如果构造特征没有提升超过 0.001(具体看赛题指标),说明这批特征无效或过拟合,应该果断删掉,不要恋战。
3.2 模型部分:LightGBM 为主力,交叉验证控制过拟合
数字制造赛题的主流模型配置是 LightGBM 配合 K 折交叉验证。17 名代码里大概率也是这个套路,因为树模型对数值特征的尺度不敏感,不需要做标准化,而且 LightGBM 在中等规模数据上的训练速度远快于深度学习模型,方便快速迭代特征方案。下面是核心训练代码的通用形态:
# train_model.py —— K折 LightGBM 训练与预测 import lightgbm as lgb import numpy as np import pandas as pd from sklearn.model_selection import KFold from sklearn.metrics import mean_squared_error, log_loss def train_lgb_kfold(X, y, params, n_splits=5, seed=42): """ K折 LightGBM 训练,返回 OOF 预测和测试集预测均值 X: 特征矩阵; y: 标签; params: LightGBM 参数 dict """ kf = KFold(n_splits=n_splits, shuffle=True, random_state=seed) oof_pred = np.zeros(len(X)) test_pred = np.zeros((len(test_df), n_splits)) for fold, (tr_idx, va_idx) in enumerate(kf.split(X)): X_tr, X_va = X.iloc[tr_idx], X.iloc[va_idx] y_tr, y_va = y.iloc[tr_idx], y.iloc[va_idx] dtr = lgb.Dataset(X_tr, label=y_tr) dva = lgb.Dataset(X_va, label=y_va) model = lgb.train( params, dtr, num_boost_round=2000, valid_sets=[dva], callbacks=[ lgb.early_stopping(100), lgb.log_evaluation(100) ] ) oof_pred[va_idx] = model.predict(X_va, num_iteration=model.best_iteration) test_pred[:, fold] = model.predict(test_df, num_iteration=model.best_iteration) # 根据赛题指标选择评估函数,回归用 RMSE,分类用 log_loss if params.get("objective") == "regression": score = mean_squared_error(y, oof_pred, squared=False) else: # 二分类一般直接输出原始概率 score = log_loss(y, oof_pred) print(f"K-Fold CV score: {score:.6f}") return oof_pred, test_pred.mean(axis=1), modelKFold里两个关键参数是shuffle=True和random_state=42。shuffle=True会在切分前打乱数据,避免原始数据按生产时间排列导致的分布偏移;random_state=42固定随机种子,确保每次运行得到相同的划分,这对复现比赛分数至关重要。early_stopping(100)的意思是验证集指标连续 100 轮不提升就停止训练,model.best_iteration取最优轮次做预测,避免过拟合。test_pred.mean(axis=1)对 5 折的测试集预测取平均,这就是常见的「OOF 融合」——用不同折训练出的模型投票,能有效降低单模型的方差。
LightGBM 的params有几组常用配置值得记在笔记本里:
params_fast = { "objective": "regression", "metric": "rmse", "learning_rate": 0.05, "num_leaves": 31, "max_depth": -1, "feature_fraction": 0.8, "bagging_fraction": 0.8, "bagging_freq": 5, "verbose": -1 } params_conservative = { "objective": "binary", "metric": "binary_logloss", "learning_rate": 0.01, "num_leaves": 15, "max_depth": 6, "min_data_in_leaf": 100, "feature_fraction": 0.6, "bagging_fraction": 0.7, "bagging_freq": 1, "lambda_l2": 1.0, "verbose": -1 }params_fast适合特征初筛阶段,learning_rate=0.05跑得快,特征是否有效一眼就能看出来;params_conservative适合最终提交前,learning_rate=0.01配合更高的min_data_in_leaf和lambda_l2正则,模型会更稳,不容易在测试集上出现极端预测。num_leaves是 LightGBM 里最需要调的参数,31 是默认值,数据量小或噪声大时降到 15 以下可以大幅减少过拟合,但代价是可能欠拟合,需要通过 CV 分数判断。
3.3 验证策略的选择:随机 K 折还是按时间切分
比赛代码里验证集的设计直接决定你能不能在本地复现排名。数字制造赛题的数据通常包含时间戳字段,这时候有两种选择。如果赛题明确说测试集是未来一段时间的数据,那验证集也应该用时间切分,模拟「用过去预测未来」的场景。如果赛题没有时间概念,数据是随机抽样的,那随机 K 折就是正确的。判断方法很简单:看代码包的 EDA 部分有没有画时间序列图,或者看特征里有没有time、date、timestamp列。
# 按时间切分的验证代码,适用于时间敏感的制造数据 df = df.sort_values("timestamp").reset_index(drop=True) train_size = int(len(df) * 0.8) # 直接按行位置切分,前 80% 作训练,后 20% 作验证 tr = df.iloc[:train_size] va = df.iloc[train_size:] # 检查切分后的标签分布,防止某一时间段数据分布剧烈波动 print(tr["label"].describe()) print(va["label"].describe())按时间切分时,最怕的是标签在时间维度上有缓慢漂移,比如前 80% 的合格率是 95%,后 20% 变成了 90%。这时模型学到的规律在验证集上必然表现不佳,但这恰恰反映了真实预测时的困难——如果赛题的测评指标就是针对未来数据,那么「验证集分数难看」不一定是坏事,说明你没有数据泄露。反之,如果随机 K 折拿到的分数很高但提交后掉了名次,大概率是随机切分时验证集和训练集共享了某些时间相关的隐藏信息。
4. 参数调优与后处理:把 17 名的代码改造成自己的稳定模板
看了别人的代码,最终要落到自己能用。17 名队伍的代码是一个很好的起点,但要在下一场比赛里用出效果,需要对参数调优和后处理做系统性改造。这两个模块是「复制代码」和「吸收思路」的分水岭。
4.1 用 Optuna 做贝叶斯搜索,替代手动调参
手动调参通常的做法是每改一次参数跑一轮完整 K 折,一个调参周期下来耗时 3 到 4 小时,而且容易陷入局部最优。用 Optuna 做贝叶斯搜索是当前比赛圈更高效的方案,它能在 50 到 100 次试验里找到比手动调参更好的参数组合。
# tune_params.py —— Optuna 搜索 LightGBM 超参数 import optuna import lightgbm as lgb from sklearn.model_selection import KFold from sklearn.metrics import mean_squared_error import numpy as np def objective(trial): params = { "objective": "regression", "metric": "rmse", "learning_rate": trial.suggest_float("learning_rate", 0.005, 0.05, log=True), "num_leaves": trial.suggest_int("num_leaves", 8, 64), "max_depth": trial.suggest_int("max_depth", 3, 10), "min_data_in_leaf": trial.suggest_int("min_data_in_leaf", 20, 300), "feature_fraction": trial.suggest_float("feature_fraction", 0.4, 0.9), "bagging_fraction": trial.suggest_float("bagging_fraction", 0.5, 0.9), "lambda_l2": trial.suggest_float("lambda_l2", 1e-3, 10.0, log=True), "verbose": -1 } kf = KFold(n_splits=3, shuffle=True, random_state=42) scores = [] for tr_idx, va_idx in kf.split(X): X_tr, X_va = X.iloc[tr_idx], X.iloc[va_idx] y_tr, y_va = y.iloc[tr_idx], y.iloc[va_idx] dtr = lgb.Dataset(X_tr, label=y_tr) dva = lgb.Dataset(X_va, label=y_va) model = lgb.train( params, dtr, num_boost_round=1000, valid_sets=[dva], callbacks=[lgb.early_stopping(50), lgb.log_evaluation(0)] ) pred = model.predict(X_va, num_iteration=model.best_iteration) scores.append(mean_squared_error(y_va, pred, squared=False)) return float(np.mean(scores)) study = optuna.create_study(direction="minimize") study.optimize(objective, n_trials=50, show_progress_bar=True) print("Best params:", study.best_params) print("Best RMSE:", study.best_value)这段代码把调参搜索空间压缩到了 7 个关键超参数。suggest_float的log=True选项适用于学习率和正则化系数这类跨越数量级的参数,比如0.005到0.05这个区间,线性采样会让大部分试验集中在数值较大的区域,对数采样则让整个区间内的采样密度更均匀。n_trials=50是性价比很高的设置,50 次试验大约需要 40 分钟到 1 小时(视数据量而定),已经能逼近人工调参几天的效果。metrics选用 RMSE 与赛题一致,验证指标就是优化目标,不会出现「本地分数高但提交分数低」的错位。
调参得到的best_params不要直接用于最终模型,建议对learning_rate减半、num_boost_round翻倍重训一次。这是因为 Optuna 在低轮数下找到的参数组合可能在长训练下不是最优,小学习率加上充分训练往往能比原始参数好 0.001 到 0.0005 的 CV 分数,在排名上可能跨越 3 到 5 个名次。
4.2 后处理:回归赛题的截断与分类赛题的概率校准
赛后处理是很多新手忽略的环节。以回归赛题为例,模型输出的预测值有时会出现明显不合理的极端值,这时候可以做截断处理。如果赛题的评价指标是 RMSE(均方根误差),预测值比真实值极端得多时,RMSE 会被平方放大,截断能保护分数。
# post_process.py —— 回归预测值的分位数截断 import numpy as np import pandas as pd def clip_predictions(pred: np.ndarray, bounds: tuple = None) -> np.ndarray: """ 对预测值做分位数截断,消除极端异常值 bounds: (lower_quantile, upper_quantile),如 (0.01, 0.99) """ if bounds is None: # 用训练集标签的 1% 和 99% 分位数作为截断边界 lower = np.percentile(y_train, bounds[0] * 100) if bounds else np.percentile(y_train, 1) upper = np.percentile(y_train, bounds[1] * 100) if bounds else np.percentile(y_train, 99) else: lower = np.percentile(y_train, bounds[0] * 100) upper = np.percentile(y_train, bounds[1] * 100) return np.clip(pred, lower, upper) # 返回截断前后的 RMSE 对比,确认后处理是否有效 from sklearn.metrics import mean_squared_error pred_raw = model.predict(X_valid) pred_clipped = clip_predictions(pred_raw, bounds=(0.005, 0.995)) print(f"Raw RMSE: {mean_squared_error(y_valid, pred_raw, squared=False):.6f}") print(f"Clipped RMSE: {mean_squared_error(y_valid, pred_clipped, squared=False):.6f}")np.percentile(y_train, 1)计算训练集标签的 1% 分位数,np.clip把预测值限制在这个范围内。注意截断边界不能直接观测验证集标签来定,否则会有数据泄露;正确的做法是只用训练集标签的分布来设定边界,验证集只做评估。如果截断后验证集 RMSE 没有下降,说明模型预测的极端值本身是合理的,截断无效,直接放弃后处理。
如果赛题是二分类,后处理的重点是概率校准。常见做法是对 OOF 预测的概率做 Platt Scaling 或 Isotonic Regression,但考虑到制造赛题通常只看 AUC 或 log loss,概率校准对 AUC 没有影响,对 log loss 有微弱影响。我的经验是:提交前先确认赛题指标,如果只看 AUC,概率校准这一步可以跳过,省下时间做特征迭代。
4.3 特征重要性筛选:放心删掉排名靠后的特征
17 名代码里可能保留了大量特征,但这不等于每个特征都有用。训练完成后输出特征重要性,把排名后 30% 的特征删掉重新训练一次,通常 CV 分数不会明显下降,但训练时间会缩短、模型鲁棒性会提升。这个操作在比赛后期尤其有效——特征越多,模型越容易学到噪声。
# feature_select.py —— 基于 gain 重要性筛选特征 import pandas as pd # model 是训练好的 LightGBM 模型 importance_df = pd.DataFrame({ "feature": model.feature_name(), "gain": model.feature_importance(importance_type="gain"), "split": model.feature_importance(importance_type="split"), }).sort_values("gain", ascending=False) print(importance_df.head(20)) # 保留累计 gain 占比前 95% 的特征,删掉其余 importance_df["cum_gain"] = importance_df["gain"].cumsum() / importance_df["gain"].sum() keep_cols = importance_df.loc[importance_df["cum_gain"] <= 0.95, "feature"].tolist() # 至少要保留 10 个特征,防止过滤太狠 keep_cols = list(set(keep_cols)) if len(keep_cols) >= 10 else importance_df["feature"].tolist() print(f"保留特征数: {len(keep_cols)},删除: {len(model.feature_name()) - len(keep_cols)} 个")importance_type="gain"计算的是每个特征在分裂时带来的平均信息增益,比split(分裂次数)更能反映特征的真实贡献。cumsum() / sum()得到累计贡献率,保留到 95% 是一个相对保守的截断点,保证删除的都是无关特征。这个操作在下一次特征迭代时可以直接复用:新构造的特征如果排在重要性倒数 10% 以内,下次迭代就不再浪费时间重复构造同类型特征。
5. 用增量验证法判断代码改动的有效性
拿到一份比赛代码之后,最大的风险不是代码看不懂,而是你改了之后不知道是变好了还是变坏了。直接提交测试显然不现实,天池的比赛每天提交次数有限;单看 CV 分数又可能被随机种子和 K 折划分的波动迷惑。这里有一个可以套用的判断框架:每次只改一个变量,用同一个固定种子跑完整流程,对比新老版本的 OOF 预测差异。
具体做法分三步。第一步,用原始代码跑一遍完整 K 折,记录每折的验证分数和 OOF 预测向量,保存为oof_original.npy。第二步,修改你想尝试的代码,比如加一个新特征、改一个参数、换一种缺失值填充方式,用相同的random_state=42重新跑一遍,同样保存oof_modified.npy。第三步,对比两个版本的 CV 分数差异,同时计算两版 OOF 预测的相关系数。如果相关系数接近 0.99 而 CV 分数只提升了 0.0005,说明改动并没有带来本质变化,这个提升可能是噪声;如果相关系数在 0.95 左右且 CV 分数提升了 0.002 以上,说明改动真实地改变了模型的决策边界,值得提交测试一次。
这个方法的数学依据是 OOF 预测直接反映了模型对每个样本的判断,两个版本预测的差异越大,说明改动对模型行为的影响越显著。把评估维度从「只看分数」扩展到「分数+预测相关性」,能有效过滤掉大量由于随机波动造成的伪提升。如果你在复现过程中发现修改后的代码在本地 CV 上提升了,但提交后分数反而下降,优先检查是不是验证集切分方式与赛题评测方式不一致,其次检查后处理步骤有没有用验证集的信息去拟合。
最后给你一个实操层面的提示:所有比赛代码的 zip 都应该建立「两个版本」管理习惯——第一个版本是原封不动的复现版,第二个版本是你自己迭代后的改进版。这样每次实验都能准确反推,方便在后续比赛中快速定位哪个模块值得继续深挖。
本文还有配套的精品资源,点击获取