※本記事は、クリストファー・マニング教授によるスタンフォード大学のオンライン講義「ニューラルネットワークの数学的基盤:勾配降下法と逆伝播の解説」の内容を基に作成されています。講義の動画は https://youtu.be/HnliVHU2g9U でご覧いただけます。本記事では、講義の内容を要約しております。なお、本記事の内容は原講義の内容を正確に反映するよう努めていますが、要約や解釈による誤りがある可能性もありますので、正確な情報や文脈については、オリジナルの講義をご視聴いただくことをお勧めいたします。
クリストファー・マニング教授はスタンフォード大学のトーマス・M・シーベル機械学習教授であり、言語学および計算機科学の教授でもあります。また、スタンフォード人工知能研究所(SAIL)の所長を務めています。
スタンフォード大学のオンラインAIプログラムについての詳細は https://stanford.io/ai でご覧いただけます。この講義の受講に関する詳細は https://online.stanford.edu/courses/c... で、講義スケジュールとシラバスについては https://web.stanford.edu/class/archiv... でご確認いただけます。
1. 講義の導入と準備
1.1. 課題の状況と提出について
こんにちは皆さん、始めましょう。今日は第2週の火曜日なので、皆さんは課題1を終えたはずです。皆さん課題1は終わりましたか?
私がこれを言っているのは、おそらく間違った人に向けて話しているかもしれませんが、毎年一部の学生が課題1で遅延日数を使ってしまうケースがあります。それは本当に間違った使い方です。
ですから、皆さんが課題1を終えていることを願っています。これは簡単な導入部分として設計されており、そこからすぐに次に進みます。
1.2. 課題2の目的と内容
今日は課題2を公開しました。課題2には二つの目的があります。一つ目の目的は、皆さんに数学を実践してもらうことです。ニューラルネットワークが実際に何を計算し、どのように計算するかについての理解を深めるための数学です。そしてこれは今日の講義で説明する内容でもあります。
しかし同時に、課題2ではおそらく三つのことを学ぶことになります。課題2では依存関係のペアリング(dependency pairing)についても学びます。これは言語構造と言語学に関する内容です。
さらに三つ目として、課題2ではPyTorchの使用を始めます。PyTorchはディープラーニングのための主要なソフトウェアフレームワークの一つで、このクラスで使用していきます。課題2でのPyTorchの利用は非常に足場が組まれています。基本的には「これがあって、この二行を書いて、この二つの関数を使ってください」というような形式です。
1.3. PyTorchチュートリアルの案内
PyTorchの習得をサポートするために、金曜日の午後3時30分にGates B01でPyTorchに関するチュートリアルを行います。これも録画される予定です。このチュートリアルは、課題2に取り組む前にPyTorchとその動作方法についてより深く理解するための絶好の機会です。課題2に取り組む前に、PyTorchについての感覚をつかんでおくといいでしょう。
1.4. 推奨読書資料について
ほとんどの講義で、さらに読むべき資料を紹介しています。このクラスのすべての講義の中でも、多くの人にとって今回は本当に推奨読書資料を見るといい機会かもしれません。いくつかの読み物があり、それらは行列微積分と、このクラスで必要となる線形代数についての短いチュートリアルやレビューとなっています。
これらを読むことを強くお勧めします。もしその中で一つが特にお気に入りになれば、Edで教えてください。どれが最も良いと思うか、選んでみてください。個人的には、リストの最初にあるものが好きですが、皆さんは違う意見を持つかもしれません。
今日は全て数学の内容で、木曜日は言語と言語学の内容になります。人によっては言語や言語学も難しいと感じるかもしれませんが、それはそれぞれのタイプによるものでしょう。
2. ニューラルネットワークの基礎
2.1. 前回の復習:ニューラルネットワークの構造
前回の講義で、小さなニューラルネットワークを紹介し、オレンジ色のそれぞれの部分は基本的に小さなロジスティック回帰ユニットのようなものだと説明しました。統計学や機械学習のクラス(Stats 109など)で学ぶロジスティック回帰との重要な違いは、そちらでは一つのロジスティック回帰を使い、入力特徴を定義し、出力に何らかの決定変数を持つのに対し、こちらでは小さなロジスティック回帰のカスケードを構築している点です。
ニューラルネットワークでは、最終的に何が欲しいかを定義し、目的関数や損失関数でそれを捉えます。しかし中間の部分では、ニューラルネットワークが自分自身で下流のニューロンに役立つ入力は何か、最終的な計算に役立つ出力を提供するために入力に関してどのような関数を作るべきかを学習する機会があります。
もしこれについてあまり考えたことがない場合は、このアイデアについてしばらく考えてみる価値があります。これは本当に強力なアイデアであり、ニューラルネットワークを他の形式の機械学習よりも多くの状況でより強力にしているものです。最終的に行いたいことのために下流で役立つものを計算するための中間レベルの表現が自己組織化されるという事実が重要です。
この図を再度表示した理由はもう一つあります。ここから行列の話に直接移りたいからです。ニューロンを好きなように配線することもできますし、人間の脳を見るとニューロンが好きなように配線されているように見えますが、ニューラルネットワークで行われることは基本的に常にレイヤーの規則的な構造を持っています。このようなレイヤーの規則的な構造があると、一つのレイヤーのニューロンの出力を重みとともに次のレイヤーの入力を生成するために使います。
2.2. 行列計算としてのレイヤー処理
レイヤーの規則的な構造があると、あるレイヤーのニューロンの出力X1、X2、X3を取り、それらすべてに重みを掛け、バイアス項を加え、そして非線形関数を通して次のレイヤーの値を得ることになります。これをベクトルとしてまとめると、まず行列乗算を行い、入力のWXを計算し、次にバイアス項をベクトルとして加えてZ(中間値)を得ます。その後、非線形性または活性化関数を適用して、ニューラルネットワークの次のレイヤーの値を得ます。
活性化関数はベクトルに適用されてベクトルを生成しますが、実際にはそのベクトルの個々の要素に一つずつ操作を行います。つまり、ベクトルの各要素に適用されるスカラー関数があるのです。
前回の例を続けて使います。入力ウィンドウの中央にある単語が位置(location)かどうかを判断するというものでした。行列乗算を行い、非線形関数を通し、ドット積を計算し、それをシグモイド関数に入れて、「はい」か「いいえ」を予測します。
2.3. 非線形性(活性化関数)の役割と種類
最後に少し話したいのは、非線形性または活性化関数のfについてです。これらが歴史的にどこから来たのかというと、基本的なニューロンの動作を、入力の行列乗算を行い、そしてニューロンが発火すべきかどうかを見るための閾値またはバイアス項を持つことで表現できるというアイデアからです。
実際、1940年代にさかのぼる最初の実装では、閾値として行われました。つまり、活性化が閾値θより大きければ1を出力し、そうでなければ0を出力するというものでした。しかし、閾値を使うと、二つの線は平らになります。勾配がないのです。これは学習をはるかに難しくします。
ニューラルネットワークで構築する秘訣、そして今日では「勾配ベースの学習」という名前で知られているものの全体的なアイデアは、実際に勾配を持っていれば、春休みにスキーをするようなもので、どこが急なのかを把握して急な方向に進むことができるということです。これにより関数の最適化ができ、はるかに速く学習できるようになります。そのため、単に閾値ユニットを持ちたくなく、勾配を持つものが欲しいのです。
その後の研究では、勾配を持つ活性化関数が使われるようになりました。最初に広く使われたのは、確率に変換するためのシグモイド関数(ロジスティック関数)でした。しかし、これは出力が常に非負であるため、数値が大きくなる傾向があるという点で不完全に思えました。
そこで、tanh関数がかなり使われるようになりました。実際、課題3でもtanh関数を使うことになります。通常、tanh関数は指数関数で表されますが、数学が不慣れだとtanhとロジスティック関数の関係は明らかではないかもしれません。しかし、これを数学の問題として考えると、tanhは実際にロジスティック関数を2倍に引き伸ばして1だけ下げた同じ関数なのです。
しかし、tanhを計算するには指数計算をする必要があり、コンピュータでは少し遅いこともあります。もっと安価なもので済まないかと考えられ、「ハードtanh」と呼ばれるものが検討されました。これは中央で傾きが1あり、その領域外では平らになるものです。これは多くの場合うまく機能し、それがReLU(整流線形ユニット)の人気につながりました。
ReLUは単純に負の領域では0、正の領域ではx(入力値)です。これは勾配ベースの学習について言ったことに反するようで、負の領域では勾配がなく、ニューロンは「死んで」いるからです。しかし正の領域では勾配があり、特にシンプルです - 傾きは常に1です。
これはまだ少し不自然に感じますが、個々のニューロンが負になったときに半分の時間「死んで」いるにもかかわらず、ニューラルネットワーク全体では一部のユニットは活性化されているため、一種の特殊化を提供します。また、傾きが常に1であるという事実は、後で話す勾配の逆流が非常に簡単に行えるようになります。そのため、ReLUによる学習は非常に効果的であることが分かり、ReLU非線形性が至る所で使われるようになり、標準的な選択肢となりました。課題でもReLUを使用することになりますが、特に課題2ではそれがうまく機能することを確認できます。
それでも、ある時点で人々は「範囲の半分で死んでいることは、数年間うまく機能したように見えても、やはり良いアイデアではないかもしれない」と考え直しました。そこで後に起こったのは、ある意味でReLUに似ているが実際には「死んで」いない他の関数を考案することでした。
その一つのバージョンが「Leaky ReLU」です。Leaky ReLUでは、負の半分も直線になりますが、非常に小さな傾きを持ちます。それでも少し傾きがあります。そのバリエーションとして「Parametric ReLU」があり、負の部分の傾きが実際にはどれくらいなのかを指定する追加パラメータを持ちます。
最近では、特に最近のTransformerモデルでよく見られるのは、SwiSHやGELUなどの非線形性です。これらは両方とも洗練された関数ですが、基本的にはほぼx(ただ近似的に)であり、下部に曲線部分があり、これもまた少しの勾配を与えます。曲線が反対方向に行くのは少し変ですが、最近のTransformerモデルでよく使われ、うまく機能しているようです。
2.4. 非線形性が必要な理由
非線形性が必要な理由について考えることが重要です。ニューラルネットワークで行っているのは関数近似です。学習したい非常に複雑な関数があります。例えば、テキストからその意味を抽出したり、視覚的なシーンを解釈したりするような関数です。そのため、本当に優れた関数近似器を構築したいのです。
もし単に行列の乗算だけを行っているならば、ベクトルの行列乗算は線形変換です。これでは複雑な関数を乗算することができません。厳密に言えば、最後にバイアスを追加すると、それはアフィン変換になりますが、単純に考えれば線形変換です。複数の行列乗算、つまり複数の線形変換を行った場合でも、それらは合成されるため、これら二つの行列を掛け合わせるだけで単一の線形変換になってしまいます。
表現力という観点では、単なる行列乗算を行う多層ネットワークでは何も力を得られません。ただし、学習という観点では実際に力を得ることができます。ニューラルネットワークを研究する理論コミュニティでは、表現力はないものの興味深い学習特性を持つため、非線形性のない行列乗算の連続である「線形ニューラルネットワーク」を調査した論文が多くあります。
しかし私たちは、このような直線だけでなく、もっと複雑な関数を学習できるようになりたいのです。そのためには線形変換以上のものが必要であり、非線形関数を計算できるものが必要です。活性化関数によって非線形関数を得ることができるのです。
3. 勾配ベースの学習
3.1. 勾配降下法の基本概念
続いて今日の主題に入りましょう。私たちが行いたいのは勾配ベースの学習です。これが確率的勾配降下法の方程式です。ここで逆さの三角形の記号は勾配を表しています。目的関数の傾きを計算したいのです。これが勾配を計算することで学習する方法です。
私たちが知りたいのは、任意の関数の勾配をどのように計算するかということです。今日はまず手作業で数学的に行う方法を説明し、次に計算でどのように行うかを議論します。これが実際にはニューラルネットワークを支えるパワーとして知られている有名なバックプロパゲーションアルゴリズムですが、バックプロパゲーションアルゴリズムは単に数学を自動化しているだけです。
数学の部分に関しては行列微積分になります。この時点で、私よりもはるかに多くの数学を知っている人から、これをほとんど学んだことがない人まで、大きなスペクトルがあります。しかし、基本的なことを説明する、あるいは思い出させることができれば、少なくともその他の資料を読み、課題2に取り組むための出発点になると思います。
時間の半分をこの二つの部分に費やす予定です。この後、皆さんが「ああ、ニューラルネットワークが内部でどのように機能しているのか実際に理解できた」と感じられることを願っています。
3.2. 行列微積分の基礎
スタンフォードの学生であれば、Math 51を受講したかもしれません。または受講できたかもしれません。このコースでは線形代数、多変量微積分、現代的な応用について教えています。Math 51は今日話すことすべてを含み、さらに多くのことをカバーしています。もし実際にそれを知っていて覚えているなら、次の35分はInstagramを見ていてもかまいません。
しかし問題は、多くの人が初年次にこれを取ったり、10週間で多くのことを扱うことで、このクラスを受講した人の多くが2年後には実際にそれを使う能力をあまり持っていないことです。
この本を本当に熱心に長い時間読んでいれば、実際に本の最後の方の付録Gに、ニューラルネットワークと多変数連鎖律についての付録があることに気づいたでしょう。これはまさに私たちがニューラルネットワークで使うものです。しかし問題が二つあります。一つは、これが本の697ページ目にあることで、そこまで到達する人がいるかどうかわからないこと。もう一つは、たとえそこまで行っても、これらのページは非常に密度の高いテキストページで、理解するのが簡単ではないことです。
ですから、これについての私の説明を試みます。頭に入れておくべき考え方は、もし基本的な単変数微積分を覚えていれば、例えば3x²の導関数が6xであることを知っていれば、それだけで十分です。要点は、多変数微積分は単変数微積分と同じようなものですが、行列を使うということです。
これが私たちの信条であり、それに基づいて行列微積分、またはそれを一般化したテンソル微積分を行います。つまりベクトル、行列、より高次のテンソルを使います。ニューラルネットワークの世界で「ベクトル化された勾配」と呼ばれるものでこれらを行うことができれば、それが操作を行う高速で効率的な方法になります。
すべてを考え抜きたければ、一度に一つの変数で行い、正しいことをしているかを確認できます。これは最初の講義でもある程度示しましたが、ネットワークを高速に動かしたければ、行列微積分を行いたいのです。では、それを行うための準備をしましょう。
これは皆さんが覚えていると信じる部分です。f(x) = x³があり、単変数微分を行うと、導関数は3x²です。これを覚えていますか?これは皆が始められるポイントです。
この導関数は物事の傾きを表しています。つまり、傾きによって物事がどれだけ急かを知ることができ、スキーができるのです。それが私たちの目標です。物事の傾きは、入力を少し変えたときに出力がどれだけ変わるかを示します。これが急さの尺度です。
導関数が3x²なので、x = 1のとき、傾きは約3 × 1² = 3です。関数の値をx = 1.01で計算すると、0.01だけxを動かしたことで出力が約0.03増加しています。一方、x = 4に行くと、導関数は3 × 4² = 48になり、4から4.01への小さな違いが出力で48倍に拡大されて64から約64.48になっています。
では、マントラを思い出しましょう。これは単変数微積分とまったく同じですが、より多くのものがあります。n個の入力を持つ関数がある場合、その勾配を計算します。これは各入力に関する偏導関数です。勾配は入力の数と同じサイズのベクトルになります。
このファンキーな記号(∂)は人によって様々な発音がありますが、これは本当にdの一種の書き方です。私はほとんどの場合、単にdと呼びますが、時には「偏微分」や「変なd」などと呼ぶ人もいます。各変数に対して∂f/∂x₁、∂f/∂x₂などを計算します。
さらに進んで、n個の入力とm個の出力を持つ関数がある場合、勾配はヤコビアン(Jacobian)と呼ばれるものになります。実際には、この名前の由来となった人はドイツ系ユダヤ人なので、本当は「ヤコビ(Jacobi)」と発音すべきですが、この国では誰もそう言いません。
ヤコビアンは偏導関数の行列で、各出力と各入力に対して、入力の成分と出力の間の偏導関数を計算します。これはニューラルネットワークのレイヤーで持つようなものです。ニューラルネットワークのレイヤーではn個の入力とm個の出力があるため、このようなヤコビアンを使用することになります。
<userStyle>Normal</userStyle>
3.3. 合成関数の導関数と連鎖律
ニューラルネットワーク全体のアイデアは、これらの多層の計算があり、それらが関数の合成に対応するということです。関数を計算するためにも、その勾配を計算するためにも、合成の方法を知る必要があります。
一変数関数があり、二つの関数の合成に関してその導関数を計算したい場合、私たちは計算を掛け算しています。例えば、z(y) = 3y²を合成する場合、その導関数は6yになります。部分的に計算すると、dz/dy = 3yとなり、dy/dx = 2xとなります。これらの二つの部分を掛け合わせて全体の導関数を計算すると、6xという同じ答えが得られます。
行列微積分は単変数微積分とまったく同じですが、異なる次元のテンソルを使用します。テンソルという言葉は、スカラーからベクトル、行列、そして計算機科学では通常多次元配列と呼ばれるものまで、そのサイズのスペクトルを上がっていくものを意味します。このスペクトルは異なる次元のテンソルと呼ばれます。
多変数関数がある場合、ヤコビアンを掛け合わせます。ここでは関数 WX + B があり、それに非線形性 f を合成して h を得ます。これらの偏導関数(ヤコビアン)の積として同じ方法で計算できます。
いくつかの例を見てみましょう。まず要素ごとの活性化関数から始めます。以前に計算された量の活性化関数としてベクトルを計算する場合、h_i = f(z_i) のように成分ごとに計算します。ここで f は実際にはスカラーに適用される活性化関数ですが、全体としてこのレイヤーは n 個の出力と n 個の入力を持つ関数であり、n × n のヤコビアンを持ちます。
このヤコビアンの定義では、i = j の場合、出力 h_j は z_i に依存し、それ以外の場合はゼロになります。対角成分以外では、値を変更しても出力は変わりません。出力は対応するインデックスにのみ依存するからです。したがって、活性化関数のヤコビアンは、対角項以外がすべてゼロの行列になります。対角項は活性化関数を計算している部分に対応し、それらの部分については活性化関数の導関数を計算する必要があります。
これは課題2の問題の一つであり、「ロジスティック関数の導関数を計算できますか?」というものです。今日はその答えを明かしませんが、それを f'(z) に直接代入できるようになります。
ヤコビアンで行いたい他のことは、WX + B というニューラルネットワークのレイヤーがあり、X に関するその偏導関数を計算したい場合です。ここでマントラを思い出すと役立ちます。「行列微積分は単変数微積分と同じだが、行列を使う」。あまり頭を使わずに、単変数微積分と同じなら答えはどうなるはずかと考えれば、それは明らかに W になります。実際にそうなります。
同様に、WX + B の B に関する偏導関数を計算したい場合、単変数微積分では1になるので、行列微積分では単位行列になります。これは B が実際にはベクトルであり、単位行列として出てくる必要があることを反映しています。
例の上部では、この種のベクトルのドット積 U^T h を行いました。その微分を計算すると、h^T が出てきます。これは最初のクラスでドット積計算を行ったときのようなもので、各個々の要素に対して反対の項が得られ、結果として他のベクトルが出てきます。これらは自宅で練習として計算し、答えがなぜそうなるのか確認するとよいでしょう。
4. バックプロパゲーション
4.1. バックプロパゲーションアルゴリズムの2つの要素
ニューラルネットワークを支える有名なものがバックプロパゲーションアルゴリズムです。このアルゴリズムは人々を有名にしましたが、その発明は効果的な学習アルゴリズムを提供したからです。しかし、基本的なレベルでは、バックプロパゲーションアルゴリズムはたった二つのことだけです。
一つ目は連鎖律を使うこと、つまり複雑な関数の微積分を行うことです。二つ目は中間結果を保存して、同じ計算を二度と行わないようにすることです。バックプロパゲーションアルゴリズムはそれだけです。
計算的に関数を扱い、バックプロパゲーションを行いたい場合、それらをグラフとして表現することができます。このようなグラフは何らかの形でニューラルネットワークフレームワーク内部で使用されています。ここでは、中央の単語が位置(location)かどうかを見つけるための小さなニューラルネットワークを再表現しています。Xベクトル入力を取り、Wを掛け、Bを加え、非線形関数を通し、ベクトルUとドット積を計算します。これが私の計算です。
このグラフのソースノードは入力であり、内部ノードはそこで行う操作です。そしてそれらを接続するエッジはそれぞれの操作の結果を渡します。WXを加算関数にBと一緒に渡し、それがZを与え、それを非線形関数に渡してHを得、そしてそれをUとドット積を計算してSを得ます。
この計算を行いますが、これはフォワードプロパゲーションまたはニューラルネットワークのフォワードパスと呼ばれています。フォワードパスは単に関数を計算するだけです。しかし一度それを行った後、勾配を計算して勾配ベースの学習を行いたいのです。その部分はバックプロパゲーションまたはバックワードパスと呼ばれています。
バックワードパスでは同じグラフを使用し、勾配を後ろに渡します。右側から始め、ds/dsを持ちます。ds/dsは1です。なぜなら、Sを変更するとSが変わるからです。そして私たちが行いたいのは、さらに後ろに戻って、ds/dz、ds/db、ds/dw、ds/dxなどを計算することです。これが勾配として計算したいものです。
<userStyle>Normal</userStyle>
4.2. 計算グラフとフォワードパス
計算を扱う方法を見ていきましょう。ニューラルネットワークをグラフとして表現することで、各要素とその関連性を明確にできます。例えば小さなニューラルネットワークを例に取ると、入力ベクトルXを取り、それに重み行列Wを掛け、バイアスBを加え、非線形性を通し、最後にベクトルUとドット積を計算するという流れがあります。
このグラフのソースノードは入力であり、内部ノードは実行する操作を表しています。ノードを接続するエッジは、各操作の結果を次の操作に渡します。具体的には、WXが計算され、それがBとの加算操作に渡されてZが生成されます。次にZは非線形関数を通してHを生成し、さらにそれがUとのドット積計算に使われてスコアSが計算されます。
これが私たちが実行する計算であり、これはフォワードプロパゲーションまたはニューラルネットワークのフォワードパスと呼ばれています。フォワードパスは単純に関数値を計算します。しかしその後、勾配を計算して勾配ベースの学習を行いたいのです。
フォワードパスの計算は非常に直接的です。入力から始めて、各ノードで定義された操作を順番に適用していくだけです。このプロセスはニューラルネットワークの「予測」フェーズに対応しています。入力データに対して、ネットワークがどのような出力を生成するかを見ています。
計算グラフを使用する利点は、複雑な関数を小さな部分に分解できることです。各ノードは単純な操作を表し、それらを組み合わせることで複雑な計算を行います。また、この構造によって効率的な勾配計算が可能になります。
4.3. バックワードパスでの勾配の伝播
フォワードパスが完了したら、次は勾配を計算するバックワードパスを実行します。これはグラフの右側から始めて逆方向に進みます。初めにds/dsを計算しますが、これは単純に1です。なぜならSを変更するとSが変わるためです。その後、グラフを遡りながらds/dz、ds/db、ds/dw、ds/dxなどの勾配を計算していきます。
単一ノードを見てみましょう。例えば、h = f(z)という非線形性ノードでは、上流勾配ds/dhがあり、次の変数への下流勾配ds/dzを計算したいとします。そのためには、fの勾配(局所勾配)を見ます。これによって連鎖律が得られます。ds/dz = ds/dh × dh/dzとなります。つまり、下流勾配 = 上流勾配 × 局所勾配です。
複数の入力を持つノードの場合、例えばz = Wx + bのようなケースでは、単一の上流勾配があり、各入力に関する下流勾配を計算したいとします。各入力に関する局所勾配を計算し、上流勾配と局所勾配を掛け合わせるという同じ方法で行います。これも連鎖律の適用です。
具体的な例を見てみましょう。これは通常ニューラルネットワークで見るようなものではありませんが、簡単な例として、f(x, y, z) = (x + y) × max(y, z)という関数を考えます。現在のx、y、zの値はそれぞれ1、2、0とします。
フォワードプロパゲーションでは、足し算を行い、max関数を適用し、それら二つを掛け合わせることでfの値を得ます。現在の値でこれを実行すると、2と0のmaxは2、1と2の足し算は3、答えは6となります。
続いてバックワードプロパゲーションを実行します。局所勾配を計算すると、da/dx = 1(a = x + y)、da/dy = 1、max関数については、二つのうちどちらが大きいかによって決まります。大きい方は1の勾配、小さい方は0の勾配を持ちます。積については、df/da = bおよびdf/db = aとなります。
これらの局所勾配を使って導関数を計算します。df/df = 1、それを二つの局所勾配a, bに掛け合わせると、2と3になります(数値が入れ替わっているのが特徴です)。max関数については、最大のものは上流勾配×1で3、他方は0になります。足し算については、勾配は両方向に同じように送られるので、両方とも2になります。
これによりdf/dx = 2、df/dy = 3 + 2 = 5、df/dz = 0という結果が得られます。これが正しいか確認しましょう。zをわずかに変更してz = 0.1にしても、計算される関数値はまったく変わりません。そのため、勾配は0です。これは正解です。
xを1から1.1に変更すると、1.1 + 2 = 3.1となり、maxは2のままで、3.1 × 2 = 6.2となります。xの0.1の変化により値が0.2上昇しているので、勾配が2であることに対応します。
最後に、yを2から2.1に変更した場合、2.1 + 1 = 3.1、maxは2.1となり、3.1 × 2.1 = 6.51となります。この0.1の変化で約0.5の上昇があるので、勾配が5であることに対応します。
これは、計算グラフで外向きの分岐がある場合、バックプロパゲーションを実行する際の正しい方法は勾配を合計することを示しています。この例では、yが二つの異なるものに入力されていたので、計算した上流勾配2と3を合計して5を得ました。
計算グラフ内での勾配の動きを考えると、加算操作では勾配が分配され、同じ上流勾配が各入力に送られます。max操作では勾配のルーターのようなもので、一つの入力に勾配を送り、他の入力には何も送りません。乗算では少し変わっていて、フォワード係数を入れ替えるようなことをします。上流勾配に反対のフォワード係数を掛けて下流勾配を得ます。
<userStyle>Normal</userStyle>
4.4. 効率的な勾配計算
関数の値を順伝播で計算し、それを逆に実行して勾配を計算するというこの体系的な方法があります。バックプロパゲーションアルゴリズムの他の主要な点は、これを効率的に行いたいということです。
間違った方法は「よし、ds/db、ds/dw、ds/dx、ds/duを計算したいから、一つずつ計算して、すべてが終わったら止めよう」と言うことです。これは、まずds/dbを計算すると青い部分すべてを計算することになり、次にds/dwに進むと赤い部分すべてを計算することになります。
しかし、数学の部分で見たように、これらの部分はまったく同じなのです。まったく同じ計算を行っているので、この部分を一度だけ計算し、この上流勾配やエラー信号を計算して共有したいのです。
私たちが持ちたい図は、共有部分を一緒に計算し、必要な小さな部分だけを別々に計算するというものです。アルゴリズムとしての一般化は、通常はニューラルネットワークのレイヤーと行列を持っていて、それらをベクトルと行列として表現できるという点です。
厳密に言えば、これは必須ではありません。順伝播と逆伝播のアルゴリズムは、サイクルのない有向非巡回グラフ(DAG)であれば、完全に任意の計算グラフで動作します。一般的なアルゴリズムでは、他の変数に依存する多くの変数があり、各変数がその左側の変数にのみ依存するようにソートする方法があります。これはトポロジカルソートと呼ばれます。
これにより、既に計算された変数に基づいて変数を計算する順伝播の方法が得られます。きれいな行列乗算だけでなく、何か特別な操作を追加することも完全に許可されています。または、完全に接続されていなくてもかまいません。任意の計算グラフを持つことができます。
これにより順伝播が得られ、順伝播を行った後は、出力勾配を1で初期化し、ノードを逆順に訪問します。各ノードでは、上流勾配と局所勾配を使用して下流勾配を計算します。そして計算グラフを逆方向に進み、すべての下流勾配を計算します。
ここで重要なのは、正しく行えば、勾配の計算は順方向計算と同じビッグO複雑性を持つということです。導関数によって異なる関数を持つかもしれませんが、ビッグO項で言えば、逆伝播で順伝播よりも多くの作業を行っているなら、この効率的な計算ができていないことを意味し、一部の作業を再計算していることになります。
5. 自動微分と実装
5.1. 自動微分の概念
ここでの良いアルゴリズムがあるため、バックワードパスを自動的に計算できるはずです。これは自動微分と呼ばれています。もし順伝播で計算しているものの記号形式があれば、「コンピューター、バックワードパスを計算してくれませんか?」と言えばよいのです。数学的には、コンピューターがすべての関数の記号形式を見て、それらの導関数を計算し、すべてを自動的に行うことができるはずです。
初期には、主にモントリオール大学からの先駆的なディープラーニングフレームワークであるTheanoがありました。Theanoはまさにそれを試みました。順伝播計算全体が記号形式で保存され、それが自動的にバックワードパスを計算していました。
しかし、何らかの理由でそれは重すぎる、または異なるものを扱うのが難しい、あるいは単に人々が自分のPythonを書きたいと思ったのか、このアイデアは完全には成功しませんでした。
実際には、現在の主要なフレームワークはすべて、それよりも自動化の度合いが低いものに戻っています。時間を逆行したようですが、ソフトウェアははるかに良くなり、より安定して高速になっています。
現代のディープラーニングフレームワークはすべて、「計算グラフを管理し、順伝播パスと逆伝播パスを実行できるが、局所的な導関数は自分で計算する必要がある」と言っています。レイヤーや活性化関数などの関数をニューラルネットワークに入れる場合、そのPythonクラスで順方向計算と局所勾配が何であるかを教える必要があります。フレームワークはあなたの局所勾配を呼び出し、それが正しいと仮定します。
自動化される部分は、ディープラーニングソフトウェア内部で計算グラフを使って計算し、順伝播と逆伝播があり、以前の図で示したことを行っているということです。順伝播では、グラフのすべてのノードをトポロジカルソートし、各ノードの順伝播関数を呼び出します。これによりトポロジカルソートされているため、既に計算された入力に基づいて局所値を計算できます。
次に逆伝播を実行し、トポロジカルソートを逆転して、上流エラー信号と局所勾配の積として勾配を計算します。
人間が実装する必要があるのは、乗算ゲートやニューラルネットワークレイヤーなど、どんなものでも順伝播パスと逆伝播パスを実装することです。単純な例では、乗算の順伝播は二つの数字を掛け合わせて返すだけです。これを局所ノードに指定します。
もう一つの部分は、これらの勾配を計算することですが、これは今行った例のようなものです。ただし、今持っているものでは、上流勾配を入力として取るbackwardは、どの関数値で計算するかを知らなければ下流勾配を計算できないという問題があります。
標準的なトリックは、すべての人がこのコードを書く方法ですが、順伝播がバックワードの前に計算されるという事実に依存しています。forwardメソッドはクラスのローカル変数に入力の値を保存し、バックワードパスに到達したときに使用できるようにします。そして以前行ったように、dxは上流エラー信号に反対の入力を掛けたものになり、dyも同様です。これで答えが得られます。
<userStyle>Normal</userStyle>
5.2. 現代のディープラーニングフレームワークの設計
現代のディープラーニングフレームワークは、完全な記号的微分から少し後退した設計を採用しています。時間を逆行したようですが、実際にはソフトウェアは大幅に改善され、より安定して高速になっています。
すべての現代的なディープラーニングフレームワークは基本的に「計算グラフを管理し、順伝播パスと逆伝播パスを実行できるが、局所的な導関数は自分で計算する必要がある」というアプローチを取っています。レイヤーや活性化関数などをニューラルネットワークに組み込む場合、そのPythonクラスで順方向計算と局所勾配が何であるかを明示的に定義する必要があります。フレームワークはあなたの局所勾配関数を呼び出し、それが正しいと仮定します。
これにより、手動で行う必要がある部分が少し増えました。実際のコードは明らかに異なりますが、おおよそディープラーニングソフトウェア内部では、計算グラフを使用して計算し、順伝播と逆伝播があり、以前の図で説明したことを行っています。
順伝播については、グラフのすべてのノードをトポロジカルにソートし、各ノードについて順伝播関数を呼び出します。これにより、トポロジカルソートされているため既に計算された入力に基づいて局所値を計算できます。続いて逆伝播を実行し、トポロジカルソートを逆転させて勾配を計算します。この勾配は上流エラー信号と局所勾配の積になります。
人間が実装する必要があるのは、単一のゲート(乗算ゲートなど)やニューラルネットワークレイヤーなど何であれ、順伝播パスと逆伝播パスです。この設計は、理論的にはより多くの手動作業が必要ですが、実際には柔軟性と理解可能性のバランスをうまく取っており、最新のフレームワークが広く採用される要因となっています。
5.3. フォワードパスとバックワードパスの実装
人間が実装する必要があるのは、乗算ゲートのような単一ゲートやニューラルネットワークレイヤーなど何であれ、順伝播パスと逆伝播パスです。例えば、単純な乗算の例では、順伝播パスは二つの数を掛け合わせて結果を返すだけです。これをローカルノードに指定します。
もう一つの部分は勾配を計算することですが、これは既に見た例と同様です。しかし、ここに小さなトリックがあります。今のコードだと、バックワードは上流勾配を入力として取りますが、どの関数値で計算されているかを知らなければ下流勾配を計算できません。
すべての人がこのコードを書く標準的な方法は、順伝播がバックワードの前に計算されることに依存しています。順伝播メソッドはクラスのローカル変数に入力の値を保存し、バックワードパスに到達したときにそれらを利用できるようにします。
これにより、先ほど行ったように計算できます。dxは上流エラー信号に反対の入力を掛けたものになり、dyも同様です。これが答えを与えてくれます。
このアプローチの鍵は、順伝播時に計算された値を保存して、逆伝播時に再利用することです。これにより、順伝播と逆伝播の間で情報が効率的に共有され、必要な計算が最小限に抑えられます。コードでは通常、クラスの初期化で必要なパラメータを設定し、順伝播メソッドで入力値と中間結果を保存し、逆伝播メソッドでそれらを使用して勾配を計算します。
この基本的なパターンは、単純な乗算演算から複雑なニューラルネットワークレイヤーまで、すべてのコンポーネントで繰り返されます。各コンポーネントは順伝播と逆伝播の両方を実装する必要があり、フレームワークはそれらを適切な順序で呼び出して、全体のモデルの勾配を計算します。
5.4. 数値勾配による勾配チェック
バックワード計算のために関数の導関数の数学を正しく理解する必要があります。バックワード計算が正しいかどうかを確認する標準的な方法は、数値勾配による手動勾配チェックを行うことです。
その方法は、例で「1から1.1に変更すると勾配はおおよそどうなるべきか」と確認したときのように、それを自動化された方法で行います。値Xにおいて勾配がどうあるべきかを推定し、その方法は小さなh(魔法の数字はなく、関数によって異なりますが、ニューラルネットワークでは通常10^-4程度が良い)を選び、x+hとx-hでの関数値を計算し、その差を2hで割ります。
これはバックワードパスが計算している勾配の推定値を与えるはずで、これら二つの数値がおおよそ等しい(10^-2程度の範囲内)であれば、おそらく勾配を正しく計算していることになります。もし等しくなければ、おそらく何か間違いを犯しています。
なお、最初の例では単にxとx+hを比較しました。これは一方向の推定で、通常は数学の授業で教えられる方法です。しかし、勾配を数値的にチェックする場合は、この両側推定を行う方が遥かに良いです。これはhの両側で等しく行うとき、はるかに正確で安定するからです。
これは簡単に見えますが、もしこれがそれほど良いなら、なぜ皆がいつもこれを行わず、微積分を忘れないのでしょうか?その理由は、これを行うのが信じられないほど遅いからです。モデルの各パラメータについてこの計算を繰り返す必要があり、バックプロパゲーションアルゴリズムから得られるような種類の高速化が得られないからです。
しかし、実装が正しいかどうかをチェックするには役立ちます。以前、PyTorchのようなフレームワークがなかった時代には、すべてを手書きで行い、人々はしばしば間違えていました。現在ではそれほど必要ではなくなりましたが、独自の新しいレイヤーを実装した場合、それが正しく動作しているかどうかをチェックするのには良い方法です。
5.5. フレームワークの利便性と理解の重要性
ニューラルネットワークとバックプロパゲーションについて知っておく必要があることはこれですべてです。バックプロパゲーションは効率的に適用された連鎖律であり、順伝播は単なる関数の適用、逆伝播は効率的に適用された連鎖律です。
私たちは学生に少し苦痛を与え、これらの計算をいくつか行わせ、宿題をやらせることになります。これは皆さんの中には他の人よりも難しいと感じる人もいるでしょう。
ある意味では、これを実際に知る必要はありません。現代のディープラーニングフレームワークの美しさは、すべてを代わりにやってくれることです。一般的なレイヤータイプを事前に定義し、それらをレゴのピースのように組み合わせるだけで、正しく計算されます。これはまさに、全国および世界中の高校生が科学フェアでディープラーニングプロジェクトを行える理由です。この数学をすべて理解する必要はなく、与えられたものを使うだけでよいのです。
しかし、私たちはあなたが実際にフードの下で何が起こっているのか、ニューラルネットワークがどのように機能するのかについて何か理解することを望んでいます。そのため、少し苦しむことになります。もちろん、より複雑なものを見て理解したい場合は、何が起こっているのかについて何らかの感覚を持つ必要があります。
後で現代のニューラルネットワークに進んだ時、爆発する勾配や消失する勾配などについて少し話します。もし物事がなぜうまくいかないのか、何がうまくいっていないのかを理解したいなら、それがすべてブラックボックスの魔法だと考えるのではなく、実際に何を計算しているのかを知りたいでしょう。そのため、これについて何かを教えることを望んでいます。