
去年做了一期很有意思的实验拿到一批大规模天文表格数据几十万行的样本每一行是一个天体对象主要特征是各个波段的星等、红移、形状参数任务是判断这个天体到底是恒星、星系还是类星体。这就是天文数据挖掘实验也是我机器学习实战系列的第五篇。这个方向听起来很“硬核”但上手之后你会发现它跟普通商业分类问题在流程上没本质区别真正麻烦的是数据本身的物理特性和一堆隐藏得很深的坑。所以这篇不只讲模型调参更想把天文数据在挖掘过程中最容易踩的几个雷区以及我最后是怎么排掉它们的一次性说清楚。想接触数据竞赛、或者对特征工程和类别不平衡处理有好奇心的朋友这篇应该能给你不少可复现的思路。1. 赛题拆解天文数据挖掘到底在预测什么1.1 先从物理背景说起为什么需要机器学习天文巡天望远镜每天产出的数据是海量的天体影像和对应的测光参数不断涌入数据库如果靠天文学家人工去识别每个天体的类别工作量完全不可接受。于是一个很自然的做法出现了把已经人工确认过类别的天体样本作为训练集让模型自动总结“恒星长什么样、星系长什么样、类星体长什么样”再去给未标注的天体做分类。这个流程本质上是监督学习。物理上恒星是银河系内部的点源星系是银河系之外的扩展源类星体则是极远距离上非常明亮的活动星系核从图像上看也接近点源。仅仅靠肉眼去看一小块图像这三个类别并不好区分但它们的测光颜色特征、红移分布、形态参数往往有明显差异。机器学习的任务就是把这些差异从特征空间里找出来。我做的这场实验中官方给的数据就是一张大宽表每行代表一个天体包含了几个常用波段的星等值、红移估计、若干形态拟合参数以及标签列。看起来平淡无奇和常见的信用评分表格没什么两样但这正是迷惑性的开始天文数据列名的物理含义决定了你不能把它当成普通数值随意处理。1.2 任务定义与评价指标赛题是典型的三分类样本标签共有恒星、星系、类星体三类。训练集大概有十几万条测试集是未知标签的结果需要提交每个样本的概率或类别判断。这种任务如果只看准确率很容易被带偏——比如三类样本本来就不平衡恒星占了七成一个“全预测成恒星”的模型就能拿到不错的表现分但没有任何实际意义。这类竞赛通常用两类指标一个是宏平均F1一个是AUC。宏平均F1先把每个类别的F1算出来再取算术平均所以稀有类别哪怕数量很少也会在指标里占同样的权重缺了它模型分数立刻掉下来。AUC则更关心排序能力不依赖阈值选择。我这次以宏平均F1为主AUC作为辅助参考训练和调参都围绕宏F1来做阈值也在验证集上单独搜索尽量让每一类都被照顾到而不只是追着大头样本跑。这里有一个很容易忽略的小细节训练集和测试集的天体可能来自不同的天区或观测时段特征分布会漂移赛题组一般不会给你提示但如果你只按随机划分来交叉验证可能会得到一个偏乐观的分数。我习惯在训练集里再按红移或坐标分桶做“分区交叉验证”这样测出来的成绩更接近真实测试集的难度。2. 拿到训练集之后的第一天数据探查与三项关键预处理2.1 天文表格数据的“脏”与“怪”读入数据后我做的第一件事不是看分布而是逐列统计缺失值比例、取值范围和单位。天文数据里有两种“非正常缺失”非常常见一是某波段没有探测到光子记录为“未检测到”二是探测器动态范围有限亮度过高发生饱和记录为截断值。这两种情况都会以缺失值或异常大/小的数值出现在表里但它们的物理含义完全不同。第一种缺失往往意味着“这个天体在这个波段很暗”这对判断类别是有用的信息不能简单填0或填均值。第二种饱和值则说明光度测量不可信如果还拿它去算颜色指数会算出很离谱的负值直接污染特征。我采用的办法是保留一个原始的缺失标记列把缺失单独编码为-999再额外增加一列0/1标记表示“该波段是否有效探测”让模型自己学会利用这个信息。实际涨分不多但稳定性提升明显。单位也是常见的坑。星等本身是对数亮度越小越亮有些列给的是星等有些列给的是通量。如果不小心把一个通量列当成星等列去算颜色特征物理意义就完全错了。我在探查阶段把每个列的最小值、最大值、分位数都过了一遍凡是出现“负星等”或“异常小数”的列都会回头核对口径。这个习惯帮我提前发现了三处单位不统一的列。数据探查阶段我还会快速做一次特征相关性热力图。天文数据的特征之间天然高度相关比如五个波段的星等彼此就是强相关的直接丢进树模型虽然也能跑但会稀释真正有判别力的组合特征。相关性分析不仅是为了筛选更是为了理解哪些波段组合可能更有物理区分度给后面的手工特征构造指路。2.2 类别不平衡不是小问题天文样本里恒星数量远超类星体这种不平衡是最典型的干扰项。不能只靠把所有样本扔进模型让它自己学因为多数树模型在默认参数下会偏向多数类导致稀有类别的召回率很低。我开始的第一版基线没有做任何处理宏F1只有0.71其中类星体一类的F1惨不忍睹才0.45左右。修正方式里我用到了三管齐下分层K折保证每一折的训练集里三类比例差不多模型里加上类别权重参数让稀有类别在损失函数中获得更大惩罚评估时直接看宏F1而不是准确率。这三条里最核心的是分层K折因为天文样本的组织顺序往往跟观测天区相关如果直接随机切会出现某一折缺少某个类别的极端情况。还有一个容易踩的坑是过采样。有人会把稀有类别复制几倍让模型更容易看到它。但在天文数据里类星体样本本来就存在较大的观测噪声简单复制只会让模型记住少量样本的噪声模式验证集上虚高线上反而退步。我的做法是不做过采样全靠类别权重和更精细的特征把稀有类别区分出来结果反而更稳。2.3 缺失值填充的物理含义表面上看用中位数填充缺失值很常见但天文数据里不能一概而论。类星体在光学波段可能很亮但是在某个紫外波段探测不到这本身就是“该天体在该波段辐射很弱”的信号。如果把这个缺失填成全局中位数等于抹掉了这个关键线索。我的处理是把缺失分成两类如果缺失比例很低直接用该类别内部的分布去随机采样填充如果缺失比例很高宁可保留专门的缺失标记特征也不要做过多猜测。一个很实用的操作是给每个缺失率高的列都额外生成一个“是否缺失”的布尔特征树模型能从这些布尔特征里分裂出非常稳健的规则比如“紫外波段缺失且红移大于1的天体更可能是类星体”。这类规则人很难事先想到但树模型非常擅长学习而且几乎不会过拟合。3. 特征工程天文领域的“物理先验”是最大的提分武器3.1 颜色指数与星等最有解释力的组合特征天文数据挖掘里最经典的特征就是颜色指数本质上是两个波段的星等差值。为什么会这么有用因为恒星、星系、类星体在光谱能量分布上的差异会直接体现在不同波段的亮度对比上恒星往往比较“红”星系在光学波段温和地分布类星体因为中心引擎极亮且红移高在某些波段会出现强烈的颜色异常。我拿到的基础列是u、g、r、i、z五个波段的星等于是我用两两组合构造了一个颜色指数家族比如u-g、g-r、r-i、i-z以及跨越更大波段的u-r、g-z。这一步几乎零成本却让模型的宏F1从基线直接涨了两三个点。关键原因在于模型不需要自己去组合特征就能直接在颜色平面上找到非线性边界。我建议构造这类特征前先画几张小样本的散点图检查不同类别在颜色空间里的分布重叠程度。如果某两个颜色指数几乎完全相关只保留一个即可没必要全部塞进去。我当时看到g-r和r-i存在较强的相关性就删掉了其中一个原始颜色特征让模型把注意力放在信息量更高的组合上。3.2 从形态参数里挖出更细的线索除了测光颜色表中还有形态参数类点源程度、半光半径、椭圆率、表面亮度拟合值等。这些参数直接关系到天体是点源还是扩展源。恒星的半光半径非常小近似于点扩散函数星系有可分辨的延展结构半光半径会明显偏大类星体虽然本质上位于极远的星系中心但观测上常常也呈点源形态参数会和恒星混淆。所以单看形态参数并不能很好把恒星和类星体分开但它们组合起来能先排除掉大量星系样本让模型面对的分类边界更清晰。我额外构造了几个比值型特征半光半径与点扩散函数半径的比值、表面亮度与星等的比值、椭圆率与颜色指数的交互项。这类物理量纲不一样的比值特征树模型在分裂时能产生更平滑的规则对噪声也更鲁棒。有一点要提醒形态参数如果是从图像模型里拟合出来的拟合质量在各观测条件下差异很大。我会按“拟合置信度”做一个筛选把低置信度样本做上标记而不是直接删掉。因为低质量拟合本身可能暗示目标很暗、很边缘这些信息有时恰恰对分类有帮助。3.3 特征筛选不要无脑堆特征刚开始我倾向于把所有可能的特征都堆上去觉得树模型会自动挑。后来验证集分数不升反降而且是那种反复波动的小幅下降才意识到冗余特征太多会让分裂选择不稳定尤其当特征数量超过样本数量到一定程度的时候树模型也会出现方差变大。我用LightGBM自带的重要性排序做了一轮筛选去掉重要性排名靠后而且与高重要性特征相关系数超过0.8的列。这一轮特征数量从40多降到24个验证宏F1反而涨了0.4个百分点左右。后续我又加入了一些物理先验组合特征再重复一轮筛选逐步形成最终的特征集。筛选时别只看gain重要性还要看分裂次数。有的特征重要性高但分裂次数少说明它只在少数样本上提供了很大增益这种特征在交叉验证里波动大。我更偏向选用分裂次数中等、gain稳定的特征它们在跨天区、跨波段的泛化上表现更好。这个经验在最后几次提交中得到了印证删掉几个高波动特征后公共榜和本地验证的差距明显缩小了。4. 模型选型与训练策略为什么我选了树模型而非深度学习4.1 表格数据场景下树模型的基本盘说实话我之前也试过把特征标准化之后塞进一个全连接网络效果一言难尽调参花了两倍时间最后宏F1只和LightGBM默认参数打平稳定性还差一些。对于这种特征维度几十、样本量十几万的表格数据梯度提升树依然是性价比最高的选择。原因不复杂天文特征之间大量存在阈值型规律比如“红移高于1.5且u-g颜色异常”这样的规则树模型天然擅长捕捉而神经网络要靠大量参数去拟合同样的分片边界需要更多数据和更精细的正则化。LightGBM和XGBoost在单机上训练速度也够用不需要上GPU迭代效率高得多。我最终选择LightGBM是看中它对类别特征的天然支持、内置的正则化和直方图优化。XGBoost我也跑了同样的特征输入分数基本相当但LightGBM的早期停止更平滑调参量略少。这里没有标准答案重点是别把时间浪费在反复比较两个库上选一个吃透参数和特征就好。4.2 训练策略分层采样、早停与交叉验证我采用五折分层交叉验证每一折训练出一个模型再用五折的验证概率拼接成“干净”的预测。这样得到的验证宏F1和真实测试集的表现高度相关比单个训练验证划分可靠得多。每折训练时我都在训练子集里再留出最后15%作为早停验证集用early stopping控制迭代轮数防止单折过拟合。模型参数上学习率设为0.03叶子数限制在127左右最小样本数设成50特征采样比例0.8bagging比例0.8。这些参数适合表格特征数量在二三十维、样本十几万的情况如果你的特征量级差异很大记得按比例调整。一个被反复验证有效的小策略在交叉验证里记录每一折验证集的类目别F1不要只看平均。我在第三折的类星体F1明显比其它折低排查发现那一折覆盖的天区里近一半类星体的颜色分布跟整体不一致属于观测深度差异导致的分布漂移。如果没有分区认识我会误以为是模型不稳定从而乱调参数。这提醒我交叉验证不仅要看均值更要看波动来源。4.3 概率校准与阈值搜索模型输出的概率分数在竞赛场景里往往不是最终提交的类别标签尤其是用宏F1评估时直接按0.5阈值判类会吃亏。我在验证集上遍历阈值找出让宏F1最高的判定边界。三分类问题里我把类星体看成“高优先级类别”先判断某样本是类星体的概率是否超过其专属阈值再在剩余样本里用另一个阈值区分恒星和星系。这样做之后宏F1又涨了0.5到0.8个百分点。原因在于宏平均指标对不同类别的阈值敏感度完全不同统一阈值是很粗糙的做法。如果你也面对类别不平衡的多分类强烈建议把阈值当成可优化参数而不是固定0.5。概率校准我用了Platt缩放和等渗回归两种对比后发现等渗回归在这个数据上提升更大。但这个结论有风险等渗回归在验证集上可能出现过度拟合尤其是稀有类别样本数很少时。我采用五折内做嵌套校准把校准器也放进交叉验证流程里避免高估校准带来的收益。最终提交的概率就是校准后的五折平均概率这个输出再配合阈值搜索基本就是我的最优配置。5. 比赛复盘从榜单一度冲到高位又掉下来的几个教训5.1 公共榜分数波动不要过度相信单次结果比赛进行到中段我用一个版本特征提交上去公共榜分数一下冲到了前列当时还挺兴奋。结果第二次提交同样的模型加了一点微小变化分数掉了一大截。后来才明白公共榜只是测试集的一个小抽样样本量不大单次提交的分数方差很高。我差点就因为一次好成绩选择了错误方向。从那以后我给自己定了规矩任何改动必须先在本地交叉验证上有明确的提升而且这个提升要在至少三折上同步出现才值得去提交一次公共榜。公共榜更多是用来确认“没有发生严重的分布偏移”而不是做细粒度调参。顺着公共榜反复试探很容易在少量样本上过拟合线上私有榜一换样本就露馅。5.2 误把稀有样本当成噪声删除的后悔事有一版特征工程里我发现类星体样本里有一些点在颜色空间上跟恒星高度重叠看起来很像异常值于是顺手用聚类算法把它们标出来当成噪声删掉了一部分。验证集宏F1不但没涨反而掉了一个多点。排查之后发现这些“重叠点”其实是一类低红移类星体它们的物理性质与普通类星体不同在颜色上确实更接近恒星但正是这种难样本才最有价值。在普通数据竞赛里清洗异常值通常很安全但在天文数据里“看着奇怪”的样本往往是物理上有特殊意义的天体不能按统计离群点处理。我的教训是任何删除操作之前先去查一查这些样本在物理参数上的分布看看是否有某种一致性。如果有它大概率不是噪声而是尚未被你的特征捕捉到的子类。5.3 集成与后处理最后涨分的几个百分点单模型五折交叉验证的宏F1稳定之后我做了两件事来榨取最后的分数。第一用LightGBM和XGBoost各自训练五折模型对概率做加权融合权重按验证集宏F1来配比微调后整体涨了约0.3个点。第二用RuleFit思路做了一点简单后处理对预测概率接近判定边界的样本用物理规则做修正比如“半光半径极大且颜色偏红的样本更可能是星系”把这些规则作用在边界样本上可以小幅提升宏F1。这类后处理属于经验性操作幅度不能太大。我会先在验证集上反复模拟确保每条规则带来的收益大于可能引入的噪声。最后提交版本融合了树模型的概率、学到的特征边界、以及少量物理规则修正整体比最初的基线高出了将近8个百分点这个过程中真正拉开差距的不是某个神奇模型而是对天文特征含义的理解和对验证流程的坚持。6. 可复现的代码骨架与提交检查清单6.1 数据加载与特征构造的代码结构下面给出一段适配这类表格数据竞赛的代码骨架。核心是特征构造函数和缺失标记处理其他部分可以直接复用。import pandas as pd import numpy as np import lightgbm as lgb from sklearn.model_selection import StratifiedKFold def build_features(df): df df.copy() bands [u, g, r, i, z] for b in bands: col f{b}_mag df[f{b}_missing] df[col].isna().astype(int) df[col] df[col].fillna(-999) # 颜色指数两两波段星等差值 for i in range(len(bands)): for j in range(i 1, len(bands)): b1, b2 bands[i], bands[j] df[f{b1}_{b2}_color] df[f{b1}_mag] - df[f{b2}_mag] # 形态参数组合 df[half_light_ratio] df[half_light_radius] / (df[psf_radius] 1e-6) df[surf_brightness_per_mag] df[surface_brightness] / (df[r_mag] 1e-6) return df这段代码有几点要说明。缺失值我用-999填充同时保留了缺失标记列是因为树模型可以把-999当成一个独立节点去分裂不需要它像实数一样有物理含义。填0也可以但务必保证不会与真实数值范围混淆。颜色指数构造时如果遇到缺失减法会得到-999减去另一个值结果同样进入-999量级配合缺失标记也不会误导模型。6.2 训练与预测的主流程def train_lgb(X_train, y_train, X_val, y_val): dtrain lgb.Dataset(X_train, labely_train) dval lgb.Dataset(X_val, labely_val, referencedtrain) params { objective: multiclass, num_class: 3, learning_rate: 0.03, num_leaves: 127, min_data_in_leaf: 50, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 1, metric: multi_logloss, verbose: -1 } model lgb.train( params, dtrain, num_boost_round3000, valid_sets[dval], callbacks[lgb.early_stopping(100), lgb.log_evaluation(200)] ) return model训练主函数采用分层五折调用每一折分别做早停。对于多分类损失函数可考虑增加稀有类别的权重将三个类别的权重设为[1.0, 1.0, 2.0]之类具体值要通过验证集微调。类权重的作用相当于把类星体样本“放大”在梯度更新中更有话语权但也要小心放太大导致过拟合。6.3 提交前检查清单最后一步永远是最容易翻车的我在几次比赛中都见过“模型没问题但提交格式错误”导致直接零分的情况。反复核对过之后我整理了下面这个清单测试集样本顺序是否保持原文件的id顺序很多评分脚本按行号对齐一旦打乱就全错。预测概率是否为多分类的每一列概率之和等于1部分评分脚本会做归一化但最好自己先检查。标签映射是否和官方一致比如0代表恒星、1代表星系、2代表类星体不同比赛定义不同。最终特征列顺序是否和训练时一致特征名错位会导致预测结果完全错乱。是否重新训练了全量数据模型而不是只拿五折模型之一去提交五折平均通常更稳定但如果时间紧至少要用最后一折模型加测试集特征完成预测。我一般会在提交前写一个小脚本把提交文件和原始测试集文件读进来逐一检查行数、id对应关系和概率范围全部通过才导出。这个流程看着笨但能挡住九成以上的低级失误。做完这一轮天文数据挖掘实验我最大的感受是这个赛题真正难的并不是“机器学习”而是你要先听懂天文学家留下的列名在说什么。星等、颜色指数、红移、半光半径每一个名词背后都藏着一整条物理规律把它们翻译成特征的过程才是从0.70到接近0.80宏F1的关键。最后再分享一个小技巧遇到看不懂的列名先别急着丢进模型花十分钟查一查这个参数在天文里是怎么测量出来的往往比多调十组参数更有效。竞赛的结果只是一份排名但这一套“物理背景特征重构稳健验证”的思路放到任何表格数据挖掘任务里都依然管用。