Deep Learning论文笔记之(五)CNN卷积神经网络代码理解
Deep Learning論文筆記之(五)CNN卷積神經(jīng)網(wǎng)絡(luò)代碼理解
zouxy09@qq.com
http://blog.csdn.net/zouxy09
?
???????? 自己平時(shí)看了一些論文,但老感覺(jué)看完過(guò)后就會(huì)慢慢的淡忘,某一天重新拾起來(lái)的時(shí)候又好像沒(méi)有看過(guò)一樣。所以想習(xí)慣地把一些感覺(jué)有用的論文中的知識(shí)點(diǎn)總結(jié)整理一下,一方面在整理過(guò)程中,自己的理解也會(huì)更深,另一方面也方便未來(lái)自己的勘察。更好的還可以放到博客上面與大家交流。因?yàn)榛A(chǔ)有限,所以對(duì)論文的一些理解可能不太正確,還望大家不吝指正交流,謝謝。
?
?????? 本文的代碼來(lái)自githup的Deep Learning的toolbox,(在這里,先感謝該toolbox的作者)里面包含了很多Deep Learning方法的代碼。是用Matlab編寫的(另外,有人翻譯成了C++和python的版本了)。本文中我們主要解讀下CNN的代碼。詳細(xì)的注釋見代碼。
???????在讀代碼之前,最好先閱讀下我的上一個(gè)博文:
???????? Deep Learning論文筆記之(四)CNN卷積神經(jīng)網(wǎng)絡(luò)推導(dǎo)和實(shí)現(xiàn)
??????????? http://blog.csdn.net/zouxy09/article/details/9993371
?????? 里面包含的是我對(duì)一個(gè)作者的CNN筆記的翻譯性的理解,對(duì)CNN的推導(dǎo)和實(shí)現(xiàn)做了詳細(xì)的介紹,看明白這個(gè)筆記對(duì)代碼的理解非常重要,所以強(qiáng)烈建議先看懂上面這篇文章。
?
???????? 下面是自己對(duì)代碼的注釋:
cnnexamples.m
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% errorcnn.layers = {struct('type', 'i') %input layerstruct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layerstruct('type', 's', 'scale', 2) %sub sampling layerstruct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layerstruct('type', 's', 'scale', 2) %subsampling layer };% 這里把cnn的設(shè)置給cnnsetup,它會(huì)據(jù)此構(gòu)建一個(gè)完整的CNN網(wǎng)絡(luò),并返回 cnn = cnnsetup(cnn, train_x, train_y);% 學(xué)習(xí)率 opts.alpha = 1; % 每次挑出一個(gè)batchsize的batch來(lái)訓(xùn)練,也就是每用batchsize個(gè)樣本就調(diào)整一次權(quán)值,而不是 % 把所有樣本都輸入了,計(jì)算所有樣本的誤差了才調(diào)整一次權(quán)值 opts.batchsize = 50; % 訓(xùn)練次數(shù),用同樣的樣本集。我訓(xùn)練的時(shí)候: % 1的時(shí)候 11.41% error % 5的時(shí)候 4.2% error % 10的時(shí)候 2.73% error opts.numepochs = 10;% 然后開始把訓(xùn)練樣本給它,開始訓(xùn)練這個(gè)CNN網(wǎng)絡(luò) cnn = cnntrain(cnn, train_x, train_y, opts);% 然后就用測(cè)試樣本來(lái)測(cè)試 [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
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),前面兩維表示圖像的行與列,% 第三維就表示有多少個(gè)圖像。這樣squeeze(x(:, :, 1))就相當(dāng)于取第一個(gè)圖像樣本后,再把第三維% 移除,就變成了28x28的矩陣,也就是得到一幅圖像,再size一下就得到了訓(xùn)練樣本圖像的行數(shù)與列數(shù)了mapsize = size(squeeze(x(:, :, 1)));% 下面通過(guò)傳入net這個(gè)結(jié)構(gòu)體來(lái)逐層構(gòu)建CNN網(wǎng)絡(luò)% n = numel(A)返回?cái)?shù)組A中元素個(gè)數(shù)% net.layers中有五個(gè)struct類型的元素,實(shí)際上就表示CNN共有五層,這里范圍的是5for l = 1 : numel(net.layers) % layerif strcmp(net.layers{l}.type, 's') % 如果這層是 子采樣層% subsampling層的mapsize,最開始mapsize是每張圖的大小28*28% 這里除以scale=2,就是pooling之后圖的大小,pooling域之間沒(méi)有重疊,所以pooling后的圖像為14*14% 注意這里的右邊的mapsize保存的都是上一層每張?zhí)卣鱩ap的大小,它會(huì)隨著循環(huán)進(jìn)行不斷更新mapsize = floor(mapsize / net.layers{l}.scale);for j = 1 : inputmaps % inputmap就是上一層有多少?gòu)執(zhí)卣鲌Dnet.layers{l}.b{j} = 0; % 將偏置初始化為0endendif strcmp(net.layers{l}.type, 'c') % 如果這層是 卷積層% 舊的mapsize保存的是上一層的特征map的大小,那么如果卷積核的移動(dòng)步長(zhǎng)是1,那用% kernelsize*kernelsize大小的卷積核卷積上一層的特征map后,得到的新的map的大小就是下面這樣mapsize = mapsize - net.layers{l}.kernelsize + 1;% 該層需要學(xué)習(xí)的參數(shù)個(gè)數(shù)。每張?zhí)卣鱩ap是一個(gè)(后層特征圖數(shù)量)*(用來(lái)卷積的patch圖的大小)% 因?yàn)槭峭ㄟ^(guò)用一個(gè)核窗口在上一個(gè)特征map層中移動(dòng)(核窗口每次移動(dòng)1個(gè)像素),遍歷上一個(gè)特征map% 層的每個(gè)神經(jīng)元。核窗口由kernelsize*kernelsize個(gè)元素組成,每個(gè)元素是一個(gè)獨(dú)立的權(quán)值,所以% 就有kernelsize*kernelsize個(gè)需要學(xué)習(xí)的權(quán)值,再加一個(gè)偏置值。另外,由于是權(quán)值共享,也就是% 說(shuō)同一個(gè)特征map層是用同一個(gè)具有相同權(quán)值元素的kernelsize*kernelsize的核窗口去感受輸入上一% 個(gè)特征map層的每個(gè)神經(jīng)元得到的,所以同一個(gè)特征map,它的權(quán)值是一樣的,共享的,權(quán)值只取決于% 核窗口。然后,不同的特征map提取輸入上一個(gè)特征map層不同的特征,所以采用的核窗口不一樣,也% 就是權(quán)值不一樣,所以outputmaps個(gè)特征map就有(kernelsize*kernelsize+1)* outputmaps那么多的權(quán)值了% 但這里fan_out只保存卷積核的權(quán)值W,偏置b在下面獨(dú)立保存fan_out = net.layers{l}.outputmaps * net.layers{l}.kernelsize ^ 2;for j = 1 : net.layers{l}.outputmaps % output map% fan_out保存的是對(duì)于上一層的一張?zhí)卣鱩ap,我在這一層需要對(duì)這一張?zhí)卣鱩ap提取outputmaps種特征,% 提取每種特征用到的卷積核不同,所以fan_out保存的是這一層輸出新的特征需要學(xué)習(xí)的參數(shù)個(gè)數(shù)% 而,fan_in保存的是,我在這一層,要連接到上一層中所有的特征map,然后用fan_out保存的提取特征% 的權(quán)值來(lái)提取他們的特征。也即是對(duì)于每一個(gè)當(dāng)前層特征圖,有多少個(gè)參數(shù)鏈到前層fan_in = inputmaps * net.layers{l}.kernelsize ^ 2;for i = 1 : inputmaps % input map% 隨機(jī)初始化權(quán)值,也就是共有outputmaps個(gè)卷積核,對(duì)上層的每個(gè)特征map,都需要用這么多個(gè)卷積核% 去卷積提取特征。% rand(n)是產(chǎn)生n×n的 0-1之間均勻取值的數(shù)值的矩陣,再減去0.5就相當(dāng)于產(chǎn)生-0.5到0.5之間的隨機(jī)數(shù)% 再 *2 就放大到 [-1, 1]。然后再乘以后面那一數(shù),why?% 反正就是將卷積核每個(gè)元素初始化為[-sqrt(6 / (fan_in + fan_out)), sqrt(6 / (fan_in + fan_out))]% 之間的隨機(jī)數(shù)。因?yàn)檫@里是權(quán)值共享的,也就是對(duì)于一張?zhí)卣鱩ap,所有感受野位置的卷積核都是一樣的% 所以只需要保存的是 inputmaps * outputmaps 個(gè)卷積核。net.layers{l}.k{i}{j} = (rand(net.layers{l}.kernelsize) - 0.5) * 2 * sqrt(6 / (fan_in + fan_out));endnet.layers{l}.b{j} = 0; % 將偏置初始化為0end% 只有在卷積層的時(shí)候才會(huì)改變特征map的個(gè)數(shù),pooling的時(shí)候不會(huì)改變個(gè)數(shù)。這層輸出的特征map個(gè)數(shù)就是% 輸入到下一層的特征map個(gè)數(shù)inputmaps = net.layers{l}.outputmaps; endend% fvnum 是輸出層的前面一層的神經(jīng)元個(gè)數(shù)。% 這一層的上一層是經(jīng)過(guò)pooling后的層,包含有inputmaps個(gè)特征map。每個(gè)特征map的大小是mapsize。% 所以,該層的神經(jīng)元個(gè)數(shù)是 inputmaps * (每個(gè)特征map的大小)% prod: Product of elements.% For vectors, prod(X) is the product of the elements of X% 在這里 mapsize = [特征map的行數(shù) 特征map的列數(shù)],所以prod后就是 特征map的行*列fvnum = prod(mapsize) * inputmaps;% onum 是標(biāo)簽的個(gè)數(shù),也就是輸出層神經(jīng)元的個(gè)數(shù)。你要分多少個(gè)類,自然就有多少個(gè)輸出神經(jīng)元onum = size(y, 1);% 這里是最后一層神經(jīng)網(wǎng)絡(luò)的設(shè)定% ffb 是輸出層每個(gè)神經(jīng)元對(duì)應(yīng)的基biasesnet.ffb = zeros(onum, 1);% ffW 輸出層前一層 與 輸出層 連接的權(quán)值,這兩層之間是全連接的net.ffW = (rand(onum, fvnum) - 0.5) * 2 * sqrt(6 / (onum + fvnum)); end
cnntrain.m
function net = cnntrain(net, x, y, opts)m = size(x, 3); % m 保存的是 訓(xùn)練樣本個(gè)數(shù)numbatches = m / opts.batchsize;% rem: Remainder after division. rem(x,y) is x - n.*y 相當(dāng)于求余% rem(numbatches, 1) 就相當(dāng)于取其小數(shù)部分,如果為0,就是整數(shù)if rem(numbatches, 1) ~= 0error('numbatches not integer');endnet.rL = [];for i = 1 : opts.numepochs% disp(X) 打印數(shù)組元素。如果X是個(gè)字符串,那就打印這個(gè)字符串disp(['epoch ' num2str(i) '/' num2str(opts.numepochs)]);% tic 和 toc 是用來(lái)計(jì)時(shí)的,計(jì)算這兩條語(yǔ)句之間所耗的時(shí)間tic;% P = randperm(N) 返回[1, N]之間所有整數(shù)的一個(gè)隨機(jī)的序列,例如% randperm(6) 可能會(huì)返回 [2 4 5 6 1 3]% 這樣就相當(dāng)于把原來(lái)的樣本排列打亂,再挑出一些樣本來(lái)訓(xùn)練kk = randperm(m);for l = 1 : numbatches% 取出打亂順序后的batchsize個(gè)樣本和對(duì)應(yīng)的標(biāo)簽batch_x = x(:, :, kk((l - 1) * opts.batchsize + 1 : l * opts.batchsize));batch_y = y(:, kk((l - 1) * opts.batchsize + 1 : l * opts.batchsize));% 在當(dāng)前的網(wǎng)絡(luò)權(quán)值和網(wǎng)絡(luò)輸入下計(jì)算網(wǎng)絡(luò)的輸出net = cnnff(net, batch_x); % Feedforward% 得到上面的網(wǎng)絡(luò)輸出后,通過(guò)對(duì)應(yīng)的樣本標(biāo)簽用bp算法來(lái)得到誤差對(duì)網(wǎng)絡(luò)權(quán)值%(也就是那些卷積核的元素)的導(dǎo)數(shù)net = cnnbp(net, batch_y); % Backpropagation% 得到誤差對(duì)權(quán)值的導(dǎo)數(shù)后,就通過(guò)權(quán)值更新方法去更新權(quán)值net = cnnapplygrads(net, opts);if isempty(net.rL)net.rL(1) = net.L; % 代價(jià)函數(shù)值,也就是誤差值endnet.rL(end + 1) = 0.99 * net.rL(end) + 0.01 * net.L; % 保存歷史的誤差值,以便畫圖分析endtoc;endend
cnnff.m
function net = cnnff(net, x)n = numel(net.layers); % 層數(shù)net.layers{1}.a{1} = x; % 網(wǎng)絡(luò)的第一層就是輸入,但這里的輸入包含了多個(gè)訓(xùn)練圖像inputmaps = 1; % 輸入層只有一個(gè)特征map,也就是原始的輸入圖像for l = 2 : n % for each layerif strcmp(net.layers{l}.type, 'c') % 卷積層% !!below can probably be handled by insane matrix operations% 對(duì)每一個(gè)輸入map,或者說(shuō)我們需要用outputmaps個(gè)不同的卷積核去卷積圖像for j = 1 : net.layers{l}.outputmaps % for each output map% create temp output map% 對(duì)上一層的每一張?zhí)卣鱩ap,卷積后的特征map的大小就是 % (輸入map寬 - 卷積核的寬 + 1)* (輸入map高 - 卷積核高 + 1)% 對(duì)于這里的層,因?yàn)槊繉佣及鄰執(zhí)卣鱩ap,對(duì)應(yīng)的索引保存在每層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% 將上一層的每一個(gè)特征map(也就是這層的輸入map)與該層的卷積核進(jìn)行卷積% 然后將對(duì)上一層特征map的所有結(jié)果加起來(lái)。也就是說(shuō),當(dāng)前層的一張?zhí)卣鱩ap,是% 用一種卷積核去卷積上一層中所有的特征map,然后所有特征map對(duì)應(yīng)位置的卷積值的和% 另外,有些論文或者實(shí)際應(yīng)用中,并不是與全部的特征map鏈接的,有可能只與其中的某幾個(gè)連接z = z + convn(net.layers{l - 1}.a{i}, net.layers{l}.k{i}{j}, 'valid');end% add bias, pass through nonlinearity% 加上對(duì)應(yīng)位置的基b,然后再用sigmoid函數(shù)算出特征map中每個(gè)位置的激活值,作為該層輸出特征mapnet.layers{l}.a{j} = sigm(z + net.layers{l}.b{j});end% set number of input maps to this layers number of outputmapsinputmaps = net.layers{l}.outputmaps;elseif strcmp(net.layers{l}.type, 's') % 下采樣層% downsamplefor j = 1 : inputmaps% !! replace with variable% 例如我們要在scale=2的域上面執(zhí)行mean pooling,那么可以卷積大小為2*2,每個(gè)元素都是1/4的卷積核z = convn(net.layers{l - 1}.a{j}, ones(net.layers{l}.scale) / (net.layers{l}.scale ^ 2), 'valid'); % 因?yàn)閏onvn函數(shù)的默認(rèn)卷積步長(zhǎng)為1,而pooling操作的域是沒(méi)有重疊的,所以對(duì)于上面的卷積結(jié)果% 最終pooling的結(jié)果需要從上面得到的卷積結(jié)果中以scale=2為步長(zhǎng),跳著把mean pooling的值讀出來(lái)net.layers{l}.a{j} = z(1 : net.layers{l}.scale : end, 1 : net.layers{l}.scale : end, :);endendend% concatenate all end layer feature maps into vector% 把最后一層得到的特征map拉成一條向量,作為最終提取到的特征向量net.fv = [];for j = 1 : numel(net.layers{n}.a) % 最后一層的特征map的個(gè)數(shù)sa = size(net.layers{n}.a{j}); % 第j個(gè)特征map的大小% 將所有的特征map拉成一條列向量。還有一維就是對(duì)應(yīng)的樣本索引。每個(gè)樣本一列,每列為對(duì)應(yīng)的特征向量net.fv = [net.fv; reshape(net.layers{n}.a{j}, sa(1) * sa(2), sa(3))];end% feedforward into output perceptrons% 計(jì)算網(wǎng)絡(luò)的最終輸出值。sigmoid(W*X + b),注意是同時(shí)計(jì)算了batchsize個(gè)樣本的輸出值net.o = sigm(net.ffW * net.fv + repmat(net.ffb, 1, size(net.fv, 2)));end
cnnbp.m
function net = cnnbp(net, y)n = numel(net.layers); % 網(wǎng)絡(luò)層數(shù)% errornet.e = net.o - y; % loss function% 代價(jià)函數(shù)是 均方誤差net.L = 1/2* sum(net.e(:) .^ 2) / size(net.e, 2);%% backprop deltas% 這里可以參考 UFLDL 的 反向傳導(dǎo)算法 的說(shuō)明% 輸出層的 靈敏度 或者 殘差net.od = net.e .* (net.o .* (1 - net.o)); % output delta% 殘差 反向傳播回 前一層net.fvd = (net.ffW' * net.od); % feature vector deltaif strcmp(net.layers{n}.type, 'c') % only conv layers has sigm functionnet.fvd = net.fvd .* (net.fv .* (1 - net.fv));end% reshape feature vector deltas into output map stylesa = size(net.layers{n}.a{1}); % 最后一層特征map的大小。這里的最后一層都是指輸出層的前一層fvnum = sa(1) * sa(2); % 因?yàn)槭菍⒆詈笠粚犹卣鱩ap拉成一條向量,所以對(duì)于一個(gè)樣本來(lái)說(shuō),特征維數(shù)是這樣for j = 1 : numel(net.layers{n}.a) % 最后一層的特征map的個(gè)數(shù)% 在fvd里面保存的是所有樣本的特征向量(在cnnff.m函數(shù)中用特征map拉成的),所以這里需要重新% 變換回來(lái)特征map的形式。d 保存的是 delta,也就是 靈敏度 或者 殘差net.layers{n}.d{j} = reshape(net.fvd(((j - 1) * fvnum + 1) : j * fvnum, :), sa(1), sa(2), sa(3));end% 對(duì)于 輸出層前面的層(與輸出層計(jì)算殘差的方式不同)for l = (n - 1) : -1 : 1if strcmp(net.layers{l}.type, 'c')for j = 1 : numel(net.layers{l}.a) % 該層特征map的個(gè)數(shù)% net.layers{l}.d{j} 保存的是 第l層 的 第j個(gè) map 的 靈敏度map。 也就是每個(gè)神經(jīng)元節(jié)點(diǎn)的delta的值% expand的操作相當(dāng)于對(duì)l+1層的靈敏度map進(jìn)行上采樣。然后前面的操作相當(dāng)于對(duì)該層的輸入a進(jìn)行sigmoid求導(dǎo)% 這條公式請(qǐng)參考 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;% endnet.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);endelseif strcmp(net.layers{l}.type, 's')for i = 1 : numel(net.layers{l}.a) % 第l層特征map的個(gè)數(shù)z = zeros(size(net.layers{l}.a{1}));for j = 1 : numel(net.layers{l + 1}.a) % 第l+1層特征map的個(gè)數(shù)z = z + convn(net.layers{l + 1}.d{j}, rot180(net.layers{l + 1}.k{i}{j}), 'full');endnet.layers{l}.d{i} = z;endendend%% calc gradients% 這里與 Notes on Convolutional Neural Networks 中不同,這里的 子采樣 層沒(méi)有參數(shù),也沒(méi)有% 激活函數(shù),所以在子采樣層是沒(méi)有需要求解的參數(shù)的for l = 2 : nif strcmp(net.layers{l}.type, 'c')for j = 1 : numel(net.layers{l}.a)for i = 1 : numel(net.layers{l - 1}.a)% dk 保存的是 誤差對(duì)卷積核 的導(dǎo)數(shù)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 保存的是 誤差對(duì)于bias基 的導(dǎo)數(shù)net.layers{l}.db{j} = sum(net.layers{l}.d{j}(:)) / size(net.layers{l}.d{j}, 3);endendend% 最后一層perceptron的gradient的計(jì)算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
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)% 這里沒(méi)什么好說(shuō)的,就是普通的權(quán)值更新的公式:W_new = W_old - alpha * de/dW(誤差對(duì)權(quán)值導(dǎo)數(shù))net.layers{l}.k{ii}{j} = net.layers{l}.k{ii}{j} - opts.alpha * net.layers{l}.dk{ii}{j};endendnet.layers{l}.b{j} = net.layers{l}.b{j} - opts.alpha * net.layers{l}.db{j};endendnet.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)% feedforwardnet = cnnff(net, x); % 前向傳播得到輸出% [Y,I] = max(X) returns the indices of the maximum values in vector I[~, h] = max(net.o); % 找到最大的輸出對(duì)應(yīng)的標(biāo)簽[~, a] = max(y); % 找到最大的期望輸出對(duì)應(yīng)的索引bad = find(h ~= a); % 找到他們不相同的個(gè)數(shù),也就是錯(cuò)誤的次數(shù)er = numel(bad) / size(y, 2); % 計(jì)算錯(cuò)誤率 end總結(jié)
以上是生活随笔為你收集整理的Deep Learning论文笔记之(五)CNN卷积神经网络代码理解的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Deep Learning论文笔记之(四
- 下一篇: 深度学习(四)卷积神经网络Lenet-5