简介:基于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(nTrain+1:nTrain+nVal)); Yval = Y(idx(nTrain+1:nTrain+nVal)); Xtest = Xn(:,:,:,idx(nTrain+nVal+1:end)); Ytest = Y(idx(nTrain+nVal+1: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 - a,a从 2 线性衰减到 0;C = 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降到 8,maxIter降到 6,先验证整个流程能通,再用更大规模搜索。5.4 现象:trainNetwork 报错输入尺寸不匹配错误信息一般是Input size mismatch或者Layer 'conv' input size mismatch。原因集中在两个位置。一是imageInputLayer的输入尺寸与Xtrain的维度不一致。MATLAB 默认输入是[高度, 宽度, 通道, 样本数],如果Xtrain是[256, 2, 样本数]这种三维数组,就会报错。二是卷积核尺寸设置问题,比如theta(4)被更新成 2 或 8 这种偶数,卷积层的Padding又配置得不对,导致池化后特征图尺寸变成小数。排查办法是在训练前打印维度并调用analyzeNetwork:disp(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 流程,都会强制走一遍跨种子稳定性检查,均值低可以接受,标准差太大一定先查数据切分和适应度定义。希望帮到你。本文还有配套的精品资源,点击获取
简介:基于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(nTrain+1:nTrain+nVal)); Yval = Y(idx(nTrain+1:nTrain+nVal)); Xtest = Xn(:,:,:,idx(nTrain+nVal+1:end)); Ytest = Y(idx(nTrain+nVal+1: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 - a,a从 2 线性衰减到 0;C = 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降到 8,maxIter降到 6,先验证整个流程能通,再用更大规模搜索。5.4 现象:trainNetwork 报错输入尺寸不匹配错误信息一般是Input size mismatch或者Layer 'conv' input size mismatch。原因集中在两个位置。一是imageInputLayer的输入尺寸与Xtrain的维度不一致。MATLAB 默认输入是[高度, 宽度, 通道, 样本数],如果Xtrain是[256, 2, 样本数]这种三维数组,就会报错。二是卷积核尺寸设置问题,比如theta(4)被更新成 2 或 8 这种偶数,卷积层的Padding又配置得不对,导致池化后特征图尺寸变成小数。排查办法是在训练前打印维度并调用analyzeNetwork:disp(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 流程,都会强制走一遍跨种子稳定性检查,均值低可以接受,标准差太大一定先查数据切分和适应度定义。希望帮到你。本文还有配套的精品资源,点击获取