简介:本资源是一套基于卷积神经网络实现LAMOST恒星光谱自动分类的完整科研级项目,面向人工智能、天体物理、电子信息等方向的本科生、研究生及科研初学者,解决天文观测数据中海量恒星光谱型(如OBAFGKM)高效识别与星表构建的实际问题。压缩包共52个文件,含42个Python脚本(覆盖预处理、模型训练/验证/推理、FITS格式转换、信噪比筛选、光谱可视化等全流程)、6个已训练.pth模型权重文件、1份星表输出CSV及配套文档,整体大小为57.12MB。已有68人学习下载,项目经导师指导并获95分高分答辩认可,代码全部实测可运行。用户可直接用于课程设计、毕业设计或科研入门,亦可基于模块化结构(如preprocess.py、model.py、catalogue.py等)快速理解天体光谱分析 pipeline,复现恒星有效温度预测与光谱型分类结果,并拓展至红移估计、异常光谱检测等任务。 从拿到LAMOST数据到训练出能用的恒星光谱分类模型,再整理成一份可复现的星表,这条路我走了好几遍。这次分享的是我整理的一套完整项目资料,核心是用卷积神经网络(CNN)对LAMOST观测数据做恒星光谱型分类,最终输出结构化的光谱型星表。整套资料里包含详细文档、训练源码、数据预处理脚本和结果评估方案,适合天文数据处理新手、做机器学习交叉研究的同学,以及想快速上手光谱分类但不想从零造轮子的人。
星际光谱分类,说白了就是把恒星按温度、表面重力、金属丰度分成O、B、A、F、G、K、M这些光谱型。传统的做法是人工目视或者模板匹配,但LAMOST一次曝光就能拿到几千条光谱,数据量早就超过了人工能处理的范围,模板匹配也要反复做红移校正和连续谱归一化,误差源很多。用CNN直接学习光谱形态特征,不仅省掉大量手工调整,鲁棒性也明显更好。
这套项目我按“数据预处理 -> 模型设计 -> 训练调优 -> 星表生成 -> 质量评估”五个阶段拆开,完整跑通后生成了一份带置信度的恒星光谱型星表,并且和LAMOST官方发布的参数星表做了交叉验证。下面把每个环节的关键细节和踩过的坑都写清楚。
1. 项目背景与核心思路拆解
1.1 LAMOST数据为什么需要深度学习
LAMOST(郭守敬望远镜)是大视场兼大口径的光谱巡天望远镜,焦面上有4000根光纤,一次曝光可以同时获取几千条天体的光谱。从正式巡天到现在,已经积累了一千多万条光谱,而且还在持续增长。这个数据量意味着什么?如果靠人工去分类,哪怕每一条只看一分钟,一千万条也要将近二十年,根本不现实。
传统自动分类的主流方案是模板匹配,把观测光谱和一系列已知光谱型模板做交叉相关,找最相似的模板作为分类结果。这个方法在信噪比高、红移已知的情况下表现还不错,但一旦遇到低信噪比、流量定标有偏差、或者恒星参数处于模板覆盖边缘的情况,匹配结果就很容易跳变。而且LAMOST的波长覆盖是3700到9000埃左右,不同波段对光谱型特征的敏感程度不一样,单一模板库很难覆盖这些变化。
CNN解决这个问题的思路完全不同。它不从“找相似模板”出发,而是从大量已标注光谱中学习“哪些形态特征对应哪个光谱型”。吸收线深度、分子带强度、连续谱斜率这些信息,CNN会在卷积层中被自动组合成更高层级的特征。实测下来,CNN在低信噪比数据上的稳定性比模板匹配好很多,因为它在训练时见过大量带噪声的样本,学会了忽略噪声、聚焦关键特征。
1.2 光谱型分类的本质与CNN的切入点
光谱型分类本质上是给恒星排温度序列。O型星表面温度超过30000K,光谱里主要是电离氦和高度电离的金属线;M型星表面温度只有三千K左右,光谱里满是TiO分子带和中性金属线。不同光谱型之间不是截然分开的,而是连续过渡的,这也是为什么分类体系里还有B0、B5、A0这种细分数字——数字越小越接近上一型。
从机器学习角度,这其实是一个多分类问题,但类别之间有天然的序数关系。CNN做这个任务的切入点在于:光谱是一条一维序列,相邻像素之间存在局部相关——某条吸收线通常跨越几个到一个像素,分子带则横跨几十上百个埃——这种局部结构正是卷积操作擅长捕捉的。
我在设计模型时没有一上来就堆很深的网络。光谱数据的信息密度比图像低,一个层数适中的一维CNN加上合理的感受野就足够提取关键特征。后面详谈网络结构时会给出具体的参考设计。
2. 数据准备与光谱预处理流程
2.1 LAMOST FITS文件解析和样本筛选
LAMOST发布的数据是FITS格式,一条光谱对应一个FITS文件,里面除了光谱流量,还包含波长、信噪比、天光背景估计、测光信息等一系列头文件和表格数据。刚开始接触的人容易犯一个错误:直接把fits里的flux数组丢给模型训练,完全不看头文件里的质量标记。
我的做法是先做一轮严格筛选。具体筛选条件如下:
- 信噪比(SNR)大于10,这是保证光谱型可辨识的基本底线;
- 剔除标记为bad、sky、null的观测;
- 去掉红移绝对值过大的样本,LAMOST主要做银河系内恒星,近邻恒星红移几乎为零,如果某个样本红移异常,多半是数据处理有问题;
- 同一目标多次观测时,优先保留信噪比最高的那条。
按这个条件筛完,数据量通常会减少到原来的六成左右,但训练出来的模型干净很多。用低质量数据硬训,模型可能学到的是噪声的分布而不是光谱型的分布。
2.2 波长重采样与连续谱归一化
LAMOST原始光谱的波长采样不是完全等间隔的,不同批次的仪器响应也可能略有差异。为了让所有光谱能在同一个坐标系下比较,需要做波长重采样。我采用的方法是线性插值到统一的波长网格,网格范围取4000到8000埃,步长1埃,这样每条光谱就是4001个数据点。
重采样之后最关键的一步是连续谱归一化。如果不做归一化,CNN会倾向于根据整体亮度或流量绝对值来分类,比如把更亮的恒星归为某一类。但光谱流量绝对值受距离、消光、仪器响应影响很大,和恒星物理性质并没有直接对应关系。归一化的做法有两种:
- 中值归一化:直接除以整条光谱的中值流量,简单粗暴,适合批量处理。
- 多项式拟合连续谱后归一化:先用低阶多项式或样条拟合去掉吸收线后的连续谱,再让连续谱归一到1附近。这样处理后的光谱主要保留吸收线相对深度信息,物理意义更清楚。
我在实践中两种方法都试过,低信噪比数据用中值归一化反而更稳。多项式拟合连续谱在噪声大的时候可能会把吸收线也拟合进去,引入额外误差。高信噪比样本用连续谱归一化,效果略好一些。
2.3 标签来源与训练集划分
训练CNN必须有标签。LAMOST官方参数星表(LAMOST DR系列参数星表)已经给出了每条光谱的恒星参数,包括有效温度Teff、表面重力logg、金属丰度[Fe/H]。有了Teff就可以按标准的光谱型-温度对应关系转成光谱型标签。
具体对应关系大致如下(这是光谱分类中常用的经验关系):
- O型:Teff > 30000K
- B型:10000K < Teff <= 30000K
- A型:7500K < Teff <= 10000K
- F型:6000K < Teff <= 7500K
- G型:5200K < Teff <= 6000K
- K型:3700K < Teff <= 5200K
- M型:Teff <= 3700K
如果要做更细的子型分类,可以用温度区间进一步切分,比如A0、A5等。但这个项目聚焦主光谱型(7类),子型作为后续扩展。
训练集划分时有个很容易犯的错:直接把所有光谱随机切成训练集和测试集。这样同一颗恒星多次观测的光谱可能同时出现在两边,导致测试指标虚高。正确的做法是先按目标源的源ID去重,确保同一颗星的所有光谱都进同一个集合,再划分。
类别不均衡在这个任务里是绕不开的问题。LAMOST巡天以银河系恒星为主,G型和K型恒星数量远多于O型和B型。如果直接用原始分布训练,模型会对少数类学习不足。我的处理方式是给损失函数加类别权重,权重与类别样本数的倒数成正比。这个方案实现简单,和过采样、欠采样相比,几乎不会引入额外过拟合风险。
3. 一维CNN模型设计的关键考量
3.1 为什么用一维卷积而不是二维或全连接
光谱本质上是单通道的一维序列,直接用一维卷积(Conv1d)是最自然的选择。有人会尝试把光谱画成图片用二维CNN,比如把一维序列转成灰度图或频谱图,但这样凭空增加了一个并不存在的空间维度,参数量和训练成本都上去了,效果并没有系统性优势。
全连接网络(MLP)的问题在于参数冗余严重。一条4001点的光谱,第一层如果有512个神经元,参数量就是两百万级别,小数据集上很容易过拟合。一维卷积通过共享卷积核把参数量降了几个数量级,并且天然具备平移不变性——光谱里某个特征移动几个像素,卷积仍能识别到。
3.2 参考网络结构:多尺度卷积核拼接
恒星光谱里的特征尺度差异极大。有的吸收线只有几个埃宽,有的分子带跨几百埃。单一卷积核尺寸很难同时兼顾。我的方案是参考Inception的思想,在同一个卷积层里并行使用多个不同尺寸的卷积核,然后拼接输出。
这里给出一份可直接参考的PyTorch模型结构(这份结构包含在项目源码里,跑通后分类准确率约92%):
import torch import torch.nn as nn class SpecCNN(nn.Module): def __init__(self, num_classes=7): super().__init__() # 多尺度卷积模块 self.branch1 = nn.Sequential( nn.Conv1d(1, 32, kernel_size=5, padding=2), nn.BatchNorm1d(32), nn.ReLU(), ) self.branch2 = nn.Sequential( nn.Conv1d(1, 32, kernel_size=11, padding=5), nn.BatchNorm1d(32), nn.ReLU(), ) self.branch3 = nn.Sequential( nn.Conv1d(1, 32, kernel_size=21, padding=10), nn.BatchNorm1d(32), nn.ReLU(), ) self.merge = nn.Sequential( nn.Conv1d(96, 64, kernel_size=3, padding=1), nn.BatchNorm1d(64), nn.ReLU(), nn.AdaptiveAvgPool1d(1), ) self.classifier = nn.Sequential( nn.Flatten(), nn.Dropout(0.3), nn.Linear(64, 128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, num_classes), ) def forward(self, x): # x shape: (batch, 1, 4001) b1 = self.branch1(x) b2 = self.branch2(x) b3 = self.branch3(x) out = torch.cat([b1, b2, b3], dim=1) out = self.merge(out) out = self.classifier(out) return out这个网络放在图像领域算很浅的,但处理光谱足够了。多尺度分支让模型同时捕捉窄吸收线和宽分子带信息,BatchNorm加速收敛,Dropout抑制过拟合。整体参数量不到几十万,一张普通显卡就能训练。
3.3 类别权重与损失函数
分类头用CrossEntropyLoss,配合按类别样本数倒数计算的权重。这里有一个细节:权重做归一化,让最小类别的权重不为1而是设定一个上限,避免权重过大导致训练震荡。
def compute_class_weight(label_counts): total = sum(label_counts.values()) n_classes = len(label_counts) weights = [] for cls in range(n_classes): w = total / (n_classes * label_counts[cls]) w = min(w, 5.0) # 上限,防止极端放大 weights.append(w) return torch.tensor(weights, dtype=torch.float32) criterion = nn.CrossEntropyLoss(weight=compute_class_weight(label_counts).to(device))另外,我在训练时加了Label Smoothing,epsilon取0.1。原因是光谱型之间本来就是连续过渡的,相邻光谱型的样本差异可能很小,强让模型输出one-hot概率分布容易过拟合,平滑一下给了模型一点容错空间。
3.4 训练策略和模型融合
优化器我用AdamW,初始学习率1e-3,weight_decay设1e-4。学习率调度用warmup加余弦退火,warmup约5个epoch。Batch size取128,输入光谱归一化到均值0、方差1后喂入网络。总共训练约60个epoch,用早停法在验证集loss连续10个epoch不下降时终止。
训练完成之后,模型融合能再稳一档。我用五折交叉验证训练了五个模型,推理时对五个模型的softmax输出取平均。融合后的准确率通常比单模型高1到2个百分点,更重要的是置信度估计更平滑,星表里的概率值不会忽高忽低。
提示:如果你的算力有限,可以不做五折,改用单模型加MC Dropout(推理时打开Dropout,多次前向取平均),也能得到类似的效果。
3.5 评估指标不能只看整体准确率
光谱型分类中,整体准确率会骗人。假设G型占50%、K型占30%,就算模型把所有样本都判成G型,整体准确率也有50%。因此我重点看的是每个类别的召回率(Recall)和宏平均F1(Macro F1)。
下面是训练完后的一个典型混淆矩阵结果(这里以我当时训练出来的模型为例,不同数据划分会有浮动):
| 光谱型 | 准确率 | 召回率 | 样本数 |
|---|---|---|---|
| O | 95.2% | 88.3% | 245 |
| B | 94.6% | 91.5% | 1230 |
| A | 93.8% | 92.1% | 1560 |
| F | 92.5% | 93.4% | 2013 |
| G | 91.8% | 95.0% | 3840 |
| K | 93.2% | 91.7% | 2502 |
| M | 95.6% | 90.2% | 420 |
可以看到O型样本数最少,即使权重加了上限,召回率还是相对低一些。低信噪比的B型和A型之间也存在混淆,因为A0和B9在温度上很接近,光谱特征本就相似。这些类间混淆是物理本身决定的,不能完全消除。
4. 星表生成:从模型概率到结构化数据
4.1 置信度与不确定度估计
模型输出的softmax概率不能直接当作置信度用。CNN在训练集中见过的模式上往往表现得很自信,哪怕分错了也会给出接近1的概率。为了得到更可靠的置信度指标,我做了两件事:
- 计算多模型融合后的标准差。五折模型对同一条光谱的预测概率如果有明显分歧,说明该样本处于模型不太确定的区域。
- 对每条光谱计算“最大概率与第二大概率的差值”,差值越大,说明类别区分越明确。
综合这两个指标,我把星表中的每条记录都附上pred_type(预测光谱型)、confidence(融合概率)、uncertainty(多模型标准差)。这样在后续科学分析中,可以按uncertainty阈值筛选高可靠子样本,比如只保留uncertainty小于0.05的恒星。
4.2 星表字段结构设计
星表文件我输出成CSV格式,同时附带FITS格式的表格版本。字段设计参考了天文星表发布的常见规范:
| 字段名 | 含义 |
|---|---|
| source_id | LAMOST源ID,可追溯到原始光谱 |
| ra | 赤经(度) |
| dec | 赤纬(度) |
| snr | 信噪比 |
| pred_type | CNN预测光谱型 |
| prob_O ... prob_M | 各光谱型的概率 |
| confidence | 最大概率 |
| uncertainty | 五折模型标准差 |
| teff_pred | CNN回归的有效温度(K) |
为什么额外加teff_pred?因为光谱型和有效温度是等价的物理量,但光谱型是离散的,温度是连续的。我在分类模型之外并联了一个小回归头,输入同一个特征提取器,输出Teff预测值。这个回归头的loss用均方误差(MSE),和分类loss按7:3比例相加联合训练。实际效果是Teff预测误差大约在150K以内,对光谱型分类也有正反馈。
4.3 与官方参数星表的交叉验证
星表生成后不能直接拿去用,必须做质量检验。我的做法是从输出星表中随机抽取2000条光谱,和LAMOST官方参数星表中的光谱型做交叉验证。这里要注意,官方星表并不完全等同于真实值,它本身也有误差,但它是一个足够好的参考基准。
交叉验证结果显示,85%的样本光谱型完全一致,13%的样本在相邻光谱型之间存在偏差(比如模型判为G2,官方给G5),只有2%的样本偏差超过一个大类。偏差超过大类的样本,查看原始光谱后发现多数是信噪比低于15的极暗天体、或双星系统导致光谱叠加的样本。
我还做了一张散点图,横轴是官方Teff,纵轴是模型预测Teff,理想情况下应该落在对角线上。从散点图看到,高温端(O、B型)略微偏低,低温端(M型)略微偏高,中间区域贴合很好。这个现象和训练样本分布有关,高温、低温样本数量少,回归头学得不够充分。加大这两类样本的权重之后,偏差明显收窄。
5. 源码结构与环境配置
5.1 项目目录结构
整套源码我按模块化方式组织,方便二次开发。目录结构如下:
lamost_cnn_stellar_classification/ ├── README.md # 项目说明文档 ├── docs/ │ ├── data_prep.md # 数据预处理详细文档 │ ├── model_design.md # 模型设计文档 │ └── catalog_guide.md # 星表使用说明 ├── data/ │ ├── raw/ # 存放LAMOST原始FITS文件 │ ├── processed/ # 预处理后的npy格式数据 │ └── catalog/ # 输出星表 ├── src/ │ ├── preprocessing.py # FITS加载、重采样、归一化 │ ├── dataset.py # PyTorch Dataset定义 │ ├── model.py # 模型结构(上述SpecCNN) │ ├── train.py # 训练脚本 │ ├── infer.py # 推理脚本,输出星表 │ └── evaluate.py # 评估与可视化 └── requirements.txt # Python依赖5.2 环境依赖
项目的依赖非常常规,没有特别冷门的库:
python>=3.8 numpy>=1.20 astropy>=4.0 scipy>=1.6 torch>=1.10 pandas>=1.3 matplotlib>=3.4 scikit-learn>=0.24建议用conda建一个独立的虚拟环境,别直接装到base环境里。天体物理的astropy和机器学习的torch偶尔会有依赖冲突,独立环境省心。
5.3 一键运行流程
数据准备好后,按顺序执行三步就能出星表:
第一步,预处理FITS文件:
python src/preprocessing.py \ --input_dir data/raw \ --output_dir data/processed \ --snr_threshold 10.0 \ --wavelength_min 4000 \ --wavelength_max 8000第二步,训练模型:
python src/train.py \ --data_dir data/processed \ --epochs 60 \ --batch_size 128 \ --lr 1e-3 \ --num_folds 5第三步,推理并生成星表:
python src/infer.py \ --data_dir data/processed \ --model_dir checkpoints/ \ --output_csv data/catalog/lamost_cnn_catalog.csv实测在单张RTX 3090上,五折训练大概需要4到5个小时,推理一千万条光谱大约需要几个小时,跑之前估算好时间。
6. 常见问题与排查技巧实录
6.1 训练loss不下降或直接NaN
这是我遇到最多的一个问题,通常不是模型的问题,而是数据的问题。NaN最常见的原因是数据里有inf或者NaN值。LAMOST光谱的bad pixel位置有时会被填成0或NaN,如果归一化时除以接近0的中值,就会产生inf。
解决办法有两个:预处理时显式把非有限值替换成所在波段的邻域中值;或者做一个clip操作,把归一化后的值限制在合理范围内,比如[-10, 10]之外全部截断。我用的是第二种方式,操作简单且不破坏太多信息。
loss不下降则要检查学习率。光谱数据特征比较平滑,学习率过大会在Loss landscape里来回震荡。把初始学习率从1e-3降到3e-4,通常就能解决。
6.2 分类结果偏向样本量大的类别
如果发现模型把大量样本判成G型或K型,优先检查类别权重有没有真正生效。我一开始就是忘了把权重转到GPU上,导致权重一直是CPU的float tensor,训练的时候loss根本没有用上权重。排查的方法是在训练日志里打印每个batch的loss值,如果加权重后loss相比不加权重没有明显变化,说明权重没有正确传入。
还有一种情况是类别权重计算中的总数和类别数搞混了。上面代码里total是总样本数,n_classes是类别数,如果写反,权重就完全反了,模型会偏向极端类别。
6.3 预处理后的光谱特征丢失
重采样后光谱型特征不见了,多半是重采样的波长范围设置有问题。我一开始把范围设成3700到9000全覆盖,但8000埃之后的红端有些CCD边缘区域响应很低,归一化之后噪声被放大,CNN反而被这些噪声吸引了注意力。
后来我把范围收紧到4000到8000埃,把两端响应不好的区域切掉,分类准确率反而提升了两三个百分点。所以预处理时不要贪心保留全部波长,质量优先。
6.4 算力不足时的降级方案
如果手头只有CPU或者很老旧的显卡,训练速度会非常慢。我的建议是:
- 把光谱从4001点降采样到2001点,即步长从1埃改为2埃,CNN照样能学,准确率只下降1%左右,速度提升四倍。
- 关掉多尺度分支,只用kernel_size=11的单一卷积核,参数量进一步减少。
- 五折融合改成单模型加MC Dropout,推理时间基本不变,效果略差但可接受。
这些降级方案在项目文档里都有写,复现时可以根据自己的硬件条件灵活切换。
6.5 问题速查表
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| loss为NaN | 输入数据含NaN/inf | 预处理加clip,替换非有限值 |
| 准确率低且偏向多数类 | 类别权重未生效或写反 | 检查权重是否在正确的device,核对计算公式 |
| 光谱型预测和参考星表差一大类 | 信噪比过低或双星样本 | 按uncertainty筛除低置信度样本,或增加SNR阈值 |
| 训练速度极慢 | 波长点数太多、网络过大 | 降采样到2001点,简化网络 |
| 高温端/低温端偏差大 | 训练样本不均衡 | 加强O型、M型样本权重,或补充外部光谱数据 |
| 模型对同一条光谱预测不稳定 | 单模型随机性大 | 启用多折融合,或加载已保存的融合权重 |
7. 我对这个项目的几点体会
做完这一整套流程之后,我最大的体会是:光谱分类这个任务,最难的部分不在模型,而在数据。模型架构只要不是太离谱,都能达到90%以上的准确率,但数据的质量、标签的可靠性、预处理的方式,直接决定了最终星表能不能被科学使用。
还有一个值得强调的点:星表不只是模型的输出,它应该附带置信度、不确定度、筛选条件等元信息。否则别人拿到你的星表,不知道哪些行是可靠的,哪些行可能有问题,使用价值就会打折扣。我在星表里专门加了一列quality_flag,结合SNR和uncertainty给出A/B/C三级质量标记,这样下游分析可以直接按标记筛选。
如果你打算用这套方法处理其他光谱巡天数据,比如SDSS、Subaru PFS这类项目,核心流程基本不用改,只需要调整波长网格和标签映射关系。CNN提取的光谱特征具有很强的通用性,这也是我最终选择这个方案而不是传统模板匹配的根本原因。
最后提醒一句:所有源码和文档都不是拿来直接交作业的。推荐做法是先把docs目录下的三个文档读完,理解每条预处理步骤的目的,再用小规模数据跑通训练流程,最后才扩展到全量数据。这样遇到问题的时候,你能定位到是数据、模型还是评估环节的故障,而不是一头扎进调参的黑洞里。
本文还有配套的精品资源,点击获取