变星光变曲线这东西,外行看着就是一堆上下抖动的散点,内行看到的却是恒星的身份档案。过去几年里,我断断续续在做一件事:把这条曲线喂给一维卷积神经网络,让机器自己去判断它属于哪一类变星。这件事听起来像是把图像识别的套路搬过来,但真动手之后你会发现,光变曲线的坑和图片完全不是一回事——采样不等间隔、幅度差几个数量级、类别长尾严重、周期信息藏在相位里。这篇东西就把我从数据清洗到模型上线这一路的做法和踩过的坑完整摊开,适合已经有点深度学习基础、又想往时序科学数据方向挪一挪的读者,也适合做天文数据处理、想找个自动化分类方案的同行参考。
1. 从“人眼看曲线”到“网络看曲线”:问题到底难在哪
1.1 变星分类的真实工作现场
先说说这项工作的原始场景。一次长时间跨度的测光观测,会产出几十万到上亿条光变曲线,每条曲线是“时间—亮度(星等)—测量误差”的三元组序列。人工分类的做法是:把曲线画出来,看形状——是尖锐的食变凹陷,还是平滑的正弦起伏;看周期——几小时还是几百天;看振幅——0.01 星等还是 5 个星等;再结合颜色、光谱辅助判断。一个熟练的标注员,一条曲线快的话十几秒,慢的话要反复折叠相位试好几个周期才能定下来。
问题在于规模。几十万条曲线,靠人工过一遍,就算十个人全职干也要按年算。而且这种判断带主观性:两个标注员对同一条半规则变星给出不同标签,是非常常见的事。所以自动化分类不是“炫技需求”,是硬生生的产能需求。目标很朴素——把 80% 以上容易判断的曲线自动定下来,剩下 20% 模糊的交给人工复核。
1.2 手工特征路线为什么走不远
在深度学习介入之前,主流做法是提手工特征:周期、振幅、偏度、峰度、上升/下降沿时间比、傅里叶分解的各阶振幅比与相位差、小波系数能量分布等等。这条路我走过,能用,但天花板很明显。
第一,特征工程是“针对已知类别”设计的。你能想到用上升沿时间比去区分天琴座 RR 型的 ab 和 c 亚型,是因为你事先知道这两类的光变形状差异体现在哪里。一旦来了一类你没预料到的样本,特征集就失效了。
第二,周期搜索本身是个易错环节。常用的周期图方法在采样有规律性缺口时会产生大量混叠峰,选错周期,后面所有基于相位的特征全部跑偏。我见过一批样本因为选到了二倍周期,被整体错分成另一类。
第三,手工特征丢信息。傅里叶分解取前几阶,等于把曲线做了低通滤波,那些尖锐的食变次级凹陷、米拉变星上偶尔出现的激波隆起,全被抹平了。而这些恰恰是分类的关键证据。一维卷积的好处就在这儿:它不需要你告诉它看哪里,卷积核在整条序列上滑动,自己去找那些有判别力的局部形态。
1.3 一维卷积与光变序列的天然契合
为什么是一维卷积而不是循环网络或者 Transformer?我的理由有三条,都是实测出来的。
一是局部形态的重要性。光变曲线的判别信息高度集中在局部:食双星的“V 形或 U 形凹陷”是一个局部片段,RR 型的“快速上升、缓慢下降”也是局部陡峭度的差异。卷积核天生就是局部感受野加权重共享,处理这类模式效率极高。
二是参数量和训练成本。一条曲线重采样到 1000~2000 个点,如果上 LSTM,序列长度带来的计算开销很可观,而且长序列上的梯度传播不好调。1D-CNN 堆五六层卷积,几万到几十万参数就能跑出很好的效果,单卡训练几小时收敛,这对反复做实验太重要了。
三是对相位对齐不敏感。这一点经常被忽略。循环网络对输入顺序敏感,而光变曲线的起点是任意的——你把序列循环移位 500 个点,物理上是同一条曲线。卷积加全局池化的组合,对这种循环移位有天然的容忍度,这也是我后来在数据增强里敢大胆做随机相位平移的底气。
注意:一维卷积适合的是“已经折叠对齐好的单周期或多周期序列”。如果你直接喂原始的不等间隔时间序列,卷积核的滑动就失去了物理意义,效果会明显变差。这是很多人第一步就走错的地方。
2. 数据决定了模型的上限:光变曲线的清洗、对齐与重采样
2.1 测光表的三个必做清洗动作
拿到原始测光数据,别急着往网络里灌。我固定会做三件事,顺序不能乱。
第一件是剔除明显异常点。测光数据里混着宇宙线击中、坏像素、背景饱和导致的离群值。我的做法是对整条曲线算中位数和 MAD(中位数绝对偏差),把偏离超过 5 倍 MAD 的点直接丢掉。为什么用 MAD 而不是标准差?因为标准差本身会被离群点拉大,用标准差去剔除离群点,等于让贼来抓贼。MAD robust 得多。
第二件是过滤低信噪比点。每条测光记录通常自带误差估计,我把误差大于该曲线误差中位数 3 倍的点丢掉。这一步能砍掉接近地平线、大气质量过大时拍的废点。
第三件是剔除过短的曲线。有效观测点数少于某个阈值(我用的是 30 个点)的曲线,形状信息根本不完整,强行分类只会污染训练集。这批样本统一打上“数据不足”标签,走人工通道。
清洗完要记录清洗比例。我统计过,一个典型的地面巡天数据集,清洗阶段平均会丢掉 8%~15% 的点。如果某批数据丢点率突然飙到 30% 以上,说明观测条件或数据处理管线出了问题,这时候应该停下来查,而不是硬着头皮往下跑。
2.2 不等间隔采样到定长向量的三条路
卷积网络要求输入是定长、等间隔的向量,而天文观测天生不等间隔——白天不能观测,季节变化有观测空档,天气好坏决定采样密度。所以必须做重采样。三条路我都试过:
| 方案 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 线性/样条插值 | 在时间轴上均匀取 N 个点,插值补值 | 实现简单,保留绝对时间跨度 | 大段观测空档被插值“编造”,产生虚假形态 |
| 相位折叠分箱 | 先定周期,折叠到相位 0~1,分 N 个 bin 取中位数 | 物理意义明确,天然对齐 | 依赖周期准确性,错周期直接毁掉样本 |
| 核回归平滑 | 用高斯核在相位空间加权平均 | 对噪声鲁棒,bin 内样本不均时表现好 | 计算量大,核宽需要调 |
我最终选的是“相位折叠 + 分箱取中位数”,N 取 200。理由很实际:分类的依据本来就是相位形态,而不是绝对时间跨度。一条周期 0.5 天的 RR 型和一条周期 300 天的米拉变星,在绝对时间轴上的形态完全不可比,但折叠到相位空间后,形态差异就变得可比了。
分箱取中位数而不是均值,是因为中位数对残余离群点更稳。bin 里没有观测点的,我用相邻 bin 线性插值填充,并且额外生成一个“掩码通道”告诉网络哪些 bin 是插值来的——这个掩码通道后来被证明挺有用,模型学会了在观测稀疏的相位区间降低置信度。
import numpy as np def phase_fold_bin(t, mag, period, n_bins=200): phase = ((t / period) % 1.0) order = np.argsort(phase) phase, mag = phase[order], mag[order] edges = np.linspace(0, 1, n_bins + 1) idx = np.digitize(phase, edges) - 1 idx = np.clip(idx, 0, n_bins - 1) binned = np.full(n_bins, np.nan) mask = np.zeros(n_bins, dtype=np.float32) for i in range(n_bins): sel = mag[idx == i] if sel.size > 0: binned[i] = np.median(sel) mask[i] = 1.0 # 空洞用线性插值补,并保持 mask=0 nan_idx = np.isnan(binned) if nan_idx.all(): return None, None binned[nan_idx] = np.interp( np.flatnonzero(nan_idx), np.flatnonzero(~nan_idx), binned[~nan_idx] ) return binned.astype(np.float32), mask2.3 归一化方式选错,模型学到的全是幅度
这是我最开始栽的一个坑。星等是个对数量纲,不同变星的亮度差异巨大——食双星可能掉 3 个星等,而某些小振幅变星只有 0.02 星等。如果我直接喂原始星等,网络会优先学“幅度”,把所有大振幅的都归成一类,把所有小振幅的归成另一类。第一次训练出来的混淆矩阵简直惨不忍睹,模型把大振幅食双星和长周期米拉变星混成一团,只因为它们的峰谷差都很大。
正确做法是逐条曲线做标准化。我的处理是:先把星等转成流量(或者直接取负星等),然后除以该曲线的峰值流量,让最大亮度恒为 1,再做一次减均值除标准差。这样既保留了相对形态,又把幅度信息压掉了。
等一下,这里有个细节值得多说一句。完全压掉幅度真的好吗?不一定。有些类别之间,振幅本身就是判别特征——大振幅米拉型和小振幅半规则型,形态相似度挺高。所以我的做法是双通道输入:通道一放归一化后的形态序列,通道二放一条常数序列,常数取该曲线的原始振幅(标准化后)。这相当于给网络一个全局标量提示,让它自己决定要不要用这个信息。实测这个改动让米拉型与半规则型的混淆率降了大约 6 个百分点。
3. 网络骨架的取舍:1D-CNN的每一层该长什么样
3.1 卷积核尺寸和感受野的一笔账
网络结构我没有一上来就堆很深。200 个 bin 的输入,先用大核再逐层缩小,是我试下来最省参数又有效的配置。这里要说清楚感受野的账怎么算。
单层卷积的感受野:RF = K(K 为核宽,stride=1,无空洞)。多层堆叠时,感受野的增量是 (K-1) 乘以之前所有层的步长乘积。在 stride 全为 1 的情况下,总感受野就是各层 (K-1) 之和再加 1。
以我的配置为例:第一层核宽 7,第二层 5,第三层 3,感受野 = 1 + 6 + 4 + 2 = 13。这只能看到 13 个 bin,相当于一个相位周期的 6.5%,太窄了,看不到完整形态。所以我继续堆两层核宽 3 的卷积,感受野变成 13 + 2 + 2 = 17。还是不够。
这里就遇到一个取舍:要么堆更多层(参数增加、梯度传播变难),要么用 stride=2 的池化快速扩大感受野,要么用大核。我最后的选择是前两层大核(15 和 7),然后接两个 stride=2 的最大池化,感受野能达到接近 60 个 bin,也就是 30% 的相位周期。这个量级刚好够覆盖一个食变凹陷的完整宽度。
为什么不直接用核宽 51 的单层大核?参数是 51×通道数,其实不算多,但大核对局部尖锐特征的响应会被平均掉——食双星的次级凹陷只有几个 bin 宽,大核滑过去几乎没反应。小核堆叠才是兼顾局部细节和全局视野的解法。
实际用的结构大概是这样:
import torch.nn as nn class LightCurveCNN(nn.Module): def __init__(self, n_classes): super().__init__() self.features = nn.Sequential( nn.Conv1d(2, 32, 15, padding=7), nn.BatchNorm1d(32), nn.ReLU(), nn.Conv1d(32, 64, 7, padding=3), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(2), # 200 -> 100 nn.Conv1d(64, 128, 5, padding=2), nn.BatchNorm1d(128), nn.ReLU(), nn.MaxPool1d(2), # 100 -> 50 nn.Conv1d(128, 128, 3, padding=1), nn.BatchNorm1d(128), nn.ReLU(), nn.AdaptiveAvgPool1d(1), ) self.head = nn.Sequential( nn.Flatten(), nn.Dropout(0.3), nn.Linear(128, 64), nn.ReLU(), nn.Linear(64, n_classes), ) def forward(self, x): return self.head(self.features(x))3.2 池化层、全局平均池化与残差连接
池化这块,最大池化和平均池化我都试过。结论是:池化用最大,收尾用平均。中间层用最大池化,是因为光变曲线里的判别特征往往体现为“尖锐的极值”——食变的谷底、RR 型的峰顶,这些是稀疏但关键的位置,平均池化会把它们稀释掉。最后的全局层用自适应平均池化,是为了把所有局部响应汇总成一个稳定的全局描述子,同时避免全连接层参数爆炸。
残差连接要不要加?我的实验结论是:五层以内不用加,加到七八层以上不加会明显退化。我现在的配置是 5 个卷积层,没加残差,训练稳定。但如果你想把输入长度拉到 2000 个 bin 去保留更精细的形态,层数必然要加深,这时候残差连接就是必需品了。
还有一个我加了之后效果不错的模块:通道注意力。在每次池化之前,给每个通道算一个权重,让网络自己决定哪些卷积分支更重要。实现上就是一个全局平均池化接两层全连接再接 sigmoid,多出来几百个参数,但让宏平均 F1 提升了约 1.5 个百分点。代价是训练慢了一点,收敛轮数多了大概 15%。
3.3 分类头与损失函数的搭配
分类头不要搞太复杂。我见过有人在最后接三层全连接,每层 512 维,结果严重过拟合,训练集准确率 99%,验证集只有 78%。我的做法是全局池化后直接 dropout 0.3,接一个 64 维的隐层,然后输出。
损失函数的选择和类别分布直接相关。如果你的数据集里最大类和最小类的样本数差在 10 倍以内,交叉熵就够了。超过 10 倍,就得动点手脚。关于这一点在下一节详细说。
另外提一个容易忽略的点:标签平滑。天文标签本身就有噪声,很多样本的类别归属在专家之间都有争议。用 0.05 到 0.1 的标签平滑,等于告诉模型“别对标签太自信”,实测能降低过拟合,验证集损失曲线更平滑。我用的是 0.1。
4. 长尾分布下的训练:我试过的几种打法
4.1 类别加权、过采样与focal loss的实测排序
变星类别的分布是典型的长尾。常见的光变类型占了样本的绝大部分,而那些稀有类型可能只有几百条甚至几十条。我处理的数据集里,最大类和最小类样本量差了将近 200 倍。
三种手段我都试过,说说真实效果:
类别加权是最省事的。给每个类别的损失乘一个权重,权重取该类样本数的倒数再归一化。改动只有一行代码,好处是不改变数据分布,坏处是极端稀有的类别权重会非常大,导致模型对这类样本过度敏感,产生大量误报。我试过权重按 1/sqrt(N) 来算,比纯倒数温和一些,效果更好。
过采样是把稀有类复制多份。简单粗暴,但直接复制会导致严重过拟合——同一条曲线反复出现,模型把它背下来了。改进版是在复制时加随机扰动(相位平移、加噪声),效果明显好转。
Focal loss是我最终采用的。它通过调制因子降低易分样本的损失贡献,让模型把注意力集中在难分样本上。对长尾数据特别合适,因为稀有类往往就是难分的那些。参数上,gamma 取 2,alpha 按类别频率设置。
三者组合的实际效果对比(同一验证集,宏平均 F1):
| 策略 | 宏平均 F1 | 最小类召回 |
|---|---|---|
| 纯交叉熵 | 0.71 | 0.32 |
| 类别加权(1/N) | 0.78 | 0.55 |
| 类别加权(1/√N) | 0.81 | 0.61 |
| 过采样 + 扰动 | 0.80 | 0.58 |
| Focal loss | 0.83 | 0.66 |
| Focal loss + 轻度加权 | 0.85 | 0.69 |
最后一行是我现在用的配置。Focal loss 打底保证难样本被关注,轻度加权(权重取 1/√N 的平方根再归一化,更温和)作为补充,避免极端类别被完全忽略。
4.2 学习率调度与批大小的配合
批大小我定在 64。试过 32 和 128,32 的时候梯度噪声大,收敛慢但最终效果略好;128 的时候训练快,但小类别的梯度容易被大类别淹没,最小类召回掉了 4 个百分点。64 是个折中。
学习率用 1e-3 起步,配余弦退火。这里有个细节:warmup 很有必要。前 3 个 epoch 用线性 warmup 从 1e-5 爬到 1e-3,能显著降低早期训练不稳定的概率。不加 warmup 的时候,我遇到过好几次 loss 直接飙到 nan,尤其是 batch 里有极端振幅样本的时候。
优化器用 AdamW,权重衰减 1e-4。为什么不用 SGD?试过,收敛需要的轮数多得多,而且对学习率非常敏感,调参成本太高。AdamW 在默认参数下就能跑出不错的结果,对反复做消融实验很友好。
早停的判据不要用准确率,用验证集的宏平均 F1。理由是准确率会被大类主导,模型只要把大类分对,准确率就能很好看,但小类可能全军覆没。我设置的是连续 8 个 epoch 宏 F1 没有提升就停。
4.3 数据增强清单和它们的副作用
光变曲线的数据增强比图像有意思,因为很多增强是有物理意义的。我用的清单:
- 随机相位平移:把整条序列循环移位随机步数。物理上完全等价,因为相位起点本来就是任意的。这个增强几乎零成本,收益最大。
- 随机周期缩放:把周期乘以 0.98~1.02 的随机因子再重新折叠。模拟周期测量误差。这个增强要慎用,缩放太多会破坏形态。
- 加性高斯噪声:按曲线的信噪比水平加噪声。模拟不同观测条件。
- 随机丢点:随机把 5%~15% 的 bin 置为插值值,同时更新掩码。模拟观测空档。
- 幅度缩放:把归一化后的形态乘以 0.9~1.1。模拟测量系统误差。
副作用要说清楚。随机周期缩放用过头会伤害周期性强的类别。我一开始把缩放范围设成 ±5%,结果天琴座 RR 型 ab 和 c 亚型的区分度明显下降——因为这两类的主要差异就是周期和波形的关系,缩放把这种关系搞乱了。后来收到 ±2%,问题解决。
随机丢点要控制比例。丢太多,插值出来的形态是假的,模型会学到虚假模式。我上限设在 15%,超过这个比例,验证集指标开始掉。
还有一个反直觉的发现:水平翻转(把曲线左右镜像)不能用。图像里翻转是常规增强,但光变曲线的上升沿和下降沿不对称性恰恰是重要判别特征(RR 型就是快速上升慢速下降)。翻转等于把这个特征反过来了,会直接教坏模型。这一条我踩过,加进去之后验证集指标掉了 3 个百分点,查了半天才发现是这个增强的问题。
5. 评估环节最容易被糊弄过去的地方
5.1 宏平均F1背后的每类召回
模型训完,第一眼看什么?很多人看整体准确率。我不看,因为没意义。在一个长尾数据集上,把所有样本都预测成最大类,准确率可能就有 60%。这种模型一文不值。
我看的是混淆矩阵加每类召回。宏平均 F1 是每类 F1 的算术平均,它对小类敏感,是个更诚实的指标。但我还会单独把每类召回拉出来看,因为宏 F1 也会掩盖问题——假如某一类召回只有 0.2,但精确率很高,F1 可能还有 0.33,看着不算太烂,实际这类基本废了。
我的验收标准是分层的:大类召回要求 0.95 以上(这些是自动化的主力),中等类 0.85 以上,稀有类 0.7 以上。达不到的类别,单独拎出来分析原因,而不是整体重训。
5.2 三组高频混淆对的成因剖析
跑了几十次实验,混淆矩阵里反复出现的就是那几对,说说成因和我怎么处理的。
第一对:天琴座 RR 型 ab 与 c 亚型。这两类形态相似,都是短周期、较大振幅的脉动变星,主要差异在波形对称性和周期分布。ab 型上升沿陡、下降沿缓,c 型接近正弦。在相位折叠图上看,差异其实挺细微的。我通过提高 bin 数(从 200 提到 400)让波形细节更清晰,混淆率降了一些。另外把周期作为额外特征拼进分类头,效果更明显——因为这两类的周期分布本来就有差异,之前的信息被我归一化步骤给压掉了。
第二对:食双星中的分离型与半接型。这两类的区别在于是否有物质交流,光变曲线上表现为次级凹陷的形状和深度差异。有些样本的次级凹陷非常浅,信噪比低的时候几乎看不见。这一对靠单条曲线很难彻底分开,我的处理是接受一定的混淆率,把置信度低的样本推给人工,而不是硬调模型。硬调的结果往往是这类分对了,另一类崩了。
第三对:米拉型与半规则型。这两类的边界在学术上本身就模糊——半规则型有时候会表现出接近米拉的周期性和振幅。标注数据里本身就存在标签噪声。我的做法是对这一对做标签合并试验,看看合并成一个“长周期脉动”大类后整体指标如何。结果显示合并后宏 F1 提升明显,说明模型确实分不开,那就承认现实,把粒度放粗。
这个思路值得强调:分类粒度和模型能力要匹配。如果你的数据质量和模型能力只能支撑到“族”级别,就不要硬追求“亚型”级别,那只会制造虚假的精确度。
5.3 置信度阈值与人工复核队列的设计
自动化分类的价值不在于全自动,而在于把人力集中到刀刃上。所以我设计了一个两阈值机制。
模型输出 softmax 概率,取最大概率作为置信度。置信度高于 0.9 的,直接采用预测标签;低于 0.5 的,直接进人工队列;中间的灰色地带,按类别分别设定阈值——大类阈值可以低一些(因为大类样本多,错一点无所谓),稀有类阈值要高(错一个稀有类的代价大)。
实测下来,大约 72% 的样本能落在高置信区,且这批样本的人工抽检准确率在 96% 以上。剩下 28% 进人工,人力成本降到原来的三成不到。
还有一个细节:置信度需要校准。神经网络的 softmax 输出往往过于自信——预测概率 0.95 的样本,实际正确率可能只有 0.85。我用温度缩放做了校准,在验证集上拟合一个温度参数 T,推理时把 logits 除以 T 再 softmax。校准之后,置信度和实际准确率的对应关系好了很多,阈值设定才有依据。
6. 把实验代码变成能跑的流水线
6.1 批处理推理的内存与显存账
notebook 里跑通了,不代表能处理百万级数据。推理阶段第一个撞上的是内存。
一条曲线 200 个 bin、双通道、float32,就是 200×2×4 = 1600 字节。一百万条是 1.6 GB,听起来还好。但预处理阶段要做相位折叠、分箱、插值,如果一次性把中间结果全 load 进内存,实际占用会翻好几倍。我的做法是分块流式处理,每块 5000 条,处理完立刻转成定点数组存盘,内存峰值控制在 2 GB 以内。
显存这边,推理时 batch size 可以设得比训练大很多,因为没有反向传播的中间激活。我设的是 512,一张中等显存的卡跑满负载没问题。但要注意,推理时必须包在torch.no_grad()里,不然显存会持续增长直到 OOM——这个坑我踩过,跑到一半崩掉,查了半天才发现是忘了加装饰器。
还有一个性能点:把模型转成推理模式。model.eval()不只是关掉 dropout 和 batchnorm 的统计更新,它还会影响某些层的计算路径。忘了加这一步,推理结果会和验证时对不上,这个 bug 极其隐蔽,因为结果只是“稍微差一点”,不会报错。
6.2 数据漂移与模型版本管理
模型上线之后最容易被忽略的问题是数据漂移。望远镜的 detector 会老化,滤光片会更换,观测策略会调整,这些都会让新数据的分布和老数据产生偏移。一个在旧数据上训练到 0.85 宏 F1 的模型,两年后可能只有 0.75。
我的应对是三个动作。一是定期抽检:每个月从新数据里随机抽 500 条,人工标注后算指标,画一条时间序列曲线。二是监控输入分布:统计每条曲线的振幅分布、采样点数分布、信噪比分布,和训练集做对比,用简单的 KL 散度或者分位数对比就能发现异常。三是保留模型版本:每次重训都存下模型权重、训练数据的时间范围、超参数配置。这样出问题能快速回滚,也能对比不同版本的差异。
重训的触发条件我设了两条:抽检指标下降超过 5 个百分点,或者输入分布偏移超过阈值。任意一条触发就启动重训流程。
6.3 与人工标注员的协作接口
最后说说流程上的事。自动化分类不是替代人,是重新分配人的注意力。所以工具设计要以标注员为中心。
我做的界面很简单:左边显示相位折叠后的曲线图(带误差棒),右边显示模型给出的 top-3 类别和对应置信度,下面是几个快捷键对应常见类别。标注员看一眼曲线,如果同意模型的首选,按回车确认;不同意就按对应类别的键覆盖。所有覆盖操作都被记录,这些记录是下一轮重训最宝贵的训练数据——它们是模型真正犯错的地方。
有个经验值得分享:让标注员看到模型的理由。我给界面加了一个小模块,显示模型在曲线的哪个相位区间激活最强(用类别激活映射算出来的)。标注员反馈说这个很有用,因为当模型关注的位置明显不合理时,他们会更警惕。这实际上是一种人机互补——模型提供候选,人提供把关,而模型的“注意力”又反过来提示人去看容易忽略的地方。
数据回流也要设计好。所有人工修正过的样本,打上“已复核”标记,进入一个专门的缓冲区。重训时这个缓冲区的样本会被重复采样,权重更高。这样模型能快速从错误中学习,而不是等到下一批大规模标注。
我个人在实际操作中的体会是,这套流程里最难的从来不是网络结构,而是数据清洗的规范和标签一致性的维护。结构可以抄,超参数可以调,但如果训练集里同类样本的标签标准前后不一,再好的模型也学不出稳定的决策边界。我现在的做法是给每一类写一份“判别标准文档”,把边界情况写清楚,新标注员上岗前先做 200 条的一致性测试,和已有标注结果对比,一致率低于 90% 的不让上岗。这个办法笨,但很有效。
另外分享一个小技巧:如果你的数据量不足以从头训练,可以先用大量无标签的光变曲线做自监督预训练——比如让模型预测曲线被遮挡的一段,或者判断两个随机裁剪片段是否来自同一条曲线。这套做法在时序数据上效果不错,我试过用 30 万条无标签数据预训练,再用 2 万条有标签数据微调,宏 F1 比直接从零训练高了 4 个百分点左右,对小类别尤其明显。