[matDL框架開發直播:2]全連接層(dense)的實現和優化
=====================================
往期:用微信控制深度學習訓練:中國特色的keras插件 - 知乎專欄
[matDL框架開發直播:0]深度學習水平考試:手寫一個輕量級框架! - 知乎專欄
[matDL框架開發直播:1]matDL概述和基本使用 - 知乎專欄
=====================================
GitHub地址:QuantumLiu/matDL
在上一期我們了解了matDL的概況並搭建、訓練了第一個模型。
像我們第一個搭建的這種模型,每一層都是一個全連接層(full connected layer/FC/dense).
全連接層是最基本、最基本的神經網路/深度神經網路層。
基礎知識
全連接層前向傳播的過程就是每一個樣本的輸入數據向量(矩陣)x,與每一個層的權值(weights)矩陣W做矩陣乘法(Hadamard product (matrices))
對於一個a=p*m的矩陣和一個b=m*q的矩陣,他們的乘積c是一個p*q的矩陣。
特別的,代表一個樣本的單個向量可以視為矩陣列數為1時的特例。一般而言,現在深度學習普遍採用mini-batch方法,x往往是形如[input dim,minibatch]的矩陣張量而非單個向量。
在matDL中,輸入張量x形如[input dim,minibatch] 或 [input dim,timestep,minibatch],權值矩陣W形如[output dim,input dim] 為了方便大家理解,我用win10畫圖畫了個靈魂圖解(敢說丑砍死你)
b代表bias是偏置項
反向傳播實際上是求x和W對於輸出y的偏導數
我們看,對於一個,的導數就是
我們對於y進行反傳求導得到的也是矩陣即dx和dw,形狀和x與w一致。
所以實際上,在得到y的梯度dy後,dW和dx可以通過
輕鬆地求出來
如果我有沒有講清楚的地方歡迎指出或者提問~
matDL實現
既然全連接層本質就是一個矩陣乘法,那麼用MATLAB寫起了就非常簡單了。
創建層(init)
function layer=dense_init_gpu(prelayer,hiddensize ,flag,loss)%% Basic layer attributes%Input tensor sahpelayer.trainable=1;layer.flag=flag;if numel(prelayer.output_shape)>2 layer.timedistributed=1;else layer.timedistributed=0;endlayer.input_shape=prelayer.output_shape;dim=prelayer.output_shape(1);batchsize=prelayer.output_shape(end);layer.type="dense";layer.prelayer_type=prelayer.type;layer.output_shape=[hiddensize,layer.input_shape(2:end)];layer.hiddensize=hiddensize;layer.batchsize=batchsize;layer.batch=1;layer.epoch=1;%% Dense layer attributes%W contains weights biaslayer.weights_dim=dim+1;layer.W=(rand([hiddensize,layer.weights_dim],"single","gpuArray")-0.5)./100;layer.input=ones([layer.input_shape(1)+1,layer.input_shape(2:end)],"single","gpuArray");layer.output=zeros(layer.output_shape,"single","gpuArray");if ~strcmpi(layer.prelayer_type,"input")&&flag layer.dx=zeros(layer.input_shape,"single","gpuArray");endlayer.e=layer.output;if nargin>3&&flag [layer.loss_f,layer.loss_df]=loss_handle(loss); layer.loss=[];endlayer.ff=@(layer,prelayer)dense_ff_gpu(layer,prelayer);layer.bp=@(layer,next_layer)dense_bp_gpu(layer,next_layer);layer.configs.type=layer.type;layer.configs.input_shape=layer.input_shape;layer.configs.output_shape=layer.output_shape;layer.configs.hiddensize=layer.hiddensize;layer.configs.W=size(layer.W);end
第一個形參prelayer是所有matDL層共有的參數,代表前一個層,用來鏈式定義每一層的input。
我們重點來看這幾句
%W contains weights biaslayer.weights_dim=dim+1;layer.W=(rand([hiddensize,layer.weights_dim],"single","gpuArray")-0.5)./100;layer.input=ones([layer.input_shape(1)+1,layer.input_shape(2:end)],"single","gpuArray");
layer.weights_dim=dim+1;
正如之前所說,對於每一個輸出,還要加一個偏置項bias。如果單出來一個長度為hiddensize的向量來儲存bias會導致計算複雜。我們不妨把偏置項看做輸入數據多出來一個維度,這個維度所以數據都是常數1,,和其他數數據一樣在權值矩陣中有自己對應的。於是我們的input多了一個全1維度,權值矩陣也多了一列bias參數。
由於MATLAB的面向對象不是很好,matDL中用struct類型來實現「偽」oop。
前向計算(feedfoward)
function layer=dense_ff_gpu(layer,prelayer)if isequal(class(prelayer),"struct") if ~isequal(size(prelayer.output),layer.input_shape) error("Shape unmatched!") end if layer.timedistributed layer.input(1:end-1,:,:)=prelayer.output; else layer.input(1:end-1,:)=prelayer.output; endelse if layer.timedistributed layer.input(1:end-1,:,:)=prelayer; else layer.input(1:end-1,:)=prelayer; endendif layer.timedistributed layer.output(:)=layer.W*sq(layer.input);else layer.output=layer.W*layer.input;endendfunction a=sq(a)a=reshape(a,size(a,1),[]);end
可以看到,我額外定義了一個函數sq(),這個函數將一個形如[input dim,timestep,batchsize]的張量展開為一個形如[input dim,timestep*batchsize]的向量。
之所以需要這個函數是為了實現TimeDistributed Dense功能,即對於每一個時間步的數據產生一個獨立的輸出,可參考包裝器Wrapper - Keras中文文檔。
我們注意到
layer.input(1:end-1,:)=prelayer;
這就對應著前面定義dense層時,layer.input矩陣多出來一維全1向量來作為偏置。
dense層的ff方法和其它層一致,除了層本身有一個形參prelayer(上一層),用來接收輸入張量。
dense層的輸出賦值給了layer.output
反向傳播(backpropagation)
function layer =dense_bp_gpu(layer,next_layer)if isequal(class(next_layer),"struct") if ~isequal(size(next_layer.dx),layer.output_shape) error("Shape unmatched!") end layer.e=next_layer.dx;endlayer.dW=sq(layer.e)*sq(layer.input)";if ~isequal(layer.prelayer_type,"input") if layer.timedistributed layer.dx(:)=layer.W(:,1:end-1)"*sq(layer.e); else layer.dx=layer.W(:,1:end-1)"*layer.e; endendendfunction a=sq(a)a=reshape(a,size(a,1),[]);end
dense層的bp方法和其它層一樣,有layer本身和next_layer兩個形參。next_layer即網路連接的下一層,本層的輸出張量是下一層的輸入張量。
我們使用
isequal(class(next_layer),"struct")
來判斷本層是否是最後一層(outputlayer),對於最後一層,本層輸出對於loss的梯度layer.e將在eval_loss()時被賦值,否則獲得前一層的輸入(即本層輸出)的梯度dx的值。
如果本層不是首層
~isequal(layer.prelayer_type,"input")
那麼我們將計算本層的dx值傳給上一層。
於是,我們得到了權值矩陣的梯度dW,接下來優化器(optimizer)會根據dW和策略,對W進行參數更新,至此,我們完成了全連接層在一個batch數據上的迭代過程!
優化
全連接層的核心矩陣乘法使用的是MATLAB的gpuArray的built-in函數,效率極高,我寫了一個.cu的mex文件,希望通過調用NVIDIA爸爸的cublas庫來優化速度。
#include <cuda_runtime.h>#include <cublas_v2.h>#include <time.h>#include "mex.h"#include "gpu/mxGPUArray.h"typedef struct _matrixSize{ unsigned int uiWA, uiHA, uiWB, uiHB, uiWC, uiHC;} sMatrixSize;void matrixMultiply(float const* d_A, float const* d_B, float* d_C, sMatrixSize &matrix_size);void mexFunction(int nlhs, mxArray *plhs[], int nrhs, mxArray const *prhs[]){ mxGPUArray const *A; mxGPUArray const *B; mxGPUArray *C; mxInitGPU(); _matrixSize matrix_size; mwSize const *A_sz; mwSize const *B_sz; float const *d_A; float const *d_B; float *d_C; char const * const errId = "parallel:gpu:mexGPUExample:InvalidInput"; char const * const errMsg = "Invalid input to MEX file."; if (nrhs != 2) { mexErrMsgTxt("Need two inputs"); } A = mxGPUCreateFromMxArray(prhs[0]); B = mxGPUCreateFromMxArray(prhs[1]); A_sz=mxGPUGetDimensions(A); B_sz = mxGPUGetDimensions(B); matrix_size.uiWA = (unsigned int)A_sz[0]; matrix_size.uiHA = (unsigned int)A_sz[1]; matrix_size.uiWB = (unsigned int)B_sz[0]; matrix_size.uiHB = (unsigned int)B_sz[1]; mwSize C_sz[3] = { matrix_size.uiWA, matrix_size.uiHB, 1 }; d_A = (float const *)(mxGPUGetDataReadOnly(A)); d_B = (float const *)(mxGPUGetDataReadOnly(B)); C = mxGPUCreateGPUArray(mxGPUGetNumberOfDimensions(A), C_sz, mxGPUGetClassID(A), mxGPUGetComplexity(A), MX_GPU_DO_NOT_INITIALIZE); d_C = (float *)(mxGPUGetData(C)); matrixMultiply(d_A, d_B, d_C, matrix_size); plhs[0] = mxGPUCreateMxArrayOnGPU(C); mxFree((void*)A_sz); mxFree((void*)B_sz); mxGPUDestroyGPUArray(A); mxGPUDestroyGPUArray(B); mxGPUDestroyGPUArray(C);}void matrixMultiply(float const* d_A, float const* d_B, float* d_C, sMatrixSize &matrix_size){ cublasStatus_t status; cublasHandle_t handle; status=cublasCreate(&handle); if (status != CUBLAS_STATUS_SUCCESS) { if (status == CUBLAS_STATUS_NOT_INITIALIZED) { mexPrintf("CUBLAS initializing error"); } getchar(); return; } const float alpha = 1.0f; const float beta = 0.0f; cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_N, matrix_size.uiWA, matrix_size.uiHB, matrix_size.uiHA, &alpha, d_A, matrix_size.uiWA, d_B, matrix_size.uiWB, &beta, d_C, matrix_size.uiWA); cudaThreadSynchronize(); cublasDestroy(handle);}
然後用mexcuda編譯
mexcuda -lcublas cublasSegmm.cu -v COMPFLAGS="-arch=compute_50 -code=sm_50 -O2"
測試一下
cuseg=@cublasSegmm;a=ones(2048,2048,"single","gpuArray");b=ones(2048,2048,"single","gpuArray");tic;for i=1:100c_cu=cuseg(a,b);end;toctic;for i=1:100c_in=a*b;end;toc
結果很尷尬
什麼情況,祭出NSIGHT來進行耗時分析先測試自己寫的
看看用了哪些kernel顯然,cublas的單精度矩陣乘法使用了這個kernel函數,maxwell開頭應該是因為我的顯卡是940m,自動選擇了合適的函數。cublas還根據矩陣大小選擇了最優的Grid&Block Dimension
接下來我們看一看內置函數什麼鬼!真的沒有弄錯嗎? (多次驗證已排除)
所以我們可以得到這樣的結論:
MATLAB的gpuArray的內建矩陣乘法是通過調用cublas寫的,性能達到了最優(我自己寫的其實快2%),矩陣乘法沒有優化餘地,也就是說dense層優化餘地不大。
所以說,就連MATLAB工程師也不會幹自己寫kernel懟cublas庫這種螳臂當車的事情~
========================
開發計劃:
4.10~4.17:完善框架,evaluate方法加入metrics(指標),添加新的優化器(目前只有動量sgd);
4.17~4.24:改用cudnn的RNN替代現有函數,現有函數作為備用和演算法常式保留;用cudnn的CNN、pooling實現CNN類型的層
4.24~5.1:實現merge、多輸出/輸入模型構建,將layer、model統一為「節點」
直播計劃:
4.10~4.17:
[matDL框架開發直播:1]matDL概述和基本使用 - 知乎專欄
[matDL框架開發直播:2]全連接層:dense的實現和優化
[matDL框架開發直播:3]LSTM層的實現和優化
4.17~
[matDL框架開發直播:4]Activation激活函數和激活層
[matDL框架開發直播:5]Loss損失函數及求導
[matDL框架開發直播:6]Optimizer優化器
[matDL框架開發直播:7]Layer層
[matDL框架開發直播:8]Model模型
推薦閱讀:
※Python和MATLAB交互的基本操作
※MATLAB圖像處理中的小波變換
※plot 中哪些顏色線形搭配會讓圖好看些?
TAG:深度学习DeepLearning | MATLAB | 编程 |