ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

固有噪声也能当训练数据?光纤水声信号识别的新思路

固有噪声也能当训练数据?光纤水声信号识别的新思路 简介面向水下目标探测与环境监测研究者的深度学习实践资源基于Python实现光纤传感水声信号识别采用最优聚类模型并以光纤传感系统自身固有噪声信号分解分量作为训练数据有效解决复杂水下环境中的信号分类问题。压缩包共276个文件约6.93MB涵盖Python源码、ipynb分析笔记、训练模型权重pth、聚类结果数据csv/pkl以及可视化图表png可完整复现从数据预处理、特征提取到模型训练与评估的流程。已有46人学习下载适合具备一定Python与机器学习基础、希望将深度学习应用于水声信号处理或完成相关毕业设计的读者。资源内附多组SMOTE增强前后的特征数据对照便于理解数据平衡策略对聚类效果的影响是快速上手该方向的成套参考资料。1. 光纤水声信号识别等目标样本是等不来的固有噪声分解分量当训练数据这个反直觉起点值不值得跟打开一段光纤水听器或分布式光纤声学传感DAS的解调记录你看到的是光电转换后的电压序列。绝大多数时间它不包含任何“目标水声信号”只有系统自身固有噪声与水下环境背景的混合。真实目标样本不但少标注还得靠人工听判成本高到很难攒出一个像样的深度学习数据集。这个方案恰好反过来先把光纤传感系统自身固有噪声信号分解成若干分量用这些分解分量作为训练数据再配合最优聚类模型学出水声信号识别的边界。这是一套用 Python 就能从零跑通的落地路线适合做水声监测、海洋工程巡检、水下管线与电缆异常识别的工程师和研究生参考。2. 先立原理固有噪声为什么能当训练数据GMM 凭什么比 KMeans 更适合水声2.1 固有噪声不是垃圾数据它是水下传感通道的背景底色先把这个容易带偏的认知讲清楚。很多人听到“用噪声当训练数据”第一反应是怀疑噪声分量里除了随机性还有什么信息实际采过光纤水听器的人都知道系统自身固有噪声从来不是白噪声。它至少由三块叠加而来激光器的相对强度噪声与相位噪声、光电检测链路的热噪声和电路底噪、通过传感探头与光缆耦合进来的环境微振动。这三块叠加后时域上是一条非平稳、带缓慢漂移包络的序列频域上则是低频高能量、高频低能量的连续谱。真实水声目标信号——螺旋桨噪声、机械运转声、水下活动对象的辐射声——进入传感系统时并不是在某个频点凭空冒出来而是叠加在这层背景底色之上。所以水声识别问题的本质不是“找出目标”而是“判断当前观测与背景底色的偏离程度达到了哪一种状态”。要判断偏离就得先把背景底色学好。这里有一个很关键的产品化结论真实目标样本在工程现场很难等但固有噪声随时能采。布放完成后留出 24 到 48 小时、确认没有强干扰路过的时间窗把这段解调输出保存下来就是完整的背景底色样本。我在实际项目里就是用这样一段基线数据训练之后所有识别都发生在它的坐标系里。相比到处找公开水声数据集这个方法胜在数据与你自己的传感系统严格同源部署时不需要做跨系统迁移。2.2 为什么必须走分解VMD 把耦合噪声拆成 IMF训练样本量放大 K 倍有人会继续问既然固有噪声就是背景为什么不直接拿原始片段丢给卷积网络我早期版本就这么试过效果不好。原因在于原始固有噪声是多个物理来源的耦合信号激光器噪声、电路噪声、环境微振动在同一时刻叠加、在频谱上互相重叠。深度学习模型要在混叠信号里自己学会解耦需要的数据量和训练轮次都会成倍上升而且学到的特征经常跟着某一次采样的偶然分布走换一段噪声就失效。分解的真正价值我总结成两点。第一它把一条非平稳的混叠信号拆成若干本征模态函数IMF每个 IMF 代表一个窄带、近似平稳的振荡成分下游编码器就不再承担解耦工作。第二它是天然的数据增广。一条 4096 点的噪声分解成 K 个 IMF 后训练样本数直接乘以 K我用 K 等于 5 到 8 时一段现场噪声能产出几十万个 IMF 样本足够喂饱一个小型一维卷积自编码器。分解算法我首选 VMD变分模态分解而不是 EMD。EMD 没有参数、不用预设模态数看起来省事模态混叠却严重同一个物理成分可能被劈到两个相邻 IMF 中产生大量处在类别边界上的边缘样本这些样本后面会让轮廓系数非常难看。VMD 先在频域迭代出各模态中心频率再用带宽惩罚约束模态形状虽然需要调参但模态分隔规整得多更适合构造可复现的训练集。2.3 深度学习加聚类的分工先压缩表征再解决“这是第几类”“最优聚类模型”这个叫法其实包含两个决策最优的特征表示以及在这个表示下最优的聚类数和聚类算法。深度学习负责前半段——用一维卷积自编码器把每条 IMF 片段压缩成低维向量我在这个方案里用 32 维聚类算法负责后半段——在低维空间里把样本分成若干簇每簇对应一种背景状态或扰动模式。聚类部分我选 GMM高斯混合模型而不是更常见的 KMeans。KMeans 给出硬边界还假设簇是球形水声背景是连续渐变的随机过程同一个簇里有强有弱、有密有疏硬边界会把边界样本划到错误一边。GMM 输出每个样本属于每个簇的后验概率等于把“这条片段更像背景、还是更像哪类扰动”量化成置信度这个置信度后面可以直接用作事件触发的判据。我在这类数据上对比过只要 latent 维度控制在 32 以内GMM 的轮廓系数和 BIC 表现都稳健压过 KMeans。有一点必须提醒GMM 假设每个簇近似高斯分布所以特征空间必须低维且经过标准化。这一步正好交还给自编码器——压缩得好GMM 拟合得好压缩不好GMM 在高维协方差估计上翻车。两段式设计是整套方案的骨架后续第 4 章的代码就是按这个骨架实现。这套思路也与常见的“用 YOLO 训练自己的数据集”那类流程有本质区别目标检测有现成标注规范水声信号没有公开标注集人工听判一天也标不出多少样本聚类方案把标注问题变成参数选择问题你只需要确认簇个数是否合理这是当前工程条件下比较现实的一条路。这套方法的边界也得说清楚。噪声训练出的簇只能区分“某种状态的扰动模式”不能回答“这是哪艘船、哪台机器”这种细粒度分类问题。要回答后者你还是需要带标的真实样本。所以更准确的说法是它解决的是水声识别里的检测与事件分型问题是为后续细粒度识别准备前端而不是替代全部标注工作。3. 动手构造训练集用 Python 对固有噪声做 VMD 分解把一段噪声变成 K 类 IMF 样本3.1 环境准备一条命令装齐四个库先跑通再调参我在本地用 Python 3.10核心依赖四个库numpy 和 scipy 做信号处理vmdpy 做 VMD 分解torch 做自编码器scikit-learn 做 GMM 聚类。安装命令如下。python -m venv fiber_audio source fiber_audio/bin/activate pip install numpy scipy vmdpy torch scikit-learn说明venv 激活后依赖都装到 fiber_audio 目录里避免污染系统 Python。当前 torch 默认装 CPU 版自编码器数据量不大CPU 能跑通全程如果你机器上已经装了带 CUDA 的 torch直接去掉命令里的 torch 即可。vmdpy 是较早的包算法实现是标准 ADMM 迭代版本老不影响使用。装完跑一行验证python -c from vmdpy import VMD; print(vmd ok)如果在公司内网装不了这个包也不建议绕路装其他分支自己按 ADMM 公式重写 VMD 核心也就几十行但前期调试成本会高不少能用现成包就先用一个。3.2 数据组织一段长噪声切成训练集和 held_out 留出集我建议把固有噪声文件按 9:1 切分成 train 和 held_out 两段held_out 不参与训练留到第 6 章做泛化检查。每段样本长度我选 4096 点对应 48k 采样率下约 85 毫秒窗口。为什么是 4096它覆盖 20Hz 到 2kHz 目标频带所需的低频分辨率又不会把多个声学事件卷进同一个样本8192 点样本量减半2048 点则瞬时频率分辨率太粗。文件命名用能回溯的规则比如noise_000123_imf_02.npy前面是原始段序号后面是 IMF 序号。训练集和留出集分两个目录存放DataLoader 直接读路径列表不把全部样本一次性载入内存。这里有个容易犯的错有人会在这一步顺手提取统计特征比如过零率、均值、方差直接把统计量存成样本。自编码器需要的是原始时域波形统计量已经丢了相位和瞬时结构后面压缩表征就没有意义。所以 npy 里存的必须是原始 float32 波形不是特征。3.3 VMD 分解核心代码从原始噪声到 IMF 样本集import numpy as np from vmdpy import VMD def build_imf_dataset(noise, seg_len4096, K5, alpha2000, tau0.0): seg_count len(noise) // seg_len imf_samples [] for i in range(seg_count): seg noise[i * seg_len:(i 1) * seg_len].astype(np.float32) # VMD 返回三个值模态数组 u、频谱数组 u_hat、中心频率 omega u, _, _ VMD(seg, alpha, tau, K, 0, 1, 1e-7) # u 的形状是 (K, seg_len)一行是一条 IMF中心频率从低到高排列 imf_samples.append(u.T) return np.vstack(imf_samples) # (seg_count * K, seg_len)逻辑说明for 循环里每次取 4096 点一段交给 VMD 拆成 K 条 IMFu.T把每条 IMF 转成一行样本。全部段处理完行数为“段数 × K”每条样本长度 4096这就是后续自编码器的输入格式对应张量形状(B, 1, 4096)。参数说明alpha 是带宽惩罚因子默认 2000。目标背景是宽带噪声时可以把 alpha 降到 1200 让模态带宽更宽如果背景里存在明显机械窄带调到 3000 能把窄带模态约束得更干净。tau 保持 0它控制对噪声的容忍度前期聚类结果发散的时候先别动它。K 设 5 到 8我先用 5跑一遍 3.4 的中心频率检查再决定要不要加。DC 必须为 0不需要单独提取直流init 用 1让模态中心频率均匀初始化tol1e-7 不用改。注意alpha 和 K 是一对耦合参数。调 K 之前先固定 alpha调 alpha 时不要同时改 K否则你分辨不出是哪个参数引起的模态变化。3.4 分解质量检查三个小检查pass 之后再进第 4 章VMD 不是扔进去就完事我每次都会检查 IMF 是否真的把频带分开。from scipy.fft import rfft, rfftfreq def inspect_imfs(imf_matrix, fs48000): rows imf_matrix.shape[0] spec_centers [] for i in range(rows): spectrum np.abs(rfft(imf_matrix[i])) freq rfftfreq(imf_matrix.shape[1], 1.0 / fs) peak_idx np.argmax(spectrum) spec_centers.append((freq[peak_idx], float(np.mean(imf_matrix[i] ** 2)))) return sorted(spec_centers, keylambda x: x[0])逻辑说明对每条 IMF 做实数 FFT取幅度谱峰值位置换算成实际频率再附带一个均方能量。返回的列表按中心频率升序排列方便人工看模态分布。三个检查点第一中心频率应当随 IMF 序号单调上升若出现序号 4 的中心频率突然比序号 3 还低说明 K 给多了把同一频带劈成了两条第二模态能量不能过度集中某一条 IMF 能量占比超过 80%说明其他 IMF 是空壳这时减小 K 或加大 alpha第三看线谱图上是否存在同频成分跨模态重复出现若有就考虑缩短分段长度提高频率分辨率。这三个检查都是廉价计算几十万条样本也就几秒钟省下的时间远比花的多。4. 训练最优聚类模型一维卷积自编码器压缩表征 高斯混合模型选最优 K4.1 定义一维卷积自编码器latent_dim32 时的结构选择为什么用一维卷积而不是全连接IMF 是局部时间相关的序列一维卷积天然提取短时结构参数也比全连接少两个数量级。我用的编码器是三个 stride2 的卷积层把长度从 4096 降到 512经过自适应平均池化展平最后投影到 32 维 latent。解码器用线性上采样加卷积重建回 4096。import torch import torch.nn as nn class ConvAE(nn.Module): def __init__(self, latent_dim32): super().__init__() self.encoder nn.Sequential( nn.Conv1d(1, 16, kernel_size5, stride2, padding2), nn.ReLU(), # 长度 2048 nn.Conv1d(16, 32, kernel_size5, stride2, padding2), nn.ReLU(), # 长度 1024 nn.Conv1d(32, 64, kernel_size5, stride2, padding2), nn.ReLU(), # 长度 512 nn.AdaptiveAvgPool1d(1), nn.Flatten() ) # 输出 (B, 64) self.fc nn.Linear(64, latent_dim) self.head_in nn.Linear(latent_dim, 64 * 64) self.recon nn.Sequential( nn.Upsample(scale_factor4, modelinear, align_cornersFalse), # 64 - 256 nn.Conv1d(64, 32, kernel_size5, padding2), nn.ReLU(), nn.Upsample(scale_factor4, modelinear, align_cornersFalse), # 256 - 1024 nn.Conv1d(32, 16, kernel_size5, padding2), nn.ReLU(), nn.Upsample(scale_factor4, modelinear, align_cornersFalse), # 1024 - 4096 nn.Conv1d(16, 1, kernel_size5, padding2), ) def forward(self, x): z self.fc(self.encoder(x)) h self.head_in(z).view(z.size(0), 64, 64) return self.recon(h), z参数说明卷积核 5、stride2、padding2让长度折半但不丢边界信息三层的感受野逐步覆盖更长的上下文。latent_dim32 是经验值设 8 会丢失多状态区分度设 128 则后面 GMM 的协方差估计算不过来了。解码器用Upsample的线性模式而不是转置卷积是为了减少棋盘伪影代价是重建精度略低但这里的目标不是无损重建而是学出一个可分性强的 latent 空间。4.2 训练自编码器MSE 重构 Adam只吃 IMF 样本import numpy as np import torch from torch.utils.data import DataLoader, Dataset class ImfDataset(Dataset): def __init__(self, npy_paths): self.paths npy_paths def __len__(self): return len(self.paths) def __getitem__(self, idx): x np.load(self.paths[idx]).astype(np.float32) return torch.from_numpy(x).unsqueeze(0) # (1, 4096) model ConvAE(latent_dim32) opt torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.MSELoss() loader DataLoader(ImfDataset(train_paths), batch_size64, shuffleTrue) for epoch in range(80): total_loss 0.0 for xb in loader: opt.zero_grad() rec, _ model(xb) # xb 形状 (B, 1, 4096) loss criterion(rec, xb) loss.backward() opt.step() total_loss loss.item() print(fepoch {epoch:02d} loss {total_loss / len(loader):.6f})逻辑说明每个 batch 输入是(B, 1, 4096)的 IMF 样本输出是重建的(B, 1, 4096)目标就是输入本身。loss 用 MSE跑 80 个 epoch。如果训练 loss 下降很慢把 lr 降到 3e-4如果验证重构 loss 开始反弹要提前停止别死等到 80。参数说明batch_size64 在几十万条样本时依然很快。epoch80 足够一个三层卷积自编码器收敛我一般盯着 loss 曲线到第三个平台期就手动停。训练时随机打乱样本避免同一条噪声段分解出的 K 条 IMF 总在同一个 batch 里否则模型会偷懒记住位置而不是学结构。4.3 最优聚类选择标准化 latent、GMM 的 BIC 折线、reg_covar训练完编码器把全部 IMF 样本过一遍编码器得到 latent 矩阵 L形状(n_samples, 32)。接下来是关键的选择题。from sklearn.preprocessing import StandardScaler from sklearn.mixture import GaussianMixture from sklearn.metrics import silhouette_score import numpy as np Z StandardScaler().fit_transform(L) k_range range(2, 9) bics, sils, models [], [], [] for k in k_range: gmm GaussianMixture( n_componentsk, covariance_typefull, reg_covar1e-3, random_state42, ).fit(Z) labels gmm.predict(Z) bics.append(gmm.bic(Z)) # 样本量过大时随机抽 1 万条再算轮廓系数 sils.append(silhouette_score(Z, labels, sample_size10000)) best_k k_range[int(np.argmin(bics))] print(selected k:, best_k, sil:, max(sils))逻辑说明对 latent 做标准化是 GMM 的硬前提不标准化的话各维方差差异会主导协方差矩阵。循环 k2 到 8分别拟合 GMM计算 BIC 和轮廓系数。BIC 越低说明模型在解释数据和防止过拟合之间平衡得越好轮廓系数衡量簇间分离度。两者通常在同一拐点附近。参数说明covariance_typefull让每个簇有自己的协方差矩阵适合水声背景各簇形状不同的情形样本量不足时改成tied能加快收敛。reg_covar1e-3给协方差对角线加正则防止某类样本太少导致协方差矩阵奇异这个参数不设的话出现 NaN 时才追悔。random_state 固定只是保证可复现真正判断最优 k 的标准是 BIC 折线的大趋势不是某一次标签。我一般这样读 BIC 折线从 2 到 4 急剧下降到某个 k 后下降放缓甚至反弹那个拐点就是最优聚类数。如果轮廓系数和 BIC 拐点不一致优先信 BIC因为轮廓系数在 GMM 软分配下偏高估。4.4 保存识别管线让新样本走一遍标准化加预测聚类跑完把标准化器、编码器权重、GMM 一起打包部署时直接用。import pickle pipe { scaler: scaler, state_dict: model.state_dict(), latent_dim: 32, gmm: gmm, } with open(fiber_identification_pipe.pkl, wb) as f: pickle.dump(pipe, f)逻辑说明部署时的预处理与训练完全一致——原始噪声段先做 VMD 分解每条 IMF 过编码器得到 latent再scaler.transform最后gmm.predict_proba。这个管线的输入是一条原始噪声段输出是一个概率向量最大分量对应的簇就是模型认为当前最接近的背景状态或扰动模式。5. 避坑记录光纤水声信号聚类识别里我调过的五个参数问题5.1 前几阶 IMF 全挤在低频先查采样率和 alpha别急着加网络现象用默认 alpha2000、K5 跑出来的 IMF前四阶中心频率全在 100Hz 以下第五阶直接跳到几千赫兹整个模态图头重脚轻。原因固有噪声能量分布极不均衡低频能量比中高频高出一到两个数量级VMD 把大部分带宽预算分给了低频段。如果采集端没有抗混叠低通带外电路噪声也会把模态拖歪。解决软件上先做一次低通把信号限制在目标频带比如 0.5Hz 到 4kHz再跑 VMD。alpha 调大到 3000并在 VMD 之前对每段信号做一次归一化。分段长度不要用 2 的整数次幂以外的值不然 rfft 的频率分辨率与中心频率计算会错位。5.2 换一次随机种子聚类结果就完全不同现象同样代码random_state 从 42 换到 0best_k 从 5 跳到 7轮廓系数差了 0.1 以上。原因GMM 在高维特征下迭代初值敏感EM 算法容易收敛到局部解。latent 维度过大时协方差矩阵参数量暴涨数据相对稀疏不同初值下 EM 收敛到不同局部解的概率大幅上升。解决把 latent 维度控制到 32 以内固定 random_state 做内部验证同时在多个随机种子下重复聚类看 BIC 折线的大趋势是否一致。趋势一致说明这个最优 k 是可靠的只看某一次的 BIC 最小值容易自欺欺人。5.3 重构损失很漂亮轮廓系数却在跌现象自编码器 MSE 降到 0.001重建波形几乎叠在原始 IMF 上但聚类轮廓系数只有 0.2类别混成一团。原因MSE 在逐点欧氏距离下会优先拟合振幅大的低频成分携带“模态形状特征”的中频包络信息对 MSE 贡献极小编码器干脆把它丢了。latent 能重建但不能区分。解决对重构 loss 按频带加权给中频段更高权重或者引入一个对比损失分支同一个 IMF 段的不同裁剪块算正样本对不同模态的段算负样本对强迫 latent 保留差异信息。这是我后来改动最大的一处。5.4 训练好的模型放到现场数据上全部归到一类现象现场部署当天的数据90% 样本的 predict_proba 输出都集中到同一个簇曲线几乎不动。原因训练用的固有噪声来自布放后安静期现场当下的环境噪声里包含连续行船、风浪、工频或其他间歇强扰动。这些在训练集里几乎不存在GMM 在训练数据覆盖不足的区域只能给出模糊判断全部归一。解决现场数据先做带通预滤波再加一条“未知簇”逻辑预测概率最大值的置信度低于阈值时标记为未知事件而不是硬归到已有簇。部署后用一两天现场数据重新 fit 标准化器分布漂移大多是缓慢的定期校准就能缓解。5.5 把 parameter 直接当成显存占用来估部署开销拍脑袋现象模型 summary 显示参数量约 200k我按 200k 字节估显存结果 GPU 直接 OOM。原因参数量是数量不是字节数。FP32 一个参数占 4 字节训练还要算梯度Adam 还要维护一阶和二阶动量推理时按 batch_size 和中间激活值再放大一波。这跟“深度学习里的 parameter 应该不是 MB 吧”那类疑问是同一件事。解决按参数量乘 4 得到权重字节数训练开销再乘 2 到 4 作为中间状态预算推理看的是激活值而不是梯度。拿小 batch 先跑一次用nvidia-smi实测别只信 summary。6. 进阶验证拿真实水声信号做闭环测试把聚类输出变成事件检测6.1 SNR 扫描用真实水声信号验证模型有效光在固有噪声上测试不够我总会留一小段真实水声信号哪怕是从水声样本库或自己录的水池实验里截一段与固有噪声按不同信噪比混合用来检验聚类成员概率的变化。def mix_signal(real_water, background, snr_db): real_water real_water[:len(background)] p_s np.mean(real_water ** 2) p_n np.mean(background ** 2) gain np.sqrt(p_n / p_s * 10 ** (snr_db / 10)) return gain * real_water background逻辑说明增益公式固定背景能量目标信号能量随 SNR 变化。把 SNR 从 -10dB 扫到 10dB记录每个混合样本的predict_proba最大分量。置信度随 SNR 升高而单调升高说明模型学到的是“信号对背景底色的偏离程度”如果曲线不动或乱跳说明嵌入特征里没有包含可区分信息回到第 4 章调表征。参数说明真实水声信号太短时用交叉淡化的方式拼长别硬接否则帧边界会产生脉冲把聚类结果搅浑。每次混合都使用不同的噪声段起点避免模型记住固定的混合位置。6.2 用 held_out 做泛化检查模型学到的是结构不是某一天的噪声第 3 章切出的 held_out 段这时派上用场。把 held_out 的 IMF 样本过一遍完整管线理想情况是它们被分配到少数几个已知簇而不是均匀散落在所有簇里。如果 held_out 的簇分布与训练集差异很大说明编码器记住了某天的偶然谱形此时回到 4.2对训练集做频谱白化或给 IMF 加 5% 高斯噪声做增广让编码器不能依赖绝对振幅。泛化检查通过模型才值得拿到现场。6.3 进阶技巧软概率阈值触发结合半监督边界校准聚类拿到的是簇号但水声事件识别最终需要“是/不是”的判定。我不用argmax出硬标签而是用predict_proba的最大值做软判决最大值低于 0.85 的样本暂时挂起连续多帧挂起才触发事件。这个阈值在 SNR 扫描里标定一次现场直接用比写死一个幅值阈值可靠得多。如果想要更贴近人工标注的边界可以用 label spreading 做半监督把当天人工确认的几十个样本作为种子聚类结果的类中心作为先验让边界顺着人工标注移动。注意这一步只调整边界不改变簇数簇数一变物理含义就要重新解释。最后说一句我自己的教训这套方法最大的诱惑是“用聚类结果假装有标签”然后拿去训练监督模型。聚类簇不等于语义类别只有通过 SNR 混合测试确认了簇的物理含义你才有资格把它当类别用。识别系统的可靠性不取决于模型多复杂而取决于你对每个簇的解释是否站得住脚。希望帮到你。本文还有配套的精品资源点击获取
返回列表