自己微分メカニズムの詳細な説明

  自動微分は関数の微分値をコンピュータプログラムで計算できる方法で、その基本原理は微分アルゴリズムです。関数 function の各独立変数の 1 次導関数を手動で式を導出することなく見つけることができるため、自動微分の基本的な考え方と方法はディープ ラーニング フレームワークで広く使用されています。自動微分法が登場する以前にも、数値微分や記号微分などの方法がありましたが、これらの方法も非常に独創的ですが、実際の問題に広く使用することは困難です。

1: 基本原則

1.順伝播

  自動微分の原理は導出規則であり、アルゴリズム構造は再帰ツリーに似ています。多変数関数式 f(x1,x2..,xn) の場合、その演算は常に、次のような 2 つの独立変数または独立変数と定数の加算と乗算と見なすことができます。

f(a,b,c,d)=2\times a \times b+c \times c\times d+d\times d

その操作ツリー図は次のように表すことができます。

  任意の演算は徐々に 2 要素演算に分解でき、加算と乗算は 2 種類しかなく、加算と乗算が複数ある場合は、2 つのリーフ ノードが徐々にルート ノードに戻るプロセスでもあることがわかりました。 . リーフ ノードからルート ノードに段階的に戻るこのプロセスは、関数の任意の独立変数の偏導関数 (勾配) を見つける最初のステップであるフォワード トランスファーです。テンソルの場合、操作はより豊富になりますが、ツリー全体の構造は一貫しています。

class auto:
    def __init__(self,value):
        self.value=value
    def __add__(self,other):
        return auto(value=self.value+other.value)
    def __mul__(self, other):
        return auto(value=self.value*other.value)
    def multiply(self,other):
        return auto(value=np.multiply(self.value,other.value))
    def dot(self,other):
        return auto(value=np.dot(self.value,other.value))
if __name__=='__main__':
    a=auto(1)
    b=auto(2)
    c=auto(3)
    f1=a*b+c
    d=auto(np.random.randint(0,2,size=(3,2)))
    e=auto(np.random.randint(0,2,size=(2,3)))
    f2=d.dot(e)
    print('f1',f1.value)
    print('f2',f2.value)

  操作の前に、数値または行列を auto オブジェクトに変換すると、auto の関数を使用して対応する操作を実行できます。ここでの __add__ と __mul__ は組み込みメソッドで、数値の加算と乗算を書き換えて、'+' と '*' を直接演算に使用できるようにします。もちろん、add 関数と mul 関数を直接定義することもできるので、a+b を a.add(b) に変更する必要があり、最終的な効果は同じです。(実際、よく考えてみると、この方法は、numpy、pytorch、その他のライブラリなど、多くの側面に反映されています。計算を実行するとき、データを対応する配列またはテンソル オブジェクトに変換してから計算する必要があります。となり、計算方法も異なります。この方法と同様です。)

  ここで auto オブジェクトを使用する理由は、操作プロセス中にステップごとに再帰することができ、転送プロセス中にノードの 2 つの子ノードとこれら 2 つのノードの操作メソッドを保存できるため、後続の反転を行うことができます。

2. バックプロパゲーション

  方向伝播の理由は、順伝播によって計算ツリーを構築した後、派生プロセスがルート ノードからリーフ ノードに段階的に渡されるためです。f(a,b,c,d) in 1 を例にとると、最初に f の f 自体への偏導関数を計算すると、結果は 1 になります (このステップは理論上省略できますが、実際には省略できません)。行列演算 ); f から (2ab+ccd) への偏導関数は 1; f から dd への偏導関数は 1; その場合、f から 2ab への偏導関数は:

\frac{f}{f}.\frac{\partial f}{\partial 2ab+ccd}.\frac{2ab+ccd}{2ab}=1\times 1\times1=1

この時点で、特定の変数に対する関数の導関数を見つけたい場合、この変数の親ノードに対する関数の偏導関数と、特定の変数に対する関数の偏導関数を知る必要があることがわかりました。 node は、このノードの親ノードに対する関数の偏導関数です。そのノードに対する親ノードの偏導関数の積です。このようにして、f の a への偏導関数を次のように見つけることができます。

\frac{\partial f}{\partial a}=\frac{f}{f}.\frac{\partial f}{\partial (2ab+ccd)}.\frac{\partial (2ab+ccd)} {2ab}.\frac{\partial 2ab}{2a}.\frac{2a}{a} \\ =1\times1\times1\times b\times 2=2b

最終結果は事実に適合します。

  もちろん、ここでは a が 1 つのブランチにのみ表示されます。a が別のブランチにある場合、結果は複数の連鎖乗算の合計になります。たとえば、f=2a+2ab の場合、a の導関数は 1×2+1×2×b=2+2b であり、後でアルゴリズムを実装する際に注意する必要があります。

  行列導出の場合、行列導出は左掛け算と右掛け掛けを区別し、場合によっては行列の転置も行われるため、左から右へ自由に数を掛けることができません.この部分はTutuのブログの前に参照できます. 「行列導出の詳細解説(本質、原理、導出)」と「多層パーセプトロン、全結合ニューラルネットワーク…詳細解説」。Tutu は、ここで一般的な行列導出の結論を示しています。

Y_1=W_1.A_1 \\Y_2=W_2.Y_1 \\ \Rightarrow \\ \frac{\partial Y_2}{\partial Y_1}=W_{2}^T .\frac{\partial Y_2}{\partial Y_2} \\\frac{\partial Y_2}{\partial W_2} =\frac{\partial Y_2}{\partial Y_2}.Y_{1}^T \\ \frac{\partial Y_2}{A_1}=W_{1 }^T.W_{2}^T.\frac{\partial Y_2}{\partial Y_2} \\ \frac{\partial Y_2}{\partial W_1}=W_{2}^T.\frac{\partial Y_2}{\partial Y_2}.A_{1}^T

  この規則は比較的簡単に見つけることができます.行列の乗算では、導出された独立変数が式の乗算座標にある場合、もう一方の独立変数は転置されて右に乗算され、その逆も同様です. 行列を数と見なすと、数の転置は依然としてそれ自体であり、乗算の可換法則に従うため、通常の関数導出に退化します。このルールのアルゴリズムを auto オブジェクトに書き込むことで、BP アルゴリズムの各パラメーターの勾配を計算することができ、手動で式を導き出し、複雑なアルゴリズムの実装を実行する必要はありません。もちろん、畳み込み層については、畳み込みカーネルの偏導関数 (損失関数) を記述することもできますが、これは本質的に行列導関数のままです。アクティベーション関数については、導関数を導出できますが、畳み込み演算は通常、1 つの要素 (softmax などの関数を除く) のみを受け取り、結果を出力します。通常の方法は同じです。

2: アルゴリズムの実装

  自動微分の難しさはアルゴリズムの実装にあります. 再帰的な木構造であるため, 理解するのが難しい場合があります. 決定木やモンテカルロ木などの類似のアルゴリズムでも同様です.

  自動微分のために、フォワードパスによって生成された演算ツリー内のノードの値 (value) を格納するために使用される auto オブジェクトを定義し、子ノード auto (depend)、子ノード操作モード (opt)、初期勾配 ( grad) は None または 0 です。バックプロパゲーション プロセスの後、各ノードへの関数の導関数は、連鎖導関数の原理に従って徐々に計算されます。

import numpy as np

class auto:
    '''自动微分'''
    def __init__(self,value,depend=None,opt=''):
        self.value=value #该节点的值
        self.depend=depend #生成该节点的两个子节点
        self.opt=opt #两个子节点的运算方式
        self.grad=None #函数对该节点的梯度
    def add(self,other):
        '''数或矩阵加法'''
        return auto(value=self.value+other.value,depend=[self,other],opt='+')
    def mul(self, other):
        '''数的乘法或数与矩阵乘法'''
        return auto(value=self.value*other.value,depend=[self,other],opt='*')
    def dot(self,other):
        '''矩阵乘法'''
        return auto(value=np.dot(self.value,other.value),depend=[self,other],opt='dot')
    def sigmoid(self):
        '''sigmoid激活函数'''
        return auto(value=1/(1+np.exp(-self.value)),depend=[self],opt='sigmoid')
    def backward(self,backward_grad=None):
        '''反向求导'''
        if backward_grad is None:
            if type(self.value)==int or float:
                self.grad=1
            else:
                a,b=self.value.shape
                self.grad=np.ones((a,b))
        else:
            if self.grad is None:
                self.grad=backward_grad
            else:
                self.grad+=backward_grad
        if self.opt=='+':
            self.depend[0].backward(self.grad)
            self.depend[1].backward(self.grad) #对于加法,把函数对自己的梯度传给自己对子节点的梯度
        if self.opt=='*':
            new=self.depend[1].value*self.grad
            self.depend[0].backward(new)
            new=self.depend[0].value*self.grad
            self.depend[1].backward(new)
        if self.opt=='dot':
            new=np.dot(self.grad,self.depend[1].value.T)
            self.depend[0].backward(new)
            new=np.dot(self.depend[0].value.T,self.grad)
            self.depend[1].backward(new)
        if self.opt=='sigmoid':
            new=self.grad*(1/(1+np.exp(-self.depend[0].value)))*(1-1/(1+np.exp(-self.depend[0].value)))
            self.depend[0].backward(new)

if __name__=='__main__':
    a=auto(3)
    b=auto(4)
    c=auto(5)
    f1=a.mul(b).add(c).sigmoid()
    f1.backward()
    print(a.grad,b.grad,c.grad) #f1对a,b,c,导数
    A=auto(np.random.randint(1,20,size=(3,4)))
    B=auto(np.random.randint(1,20,size=(4,3)))
    F=A.dot(B)
    F.backward()
    print(A.grad,B.grad)

  auto を構築した後、使用時に数値または行列を auto オブジェクトに変換し、それに含まれる計算関数を使用して計算を実行し、最終結果に対して逆方向を使用して、独立変数ごとにこの関数の勾配を見つけます。もちろん、backward を使用する人は誰でも、各独立変数の偏微分を取る人です。深層学習では、通常、パラメータに対する損失関数の偏導関数です。上記のコードに関して、学習に興味がある人はさらに関数を追加して auto の機能を充実させることもできます。

  注意深い学生は、pytorch や tensorflow などの深層学習フレームワークでは、backward() のメソッドも勾配を見つけるために使用されていることがわかります。これは、ここでの使用法とほぼ同じであり、これらの背後でも自動微分が使用されていることを示しています。ディープ ラーニング フレームワーク この方法はさらに複雑です。

3: まとめ

  勾配を計算する方法として、自動微分法は深層学習の発展を大きく促進しました.手動で式を導出し、アルゴリズムを実装するという面倒な操作を取り除くことができ、畳み込み、完全接続、および再帰型ニューラル ネットワーク。実際のアプリケーションで自動微分アルゴリズムを記述する必要はありませんが、その再帰アルゴリズムのアイデアは非常に重要であり、この種のアルゴリズムの基本的なアイデアをよりよく理解することができます。

おすすめ

転載: blog.csdn.net/weixin_60737527/article/details/127414198
おすすめ