ゼロから作るDeep Learning3 フレームワーク編を読む その⑥ステップ38~40
はじめに
シリーズ記事の続きです。前記事は↓。 cha-kabu.hatenablog.com
本題
ステップ38 形状を変える関数
DeZeroにnumpyのreshape/transpose関数と同じ挙動をする関数を実装します。関数の実装までは今まで同様にFunctionを継承した関数クラスを作成→インスタンス化とforward処理を同時に実行する関数を作成する流れなので、ここまでの内容を理解していれば難しくありません。
backward処理の実装では言っていることは簡単で「そういうものか」と納得できなくもないのですが、分かったような分からない様な…reshapeやtransposeの微分とは??いまいちスッキリしませんが、泥沼に嵌りそうだったので「入力側の各要素値が変わったらそのまま出力側の各要素の値が変わるから」で自分を無理矢理納得させました。
また、オリジナルのreshape関数をnumpyのreshapeの様に使えるようにする(reshape(x,shape)
の形ではなく、x.reshape(shape)
の形で使えるようにする)実装内のif文について、可変長引数の処理(tupleになる)が頭にないとつまずくかも知れません。具体的に見てみます。
# 可変長引数を引数にとり、引数そのままとその長さをprintする関数 def argprint(*arg): print("arg:",arg,"|","len:",len(arg)) argprint(2,3) # arg: (2, 3) | len: 2 argprint((2,3)) # arg: ((2, 3),) | len: 1 argprint([2,3]) # arg: ([2, 3],) | len: 1
分かりやすいのは引数にただの数値をカンマ区切りで渡した最初の例で、入力がtupleで返されてその長さが2となっています。一方引数に同じく2つの数値をtupleやlistで渡した際は、それ自体がtupleで囲われ一つの要素として見なされており、長さは1になっています。
このままだとndarrayでx.reshape(((2,3),))
とした場合にエラーが発生するのと同様にエラーになってしまうので、実装では長さが1かつshape[0]の要素がtupleかlistの時はshape=shape[0]で入力を上書きしています。
ちなみに長さが1の時でも入力がtupleやlistでない(=基本的にはintの)時には、tupleになっても(6,)等になるだけなので問題ありません。
ステップ39 和を求める関数
こちらではSumクラス→sum関数を実装します。実装済みのAddは引数二つを足し合わせるものでしたが、こちらはnumpyのsum関数と同じ働きをするもので別物です。一部実装説明を先取りして使う関数が出てきますが、基本的な実装自体はこれまでと同じ流れですので書いてある内容自体は平易です。
逆伝播は前節同様にいまいちスッキリできない説明…ただsumは分割してしまえばすべて足し算でも表現できることを考えると、さきほどよりかはちょっとだけスッキリできます。
ステップ40 ブロードキャストを行う関数
ステップ39で使用先取りして使用したbroadcast関数と、その逆伝播時に必要なsumto関数の実装です。他同様に逆伝播は分かったような分からないような説明ですが、流れはここまでのステップとほぼ同じです。
ここで使用されるsum_to関数が実装の説明が省かれている割に中々ややこしかったです。機能としてはテンソルの各要素の和を求めるのですが、要素の足し方には様々なパターンがあり、引数に出力結果のshapeを与えることでどのパターンかを指定します。例えば2×2×3のテンソルであれば以下の様な出力パターンが考えられます(漏れ合ったらすみません…)。
同じ色の線が通っている箇所を足して、その結果が出力テンソルの同じ色の箇所に格納される様子を表しています。※厳密に言うと1次元の結果になるものはshapeを例えば(1,1,1)で指定すると3階テンソル、(1,1)で指定すると行列で返されて別物になるのですがそこは省略。
また、書籍には「要素の和を求めてshapeの形状にする関数はNumpyにはありません。」と書かれていますが、これは「Numpyで任意の軸方向での要素和を求められない」という意味ではありません。上図に書いている通り、Numpyの引数にタプルを渡すと様々な軸方向での要素和が求められます。ただ、書籍の通り指定したshapeになる様に和を求めることができません。
sumto関数でも最終的にsum計算を行っているのはNumpyですが、引数shapeに例えば(2×1×1)を渡すと、関数の中でそれに沿った結果を出すように(1,2)を算出し、それをnp.sum()の引数axisに渡した結果を返します。axisに渡す引数を導くアルゴリズムは実装内容見ても良くわかりませんでしたがこれだけ分かっておけばまぁ大丈夫でしょう…
Numpyのaxis指定、いつもどれがどれだか分からなくなるのですが、こちらのサイト様の説明「設定した軸(次元)に対して演算が行われ、ほかの軸のサイズがそのまま結果のサイズとなっている。」で覚えられた気がします。無くす軸を指定しているんですね。
最後に
短めですがステップ41がまた長くなりそうだったのでここで区切ります。中々DeepLearningそのものの話に入れません…