学習の仕組み
本エピソード(ep.12)から ep.30強化学習までは『生成AI』の仕組みについてのお話しになります。
『生成AI』の仕組みについては知っている、あるいは興味が無いという方は 次に ep.31 long-term memory から読んでいただいても、話のつながりに矛盾はないようになっています。
先にすすんで、用語がわからない、というときに読み飛ばした部分に戻って読んでください。
「では、『生成AI』がどのように学習するか、見ていきましょう」システム管理者のアレックス・チャンがポケットから小さな台を出して広げ、カフェテリアのテーブルに置いた。
自分のタブレットを台の上に斜めに立てかけて、さらに薄くて小さな折り畳みキーボードとマウスを取り出した。
アレックスが天井のプロジェクタの電源を入れ、タブレット画面を壁に表示させる。
「最初に、最も簡単な学習を行うプログラム・コードを表示しましょう。これはPythonというプログラム言語のコードです」
「ここでは、プログラムが学習できることと、そのプログラムが汎用的であることを示すことが目的です」
「『生成AI』なのだがら、その二つの性能が必要なのね」
「そうです、0から3の整数を2つ発生させて、その加算結果を学習させ、最後にその合計をプログラムに予測させます」
画面にプログラム・コードが表示された。
import random
# 初期ウェイト
w1 = 0.0
w2 = 0.0
eta = 0.05 # 学習率
epochs = 100
for i in range(epochs):
# 入力(0〜3)
x1 = random.randint(0, 3) # 0から3の整数を乱数として発生→x1
x2 = random.randint(0, 3) # 0から3の整数を乱数として発生→x2
# 正解
t = x1 + x2
# プログラムによる解答
y = w1 * x1 + w2 * x2 # 積和計算でプログラムが予測する解答 y を計算
# 誤差に基づく差分
dw1 = (t - y) * x1 # 正解と回答との差 t-y を計算する
dw2 = (t - y) * x2 # 線形回帰:二乗誤差E = (t-y)^2の最小化
# ∂E/∂w1 = ∂E/∂y・∂y・∂w1= 2*(t-y)*x1
# ウェイト更新
w1 += eta * dw1 # etaは調整量を暴れさせないためのパラメタ
w2 += eta * dw2
# テスト
print("\nテスト:")
for x1 in range(4):
for x2 in range(4):
y = w1 * x1 + w2 * x2
print(f"{x1} + {x2} = {y:.2f}(正解 {x1+x2})")
「このコードを説明する前に、まず実行してみましょう」
画面が代わって以下のように表示された。
Z:\home\創作\バード・フル\プログラム>python simplep01.py
テスト:
0 + 0 = 0.00(正解 0)
0 + 1 = 1.00(正解 1)
0 + 2 = 1.99(正解 2)
0 + 3 = 2.99(正解 3)
1 + 0 = 1.00(正解 1)
1 + 1 = 2.00(正解 2)
1 + 2 = 3.00(正解 3)
1 + 3 = 3.99(正解 4)
2 + 0 = 2.01(正解 2)
2 + 1 = 3.00(正解 3)
2 + 2 = 4.00(正解 4)
2 + 3 = 5.00(正解 5)
3 + 0 = 3.01(正解 3)
3 + 1 = 4.01(正解 4)
3 + 2 = 5.00(正解 5)
3 + 3 = 6.00(正解 6)
「左の二つの数字が乱数で発生させた数字です。 = の右の数字がプログラムが予想した数字です。どうです、正解とほとんど一致しているでしょう」
「たしかに、そうだな」
「ではプログラムを順に見ていきましょう、まず最初は初期ウェイトの部分です」
# 初期ウェイト
w1 = 0.0
w2 = 0.0
「w1、w2をウェイト、あるいはパラメータといいます。現代の『生成AI』は数百兆ものパラメーターがありますが、このプログラムは最も簡単なものなので2つしかありません」
「学習する、ということは、このウェイトの数字を調整するということと同じです」
「etaやepochs はハイパーパラメーターといいます。 epochs は学習を繰り返す回数です。ここではふたつの数字を問題としてプログラムに与え、二つを足したものを正解として与える、これを100回繰り返します」
「for のところが繰り返しの命令だな」マルコが言う。
「そうです、そのとおりです。桁が下がっているところが、繰り返されます」
「ここではまず 問題の組、二つの整数を発生させます」
# 入力(0〜3)
x1 = random.randint(0, 3) # 0から3の整数を乱数として発生→x1
x2 = random.randint(0, 3) # 0から3の整数を乱数として発生→x2
そして、その正解 t も発生させます。
# 正解
t = x1 + x2
「これで、問題と正解のセットがひとつできます。そしてプログラムに正解を予測させましょう。予測する部分はここです」
# プログラムによる解答
y = w1 * x1 + w2 * x2 # 積和計算でプログラムが予測する解答 y を計算
「yがプログラムが出した解答です。右辺はウェイトと問題をかけて、足し算をしています。このような計算を積和計算といいます。『生成AI』では積和計算を多用します」
「また、学習の結果はw1, w2 に格納されるので、ここではこのようなものを『記憶セル』と呼ぶことにしましょう」
「最初は w1 も w2 もゼロですから、最初のプログラムの解答 y はゼロです。ほとんどの場合間違っています」
「そうね」
「どれくらい間違っているのかを見るために、正解と解答との差 t-y を計算しています」
# 誤差に基づく差分
dw1 = (t - y) * x1 # 正解と回答との差 t-y を計算する
dw2 = (t - y) * x2 # 線形回帰:二乗誤差E = (t-y)^2の最小化
# ∂E/∂w1 = ∂E/∂y・∂y・∂w1= 2*(t-y)*x1
「dw1 の d はデルタ、つまり微小な変化量という意味です」
「なぜ、x1 をかけているの」エレナが尋ねる。
「それは線形回帰の最小二乗法を使っているからです。誤差の二乗を最小にする、というやつです」
「なるほど、合成関数の微分ね。dy/dw1 はたしかに x1 ね」これもエレナが言った。
「そうです。別に最小二乗法を使わなくともいいのですが、ここでは最もわかりやすい方法として使っています。。ただ、このプログラムは ウェイトの更新式から、x1, x2 の部分を省いても収束します」
「最後にウェイト w1, w2 を修正します」
# ウェイト更新
w1 += eta * dw1 # ウェイトを微調整する
w2 += eta * dw2
「プログラムはこれだけです。簡単ですね。最後にテストの部分で学習された w1, w2 で足し算のすべてのパターンを計算しています」
# テスト
print("\nテスト:")
for x1 in range(4):
for x2 in range(4):
y = w1 * x1 + w2 * x2
print(f"{x1} + {x2} = {y:.2f}(正解 {x1+x2})")
「重要なのは y を計算する時 x1+x2 という情報を使っていないということです。使っているのは w1 * x1 + w2 * x2 です」
「たしかに」
「では、学習の過程を見てみましょう。途中経過を見るために2行ほど追加しましょう」
print(f'{i+1}回目:解答は {y} かな。違うかな')
print(f'違うよ、正解は {t} だよ、正解との差は {t-y } だね。\n')
実行すると、画面に以下のように表示された。
1回目:解答は 0.0 かな。違うかな
違うよ、正解は 2 だよ、正解との差は 2.0 だね。
2回目:解答は 0.6000000000000001 かな。違うかな
違うよ、正解は 5 だよ、正解との差は 4.4 だね。
3回目:解答は 3.9000000000000004 かな。違うかな
違うよ、正解は 6 だよ、正解との差は 2.0999999999999996 だね。
4回目:解答は 1.51 かな。違うかな
違うよ、正解は 2 だよ、正解との差は 0.49 だね。
……
99回目:解答は 3.000645541777643 かな。違うかな
違うよ、正解は 3 だよ、正解との差は -0.0006455417776431993 だね。
100回目:解答は 1.000556656272755 かな。違うかな
違うよ、正解は 1 だよ、正解との差は -0.0005566562727550295 だね。
「このように、学習によって徐々に正解に近づいています。この様子をグラフで見てみましょう。横軸が学習の回数で、縦軸が誤りの量です」
「このように徐々に w1, w2 を調整していき、それにつれて緑の線、つまり誤りの度合いがゼロに近づいていきます。学習しているのです」
「ほんとうに学習しているみたいね」
アレックスが最後にグラフを表示したPythonプログラム全文は、以下の通りです。
import random
import matplotlib.pyplot as plt
# 初期ウェイト
w1 = 0.0
w2 = 0.0
eta = 0.05 # 学習率
epochs = 100
# グラフ用の記録リスト
w1_list = []
w2_list = []
err_list = []
for i in range(epochs):
# 入力(0〜3)
x1 = random.randint(0, 3) # 0から3の整数を乱数として発生→x1
x2 = random.randint(0, 3) # 0から3の整数を乱数として発生→x2
# 正解
t = x1 + x2
# プログラムによる解答
y = w1 * x1 + w2 * x2 # 積和計算でプログラムが予測する解答 y を計算
print(f'{i+1}回目:解答は {y} かな。違うかな')
print(f'違うよ、正解は {t} だよ、正解との差は {t-y } だね。\n')
# 記録
w1_list.append(w1)
w2_list.append(w2)
err_list.append(t-y)
# 誤差に基づく差分
dw1 = (t - y) * x1 # 正解と回答との差 t-y を計算する
dw2 = (t - y) * x2 # 線形回帰:二乗誤差E = (t-y)^2の最小化
# ∂E/∂w1 = ∂E/∂y・∂y・∂w1= 2*(t-y)*x1
# ウェイト更新
w1 += eta * dw1 # ウェイトを微調整する
w2 += eta * dw2
# テスト
print("\nテスト:")
for x1 in range(4):
for x2 in range(4):
y = w1 * x1 + w2 * x2
print(f"{x1} + {x2} = {y:.2f}(正解 {x1+x2})")
# グラフ描画
plt.figure(figsize=(10, 6))
plt.plot(w1_list, label="w1")
plt.plot(w2_list, label="w2")
plt.plot(err_list, label="t - y (error)")
plt.xlabel("epochs")
plt.ylabel("value")
plt.title("Learning curves of w1, w2, and error")
plt.legend()
plt.grid(True)
plt.show()




