lynxeyedの電音鍵盤

MBDとFPGAと車載で使うデバイスの備忘録

MATLABによる軽量NNの開発(1:環境の整備)

ゼロから作るDeepLearning

前回の話題からの続きになります。
こんな本をいただきました。有難うございます。




5章の誤差逆伝播を重点的にお勉強して、4~7章を読みました。SURF/SIFT/k-means/KL変換など古典的な画像処理をしていた人は4,5章だけでもいいかもしれません。それにしてもとても良い本ですね。この本。最近のNN論文を読むスピードが格段に上がります。

そういえば学生時代はまだ勾配から誤差を求めていて遅すぎて死にかけたことがありました。懐かしい。

本のサンプルコード

おそらくCS231n授業のAssignmentから引用しているのかと思われます。
Qiitaなどにコードの解説記事が乱立しているところを見ると、あー、あのPythonコードの記述やっぱ難しいんだな。という気持ちになります。
しかし、6章までの解説はかなり丁寧ですし、コードも各レイヤの順伝播、逆伝播をしっかり明記しているのはとても良いです。

この本を基にして軽量NNを実装していこうと思います。

とはいえ、Python/Numpyのお作法にリソース割かれたくないなーという気持ちが強くなります。ファー

MATLABを使用

MATLAB Deep Learning Toolboxいいですよね。僕もライセンス持ってて、よく使います。
しかし今回は、論文を見ながら各層をプリミティブに変更していくと思うので、自前で用意してしまってもいいかなという気持ちです。
今回の最終的に実装するエッジデバイスFPGAですので、GPUとアプローチがかなり異なります。
そう考えると最終的にフレームワーク以外全部自前、ということになりそうです。もちろん、ある程度レイヤが固まったら、DLTつかってもいいかも。高速に動作するはずだし。兎にも角にも、Simulinkとの連携への期待も込めてMATLABで設計していきます。

雛形を作る

ニューラルネットワークの各レイヤの理論はすごいですが、プログラム上はそんなに複雑ではない*1ので、ほぼフルスクラッチでも大変ではないと思います。

上記の「ゼロD」本のコードをそのままMATLABに逐次翻訳は、できなくはないと思いますが、やる意味はなさそうです。
PythonMATLABは1:1で逐次翻訳できそうでいて、全く思想が違う言語同士なので、動きを理解したら、1から書いた方が早いでしょう。

とはいえ、全部1から書くのも骨が折れます。雛形を用意し改造していく方がいいでしょう。今回のエンジニアリングの目的はコードを書くことではなく、論文を実機レベルに落とし込む設計をし、検証、実装することです。

で、いいものをMATLAB File Exchangeで見つけました。フルスクラッチの単純なMNIST向けCNNです。
jp.mathworks.com


ここの要となるコードは勾配を求めるnet_grad.mですが、ニューラルネットワーク各レイヤの伝播はこうなってました。

       %%順伝播
	z = w3*fc+b3;     % Affine
	z = relu(z);		% ReLu
	out = w4*z + b4;	%Affine
	out = relu(out);
	probs = softmax(out);
	L = cross_entropy(probs, lab);

%%------(中略)------------
	%% 逆伝播
	dw4= dout*z';	
	db4 = sum(dout,2); 
	dz = w4'*dout;          % ん?どこからどこまでが一つのレイヤかがわからん
 
	dw4 = sum(dw4,5);
	dz= diff_relu(z).*dz;
 
	dw3 = dz*fc';
	db3 = dz;
	db3 = sum(db3,2); 
 
	dfc = w3'*dz;

%% うんこうんこ

ちょっと順伝播、逆伝播がわかりづらかったので書き直しました。
例えば、Affineレイヤを「ゼロD」本の様に記述すると、MATLABではこうなると思います。

%% Affineレイヤ
classdef my_affine
    properties
        W
        b
        x
        dW
        db
        
    end
    methods
        function obj = my_affine(W, b)
            if nargin == 2
                
                obj.W = W;
                obj.b = b;
                obj.dW = [];
                
            end
        end
        %% 順方向    
        function [obj, out] = forward(obj, x)
            
            obj.x = x;
            out = obj.W * obj.x + obj.b;
            
        end
        %% 逆方向
        function [obj, dx] = backward(obj, dout)
            %size(obj.x)
            dx = obj.W' * dout;
            obj.dW = dout * obj.x';
            obj.db = sum(dout, 2);
            
        end
    end
end
 

こんな感じで各レイヤを記述すると、先程のnet_grad.mのコードは以下の様になります。

	%% 順伝播
	[Affine1,out]       = Affine1.forward(fc);
	[Relu3,out]         = Relu3.forward(out);
	[Affine2,out]       = Affine2.forward(out);
	[lastLayer, ~]      = lastLayer.forward(out, label);

%%------(中略)------------

 	%% 逆伝播
	[lastLayer, dout]   = lastLayer.backward(dout);
	[Affine2, dout]     = Affine2.backward(dout);
	[Relu3, dout]       = Relu3.backward(dout);
	[Affine1, dfc]      = Affine1.backward(dout);


スッキリ書けます。今回各レイヤ(Convolution/Affine/ReLu/Softmax/Cross-entropy-error)を書き直し、この雛形に押し込んだ形にしました。
コード全文は以下から。
github.com

MNISTの文字の学習です。最初は文字認識率が80%後半台と畳み込みをしてる割にあまり向上しません。
4~5epochくらい回せば、95%付近まで改善します。暇な時に回してみるのもいいかもしれません。
↓ 半日くらい根気強く回せば、このくらい(96~97%ほど)
f:id:Lynx-EyED:20210422164608p:plain

これから

FPGA向けBinaryNetの検証と、その派生論文を調べ、実装していきます。畳み込み層の計算量の低減を当面の課題とします。
このネタも、去年のRISCVネタ並みに引き摺りまくっていく次第ですのでよろしくお願いします。

*1:カルマンフィルタなどもそうですね