AI時代が到来
AIが我々の日常に浸透して当たり前のインフラとなり、早2年が経ちました。
誰もが簡単にウェブからAIを享受できる時代となってとても世の中が便利になりました。
しかし同時に「AIは魔法」のような認識が広まっていっていて、少し恐ろしい時代になったなとも感じています。
今回はAI、とりわけ現代のAIの基礎ともなっているニューラルネットワークの仕組みに触れながら、AIは魔法じゃないんだよ、という事までを見ていこうと思います。
先に注意ですが、この記事ではPyTorchやTensorflowといった実際のフレームワークや現代のAIに利用されているTransformerなどの仕組みを解説するものではありません。
少しAIがわかるようになるための、その原始的な仕組みをゆっくり追っていきます。
そのため、「仕組みなんかどうでもいいんだよ、俺は使えたらそれでいいんだよ」という場合はQiitaには素晴らしい記事がたくさんあるので、そちらをご覧ください。
ニューラルネットワーク
ニューラルネットワークって言葉は聞いたことがある人も多いでしょう。
元々は人間の脳の働きを模倣したものとして考案されました。
人間の脳は多くがまだ解明されていませんが、多数のニューロンと呼ばれる神経細胞が相互に影響しあって信号を受け渡しているとされています。
この処理によって人間の脳はさまざまな能力を得ているわけです。
ここに注目してこの仕組みをコンピュータでシュミレートすること高い判断能力を与えようとしたものです。
歴史は1950年代に遡り、アメリカの心理学者のフランク・ローゼンバルトが単純パーセプトロンを提唱したのが起源とされています。
それ以前に外科医のウォーレン・マカロックと数学者のウォルター・ピッツによって形式ニューロンというものが提案され、これがパーセプトロンの基であるとも言われますが、こちらは興味があったら調べてみてください
これがいくつもの冬の時代を迎えながら今日の陽の目を浴びる存在になっていきました。実は内部の数式自体は時代によって進化しましたが、この内部の構成自体は大きく変わっていないのです。
人工ニューロン
先ほどのニューラルネットワークの画像の丸の中身を少し覗いてみましょう。これこそが人間の脳内のニューロンに対応する「人工ニューロン」と呼ばれるものです。
線形結合
人工ニューロンでは
$$
y = X \cdot W + b
$$
という式が最初に入力Xに対して行われます。
ここは線形結合と呼ばれる処理を行います。そもそも、この式ってなんかみたことありませんか?
中学校くらいで皆さん、一次関数というものをやったと思います。
$$
y = ax + b
$$
ここで行う線形結合というのは、このaとxが任意の個数ある式なんです。つまり、
$$
y = a_1x_1 + a_2x_2 + \cdots + a_nx_n + b
$$
がこの最初の線型結合の中身です。
実際の計算のお話(すでにディープラーニングの仕組みの知識が少しある人向け)
実際にはXは以下のように行列で渡されることがほとんどです。
$$
X = \left(
\begin{array}{cccc}
x_{11} & x_{12} & \cdots & x_{1i} & \cdots & x_{1n} \\
x_{21} & x_{22} & \cdots & x_{2i} & \cdots & x_{2n} \\
\vdots & \vdots & & \vdots & & \vdots \\
x_{m1} & x_{m2} & \cdots & x_{mi} & \cdots & x_{mn}
\end{array}
\right
)
$$
このケースではXは$m×n$の行列となります。これはつまり、n次元のデータを持つmレコードのデータということになります。
この場合、Wは$n×o(oは任意の数)$となります。これはo個のニューロンが存在するという意味になります。
また、bは通常$o×1$のベクトルとなります。o個のニューロンにb(バイアス)が一つずつ存在しているイメージです。
この計算式で
$$
y = \left(
\begin{array}{cccc}
\sum_{l=1}^{n} x_{1l}w_{l1}+b_1 & \sum_{l=1}^{n} x_{1l}w_{l2}+b_2 & \cdots & \sum_{l=1}^n x_{1l}w_{li}+b_i & \cdots & \sum_{l=1}^{n} x_{1l}w_{lo}+b_o \\
\sum_{l=1}^{n} x_{2l}w_{l1}+b_1 & \sum_{l=1}^{n} x_{2l}w_{l2}+b_2 & \cdots & \sum_{l=1}^n x_{2l}w_{li}+b_i & \cdots & \sum_{l=1}^{n} x_{2l}w_{lo}+b_o \\
\vdots & \vdots & \ddots & \vdots & \ddots & \vdots \\
\sum_{l=1}^{n} x_{ml}w_{l1}+b_1 & \sum_{l=1}^{n} x_{ml}w_{l2}+b_2 & \cdots & \sum_{l=1}^n x_{ml}w_{li}+b_i & \cdots & \sum_{l=1}^{n} x_{ml}w_{lo}+b_o
\end{array}
\right
)
$$
このy行列の一つの要素$\sum_{l=1}^n x_{1l}w_{l1}+b1$というものが本編内で記載していた$y = a_1x_1 + a_2x_2 + \cdots + a_nx_n$に当たる計算です。
つまり、ニューロン単位の計算を抽出したものであるということです。
さてさて、ここで一部の人からツッコミが入っている頃でしょう。
「線形結合ってなんだよ、どういうことだよ!」と。
ここは難しく考えずに「入力のデータをデータを持たせたまま別の形に変える」処理だと思ってください。
今回のデータというのはn個の$x$でしたね。しかし、n個の$x$そのものだと扱いづらいですよね。
だから、このデータを$a_i (1\le i \le n)$というものを掛けて足し合わせることで$y$という値に変換しているイメージです。
誤解を避けるためのお話
正確にはここはベクトルとベクトルの内積を利用して線形写像を求めています。
写像とは何か、というところですが、写像はある二つの集合において、一つの集合の各元に対して、もう一つの集合のただ一つの元と結びつけることを言います。
言い換えれば、ある集合のある要素を表す別の集合の要素をマッピングするということです。もっと言い換えて仕舞えば、ある集合をそのまま別の集合における見え方を決めるということです。
これは情報の見方を変えて圧縮したり、拡大しているわけです。
情報の見え方を変えただけであれば、それが持つデータそのものの本質は変わらないので変換した値も元の情報の特徴を捉えていると考えることができるのです。
現実世界で考えると、例えば人は3次元にいますが、その影は2次元であるわけですよね。
これは3次元を2次元に投影した写像と考えられます。
しかし多くの人はその影を見て、それが人の影であることはわかりますし、体格やある程度の輪郭もわかるわけです。
ただし影からは顔や服の色はわかりませんね。とはいえ、それが人であるかどうか、ということを考えるには十分なわけです。情報はある程度圧縮されても重要な情報、必要な特徴は捉えているのです。
そして、ベクトルの空間では線形性を用いて写像を作ります。
これを線形写像とよび、定義は
$$
f: V\to W
$$
この時、$x,y \in V$とスカラー$a,b$に対して
$$
f(ax+by) = af(x)+bf(y)
$$
となります。
ちなみに、ベクトル空間の内積が線形写像である証明としては以下のようになります。
ベクトル$v\in\mathbb R^n$を固定して
$$
f_v(u) = u \cdot v = \sum_{i=1}^nu_iv_i
$$
という写像を考えます。これは
$$
f_v(au+bw) = (au + bw) \cdot v = a(u \cdot v) + b(w \cdot v) = af_v(u) + bf_v(w)
$$
を満たすため、$\mathbb R^n \to \mathbb R$の線形写像です。
ここでポイントは写像の先(像)となる「別のベクトル空間」の$W$はどんなものでも良いのです。
スカラー空間も明確に「1次元のベクトル空間」ですから、線形写像の像先としても問題ないわけです。
従って、内積計算によって別のベクトル空間の1次元空間へ線形写像を出力しますが、あくまで見え方が変わっているだけなので、このスカラーは元のデータの本質的な特徴は依然として持っているため、この処理でむしろデータの必要なデータを抽出しながらデータの本質をコンピュータが理解することを助けるのです。
これによって、yはただの数値になりますがこれ自体が入力の特徴を一部捉えているデータとして扱えるのです。
n個の$x$と1個の$y$どちらが扱いやすいかは直感的にわかりますね。
この特徴を捉えたデータという部分は非常に重要なので忘れないでください。
活性化関数 (Activation function)
ここまでで線形結合でデータを使いやすくすることに成功しました。
ただこれをそのまま利用するとAIは非常に頭の悪いものになってしまいます。
先ほどこの線形結合は一次関数に集約できると話しましたね。
つまり、この線形結合の結果は入力がどれだけあろうと最終的にはその空間における直線になってしまいます。
しかし世の中には直線で綺麗に分離できたり、綺麗に比例するものなんて少ないですよね。
例えば、薬は私たちの病気を治してくれる素晴らしいものですが、たくさん飲めば治りやすくなるものではありません。
確かに少なすぎれば効き目が悪いですし、ある一定の量までは増やせば増やすほど治りやすくなるでしょう。ところが、ある基準を超えると今度は効き目が変わらないどころか重篤な副作用を示したり、毒性を示してしまって逆に健康状態を悪化させてしまうでしょう。
これではAIは世の中の複雑なタスクに適合することができません。
非常に困った状況になりますね。ここで登場するのが活性化関数です。
これはどんな関数か、というと一言で言うと一直線で書くことのできない関数です。
直線でかける関数を線形関数というのに対をなす形で、この一直線で書けない関数を非線形関数と呼びます。
例えば以下のような関数があります。
この関数は以下のように定義されます。
$$
y = \frac{1}{1 + e^{-x}}
$$
これを通すことでコンピュータは思考を曲げることを覚えます。
比例関係が成り立たない状況を知ることで世の中の複雑な事象を捉えることができるようになるわけです。
非線形性のお話
活性化関数は実際にはニューロンという一つの大きな関数と見たときにこれに非線形性を導入する役割を持っています。
したがって、一部を除いて世の中に存在する活性化関数は全て非線形関数となっています。
これがなぜ重要なのかは本文でも説明したとおり、非線形性がないと表現力が極端に下がってしまうためです。
線形の状況しか判断できないというのはつまり、直線上に存在する値を利用する、もしくは直線で分離できるものしか分離できないということになります。
ただし、これで分離できるのであればニューラルネットワークをわざわざ使う必要もなく、線形回帰モデルを使えば良いわけです。
線形モデルの代表である多項式回帰やロジスティック回帰の内部実装は先ほどの線形結合です。各モデルについては以下のようになっています。
- 多項式回帰
線形結合の結果をそのまま(もしくはスケール処理だけを行い)出力するモデル - ロジスティック回帰
線形結合の結果をシグモイド関数という関数(本編で活性化関数の例として出してる関数)を通してその結果をそのまま予測値として扱うモデル
です。
ここで一部の人は「ん?」と感じたかもしれませんね。
ロジスティック回帰では先ほどの活性化関数を通しているのに「線形」なのか?と。
これについてはシグモイド関数をどちらも通しているのだから非線形に見える人もいるでしょう。
事実として出力そのもののプロットは、ロジスティック回帰は本編の図のように非線形になります。
ただし、ここで注意して欲しいのはニューラルネットワークの活性化関数とロジスティック回帰のシグモイド関数は意味合いが全く違うということです。
ロジスティック回帰の本質は「線形結合」です。シグモイド関数はあくまでもこの結果を二値分類としてイメージしやすくするために利用します。
先ほどのグラフの通り、シグモイド関数は入力が小さいほど0に近づき、入力が大きいほど1に近づく、単調増加の関数です。
入力前と入力後で値の大小関係が変わらない(元が大きければ大きくなるし、小さければ小さくなる)上に$0.0 \sim 1.0$という範囲を取るため、ある入力が正である(機械学習では二値の分類を正例/負例と一般に呼びます)可能性を表すのちょうど良かったのです。
要するに人の理解のための関数なのでここの非線形性は本質的にモデル自体の結果に影響しないわけです。
一方でニューラルネットワークでは活性化関数の目的が「非線形性の導入」であり、最初のニューラルネットワークのイメージのようにニューロンの出力は次の層のニューロンの入力として扱われるわけです。
まず、ここで先に活性化関数がない場合の値が線形というのが最初のニューラルネットワークの図のように層を形成していたとしても変わらないのかを証明していきます。
ここでは行列演算を利用してみていきます。
$$
X = \left(
\begin{array}{ccccc}
x_{11} & x_{12} & x_{13} \\
x_{21} & x_{22} & x_{23} \\
x_{31} & x_{32} & x_{33} \\
\end{array}
\right
)
$$
$$
W_1 = \left(
\begin{array}{ccccc}
w^1_{11} & w^1_{12} \\
w^1_{21} & w^1_{22} \\
w^1_{31} & w^1_{32}
\end{array}
\right
)
$$
$$
W_2 = \left(
\begin{array}{ccccc}
w^2_{11} \\
w^2_{21}
\end{array}
\right
)
$$
という行列を考えます。なお、本来はバイアス項が存在しますが、今回は演算のシンプル化のため省略しています。
仮にバイアス項があっても結論は同じなので興味があればぜひご自身で計算してみてください。
第一段階
まず、$X$と$W_1$の行列積を求めます。
$$
Y = X \cdot W_1 = \left(
\begin{array}{ccc}
(x_{11}w^1_{11} + x_{12}w^1_{21} + x_{13}w^1_{31}) & (x_{11}w^1_{12} + x_{12}w^1_{22} + x_{13}w^1_{32}) \\
(x_{21}w^1_{11} + x_{22}w^1_{21} + x_{23}w^1_{31}) & (x_{21}w^1_{12} + x_{22}w^1_{22} + x_{23}w^1_{32}) \\
(x_{31}w^1_{11} + x_{32}w^1_{21} + x_{33}w^1_{31}) & (x_{31}w^1_{12} + x_{32}w^1_{22} + x_{33}w^1_{32})
\end{array}
\right
)
$$
第二段階
次にこの結果の$Y$と$W_2$の行列積を求めます。
$$
Y \cdot W_2 = \left(
\begin{array}{c}
(x_{11}w^1_{11} + x_{12}w^1_{21} + x_{13}w^1_{31})w^2_{11} + (x_{11}w^1_{12} + x_{12}w^1_{22} + x_{13}w^1_{32})w^2_{21} \\
(x_{21}w^1_{11} + x_{22}w^1_{21} + x_{23}w^1_{31})w^2_{11} + (x_{21}w^1_{12} + x_{22}w^1_{22} + x_{23}w^1_{32})w^2_{21} \\
(x_{31}w^1_{11} + x_{32}w^1_{21} + x_{33}w^1_{31})w^2_{11} + (x_{31}w^1_{12} + x_{32}w^1_{22} + x_{33}w^1_{32})w^2_{21}
\end{array}
\right
)
$$
ここで、要素の計算を詳細にしてみましょう。
$$
\begin{align}
&(x_{11}w^1_{11} + x_{12}w^1_{21} + x_{13}w^1_{31})w^2_{11} + (x_{11}w^1_{12} + x_{12}w^1_{22} + x_{13}w^1_{32})w^2_{21} \\
&= x_{11}w^1_{11}w^2_{11} + x_{12}w^1_{21}w^2_{11} + x_{13}w^1_{31}w^2_{11} + x_{11}w^1_{12}w^2_{21} + x_{12}w^1_{22}w^2_{21} + x_{13}w^1_{32}w^2_{21} \\
&= (w^1_{11}w^2_{11} + w^1_{12}w^2_{21})x_{11} + (w^1_{21}w^2_{11} + w^1_{22}w^2_{21})x_{12} + (w^1_{31}w^2_{11} + w^1_{32}w^2_{21})x_{13}
\end{align}
$$
ここで$w^1$と$w^2$は定数であるため、$(w^1_{11}w^2_{11} + w^1_{12}w^2_{21})$も定数であり、結果的にこの最後の結果は
$$
W_{s} = \left(
\begin{array}{c}
w^1_{11}w^2_{11} + w^1_{12}w^2_{21} \\
w^1_{21}w^2_{11} + w^1_{22}w^2_{21} \\
w^1_{31}w^2_{11} + w^1_{32}w^2_{21}
\end{array}
\right
)
$$
という行列(ベクトル)と一度行列積を求めたことと全く同じことになってしまうのです。
そしてこの行列積は先に説明した通り、線形結合であり線形写像を作るため、結局は線形関数であるわけです。
これは仮に1万層、100万層まで増やして計算しても全く変わらず元の値に対して一度行列演算を行うだけで実現できる線形関数に帰着してしまいます。
ニューラルネットワークは層を増やすと計算量が一気に増えます。しかし、その結果が線形に帰着してしまったらわざわざ高い計算コストを払う意味がわからないですよね。
そこで活性化関数というものが各線形結合の結果に適用してから次のニューロンへ値を投げるわけです。
展開などは非線形が入ると非常に複雑になってしまうので行いませんが(というか無理)、イメージのために$Y \cdot W_2$を書いてみましょう。
$$
Y \cdot W_2 = \left(
\begin{array}{c}
\frac{1}{1 + e^{(\frac{1}{1+e^{(x_{11}w^1_{11} + x_{12}w^1_{21} + x_{13}w^1_{31})w^2_{11} + (x_{11}w^1_{12} + x_{12}w^1_{22} + x_{13}w^1_{32})}}w^2_{21})}} \\
\frac{1}{1 + e^{(\frac{1}{1+e^{(x_{21}w^1_{11} + x_{22}w^1_{21} + x_{23}w^1_{31})w^2_{11} + (x_{21}w^1_{12} + x_{22}w^1_{22} + x_{23}w^1_{32})}}w^2_{21})}} \\
\frac{1}{1 + e^{(\frac{1}{1+e^{(x_{31}w^1_{11} + x_{32}w^1_{21} + x_{33}w^1_{31})w^2_{11} + (x_{31}w^1_{12} + x_{32}w^1_{22} + x_{33}w^1_{32})}}w^2_{21})}}
\end{array}
\right
)
$$
LaTeXの表示が崩れるくらいには複雑な計算式になります。
もし猛者がいるならこれをそれぞれ$x$についてまとめて、線形結合にできるかを確認してみてもいいかもしれません。
私は絶対にやりたくないですが。
こんな感じで先ほどまでシンプルに書き換えられたたった3つのニューロンの計算は非線形関数である活性化関数が入るだけでここまで一気に複雑になるわけです。
この複雑さが非常に豊かな表現力を生むのですが(これは前述の写像の話に近いですが、式が複雑になるということは取れる値も増えるため、結果的に表現空間が大きくなる)、逆に人間の理解できる範疇を超えてしまうのでニューラルネットワークの出力はブラックボックスになってしまうというマイナスもあります。
ニューラルネットワークで出力に入力がどれだけ影響しているか、というのを測るための手法も現在は存在していますが、それはまた別のお話なので機会があればまたお話しましょう。
余談ですが、活性化関数は人間の脳内のニューロンが樹状突起と呼ばれる前のニューロンからの信号を受け取る器官からの入力信号を複数受け取って、細胞体という部分で演算を行い、細胞体が一定の閾値で活性化(興奮状態)すると信号を軸索を通じて次のニューロンに流すとされているところから、この活性化状態を擬似的数学的に再現する関数として導入されたことから活性化関数と呼ばれているそうです。
まとめ
ここまででニューラルネットワークの最小単位とも言える人工ニューロンの中身についてみてきました。
人工ニューロンは受け取った入力に対して線形結合を行って扱いやすい形にデータを圧縮して、直線では表現できない部分に対応するために活性化関数という非線形の関数を通して、その結果を出力するものでした。
これによって世の中の複雑な事象にも対応した処理が実現できるのでしたね。
これで人工ニューロンについての話は終わりです。
どうでしょうか?複雑なAIも実際の中身を最小単位で見ると案外大したことなく感じるのではないでしょうか?
ニューラルネットワークと人工ニューロン
ここからは人工ニューロンを相互に接続したニューラルネットワークをみていきます。
とはいえ、人工ニューロンを知ってしまえばニューラルネットワークに難しいことは何もありません。
単体の人工ニューロンの限界
実際に単体のニューロンを使っての学習というのも行うことは可能です。
ただし複雑な事象を学習するためには単体のニューロンではあまりに非力です。
このケースでの単体のニューロンが何を指すか、を定義しておきましょう。
最初のニューラルネットワークの図における、中間の層つまり、以下の画像の赤の囲いの中が1つのニューロンであることを考えます。
このケースでは単体の人工ニューロンの出力は活性化関数を持っていても入力の線形結合の結果の大小関係おおよそに一致してしまうのです。
これは活性化関数が多少の例外はあれど、多くのケースで単調増加な性質を持つためです。つまり、元の値が大きければ出力値も大きく、逆に入力が小さければ小さくなってしまいます。
結局のところ、最終的な判断は出力が大きいのか、小さいのか、といった形でそれがYesなのかNoなのかを分ける形になります。言い換えると、最終的な出力の大きさがその値であるかどうかを示す確率になるのです。
少し難しいですね、例を挙げて考えてみましょう。
あなたは熱中症になるかならないか、という予測を行いたいとします。
この時直感的にあなたは [その日の気温、その日の湿度、最後の水分補給からの時間] が関係していると考えたとします。
これで 熱中症になる or ならない を判断していきます。
つまり、その日の気温を$x_1$、その日の湿度を$x_2$、最後の水分補給からの時間を$x_3$とすると単体のニューロンは
$$
y = x_1w_1 + x_2w_2 + x_3w_3 + b
$$
となって
$$
output = \frac{1}{1+e^{-y}}
$$
となるわけです。
この出力は線形結合の結果である$y$が大きいとoutputも大きい値を返します。
そして、この情報しかないので最終的な結果もこの結果に依存するので、
$$
outputが大きい = 熱中症になる可能性が高い
$$
となってしまうわけです。これは線形結合に強く依存するので非線形関数の表現力をうまく発揮できないわけです。
ネットワーク化
ニューラルネットワークの高い表現力を活かすには非線形関数を持つニューロンを重ねることが非常に重要です。
先ほどの単体のニューロンでは単体の結果だけを扱いましたが、実際のニューラルネットワークでは以下のようにニューロンが一つの層に複数のニューロンが含まれます。
さらにこの層が重なりあってネットワークのようにつながりあっています。
ここで重要な点として、層のすべてのニューロンが一つ前の層のすべてのニューロンと一つ後の層のすべてのニューロンと繋がっています。
ただし、同じ層のニューロン同士は繋がっていません。
ニューラルネットワークのある層への入力は一つ前の層のすべてのニューロンの出力になります。
上のイメージで言えば、赤で囲った層のすべてのニューロンに一つ前の層の5個のニューロンの結果が等しく流れます。
ただし、先ほど話したように同じ層のニューロン同士は繋がっていないのでそれぞれが独立したパラメータと呼ばれるニューロンの説明で出てきた$a_1, a_2, a_3, a_4, a_5$を持っています。
それぞれのニューロンにおいて、このパラメータと入力の線形結合を計算すると、独立したニューロン同士のパラメータなので当然お互いに全く異なる出力が発生します。これに対して活性化関数を通すことで値が非線形に変換されます。
そして、この層のすべてのニューロンは次の層のすべてのニューロンに等しく結果を流すため、次の層ではこの6個のニューロンからの出力を入力として扱うのです。
これを繰り返していくと、非線形の関数が多重に重なって非常に曲がりくねった値を出力するようになっていきます。
これによって、複雑な表現を獲得することができるようになるのです。
重ね合わせることによる表現のお話
ニューラルネットワークでは入力に近い、イメージの左側を上流と呼び、逆に出力に近い方を下流と呼びます。
重ね合わせによる表現力の爆発は線形写像による特徴抽出と非線形関数による非線形変換を多重に掛け合わせた結果によるものです。
入力層から最上流の層にデータが入力されると、第一層は入力を線形結合して写像を作ります。
この時、その層の中ではお互いのニューロンは接続していないため独立して特徴を抽出します。
一つの層の中で入力に対する様々な切り口の特徴を抽出して、これを非線形変換を行った上で次の層に出力を投げます。
次の層では一つ前の層の出力をさらにそれぞれのニューロンで線形結合をするため、前の層で様々な観点から抽出した特徴を元にさらに別の切り口から特徴を抽出します。これをさらに非線形変換を行います。
これを繰り返すと、データの特徴抽出と非線形情報を畳み込む形になるため、ネットワーク全体を関数としてみた時には非常に曲がりくねったような関数を示すようになるわけです。
これによって線形関数が動ける空間を遥かに超越する結果空間を持つようになるのです。
ちょっと分だけだと分かりにくいのでイメージを張ってみましょう。
線形の関数では以下のように取れる値は赤の線の上となります。
一方でネットワークのイメージとして複雑な非線形の関数は以下のような領域を取ることができます。
あくまでイメージなので実際のネットワークの出力ではありませんが実際はもっと複雑になるでしょう。どちらの方が広い出力値の空間を持つかは明らかではないでしょうか。
これこそがニューラルネットワークが複雑な空間を持って複雑な問題に対応できる理由です。
取れる値の空間が大きいということはその分だけ表現できる範囲も広がるのです。
層のニューロンたちのお話
実はすでにこれより前の折りたたみの中で計算しているのですが、ニューロンの数は$W$の重みの形を見ることで判断ができます。
本質的にニューロンは線形結合と活性化関数である、という話をしましたが、活性化関数は単に関数で値を変換するだけなので、実装上は線形結合をどう行うかを考えることが大切です。
「実際の計算のお話(すでにディープラーニングの仕組みの知識が少しある人向け)」でも出ていた以下の計算が実はこの答えの一つになっています。
$$
y = \left(
\begin{array}{cccc}
\sum_{l=1}^{n} x_{1l}w_{l1}+b_1 & \sum_{l=1}^{n} x_{1l}w_{l2}+b_2 & \cdots & \sum_{l=1}^n x_{1l}w_{li}+b_i & \cdots & \sum_{l=1}^{n} x_{1l}w_{lo}+b_o \\
\sum_{l=1}^{n} x_{2l}w_{l1}+b_1 & \sum_{l=1}^{n} x_{2l}w_{l2}+b_2 & \cdots & \sum_{l=1}^n x_{2l}w_{li}+b_i & \cdots & \sum_{l=1}^{n} x_{2l}w_{lo}+b_o \\
\vdots & \vdots & \ddots & \vdots & \ddots & \vdots \\
\sum_{l=1}^{n} x_{ml}w_{l1}+b_1 & \sum_{l=1}^{n} x_{ml}w_{l2}+b_2 & \cdots & \sum_{l=1}^n x_{ml}w_{li}+b_i & \cdots & \sum_{l=1}^{n} x_{ml}w_{lo}+b_o
\end{array}
\right
)
$$
ここで、計算は$n \times o$の重み$W$と$m \times n$の入力$X$の行列積を求めていました。(バイアスは一旦無視してください。)
この時、この層のニューロン数は$o$となります。$W$の列数がニューロンの数に対応するのです。
元々一つのニューロンで求めたいのは$\sum_{i=1}^{n}x_{ki}w_{ik}$(線形結合)なので行列ではこれが行列積で簡単にもとまります。
そして、$X$のあるレコードは$W$の各列との内積を求めていくため、$W$の列数分だけ線型結合が出力できます。
最終的な出力は $m \times o$となり、本編で話した通り次の層への入力の数は今の層のニューロン数に一致するため、$o$個の入力を与えられるため、この層のニューロンは$o$個と見なすことができるのです。
ニューロンを一つ一つ考えて実装するのは少々難しいですが、これを用いることで任意個のニューロン数を簡単に実装することができるのです。
普遍性定理のお話
このニューラルネットワークにおいて、普遍性定理と呼ばれる定理が確立されています。 この定理は「十分なニューロン数の一層の隠れ層を持つニューラルネットワークは連続関数に近似することができる」というものです。 つまり、単体のニューロンではダメでしたが、一つの層だけでも十分なニューロンを持っていれば層が深くなくても様々な非線形の関数に近似できるため、適切なモデルを作ることが可能であるということです。 とはいえ、この理論は細かいところを見ていくと非常に複雑になってしまうのでここはQiitaのわかりやすい以下の記事をご参照ください。とても素晴らしい記事です。https://qiita.com/mochimochidog/items/ca04bf3df7071041561a
https://qiita.com/cfiken/items/e2047d9b27e6ef2a132b
まとめ
ニューラルネットワークはニューロンを多数相互に繋げてネットワークのようになっているものでした。
単体のニューロンでは十分な表現力が持てず、これは出力に一つの線形結合の大小が大きく影響するためのものです。
ニューロンの非線形な関数を大きく活用するには層を深くして、また各層にもいくつかのニューロンを置くことが大切でした。
これでニューラルネットワーク自体の仕組みの話は終わりです。
学習
ニューラルネットワークの計算の仕組みと高い表現力を持つ理由はここまでで説明してきました。
しかし、いかに高い表現力を持とうとその結果が正確でなければ何の意味もありません。
例えば病気を判定できるモデルがさまざまなデータを元にその人が癌であるかを判定する際に健康な人に対して「あなたは癌の確率が91.2%です」としたり、逆に早期の癌の人に「あなたは癌の確率が0.1%です」としたら何の意味もないモデルですよね。
そこで重要になる概念が「学習」です。
学習とはモデル(ここではニューラルネットワーク)が出力する結果を正確にする教育を行うことです。
先ほどの例で言えば、「こういう特徴がある人は癌である可能性が高いよ」とモデルに対して教えてあげることを指します。
AIの学習は山下り
AIの学習は地図もなく明かりもなく一寸先も見えない状況で山を下るようなものです。
これはよく言われるニューラルネットワークに限らない機械学習の学習の仕組みを語ったものなのですが、実際まさにこの通りなんです。
そもそも「モデルが正確」とは何でしょうか?
モデルが正確というのは正しい結果を返すことですね、そりゃそうですが。
そして、正しい結果を返すというのはすなわち間違いが少ないということになります。
AIの学習においてこの「間違いが少ない」というのは非常に重要なので頭のどっか片隅においておいてください。
ではどのように間違いが少ないかを確認すればいいのでしょうか。ここは少しシンプルに実例で見てみましょう。
正答 | 予測(AI) |
---|---|
3000 | 1900 |
1280 | 2000 |
4200 | 3500 |
5100 | 4900 |
のようにな正解と予測があった考えます。この結果が良いか、悪いか、それは人によると思うので感覚にお任せします、がAIの学習では「人によるんですよね」ではダメですよね。
感覚に左右されない指標が必須です。そこで、ここでは間違いを数式を用いて指標を作ります。
この数式は誤りを損失として扱う関数なので損失関数と呼ばれています。
以下は損失関数の一つです。
y = 正答, t = 予測として、
$$
loss = \frac{1}{n}\sum_{i=1}^{n}(t_{i} - x_{i})^2
$$
例えば、上記で計算すると
$$
\begin{align}
loss &= \frac{1}{4}\lbrace{(3000 - 1900)^2 + (1280 - 2000)^2 + (4200 - 3500)^2 + (5100 - 4900)^2\rbrace} \\
&= \frac{1}{4}\lbrace{1100^2 + (-720)^2 + 700^2 + 200^2\rbrace} \\
&= \frac{1}{4}(1210000 + 518400 + 490000 + 40000) = 564600
\end{align}
$$
となります。
要するに損失が$564600$であると考えられますね。これを小さくする方向にしていくことを学習と言います。
例えば、損失の大きさを以下のように考えてみましょう。
この例では赤い線の$y$軸が損失だと考えます。
水色の丸が現在の損失です。学習とはこの水色のボールを損失が少ない部分に持っていくことなんです。
あなたはこの関数の前提も地図やコンパス、等高線図といったものは一切手元にないとします。
さらに周りも真っ暗で見ることができません。
あなたがこの水色の部分にいるとして、どうやって損失を低くしますか?
簡単に考えてしまって大丈夫です。
足の感覚で道を決めるんです。もっとも降れそうな方向を探してそっちに進んでいきます。
上記の関数の例では左下とシンプルですが、実際には$x,y$軸に限らず、例えば山を下る場合は当然$z$軸も含めて3次元の方向で動きますよね。
自分がいる場所から四方八方方向は開かれているのでいろんな方向に足を伸ばしてもっとも下れる坂の方向を選んで進む決定をすることで、まぁ少なくとも効率的に降れそうですよね。
この学習方法を最も急な方向に下がっていく方式から最急降下法という名前で呼ばれています。
これによって、損失を減らすことで最初に行った「モデルが正確」という状況を作ることができるのです。
最急降下法の詳しいお話
ニューラルネットワークは先ほどのニューロンを組み合わせていますが、その中の処理は全て数学的な関数でしたね。
つまり、ニューラルネットワーク全体を巨大な複雑な一つの関数として考えることができるんです。
これに損失関数を付け加えても巨大な関数であることは変わりません。
本編で話していますが、この関数の結果が小さくなる方向にパラメータの調整を進めていくのです。
さて、パラメータってどこでしょうか。
入力は変わりませんし、活性化関数の定義そのものも変わりませんね(一部活性化関数には学習可能パラメータも存在するものがあります)。
つまり、ニューロンで見てきた$W$や$b$が調整されるパラメータになります。
最急降下法では傾きを元に下っていくことで損失を減らしていました。関数の傾きって何だったか覚えていますか?
そうですね、ここで出てくるのが微分です。特にこのケースでは各パラメータのそれぞれの傾きを知りたいですよね。
ある一つの変数に基づいての傾きを求める偏微分を活用します。
例えば
$$
z = ax^2 + by + c
$$
という関数があった場合、偏微分では$x$に着目すると$2ax$, $y$に着目すると$b$がそれぞれの偏導関数となりますね。
さて、では前回も出てきた関数を再度見てみましょう。
$$
Y \cdot W_2 = \left(
\begin{array}{c}
\frac{1}{1 + e^{(\frac{1}{1+e^{(x_{11}w^1_{11} + x_{12}w^1_{21} + x_{13}w^1_{31})w^2_{11} + (x_{11}w^1_{12} + x_{12}w^1_{22} + x_{13}w^1_{32})}}w^2_{21})}} \\
\frac{1}{1 + e^{(\frac{1}{1+e^{(x_{21}w^1_{11} + x_{22}w^1_{21} + x_{23}w^1_{31})w^2_{11} + (x_{21}w^1_{12} + x_{22}w^1_{22} + x_{23}w^1_{32})}}w^2_{21})}} \\
\frac{1}{1 + e^{(\frac{1}{1+e^{(x_{31}w^1_{11} + x_{32}w^1_{21} + x_{33}w^1_{31})w^2_{11} + (x_{31}w^1_{12} + x_{32}w^1_{22} + x_{33}w^1_{32})}}w^2_{21})}}
\end{array}
\right
)
$$
さぁ、では$w_{11}^{1}$の偏微分を求めてください!
正直これをみてすぐに答えられる人は人間の知能を超えた化け物か、AIかでしょう、それこそ。
まぁ普通にこれを解くのは、まぁ僕はやりたくないです。そもそも解析的な導関数を求めてもこの関数特化になってしまうので汎用性も低くなります。
連鎖率
じゃあどうするのか、ここで大切になるのが連鎖律(Chain Rule) です。
例えば、ある関数$f(x)$が$g(t), h(x)$を用いて$g(h(x))$と表せられる場合に以下の微分(導関数)を考えます。
$$
\frac{dy}{dx} = g(h(x))'
$$
少し具体的にみてみましょう。
$$
g(t) = t^7
$$
$$
t = h(x) = \frac{1}{1 + e^{-x}}
$$
として、
$$
\frac{dy}{dx} = g(h(x))' = \lbrace{(\frac{1}{1 + e^{-x}})^7\rbrace}'
$$
これはパッとみて解ける気がしないですね。ここで微分の連鎖率
の出番なのです。
合成関数の定義から、中間値として$t$を考えます。すると、$\frac{dy}{dx}$は以下のように$t$の$x$に対する微分と$y$の$t$に対する微分に分けられます。
$$
\frac{dt}{dx} = h'(x)
$$
$$
\frac{dy}{dt} = g'(t)
$$
つまり、$\frac{dy}{dx}$は以下のように表現できます。
$$
\frac{dy}{dx} = \frac{dy}{dt} \cdot \frac{dt}{dx} = g'(t) \times h'(x)
$$
ここで$t$は$h(x)$ですがg'(t)ではあくまで$t = h(x)$は定数なので中の値が何であるかを気にする必要が一切ありません。
少し分かりづらいと思うので、実際に先ほどのシグモイド関数の7乗の導関数を求めてみましょう。
$$
\begin{align}
&h(x) = \frac{1}{1 + e^{-x}}より\\
&h'(x) = \frac{1}{1 + e^{-x}}\lbrace1 - \frac{1}{1 + e^{-x}}\rbrace = sigmoid(x) \cdot (1 - sigmoid(x))
\end{align}
$$
また
$$
\begin{align}
&g(t) = t^7より \\
&g'(t) = 7t^6
\end{align}
$$
したがって、
$$
f'(x) = g'(t) \cdot h'(x) = 7t^6 \cdot sigmoid(x) \cdot (1 - sigmoid(x)) \\
$$
ここで$t = sigmoid(x)$なので
$$
f'(x) = 7sigmoid^6(x) \cdot sigmoid(x) \cdot (1 - sigmoid(x))
$$
どこまでまとめるか、という点はありますが、これより
$$
f'(x) = 7sigmoid^7(x) \cdot (1 - sigmoid(x))
$$
となってシグモイド関数の7乗の導関数を求められました。
ここで大切なのは異なる関数の合成関数はそれぞれの関数の導関数の掛け算で求められるということです。
つまり、$x$が明確な値のケースでその微分値も同様に掛け算で求められるのです。
そして、先ほど話したように上位の関数(今回であれば$g(t)$)は下位の関数がどんな計算でも良いということです。
例えば、$i(x) = x^2$として
$$
t = h(i(x)) = sigmoid(x^2)
$$
の場合であっても
$$
g'(t) = 7t^6
$$
は変わらないので、$t$に$sigmoid(x^2)$を代入して、$t$の微分値
$$
\begin{align}
t' = h'(i(x)) &= h'(i(x)) \cdot i'(x) \\
&= sigmoid(x^2) \cdot (1 - sigmoid(x^2)) \cdot 2x
\end{align}
$$
となって、$t = sigmoid(x^2)$なので
$$
\begin{align}
f'(x) &= 7sigmoid^6(x^2) \times t' \\
&= 7sigmoid^6(x^2) \times sigmoid(x^2) \cdot (1 - sigmoid(x^2)) \times 2x \\
&= 14xsigmoid^7(x^2) \cdot(1 - sigmoid(x^2))
\end{align}
$$
となるのです。
このように$g'(t)$の部分は最初のものと一切変わっていない(入力の$t$が変わっただけ)ことがわかりますね。
また、ここで注目して欲しいのは
$$
7sigmoid^6(x^2) \times sigmoid(x^2) \cdot (1 - sigmoid(x^2)) \times 2x
$$
の部分です。
このように連鎖律はその関数の微分を各関数の微分の積で表すことができ、これは関数が3個以上になろうと使用できるのです。
これによって、ニューラルネットワークのように無数の線形結合と活性化関数が折り重なっている非常に複雑な合成関数であっても、それぞれの元の関数の微分式が分かっていれば単にそれを掛け合わせていくだけで微分値を求めることができるのです。
誤差逆伝播
微分の求め方は連鎖律で見てきました。
さて、ではこのニューラルネットワークで各パラメータの傾きを偏微分として求めていくわけですが、そもそも各パラメータは何を少なくするための微分でしたでしょうか。
それはニューラルネットワークの損失でしたね。
つまり、連鎖律を利用して各パラメータの損失に対する傾きを求めるためには損失から微分値を掛け合わせていく必要があります。
ニューラルネットワークは合成関数ですから、損失の値から損失関数の微分値を求めて、それをさらに上の関数の微分を掛け合わせて、さらにその上の関数の微分を掛け合わせて...と一つ一つの関数を徐々に遡ってその関数の微分を求めていきます。
これによって、あるパラメータの損失に対する微分値を求めることができ、この方法は損失を遡って伝播させる姿から誤差逆伝播法と呼ばれています。
これによって求められた各パラメータの損失に対する傾きを利用すると、その時点におけるもっとも誤差を減らす方向を示すことができ、これでパラメータを調整することで誤差を減らすことができます。
これが最急降下法の本質と計算です。
まとめ
どれだけ表現力が豊かなニューラルネットワークでもその結果が正確でなければ何の意味もないことを話しました。
学習させて「モデルを正確にする」ことが大切であり、「モデルが正確」というのは「間違いが少ない」ということでした。
この間違いの度合いを損失関数という関数を導入してモデルの出力と正解から導きました。
そして、学習とはこの損失を減らす方向に動くものであり、もっとも急な方向に下っていくことから最急降下法という概念を学びました。
ニューラルネットワークのまとめ
ニューラルネットワークの歴史からプリミティブな単位である「ニューロン」の仕組みを学びました。
さらにこのニューロンの非線形性という大きな特徴を活かすために複数のニューロンを用いてネットワークのように相互接続したニューラルネットワークを構成することについても見てきました。
さらに表現力豊かなニューラルネットワークを学習させる、ということを学んできました。
今回の内容からニューラルネットワークというものが実は非常にシンプルな数式を重ね合わせて作られている、ということを理解していただければ私の目的は達成と言えます。
なお、本編では非常に表面的な本質の話をしてきましたが、もしも興味を持ったらぜひ折りたたみの「〇〇のお話」を見ていただければと思います。どれも数学の知識を必要とするものですが、ニューラルネットワークの理論としてより深い理解に非常に重要なものとなっています。
現代のAIは魔法じゃない
ここまででニューラルネットワークはシンプルな算数で動いているということを見てきました。
実は現代の大規模言語モデルも本質的な仕組みは変わりません。
もちろん、Transformerやそこから派生した現代のアルゴリズムは自己注意機構や交差注意機構など複雑な理論がたくさん利用されていますが、誤差を求めてこれを減らす方向にその関数を下っていくというのは一切変わりません。
その結果として予測が正確になり、その予測の対象が「人が使う言葉である」というだけなんです。
確率論的言語モデル
現代の大規模言語モデルは言葉を確率論として求めて出力しています。
どういうことか、おそらく皆さんもChatGPTとかGeminiとかを利用する際に「トークン」という言葉を聞いたことがあると思います。
トークンというのは誤解を恐れずに言えば世の中の言葉をコンピュータに理解させるために数値化したものです。
こんにちは、今日は晴れです。
という文章がOpenAIのGPT-4oのトークンでは
こんにちは
、
今日は
晴
れ
です
。
と分割されて、これが
[95839, 1395, 170411, 123139, 9472, 15121, 788]
となってトークン化されます。
これを受け取ってモデルはこの入力に続くトークンを予測するんです。
先ほどのニューラルネットワークの話では「損失を減らす」という話をしました。
では正解データってこのケースでは何でしょうか。
言語の正解データは「世の中に存在する全てのテキスト」です。当たり前ですが、この文章も含めて言語モデルの正解は過去に人間が紡いできた言葉たちなんですよね。
したがって、「正確なモデル」ということはモデルが上に書いていたトークンを受け取って、世の中の過去の人間がどのように返してきたのか、ということをなぞるわけです。
例えば「こんにちは、ユーザさん」と返してくるのが自然ですよね。この時、モデルは以下のような応答をするイメージです。(分かりやすさのためにトークンではなく言葉としています。)
次の言葉 | 確率 |
---|---|
こんにちは | 90% |
嫌い | 1% |
遊ぼう | 1% |
やぁ | 5% |
雨 | 2% |
正解 | 1% |
この結果から次の言葉として「こんにちは」を選択します。
そして、モデルにこの「こんにちは」をさらに入力してその次の言葉、さらにその次の言葉...と繰り返すことで言葉を紡いでいます。
正解データが世の中のテキストということはつまり、世の中でどのような言葉が過去に交わされてきたかを確率的に求めて文を作るので、裏を返すとLLMは本質的に言葉の意味を言葉として理解しているわけではないのです。
あくまで過去に人間がどんな言葉を返したか、というデータを見て「こう返せば人間ぽいでしょ」ということを予測しているに過ぎないのです。
トークンのお話 (難しくないです)
実際のトークンは本編で記載した数字ではなく、ベクトルで入力されています。
この数字はトークンIDであり、これをキーのようにして数千次元のベクトルを呼び出しています。
自然言語処理ではベクトル化という処理は非常に重要であり、これはその言語ベクトル空間における位置を示していいます。
言葉の意味が近いと近いベクトルになりやすい、などあります。Qiitaには素晴らしい専門の記事がたくさんあるので気になった方は「自然言語 ベクトル」などで検索してみてください。
したがって、LLMのモデルそのものに正しい情報かどうか、ということはそもそも関係ないわけです。
数学の問題を間違える理由はその問題を解くことを考えているわけではなく、あくまでその問題の内容に対してもっともそれっぽい内容を返すというのが、言語モデルのそもそもの仕様だからです。
そのモデルが学習したデータに存在しない計算を正しく出力できるわけがないので、あくまでそれに言葉的に自然なものを選択するわけです。
コードを実行できるLLMのお話 (難しくないです)
現代のLLMでは過去のテキスト的な結果だけではなく、Pythonなどのコードを生成して実行することで正しい計算の結果を返せるようになっているモデルもあります。
これを利用することで本来モデルが持っていない計算であっても正確に行うことができるようになってきています。
これとCoTと呼ばれる物事を分解して「ステップバイステップで考える」という指示を合わせることで簡単な計算に分割して、それをプログラミングで正しい情報を得て、計算していくということができるようになっています。
ただし、本編ではあくまでも純粋な言語モデルに限った話をしているため、計算ができないとしています。
魔法じゃない
確率的にどんな言葉を返そうか、という予測をものすごい計算量の計算を行なって正しく予測することを繰り返すことでテキストを生み出しているのが現代のAIの挙動です。
どんな事でも正しい情報を探して答えてくれるような魔法の道具ではないわけです。
そもそも、私たちが入力した言葉に対応するトークンに続く可能性が高いトークンを予測しているだけであり、テキストそのものを理解しているわけでもないのです。
当然、言葉の意味を直接理解しているわけではなく、あくまでもトークンという数字を扱っているだけに過ぎないわけです。
そして、これをこの記事の中で学んできたような簡単な計算をその数値に対して繰り返して、確率の高いトークンという数字を選んでそれを返しているだけなのです。
AIは「正しい情報を返すように努力しています」という言葉を言いますが、本当に正しい情報を返すように努力するなんて考えてもいません。
そういった制約がある中で世の中のエンジニアたちはどうやってより正しい情報を返せるようにするか、という機構を日夜試行錯誤しています。
ウェブ検索やRAGで与えるテキストはAIに「この情報を使ってでユーザの入力に続きそうな言葉を作って」としているイメージです。
これはウェブ検索や追加情報をAIに与えるというエンジニアたちの努力の結晶です。その場で一緒に情報が与えられることでその中からそれっぽいものを作ると結果的に正しい情報になるのです。
魔法ではなく、世の中の技術者たちの努力の結晶なのです。
理解していてもハルシネーションにむかつくことはあるでしょうが、そんな時は「確率論だもんな、運が悪かったんだ。」くらいに思って自分で調べ直す機会にしてもいいかもしれませんね。
AIの意味理解のお話 (補足)
現代の自然言語処理では位置情報が言葉の意味を示すという理論が広く使われており、
「Position Encoding」でベクトルの作成を行なっています。
これによってAIは意味を理解しているという風に捉えることもできます。
またSelf-Attentionによって、深いテキストの意味理解をしているという見方もあります。
ただし、本文の中ではあくまでもある言葉を受けてその意味を元に答えを正しくしようという思考力的な意味の理解をとらえているかどうか、という部分で誤解を恐れずに言えば「意味理解をしていない」というものになっています。
例えば、$1 + 1 = 2$という情報を持ってるモデルが$1 + 2 =$という問題が未知であるとしたときに、「+があって、1と2を足すってことは2より1大きくなるから、3だ!」という論理的な推論を行うかどうか、という部分でここでは単純化して「意味を直接理解しているわけではない」としています。
最後に
今回は、AIは魔法じゃないよって話をニューラルネットワーク自体の仕組みを説明することから始めてきました。
現代の技術の結晶のLLMを便利に使うのは素晴らしいことですが、言語モデルの本質はニューラルネットワークであり、確率的な推論であることを知っていただけたら嬉しいです。
そして、確率論だからこそ限界があるということもわかってあげてください。
魔法じゃないけど、完璧じゃないけど、まるで魔法のように使えるように日夜研究を進められていて、我々はたった数千円でその粋を使えるんです。
確率的な限界を知って、それをどうやって克服しようとしているのか、世の中のその挑戦に興味を持ってくれたらそれはとても嬉しいです。
※ この記事は私が全て書いており、文章のライティングにおいてはAIを使用してません。
内容のチェックやファクトチェックはAIも活用しある程度行なっていますが、僕自身はこの分野の専門家でも、研究者でもないため、誤りがあれば遠慮なく指摘していただけますと幸いです。