
把射电望远镜收到的原始时间序列交给 Keras 模型去分类这件事已经不算是前沿实验室里的专属操作了。天文观测数据量增长很快射电巡天项目每天能产出大量动态谱靠人工肉眼去看脉冲星候选体和射频干扰效率很难跟上。这篇文章不聊太多理论推导直接给一套用深度学习和 Keras 解码宇宙信号的落地流程数据怎么准备、样本怎么构造、模型怎么训练、训练完怎么评估、最后怎么部署成批量任务或接口服务。这个流程更适合下面几类读者想用深度学习入门天文信号处理的同学需要在教学或科研里快速验证“信号分类”方案的工程师以及已经跑通过 TensorFlow/Keras 基础项目、想找一个更具体应用场景的人。不需要你先成为射电天文学专家但建议掌握 Python 基础和 Keras 的基本用法。文中会给出代码模板和通用参数你只需要把文件路径、信号长度、类别数替换成自己手上的真实数据。1. 核心能力速览能力项说明项目类型深度学习在天文信号处理中的应用路线覆盖信号分类、去噪、异常检测核心框架TensorFlow / Keras配套 NumPy、SciPy、Matplotlib、Astropy主要功能脉冲星候选体识别、射频干扰标记、单脉冲/周期信号分类、信号去噪硬件需求CPU 可跑通小模型和推理GPU 用于加速训练显存大小由网络规模与 batch size 决定显存占用需以实际模型版本和输入长度为准小模型 2-4G 即可起步深模型或大批量需更高支持平台Windows / Linux / macOSGPU 加速建议 Linux 或 Windows NVIDIA启动方式Python 脚本训练训练后可导出模型文件没有固定 WebUI是否支持 API支持训练后可通过 Flask、FastAPI 或 TensorFlow Serving 封装成 HTTP 接口是否支持批量任务支持用 tf.data 和脚本循环即可批量读取文件夹并输出预测结果适合场景科学研究、教学实验、信号巡天数据预处理、自动化信号分析管道从材料看这套方案更准确的定位是“技术路线”而不是某个单一开源仓库。你可以把它当成一个基线框架换数据、改类别、调网络结构就能迁移到不同观测场景。2. 宇宙信号为什么需要深度学习2.1 射电信号的难点射电望远镜接收到的原始数据里真正值得关注的宇宙信号通常淹没在噪声里。脉冲星的周期辐射可能只占很短的时间窗口快速射电暴则是毫秒量级的单脉冲而地面射频干扰、仪器响应波动又会造成大量假阳性样本。传统处理方法依赖阈值判定和人工设计的特征例如消色散后的信噪比、脉冲轮廓形状、周期统计量等。这些特征在小规模数据上有效可一旦数据量上来规则之间的组合会变得非常多调参和维护成本很高。此外不同观测设备的采样率、频率分辨率、噪声基底并不同。一个在高频段表现良好的特征组合换到低频段数据上可能完全失效。这导致传统信号处理流程很难做跨数据集迁移每个新项目都要重新设计规则。深度学习的好处在于可以把“特征工程”变成“表示学习”模型直接从原始时间序列或动态谱图像里提取有用模式减少人工规则介入。2.2 典型任务类型用 Keras 处理宇宙信号常见任务主要分三类。第一是分类输入是一段动态谱或时间序列输出是信号类别例如“周期脉冲候选体”、“单脉冲候选体”、“射频干扰”、“纯噪声”。第二是去噪与重建通常用自编码器结构把带噪信号编码成低维表示再解码出干净信号适合做信号增强。第三是异常检测很多未知天体信号没有先验标签用自编码器重构误差就能把偏离正常分布的数据挑出来。这三类任务里分类最容易上手。你只需要准备标注好的样本用一维卷积网络或循环网络就能完成训练。去噪和异常检测不需要太多人工标签但评估起来更主观需要结合专业人员的判断。本文的代码示例以分类为基线因为分类任务最能反映从数据到模型再到部署的完整链路验证起来也最直观。2.3 适用场景与使用边界这个流程适合快速验证信号分类方案、批量筛选观测数据、以及作为天文数据分析课程的项目作业。它也能用于工程化信号筛选把低信噪比候选体先由模型粗筛一遍再交给研究者人工复核可以显著减少人工工作量。但也有不适合的场景。如果你面对的是信号形态差异极大、样本极少的新天体类型直接上深度学习很容易过拟合。如果系统对延迟要求极高例如实时监测系统里的毫秒级决策Keras 模型还需要做剪枝、量化和 C 推理部署不能直接套用训练脚本。更重要的是深度学习模型输出的是概率不是科学结论。模型标记为“候选体”的结果必须经过专业验证流程确认不能把预测结果直接当作发现。3. 环境准备与数据获取3.1 开发环境安装环境安装并不复杂核心依赖是 TensorFlow、NumPy、SciPy、Matplotlib 和 Astropy。Astropy 用于读取 FITS 格式的天文数据如果你手上只有 CSV 或 NumPy 数组可以暂时不装。建议先创建一个独立虚拟环境避免依赖冲突。python -m venv kosmic source kosmic/bin/activate # Windows 下使用 kosmic\Scripts\activate pip install tensorflow numpy scipy matplotlib astropy scikit-learn如果你的机器有 NVIDIA GPU想用 GPU 训练需要确认显卡驱动、CUDA 和 cuDNN 与 TensorFlow 版本匹配。这里不写死版本因为不同 TensorFlow 版本对应的 CUDA 版本差异很大。更稳妥的做法是直接安装 TensorFlow 官方推荐的组合并用tf.config.list_physical_devices(GPU)验证 GPU 是否被识别。import tensorflow as tf print(tf.__version__) print(tf.config.list_physical_devices(GPU))如果 GPU 列表为空说明驱动或 CUDA 环境有问题此时模型仍能在 CPU 上运行只是训练速度会慢不少。第一次跑通流程时CPU 训练小规模数据是完全可以接受的。3.2 数据来源与格式天文信号数据的来源主要有三类公开射电巡天数据、望远镜数据集和仿真数据。公开数据通常以 FITS 文件格式存储打开后能看到频率-时间二维数组也就是动态谱。每行是一个频率通道每列是一个时间积分数值代表那个时频像素上的辐射强度。模型可以把这个二维数组当作“图像”用二维卷积处理也可以先压成一维时间序列用一维卷积处理。两种方式都能走通各有利弊。如果暂时没有真实 FITS 数据推荐先用仿真数据把流程跑通。模拟数据的好处是标签完全可信模型能不能学到规律可以快速判断。真实数据往往需要额外做射频干扰剔除、通道归一化、时间窗口截取这些预处理会占掉整个项目一半以上的时间。3.3 读取 FITS 动态谱下面给一个 FITS 读取模板注意不同望远镜的数据结构不同实际读取时要以文件头中的信息为准。from astropy.io import fits import numpy as np def load_dynamic_spectrum(path): 从 FITS 文件读取频率-时间动态谱。 不同任务的文件头可能不同需要按实际数据结构调整索引。 with fits.open(path) as hdul: hdul.info() # 先查看 HDU 结构 data hdul[0].data # 常用主表按需修改 return np.asarray(data, dtypenp.float32) if __name__ __main__: ds load_dynamic_spectrum(./data/sample.fits) print(ds.shape)读取之后你可以把动态谱保存成.npy文件后续训练直接加载免去每次重复解析 FITS 的开销。如果数据量很大也可以用mmap或批量读取方式避免一次性载入所有数据导致内存溢出。4. 样本构造与数据预处理4.1 用模拟数据验证流程先做一套三类别的模拟信号纯噪声、周期脉冲、单脉冲。每个样本长度设成 256 个时间点采样率统一标签分别是 0、1、2。这样做的目的是先把模型流程跑通再迁移到真实数据。import numpy as np def make_noise(length256): return np.random.normal(0, 1.0, sizelength).astype(np.float32) def make_periodic_signal(length256, period32): t np.arange(length) base np.sin(2 * np.pi * t / period) * 0.2 burst_phase t % period pulse np.where(burst_phase 4, 1.0, 0.0) return (base pulse * 0.8).astype(np.float32) def make_single_burst(length256, start90, width10): signal np.zeros(length, dtypenp.float32) signal[start:start width] 1.0 signal np.random.normal(0, 0.1, sizelength).astype(np.float32) return signal def build_synthetic_dataset(n_samples3000, length256): X np.zeros((n_samples, length, 1), dtypenp.float32) y np.zeros((n_samples,), dtypenp.int32) for i in range(n_samples): kind np.random.choice([0, 1, 2]) if kind 0: X[i, :, 0] make_noise(length) elif kind 1: X[i, :, 0] make_periodic_signal(length) else: X[i, :, 0] make_single_burst(length) y[i] kind return X, y X, y build_synthetic_dataset() print(X.shape, y.shape)4.2 标签设计与样本均衡真实场景里纯噪声样本通常远多于脉冲样本类别不平衡会让模型把所有样本都预测成噪声。解决思路有几种对少数类做重采样对多数类做欠采样或者在损失函数里给稀有类别更高的权重。用 Keras 训练时最简单的做法是计算class_weight并传入fit。from sklearn.utils.class_weight import compute_class_weight classes np.unique(y) weights compute_class_weight(class_weightbalanced, classesclasses, yy) class_weight dict(zip(classes, weights)) print(class_weight)标签设计也要保持简单明确。模拟数据里“周期脉冲”和“单脉冲”在波形上差异很大模型容易区分。真实数据的类别边界往往是模糊的比如部分射频干扰和单脉冲在频谱特征上很接近。这时不要急着增加类别先把二分类或三分类跑稳再逐步细分。4.3 构建 tf.data 数据管道数据量增大后不建议用model.fit(X, y)一把梭而是用tf.data.Dataset做批处理和打乱。它支持并行读取、预取和缓存训练速度更稳定。import tensorflow as tf def make_tf_dataset(X, y, batch_size32, shuffleTrue): dataset tf.data.Dataset.from_tensor_slices((X, y)) if shuffle: dataset dataset.shuffle(buffer_sizelen(X)) dataset dataset.batch(batch_size).prefetch(tf.data.AUTOTUNE) return dataset train_ds make_tf_dataset(X[:2400], y[:2400]) val_ds make_tf_dataset(X[2400:2700], y[2400:2700], shuffleFalse) test_ds make_tf_dataset(X[2700:], y[2700:], shuffleFalse)prefetch(tf.data.AUTOTUNE)可以让数据加载与模型计算重叠避免训练时频繁等待数据。如果单个样本较大还可以在map里做归一化或数据增强例如加随机噪声、随机时间偏移增强模型鲁棒性。5. Keras 模型设计与训练5.1 一维卷积分类模型射电信号本质是一维时间序列一维卷积网络比全连接网络更适合提取局部模式。它的参数比二维卷积少输入维度也更容易对齐。下面这个模型把输入长度设为 256输出 3 个类别中间加入批量归一化和 Dropout 控制过拟合。from tensorflow import keras from tensorflow.keras import layers model keras.Sequential([ layers.Input(shape(256, 1)), layers.Conv1D(32, kernel_size7, strides2, activationrelu), layers.BatchNormalization(), layers.MaxPooling1D(pool_size2), layers.Conv1D(64, kernel_size5, strides2, activationrelu), layers.BatchNormalization(), layers.MaxPooling1D(pool_size2), layers.Conv1D(128, kernel_size3, activationrelu), layers.GlobalAveragePooling1D(), layers.Dropout(0.3), layers.Dense(64, activationrelu), layers.Dense(3, activationsoftmax) ]) model.compile( optimizerkeras.optimizers.Adam(learning_rate1e-3), losssparse_categorical_crossentropy, metrics[accuracy] ) model.summary()如果输入不是时间序列而是频率-时间动态谱可以把Conv1D改成Conv2D输入形状变成(height, width, 1)。天文信号处理里动态谱图像用二维卷积通常也能得到不错效果但训练参数量会上升显存占用同步增加。第一次实验建议先用 Conv1D 验证整个流程。5.2 训练配置与回调训练时不要只盯着模型结构回调函数能帮你省很多事。EarlyStopping在验证集指标不再提升时自动停止ModelCheckpoint保存最优模型ReduceLROnPlateau在指标停滞时降低学习率。callbacks [ keras.callbacks.EarlyStopping(patience15, restore_best_weightsTrue), keras.callbacks.ModelCheckpoint( best_model.keras, save_best_onlyTrue, monitorval_accuracy ), keras.callbacks.ReduceLROnPlateau( factor0.5, patience5, min_lr1e-6 ) ] history model.fit( train_ds, validation_dataval_ds, epochs100, callbackscallbacks, class_weightclass_weight, verbose2 )这里保存路径用的是.keras格式新版 Keras 推荐使用这种方式。如果你的环境较老也可以改成.h5。路径和文件名根据自己的目录调整即可。5.3 训练过程与显存观察训练过程中除了看 loss 和 accuracy还要留意训练环境。GPU 训练时可以用nvidia-smi实时查看显存占用窗口显示的内容包括当前进程、显存使用量、GPU 利用率。模型小、batch size 小显存占用低加深网络或增大 batch size显存占用会明显上涨。如果出现ResourceExhaustedError优先减小 batch size然后再考虑换更小的输入长度。训练结束后可以用model.evaluate(test_ds)看测试集指标也可以手动绘制训练曲线观察是否过拟合。验证集 loss 一路下降、训练集 loss 快速降到很低通常说明模型在记忆训练数据需要增强正则化或增加数据量。import matplotlib.pyplot as plt plt.plot(history.history[loss], labeltrain_loss) plt.plot(history.history[val_loss], labelval_loss) plt.legend() plt.title(Loss Curve) plt.show()6. 模型评估与效果验证6.1 用混淆矩阵看漏检准确率在很多不平衡数据集上会骗人。比如 95% 的样本是噪声模型全部预测成噪声也能得到 95% 的准确率但这种模型没有任何实用价值。更可靠的做法是看混淆矩阵它可以直接展示哪些类别被漏掉、哪些类别被误判。from sklearn.metrics import classification_report, confusion_matrix y_pred np.argmax(model.predict(test_ds), axis-1) print(classification_report(y[:], y_pred, target_names[noise, periodic, burst])) print(confusion_matrix(y[:], y_pred))混淆矩阵里的对角线是正确分类的数量非对角线区域就是错误来源。如果“burst”被大量判成“noise”说明单脉冲样本的模型置信度不足需要增加这类样本或调整类别权重而不是简单增加整体训练数据。6.2 精确率、召回率与 F1在信号检测场景里“漏检”通常比“误报”更危险。一个候选体如果被模型过滤掉后续人工复核根本没有机会看到它。因此需要重点关注每个类别的召回率而不是整体准确率。精确率表示模型预测为该类别的样本中有多少是真的召回率表示真实类别样本中有多少被正确找回F1 是两者的调和平均。如果稀有信号类别的召回率偏低可以降低输出概率的判定阈值。比如默认把概率最大的类别作为预测结果但你可以让“burst”在概率超过 0.3 时就被触发宁可多一点误报也不要漏掉潜在候选体。6.3 阈值调整阈值调整的本质是重新定义判定规则。Keras 模型最后接的是 softmax输出的是三个类别的概率向量。你可以在推理阶段设置不同阈值观察准确率和召回率的变化找到适合科学需求的平衡点。prob model.predict(test_ds, verbose0) burst_prob prob[:, 2] pred_burst (burst_prob 0.3).astype(int)这种调整不会改变模型参数只改变决策策略。对于真实科研流程漏检的代价往往很高阈值就应该设置得宽松一些。如果只是做大批量粗筛后续还会用其他方法复核那么可以更激进地减少误报节省下游人工时间。7. 批量推理与接口部署7.1 批量预测与结果导出模型训练好之后常见的用法是对整个文件夹的信号文件做批量预测把结果保存成 CSV 或 JSON 表格。这比一张张图片手动测试更适合工程环境。import glob import numpy as np import pandas as pd model keras.models.load_model(best_model.keras) records [] for path in glob.glob(./signals/*.npy): sig np.load(path).reshape(1, -1, 1).astype(np.float32) prob model.predict(sig, verbose0)[0] records.append({ file: path, pred_class: int(np.argmax(prob)), prob_noise: float(prob[0]), prob_periodic: float(prob[1]), prob_burst: float(prob[2]) }) df pd.DataFrame(records) df.to_csv(classification_results.csv, indexFalse)批量任务运行时建议在输出表里同时保留概率向量而不是只保留最终的类别标签。这样后续想调整阈值时不需要重新跑一遍模型。7.2 模型导出与轻量化Keras 训练脚本里保存的best_model.keras可以直接用于推理但如果你想把它接入其他推理框架可以考虑导出为 SavedModel 或用 tf 2onnx 转成 ONNX。SavedModel 是 TensorFlow Serving 的标准格式ONNX 则便于跨框架部署。model.export(saved_model_dir) # 导出为 SavedModel如果你的输入信号长度是变长的导出时需要注意模型的固定输入形状。更推荐的做法是在训练阶段就固定输入长度导出后再用零填充或截断方式统一输入。模型轻量化不是必须步骤第一次跑通流程时用 Keras 原生格式加载即可。7.3 HTTP 接口示例如果需要把模型接入现有系统可以用 Flask 包一个轻量接口。这个示例只适合本地测试正式对外提供服务时必须加访问控制、请求频率限制和鉴权避免接口被滥用。from flask import Flask, request, jsonify import numpy as np import tensorflow as tf app Flask(__name__) model tf.keras.models.load_model(best_model.keras) app.route(/predict, methods[POST]) def predict(): data request.get_json() signal np.array(data[signal], dtypenp.float32) if signal.ndim 1: signal signal.reshape(1, -1, 1) prob model.predict(signal, verbose0)[0] return jsonify({ probabilities: prob.tolist(), pred_class: int(np.argmax(prob)) }) if __name__ __main__: app.run(host127.0.0.1, port8000)本地调试可以启动服务再用 curl 简单测试curl -X POST http://127.0.0.1:8000/predict \ -H Content-Type: application/json \ -d {signal: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.0]}注意代码里的模型输入长度是训练时固定的请求里 signal 数组长度必须与模型输入端匹配否则会报维度错误。7.4 浮点精度与推理性能训练和推理阶段的浮点精度会影响显存占用和速度。默认 Keras 模型通常使用 FP32推理时如果对精度要求不是极高可以尝试混合精度甚至转成 FP16 模型。FP16 能减少一半显存占用推理速度通常更快但精度会有轻微损失需要重新评估效果。from tensorflow.keras import mixed_precision mixed_precision.set_global_policy(mixed_float16)这个设置最好在构建模型之前开启。FP16 训练并不是所有算子都支持某些层需要保持 FP32 避免数值溢出。如果模型效果出现明显下降就切回 FP32。另一个方向是 BF16它对指数范围更友好但要看硬件是否支持。8. 资源占用与性能观察8.1 训练阶段资源观察训练阶段主要关注三个资源指标CPU 使用率、内存占用、GPU 显存占用。Linux 下用nvidia-smi -l 1可以每秒刷新一次 GPU 状态Windows 下任务管理器也能看到 GPU 专用内存。如果只用 CPU 训练最常见的问题是内存不足因为数据一次性载入内存后还要做 batch 拆分和 shuffle内存占用可能是数据体积的几倍。8.2 CPU/GPU 差异CPU 和 GPU 在训练速度上差异明显但并不是所有模型都能从 GPU 获得巨大收益。小模型、小数据量时GPU 加速效果不明显反而可能因为数据加载和显存拷贝带来额外开销。模型层数较深、batch size 较大时GPU 的优势才体现出来。推理阶段如果你的任务是离线的批量筛选CPU 可以接受。实时处理大量数据时GPU 或经过量化的 CPU 模型更合适。显存占用需要以本机实测为准不要轻信网上流传的“固定占用 4G”之类的说法因为输入长度、batch size、卷积核数量都会影响最终数值。8.3 显存不足的解决思路遇到显存不足时优先减小 batch size比如从 32 降到 16 或 8。如果还不满足可以降低输入信号长度或者减少卷积核数量。使用tf.data的缓存和预取功能可以减少数据读取卡顿但无法解决显存物理上限。如果训练集非常大也可以考虑梯度累积让模型每几步再执行一次优化器更新模拟更大的 batch size不过这会增加代码复杂度第一次用默认配置就好。9. 常见问题、最佳实践与后续路线9.1 高频问题排查表问题现象可能原因排查方式解决方案安装 TensorFlow 失败Python 版本或 pip 源问题查看 pip 报错信息确认 Python 版本换用虚拟环境或换 pip 镜像源GPU 识别不到驱动、CUDA、cuDNN 版本不匹配运行tf.config.list_physical_devices(GPU)安装与 TensorFlow 版本匹配的 CUDA/cuDNN训练时显存溢出batch size 过大或模型过深查看报错中的 tensor shape减小 batch size、缩短输入长度或换小模型验证集 loss 高训练集 loss 低过拟合观察训练曲线增加 Dropout、加数据增强、增加数据量模型全部预测成某一类类别不平衡查看预测分布和混淆矩阵使用 class_weight 或重新采样接口请求报维度错误输入长度与训练时不匹配打印 request 数据 shape在接口里做长度校验和截断补齐批量任务卡住数据读取阻塞或内存不足查看 CPU/内存占用用 tf.data 改造读取流程分批处理模型推理结果不稳定同一输入多次输出不同确认是否打开 Dropout 推理模式用model.predict时会自动走推理模式检查自定义流程9.2 工程与合规建议处理天文数据时首先要确认数据来源和许可协议。公开巡天数据一般允许研究使用但发布结果时需要正确引用数据来源。如果数据来自合作项目或未公开观测要遵守共享协议不能随意公开原始样本。对模型输出的候选体应保留概率、原始文件路径、预处理参数等完整日志方便追溯。工程上建议第一次用小模型和少量数据跑通再逐步增加复杂度。把所有输入文件、训练脚本、模型权重、输出结果分目录管理输出文件统一带时间戳。批量任务要加日志和失败重试接口服务要限制访问范围和请求速率不要把没有任何鉴权的服务直接挂到公网。9.3 后续可扩展的方向流程跑通之后可以考虑几个扩展方向。一是从 Conv1D 换成 Conv2D直接输入频率-时间动态谱看看二维卷积能否提升分类效果。二是用自编码器做无监督异常检测把重构误差作为“未知信号评分”。三是引入迁移学习先用大量模拟数据预训练再用少量真实标注数据微调。四是对批量信号做实时流式处理配合消息队列搭建自动筛选管道。这个流程最大的价值不是某个模型结构本身而是让你掌握“数据预处理 → 模型训练 → 效果评估 → 批量推理 → 接口服务”的完整闭环。先跑通这个闭环再根据实际数据去调整信号长度、类别定义和网络结构后续接新的巡天数据时会顺手很多。建议把这篇里的代码模板保存下来作为以后处理信号分类任务时的基线脚本备用。