DeepLearnToolbox-master CNN
没想到,又回来看这代码了,结合文章Notes on Convolutional Neural Networks看效果比较好。
- 问题:
1)这代码在池化层没有参数,所以这和论文里面有点不一样。但感觉无法明白,池化层有参数(除了激活函数能够增加一些非线性之外,这有意义吗)?
2)对于bp和卷积之类的内容,理解不到位,其实理解还是不到位。
3)参数初始化理解不到位。
4)
5)
- 创新点:
1)
2)
3)
4)
- 全文总结:
1)
2)
3)
4)
5)
- 想法:
1)看看caffe代码,再对比这个代码,以及论文Notes on Convolutional Neural Networks应该效果比较好。
2)
3)
4)
5)
- 看懂本文,最好需要阅读的相关论文:
1)
2)
3)
4)
5)
6)
7)
结合别人写的博客,看看效果更好。
先看代码:test_example_CNN.m(依次调用 cnnsetup.m,cnntrain.m,cnntest.m)
function test_example_CNN clear;close all;clc; disp('当前正在执行的程序是:'); disp([mfilename('fullpath'),'.m']); isOctave=0; addpath(genpath('../data/')); addpath(genpath('../CNN/')); addpath(genpath('../Util/')); load mnist_uint8;%加载数据 %下面的数据train_x为训练数据样本,train_y为训练数据标签。 %下面的数据test_x为测试数据样本,test_y为测试数据标签。 %由于初始的训练和测试样本格式为 nx784 28x28=784,所以把784维改变维度为28x28 %样本都是28x28的图像,样本数据格式为28x28xn(n为样本个数) %标签数据格式为10xn (10为总类别数,所以除了对应类别为1,其余为0),n为样本个数 train_x = double(reshape(train_x',28,28,60000))/255;%还原图像数据,并归一化 test_x = double(reshape(test_x',28,28,10000))/255; train_y = double(train_y'); test_y = double(test_y'); %% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% error rand('state',0) %cnn为结构体,这个结构体中字段layers的值为5个单元数组,每个单元数组中都有1个结构体 %而每个结构体中都有字段和值,下面就为定义的不同层(单元数组)对应的字段和值 %这是一个比较老的工具箱,只定义了三种层:i 输入;c 卷积;s 下采样、池化 %'c'的outputmaps是convolution之后有多少张图,也就是有多少个卷积核的意思。(不知道这个卷积到底是遍历卷积还是怎么卷积的)? %'c'的kernelsize应该就是卷积核的尺寸吧。 %'s'的scale就是池化的尺寸为scale*scale的区域 %cnn结构如下,从后面代码中看到的隐藏信息有:卷积层的步长为1,池化层位非重叠池化(步长就是池化scale的大小) %cnn最后池化层的输出,拉成一个[192,1](这假定只输入了一个样本),然后输出为10类。192=4x4x12 %一个192x10的全连接网络,sigmoid为激活,值域刚好为[0,1]和标签1正好合适 %可以看出,这是一个非常古典的cnn,能看这代码,对于学习cnn很有帮助! %特征图谱的尺寸变化为 28(i)24(c)12(s)8(c)4(s) cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer };%设置CNN的结构,输入层,卷积层,降采样,卷积,降采样 opts.alpha = 1;%学习率 opts.batchsize = 50;%每批训练数据的大小 opts.numepochs = 1;%训练的次数 %大体一看就是下面三个函数 %设置网络,初始化卷积核、偏置,第一个参数为网络的结构,第二个为训练的样本,第三个为训练的标签 cnn = cnnsetup(cnn, train_x, train_y); %训练网络,第一个参数为网络的结构,第二个为训练的样本,第三个为训练的标签,第四个为附加选项 cnn = cnntrain(cnn, train_x, train_y, opts); %测试网络,第一个参数为网络的结构,第二个为测试的样本,第三个为测试的标签,返回错误率和错误的标签 [er, bad] = cnntest(cnn, test_x, test_y); %plot mean squared error figure; plot(cnn.rL);%绘制均方误差曲线 disp(er);%显示误差 assert(er<0.12, 'Too big error');
cnnsetup.m(初始化网络)
function net = cnnsetup(net, x, y) %assert(~isOctave() || compare_versions(OCTAVE_VERSION, '3.8.0', '>='), %['Octave 3.8.0 or greater is required for CNNs as there is a bug in convolution in previous versions. %See http://savannah.gnu.org/bugs/?39314. Your version is ' myOctaveVersion]); %x为训练集(28x28x60000),y为训练集对应的标签(10x60000) %每层输入的特征图谱的层数,也就是上一层的输出的特征图谱的层数, %在这个代码中,只有卷积层才会改变网络特征图谱的层数 %所以在每个卷积层最后,都把inputmaps改为当前层的outputmaps %初始的特征图谱就是一张图像,而且只有一个通道,所以 inputmaps = 1 inputmaps = 1; %squeeze为移除单一的维度 %初始化mapsize,为训练集的样本的尺寸,由于取得是第一个样本,所以加入了squeeze来去除第三个维度 %mapsize作为经过卷积或池化后特征图谱的尺寸,是一个非常重要的参数。 mapsize = size(squeeze(x(:, :, 1))); %尤其注意这几个循环的参数的设定 %net.layers为一个单元数组,按顺序每层一个单元,所以numel(net.layers)为总层数 for l = 1 : numel(net.layers) % layer,numel(net.layers) 表示有多少层 %对于不同的层,用不同的方法来处理 %strcmp比较字符串,对于大小写敏感 if strcmp(net.layers{l}.type, 's')%降采样层 %subsampling层的mapsize,直接除以scale,非重叠池化 mapsize = mapsize / net.layers{l}.scale; %floor趋向于负无穷,assert生成错误,当条件违反 %这个语句,就是说明这个代码的卷积和池化每步的尺寸,都是严格设计的 assert(all(floor(mapsize)==mapsize), ['Layer ' num2str(l) ' size must be integer. Actual: ' num2str(mapsize)]); for j = 1 : inputmaps net.layers{l}.b{j} = 0;%bias统一设置为0 end end %strcmp比较字符串,对于大小写敏感 if strcmp(net.layers{l}.type, 'c')%卷积层 %卷积层大小为上层特征图谱大小-核大小+1,就是步长为1的卷积处理,常规的尺寸 mapsize = mapsize - net.layers{l}.kernelsize + 1; %fan_out为当前层需要学习的参数个数,由于权值共享,每层都有 net.layers{l}.kernelsize ^ 2 %个参数,乘以层数,就是当前层的参数个数 fan_out = net.layers{l}.outputmaps * net.layers{l}.kernelsize ^ 2; for j = 1 : net.layers{l}.outputmaps % output map %fan_out为假定,这层一个特征图谱对应于上层一个特征图谱,但是,这里假定的是这层的每一个特征图谱 %都和上一层的多个特征图谱相连,所有就是inputmaps * net.layers{l}.kernelsize ^ 2 %不过具体还是看后面的代码,这也不确定? fan_in = inputmaps * net.layers{l}.kernelsize ^ 2; for i = 1 : inputmaps % input map,对于每一个后层特征图,有多少个参数链到前层 %rand:在(0,1)内的标准正态分布,减去0.5,就是(-0.5,0.5),然后乘以2,就是(-1,1) %设置每层的权重,权重设置为:-1~1之间的随机数 * sqrt(6/(输入神经元数量+输出神经元数量)) %MATLAB中单元数组的格式为嵌套,多维单元数组是1xn签到者1xm,和数组的mxn不一样,但是调用还是一样的。 net.layers{l}.k{i}{j} = (rand(net.layers{l}.kernelsize) - 0.5) * 2 * sqrt(6 / (fan_in + fan_out)); end %设置每层的偏置,就是每个输出层,都有一个偏置 net.layers{l}.b{j} = 0; end inputmaps = net.layers{l}.outputmaps;%把上一层的输出变成下一层的输入 end end % 'onum' is the number of labels, that's why it is calculated using size(y, 1). %If you have 20 labels so the output of the network will be 20 neurons. % 'fvnum' is the number of output neurons at the last layer, the layer just before the output layer. % 'ffb' is the biases of the output neurons. % 'ffW' is the weights between the last layer and the output neurons. %Note that the last layer is fully connected to the output layer, that's why the size of the weights is (onum * fvnum) %prod为数组中元素的乘积,inputmaps改为了最后一层输出的特征图谱的层数 %因此这儿的作用就是计算输出层之前那层神经元的个数,fvnum=4×4×12=192 %fvnum是最后一层输出神经元的个数 fvnum = prod(mapsize) * inputmaps; %输出层的神经元个数,也就是标签数 onum = size(y, 1); %输出层偏置,这里是最后一层神经网络的设定 net.ffb = zeros(onum, 1); %输出层权重,这样定义也是由于最后一层和输出的标签是全连接的 net.ffW = (rand(onum, fvnum) - 0.5) * 2 * sqrt(6 / (onum + fvnum)); end
cnntrain.m(依次调用函数 cnnff.m,cnnbp.m,cnnapplygrads.m)
function net = cnntrain(net, x, y, opts) %net为网络,x为训练数据,y为标签,opts为训练参数 %m为样本数量,size(x)=[28*28*60000] m = size(x, 3); %总样本数除以训练时一批数据包含的图片数量,等于一次样本整体训练,需要的步数 numbatches = m / opts.batchsize; %步数必须是可以整除的 if rem(numbatches, 1) ~= 0 error('numbatches not integer'); end net.rL = [];%rL是最小均方差的平滑序列,绘图时使用 for i = 1 : opts.numepochs%对于样本整体迭代的次数 disp(['epoch ' num2str(i) '/' num2str(opts.numepochs)]);%显示当前迭代的次数 tic;%计开始 %每次整体训练,都重新打乱样本的顺序 kk = randperm(m); for l = 1 : numbatches%分成numbatches批,MNIST分了50批,训练每个batch batch_x = x(:, :, kk((l - 1) * opts.batchsize + 1 : l * opts.batchsize));%获取每批的训练样本和标签 batch_y = y(:, kk((l - 1) * opts.batchsize + 1 : l * opts.batchsize)); %很明显,关键的就是下面三个函数 net = cnnff(net, batch_x);%完成前向过程 net = cnnbp(net, batch_y);%完成误差传导和梯度计算过程 net = cnnapplygrads(net, opts);%应用梯度,模型更新 if isempty(net.rL)%net.L为模型的costfunction,即最小均方误差,net.rL是平滑后的序列 net.rL(1) = net.L; end net.rL(end + 1) = 0.99 * net.rL(end) + 0.01 * net.L; end toc;%计时结束 end end
cnnff.m(CNN的前向传播)
function net = cnnff(net, x) %网络的层数 n = numel(net.layers); %a是输入map,是一个[28,28,50]的序列,这里就是在每层增加一个单元数组a %作为当前层的激活? net.layers{1}.a{1} = x; %输入的层数,输入的是一个通道的图像 inputmaps = 1; for l = 2 : n % for each layer %和初始化类似,先判断是c还是s,然后实施对应的处理 %从第二层开始,所以用的是 if elseif 代码 if strcmp(net.layers{l}.type, 'c') % !!below下面 can probably be handled by insane(疯狂) matrix operations for j = 1 : net.layers{l}.outputmaps % for each output map %create temp output map %还是步长为1的卷积的基本操作,和初始化设置不同,这里是3维的处理,并且区分了输入和激活 %对于第二层,也就是第一个卷积层:z=zeros([28,28,50]-[4,4,0]=zeros[24,24,50] z = zeros(size(net.layers{l - 1}.a{1}) - [net.layers{l}.kernelsize - 1 net.layers{l}.kernelsize - 1 0]); for i = 1 : inputmaps %convolve with corresponding kernel and add to temp output map %这里是后层的每个特征图谱,都和前一层的所有特征图谱相连,这也是方便规范化编程。 %虽然在LeNet5中不是这样搞的,但是好像AlexNet之后都是这样搞,好像是方便GPU计算。 %这也算搞懂了,为什么里面这两个for是这样编写的,先output后input %一开始其实很不理解这两个for这样编排,从代码整体实验来说,还是这样比较好,LeNet5这样搞 %只能是针对某一特定的模型,可以每层都单独编写代码,但是现在的多层NN,这样编写太费劲了 %还是统一的实现比较方便。 z = z + convn(net.layers{l - 1}.a{i}, net.layers{l}.k{i}{j}, 'valid'); end %add bias, pass through nonlinearity %对于上一层所有特征图谱累加的结果,再通过sigmoid,由于sigmoid是单调递增的函数 %所以这样还是可以区分出不同输入的差异。 net.layers{l}.a{j} = sigm(z + net.layers{l}.b{j}); end % set number of input maps to this layers number of outputmaps %由于CNN,只会在卷积层会改变特征图谱的层数,所以在卷积层结尾都改变inputmaps为卷积层的输出 inputmaps = net.layers{l}.outputmaps;%本层的输出作为下层的输入 elseif strcmp(net.layers{l}.type, 's') % downsample %就是平均池化,不改变输出的层数,所以就是遍历inputmaps for j = 1 : inputmaps %由于这里的平均池化,都是尺寸为2的平均池化。就等于2x2的卷积核,每个元素都是0.25。 %所以才会出现下面这样的代码ones(net.layers{l}.scale)为2x2的单位矩阵(元素都为1) %net.layers{l}.scale ^ 2=4。所以结果为2x2的矩阵,元素都为0.25,这样也就是平静池化。 z = convn(net.layers{l - 1}.a{j}, ones(net.layers{l}.scale) / (net.layers{l}.scale ^ 2), 'valid'); %由于上面的卷积操作的步长为1进行的平均池化,但是池化的步长为scale。 %所以就把计算的结果,从1开始隔一个scale进行采样,就是平均池化的结果。这还是有点巧妙的 net.layers{l}.a{j} = z(1 : net.layers{l}.scale : end, 1 : net.layers{l}.scale : end, :); end end end %concatenate all end layer feature maps into vector %下面这个net.fv 就把所有输出的所有特征图谱整合成一个向量 net.fv = []; %numel(net.layers{n}.a) 是输出的特征图谱的层数 for j = 1 : numel(net.layers{n}.a) %取出最后一层,对应的特征图谱的尺寸,[4,4,opts.batchsize] %opts.batchsize为每个小批次样本的个数 sa = size(net.layers{n}.a{j}); %reshape(net.layers{n}.a{j}, sa(1) * sa(2), sa(3)) %就是把最后一层对应的特征图谱按照样本拉成列向量 %然后,由于;,循环下去,就是把每个样本输出的所有特征拉成了一个列向量net.fv net.fv = [net.fv; reshape(net.layers{n}.a{j}, sa(1) * sa(2), sa(3))]; end %feedforward into output perceptrons,最后一层的perceptrons,数据识别的结果 %输出乘以权值加上对应的偏置,再经过一个sigmoid成为了网络最后的输出 o,也就是数据识别的结果 %net.o = sigm(net.ffW * net.fv + repmat(net.ffb, 1, size(net.fv, 2)));%计算输出 %上面这个代码的效率还是不如下面的高,所以修改了一下。 net.o = sigm(bsxfun(@plus,net.ffW * net.fv,net.ffb)); end
cnnbp.m(计算CNN的bp,CNN反向传播的难点都在这,不过这代码里面池化层没有参数,所以简化了很多)
function net = cnnbp(net, y) %net为网络的整体参数,y为数据集的标签 %获取网络的层数,n=5 n = numel(net.layers); %误差,输出值和期望值之差,net.e尺寸为[10,50],50为样本数 net.e = net.o - y; % loss function,这就是基本的损失函数,均方差 net.L = 1/2* sum(net.e(:) .^ 2) / size(net.e, 2); %% backprop deltas %从最后一层的error倒推回来deltas ,和神经网络的bp有些类似 %output delta 输出层的残差,由于是最后一层,所以误差是这样单独计算的,这层和bp完全一样 %由于最后一层为一个简单的全连接的sigmoid,素以乘上了sigmoid的导数 %变量命名规则,带有d的都为deltas %od:output deltas输出的残差 %fvd:feature vector delta最后特征向量残差 %其余就是对应层的残差,直接在结构体net对应层中增加单元数组d %net.od尺寸为[10,50] net.od = net.e .* (net.o .* (1 - net.o)); % feature vector delta,特征向量误差size=192×50, %net.fvd尺寸为[192,50],net.ffW尺寸为[10,192] net.fvd = (net.ffW' * net.od); % only conv layers has sigm function %这个网络从函数cnnff.m可以看出,只有卷积层才有sigmoid处理 %池化层,z就是a,直接没有经过sigmoid激活 %所以加入了下面一个激活,但是由于这个网络的结构最后一层为下采样层 %所以这段代码,应该是作者加入为了方便规划化后面的代码写的 if strcmp(net.layers{n}.type, 'c') net.fvd = net.fvd .* (net.fv .* (1 - net.fv));%卷积层的误差需要进行求导 end %这是算delta的步骤 %这部分的计算参看Notes on Convolutional Neural Networks,其中的变化有些复杂 %和这篇文章里稍微有些不一样的是这个toolbox在subsampling(也就是pooling层)没有加sigmoid激活函数 %所以这地方还需仔细辨别 %这个toolbox里的subsampling是不用计算gradient的,而在上面那篇note里是计算了的 %net.layers{n}.a{1}为最后一层,激活的数据的尺寸 %专业一点说,就是最后一层特征map的大小。这里的最后一层都是指输出层的前一层 %由于最后一层为池化层,而这个代码,池化的特征没有经过sigmoid %由于这个网络,最后一层为12层的池化层,所以这里面选择的是12个数据中的其中一个 %由于小批次尺寸为50,所以这个sa尺寸为[4,4,50] sa = size(net.layers{n}.a{1}); %把最后一层特征map拉成一条向量,对于单个样本来说,特征维数是这样 fvnum = sa(1) * sa(2);%fvnum =16 % reshape feature vector deltas into output map style %遍历最后一层的所有特征图谱,numel(net.layers{n}.a)为最后一层特征图谱的层数 %本网络最后一层为12层的池化层,所以 numel(net.layers{n}.a)=12 for j = 1 : numel(net.layers{n}.a) %net.fvd为最后一层残差的反向累加,如果最后一层为池化层,由于没有经过sigmoid,所以直接导入 %如果最后一层为卷积层,所以要乘以sigmoid的导数 %net.fvd尺寸为[192,50],sa(1)=4, sa(2)=4, sa(3)=50,192=12x16,fvnum=16 %一开始为了计算方便,所以反向传输的参数被整成向量的形式,但是对于不同层的特征图谱 %反向传输计算的残差都是不一样的,所以把矩阵按照对应的层分割开来,这样就按照12层,每层都是4x4=16的特征图谱来reshape net.layers{n}.d{j} = reshape(net.fvd(((j - 1) * fvnum + 1) : j * fvnum, :), sa(1), sa(2), sa(3)); end %对应的特征图谱的残差和特征图谱的尺寸是一样的 %由于最后一层涉及到,向量化形式的特征的拆分问题,所以单独处理 %下面就是残差的逐层反向传播 for l = (n - 1) : -1 : 1 %这判断是卷积还是池化层,分开处理 if strcmp(net.layers{l}.type, 'c')%参见paper,注意这里只计算了'c'层的gradient,因为只有这层有参数 %如果是卷积层,那么后面是池化层,只要对于池化层的残差进行扩展即可 %而由于这个网络的设计,池化层不会改变特征图谱的层数,所以直接遍历卷积层的层数就可以 for j = 1 : numel(net.layers{l}.a) %由于残差和激活值都是3维数组(前两维为特征图谱的维度,最后一维为每批次样本个数) %所以缩放池化层的前两层的尺寸和卷积层一样(而最后的样本层不变),然后除以一个缩放系数,使得这层整体的残差不变。 %这样也符合池化的原理,不过这只是对于平均池化的,最大化池化是缩放到相同的尺寸,但是残差保留在对应的位置上 %由于这个池化层没有激活,所以是这层残差比较简单 %不知道 这个expand为util文件夹内,作者自己编写的函数,为对应维度的缩放 net.layers{l}.d{j} = net.layers{l}.a{j} .* (1 - net.layers{l}.a{j}) .* (expand(net.layers{l + 1}.d{j}, [net.layers{l + 1}.scale net.layers{l + 1}.scale 1]) / net.layers{l + 1}.scale ^ 2); end elseif strcmp(net.layers{l}.type, 's') %如果这层为池化层,那么后面一层为卷积层,卷积层特征图谱的层数会发生变化,而且一般的变化趋势是层数增加 %先遍历当前层特征图谱的层数 for i = 1 : numel(net.layers{l}.a) %定义z为当前层(池化层)特征图谱中一层的尺寸,比如第3层位池化层,尺寸为[12,12,50] z = zeros(size(net.layers{l}.a{1})); %然后遍历后一层特征图谱的层数,潜在的意思,就是卷积过程,后面的每一层都是遍历前一层的所有特征图谱 %所以bp传输,残差传输也是从后面所有的层传给前面某一层 for j = 1 : numel(net.layers{l + 1}.a) %卷积层的反向传输就是这样,卷积核先翻转180度,然后convn默认会翻转卷积核180度,这样就是一个相关操作 %从计算上,能够理解这样计算结果是正确的,但是还是不能够理解里面的数学原理,不懂反卷积呀。 z = z + convn(net.layers{l + 1}.d{j}, rot180(net.layers{l + 1}.k{i}{j}), 'full'); end net.layers{l}.d{i} = z; end end end %% calc gradients %上面计算残差,下面计算梯度 %参见paper,注意这里只计算了'c'层的gradient,因为只有这层有参数 % 这里与 Notes on Convolutional Neural Networks 中不同,这里的 子采样 层没有参数,也没有激活函数,所以在子采样层是没有需要求解的参数的 for l = 2 : n if strcmp(net.layers{l}.type, 'c') for j = 1 : numel(net.layers{l}.a) %计算卷积核的导数,卷积层对于前面层的每一个特征图谱都有一个对应的卷积核 %所以遍历前面层的每一个特征图谱 for i = 1 : numel(net.layers{l - 1}.a) % dk 保存的是 误差对卷积核 的导数 %size(net.layers{l}.d{j}, 3)为每批的样本数 %convn(flipall(net.layers{l - 1}.a{i}), net.layers{l}.d{j}, 'valid') %为对两个三维的数组卷积,得到了二维的结果,感觉对于第三维进行了累加。所以除以一个样本个数,得到均值 net.layers{l}.dk{i}{j} = convn(flipall(net.layers{l - 1}.a{i}), net.layers{l}.d{j}, 'valid') / size(net.layers{l}.d{j}, 3); end % db 保存的是 误差对于bias基 的导数 %对于所有样本的所有偏差进行累加,然后除以样本个数得到偏差 net.layers{l}.db{j} = sum(net.layers{l}.d{j}(:)) / size(net.layers{l}.d{j}, 3); end end end %计算尾部单层感知器的参数,最后一层perceptron的gradient的计算 %net.od尺寸为[10,50],net.fv尺寸为[192,50] %net.dffW尺寸为[10,192]为计算的最后全连接层权系数的偏差 net.dffW = net.od * (net.fv)' / size(net.od, 2);%size(net.0d)=50,修改量,求和/50 %net.dffb尺寸为[10,1],就是样本整体的残差,不过这没有取均值 net.dffb = mean(net.od, 2);%第二维取均值 function X = rot180(X) X = flipdim(flipdim(X, 1), 2); end end
cnnapplygrads.m
function net = cnnapplygrads(net, opts) for l = 2 : numel(net.layers)%从第二层开始 if strcmp(net.layers{l}.type, 'c')%对于每个卷积层, for j = 1 : numel(net.layers{l}.a)%枚举改层的每个输出%枚举所有卷积核的net.layers{l}.k{ii}{j} for ii = 1 : numel(net.layers{l - 1}.a)%枚举上层的每个输出 % 这里没什么好说的,就是普通的权值更新的公式:W_new = W_old - alpha * de/dW(误差对权值导数) net.layers{l}.k{ii}{j} = net.layers{l}.k{ii}{j} - opts.alpha * net.layers{l}.dk{ii}{j}; end %修改偏置 net.layers{l}.b{j} = net.layers{l}.b{j} - opts.alpha * net.layers{l}.db{j}; end end end %单层感知器的更新 net.ffW = net.ffW - opts.alpha * net.dffW; net.ffb = net.ffb - opts.alpha * net.dffb; end
cnntest.m
function [er, bad] = cnntest(net, x, y) %输出错误率,错误的索引;输入 训练好的网络,测试样本,测试样本的标签 % feedforward net = cnnff(net, x);%前向传播 [~, h] = max(net.o);%找到输出的最大值 [~, a] = max(y);%找到真实的标签 bad = find(h ~= a);%找到标签不等的索引 er = numel(bad) / size(y, 2);%计算错误率。其中y的第二维是测试样本的数量 end