本節将會講到卷積神經網絡的實作。說到卷積神經網絡,在圖像識别和目标檢測方面已經取得了不錯的效果,為什麼要叫做卷積神經網絡呢?主要是因為在特征提取的時候,輸入圖像會通過卷積核對原始圖像進行特征抽取,然後再通過神經網絡進一步進行特征提取,也可以稱為降維,再通過分類器得到分類或者識别的結果,斯坦福大學研究人員通過卷積神經網絡訓練貓的圖像,在 YouTube 視訊中找到了關于貓的視訊,這也是一個強大的應用。此外在圖檔和視訊場景了解方面, Google 和斯坦福的研究人員,研發出了一款能夠描述圖檔場景的軟體,裡面的算法也有卷積神經網絡,如下圖所示:

人:“A group of men playing Frisbee in the park.”
計算機:“A group of young people playing a game of Frisbee.”
人: “Elephants of mixed ages standing in a muddy landscape.” 計算機: “A herd of elephants walking across a dry grass field.”
卷積神經網絡也應用于為物體貼标簽,下面是執行個體,我想它如果運用于Google眼鏡的話,“當我們看到一幅自然圖檔,眼鏡就會顯示你看到的所有東西,并給于标簽,那麼人的學習認知能力有多強大啊。
圖檔貼标簽執行個體
目前,卷積神經網絡無疑成為計算機視覺以及模式識别領域的熱門話題,它對處理多分類,大型的圖檔分類有着非常好的效果,本文主要包括以下幾個内容:
1.卷積神經網絡的整體架構
1.1 卷積層
1.2 采樣層
1.3 全連接配接層
2.卷積神經網絡的建立(前饋網絡)
3.卷積和相關的差別
4.核函數的標明
5.BP回報調節參數(回報網絡)
6.優化方法的標明
7.實驗
1.卷積神經網絡的整體架構
下面先給出圖,然後再說明這個架構。
在上圖的這個架構下,輸入的是RGB圖像,用到顔色資訊,如果你處理的灰階圖像,那麼你的輸入層數就為1層,但是這會降低最後識别率。輸入圖像(input)為32×32×3,當然你要處理的圖像尺寸不一樣,那麼你的輸入層的大小就不一樣了,輸入的圖像經過一個3×5×5的濾波器就得到了一個featuremap,由于此處的濾波未填充邊緣,一個濾波器就産生一張28×28的featuremap,那麼用64個濾波器進行卷積,就會産生64張featuremaps,這就叫做特征圖(卷積層C)。不同的濾波器提取出來的特征當然不一樣,這時就需要通過采樣層了,采樣層采用的是2×2的核,采用的均值采樣,最終得到的featuremaps變為14×14×64(采樣層S),得到特征再利用64個5×5×64的濾波器濾波得到64張10×10×64(卷積層C),這個時候是立體的濾波,不同于平面濾波,但是原理都一樣。又通過2×2的核采樣得到64張5×5的featuremaps(采樣層S),這時就有64×5×5=1600個神經元了,(當然你還可以用5×5×64的濾波器濾波,這時得到featuremap就為1×1了,當然這隻是讨論),這1600個神經元,是最後一層采樣層的每一張featuremap拉伸成一個向量的結果,一張featuremap的向量為25維,那麼64張就為1600維。接下來做的工作就是降維了,可以通過普通神經網絡進行降維,此處稱為“全連接配接層”,這裡我設定第一層全連接配接層f1的神經元個數為1024,第二層全連接配接層f的神經元個數為512,第二層神經元就稱為一張圖檔的特征值,這時你要連接配接一個分類器,此處的分類器為softmax分類器,可以實作多分類的。
這個架構可以參見前兩篇博文:
http://blog.csdn.net/hlx371240/article/details/41208515
http://blog.csdn.net/hlx371240/article/details/40015395
這樣一個卷積神經網絡的架構就介紹完了,裡面的參數(如卷積核的大小,卷積核的數量,采樣層的采樣方式,如均值,max值,全連接配接層神經元的個數,層數)大家都可以自己進行設計,沒有統一的标準。
下面是MATLAB給出的網絡結構
[plain] view plain copy
- <span style="font-family:Times New Roman;font-size:14px;"><span style="font-family:Times New Roman;"><span style="font-size:18px;">cnn.layers = {
- struct('type', 'i','inputmaps',3 ,'inputsize',32) %input layer
- struct('type', 'c', 'outputmaps', 64, 'kernelsize', 5) %convolution layer
- struct('type', 's', 'scale', 2) %sub sampling layer
- struct('type', 'c', 'outputmaps', 64, 'kernelsize', 5) %convolution layer
- struct('type', 's', 'scale', 2) %subsampling layer
- struct('type', 'f1', 'neuron', 1024) %full-connection layer
- struct('type', 'f', 'neuron', 512) %full-connection layer
- struct('type', 'o', 'scale', 10)
- }; </span></span></span>
可以看到該卷積神經網絡一共有 8層(層數可以自己設定),可以看出,整個網絡有1層輸入層,2層卷積層,2層采樣層,2層全連階層,以及1層輸出層。
1.1卷積層
此處的卷積跟圖像裡面的卷積一樣,但是不是事先訓練好的,而是随機産生的卷積核,而且卷積核的大小都是自己設定的。你可以設定為任意尺寸大小,如5×5,6×6等,當然卷積核不一定都為正方形,可以是矩形,如7×6,8×9等等都行,這隻是單層核, 也可以是多層核,假如輸入圖像是RGB的圖像,含有3通道,那麼你需要設定一個3D核,一個3D核與輸入圖像就得到一張特征圖featuremap,64個3D核就得到64張featuremaps。
另外我們來看卷積過後的圖像大小,其實在前面的博文中已經指出來了,卷積神經網絡做卷積的時候,原圖是不進行擴充的,是以卷積過後圖檔是會減小的,比如,20×20的圖檔,現在有5×5的核去卷積,那麼就得到(20-5)+1×(20-5)+1=16×16大小的featuremap,對于3D核也是一樣的。
可以參見博文:http://blog.csdn.net/hlx371240/article/details/41208515
1.2 采樣層
采樣層一般是正方形的核函數,這個核函數可以是均值核,也可以是求取核區域的最大值,用均值核叫做meanpooling,用求取區域最大值的核稱為maxpooling。對于卷積得到的大小為30×30的圖像,假如我們采用2×2的核函數采樣,那麼就得到15×15大小的圖像,如果采用3×3的核函數,就産生了10×10大小的圖像,如果有人用4×4的核,那麼30不能整除4,是以選擇這個尺寸的核函數不太合适,你可以選擇5×5的核。采樣是圖像特征的進一步抽取。
可以參見博文:http://blog.csdn.net/hlx371240/article/details/41208515
2.卷積神經網絡的建立(前饋網絡)
卷積神經網絡分為前饋網絡和回報網絡,前饋網絡可以說是得到最終機率值然後進行判别。
第一層:一個5×5×3的卷積核和輸入的32×32×3的RGB圖像做卷積,卷積核是3層,RGB也為3層,他們分别做卷積然後相加,這樣就得到一張28×28的圖像,此處的圖像就為一個通道了。我們可以用64個卷積核對圖像進行卷積,這樣就得到64張28×28大小的圖像。(每一個卷積後得到值都要經過非線性變換,如一些非線性的核函數)卷積後再用2×2的采樣核,得到14×14大小的圖像,由于上一層有64張featuremaps,采樣不會改變featuremaps的數量,還是64個featuremaps。然後經過第二次卷積,這時的卷積核是5×5×64的大小,是個3D的核函數,一共含有64層,每一層與上層采樣的64張featuremaps分别做卷積得到一張featuremap,同樣的用64個5×5×64核函數就得到64張featuremaps,(每一個卷積後得到值都要經過非線性變化)。卷積後産生了64張10×10大小的圖像,這時再經過采樣得到64個5×5大小的子圖,一共加起來為1600個神經元,當然此時還有可以用64個5×5×64的卷積核卷積,得到64個單獨的神經元。現在我沒有加入這層,而是1600個神經元。這時就可以用普通的神經網絡再進行降維。
當然最後一層接的是Softmax分類器。
可以參見博文:http://blog.csdn.net/hlx371240/article/details/40015395
Ng的文章Sparse autoencoder:http://nlp.stanford.edu/~socherr/sparseAutoencoder_2011new.pdf
cnnff.m
[plain] view plain copy
- <span style="font-family:Times New Roman;font-size:14px;">function net = cnnff(net, x)
- [A B C D]=size(x);
- inputmaps = net.layers{1}.inputmaps; % 輸入層隻有一個特征map,也就是原始的輸入圖像
- n = numel(net.layers); % 層數
- for i=1:inputmaps
- channel=reshape(x(:,:,i,:),net.layers{1}.inputsize,net.layers{1}.inputsize,D);
- net.layers{1}.a{i}=channel; % 網絡的第一層就是輸入,但這裡的輸入包含了多個訓練圖像
- end
- for l = 2 : n % for each layer
- if strcmp(net.layers{l}.type, 'c') % 卷積層
- % !!below can probably be handled by insane matrix operations
- % 對每一個輸入map,或者說我們需要用outputmaps個不同的卷積核去卷積圖像
- for j = 1 : net.layers{l}.outputmaps % for each output map
- % create temp output map
- % 對上一層的每一張特征map,卷積後的特征map的大小就是
- % (輸入map寬 - 卷積核的寬 + 1)* (輸入map高 - 卷積核高 + 1)
- % 對于這裡的層,因為每層都包含多張特征map,對應的索引儲存在每層map的第三維
- % 是以,這裡的z儲存的就是該層中所有的特征map了
- z = zeros(size(net.layers{l - 1}.a{1}) - [net.layers{l}.kernelsize - 1 net.layers{l}.kernelsize - 1 0]);
- for i = 1 : inputmaps % for each input map
- % convolve with corresponding kernel and add to temp output map
- % 将上一層的每一個特征map(也就是這層的輸入map)與該層的卷積核進行卷積
- % 然後将對上一層特征map的所有結果加起來。也就是說,目前層的一張特征map,是
- % 用一種卷積核去卷積上一層中所有的特征map,然後所有特征map對應位置的卷積值的和
- % 另外,有些論文或者實際應用中,并不是與全部的特征map連結的,有可能隻與其中的某幾個連接配接
- z = z + convn(net.layers{l - 1}.a{i}, net.layers{l}.k{i}{j}, 'valid');
- end
- % add bias, pass through nonlinearity
- % 加上對應位置的基b,然後再用sigmoid函數算出特征map中每個位置的激活值,作為該層輸出特征map
- net.layers{l}.a{j} = relu(z + net.layers{l}.b{j});
- end
- % set number of input maps to this layers number of outputmaps
- inputmaps = net.layers{l}.outputmaps;
- elseif strcmp(net.layers{l}.type, 's') % 下采樣層
- % downsample
- for j = 1 : inputmaps
- % !! replace with variable
- % 例如我們要在scale=2的域上面執行mean pooling,那麼可以卷積大小為2*2,每個元素都是1/4的卷積核
- z = convn(net.layers{l - 1}.a{j}, ones(net.layers{l}.scale) / (net.layers{l}.scale ^ 2), 'valid');
- % 因為convn函數的預設卷積步長為1,而pooling操作的域是沒有重疊的,是以對于上面的卷積結果
- % 最終pooling的結果需要從上面得到的卷積結果中以scale=2為步長,跳着把mean pooling的值讀出來
- net.layers{l}.a{j} = z(1 : net.layers{l}.scale : end, 1 : net.layers{l}.scale : end, :);
- end
- elseif strcmp(net.layers{l}.type, 'f1')
- net.layers{l-1}.fv = [];
- for j = 1 : numel(net.layers{l-1}.a) % 最後一層的特征map的個數
- sa = size(net.layers{l-1}.a{j}); % 第j個特征map的大小
- % 将所有的特征map拉成一條列向量。還有一維就是對應的樣本索引。每個樣本一列,每列為對應的特征向量
- net.layers{l-1}.fv = [net.layers{l-1}.fv; reshape(net.layers{l-1}.a{j}, sa(1) * sa(2), sa(3))];
- end
- net.layers{l}.a = relu(net.layers{l-1}.ffW * net.layers{l-1}.fv + repmat(net.layers{l-1}.ffb, 1, size(net.layers{l-1}.fv, 2)));
- elseif strcmp(net.layers{l}.type, 'f')
- net.layers{l}.a = relu(net.layers{l-1}.ffW * net.layers{l-1}.a+repmat(net.layers{l-1}.ffb, 1, size(net.layers{l-1}.a, 2)));
- elseif strcmp(net.layers{l}.type, 'o')
- net.layers{l}.a = relu(net.layers{l-1}.ffW * net.layers{l-1}.a+repmat(net.layers{l-1}.ffb, 1, size(net.layers{l-1}.a, 2)));
- end
- end
- end</span>
3.卷積和相關的差別
下面直接給一張圖來說明卷積和相關的差別
相關是核直接與原圖像做線性運算,而卷積是需要把核做180度然後再做線性相加運算。
rot180.m
[plain] view plain copy
- <span style="font-family:Times New Roman;">function X = rot180(X)
- X = flipdim(flipdim(X, 1), 2);
- end</span>
4.核函數的標明
下面介紹3種核函數tanh,sigmiod,Relu核函數,分别給出它們的圖像
下面分别畫出它們的導數的圖像
一般的神經網絡選用的是sigmoid核函數,但是對于深度網絡來說,回報時的系數太小,導緻從最後一層回報到第一層的參數已經太小,深度網絡很多人選用Relu核,在這篇文章中Imagenet classification with deep convolutional neural networks選用的Relu核,并産生了很好的效果,代碼在https://code.google.com/p/cuda-convnet/中,這個代碼是用C++和python寫的。
5.BP回報調節參數(回報網絡)
參考文章:http://nlp.stanford.edu/~socherr/sparseAutoencoder_2011new.pdf
Jake Bouvrie, Notes on Convolutional Neural Networks
現在我隻講卷積回報,普通神經網絡的回報可以參見文章http://nlp.stanford.edu/~socherr/sparseAutoencoder_2011new.pdf
第一層全連接配接層通過回報得到1600個神經元的誤差d,然後組合成5×5×64的形式,這個是誤差featuremaps,上采樣得到10×10×64的featuremaps,這是卷積層的誤差d,一共有64層,每一層需要跟前饋網絡的14×14×64分别做卷積,得到一個5×5×64的核的誤差d(也可以稱為梯度),這樣64層分别與前饋網絡做卷積就能到64個5×5×64個核函數的誤差d,可以參見上圖的3,最後要乘以核函數的導數,再通過4求出每一個參數的△W和△b.
cnnbp.m
[plain] view plain copy
- <span style="font-family:Times New Roman;font-size:14px;">function net = cnnbp(net, y)</span>
[plain] view plain copy
- <div style="text-align: left;"><span style="font-family:Times New Roman;font-size:14px;"><strong style="line-height: 26px; color: rgb(102, 102, 0); background-color: rgb(255, 255, 255);"></strong></span></div><span style="font-family:Times New Roman;font-size:14px;">n = numel(net.layers); % 網絡層數
- % error
- net.layers{n}.e = net.layers{n}.a - y;
- % loss function
- % 代價函數是 均方誤差
- net.layers{n}.L = 1/2* sum(net.layers{n}.e(:) .^ 2) / size(net.layers{n}.e, 2);
- %% backprop deltas
- % 這裡可以參考 UFLDL 的 反向傳導算法 的說明
- % 輸出層的 靈敏度 或者 殘差
- net.layers{8}.od = net.layers{8}.e .* ReluInv(net.layers{8}.a); % output delta
- % 殘差 反向傳播回 前一層
- net.layers{7}.d = (net.layers{7}.ffW' * net.layers{8}.od) .* ReluInv(net.layers{7}.a); % feature vector delta
- net.layers{6}.d = (net.layers{6}.ffW' * net.layers{7}.d) .* ReluInv(net.layers{6}.a);
- net.layers{5}.fvd = (net.layers{5}.ffW' * net.layers{6}.d);
- % net.layers{l-1}.d = net.layers{l-1}.fvd .* (net.layers{l-1}.o .* (1-net.layers{l-1}));
- % if strcmp(net.layers{n}.type, 'c') % only conv layers has sigm function
- % net.fvd = net.fvd .* (net.fv .* (1 - net.fv));
- % end
- % reshape feature vector deltas into output map style
- sa = size(net.layers{5}.a{1}); % 最後一層特征map的大小。這裡的最後一層都是指輸出層的前一層
- fvnum = sa(1) * sa(2); % 因為是将最後一層特征map拉成一條向量,是以對于一個樣本來說,特征維數是這樣
- for j = 1 : numel(net.layers{5}.a) % 最後一層的特征map的個數
- % 在fvd裡面儲存的是所有樣本的特征向量(在cnnff.m函數中用特征map拉成的),是以這裡需要重新
- % 變換回來特征map的形式。d 儲存的是 delta,也就是 靈敏度 或者 殘差
- net.layers{5}.d{j} = reshape(net.layers{5}.fvd(((j - 1) * fvnum + 1) : j * fvnum, :), sa(1), sa(2), sa(3));
- end
- % 對于 輸出層前面的層(與輸出層計算殘差的方式不同)
- for l = (n - 4) : -1 : 1
- if strcmp(net.layers{l}.type, 'c')
- for j = 1 : numel(net.layers{l}.a) % 該層特征map的個數
- % net.layers{l}.d{j} 儲存的是 第l層 的 第j個 map 的 靈敏度map。 也就是每個神經元節點的delta的值
- % expand的操作相當于對l+1層的靈敏度map進行上采樣。然後前面的操作相當于對該層的輸入a進行sigmoid求導
- % 這條公式請參考 Notes on Convolutional Neural Networks
- % for k = 1:size(net.layers{l + 1}.d{j}, 3)
- % net.layers{l}.d{j}(:,:,k) = net.layers{l}.a{j}(:,:,k) .* (1 - net.layers{l}.a{j}(:,:,k)) .* kron(net.layers{l + 1}.d{j}(:,:,k), ones(net.layers{l + 1}.scale)) / net.layers{l + 1}.scale ^ 2;
- % end
- net.layers{l}.d{j} = ReluInv(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) % 第l層特征map的個數
- z = zeros(size(net.layers{l}.a{1}));
- for j = 1 : numel(net.layers{l + 1}.a) % 第l+1層特征map的個數
- 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
- % 這裡與 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 儲存的是 誤差對卷積核 的導數
- 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
- elseif strcmp(net.layers{l}.type, 'f1')
- net.layers{l-1}.dffW = net.layers{l}.d * net.layers{l-1}.fv'/ size(net.layers{l}.d, 2);
- net.layers{l-1}.dffb = mean(net.layers{l}.d, 2);
- elseif strcmp(net.layers{l}.type, 'f')
- net.layers{l-1}.dffW = net.layers{l}.d * net.layers{l-1}.a'/ size(net.layers{l}.d, 2);
- net.layers{l-1}.dffb = mean(net.layers{l}.d, 2);
- elseif strcmp(net.layers{l}.type, 'o')
- net.layers{l-1}.dffW = net.layers{l}.od * net.layers{l-1}.a'/ size(net.layers{l}.od, 2);
- net.layers{l-1}.dffb = mean(net.layers{l}.od, 2);
- end
- end
- end</span>
6.優化方法標明
優化方法包括梯度法,共轭梯度法,牛頓法,拟牛頓法,但是很多國外大牛都直接用随機批量梯度法,學習率(步長)是按照經驗取的,下面給出更新公式,可以結合上面一節。
大家在更新的時候不用管λW這一項,m代表每次批量處理的圖像。
7.實驗
實驗采用的是cifar-10資料庫,如下圖所示
cnnexample.m
[plain] view plain copy
- <span style="font-family:Times New Roman;font-size:14px;">clear all; close all; clc; </span>
[plain] view plain copy
- <div style="text-align: left;"><span style="font-family:Times New Roman;font-size:14px;"><strong style="color: rgb(51, 0, 153); line-height: 26px; text-align: center; background-color: rgb(255, 255, 255);"></strong></span></div><span style="font-family:Times New Roman;font-size:14px;">load('traindata.mat');
- load('testdata.mat');
- load('trainlabel.mat');
- load('testlabel.mat');
- train_x = traindata;
- test_x = testdata;
- trainlabel=double(trainlabel);
- trainlabel(trainlabel==0) = 10;
- train_y = full(sparse(trainlabel, 1:50000, 1));
- testlabel=double(testlabel);
- testlabel(testlabel==0) = 10;
- test_y = full(sparse(testlabel, 1:10000, 1));
- %% ex1
- %will run 1 epoch in about 200 second and get around 11% error.
- %With 100 epochs you'll get around 1.2% error
- clear traindata testdata trainlabel testlabel
- cnn.layers = {
- struct('type', 'i','inputmaps',3 ,'inputsize',32) %input layer
- struct('type', 'c', 'outputmaps', 64, 'kernelsize', 5) %convolution layer
- struct('type', 's', 'scale', 2) %sub sampling layer
- struct('type', 'c', 'outputmaps', 64, 'kernelsize', 5) %convolution layer
- struct('type', 's', 'scale', 2) %subsampling layer
- struct('type', 'f1', 'neuron', 1024) %full-connection layer
- struct('type', 'f', 'neuron', 512) %full-connection layer
- struct('type', 'o', 'scale', 10)
- };
- % 這裡把cnn的設定給cnnsetup,它會據此建構一個完整的CNNs網絡,并傳回
- cnn = cnnsetup(cnn, train_x, train_y);
- % 學習率
- opts.alpha = 0.5;
- % 每次挑出一個batchsize的batch來訓練,也就是每用batchsize個樣本就調整一次權值,而不是
- % 把所有樣本都輸入了,計算所有樣本的誤差了才調整一次權值
- opts.batchsize = 200;
- % 訓練次數,用同樣的樣本集。我訓練的時候:
- % 1的時候 11.41% error
- % 5的時候 4.2% error
- % 10的時候 2.73% error
- opts.numepochs = 50;
- % 然後開始把訓練樣本給它,開始訓練這個CNN網絡
- cnn = cnntrain(cnn, train_x, train_y, opts);
- % 然後就用測試樣本來測試
- [er, bad] = cnntest(cnn, test_x, test_y);
- %plot mean squared error
- plot(cnn.rL);
- %show test error
- disp([num2str(er*100) '% error']); </span>
cnntrain.m
[plain] view plain copy
- <span style="font-family:Times New Roman;font-size:14px;">function net = cnntrain(net, x, y, opts)
- m = size(x, 4); % m 儲存的是 訓練樣本個數
- numbatches = m / opts.batchsize;
- % rem: Remainder after division. rem(x,y) is x - n.*y 相當于求餘
- % rem(numbatches, 1) 就相當于取其小數部分,如果為0,就是整數
- if rem(numbatches, 1) ~= 0
- error('numbatches not integer');
- end
- net.rL = [];
- for i = 1 : opts.numepochs
- % disp(X) 列印數組元素。如果X是個字元串,那就列印這個字元串
- disp(['epoch ' num2str(i) '/' num2str(opts.numepochs)]);
- % tic 和 toc 是用來計時的,計算這兩條語句之間所耗的時間
- tic;
- % P = randperm(N) 傳回[1, N]之間所有整數的一個随機的序列,例如
- % randperm(6) 可能會傳回 [2 4 5 6 1 3]
- % 這樣就相當于把原來的樣本排列打亂,再挑出一些樣本來訓練
- kk = randperm(m);
- for l = 1 : numbatches
- % 取出打亂順序後的batchsize個樣本和對應的标簽
- 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); % Feedforward
- % 得到上面的網絡輸出後,通過對應的樣本标簽用bp算法來得到誤差對網絡權值
- %(也就是那些卷積核的元素)的導數
- net = cnnbp(net, batch_y); % Backpropagation
- % 得到誤差對權值的導數後,就通過權值更新方法去更新權值
- net = cnnapplygrads(net, opts);
- if isempty(net.rL)
- net.rL(1) = net.layers{8}.L; % 代價函數值,也就是誤內插補點
- end
- net.rL(end + 1) = 0.99 * net.rL(end) + 0.01 * net.layers{8}.L; % 儲存曆史的誤內插補點,以便畫圖分析
- end
- toc;
- end
- end </span>
cnntest.m [plain] view plain copy
- <span style="font-family:Times New Roman;font-size:14px;">function [er, bad] = cnntest(net, x, y)
- % feedforward
- net = cnnff(net, x); % 前向傳播得到輸出
- % [Y,I] = max(X) returns the indices of the maximum values in vector I
- [~, h] = max(net.o); % 找到最大的輸出對應的标簽
- [~, a] = max(y); % 找到最大的期望輸出對應的索引
- bad = find(h ~= a); % 找到他們不相同的個數,也就是錯誤的次數
- er = numel(bad) / size(y, 2); % 計算錯誤率
- end </span>
flipall.m
[plain] view plain copy
- <span style="font-family:Times New Roman;font-size:14px;">function X=flipall(X)
- for i=1:ndims(X)
- X = flipdim(X,i);
- end
- end</span>
relu.m [plain] view plain copy
- <span style="font-family:Times New Roman;font-size:14px;">function X = relu(P)
- X=log(1+exp(P));
- end</span>
ReluInv.m
[plain] view plain copy
- <span style="font-family:Times New Roman;font-size:14px;">function reluInv=ReluInv(x)
- reluInv = exp(x)./(1 + exp(x));
- end</span>
expand.m [plain] view plain copy
- <span style="font-family:Times New Roman;font-size:14px;">function B = expand(A, S)
- if nargin < 2
- error('Size vector must be provided. See help.');
- end
- SA = size(A); % Get the size (and number of dimensions) of input.
- if length(SA) ~= length(S)
- error('Length of size vector must equal ndims(A). See help.')
- elseif any(S ~= floor(S))
- error('The size vector must contain integers only. See help.')
- end
- T = cell(length(SA), 1);
- for ii = length(SA) : -1 : 1
- H = zeros(SA(ii) * S(ii), 1); % One index vector into A for each dim.
- H(1 : S(ii) : SA(ii) * S(ii)) = 1; % Put ones in correct places.
- T{ii} = cumsum(H); % Cumsumming creates the correct order.
- end
- B = A(T{:}); </span>
cnnsetup.m
[plain] view plain copy
- <span style="font-family:Times New Roman;font-size:14px;">function net = cnnsetup(net, x, y)
- inputmaps = net.layers{1}.inputmaps;
- % B=squeeze(A) 傳回和矩陣A相同元素但所有單一維都移除的矩陣B,單一維是滿足size(A,dim)=1的維。
- % train_x中圖像的存放方式是三維的reshape(train_x',28,28,60000),前面兩維表示圖像的行與列,
- % 第三維就表示有多少個圖像。這樣squeeze(x(:, :, 1))就相當于取第一個圖像樣本後,再把第三維
- % 移除,就變成了28x28的矩陣,也就是得到一幅圖像,再size一下就得到了訓練樣本圖像的行數與列數了
- mapsize = size(squeeze(x(:, :, 1)));
- % 下面通過傳入net這個結構體來逐層建構CNN網絡
- % n = numel(A)傳回數組A中元素個數
- % net.layers中有五個struct類型的元素,實際上就表示CNN共有五層,這裡範圍的是5
- for l = 1 : numel(net.layers) % layer
- if strcmp(net.layers{l}.type, 's') % 如果這層是 子采樣層
- % subsampling層的mapsize,最開始mapsize是每張圖的大小28*28
- % 這裡除以scale=2,就是pooling之後圖的大小,pooling域之間沒有重疊,是以pooling後的圖像為14*14
- % 注意這裡的右邊的mapsize儲存的都是上一層每張特征map的大小,它會随着循環進行不斷更新
- mapsize = floor(mapsize / net.layers{l}.scale);
- for j = 1 : inputmaps % inputmap就是上一層有多少張特征圖
- net.layers{l}.b{j} = 0; % 将偏置初始化為0
- end
- end
- if strcmp(net.layers{l}.type, 'c') % 如果這層是 卷積層
- % 舊的mapsize儲存的是上一層的特征map的大小,那麼如果卷積核的移動步長是1,那用
- % kernelsize*kernelsize大小的卷積核卷積上一層的特征map後,得到的新的map的大小就是下面這樣
- mapsize = mapsize - net.layers{l}.kernelsize + 1;
- % 該層需要學習的參數個數。每張特征map是一個(後層特征圖數量)*(用來卷積的patch圖的大小)
- % 因為是通過用一個核視窗在上一個特征map層中移動(核視窗每次移動1個像素),周遊上一個特征map
- % 層的每個神經元。核視窗由kernelsize*kernelsize個元素組成,每個元素是一個獨立的權值,是以
- % 就有kernelsize*kernelsize個需要學習的權值,再加一個偏置值。另外,由于是權值共享,也就是
- % 說同一個特征map層是用同一個具有相同權值元素的kernelsize*kernelsize的核視窗去感受輸入上一
- % 個特征map層的每個神經元得到的,是以同一個特征map,它的權值是一樣的,共享的,權值隻取決于
- % 核視窗。然後,不同的特征map提取輸入上一個特征map層不同的特征,是以采用的核視窗不一樣,也
- % 就是權值不一樣,是以outputmaps個特征map就有(kernelsize*kernelsize+1)* outputmaps那麼多的權值了
- % 但這裡fan_out隻儲存卷積核的權值W,偏置b在下面獨立儲存
- fan_out = net.layers{l}.outputmaps * net.layers{l}.kernelsize ^ 2;
- for j = 1 : net.layers{l}.outputmaps % output map
- % fan_out儲存的是對于上一層的一張特征map,我在這一層需要對這一張特征map提取outputmaps種特征,
- % 提取每種特征用到的卷積核不同,是以fan_out儲存的是這一層輸出新的特征需要學習的參數個數
- % 而,fan_in儲存的是,我在這一層,要連接配接到上一層中所有的特征map,然後用fan_out儲存的提取特征
- % 的權值來提取他們的特征。也即是對于每一個目前層特征圖,有多少個參數鍊到前層
- fan_in = inputmaps * net.layers{l}.kernelsize ^ 2;
- for i = 1 : inputmaps % input map
- % 随機初始化權值,也就是共有outputmaps個卷積核,對上層的每個特征map,都需要用這麼多個卷積核
- % 去卷積提取特征。
- % rand(n)是産生n×n的 0-1之間均勻取值的數值的矩陣,再減去0.5就相當于産生-0.5到0.5之間的随機數
- % 再 *2 就放大到 [-1, 1]。然後再乘以後面那一數,why?
- % 反正就是将卷積核每個元素初始化為[-sqrt(6 / (fan_in + fan_out)), sqrt(6 / (fan_in + fan_out))]
- % 之間的随機數。因為這裡是權值共享的,也就是對于一張特征map,所有感受野位置的卷積核都是一樣的
- % 是以隻需要儲存的是 inputmaps * outputmaps 個卷積核。
- 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; % 将偏置初始化為0
- end
- % 隻有在卷積層的時候才會改變特征map的個數,pooling的時候不會改變個數。這層輸出的特征map個數就是
- % 輸入到下一層的特征map個數
- inputmaps = net.layers{l}.outputmaps;
- end
- if strcmp(net.layers{l}.type, 'f1')
- fvnum = prod(mapsize) * inputmaps;
- onum = net.layers{l}.neuron;
- net.layers{l-1}.ffb = zeros(onum, 1);
- net.layers{l-1}.ffW = (rand(onum, fvnum) - 0.5) * 2 * sqrt(6 / (onum + fvnum));
- end
- if strcmp(net.layers{l}.type, 'f')
- fvnum = net.layers{l-1}.neuron;
- onum = net.layers{l}.neuron;
- net.layers{l-1}.ffb = zeros(onum, 1);
- net.layers{l-1}.ffW = (rand(onum, fvnum) - 0.5) * 2 * sqrt(6 / (onum + fvnum));
- end
- if strcmp(net.layers{l}.type, 'o')
- fvnum = net.layers{l-1}.neuron;
- onum = net.layers{l}.scale;
- net.layers{l-1}.ffb = zeros(onum, 1);
- net.layers{l-1}.ffW = (rand(onum, fvnum) - 0.5) * 2 * sqrt(6 / (onum + fvnum));
- end
- % fvnum 是輸出層的前面一層的神經元個數。
- % 這一層的上一層是經過pooling後的層,包含有inputmaps個特征map。每個特征map的大小是mapsize。
- % 是以,該層的神經元個數是 inputmaps * (每個特征map的大小)
- % prod: Product of elements.
- % For vectors, prod(X) is the product of the elements of X
- % 在這裡 mapsize = [特征map的行數 特征map的列數],是以prod後就是 特征map的行*列
- %fvnum = prod(mapsize) * inputmaps;
- % onum 是标簽的個數,也就是輸出層神經元的個數。你要分多少個類,自然就有多少個輸出神經元
- %onum = size(y, 1);
- % 這裡是最後一層神經網絡的設定
- % ffb 是輸出層每個神經元對應的基biases
- %net.ffb = zeros(onum, 1);
- % ffW 輸出層前一層 與 輸出層 連接配接的權值,這兩層之間是全連接配接的
- %net.ffW = (rand(onum, fvnum) - 0.5) * 2 * sqrt(6 / (onum + fvnum));
- end</span>
資料準備:整個資料為4-D的double型資料
每一維資料是這樣的,前2維代表每一幀圖像的大小,如文章中指的32×32,第3維代表輸入圖檔的通道,一共為3通道,那麼前3維就組成了一個樣本,第4維代表樣本的數量。這樣就生成了訓練集。測試集也一樣。
資料的标簽:
trainlabel=double(trainlabel);
trainlabel(trainlabel==0) = 10;
train_y = full(sparse(trainlabel, 1:50000, 1));
每一類分别給出相應的标簽,如1,2,3,…,n