简介这份资源是面向计算机专业学生与深度学习初学者的毕设及课程作业参考项目聚焦近红外光谱数据的回归分析建模。近红外光谱作为一种非破坏性分析技术广泛用于化学、生物医学与食品检测项目通过深度学习挖掘光谱与物质成分之间的映射关系帮助读者理解从数据预处理、特征工程到模型训练、验证与评估的完整流程。压缩包共9个文件以8个Python源码和1份Markdown说明为主Python部分承担网络搭建与训练逻辑涵盖卷积网络、视觉Transformer及迁移学习等多种结构Markdown则用于梳理项目说明与使用指引整体约27KB结构紧凑便于快速上手。目前已有236人学习下载适合希望将深度学习落地到光谱回归任务、需要可运行代码对照练习的读者参考借鉴。1. 近红外光谱回归这件事卡住人的从来不是模型结构近红外光谱数据回归分析说白了就是拿一束光扫过样品从吸光度曲线里反推出某个连续值——蛋白质含量、水分、淀粉、某种活性成分浓度。它跟图像分类最大的区别在于样本量小、维度高、共线性严重而且你拿到的往往是一份几百行、上千列的矩阵标签只有一列。很多同学做毕设或课程作业时第一反应是“上深度学习”结果把 PLS偏最小二乘跑出 0.9 的 R²换成三层 MLP 反而掉到 0.6于是开始怀疑人生。这个方向真正能落地的路径不是把 ResNet 搬过来而是先做光谱预处理再用一维卷积或全连接网络做特征压缩最后用回归头输出连续值。它适合两类人一类是化工、食品、农业相关专业手头有光谱仪导出的 CSV需要交一份能跑通、能解释的作业另一类是刚接触光谱数据、想找一个完整小项目练手的开发者。下面我按“数据长什么样 → 预处理怎么做 → 模型怎么搭 → 坑在哪”的顺序把这条链路拆开讲。2. 近红外光谱数据到底长什么样从 CSV 到可训练张量2.1 一份典型光谱数据的结构近红外光谱仪导出的原始文件常见格式是 CSV 或 Excel第一列是波长nm 或 cm⁻¹后面每一列是一个样本的吸光度。也有反过来的每一行是一个样本列是不同波长点。你拿到手第一件事不是写模型而是确认方向。我一般会先跑一段脚本看形状和数值范围。import pandas as pd import numpy as np # 假设文件第一列是波长后续每列是一个样本 raw pd.read_csv(spectra.csv, index_col0) print(原始形状:, raw.shape) # (波长点数, 样本数) print(波长范围:, raw.index.min(), raw.index.max()) print(吸光度范围:, raw.values.min(), raw.values.max()) # 转成 样本 x 波长 的矩阵 X raw.values.T.astype(np.float32) print(转置后:, X.shape) # (样本数, 波长点数)这段代码做三件事读文件、确认波长轴方向、把矩阵转成“行是样本、列是特征”的常规布局。参数上要注意index_col0如果原文件没有索引列这行会吃掉第一列真实数据。吸光度范围通常在 0 到 3 之间如果出现负值或超过 4多半是仪器基线漂移或文件拼接错误先别急着建模。2.2 标签对齐与样本划分的坑标签文件往往是单独一份 Excel里面是样本编号和对应的理化值。合并时最容易翻车的地方是编号顺序不一致。我见过太多人直接pd.concat结果标签和光谱错位模型训练 loss 正常下降验证集 R² 却是负的。labels pd.read_excel(labels.xlsx) # 列: sample_id, y labels labels.set_index(sample_id) # 假设光谱列名就是 sample_id sample_ids raw.columns.tolist() y labels.loc[sample_ids, y].values.astype(np.float32) print(标签数量:, len(y), 光谱样本数:, X.shape[0]) assert len(y) X.shape[0], 标签与样本数量不匹配这里用loc按光谱列名顺序取标签而不是按标签文件顺序保证一一对应。assert那行是后悔药数量对不上直接报错比训练完才发现强。样本划分上光谱数据不能随机打乱因为同批次样品光谱高度相似随机划分会导致验证集泄漏。常见做法是按批次或按测量时间做 Kennard-Stone 划分或者至少用GroupShuffleSplit按批次分组。3. 预处理决定上限SNV、一阶导与 Savitzky-Golay 的实操参数3.1 为什么预处理比模型更关键近红外光谱的干扰主要来自三块基线漂移、散射效应、高频噪声。标准正态变量变换SNV解决散射一阶导数消除基线Savitzky-Golay 平滑去噪。这三步做完PLS 的 R² 往往能从 0.7 拉到 0.9 以上。深度学习模型如果直接吃原始光谱第一层卷积要同时学这些变换样本量不够时根本学不动。from scipy.signal import savgol_filter def snv(X): # 对每个样本做标准正态变量变换 mean X.mean(axis1, keepdimsTrue) std X.std(axis1, keepdimsTrue) return (X - mean) / (std 1e-8) def first_derivative(X, window11, poly2): # Savitzky-Golay 一阶导数 return savgol_filter(X, window_lengthwindow, polyorderpoly, deriv1, axis1) X_snv snv(X) X_deriv first_derivative(X_snv, window11, poly2) print(预处理后范围:, X_deriv.min(), X_deriv.max())SNV 按行做每个样本独立不跨样本泄漏信息。savgol_filter的window_length必须是奇数且大于polyorder。窗口 11、多项式 2 是我在 1000 到 2500 nm 范围、分辨率 2 nm 数据上的常用起点。窗口太大导数峰会变钝太小噪声压不住。做完一阶导后数值范围会变成 ±0.01 量级这是正常的后面标准化层会处理。3.2 波长筛选别把 2000 个点全塞进去全谱建模不是不行但冗余波长会稀释有效信号。常见做法是先用 PLS 的 VIP 得分或相关系数筛一轮再送进神经网络。下面这段用 PLS 回归的 VIP 做初筛。from sklearn.cross_decomposition import PLSRegression pls PLSRegression(n_components10) pls.fit(X_deriv, y) # 计算 VIP t pls.x_scores_ w pls.x_weights_ q pls.y_loadings_ p, h w.shape vips np.zeros(p) s np.diag(t.T t q.T q).reshape(h, -1) total_s np.sum(s) for i in range(p): weight np.array([(w[i, j] / np.linalg.norm(w[:, j])) ** 2 for j in range(h)]) vips[i] np.sqrt(p * (s.T weight) / total_s) selected np.argsort(vips)[-300:] # 取 VIP 最高的 300 个波长 X_sel X_deriv[:, selected] print(筛选后形状:, X_sel.shape)n_components一般取 8 到 15太少欠拟合太多过拟合。VIP 大于 1 通常认为重要但直接取 Top-300 更可控。注意筛选必须只在训练集上算 VIP再应用到验证集否则又是泄漏。这一步做完特征从 1000 降到 300后面网络参数量能少一个量级。4. 用一维卷积搭回归模型结构、损失与训练参数4.1 一维卷积为什么适合光谱光谱是一维连续信号局部峰位对应特定化学键的倍频和合频吸收。一维卷积核在波长轴上滑动正好捕捉这种局部模式。相比全连接卷积层参数共享小样本下更稳。我的常用结构是两层 Conv1d BN ReLU 池化展平后接两层全连接回归头。import torch import torch.nn as nn class SpectraCNN(nn.Module): def __init__(self, in_len): super().__init__() self.features nn.Sequential( nn.Conv1d(1, 16, kernel_size7, padding3), nn.BatchNorm1d(16), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(16, 32, kernel_size5, padding2), nn.BatchNorm1d(32), nn.ReLU(), nn.AdaptiveAvgPool1d(16) # 固定输出长度避免输入长度变化 ) self.regressor nn.Sequential( nn.Flatten(), nn.Linear(32 * 16, 64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, 1) ) def forward(self, x): # x: (batch, 1, 波长数) x self.features(x) return self.regressor(x).squeeze(-1)AdaptiveAvgPool1d(16)是关键它把任意波长数压成固定 16 维这样换数据集不用改全连接层输入维度。Dropout(0.3)在小样本上很必要我试过 0.5 会欠拟合0.1 又压不住过拟合。卷积核 7 和 5 对应大概 14 nm 和 10 nm 的窗口跟常见吸收峰宽度匹配。4.2 训练循环与三个必调参数回归任务用 MSE 损失但标签量纲差异大时先对 y 做标准化。优化器选 AdamW学习率 1e-3权重衰减 1e-4。批次大小 16 到 32样本少就 8。from torch.utils.data import DataLoader, TensorDataset # y 标准化 y_mean, y_std y_train.mean(), y_train.std() y_train_n (y_train - y_mean) / y_std y_val_n (y_val - y_mean) / y_std train_ds TensorDataset(torch.tensor(X_train[:, None, :]), torch.tensor(y_train_n)) train_loader DataLoader(train_ds, batch_size16, shuffleTrue) model SpectraCNN(X_train.shape[1]) opt torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) loss_fn nn.MSELoss() for epoch in range(200): model.train() for xb, yb in train_loader: opt.zero_grad() pred model(xb) loss loss_fn(pred, yb) loss.backward() opt.step() # 每 20 轮看一次验证集 if epoch % 20 0: model.eval() with torch.no_grad(): val_pred model(torch.tensor(X_val[:, None, :])) val_rmse torch.sqrt(loss_fn(val_pred, torch.tensor(y_val_n))) print(fepoch {epoch}, val RMSE {val_rmse:.4f})三个必调参数学习率、Dropout 比例、卷积核数量。学习率 1e-3 是起点验证 loss 震荡就降到 3e-4。Dropout 从 0.3 开始过拟合就加欠拟合就减。卷积核数量 16/32 够用加到 64/128 在小样本上几乎必过拟合。验证集 RMSE 用标准化后的值看趋势最终报告要反标准化回原始量纲。5. 避坑与排查光谱回归里最容易翻车的五件事5.1 验证集 R² 为负训练 loss 却正常下降现象训练集 MSE 一路降验证集 R² 是负数。原因九成是标签错位或数据泄漏。先检查sample_id对齐再确认划分时没有把同批次样本分到两边。解决用GroupShuffleSplit按批次分组或者手动按测量日期切分。5.2 预处理在划分之前做了现象模型指标好得离谱换一批数据就崩。原因SNV 和导数本身不泄漏但标准化减均值除标准差如果用了全量数据验证集信息就进训练了。解决所有涉及统计量的步骤均值方差只能从训练集算再应用到验证集。5.3 一阶导后信噪比反而变差现象导数光谱毛刺多模型学不动。原因savgol_filter窗口太小或没先平滑。解决先做一次窗口 15 的平滑再做窗口 11 的一阶导或者直接用deriv1同时平滑和求导但窗口要加大到 15 以上。5.4 批次大小设成 1 或全量现象loss 剧烈震荡或完全不降。原因批次太小梯度噪声大全量又失去随机性。解决样本 200 以内用 8 到 16200 到 1000 用 32再大可以 64。光谱数据通常不大16 是安全区。5.5 用分类指标评估回归现象有人拿准确率说事。原因回归输出连续值没有类别。解决看 RMSE、MAE、R²以及预测 vs 真实的散点图。R² 大于 0.85 在近红外里算可用大于 0.9 算不错但要看具体成分和仪器。6. 把模型跑稳之后我习惯再做两件事第一件是看残差分布。把验证集预测值和真实值的差画出来如果残差随预测值增大而增大说明模型在高浓度区欠拟合这时候要么加样本要么对 y 做对数变换。第二件是保存预处理参数。SNV 不需要保存但标准化的均值和方差、VIP 筛选的波长索引、y 的均值和方差这些必须跟模型权重一起存否则部署时对不上。import joblib joblib.dump({ x_mean: X_train.mean(axis0), x_std: X_train.std(axis0) 1e-8, selected_idx: selected, y_mean: y_mean, y_std: y_std }, preprocess.pkl) torch.save(model.state_dict(), spectra_cnn.pth)推理时先加载preprocess.pkl按同样顺序做 SNV、导数、标准化、波长筛选再送进模型最后反标准化输出。这套流程我踩过最大的坑是忘了存selected_idx换台机器重新筛波长结果对不上排查了一下午。血泪经验就是预处理里任何带随机性或依赖训练集统计量的步骤全部落盘。另外如果作业要求对比传统方法别只跑 PLS。把支持向量回归SVR和随机森林也跑一遍用同一套预处理和划分表格里列 RMSE 和 R²。我一般会保留 PLS 作为 baseline因为它在小样本光谱上经常打赢深度学习这不是玄学是样本量决定的。模型选型上样本少于 150 优先 PLS 或 SVR150 到 500 可以试一维卷积超过 500 再考虑更深的网络。这个阈值不是铁律但能帮你少走弯路。希望帮到你。本文还有配套的精品资源点击获取