前回(#9)は、nnの学習の仕組みとバックプロパゲーションがなぜ“逆”方向伝播と呼ばれるのか、学習の各ステップを、知る、考える、更新するの3ステップに分けて紹介しました。
【仕組み解説】全結合層をゼロから実装しよう:NNの学習の仕組み-誤差と損失、逆伝播-(NN #9)
NNが賢くなる仕組みを解説。損失関数、誤差の計算、そして学習の核となる逆伝播(Backward)の理論を理解します。
今回は、前回紹介した逆伝播を第8回で作った順方向だけの全結合層に追加して、完成させます。
また、記事シリーズ中で取り扱った XORをnnで解けるように実装してみようと思います。
【仕組み解説】全結合層をゼロから実装しよう:全結合層の順方向だけ実装してみる。(NN #8)
これまでの理論に基づき、順伝播(Forward)をゼロから実装します。入力から予測値を導くNNの骨格を作り上げます。
ゼロから作る全結合層
ゼロから作る全結合層シリーズでは、初心者でも理解しやすいように、 「パーセプトロンの仕組み」から「全結合層の実装」までを...
この記事シリーズでは、ディープラーニングに入るまでの道筋をその根本的な設計思想からディープラーニングの肝であり、基礎の最小単位である全結合層について、ゼロから作って理解していきます。
実装
線形レイヤーAffineの逆伝播実装
線形計算の考えるステップを実装してみます。
実装する中身
実装する中身は、知るステップはもう実装済み(#8)なので、”考える”ステップを実装します。
今の傾きを知るには、今の重みに出力から帰ってきた値をかけたら求まります。
(微分のある地点の導関数 \(f(x)\)(傾き)を求める操作と同じです。)
コード
class Affine:
def __init__(self, W, b):
self.W = W
self.b = b
self.x = None
self.dW = None
self.db = None
def forward(self, x):
self.x = x
return torch.matmul(x, self.W) + self.b
def backward(self, dout):
dx = torch.matmul(dout, self.W.t())
self.dW = torch.matmul(self.x.t(), dout)
self.db = torch.sum(dout, dim=0)
return dxReluの逆伝播実装
Reluでは、0を超えたら、そのままの値。0未満なら0を返すようにしていたので、逆方向のときも同じことをします。
順方向のときに0にしたところは0にして、それ以外には触りません。
コード
class Relu:
def __init__(self):
self.mask = None
def forward(self, x):
self.mask = (x <= 0)
out = x.clone()
out[self.mask] = 0
return out
def backward(self, dout):
dout[self.mask] = 0
return dout損失関数を作る
平均二乗誤差を計算するクラス MSE
平均二乗誤差を計算するクラスです。
コード
# 【NEW】損失関数:平均2乗誤差 (Mean Squared Error)
class MSE:
def __init__(self):
self.y = None
self.t = None
def forward(self, y, t):
self.y = y
self.t = t
# Loss = 0.5 * (y - t)^2 の合計
loss = 0.5 * torch.sum((y - t) ** 2)
return loss
def backward(self, dout=1):
# Lossの微分 = (y - t)
# (平均二乗で定義した0.5 * (y - t)^2の微分)
# これが最初の「誤差」として逆伝播のスタート地点になります
dx = self.y - self.t
return dx1サイクルの学習手順を見てみる
学習手順を順次見ていきましょう。
0. (初めだけ)データの用意と重みの初期化
学習するためには、入力と正解のセットが必要なので、用意します。
本来は、計測値と出力のセットなどを用意しますが、今回はサンプルとしてランダムな値を入れています。
# 0. データの準備
torch.manual_seed(42) # 結果固定
# 入力(X): 2件のデータ, 3つの特徴量
X = torch.randn(2, 3)
# 正解(T): 2件のデータ, 1つの正解値 (回帰問題を想定)
T = torch.randn(2, 1)
# 重みの初期化
W1 = torch.randn(3, 4) # 1層目の重み
b1 = torch.zeros(4)
W2 = torch.randn(4, 1) # 2層目の重み (出力用)
b2 = torch.zeros(1)
1. (初めだけ)モデルの定義と学習率
今回のモデルは2層(出力用に1)のモデルを作りました。
学習時、1ステップでどのくらい更新するかを決める小さい値を掛けて更新を行います。
この小さい値を学習率(leaning rate)といいます。
# レイヤーの積み上げ
layers = [
Affine(W1, b1),
Relu(),
Affine(W2, b2) # 最後にもう一度Affineを通して1つの値にする
]
loss_layer = MSE()
learning_rate = 0.1 # 学習率 (更新の歩幅)
一般に使われているモデルは、このAffineやRelu(またはその他の層 Softmaxとか)を重ねて多層化し、スキップ接続など色々して精度を上げています。今回は、入門ということで、シンプルに実装してあります。
2. 学習サイクル:知るステップ
学習本番です。まずは、知るステップでしたね。ここの出力は、第8回で作ったものに出力用のAffineレイヤーをもう一回掛けたものになります。
print("--- 学習サイクル開始 ---")
# === STEP 1: 順伝播 (知る) ===
print("\n【Step 1: 順伝播 (Forward)】")
out = X
for layer in layers:
out = layer.forward(out)
print(f"Layer {layer.__class__.__name__} passed. Shape: {out.shape}")
y_pred = out # AIの予測値3. 学習サイクル:Lossの計算
順方向の出力 y_predが得られたので、MSEに通して、Lossを計算します。
これで、現在の学習がどのくらい進んでいるかを確認できます。
# === ロス計算 ===
print("\n【Step 2: ロス計算 (Loss)】")
loss = loss_layer.forward(y_pred, T)
print(f"現在の予測: \n{y_pred.T} (転置表示)")
print(f"正解データ: \n{T.T} (転置表示)")
print(f"Lossの値 : {loss.item():.4f}")
print("→ まだ全然合っていませんね。")
4. 学習サイクル:考える (微分の計算)
考えるステップです。一番うしろの層から順に誤差を渡して傾き(勾配)を計算します
# === STEP 2: 考える (Backward) ===
print("\n【Step 3: 考える (Backward)】")
# 一番後ろのLoss層からスタート
# dout = t-y (平均二乗で定義した0.5(t-y)^0.5の微分)
dout = loss_layer.backward()
# レイヤーを逆順にして誤差を伝えていく
for layer in reversed(layers):
dout = layer.backward(dout)
print("逆伝播完了。各層が「修正方向(勾配)」を計算しました。")
# 試しに1層目の重み(W1)の勾配を見てみる
print(f"W1の勾配(dW)の例:\n{layers[0].dW}")
print("→ この値の分だけ、逆に動かせば誤差が減るはずです。")5. 学習サイクル:更新
更新ステップです。傾きがわかったので、学習率を掛けて更新します。
# === STEP 3: 更新 (Update) ===
print("\n【Step 4: 更新 (Update)】")
# パラメータを持っている層(Affine)だけ更新
# W = W - lr * dW
layers[0].W -= learning_rate * layers[0].dW
layers[0].b -= learning_rate * layers[0].db
layers[2].W -= learning_rate * layers[2].dW
layers[2].b -= learning_rate * layers[2].db
print("パラメータを更新しました!")6. 結果の確認
結果を確認してみます。lossが小さくなっていれば成功です。
# --- 確認:本当によくなった? ---
print("\n【確認テスト】")
out_new = X
for layer in layers:
out_new = layer.forward(out_new)
loss_new = loss_layer.forward(out_new, T)
print(f"更新前のLoss: {loss.item():.4f}")
print(f"更新後のLoss: {loss_new.item():.4f}")
if loss_new < loss:
print("成功 Lossが減りました!")
else:
print("失敗 Lossが増えてしまいました(学習率が大きすぎたかも?)")コード全文
(長いので折りたたんでいます。)
import torch
# --- 1. 必要なクラスの定義 (Affine, Relu, MSE) ---
class Affine:
def __init__(self, W, b):
self.W = W
self.b = b
self.x = None
self.dW = None
self.db = None
def forward(self, x):
self.x = x
return torch.matmul(x, self.W) + self.b
def backward(self, dout):
dx = torch.matmul(dout, self.W.t())
self.dW = torch.matmul(self.x.t(), dout)
self.db = torch.sum(dout, dim=0)
return dx
class Relu:
def __init__(self):
self.mask = None
def forward(self, x):
self.mask = (x <= 0)
out = x.clone()
out[self.mask] = 0
return out
def backward(self, dout):
dout[self.mask] = 0
return dout
# 【NEW】損失関数:平均2乗誤差 (Mean Squared Error)
class MSE:
def __init__(self):
self.y = None
self.t = None
def forward(self, y, t):
self.y = y
self.t = t
# Loss = 0.5 * (y - t)^2 の合計
loss = 0.5 * torch.sum((y - t) ** 2)
return loss
def backward(self, dout=1):
# Lossの微分 = (y - t)
# これが最初の「誤差」として逆伝播のスタート地点になります
dx = self.y - self.t
return dx
# --- ここから学習の実践 ---
# 0. データの準備
torch.manual_seed(42) # 結果固定
# 入力(X): 2件のデータ, 3つの特徴量
X = torch.randn(2, 3)
# 正解(T): 2件のデータ, 1つの正解値 (回帰問題を想定)
T = torch.randn(2, 1)
# 重みの初期化
W1 = torch.randn(3, 4) # 1層目の重み
b1 = torch.zeros(4)
W2 = torch.randn(4, 1) # 2層目の重み (出力用)
b2 = torch.zeros(1)
# レイヤーの積み上げ
layers = [
Affine(W1, b1),
Relu(),
Affine(W2, b2) # 最後にもう一度Affineを通して1つの値にする
]
loss_layer = MSE()
learning_rate = 0.1 # 学習率 (更新の歩幅)
print("--- 学習サイクル開始 ---")
# === STEP 1: 順伝播 (知る) ===
print("\n【Step 1: 順伝播 (Forward)】")
out = X
for layer in layers:
out = layer.forward(out)
print(f"Layer {layer.__class__.__name__} passed. Shape: {out.shape}")
y_pred = out # AIの予測値
# === STEP 2: ロス計算 ===
print("\n【Step 2: ロス計算 (Loss)】")
loss = loss_layer.forward(y_pred, T)
print(f"現在の予測: \n{y_pred.T} (転置表示)")
print(f"正解データ: \n{T.T} (転置表示)")
print(f"Lossの値 : {loss.item():.4f}")
print("→ まだ全然合っていませんね。")
# === STEP 3: 考える (Backward) ===
print("\n【Step 3: 考える (Backward)】")
# 一番後ろのLoss層からスタート
dout = loss_layer.backward()
# レイヤーを逆順にして誤差を伝えていく
for layer in reversed(layers):
dout = layer.backward(dout)
print("逆伝播完了。各層が「修正方向(勾配)」を計算しました。")
# 試しに1層目の重み(W1)の勾配を見てみる
print(f"W1の勾配(dW)の例:\n{layers[0].dW}")
print("→ この値の分だけ、逆に動かせば誤差が減るはずです。")
# === STEP 4: 更新 (Update) ===
print("\n【Step 4: 更新 (Update)】")
# パラメータを持っている層(Affine)だけ更新
# W = W - lr * dW
layers[0].W -= learning_rate * layers[0].dW
layers[0].b -= learning_rate * layers[0].db
layers[2].W -= learning_rate * layers[2].dW
layers[2].b -= learning_rate * layers[2].db
print("パラメータを更新しました!")
# --- 確認:本当によくなった? ---
print("\n【確認テスト】")
out_new = X
for layer in layers:
out_new = layer.forward(out_new)
loss_new = loss_layer.forward(out_new, T)
print(f"更新前のLoss: {loss.item():.4f}")
print(f"更新後のLoss: {loss_new.item():.4f}")
if loss_new < loss:
print("成功 Lossが減りました!")
else:
print("失敗 Lossが増えてしまいました(学習率が大きすぎたかも?)")
1ステップ分学習してみる。
上記のモデル学習プログラムを実行すると学習の1ステップの中身が表示されます。
以下に特に見てほしいところを抜き出して説明します。
ロス計算
【Step 2: ロス計算 (Loss)】
現在の予測:
tensor([[ 0.0000, -0.4941, -0.3093, -0.4258]]) (転置表示)
正解データ:
tensor([[0., 1., 1., 0.]]) (転置表示)
Lossの値 : 2.0640
→ まだ全然合っていませんね。予測とLossの計算結果です。予測を見ると、学習前の予測だと大きく正解データと離れていることが確認できると思います。
Lossも 2.0640と大きめの値になっています。
勾配計算
【Step 3: 考える (Backward)】
逆伝播完了。各層が「修正方向(勾配)」を計算しました。
W1の勾配(dW)の例:
tensor([[0.0000, 1.1860, 0.1110, 0.2327],
[0.0000, 0.0000, 0.5005, 1.0492]])
→ この値の分だけ、逆に動かせば誤差が減るはずです。勾配計算結果の一部です。
これをみると、一行目の1つ目と二行目の1つ目と2つ目はReluで0担っているようです。また、一行目の1つ目、二行目の4つ目が一回の学習で大きく更新されそうであることもわかります。
更新と結果
【Step 4: 更新 (Update)】
パラメータを更新しました!
【確認テスト】
更新前のLoss: 2.0640
更新後のLoss: 0.7897
🎉 成功です! Lossが減りました!更新結果を見ると、Lossが2くらいあったのから 0.7にまで改善しました。(これは初期乱数によるので毎回同じにはなりません)
うまく学習ができていることが確認できます。
まとめと次回:XORをNNで解いてみる。
今回で基本的なNNの実装ができたと思います。
次回は、これまでの集大成として、XOR問題を今回作ったNNで解いてみようと思います。
次回の実装内容としては、今回の1ステップ(エポック)をループさせて学習ループを実装し、学習させ、学習後のモデルで推論してみようと思います。
エポックって何?学習がうまく行かないローカルミニマム問題についても紹介しようと思うので、今回でモデルを理解した!という方は見に行ってみてください。
【仕組み解説】-完結- XOR問題をNNで解く:学習ループと推論の実装(NN #11)
XOR問題を実際にNNで学習させ、解けることを確認してシリーズ完結。エポックやローカルミニマムといった概念も解説します。
(もしまだ、投稿されていなかった場合、または、自分で作ってみたい!という場合は、入力を以下のように変えて、重みのカタチを、これに合わせると実装できます。(形については第6回二紹介しているので忘れた!というときは確認してみてください))
import numpy as np
# 入力(X): 4件のデータ, 2つの特徴量
X_xor = [[0.0,0.0],[0.0,1.0],[1.0,0.0],[1.0,1.0]]
T_xor = [[0.0], [1.0], [1.0], [0.0]]
X_xor_array = np.array(X_xor)
T_xor_array = np.array(T_xor)
X = torch.from_numpy(X_xor_array).float()
T = torch.from_numpy(T_xor_array).float()
print(f"X shape: {X.shape}, dtype: {X.dtype}") # dtype float32
print(f"T shape: {T.shape}, dtype: {T.dtype}")
print(X.shape)
print(T.shape)ゼロから作る全結合層
ゼロから作る全結合層シリーズでは、初心者でも理解しやすいように、 「パーセプトロンの仕組み」から「全結合層の実装」までを...
ここまで読んでいただきありがとうございます。
では、次の記事で。 lumenHero