近红外光谱回归分析:深度学习模型对比与实验全解析
简介:一份面向计算机专业学生、用于毕设或课程作业的深度学习教育项目,聚焦近红外光谱数据的回归分析。近红外光谱属于非破坏性检测技术,项目将深度学习模型应用于成分或性质预测,是数据科学领域的典型实践。压缩包共9个文件,以8个Python脚本为主,辅以1个Markdown说明文档,整体大小仅27KB,代码结构清晰。脚本中实现了ConvNet、SpectFormer、DeepVit、VitNet等模型,并提供对应的迁移学习版本,便于对照不同网络结构在光谱回归任务上的表现。已有235人浏览学习。通过阅读这些源码,可以掌握数据预处理、模型搭建、训练验证等完整流程,也可直接复用或修改模型用于自己的实验或课题,适合想快速上手深度学习回归项目的学生参考。
1. 近红外光谱数据回归分析模型:这套代码包不是黑匣子,是一组可复现的对照实验
拿到「基于深度学习的近红外光谱数据回归分析模型」这套代码包时,我第一反应是:又一个调参模板?拆完才觉得,它最值钱的不是某一个模型有多先进,而是同一份数据上同时给了四条结构路线——ConvNet、VitNet、DeepVit、SpectFormer——每条还带一个带 TL 后缀的迁移学习变体。近红外光谱回归任务正好卡在几个老问题上:波长点太多、样本量太少、化学吸收带相互重叠,单靠一个网络很难把「为什么这样设计」讲清楚。这套结构把最常见的解题路径一次铺开了。适合两种人:一种是做计算机类毕设、课程设计需要完整实验对比的同学,另一种是手里已有光谱数据、想快速评估不同网络上限在哪的工程师。文件列表很干净,每个网络结构一个 py 文件,带 TL 的变体单独拆开,README.md 负责串联入口,按顺序跑就能复现。
2. 从近红外光谱 CSV 到可训练张量:预处理顺序、划分策略与回归指标
近红外光谱的输入形式和图像差别很大。图像是二维空间信息,光谱是一条一维曲线,横轴是波长(常见 780–2500 nm),纵轴是吸光度或反射率。拿到手的数据,常见格式是 CSV 表格存所有样本:每行一个样本,第一列是目标值(水分、蛋白、某成分含量),后面几百上千列是波长点的吸光度;也有转置存放的。第一步不是忙着建模,而是确认数据的存储方向、缺失值情况和量纲是否一致。近红外吸收主要来自 O-H、N-H、C-H 等含氢基团的倍频与合频振动,谱带重叠严重,这决定了后面必须做平滑和导数处理,否则模型很容易被基线漂移和高频噪声带偏。
2.1 把数据读进来:每行一个样本,字段名就是波长点
先用 pandas 把 CSV 读进来,打印 shape 和缺失值。光谱数据经常出现两种情况:某一列(某个波长点)全为 0,或者某几行样品吸光度异常偏高,后者多半是样品表面状态不同导致的散射差异,不能直接删,要靠预处理矫正。
import pandas as pd import numpy as np # 常见近红外光谱 CSV:每行一个样本,第一列是目标值,其余列是波长点 data = pd.read_csv("spectra.csv") print("原始形状:", data.shape) # (样本数, 波长点数 + 1) print("缺失值统计:\n", data.isna().sum().sum()) # 分离目标值和光谱矩阵 y = data.iloc[:, 0].values.astype("float32") # 第一列是浓度/含量 X = data.iloc[:, 1:].values.astype("float32") # 其余列是吸光度 # 波长点数量直接决定模型输入维度,记下来后面设置网络要用 seq_len = X.shape[1] print(f"样本数: {X.shape[0]}, 波长点数: {seq_len}")这段代码的逻辑是先把数据拆成X和y,y是回归目标,X是光谱矩阵,形状是(N, L)。seq_len后面做 ConvNet 和 Transformer 都要用到,一维卷积的输入长度和 ViT 切 patch 的总长都由它决定。注意 dtype 统一转成 float32,PyTorch 默认浮点类型就是 float32,不转会在 loss 计算时出现类型不匹配的报错。缺失值处理上,我一般先看缺失比例,低于 1% 直接按列均值填充;高于 5% 就查仪器采集记录,大概率是样品问题。
2.2 预处理三步走:SG 平滑、SNV、一阶导数,顺序别反
近红外光谱预处理是典型的「做对了不出彩,做错了全翻车」的环节。最常见的组合是 Savitzky-Golay 平滑 + SNV(标准正态变换)+ 一阶导数。SG 平滑负责去高频噪声,SNV 逐行消除基线漂移和光程差异,一阶导数进一步消掉基线平移。顺序不能反:先平滑再求导,否则噪声会被导数放大。
from scipy.signal import savgol_filter # 1) SG 平滑:窗口 11、多项式阶数 2,对每一条光谱单独做 X_sg = savgol_filter(X, window_length=11, polyorder=2, axis=1) # 2) SNV:按样本行做,消除基线漂移和光程差异 X_snv = (X_sg - X_sg.mean(axis=1, keepdims=True)) / X_sg.std(axis=1, keepdims=True) # 3) 一阶导数:用 SG 在平滑的同时求导,窗口可以小一点 X_deriv = savgol_filter(X_snv, window_length=9, polyorder=2, deriv=1, axis=1) print("预处理后形状:", X_deriv.shape, "均值≈", X_deriv.mean().round(6))逻辑说明:三个变换全部作用于axis=1,也就是对每条光谱内部做计算。window_length控制平滑窗口覆盖的波长点数,窗口越大越平滑,但会把窄的吸收峰一起抹掉,近红外谱峰的半高宽通常在 10–30 nm 左右,按采样间隔折算成点数后,窗口取 9–15 是安全区。deriv=1表示一阶导数,如果二阶导数可以消掉基线斜率,但对噪声更敏感,毕设阶段先用一阶就够。预处理后的均值应该接近 0,如果均值还明显偏离,说明 SNV 的 axis 选反了,大概率是按列做的。
这里补一句 C++ 的角色:如果光谱矩阵到了几十万行、几百个波长点这种规模,Python 的 pandas 循环会卡在 IO 和逐行运算上,用 C++ 写数据清洗和批量切分会快不少;但毕设和课程设计这个数据量,Python 的 scipy 和 pandas 足够,不需要额外引入 C++ 模块。
2.3 划分数据集:近红外数据不建议直接随机切
很多人在这一步直接train_test_split(random_state=42),这是近红外回归里最容易埋雷的地方。近红外样品往往来自同一批次、同一工艺,相邻编号的光谱差异极小,随机切分会把非常相似的样本同时分进训练和测试集,测试集 R² 虚高到 0.99,换个批次数据直接崩。近红外领域的通行做法是按光谱距离划分,Kennard-Stone 算法是主流,思路是每次选离已选样本最远的样本进训练集,让测试集尽可能独立。
from sklearn.model_selection import train_test_split from sklearn.metrics import r2_score, mean_squared_error # 快速验证版本:先固定随机种子,保证实验可复现 X_train, X_test, y_train, y_test = train_test_split( X_deriv, y, test_size=0.2, random_state=42 ) # 正式实验建议:用 Kennard-Stone 按光谱距离划分 # 常见做法是用 ks 库或自己实现距离贪婪采样,这里给出替代方案 def ks_split(X, test_size=0.2): n = X.shape[0] n_test = int(n * test_size) selected = [np.argmin(np.linalg.norm(X - X[0], axis=1))] while len(selected) < n - n_test: dist = np.min(np.linalg.norm(X[:, None, :] - X[selected], axis=2), axis=1) selected.append(int(np.argmax(dist))) mask = np.zeros(n, dtype=bool) mask[selected] = True return X[~mask], X[mask], y[~mask], y[mask]逻辑说明:train_test_split只适合快速摸流程,正式跑实验矩阵必须换成 KS 划分或者至少按批次分组。上面的ks_split是 Kennard-Stone 的简化实现,np.argmax(dist)每次挑离已选样本最远的光谱进训练集,保证测试集是「模型没见过的光谱范围」。这个方法在近红外文献里很常见,缺点是计算距离矩阵时内存占用高,样本量超过两三万就建议用分批近似。数据划分完后,回归指标要同时看三个,不要只盯 R²。
| 指标 | 作用 | 近红外回归常用参考 |
|---|---|---|
| R² | 决定系数,看模型解释了多大比例的方差 | >0.95 很稳,0.90–0.95 够用 |
| RMSEP | 测试集预测均方根误差,看绝对误差 | 越小越好,必须结合 y 的量纲看 |
| RPD | y 的标准差除以 RMSEP | >3 优秀,2.5–3 可用,<2 谨慎 |
RMSEP 里的 P 指 Prediction,专门评价测试集预测,比训练集 RMSE 更有说服力。RPD 这个指标在近红外领域几乎是默认要报的,它回答的是「模型误差比样品本身的波动小了多少」,写论文和答辩时比单纯报 R² 更能堵住追问。
注意:预处理里的统计量只能从训练集算出,再用到测试集上。SNV 本身逐样本独立,问题不大,但后面如果做标准化,均值方差必须来自训练集,否则就是数据泄漏,指标再好看也站不住。
3. ConvNet、VitNet、DeepVit、SpectFormer:四个模型家族怎么选,TL 后缀到底加了什么
文件列表里两组 py 的命名逻辑很清楚:ConvNet、VitNet、DeepVit、SpectFormer 是四个基础结构,带 TL 后缀的是各自对应的迁移学习变体。TL 基本可以断定是 Transfer Learning 的缩写,具体迁移策略和权重来源以 README.md 为准,但这个命名本身已经说明了一份完整的对比实验应该怎么组织——同一个结构,从头训练 vs 迁移学习各跑一组,正好回答答辩时最常被问的「迁移学习在你的任务里到底有没有用」。
3.1 ConvNet.py:一维卷积基线,先把下限做扎实
一维卷积是近红外光谱回归最朴素的深度学习方案,也是所有对比实验的「地板」。光谱输入从二维图像降成一维序列后,Conv1d的卷积核只在波长方向上滑动,感受野覆盖相邻的波长点。近红外吸收峰不是孤立点,而是一段连续的波段,卷积核能直接学习到「峰形 + 局部波段相关性」这类特征。
典型结构是:Conv1d → BatchNorm → ReLU → AdaptiveAvgPool1d → Linear,卷积核大小取 17 左右,对应约 17 个波长点的邻域,接近半个吸收峰的宽度。AdaptiveAvgPool1d(1)把整条光谱压成一个向量,替代全连接层处理序列长度,参数量小、不容易过拟合。这个结构跑出来的结果就是整组实验的基准线,Transformer 模型如果连这个下限都打不过,问题通常不在结构,而在数据预处理或超参。
3.2 VitNet.py 与 DeepVit.py:把光谱切成 patch 的 Transformer 路线
VitNet 的思路是把一维光谱切成长度相等的 patch,每个 patch 经过线性映射后加上位置编码,再送进 Transformer 编码器。近红外光谱里,同一个成分的吸收峰会出现在多个波段,相隔很远但相关性很强,Transformer 的自注意力机制正好擅长建模这种长程依赖。比如水的 O-H 吸收在 1400 nm 和 1900 nm 附近都有信号,纯 CNN 要靠堆层数才能把这两个位置的感受野连起来,Transformer 一层注意力就能直接关联。
DeepVit 从命名看是 VitNet 的加深加宽版本,差异大概率在 Transformer encoder 的层数(depth)和嵌入维度(embed_dim)上。这两个模型的对比要回答的问题是:在近红外这种中等长度序列上,加大模型容量能不能带来实打实的收益,还是只是把训练时间变长。光谱数据样本量通常只有几百到几千,DeepVit 非常容易过拟合,所以它的默认超参、正则化设置反而是这组实验里最有看点的部分。
3.3 SpectFormer.py:CNN 局部特征 + Transformer 全局依赖的混合
SpectFormer 从名字看是 Spect + Former 的组合,也是我拆这个包时最期待的文件。纯 CNN 的局部感受野高效但视野窄,纯 Transformer 的全局注意力强但容易在小样本上过拟合,混合结构是这两条路的折中:浅层先用一维卷积提取局部峰形特征,再把卷积输出的特征图切成 patch 或直接作为 token 序列送进 Transformer。
这个设计对近红外光谱是说得通的。卷积层输出的每个位置已经包含了邻近波段的局部信息,Transformer 在这些局部特征之上建模跨波段的依赖,语义上比直接用原始波长点做注意力更干净。SpectFormer 和 ConvNet 的对比实际是「局部特征 + 全局关系」和「纯局部特征」的对比,SpectFormer 和 VitNet 的对比则是「局部建模是否必要」的对比。三组模型互相参照,正好覆盖评审最关心的两个问题:结构设计有没有依据、实验有没有对照组。
3.4 带 TL 后缀的那一组:迁移学习到底迁移了什么
四个 TL 文件是这套代码包在毕设答辩时最有价值的部分。近红外深度学习最常见的困境是:光谱数据量太少,从头训练一个容量稍微大一点的网络就过拟合。迁移学习的常规做法是先在更大规模的数据上预训练,再在目标任务上微调。但要注意,ImageNet 预训练权重是二维三通道输入,近红外光谱是一维输入,输入层通道数和序列长度都对不上,直接加载预训练权重一定会报 shape mismatch。
所以这里的 TL 变体,迁移的通常不是 ImageNet 权重本身,而是这类结构的「训练策略」——比如冻结浅层、只微调深层,或者把同一结构在公开光谱数据上做自监督预训练再微调。具体是哪种,以 README.md 的实现为准,但你可以把 TL 变体理解成「同一个网络换一种训练方式」,而不是「换一个网络」。这张表是我拆完文件后对每个脚本的定位判断:
| 文件 | 结构路线 | 在实验矩阵中的角色 |
|---|---|---|
| ConvNet.py | 1D CNN 基线 | 定下限,作为所有模型对比的基准 |
| ConvNetTL.py | ConvNet + 迁移学习 | 看预测练/微调策略对 CNN 是否有提升 |
| VitNet.py | 光谱切 patch 进 Transformer | 纯 Transformer 路线的基线 |
| VitNetTL.py | VitNet + TL | 看迁移学习能否缓解 Transformer 过拟合 |
| DeepVit.py | 加深加宽的 VitNet | 验证模型容量在光谱数据上的上限 |
| DeepVitTL.py | DeepVit + TL | 深度模型与迁移学习是否兼容 |
| SpectFormer.py | CNN + Transformer 混合 | 局部特征 + 全局依赖的混合结构 |
| SpectFormerTL.py | SpectFormer + TL | 混合结构结合迁移学习后的最终效果 |
提示:拆这类代码包时,最忌直接把文件名当结论。先打开 README.md,确认每个 TL 文件里预训练权重的来源和微调策略,再决定你的论文里怎么写「迁移学习」这一章。
4. 把训练跑起来:命令行入口、六个关键超参与三组实验矩阵
环境装好后,这套代码的入口设计比较直白:每个模型一个 py 文件,直接python 文件名.py就能启动训练。动手深度学习的人都知道,跑通一个骨架半小时,排一个环境 bug 一晚上。近红外回归任务用 PyTorch 实现是当前最常见的方案,torch 1.13 以上版本对Conv1d和 Transformer Encoder 的支持都很成熟,不需要额外装复杂的库。
4.1 环境与入口:先跑通 README.md,再换你自己的数据
建议环境是 Python 3.8+,核心依赖包括 numpy、pandas、scipy、scikit-learn、torch。命令行入口的一般形式如下,实际参数名以 README.md 为准,但结构基本逃不出这套:
# 以 SpectFormer 从头训练为例 python SpectFormer.py --data ./data/spectra.csv \ --target column_name \ --epochs 80 --batch_size 32 --lr 1e-3 \ --patch_size 64 --embed_dim 256 --depth 8这个命令的逻辑是:--data指定光谱 CSV 路径,--target指定目标值列名,后面四个是模型的四个核心超参。第一次运行时先把--epochs改小到 10,确认数据能读进来、loss 能下降,再跑完整训练。这么做的好处是避免数据格式问题在跑了 30 个 epoch 之后才暴露,白白浪费时间。
GPU 不是必须的。近红外光谱序列长度在几百到一千左右,样本量通常不过万,这类规模的模型在 CPU 上跑 80 个 epoch 也就是几十分钟到一两个小时。如果你的数据量特别大,再考虑租云平台或者用 GPU 实例,毕设场景完全不需要一上来就上多卡。
4.2 六个关键超参:值和含义要对得上
调参之前先搞清楚每个参数在这个任务里的物理含义。这六个参数是近红外光谱 Transformer 和 CNN 训练里最常动的,优先级从高到低排列如下:
| 参数 | 建议范围 | 对光谱回归的影响 |
|---|---|---|
| batch_size | 16–64 | 光谱样本差异大,batch 太小 loss 抖,太大泛化下降 |
| lr | CNN 1e-3,Transformer 1e-4 | Transformer 对学习率极敏感,1e-3 大概率发散 |
| epochs | 60–120,配早停 | 光谱任务收敛慢,固定 epochs 不如早停可靠 |
| patch_size | 32–128 | 过小序列太长、注意力稀疏,过大丢失局部峰形 |
| embed_dim | 128–512 | 决定 token 的表示容量,小数据用 128–256 更稳 |
| depth | 4–12 | 层数过深在小样本上直接过拟合,4–6 层起步 |
这里有一条血泪经验:Transformer 类模型在光谱上的学习率玄学比图像还严重。图像任务 1e-3 能跑,近红外 Transformer 同样用 1e-3 经常 loss 前 20 个 epoch 纹丝不动,降到 1e-4 才开始下降。原因是光谱序列的数值范围和图像像素差异很大,Adam 的一阶矩估计在稀疏梯度下容易走偏。所有模型先固定随机种子,再在同一套超参下跑对比,否则你分不清是结构差异还是随机种子差异。
4.3 三组实验矩阵:从基线到上限一次跑齐
这套代码最省心的设计是它天然支持三组实验。第一组是四个基础模型从头训练对比,回答「同样预算下哪种结构强」;第二组是四个 TL 模型对比,回答「迁移学习能不能涨点」;第三组是消融实验,挑一个模型改 patch_size、embed_dim、depth,回答「你论文里的参数为什么取这个值」。
| 实验组 | 模型 | 要回答的问题 |
|---|---|---|
| A 组:从头训练基线 | ConvNet、VitNet、DeepVit、SpectFormer | 四种结构的默认能力排序 |
| B 组:迁移学习对比 | ConvNetTL、VitNetTL、DeepVitTL、SpectFormerTL | 预测练/微调策略在同一结构上的增益 |
| C 组:消融实验 | 任选一模型,改 patch_size / embed_dim / depth | 每个超参对结果的贡献有多大 |
实验矩阵的管理建议用一张 Excel 或 CSV 记录每次运行的参数和结果。近红外实验跑一次不长,但架不住次数多,不记录的话三天后就分不清哪组是哪组了。每组实验固定同一个随机种子,这是整套实验可复现的前提。
4.4 训练日志怎么判:早停、最优权重与 loss 曲线
训练脚本骨架可以抽象成下面的逻辑,不管包里的实现细节如何,好的训练循环都应该包含固定种子、保存最优权重、早停三件事:
# 训练骨架:固定种子、保存最优、早停 import torch import numpy as np def set_seed(seed=42): np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) set_seed(42) model = build_model_from_args(args) # 替换成包里的 ConvNet / SpectFormer 等 optimizer = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=args.epochs) best_val = float("inf") patience = 0 for epoch in range(args.epochs): train_loss = run_one_epoch(model, optimizer, train_loader, mode="train") val_loss, val_r2 = run_one_epoch(model, optimizer, val_loader, mode="eval") if val_loss < best_val: best_val = val_loss torch.save(model.state_dict(), "best_checkpoint.pt") patience = 0 else: patience += 1 if patience >= 15: print(f"early stop at epoch {epoch}") break逻辑说明:set_seed(42)固定了 numpy 和 torch 的随机源,保证两次运行结果一致,这是实验可复现的地基。AdamW比 Adam 多了权重衰减解耦,在光谱小数据上更不容易过拟合,weight_decay=1e-4是经验值。CosineAnnealingLR让学习率随训练周期余弦下降,前期大步探索、后期小步收敛,比固定学习率好用。
判断训练是否正常,重点看两个曲线:train loss 持续下降说明模型在学,val loss 下降后反弹说明过拟合开始,val loss 震荡不降说明学习率过高或 patch_size 不合适。torch.save(model.state_dict(), "best_checkpoint.pt")保存的是权重而不是整个模型,加载时要用model.load_state_dict(torch.load("best_checkpoint.pt")),这是 PyTorch 的推荐做法,迁移到新数据时也只需要换权重文件。
5. 近红外光谱深度学习避坑手册:五个翻车现场与排查思路
近红外深度学习的坑,九成不在网络设计,而在数据口径。这一章是这套代码最常见的五个翻车场景,每条都是我见过或自己踩过的真实案例,按「现象 → 原因 → 解决」写清楚。
5.1 随机划分让 R² 虚高到 0.99,换个仪器就翻车
现象:直接用train_test_split(random_state=42)划分数据,测试集 R² 到了 0.99,loss 曲线漂亮得像教材里的示意图。你以为模型已经练成,结果用到新采集的样品上,预测值和实测值能偏出半个数量级。
原因:近红外样品往往来自同一批次、同一工艺,相邻编号的光谱差异极小。随机切分会把非常相似的样本同时分进训练和测试集,模型等于变相记住了样品编号附近的数据,这是典型的数据泄漏。论文复现时别人用同样的数据却复现不出这个 R²,问题不在代码,在划分方式。
解决:正式实验用 Kennard-Stone 按光谱距离划分训练集和测试集,让测试集覆盖模型没见过的光谱范围。如果只是为了快速验证流程,随机划分加固定种子可以,但要把「随机划分 R² 虚高」写进论文的局限性。我从那以后每次拿到近红外数据,第一件事就是问一句:这批样本是不是同一批采集的?是的话直接放弃随机划分。
5.2 axis 选错:把每一列波长做归一化,光谱形变被抹平
现象:预处理代码里写了一句X = (X - X.mean(axis=0)) / X.std(axis=0),跑完模型 R² 和 RMSE 都还能看,但换一组新数据后预测值整体偏移,而且模型怎么调都救不回来。
原因:axis=0是沿着波长方向统计,每一列做标准化,这会抹掉同一样本内部不同波段的相对强弱。而近红外吸光度最有用的信息恰恰是同一光谱内部不同波段的相对关系——SNV 和标准正态变换都要求按样本行计算,也就是axis=1。搞反方向后,光谱的峰形被破坏,模型学到的是被扭曲后的特征。
解决:先打印数组形状确认(N, L),预处理一律用axis=1对每个样本内部操作。代码里写上注释,标注「按样本行做,不改方向」;如果用了标准化而非 SNV,均值方差必须来自训练集,测试集复用同一组统计量。
5.3 Transformer 训练抖得像心电图,loss 就是下不来
现象:VitNet 或 SpectFormer 一上来 train loss 就高频抖动,把学习率降到 1e-4 之后还在抖,或者 loss 掉到一定程度就卡死不动,val loss 完全不跟。
原因:三个最常见的原因叠加——patch_size 切得太小导致序列过长,Transformer 的注意力在长序列上不稳定;学习率没有 warmup,前几个 step 就把权重冲乱;输入特征没有归一化,数值范围过大让注意力权重的 softmax 饱和。
解决:patch_size 取 64 以上,近红外几百个波长点切出 8–16 个 patch 就够;学习率降到 1e-4 并加 warmup,前 10 个 epoch 线性升到目标学习率;输入过一遍预处理保证均值接近 0。如果还抖,就把 depth 从 8 降到 4,减少一层注意力就少一层不稳定因素。
5.4 R² 很好看,RMSE 却大得离谱,边界样本在作怪
现象:测试集 R² = 0.96,看起来很不错,RMSEP 却比工业标准高一大截。画出预测-实测散点图发现,大部分点贴合对角线,只有一两个极端样本的残差特别大。
原因:R² 对线性拟合比例敏感,RMSE 对平方误差敏感。一两个离群点能把 RMSE 拉爆,但 R² 仍然好看。这是近红外回归报告里最常见的「指标打架」:只报 R² 会高估模型,只报 RMSE 会低估模型。真实原因往往是测试集里混入了极端浓度样本,或者个别样品表面状态异常导致光谱失真。
解决:三个指标一起报,R²、RMSEP、RPD 缺一不可。画出残差图,把偏差超过 3 倍标准差的样本单独标出来。如果是记录错误就修正,如果是真实极端工况,要把这类样本单独评估,或者补充更多极端样本进训练集。论文里只报 R² 不报残差,答辩时被问住几乎是必然的。
5.5 TL 版本跑不起来:一维光谱加载不了二维预训练权重
现象:想用 ImageNet 预训练权重做迁移学习,加载权重时直接报 shape mismatch;强行忽略形状错误后,训练完全不涨,甚至从头开始 loss 也不降。
原因:ImageNet 预训练权重是二维三通道输入,近红外光谱是一维输入,输入层通道数和序列长度都对不上。能够匹配的只有中间层的结构,输入层和输出层必然报错。就算跳过输入层,预训练模型学到的二维边缘、纹理特征对一维光谱的吸收峰也没有什么可迁移的价值。
解决:打开 README.md,先确认 TL 版本的预训练权重到底从哪来、怎么加载。如果包内没有一维预训练权重,常见做法是把 TL 理解成「训练策略」而不是「直接拷权重」——比如先在公开光谱数据集上做自监督预训练,再在目标数据上微调,或者冻结浅层、只微调深层。把 TL 的实现方式写清楚,答辩时反而成为加分项。
6. 把结果做成论文级图表:五折交叉验证、残差分布与 RPD 报告
实验跑完只是第一步,近红外回归的结论必须经过交叉验证才有说服力。单次划分的测试集 R² 只能证明「这组数据上跑了一次」,五折交叉验证能把结论从「一次运气」变成「平均表现」。
6.1 五折交叉验证这样跑
交叉验证的关键坑在预处理统计量的泄漏。每一折都必须在训练折上重新计算标准化参数,测试折只能用训练折算出来的均值方差,否则等于提前偷看了答案。
# 五折交叉验证:每折在训练折上重算预处理统计量,防止数据泄漏 from sklearn.model_selection import KFold from sklearn.metrics import r2_score, mean_squared_error import numpy as np X = np.load("X_preprocessed.npy") # 已经做完 SG+SNV 的谱图矩阵 y = np.load("y_target.npy") # 浓度/含量真值 kf = KFold(n_splits=5, shuffle=True, random_state=42) r2_list, rmse_list = [], [] for fold, (tr_idx, te_idx) in enumerate(kf.split(X)): X_tr, X_te = X[tr_idx], X[te_idx] y_tr, y_te = y[tr_idx], y[te_idx] # 标准化只能在训练折上拟合 mu, sigma = X_tr.mean(axis=0), X_tr.std(axis=0) + 1e-8 X_tr = (X_tr - mu) / sigma X_te = (X_te - mu) / sigma model = train_model(X_tr, y_tr) # 换成包里的 ConvNet 或 SpectFormer pred = model.predict(X_te) r2_list.append(r2_score(y_te, pred)) rmse_list.append(mean_squared_error(y_te, pred, squared=False)) print(f"R2 = {np.mean(r2_list):.3f} ± {np.std(r2_list):.3f}") print(f"RMSE = {np.mean(rmse_list):.3f} ± {np.std(rmse_list):.3f}")逻辑说明:KFold(shuffle=True, random_state=42)先打乱再分折,避免样本顺序影响折的构成。每折循环里重算mu和sigma,这一步是交叉验证和数据泄漏的分水岭。最后输出的均值加减标准差比单次 R² 可信得多,写论文时直接作为模型性能的正式指标。
6.2 回归图与 RPD 报告
交叉验证之后,还差两张图就能把结论钉死。第一张是预测值对实测值的散点图,对角线是理想预测线,点越贴线越好,斜率偏离 1 说明存在系统性偏差。第二张是残差图,横轴是实测值,纵轴是预测减实测,残差如果在零点上下均匀分布说明模型没有明显的系统性倾向,如果呈现漏斗形则说明浓度越高误差越大。
RPD 的计算方式是把测试集 y 的标准差除以 RMSEP,这个比值直接反映模型的实用价值。RPD 大于 3 说明模型可以用于定量预测,2.5 到 3 之间说明模型可用于粗糙筛选,小于 2 说明在这个任务上不如直接取均值。这套结论连同交叉验证的均值方差一起写进论文的结果表,比单独报一个 R² 的说服力强很多。
从那以后我拿到任何近红外数据,都强制先跑一遍五折交叉验证,用平均 R² 和 RPD 决定要不要继续往下做,而不是盯着单次划分的数字自我感动。这套代码包的价值也正在这里——它把结构、对比、验证的链路都铺好了,剩下要你动手的,是把数据口径和验证逻辑做扎实。希望帮到你。
本文还有配套的精品资源,点击获取