简介基于Matlab的鲸鱼算法优化卷积神经网络WOA-CNN通信辐射源识别代码包面向信号处理、模式识别方向的研究者以及需要利用智能优化算法调优CNN模型的高校学生。代码共10个文件、约215KB包含主函数main.m、WOA.m、CostFunction.m、getData.m四个MATLAB脚本外加4张运行效果图、一份Excel输入数据表和asv备份文件模块划分清晰便于按需调用。程序基于Matlab 2019b环境开发调试涵盖数据读取、WOA优化、CNN训练与结果可视化完整流程运行后会直接得到识别准确率与分类效果图适合快速验证算法有效性。已有664人学习下载既可用于复现论文实验也可在此基础上调整优化算子或网络层数进一步开展通信辐射源个体识别相关研究。1. 通信辐射源识别为什么用基于 MATLAB 的 WOA-CNN 来调 CNN 超参数通信辐射源识别这几年在无线电管理、物联网设备认证和非法电台溯源里越来越常见难点在于同类设备之间的细微指纹差异很难用肉眼或传统特征描述清楚。CNN 能自动从 I/Q 信号里抽特征但网络结构里的学习率、卷积核数、训练轮数直接影响分类上限而手动试参数更像是在赌运气。基于 MATLAB 实现鲸鱼算法优化卷积神经网WOA-CNN的核心思路是把 CNN 超参数寻优当成一个有边界的搜索问题让算法在迭代中替人试错。这套代码适合手里已经有 I/Q 样本数据但还没有跑通一套自动调参流程的从业者也适合刚接触 CNN 分类、想直接看整体工程怎么组织的新手。2. 原始 I/Q 数据怎么进 CNN切片、归一化与样本划分2.1 从原始 I/Q 序列构造训练样本辐射源识别拿到的通常是一段连续 I/Q 采样而不是现成的图片。CNN 的输入要求是四维张量常见格式是“高度 x 宽度 x 通道数 x 样本数”。I 路和 Q 路可以分别作为两个通道相当于把一段复数基带信号折叠成一张“带通道的时域图”。处理流程的第一步是切窗。窗口长度需要根据符号速率定太长会把多个符号混进同一张图里特征被平均掉太短又可能截不到一个完整符号。模拟项目 X 里我用 256 个采样点作为一个窗口步长取 128让相邻窗口有 50% 重叠这样样本量比纯滑窗少一些但类别边界不容易被切碎。下面是切片函数function [X, Y] sliceIQ(iqData, labels, win, step) n size(iqData, 1); numWin floor((n - win) / step) 1; X zeros(win, 2, 1, numWin, single); Y strings(numWin, 1); for k 1:numWin startIdx (k - 1) * step 1; seg iqData(startIdx:startIdx win - 1, :); % seg 是 [win, 2]两个通道分别放 I 和 Q X(:, :, 1, k) seg; % 标签取窗口起始位置的值前提是同一段连续数据属于同一种设备 Y(k) string(labels(startIdx)); end Y categorical(Y); end这段代码的关键点有两个。第一是X(:, :, 1, k) seg把[win, 2]的片段直接塞进四维数组的第三个维度也就是通道维这样 CNN 的 imageInputLayer 可以定义成[win, 2, 1]。第二是标签直接用categorical包装训练时 classificationLayer 会要求标签是这个类型用普通数值数组会报错。切窗时有个容易被忽略的细节如果原始数据里混杂了多个发射设备必须先在设备切换点处做分段不能跨设备切片。否则一个窗口内同时存在两台设备的信号类别标签等于随机贴后面再怎么调参也救不回来。拿到窗口序列后如果win不是正方形可以把它重塑成16 x 16的矩阵。常见做法是img reshape(seg, 16, 16, 2)把 256 个采样点排成 16 行两个通道分开 reshape最终输入尺寸是[16, 16, 2, 样本数]。这样做能让卷积核在同一局部区域内同时看到 I/Q 两路的相对关系比直接拉成一维向量喂给全连接层更稳定。2.2 逐样本归一化与训练/验证/测试划分信号数据很容易出现不同设备发射功率差异大、整体幅度相差几倍的情况。如果对整批数据做全局归一化小信号样本的细微信号会被大信号样本压掉。我一般对每个样本独立做 Z-score 归一化也就是逐窗口计算均值和标准差后标准化function Xn normalizeEachSample(X) numSamples size(X, 4); Xn zeros(size(X), single); for i 1:numSamples for ch 1:2 x X(:, :, ch, i); Xn(:, :, ch, i) (x - mean(x(:))) / (std(x(:)) 1e-6); end end end加1e-6是为了防止某一段全零数据导致除零。单独归一化之后CNN 的 imageInputLayer 就不要再开Normalization选项否则等于做了两次标准化特征幅度会发生不可控偏移。数据集划分是最容易埋雷的一步。常规做法是randperm把所有样本随机打乱按 7:1.5:1.5 的比例切出训练、验证、测试集rng(2024); idx randperm(numSamples); nTrain round(0.7 * numSamples); nVal round(0.15 * numSamples); Xtrain Xn(:,:,:,idx(1:nTrain)); Ytrain Y(idx(1:nTrain)); Xval Xn(:,:,:,idx(nTrain1:nTrainnVal)); Yval Y(idx(nTrain1:nTrainnVal)); Xtest Xn(:,:,:,idx(nTrainnVal1:end)); Ytest Y(idx(nTrainnVal1:end));这里要特别注意如果样本是按连续窗口重叠切出来的那randperm随机划分会把同一台设备的相邻窗口同时分进训练集和验证集造成严重的数据泄露。更稳的划分方式是按设备 ID 分组整台设备要么进训练要么进验证。这个问题会在第 5 章详细展开现在只需要记住信号类任务不能先切片再随机分应该先按设备分完再切片。3. WOA 与 CNN 的耦合方式位置编码、适应度函数和主循环3.1 把 CNN 超参数编码成鲸鱼个体位置WOA 鲸鱼优化算法是典型的元启发式方法它不像网格搜索那样枚举所有组合而是让一组“鲸鱼”在搜索空间里移动。每条鲸鱼的位置是一个向量向量里的每一维对应一个 CNN 超参数。把这个诉求套到通信辐射源识别任务里我通常选 5 个维度位置分量对应 CNN 超参数搜索范围处理后取值θ1初始学习率1e-4 到 1e-2直接传给 trainingOptionsθ2第一层卷积核数量8 到 64四舍五入取整θ3第二层卷积核数量8 到 64四舍五入取整θ4卷积核尺寸3 到 7取奇数θ5Dropout 概率0.1 到 0.5直接使用初始化位置时学习率不要用线性均匀分布。因为 1e-4 和 1e-2 之间差了 100 倍线性分布会让大部分个体落在靠近大数值的一端小学习率区域基本没人探索。我一般把学习率单独做对数刻度dim 5; lb [1e-4, 8, 8, 3, 0.1]; ub [1e-2, 64, 64, 7, 0.5]; positions zeros(pop, dim); for i 1:pop positions(i, 1) 10^(-4 rand * 2); % 10^-4 ~ 10^-2 positions(i, 2) round(8 rand * 56); % 8 ~ 64 positions(i, 3) round(8 rand * 56); positions(i, 4) 3 2 * round(rand * 2); % 3、5、7 positions(i, 5) 0.1 rand * 0.4; end卷积核数量如果直接用连续随机数后面构建层结构时必须round不然convolution2dLayer的numFilters会报错。这里我用了一个取巧做法让位置向量里存的就是连续值但更新过程结束后在真正训练网络时才取整。这样 WOA 计算距离和位置更新时不受离散化影响比“更新一次就取整一次”更顺滑。3.2 WOA 三种更新策略包围、气泡网和随机搜索WOA 的核心更新逻辑分三条路径。用伪代码描述第一条是收缩包围当随机概率p 0.5且系数|A| 1时当前个体朝当前最优个体移动D abs(C * leaderPos - positions(i, :)); positions(i, :) leaderPos - A * D;第二条是随机探索当p 0.5且|A| 1时个体不再朝最优个体靠而是随机选一条其他鲸鱼作为参照避免整个种群掉进同一个局部最优randIdx randi(pop); D abs(C * positions(randIdx, :) - positions(i, :)); positions(i, :) positions(randIdx, :) - A * D;第三条是螺旋气泡网攻击当p 0.5时个体沿对数螺旋路径靠近最优解l rand; D abs(leaderPos - positions(i, :)); positions(i, :) D .* exp(1 * l) .* cos(2 * pi * l) leaderPos;其中A 2 * a * rand - aa从 2 线性衰减到 0C 2 * rand。理解这三个分支的意义比背公式更重要|A| 1时算法强制做全局探索|A| 1时才局部收敛螺旋分支则保证种群不会因为局部探索停滞。实际应用到 CNN 超参数搜索时种群数通常在 8 到 16 之间迭代次数 10 到 20 次。这个规模下 WOA 的收敛速度不比粒子群差而且它不需要额外调节惯性权重代码上更省心。3.3 适应度函数每次迭代都完整训练一次 CNNWOA 的“适应度”在这里就是 CNN 在验证集上的分类准确率。每评估一条鲸鱼就要构建一次网络、训练一次、预测一次验证集。这个成本非常现实所以搜索阶段的训练轮数必须控制住我一般只用 6 到 10 轮够让网络完成初步学习即可。等搜索结束拿到最优参数再用 30 到 40 轮做最终复训。function acc woaCost(theta, Xtrain, Ytrain, Xval, Yval) layers buildCNN(theta); options trainingOptions(adam, ... InitialLearnRate, theta(1), ... MaxEpochs, 8, ... MiniBatchSize, 64, ... Shuffle, every-epoch, ... Verbose, false); net trainNetwork(Xtrain, Ytrain, layers, options); pred classify(net, Xval); acc mean(pred(:) Yval(:)); endbuildCNN负责根据theta生成层结构function layers buildCNN(theta) f1 round(theta(2)); f2 round(theta(3)); ks round(theta(4)); if mod(ks, 2) 0 ks ks 1; end layers [ imageInputLayer([16 16 2], Normalization, none) convolution2dLayer(ks, f1, Padding, same) batchNormalizationLayer reluLayer maxPooling2dLayer(2, Stride, 2) convolution2dLayer(ks, f2, Padding, same) batchNormalizationLayer reluLayer fullyConnectedLayer(10) softmaxLayer classificationLayer ]; end两层卷积加池化的结构在辐射源识别里比较常用。第一层卷积捕捉 I/Q 通道的局部相关性第二层在池化后的低分辨率特征上进一步组合最后一层的fullyConnectedLayer(10)表示 10 个辐射源类别实际使用时改成自己数据集的类别数即可。imageInputLayer里的Normalization必须写成none因为前面已经逐样本做过了 Z-score这里再做一遍反而会破坏幅度信息。4. 主脚本跑通种群初始化、适应度评估与结果回传4.1 主脚本骨架整个 WOA-CNN 的流程可以压缩成一个主脚本加三个子函数loadData、evaluatePopulation、updateWhale。下面是主脚本的完整框架clear; clc; close all; rng(2024); pop 12; maxIter 10; dim 5; lb [1e-4, 8, 8, 3, 0.1]; ub [1e-2, 64, 64, 7, 0.5]; [Xtrain, Ytrain, Xval, Yval, Xtest, Ytest] loadData(); positions zeros(pop, dim); for i 1:pop positions(i, 1) 10^(-4 rand * 2); positions(i, 2) 8 rand * 56; positions(i, 3) 8 rand * 56; positions(i, 4) 3 2 * round(rand * 2); positions(i, 5) 0.1 rand * 0.4; end bestAcc 0; leaderPos positions(1, :); for iter 1:maxIter accs evaluatePopulation(positions, Xtrain, Ytrain, Xval, Yval); [curBest, bestIdx] max(accs); if curBest bestAcc bestAcc curBest; leaderPos positions(bestIdx, :); end positions updateWhale(positions, leaderPos, iter, maxIter, lb, ub); fprintf(iter %3d / %3d, best acc %.4f\n, iter, maxIter, bestAcc); end save(woa_result.mat, leaderPos, bestAcc);主脚本里的pop 12是种群大小maxIter 10是迭代轮数。这两个参数直接决定总训练次数12 x 10 120 次。如果评估一次需要 10 秒总时长就是 20 分钟如果评估一次要 60 秒就是 2 小时。所以我通常先跑一次完整流程量出单次评估耗时再反推pop和maxIter该取多大。updateWhale函数里包含第 3 章提到的三种更新策略最后一定要加边界限制。很多新手的代码会在迭代过程中让某些维度飞出边界比如卷积核尺寸变成 9 甚至 11网络训练时间瞬间拉长适应度反而下降。边界修正很简单positions(i, :) min(max(positions(i, :), lb), ub);4.2 适应度评估中的异常保护evaluatePopulation的写法直接决定整个搜索过程能不能跑完。CNN 训练会出现各种意外比如某组超参数导致内存不足或者网络结构尺寸不匹配。这种情况下如果整个脚本直接抛异常前面跑完的迭代全部白费。我习惯给每只鲸鱼的评估加try-catch失败个体适应度给 0保证主循环继续function accs evaluatePopulation(positions, Xtrain, Ytrain, Xval, Yval) pop size(positions, 1); accs zeros(pop, 1); for i 1:pop try accs(i) woaCost(positions(i, :), Xtrain, Ytrain, Xval, Yval); catch ME fprintf(个体 %d 评估失败: %s\n, i, ME.message); accs(i) 0; end end end这里除了打印错误信息还建议把失败个体的参数一并打印出来。这样事后能从日志里反推是哪一组超参数触发了异常避免下一轮搜索再次踩进同一个无效区域。失败原因最常见的是卷积核尺寸与输入尺寸不匹配或者是内存不足这些都会在第 5 章详细说。4.3 搜索阶段要不要开验证集早停训练 CNN 时trainingOptions里可以配置ValidationData和ValidationPatience。但在 WOA 搜索阶段我强烈建议关闭早停。原因很简单早停是基于验证集做的而适应度函数里也在用验证集算准确率。如果训练阶段提前停止实际参与评估的是一个没有完全收敛的网络最后反馈给 WOA 的适应度既包含超参数好坏也包含训练状态噪声搜索过程容易震荡。正确做法是搜索阶段固定MaxEpochs每次评估用相同训练轮数保证所有鲸鱼在同一个起跑线上比较拿到最优参数之后最终复训阶段再打开早停和ValidationFrequency。搜索阶段的稳定性比最终精度更重要这一点在元启发式与深度学习结合时尤其明显。如果搜索目标是看某条鲸鱼是否值得进一步微调也可以在每个迭代结束后保存一份日志到 CSV方便后面用 Excel 或 Python 画曲线logRow [iter, i, accs(i)]; dlmwrite(woa_log.csv, logRow, -append);日志文件里至少要有迭代轮数、个体编号、适应度三个字段。调参时看两样东西适应度曲线是否整体上升以及当前 leader 对应的网络参数是否在合理范围。如果位置向量的学习率已经贴到上界或下界说明初始搜索范围没给够下一轮要把边界扩大。5. WOA-CNN 避坑手册五个高频翻车点与对应解法5.1 现象适应度曲线前期不动后期震荡搜索前期适应度一直停在同一个值附近感觉鲸鱼群体完全没有移动后期突然开始跳但最优值不稳定每轮都在变。原因有两个。第一A的衰减速度太快前期探索不足种群过早集中在初始 leader 附近第二学习率搜索范围跨度过大导致相同网络结构在不同学习率下验证精度差异巨大适应度面太粗糙。解决办法是在迭代后期加入模拟变异。我一般会在iter maxIter * 0.7时把当前适应度最差的一半个体重新打散让它们围绕 leader 做小幅随机扰动if iter round(maxIter * 0.7) badIdx accs median(accs); positions(badIdx, :) leaderPos randn(sum(badIdx), dim) .* 0.1 .* (ub - lb); positions(badIdx, :) min(max(positions(badIdx, :), lb), ub); end这样做了之后搜索后期既能保持 leader 附近的局部精修又能避免整群鲸鱼挤在同一个点导致适应度不再变化。5.2 现象验证集准确率比训练集低十个百分点以上这是信号分类任务里最容易翻车的陷阱。训练集准确率已经到 98%验证集只有 85%第一反应是模型过拟合然后去加 Dropout、改网络结构但真正原因很可能不是模型而是数据划分方式。因为 I/Q 信号切窗采用了 50% 重叠同一个设备相邻窗口高度相关。如果直接用randperm随机划分训练集和验证集里都会出现同一台设备的片段。CNN 很容易记住这种连续相似片段验证集里“见过”的片段得分虚高等遇到真没见过的设备片段时准确率直线下降。正确做法是按设备分组划分。先拿到每条样本对应的设备编号按设备随机分配devIds unique(deviceId); selDev randsample(numel(devIds), round(0.7 * numel(devIds))); trIdx ismember(deviceId, devIds(selDev)); valIdx ~trIdx;这个改动会让验证集准确率看起来比随机划分低一些但它才是真实泛化能力的反映。如果实际数据本身就是同设备多段记录这个坑务必要绕开。5.3 现象迭代一次要跑五分钟一天跑不完WOA-CNN 的耗时问题属于“资源设计”问题不是代码 bug。很多人默认搜索阶段也用 30 轮训练结果一次评估几分钟整个搜索要跑十几个小时。常见做法是两阶段训练策略搜索阶段把MaxEpochs限制在 6 到 10验证集只取少量样本搜索结束后用 leader 参数再训练 30 到 40 轮。搜索阶段的目标是区分超参数好坏不是追求最终精度所以训练轮数统一即可。另一个可行措施是提高MiniBatchSize从 64 调整到 128 或 256前提是 GPU 内存够用。批量大小在搜索阶段只影响训练稳定性只要不太离谱对验证精度影响不大但时间能省一半。如果 CPU 环境下跑得太慢就把pop降到 8maxIter降到 6先验证整个流程能通再用更大规模搜索。5.4 现象trainNetwork 报错输入尺寸不匹配错误信息一般是Input size mismatch或者Layer conv input size mismatch。原因集中在两个位置。一是imageInputLayer的输入尺寸与Xtrain的维度不一致。MATLAB 默认输入是[高度, 宽度, 通道, 样本数]如果Xtrain是[256, 2, 样本数]这种三维数组就会报错。二是卷积核尺寸设置问题比如theta(4)被更新成 2 或 8 这种偶数卷积层的Padding又配置得不对导致池化后特征图尺寸变成小数。排查办法是在训练前打印维度并调用analyzeNetworkdisp(size(Xtrain)); disp(size(Xval)); analyzeNetwork(buildCNN(theta));analyzeNetwork会逐层显示尺寸变化哪一层出了问题一目了然。代码里强制ks取奇数只是在源头堵住一种情况真正稳妥的方式是每次从woaCost拿到的theta都要经过round和奇偶校验。搜索阶段可以把ks的范围限在 3、5、7 三个值避免边界处出现异常组合。5.5 现象WOA 收敛后模型效果还不如手工调参版本搜索结束leader 参数在验证集上表现很好但拿到测试集跑一遍效果反而不如之前手工确定的固定结构。原因可能是适应度函数只用了验证准确率没有考虑模型稳定性和过拟合程度。当数据类别不平衡时准确率本身有欺骗性全部预测成多数类也能拿到高准确率。更常见的情况是搜索阶段用MaxEpochs 8评估而手工调参版本训练了 50 轮比较基准本身不公平。解决方式是把适应度从验证准确率改成“验证准确率减去过拟合惩罚”。我在一个项目里用过这样的公式trainAcc mean(trainPred(:) Ytrain(:)); valAcc mean(pred(:) Yval(:)); score valAcc - 0.5 * max(0, trainAcc - valAcc);这个表达式的含义是如果某组参数训练集准确率明显高于验证集就认为它过拟合风险高扣分。对于辐射源识别这种类别差距极小的任务用这个score作为 WOA 适应度比直接用验证准确率更容易选出泛化能力好的参数。6. 验证与落地把最优参数回传后如何复现并做跨种子稳定性检查6.1 保存最优参数并复现最终训练WOA 搜索结束后leaderPos只是一组连续向量。真正要交付的是用它训练出来的最终网络。我习惯把搜索阶段和最终训练阶段分开避免每次打开脚本都要重新跑一遍搜索。save(woa_best.mat, leaderPos, bestAcc); load(woa_best.mat, leaderPos); rng(2024); finalNet cnnTrainWithParams(leaderPos, Xtrain, Ytrain, 40); [testAcc, cm] evalOnTest(finalNet, Xtest, Ytest); save(finalNet.mat, finalNet, testAcc);复现训练时要固定随机种子。否则同一个leaderPos训两次结果可能差一到两个百分点因为网络权重初始化和数据打乱都是随机的。我一般把rng(2024)这种种子号写死在主脚本开头并把leaderPos的数值一并存进 log 文本方便后面复核。6.2 用跨种子稳定性检查代替单次测试最后一个建议是用多个随机种子重复划分数据集和训练观察测试准确率的均值和标准差。在实际项目中一次随机划分得到 96.8% 的测试准确率很好看但换一组数据重新划分可能只有 81%。单次划分的准确率只是运气均值加标准差才接近真实水平。testAccs zeros(5, 1); for seed 1:5 rng(seed); [Xtr, Ytr, Xte, Yte] resplitData(seed); net cnnTrainWithParams(leaderPos, Xtr, Ytr, 40); testAccs(seed) evalOnTest(net, Xte, Yte); end fprintf(mean acc %.4f, std %.4f\n, mean(testAccs), std(testAccs));这个验证方法对 WOA-CNN 尤其重要因为 WOA 搜索过程本身也依赖随机数。换一个随机种子搜索到的 leader 可能不同最终效果也会有波动。我做评估时会给出两组数字同一 leader 下不同训练种子的稳定性和完全重新搜索后 leader 的稳定性。如果后者的标准差超过两个百分点说明搜索空间或适应度函数还需要调整。经验教训是调试这类“算法优化网络”的工程时单次结果永远不能作为交付依据。从那以后我每次跑这套 WOA-CNN 流程都会强制走一遍跨种子稳定性检查均值低可以接受标准差太大一定先查数据切分和适应度定义。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?