CNN卷積神經網絡代碼理解

Deep Learning CNN卷積神經網絡代碼理解

 

        自己平時看了一些論文,但老感覺看完過後就會慢慢的淡忘,某一天重新拾起來的時候又好像沒有看過一樣。所以想習慣地把一些感覺有用的論文中的知識點總結整理一下,一方面在整理過程中,自己的理解也會更深,另一方面也方便未來自己的勘察。更好的還可以放到博客上面與大家交流。因爲基礎有限,所以對論文的一些理解可能不太正確,還望大家不吝指正交流,謝謝。

 

       本文的代碼來自githupDeep Learningtoolbox,(在這裏,先感謝該toolbox的作者)裏面包含了很多Deep Learning方法的代碼。是用Matlab編寫的(另外,有人翻譯成了C++Python的版本了)。本文中我們主要解讀下CNN的代碼。詳細的註釋見代碼。

       在讀代碼之前,最好先閱讀下我的上一個博文:

         Deep Learning論文筆記之(四)CNN卷積神經網絡推導和實現

            http://blog.csdn.net/zouxy09/article/details/9993371

       裏面包含的是我對一個作者的CNN筆記的翻譯性的理解,對CNN的推導和實現做了詳細的介紹,看明白這個筆記對代碼的理解非常重要,所以強烈建議先看懂上面這篇文章。

 

         下面是自己對代碼的註釋:

cnnexamples.m

  1. clear all; close all; clc;  
  2. addpath('../data');  
  3. addpath('../util');  
  4. load mnist_uint8;  
  5.   
  6. train_x = double(reshape(train_x',28,28,60000))/255;  
  7. test_x = double(reshape(test_x',28,28,10000))/255;  
  8. train_y = double(train_y');  
  9. test_y = double(test_y');  
  10.   
  11. %% ex1   
  12. %will run 1 epoch in about 200 second and get around 11% error.   
  13. %With 100 epochs you'll get around 1.2% error  
  14.   
  15. cnn.layers = {  
  16.     struct('type', 'i') %input layer  
  17.     struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer  
  18.     struct('type', 's', 'scale', 2) %sub sampling layer  
  19.     struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer  
  20.     struct('type', 's', 'scale', 2) %subsampling layer  
  21. };  
  22.   
  23. % 這裏把cnn的設置給cnnsetup,它會據此構建一個完整的CNN網絡,並返回  
  24. cnn = cnnsetup(cnn, train_x, train_y);  
  25.   
  26. % 學習率  
  27. opts.alpha = 1;  
  28. % 每次挑出一個batchsize的batch來訓練,也就是每用batchsize個樣本就調整一次權值,而不是  
  29. % 把所有樣本都輸入了,計算所有樣本的誤差了才調整一次權值  
  30. opts.batchsize = 50;   
  31. % 訓練次數,用同樣的樣本集。我訓練的時候:  
  32. % 1的時候 11.41% error  
  33. % 5的時候 4.2% error  
  34. % 10的時候 2.73% error  
  35. opts.numepochs = 10;  
  36.   
  37. % 然後開始把訓練樣本給它,開始訓練這個CNN網絡  
  38. cnn = cnntrain(cnn, train_x, train_y, opts);  
  39.   
  40. % 然後就用測試樣本來測試  
  41. [er, bad] = cnntest(cnn, test_x, test_y);  
  42.   
  43. %plot mean squared error  
  44. plot(cnn.rL);  
  45. %show test error  
  46. disp([num2str(er*100) '% error']);  
clear all; close all; clc;
addpath('../data');
addpath('../util');
load mnist_uint8;

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 
%will run 1 epoch in about 200 second and get around 11% error. 
%With 100 epochs you'll get around 1.2% error

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的設置給cnnsetup,它會據此構建一個完整的CNN網絡,並返回
cnn = cnnsetup(cnn, train_x, train_y);

% 學習率
opts.alpha = 1;
% 每次挑出一個batchsize的batch來訓練,也就是每用batchsize個樣本就調整一次權值,而不是
% 把所有樣本都輸入了,計算所有樣本的誤差了才調整一次權值
opts.batchsize = 50; 
% 訓練次數,用同樣的樣本集。我訓練的時候:
% 1的時候 11.41% error
% 5的時候 4.2% error
% 10的時候 2.73% error
opts.numepochs = 10;

% 然後開始把訓練樣本給它,開始訓練這個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']);


cnnsetup.m

  1. function net = cnnsetup(net, x, y)  
  2.     inputmaps = 1;  
  3.     % B=squeeze(A) 返回和矩陣A相同元素但所有單一維都移除的矩陣B,單一維是滿足size(A,dim)=1的維。  
  4.     % train_x中圖像的存放方式是三維的reshape(train_x',28,28,60000),前面兩維表示圖像的行與列,  
  5.     % 第三維就表示有多少個圖像。這樣squeeze(x(:, :, 1))就相當於取第一個圖像樣本後,再把第三維  
  6.     % 移除,就變成了28x28的矩陣,也就是得到一幅圖像,再size一下就得到了訓練樣本圖像的行數與列數了  
  7.     mapsize = size(squeeze(x(:, :, 1)));  
  8.   
  9.     % 下面通過傳入net這個結構體來逐層構建CNN網絡  
  10.     % n = numel(A)返回數組A中元素個數  
  11.     % net.layers中有五個struct類型的元素,實際上就表示CNN共有五層,這裏範圍的是5  
  12.     for l = 1 : numel(net.layers)   %  layer  
  13.         if strcmp(net.layers{l}.type, 's') % 如果這層是 子採樣層  
  14.             % subsampling層的mapsize,最開始mapsize是每張圖的大小28*28  
  15.             % 這裏除以scale=2,就是pooling之後圖的大小,pooling域之間沒有重疊,所以pooling後的圖像爲14*14  
  16.             % 注意這裏的右邊的mapsize保存的都是上一層每張特徵map的大小,它會隨着循環進行不斷更新  
  17.             mapsize = floor(mapsize / net.layers{l}.scale);  
  18.             for j = 1 : inputmaps % inputmap就是上一層有多少張特徵圖  
  19.                 net.layers{l}.b{j} = 0; % 將偏置初始化爲0  
  20.             end  
  21.         end  
  22.         if strcmp(net.layers{l}.type, 'c') % 如果這層是 卷積層  
  23.             % 舊的mapsize保存的是上一層的特徵map的大小,那麼如果卷積核的移動步長是1,那用  
  24.             % kernelsize*kernelsize大小的卷積核卷積上一層的特徵map後,得到的新的map的大小就是下面這樣  
  25.             mapsize = mapsize - net.layers{l}.kernelsize + 1;  
  26.             % 該層需要學習的參數個數。每張特徵map是一個(後層特徵圖數量)*(用來卷積的patch圖的大小)  
  27.             % 因爲是通過用一個核窗口在上一個特徵map層中移動(核窗口每次移動1個像素),遍歷上一個特徵map  
  28.             % 層的每個神經元。核窗口由kernelsize*kernelsize個元素組成,每個元素是一個獨立的權值,所以  
  29.             % 就有kernelsize*kernelsize個需要學習的權值,再加一個偏置值。另外,由於是權值共享,也就是  
  30.             % 說同一個特徵map層是用同一個具有相同權值元素的kernelsize*kernelsize的核窗口去感受輸入上一  
  31.             % 個特徵map層的每個神經元得到的,所以同一個特徵map,它的權值是一樣的,共享的,權值只取決於  
  32.             % 核窗口。然後,不同的特徵map提取輸入上一個特徵map層不同的特徵,所以採用的核窗口不一樣,也  
  33.             % 就是權值不一樣,所以outputmaps個特徵map就有(kernelsize*kernelsize+1)* outputmaps那麼多的權值了  
  34.             % 但這裏fan_out只保存卷積核的權值W,偏置b在下面獨立保存  
  35.             fan_out = net.layers{l}.outputmaps * net.layers{l}.kernelsize ^ 2;  
  36.             for j = 1 : net.layers{l}.outputmaps  %  output map  
  37.                 % fan_out保存的是對於上一層的一張特徵map,我在這一層需要對這一張特徵map提取outputmaps種特徵,  
  38.                 % 提取每種特徵用到的卷積核不同,所以fan_out保存的是這一層輸出新的特徵需要學習的參數個數  
  39.                 % 而,fan_in保存的是,我在這一層,要連接到上一層中所有的特徵map,然後用fan_out保存的提取特徵  
  40.                 % 的權值來提取他們的特徵。也即是對於每一個當前層特徵圖,有多少個參數鏈到前層  
  41.                 fan_in = inputmaps * net.layers{l}.kernelsize ^ 2;  
  42.                 for i = 1 : inputmaps  %  input map  
  43.                     % 隨機初始化權值,也就是共有outputmaps個卷積核,對上層的每個特徵map,都需要用這麼多個卷積核  
  44.                     % 去卷積提取特徵。  
  45.                     % rand(n)是產生n×n的 0-1之間均勻取值的數值的矩陣,再減去0.5就相當於產生-0.5到0.5之間的隨機數  
  46.                     % 再 *2 就放大到 [-1, 1]。然後再乘以後面那一數,why?  
  47.                     % 反正就是將卷積核每個元素初始化爲[-sqrt(6 / (fan_in + fan_out)), sqrt(6 / (fan_in + fan_out))]  
  48.                     % 之間的隨機數。因爲這裏是權值共享的,也就是對於一張特徵map,所有感受野位置的卷積核都是一樣的  
  49.                     % 所以只需要保存的是 inputmaps * outputmaps 個卷積核。  
  50.                     net.layers{l}.k{i}{j} = (rand(net.layers{l}.kernelsize) - 0.5) * 2 * sqrt(6 / (fan_in + fan_out));  
  51.                 end  
  52.                 net.layers{l}.b{j} = 0; % 將偏置初始化爲0  
  53.             end  
  54.             % 只有在卷積層的時候纔會改變特徵map的個數,pooling的時候不會改變個數。這層輸出的特徵map個數就是  
  55.             % 輸入到下一層的特徵map個數  
  56.             inputmaps = net.layers{l}.outputmaps;   
  57.         end  
  58.     end  
  59.       
  60.     % fvnum 是輸出層的前面一層的神經元個數。  
  61.     % 這一層的上一層是經過pooling後的層,包含有inputmaps個特徵map。每個特徵map的大小是mapsize。  
  62.     % 所以,該層的神經元個數是 inputmaps * (每個特徵map的大小)  
  63.     % prod: Product of elements.  
  64.     % For vectors, prod(X) is the product of the elements of X  
  65.     % 在這裏 mapsize = [特徵map的行數 特徵map的列數],所以prod後就是 特徵map的行*列  
  66.     fvnum = prod(mapsize) * inputmaps;  
  67.     % onum 是標籤的個數,也就是輸出層神經元的個數。你要分多少個類,自然就有多少個輸出神經元  
  68.     onum = size(y, 1);  
  69.   
  70.     % 這裏是最後一層神經網絡的設定  
  71.     % ffb 是輸出層每個神經元對應的基biases  
  72.     net.ffb = zeros(onum, 1);  
  73.     % ffW 輸出層前一層 與 輸出層 連接的權值,這兩層之間是全連接的  
  74.     net.ffW = (rand(onum, fvnum) - 0.5) * 2 * sqrt(6 / (onum + fvnum));  
  75. end  
function net = cnnsetup(net, x, y)
    inputmaps = 1;
	% 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
    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


cnntrain.m

  1. function net = cnntrain(net, x, y, opts)  
  2.     m = size(x, 3); % m 保存的是 訓練樣本個數  
  3.     numbatches = m / opts.batchsize;  
  4.     % rem: Remainder after division. rem(x,y) is x - n.*y 相當於求餘  
  5.     % rem(numbatches, 1) 就相當於取其小數部分,如果爲0,就是整數  
  6.     if rem(numbatches, 1) ~= 0  
  7.         error('numbatches not integer');  
  8.     end  
  9.       
  10.     net.rL = [];  
  11.     for i = 1 : opts.numepochs  
  12.         % disp(X) 打印數組元素。如果X是個字符串,那就打印這個字符串  
  13.         disp(['epoch ' num2str(i) '/' num2str(opts.numepochs)]);  
  14.         % tic 和 toc 是用來計時的,計算這兩條語句之間所耗的時間  
  15.         tic;  
  16.         % P = randperm(N) 返回[1, N]之間所有整數的一個隨機的序列,例如  
  17.         % randperm(6) 可能會返回 [2 4 5 6 1 3]  
  18.         % 這樣就相當於把原來的樣本排列打亂,再挑出一些樣本來訓練  
  19.         kk = randperm(m);  
  20.         for l = 1 : numbatches  
  21.             % 取出打亂順序後的batchsize個樣本和對應的標籤  
  22.             batch_x = x(:, :, kk((l - 1) * opts.batchsize + 1 : l * opts.batchsize));  
  23.             batch_y = y(:,    kk((l - 1) * opts.batchsize + 1 : l * opts.batchsize));  
  24.   
  25.             % 在當前的網絡權值和網絡輸入下計算網絡的輸出  
  26.             net = cnnff(net, batch_x); % Feedforward  
  27.             % 得到上面的網絡輸出後,通過對應的樣本標籤用bp算法來得到誤差對網絡權值  
  28.             %(也就是那些卷積核的元素)的導數  
  29.             net = cnnbp(net, batch_y); % Backpropagation  
  30.             % 得到誤差對權值的導數後,就通過權值更新方法去更新權值  
  31.             net = cnnapplygrads(net, opts);  
  32.             if isempty(net.rL)  
  33.                 net.rL(1) = net.L; % 代價函數值,也就是誤差值  
  34.             end  
  35.             net.rL(end + 1) = 0.99 * net.rL(end) + 0.01 * net.L; % 保存歷史的誤差值,以便畫圖分析  
  36.         end  
  37.         toc;  
  38.     end  
  39.       
  40. end  
function net = cnntrain(net, x, y, opts)
    m = size(x, 3); % 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.L; % 代價函數值,也就是誤差值
            end
            net.rL(end + 1) = 0.99 * net.rL(end) + 0.01 * net.L; % 保存歷史的誤差值,以便畫圖分析
        end
        toc;
    end
    
end


cnnff.m

  1. function net = cnnff(net, x)  
  2.     n = numel(net.layers); % 層數  
  3.     net.layers{1}.a{1} = x; % 網絡的第一層就是輸入,但這裏的輸入包含了多個訓練圖像  
  4.     inputmaps = 1; % 輸入層只有一個特徵map,也就是原始的輸入圖像  
  5.   
  6.     for l = 2 : n   %  for each layer  
  7.         if strcmp(net.layers{l}.type, 'c') % 卷積層  
  8.             %  !!below can probably be handled by insane matrix operations  
  9.             % 對每一個輸入map,或者說我們需要用outputmaps個不同的卷積核去卷積圖像  
  10.             for j = 1 : net.layers{l}.outputmaps   %  for each output map  
  11.                 %  create temp output map  
  12.                 % 對上一層的每一張特徵map,卷積後的特徵map的大小就是   
  13.                 % (輸入map寬 - 卷積核的寬 + 1)* (輸入map高 - 卷積核高 + 1)  
  14.                 % 對於這裏的層,因爲每層都包含多張特徵map,對應的索引保存在每層map的第三維  
  15.                 % 所以,這裏的z保存的就是該層中所有的特徵map了  
  16.                 z = zeros(size(net.layers{l - 1}.a{1}) - [net.layers{l}.kernelsize - 1 net.layers{l}.kernelsize - 1 0]);  
  17.                 for i = 1 : inputmaps   %  for each input map  
  18.                     %  convolve with corresponding kernel and add to temp output map  
  19.                     % 將上一層的每一個特徵map(也就是這層的輸入map)與該層的卷積核進行卷積  
  20.                     % 然後將對上一層特徵map的所有結果加起來。也就是說,當前層的一張特徵map,是  
  21.                     % 用一種卷積核去卷積上一層中所有的特徵map,然後所有特徵map對應位置的卷積值的和  
  22.                     % 另外,有些論文或者實際應用中,並不是與全部的特徵map鏈接的,有可能只與其中的某幾個連接  
  23.                     z = z + convn(net.layers{l - 1}.a{i}, net.layers{l}.k{i}{j}, 'valid');  
  24.                 end  
  25.                 %  add bias, pass through nonlinearity  
  26.                 % 加上對應位置的基b,然後再用sigmoid函數算出特徵map中每個位置的激活值,作爲該層輸出特徵map  
  27.                 net.layers{l}.a{j} = sigm(z + net.layers{l}.b{j});  
  28.             end  
  29.             %  set number of input maps to this layers number of outputmaps  
  30.             inputmaps = net.layers{l}.outputmaps;  
  31.         elseif strcmp(net.layers{l}.type, 's') % 下采樣層  
  32.             %  downsample  
  33.             for j = 1 : inputmaps  
  34.                 %  !! replace with variable  
  35.                 % 例如我們要在scale=2的域上面執行mean pooling,那麼可以卷積大小爲2*2,每個元素都是1/4的卷積核  
  36.                 z = convn(net.layers{l - 1}.a{j}, ones(net.layers{l}.scale) / (net.layers{l}.scale ^ 2), 'valid');   
  37.                 % 因爲convn函數的默認卷積步長爲1,而pooling操作的域是沒有重疊的,所以對於上面的卷積結果  
  38.                 % 最終pooling的結果需要從上面得到的卷積結果中以scale=2爲步長,跳着把mean pooling的值讀出來  
  39.                 net.layers{l}.a{j} = z(1 : net.layers{l}.scale : end, 1 : net.layers{l}.scale : end, :);  
  40.             end  
  41.         end  
  42.     end  
  43.   
  44.     %  concatenate all end layer feature maps into vector  
  45.     % 把最後一層得到的特徵map拉成一條向量,作爲最終提取到的特徵向量  
  46.     net.fv = [];  
  47.     for j = 1 : numel(net.layers{n}.a) % 最後一層的特徵map的個數  
  48.         sa = size(net.layers{n}.a{j}); % 第j個特徵map的大小  
  49.         % 將所有的特徵map拉成一條列向量。還有一維就是對應的樣本索引。每個樣本一列,每列爲對應的特徵向量  
  50.         net.fv = [net.fv; reshape(net.layers{n}.a{j}, sa(1) * sa(2), sa(3))];  
  51.     end  
  52.     %  feedforward into output perceptrons  
  53.     % 計算網絡的最終輸出值。sigmoid(W*X + b),注意是同時計算了batchsize個樣本的輸出值  
  54.     net.o = sigm(net.ffW * net.fv + repmat(net.ffb, 1, size(net.fv, 2)));  
  55.   
  56. end  
function net = cnnff(net, x)
    n = numel(net.layers); % 層數
    net.layers{1}.a{1} = x; % 網絡的第一層就是輸入,但這裏的輸入包含了多個訓練圖像
    inputmaps = 1; % 輸入層只有一個特徵map,也就是原始的輸入圖像

    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} = sigm(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
        end
    end

    %  concatenate all end layer feature maps into vector
	% 把最後一層得到的特徵map拉成一條向量,作爲最終提取到的特徵向量
    net.fv = [];
    for j = 1 : numel(net.layers{n}.a) % 最後一層的特徵map的個數
        sa = size(net.layers{n}.a{j}); % 第j個特徵map的大小
		% 將所有的特徵map拉成一條列向量。還有一維就是對應的樣本索引。每個樣本一列,每列爲對應的特徵向量
        net.fv = [net.fv; reshape(net.layers{n}.a{j}, sa(1) * sa(2), sa(3))];
    end
    %  feedforward into output perceptrons
	% 計算網絡的最終輸出值。sigmoid(W*X + b),注意是同時計算了batchsize個樣本的輸出值
    net.o = sigm(net.ffW * net.fv + repmat(net.ffb, 1, size(net.fv, 2)));

end


cnnbp.m

  1. function net = cnnbp(net, y)  
  2.     n = numel(net.layers); % 網絡層數  
  3.   
  4.     %  error  
  5.     net.e = net.o - y;   
  6.     %  loss function  
  7.     % 代價函數是 均方誤差  
  8.     net.L = 1/2* sum(net.e(:) .^ 2) / size(net.e, 2);  
  9.   
  10.     %%  backprop deltas  
  11.     % 這裏可以參考 UFLDL 的 反向傳導算法 的說明  
  12.     % 輸出層的 靈敏度 或者 殘差  
  13.     net.od = net.e .* (net.o .* (1 - net.o));   %  output delta  
  14.     % 殘差 反向傳播回 前一層  
  15.     net.fvd = (net.ffW' * net.od);              %  feature vector delta  
  16.     if strcmp(net.layers{n}.type, 'c')         %  only conv layers has sigm function  
  17.         net.fvd = net.fvd .* (net.fv .* (1 - net.fv));  
  18.     end  
  19.   
  20.     %  reshape feature vector deltas into output map style  
  21.     sa = size(net.layers{n}.a{1}); % 最後一層特徵map的大小。這裏的最後一層都是指輸出層的前一層  
  22.     fvnum = sa(1) * sa(2); % 因爲是將最後一層特徵map拉成一條向量,所以對於一個樣本來說,特徵維數是這樣  
  23.     for j = 1 : numel(net.layers{n}.a) % 最後一層的特徵map的個數  
  24.         % 在fvd裏面保存的是所有樣本的特徵向量(在cnnff.m函數中用特徵map拉成的),所以這裏需要重新  
  25.         % 變換回來特徵map的形式。d 保存的是 delta,也就是 靈敏度 或者 殘差  
  26.         net.layers{n}.d{j} = reshape(net.fvd(((j - 1) * fvnum + 1) : j * fvnum, :), sa(1), sa(2), sa(3));  
  27.     end  
  28.   
  29.     % 對於 輸出層前面的層(與輸出層計算殘差的方式不同)  
  30.     for l = (n - 1) : -1 : 1  
  31.         if strcmp(net.layers{l}.type, 'c')  
  32.             for j = 1 : numel(net.layers{l}.a) % 該層特徵map的個數  
  33.                 % net.layers{l}.d{j} 保存的是 第l層 的 第j個 map 的 靈敏度map。 也就是每個神經元節點的delta的值  
  34.                 % expand的操作相當於對l+1層的靈敏度map進行上採樣。然後前面的操作相當於對該層的輸入a進行sigmoid求導  
  35.                 % 這條公式請參考 Notes on Convolutional Neural Networks  
  36.                 % for k = 1:size(net.layers{l + 1}.d{j}, 3)  
  37.                     % 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;  
  38.                 % end  
  39.                 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);  
  40.             end  
  41.         elseif strcmp(net.layers{l}.type, 's')  
  42.             for i = 1 : numel(net.layers{l}.a) % 第l層特徵map的個數  
  43.                 z = zeros(size(net.layers{l}.a{1}));  
  44.                 for j = 1 : numel(net.layers{l + 1}.a) % 第l+1層特徵map的個數  
  45.                      z = z + convn(net.layers{l + 1}.d{j}, rot180(net.layers{l + 1}.k{i}{j}), 'full');  
  46.                 end  
  47.                 net.layers{l}.d{i} = z;  
  48.             end  
  49.         end  
  50.     end  
  51.   
  52.     %%  calc gradients  
  53.     % 這裏與 Notes on Convolutional Neural Networks 中不同,這裏的 子採樣 層沒有參數,也沒有  
  54.     % 激活函數,所以在子採樣層是沒有需要求解的參數的  
  55.     for l = 2 : n  
  56.         if strcmp(net.layers{l}.type, 'c')  
  57.             for j = 1 : numel(net.layers{l}.a)  
  58.                 for i = 1 : numel(net.layers{l - 1}.a)  
  59.                     % dk 保存的是 誤差對卷積核 的導數  
  60.                     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);  
  61.                 end  
  62.                 % db 保存的是 誤差對於bias基 的導數  
  63.                 net.layers{l}.db{j} = sum(net.layers{l}.d{j}(:)) / size(net.layers{l}.d{j}, 3);  
  64.             end  
  65.         end  
  66.     end  
  67.     % 最後一層perceptron的gradient的計算  
  68.     net.dffW = net.od * (net.fv)' / size(net.od, 2);  
  69.     net.dffb = mean(net.od, 2);  
  70.   
  71.     function X = rot180(X)  
  72.         X = flipdim(flipdim(X, 1), 2);  
  73.     end  
  74. end  
function net = cnnbp(net, y)
    n = numel(net.layers); % 網絡層數

    %  error
    net.e = net.o - y; 
    %  loss function
	% 代價函數是 均方誤差
    net.L = 1/2* sum(net.e(:) .^ 2) / size(net.e, 2);

    %%  backprop deltas
	% 這裏可以參考 UFLDL 的 反向傳導算法 的說明
	% 輸出層的 靈敏度 或者 殘差
    net.od = net.e .* (net.o .* (1 - net.o));   %  output delta
	% 殘差 反向傳播回 前一層
    net.fvd = (net.ffW' * net.od);              %  feature vector delta
    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{n}.a{1}); % 最後一層特徵map的大小。這裏的最後一層都是指輸出層的前一層
    fvnum = sa(1) * sa(2); % 因爲是將最後一層特徵map拉成一條向量,所以對於一個樣本來說,特徵維數是這樣
    for j = 1 : numel(net.layers{n}.a) % 最後一層的特徵map的個數
		% 在fvd裏面保存的是所有樣本的特徵向量(在cnnff.m函數中用特徵map拉成的),所以這裏需要重新
		% 變換回來特徵map的形式。d 保存的是 delta,也就是 靈敏度 或者 殘差
        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')
            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} = 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) % 第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
        end
    end
	% 最後一層perceptron的gradient的計算
    net.dffW = net.od * (net.fv)' / size(net.od, 2);
    net.dffb = mean(net.od, 2);

    function X = rot180(X)
        X = flipdim(flipdim(X, 1), 2);
    end
end


cnnapplygrads.m

  1. function net = cnnapplygrads(net, opts)  
  2.     for l = 2 : numel(net.layers)  
  3.         if strcmp(net.layers{l}.type, 'c')  
  4.             for j = 1 : numel(net.layers{l}.a)  
  5.                 for ii = 1 : numel(net.layers{l - 1}.a)  
  6.                     % 這裏沒什麼好說的,就是普通的權值更新的公式:W_new = W_old - alpha * de/dW(誤差對權值導數)  
  7.                     net.layers{l}.k{ii}{j} = net.layers{l}.k{ii}{j} - opts.alpha * net.layers{l}.dk{ii}{j};  
  8.                 end  
  9.             end  
  10.             net.layers{l}.b{j} = net.layers{l}.b{j} - opts.alpha * net.layers{l}.db{j};  
  11.         end  
  12.     end  
  13.   
  14.     net.ffW = net.ffW - opts.alpha * net.dffW;  
  15.     net.ffb = net.ffb - opts.alpha * net.dffb;  
  16. end  
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)
                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
            end
            net.layers{l}.b{j} = net.layers{l}.b{j} - opts.alpha * net.layers{l}.db{j};
        end
    end

    net.ffW = net.ffW - opts.alpha * net.dffW;
    net.ffb = net.ffb - opts.alpha * net.dffb;
end


cnntest.m

  1. function [er, bad] = cnntest(net, x, y)  
  2.     %  feedforward  
  3.     net = cnnff(net, x); % 前向傳播得到輸出  
  4.     % [Y,I] = max(X) returns the indices of the maximum values in vector I  
  5.     [~, h] = max(net.o); % 找到最大的輸出對應的標籤  
  6.     [~, a] = max(y);     % 找到最大的期望輸出對應的索引  
  7.     bad = find(h ~= a);  % 找到他們不相同的個數,也就是錯誤的次數  
  8.   
  9.     er = numel(bad) / size(y, 2); % 計算錯誤率  
  10. end  
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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章