AIは何を「学習」しているのか — 直線1本で分かる機械学習【ゼロから学ぶAI 第1回】
「学習」の正体を、点5つと直線1本だけで最後まで計算します。外れ具合を1つの数にする損失、損失の谷を下る勾配降下法、学習率を上げすぎたときの発散、そして訓練データでは満点なのに外で破綻する曲線まで。数字はすべて手で検算できます。動画版つき。
要点
- 機械学習は、ルールを人が書く代わりに、答え付きの例からルール(パラメータの値)を機械に決めさせる方法である。
- 外れ具合は「予測と正解の差を2乗して平均した数」(平均二乗誤差)にまとめる。2乗しないと、プラスとマイナスの外れが打ち消し合う。
- この記事の例では損失が 11(w−2)²+0.8 という放物線になり、谷底の w=2 が最良の直線になる。
- 勾配降下法は、損失の傾きの逆向きに少しずつ w を動かす。学習率0.02なら谷底との差が1歩ごとに0.56倍になり、0.1では−1.2倍で発散する。
- 5点すべてを通る4次曲線は訓練データでの誤差が0だが、0時間で−14点、7時間で0点と予測する。訓練データでの点数は目的ではない。
出典: Deep Learning, Chapter 5: Machine Learning Basics · Efficient BackProp · Linear Models — scikit-learn documentation · numpy.polyfit — NumPy Manual
このシリーズについて — 「ゼロから学ぶ AIと技術のしくみ」は、第1回から順に読めば(見れば)積み上がるように作った解説シリーズです。各回に同じ内容の動画版があります。この第1回は前提知識なしで読めます。
ルールを書く仕事から、例を見せる仕事へ
迷惑メールを見分けるソフトを作るとします。普通のプログラミングでは、「件名に『当選』があれば迷惑メール」のようなルールを人が書きます。ところが相手は表現を変えてきます。「ご当選」「当 選」……ルールは際限なく増えていきます。
機械学習は発想を裏返します。ルールを書く代わりに、答え付きの例をたくさん見せて、見分け方のほうを機械に作らせるのです。
| 人が渡すもの | 出てくるもの | |
|---|---|---|
| 普通のプログラミング | データ + ルール | 答え |
| 機械学習 | データ + 答え | ルール |
では「機械がルールを作る」とは、具体的に何をしているのでしょうか。この記事では、点5つと直線1本だけで、それを最後まで計算してみます。
例: 勉強時間と小テストの点数
5人の生徒の、勉強時間と小テスト(20点満点)の点数です(説明のために作った架空のデータです)。
| 勉強時間 (時間) | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| 点数 (点) | 2 | 5 | 5 | 7 | 11 |
知りたいのは「勉強時間から点数を予想するルール」です。いちばん単純なルールとして、直線を考えます。
この式が言っているのは要するに、「点数は勉強時間に比例する」という仮説です。
記号を1つずつ見ます。 は勉強時間(入力)、(ワイ・ハット)は予想した点数です。正解の点数 と区別するために帽子を付けています。 は直線の傾きで、「1時間勉強すると何点上がるか」を表します。
この が、機械に決めさせたいつまみです。専門用語ではパラメータと呼びます。 なら1時間で1点、 なら1時間で3点。つまみを回すと直線の傾きが変わります。
(今回は話を簡単にするため、「0時間なら0点」の直線、つまり原点を通る直線だけを考えます。切片を入れた場合は最後に触れます。)
「学習」とは、データにいちばん合う を見つけることです。 では「いちばん合う」をどう測るのか。
外れ具合を1つの数にする: 平均二乗誤差
まず を試します。予想は 点。正解との差(正解 − 予想)は
です。この5つの差を、1つの数にまとめたい。数が1つなら「良くなった/悪くなった」が比べられるからです。
素直に足すとどうなるでしょう。実は危険です。 で試すと、予想は 点、差は
で、足すと0になります。外れているのに「誤差0」と言ってしまう。プラスの外れとマイナスの外れが打ち消し合うからです。
そこで差を2乗してから足し、件数で割って平均にします。これが平均二乗誤差(MSE)です。
この式が言っているのは要するに、「1人ずつ外れを2乗して、全員分を平均する」ということです。
記号を1つずつ。 は損失(loss)=外れ具合の数、 はデータの件数(ここでは5)、 は 番目の生徒の正解、 はその予想、 は「 から まで全部足す」記号です。
数字を入れます。
- : 差の2乗は 。合計59、5で割って
- : 差の2乗は 。合計4、5で割って
のほうがずっと小さい。つまり の直線のほうがデータに合っています。2乗には、大きな外れほど重く数える(差が2倍なら4倍)という性質もあります。
損失の地形: w を回すと L はどう変わるか
をいろいろ変えて を計算すると、次のようになります。
| 0 | 1 | 1.5 | 2 | 2.5 | 3 | |
|---|---|---|---|---|---|---|
| 44.8 | 11.8 | 3.55 | 0.8 | 3.55 | 11.8 |
を底にした、左右対称の谷です。実はこの例では、式を展開すると はぴったり放物線になります。
この式が言っているのは要するに、「 が2から離れるほど、離れた距離の2乗に比例して損失が増える」ということです。
( の2乗の平均が11、 と の積の平均が22、 の2乗の平均が44.8 なので、 と整理できます。)
この式から2つのことが読めます。 のとき なので損失は最小の 0.8。そして0.8より下には行けません。直線ではどう回しても、この5点を全部ぴったり通ることはできないからです。
ここまで来れば、学習は「損失の谷の、いちばん低い所を探すこと」と言い換えられます。
坂を下る: 勾配降下法
今回は式が分かっているので、谷底が だと一目で分かりました。しかし実際のAIでは、つまみは何百万、何十億個もあり、谷の全体像は誰にも見えません。見えるのは今いる場所の傾きだけです。
そこで、次の手順を繰り返します。
- 今いる場所の傾きを測る
- 傾きの逆向き(下り坂の向き)に、少しだけ を動かす
これが勾配降下法です。式にすると1行です。
この式が言っているのは要するに、「今の から、坂の向きと反対へ、坂の急さに比例した分だけ動く」ということです。
記号を1つずつ。 は「 を少し増やしたとき がどれだけ増えるか」の比率=傾き(微分)です。(イータ)は学習率で、1歩の大きさを決める数です。マイナスが付いているのは、傾きが正(右上がり)なら を減らし、負(右下がり)なら を増やす=下り坂へ進むためです。
この例の傾きは、放物線の式を微分して
です。 では 。右下がりの急な坂です。
学習率 、出発点 で歩いてみます。
| 歩数 | 傾き | 損失 | |
|---|---|---|---|
| 0 | 0 | −44 | 44.8 |
| 1 | 0.88 | −24.64 | 14.60 |
| 2 | 1.3728 | −13.80 | 5.13 |
| 3 | 1.6488 | −7.73 | 2.16 |
| 4 | 1.8033 | −4.33 | 1.23 |
1歩目は 。損失は 44.8 から 14.60 へ一気に下がります。谷底に近づくほど坂が緩くなるので、歩幅も自然に小さくなります。
もう一歩踏み込むと、1歩ごとに起きていることが分かります。更新の式に傾きを入れると
となり、谷底との差が毎回 倍になるのです。 なら 倍。差は と縮み、50歩でほぼ2.0に着きます。
学習率: 大きすぎると何が起きるか
歩幅を大きくすれば早く着くでしょうか。上の「差が 倍」の式で確かめます。
- : 倍率は 。 は 。マイナスなので谷底を飛び越えて反対側に出ますが、差は毎回10分の1になり、すぐ収まります。
- : 倍率は 。 は 。飛び越すたびに差が1.2倍に広がり、谷から放り出されます(発散)。
境目は倍率が になる所、 です。これより大きい学習率では、この例は決して谷底に着きません。小さすぎれば着くまでに何千歩もかかります。学習率は、実際のAIの学習でも最初に調整する数の1つです。
つまみが何百万個あっても、やっていることは同じです。つまみ1個ごとに傾きを測り(それを並べたものを勾配と呼びます)、全部のつまみを少しずつ下り坂へ動かす。今回の1行の式が、そのまま大規模な学習の中心にあります。
コードで見る
ここまでの計算は、Pythonで数行です。
import numpy as np
x = np.array([1, 2, 3, 4, 5.0]) # 勉強時間
y = np.array([2, 5, 5, 7, 11.0]) # 点数
w = 0.0 # 出発点
for step in range(50):
grad = 2 * np.mean((w * x - y) * x) # 傾き dL/dw
w -= 0.02 * grad # 学習率 0.02 で下る
print(round(w, 4)) # → 2.0
grad の行は、平均二乗誤差を で微分した式 をそのまま書いたものです。 で計算すると になり、上の表の1歩目と一致します。
覚えるのと分かるのは違う
最後に、もっと「データに合う」ルールを作ってみます。直線ではなく、5点を全部ぴったり通る曲線(4次式)です。
この式が言っているのは要するに、「つまみを5個に増やして、5人の点をすべて通るように合わせた曲線」です。
この曲線は5点すべてを通るので、5人のデータでの損失は0。直線の0.8より「良い」はずです。
ところが、データに無い所で何を予想するかを計算すると
- 0時間勉強した人: 直線は 0点、曲線は −14点
- 7時間勉強した人: 直線は 14点、曲線は 0点
曲線は、手元の5人を丸暗記しただけで、勉強時間と点数の関係を捉えていません。手元のデータでの損失を小さくすることは手段であって、目的はまだ見ていないデータで当たることです。この「丸暗記してしまう」現象を過学習と呼びます。どうやって見抜き、どう防ぐかは次の回で扱います。
切片を入れたら?
今回は「0時間なら0点」の直線だけを考えました。切片 を足して にすると、つまみが2つになり、損失の谷は放物線ではなくお椀の形(2次元の谷)になります。それでも手順は同じで、 と それぞれの傾きを測って下るだけです。このデータでは、お椀の底も にあります(たまたま原点を通る直線が最良になるデータです)。
現場ではこう使う
この記事の手順(損失を決める → 傾きを測る → 学習率の分だけ下る → 手元に無いデータで確かめる)は、データサイエンティストや機械学習エンジニアが新しい予測課題で最初の基準モデルを作るときに、そのまま出てきます。複雑なモデルに進む前に、まず直線(線形回帰)で損失がどこまで下がるかを測っておくと、後のモデルが本当に良くなったのかを比べられます。
実際に触る名前
- scikit-learn の
LinearRegression: 引数fit_interceptの既定はTrue(切片あり)。この記事のように原点を通る直線にしたいときだけFalseにします - scikit-learn の
SGDRegressor: 勾配降下法で線形モデルを当てます。1歩の大きさはeta0、その減らし方はlearning_rateで決めます - PyTorch: 損失は
torch.nn.MSELoss()(既定で平均を取る=この記事の と同じ)、更新はtorch.optim.SGD(params, lr=...)。lrがこの記事の です - NumPy の
numpy.polyfit(x, y, deg):degを上げると、この記事の4次曲線のような「全部の点を通る曲線」が1行で作れてしまいます
知らないと事故になる落とし穴
- 入力の単位を変えただけで発散する。 この例の境目 は、 の平均(11)で決まっていました(境目は )。勉強時間を「分」で入れると は60倍、 の平均は3600倍(39600)になり、同じ でも倍率は 。初手から発散します。入力の大きさをそろえて(標準化して)から学習するのはこのためです(Efficient BackProp でも入力の正規化を勧めています)
- 損失が
nanやinfになったら、まず学習率を疑う。 の例のように が往復しながら大きくなり続けると、やがて数値が表せる範囲を超えます - 手元のデータでの損失だけで良し悪しを決めない。 点が5個なら、
polyfitの次数を4まで上げるだけで訓練データの損失は0になります。評価は学習に使っていないデータで行います(分け方は第2回)
面接・実務で問われる問い
- 「学習を回したら損失が下がらず、途中から増え始めた。何を確かめるか」 — 筋道: まず学習率を下げて再実行し、下がるかを見る。下がるなら、歩幅が谷の曲がり具合に対して大きすぎた(この記事の倍率 の状態)。次に、入力の大きさがそろっているかを見る
- 「訓練データでの誤差が0のモデルを作った。良いモデルか」 — 筋道: それだけでは分からない。学習に使っていないデータで測り、訓練との差が大きければ過学習を疑う
- 「なぜ誤差を2乗するのか。足すだけでは駄目なのか」 — 筋道: この記事の の例で、足すと0になってしまうことを示す。2乗すれば打ち消し合わず、大きな外れほど重く数えられる
まとめ
- 学習とは、例のデータに合うようにパラメータ(つまみ)の値を決めること
- 合い具合は損失で測る。回帰では、差を2乗して平均した平均二乗誤差が基本
- 谷底は勾配降下法で探す。傾きの逆向きに、学習率の分だけ少しずつ動く。学習率が大きすぎると発散する
- 手元のデータで損失0でも、外で当たるとは限らない(過学習)
次回(第2回): 覚えるのと分かるのはどう違うのか — 訓練・検証・テストの分け方と、過学習の見抜き方。
コメント
コメントにはログインが必要です