最近我把某竞赛平台的天文数据挖掘比赛完整走了一遍。这个赛题的核心是给定一批天体观测数据,让模型区分三个类别:恒星、星系、类星体。听起来挺浪漫,真做起来却没那么轻松,样本量大、噪声明显、还带着明显的类别不平衡。这类赛题在真实天文数据处理里也很有代表性,不只是刷榜用的玩具,它很考验你对脏数据的容忍度、对物理特征的理解,以及对模型验证细节的把控。如果你正在准备入门数据挖掘比赛,或者想找一条“从拿到表格数据到提交结果”的完整路线,这篇复盘应该能当一份实用参考。我会沿着实际操作顺序写:赛题理解、数据清洗、特征工程、模型验证、融合提交、问题排查,尽量把每个关键步骤背后的理由也讲清楚。
这种天文数据挖掘实验之所以值得写,不只是因为分数高低,而是它几乎覆盖了表格类竞赛的全部经典环节:类别不均衡、缺失值、异常值、高相关性特征、线下线上分数偏移、模型融合。任何一个环节处理不好,排名都会差出一截。下面我开始复盘,所有经验都来自实际跑通流程后的总结,你可以直接拿去对照自己的项目。
1. 项目概述与整体思路
1.1 赛题本质:不只是“三分类”那么简单
很多新手看到这个任务,第一反应是“不就是分类嘛,三个类别,随便来个模型都能做”。但实际评估用的是 macro-F1,而不是准确率,这就让题目难度上升了一个档次。macro-F1 的意思是对每一个类别单独计算 F1 分数,再取平均。这样一来,样本量少的类星体和样本量大的恒星在最终得分里拥有同等的说话权利。
打个比方,如果班级里有 50 个学生,其中 45 个擅长数学、5 个严重偏科,老师评“综合表现”时把数学成绩和各科成绩平均拉在一起,那偏科的那 5 个学生就会拖后腿。模型也一样,哪怕恒星类识别得再准,只要类星体的精确率或召回率偏低,macro-F1 就上不去。
我在实验前先统计了训练集类别比例,大概是一半恒星、三分之一星系、剩下的才是类星体,类星体占比不到两成。这种分布直接决定了后续一切处理策略:不能盲目追求准确率,不能随便做随机划分,也不能用默认阈值一刀切。理解评价指标,永远是做竞赛的第一步。
1.2 先建立 baseline,再谈精细优化
我见过不少人拿到赛题就直奔特征工程,甚至一上来就上深度神经网络,结果花了很长时间,线上分数反而不如一个简单的 LightGBM。正确的做法是先快速建一个可以提交的 baseline,不管分数多难看,先跑通全流程。
我第一次用原始特征训练 LightGBM,五折交叉验证的 macro-F1 大约在 0.78 左右。这个分数很低,但它的价值不在分数本身,而在于给了我一个参照系。后面每做一步特征工程或调参,我都会拿它和这个 0.78 对比,知道每一步到底带来多少收益。没有 baseline,你根本不知道自己在瞎忙什么。
另一个原因是,baseline 能提前暴露数据问题。我记得第一次跑的时候就发现目标列里出现了两种形式相同的字符串,只是大小写不一致,如果不做归一会直接报错。这种问题如果等特征工程做完才发现,排查成本会高很多。
1.3 把时间预算花在刀刃上
竞赛周期长,但能集中精力做的时间其实有限,我给自己做了一个粗粒度的时间分配表,原则很朴素:先保通路,再保提分,最后保稳定性。
| 阶段 | 核心动作 | 期望产出 |
|---|---|---|
| 数据探索 | 字段理解、分布检查、缺失统计 | 明确可用特征、排除身份列 |
| 特征工程 | 构造颜色、交互、缺失标志 | 让模型看到物理含义更强的特征 |
| 建模与调参 | 树模型为主,交叉验证调参 | 稳定可靠的线下分数 |
| 模型融合 | 概率加权、Stacking | 比单模型再提 0.01~0.02 |
| 提交复盘 | 格式校验、结果归档 | 可复现的最终方案 |
这个表不是死的,后面做的时候发现特征工程收益很高,我就适当削减了调参时间。竞赛不是考试,没有固定题量,灵活调整节奏本身就是能力的一部分。
2. 数据探索与清洗实操
2.1 先搞懂每一列是什么,再动手写代码
天文赛题给的数据表一般不小,每行代表一个天体样本,列往往包含编号、不同波段星等、颜色指数、红移值、红移误差以及目标类别。第一次打开数据集时,我先做三件事:打印 shape 和数据类型,检查目标类别分布,统计每列缺失率。这三步几乎不耗时,但能避免后面的所有返工。
举个例子,有些列表面上看是数值,实际上却是占位符,比如缺失值用 -999 表示。如果不处理,模型会把 -999 当成一个有规律的特殊数值来学习,线下可能出现莫名其妙的规律,线上却不复现。因此我养成一个习惯:任何特征在进模型之前,先做一次 value_counts 或者 min/max 检查,看看有没有类似 -999、9999 这种异常数值。
目标类别部分,我直接把字符串映射成 0、1、2 三个整数。映射关系单独保存成字典,避免提交时把标签顺序搞错。这一步虽小,但提交阶段返工都是从这里埋下的雷。
2.2 编号和坐标这类“身份列”必须隔离
天文数据里一定有类似 id、ra、dec 这样的坐标字段。id 是每条样本的编号,训练它没有意义,反而会让模型学习到“某类样本集中在某个 id 区间”的假规律。RA 和 Dec 表示天体在天空中的位置,看起来很像有效特征,实际上却很危险。
为什么呢?因为巡天数据的观测覆盖并不均匀,某些天区可能观测得更多、更深,另一些天区则相对稀疏。如果直接把坐标放进模型,它很可能学到的不是天体物理规律,而是“这个天区里的目标更容易被判成星系”这种伪相关。线下验证时可能因为数据划分问题显得很准,一旦遇到分布不同的线上测试集,分数就会崩塌。
我在实验里把所有坐标类字段单独隔离,不参与建模,只在做相关性分析时用来观察样本分布。这是一个保守但安全的决定:宁可少用一个可能有用的特征,也不要让模型学到一个会炸的假规律。
2.3 缺失值处理要区分“没采到”和“物理上不存在”
天文数据的缺失非常常见,比如红移字段缺失,可能意味着目标太暗、光谱质量不够,而不是目标本身没有红移。树模型本来能直接处理缺失值,但为了让实验可控,我通常会先统一填充,并额外增加一个“是否缺失”的布尔特征。因为“缺失与否”本身可能携带观测质量的信息,把它单独作为一个特征,能让模型自己去决定这个信号有没有用。
填充策略上,我没有用 0 填充。连续特征我用中位数填充,离散特征用众数填充。中位数相比均值更抗异常值,这对天文数据尤其重要,因为少量极亮或极暗的目标会把均值拉偏。对于缺失率超过 50% 的特征,我不会急着删除,而是先观察它的分布。有些特征虽然缺失多,但缺失标志本身就有区分度,直接扔掉反而可惜。
还有一点必须强调:所有填充值都只能从训练集里计算,然后应用到训练集和测试集。如果把全量数据一起算统计量再做填充,本质上就是小小的数据泄露,会让线下分数虚高。
2.4 异常值优先裁剪,而不是直接删除
天文数据里噪声点很多,极端星等、极端红移都可能出现,我用分位数裁剪代替删除。具体做法是对每个连续特征取 1% 和 99% 分位,小于下界的压到下界,大于上界的压到上界。这样既保留了样本数量和类别分布,又限制了异常点对模型的影响。
裁剪阈值需要用字典保存下来,训练集和测试集使用同一套阈值,这一点非常关键。如果测试集单独计算分位数,就会导致特征分布不一致。我早期吃过这个亏:训练时用训练集分位数,提交前不小心在测试集上重新算了阈值,结果线下分数没变化,线上分数却掉了好几个点,后来排查了很久才发现是分布不一致导致的。
3. 特征工程:让模型看到物理差异
3.1 把原始星等转换成颜色和物理量
单看某个波段的星等,模型很难区分天体类型,但不同波段的“星等差”却能反映出天体内部的物理差异。天文上,不同波段对应不同的颜色的光,类星体和恒星在颜色空间里的位置有明显差异,所以我把 u、g、r、i、z 这几个波段做两两相减,得到 u-g、g-r、r-i、i-z 这样的颜色指数。这一步实现上就是普通列运算,但效果特别明显。
除了颜色差,我还加入了一些交互特征,比如红移乘以颜色差。因为高红移天体的颜色会受到宇宙学红移的影响,原本的观测颜色会向红色端移动,单纯的颜色差可能不够,加上红移交互后,模型更容易捕捉这种变化。另外我构造了总星等和平均星等,相当于给模型一个“这个天体整体亮度”的全局视角。
做特征工程时要控制总特征数量,不能无限制堆。我最终保留的特征在 20 到 30 个之间,删掉了一些重要性极低的原始列。对天文赛题来说,物理含义清晰的小特征集,远好过堆了几百列却解释不了的“特征垃圾场”。
3.2 连续值的分布需要仔细观察
星等在物理上是通量的对数尺度,这意味着星等的分布本身就带有压缩效果。如果某些特征动态范围特别大,我可以对它们做指数变换,恢复到通量尺度再和别的特征做比。但在这次实验里,颜色指数已经相当于两个通量比的 log,所以直接用就好,不需要过度变换。
我测试过对特征做 PCA,线性和树模型效果差别很大。PCA 对线性模型有帮助,但对树模型几乎没提升,因为树模型本身就能处理特征间的非线性关系,强行降维反而会丢失物理含义。如果你后续想用神经网络或者逻辑回归,再做标准化和 PCA;如果主力是 GBDT 系模型,保留原始单位和物理构造特征更重要。
当然,如果某些特征跨度特别大,比如红移从接近 0 到接近 5,我建议做一次 log1p 变换压缩动态范围。这不会让树模型效果变差,但对融合时的线性模型会友好很多。我实际试下来,log1p 红移特征让最后的融合分数略有提升。
3.3 用特征重要性反馈来精简特征集
特征不是越多越好,天文表格数据往往特征相关性很高,加多了只会增加过拟合风险。我跑完一版模型后会输出 LightGBM 的 feature_importance,重点看 gain 而不是 split 次数。原因很简单,某个特征可能被频繁使用但每次都只是浅浅地分割,它对最终结果的贡献未必高;gain 更能反映特征带来的信息量。
对于重要性偏低的特征,我不会直接全删,而是先看它和已有特征的相关性。如果相关性很高,就删掉;如果相关性低但重要性也低,则可能是模型没充分利用,可以先保留一段时间。特征工程应该是迭代的,不是一锤子买卖。
4. 模型构建、验证与调参
4.1 分层五折交叉验证是线下评估的基本盘
评估模型时我没有用简单的训练测试两段式,而是采用 StratifiedKFold 分层五折交叉验证。之所以强调“分层”,是因为目标类别不平衡,如果随机划分,某一折里可能类星体样本特别少,分数波动会很大,得到的结论也不可靠。
分层五折的思路是确保每一折训练集和验证集的类别比例都与全局相近。这样算出的平均分方差小、参考价值高。我在代码里固定了随机种子:random_state=42,保证每次跑出来的结果可复现。后续每次调参都基于同一套折,方便横向对比。
代码示意:
from sklearn.model_selection import StratifiedKFold from sklearn.metrics import f1_score skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) for fold, (tr_idx, va_idx) in enumerate(skf.split(X, y)): X_tr, X_va = X.iloc[tr_idx], X.iloc[va_idx] y_tr, y_va = y.iloc[tr_idx], y.iloc[va_idx] # 训练模型,计算 macro-F1这种验证方式有一个附带好处:可以快速观察五折分数是否都在一个合理区间。如果某一折明显偏低,说明数据里可能存在按天区或观测批次分布的不均匀性,这时候我会考虑改用 GroupKFold。
4.2 树模型为什么是主力,而不是深度学习
对于这种中小规模的表格型天文数据,我首选 LightGBM、XGBoost 和 CatBoost 的组合。LightGBM 训练快、内存占用低,XGBoost 稳定性好,CatBoost 对类别特征有天然支持。我用 LightGBM 作为主力模型,其他两个做差异化补充,最后再融合。
LightGBM 初始参数很关键,不能全部用默认值。我通常先设置一个偏保守的配置,再通过早停确定树数量:
import lightgbm as lgb params = { 'objective': 'multiclass', 'num_class': 3, 'metric': 'multi_logloss', 'learning_rate': 0.05, 'num_leaves': 63, 'max_depth': 7, 'feature_fraction': 0.8, 'bagging_fraction': 0.8, 'bagging_freq': 1, 'lambda_l1': 0.1, 'lambda_l2': 1.0, 'verbose': -1, }多分类任务里num_class必须与目标类别数一致,否则训练直接报错。metric中的 logloss 只是训练参考,最终排名看的还是 macro-F1,所以我还会在回调里加自定义评估函数,实时观察 F1 变化。
早停轮数我设置为 100,一般跑到 500 轮左右就稳定了。如果提前停止轮数太小,模型可能没收敛;太大则浪费时间。100 是一个工程上比较稳的折中值。
4.3 调参顺序:先结构参数,后正则参数
调参最忌讳上来就网格搜索几十组参数,那是浪费算力。我的顺序是:先确定树数量并调整learning_rate;再调num_leaves、min_data_in_leaf这类结构参数;最后调lambda_l1、lambda_l2做正则化。
| 参数范围 | 调整目标 | 容易出现的问题 |
|---|---|---|
| learning_rate 0.01~0.1 | 控制收敛速度 | 太大容易过拟合,太小训练慢 |
| num_leaves 31~127 | 控制模型复杂度 | 太大过拟合,太小欠拟合 |
| min_data_in_leaf 20~100 | 防止单叶样本过少 | 太小噪声大 |
| feature_fraction 0.6~0.9 | 控制特征采样 | 太小欠拟合 |
| lambda_l1 / lambda_l2 | 正则化 | 通常不会剧烈影响分数 |
实际上,我发现最大收益来自特征工程,调参只能带来小幅但稳定的提升。所以不要把调参当做救命稻草,它更像最后的打磨。
5. 模型融合与提交阶段的关键细节
5.1 概率平均融合:简单但有效
单模型跑到一定阶段后,我用 LightGBM、XGBoost、CatBoost 做概率融合。方法是对三个模型预测出的各类别概率做加权平均,然后取概率最大的类别作为最终结果。
权重不是随意定的。我在验证集上做了简单的网格搜索,遍历不同权重组合,找 macro-F1 最高的一组。实际结果大约在 LightGBM 0.5、XGBoost 0.3、CatBoost 0.2 时效果最好。代码示意:
import numpy as np best_weight = None best_score = 0 for w1 in np.arange(0.4, 0.9, 0.1): for w2 in np.arange(0.1, 0.4, 0.1): w3 = 1 - w1 - w2 if w3 <= 0: continue pred = w1 * p_lgb + w2 * p_xgb + w3 * p_cat score = f1_score(y_valid, pred.argmax(axis=1), average='macro') if score > best_score: best_score = score best_weight = (w1, w2, w3)融合能带来的提升通常是 0.01 到 0.02 个 macro-F1 点,听起来不多,但在竞赛里足以让名次前进不少。不过有一个前提,三个模型之间要有差异性。如果都是同一个模型换几个参数,融合基本没有意义。
5.2 可选 Stacking 与阈值调整
除概率平均外,Stacking 也值得尝试。做法是把第一层模型的预测概率作为第二层模型的输入特征,第二层用逻辑回归或简单的神经网络。但要注意,第二层不要再用原始特征,否则很容易过拟合。Stacking 在样本量充足、模型差异大的时候收益更明显。
比赛如果以 F1 作为评价指标,概率校准不是最优先的事,阈值调整更直接。某些类别概率阈值默认是 0.5,但可以针对每类单独调整。比如类星体类,如果阈值降到 0.3,召回率可能上升,精确率可能下降,需要看组合结果。不过验证集太小时,强行调阈值容易过拟合,要克制。
5.3 提交格式与版本管理的坑
提交阶段最容易出低级错误。平台要求 CSV 包含 id 和 label 两列,label 是预测类别,我提交前一定会检查三件事:行数是否等于测试集行数;label 是否包含全部三个类别;id 列是否与测试集一致。
我习惯先把提交模板做好,每次只填充预测结果,定义一个简单的断言脚本检查行数和类别。不要小看这一步,线下跑出高分但提交失败的人,每次比赛都有。模型文件也要做好版本管理,我建议用“日期+参数备注”的方式命名,记录每版模型的线下分数和线上分数,否则过几天你就会忘记之前做过什么改动。
6. 常见问题与排查技巧实录
6.1 线上分数和线下分数差很多,怎么排查?
先看特征重要性,如果某个特征重要性高得不正常,优先怀疑是否用了测试集才有的信息。再看数据预处理,所有统计量比如均值、中位数、分位数裁剪阈值是不是都只在训练集上计算过。如果这两点都没问题,就要想测试集分布是否和训练集差异很大。
天文数据中,训练集和测试集可能来自不同观测区域,这会直接导致分布偏移。解决办法是用坐标字段构造分组,用 GroupKFold 做验证,模拟这种区域差异。这样做出来的线下分数会更接近线上真实情况。
6.2 类别不平衡该不该过采样?
我建议先不要急着做 SMOTE。对树模型而言,简单过采样经常适得其反,模型会对少数类样本过度学习,线上遇到新数据反而泛化不好。我更喜欢用类别权重或直接调整阈值。
在这个实验里,类星体占比不足两成,但 LightGBM 的表现并没有明显偏向多数类,所以我没有用过采样。如果某个类别占比极低,比如只有 5%,可以优先尝试提高该类的损失权重,再决定要不要做生成式过采样。不要一开始就背大锅。
6.3 模型训练太慢、内存爆炸怎么办?
样本量大的时候,LightGBM 的内存和训练速度可以通过几个参数快速优化:max_bin从默认 255 降到 63,显著减少内存;feature_fraction和bagging_fraction适度调小,训练更快。还有一个经验:先用全部数据的 10% 跑通完整流程,确认无误后再全量训练,能节省大量排队时间。
读取超大 CSV 时,用pandas.read_csv的usecols参数只加载需要的列,不要一次把全表塞进内存。遇到实在没法全量训练的极限情况,可以考虑分批训练再做模型平均,但这是最后手段,通常优化后已经够用。
6.4 预测概率全是 0.33,模型等于没学
这个问题我在调试早期遇到过。概率均匀分布通常意味着模型没有从特征里学到判别信息,常见原因有三个:标签没有正确编码、特征全是常数、数据预处理把不同类别的样本错误地混在一起。
排查方法很简单:按类别统计每个特征的均值和标准差,如果差异几乎为零,说明特征本身没有区分度,或者列读取错了。再检查目标编码过程,确保 shuffle 时没有把标签和特征张冠李戴。
我自己复盘下来,收益最大的动作依次是:仔细理解每个字段的物理意义、构造颜色和红移交互特征、完成多模型融合。这三步加起来,把 macro-F1 从 baseline 的 0.78 拉到了 0.87 左右。最后说一个处理天文数据时最有感触的点:不要相信第一次读到的数据表,一定要先问“每一行到底是什么”“每一列代表什么物理量”。很多看起来像玄学的问题,最后都能追溯到最开始没有认真理解字段含义上。把这个基本功做扎实,后面的建模才谈得上效率。