🔥

【PyTorchで深層学習の仕組みを学ぶ②】中間層を使ってみよう〜中間層、活性化関数、ReLU〜

に公開

前回のおさらい

https://zenn.dev/kouch/articles/cf580a6c9d582d
前回は、一番簡単な関数として

y = wx + b

という一次関数を設定し、その[x, y]を学習させて、weightとbiasを予想させました。
今回は、もう少し複雑な関数として、**「インプットのxを増やす」「二次関数にする」**というレベルアップを試みます。

複雑な関数を学習させてみよう(中間層なし)

やりたいこと

今回は、このような関数を学習させてみましょう。

y = x_1^2 + x_2^2 + 5

前回と違い、インプットが[x1, x2]の二つになっていること、そして線形ではなく二次の項が入っています。

学習用のデータセットとして、[x1, x2, y]の組を9つ与えて、その後にテストデータとしての[x1, x2]を与えて、yを予測させ、実際のyと比較をさせてみます。

前回は直接weightとbiasを予想させましたが、今回の正解は二次関数になっています。
なので、意味のあるweightやbiasの比較ができないので、出力であるyを比較させましょう。

中間層なしのコード

# xの二乗の関係を学習させようとしてみる例
# このままだと線形モデルなので、うまく学習できない。
import torch

# y = x1 ** 2 + x2 ** 2 + 5
x = torch.tensor([[1.0, 2.0], [2.0, 3.0], [3.0, 3.0], [4.0, 1.0], [5.0, -3.0], [2.0, 0.0], [0.0, 4.0], [2.0, 1.0], [3.0, 2.0]])
y = torch.tensor([[10.0], [18.0], [23.0], [22.0], [39.0], [9.0], [21.0], [10.0], [18.0]])

model = torch.nn.Linear(in_features = 2, out_features = 1)
loss_fn = torch.nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr = 0.01)

model.train()

for epoch in range(1000):
	pred_y = model(x)
	loss = loss_fn(pred_y, y)

	optimizer.zero_grad()
	loss.backward()
	optimizer.step()

test_x = torch.tensor([[1.0, 4.0], [2.0, 4.0]])
test_y = model(test_x)
true_y = torch.tensor([[1.0 ** 2 + 4.0 ** 2 + 5], [2.0 ** 2 + 4.0 ** 2 + 5]])
print ("正解は", true_y)
print ("予測は", test_y)

前回との違い

model = torch.nn.Linear(in_features = 2, out_features = 1)

ここの部分です。今回は[x1, x2]という二つのインプットを与えているので、 in_features が2 になっています。

実行結果

正解 予想
テストデータ① 22 13.33
テストデータ② 25 17.7
※結果は多少上下します

最後のテストデータに対して、正解と予想が乖離してしまいました。
なぜこれが起きているかというと、「本来は二次関数にもかかわらず、一次関数の形で学習しようとしているから」というのが理由です。
つまり、モデルの表現力が実際の関数に追いついていない、という現状が起きているんです。
これを解決し、さまざまな形の関数でも学習できるように、中間層というものを使ってみましょう。

中間層を使ってみよう

やりたいこと

一つ前の中間層を使わない形では、無理やり一次関数の形に学習しようとして、学習結果がずれてしまいました。
ということでニューラルネットワークの醍醐味、中間層を使います。

中間層ありのコード

# xの二乗の関係を学習させようとしてみる例
# データ数を増やすことで、ある程度正解に近い値が出るようになった
import torch

# y = x1 ** 2 + x2 ** 2 + 5
x = torch.tensor([[1.0, 2.0], [2.0, 3.0], [3.0, 3.0], [4.0, 1.0], [5.0, -3.0], [2.0, 0.0], [0.0, 4.0], [2.0, 1.0], [3.0, 2.0]])
y = torch.tensor([[10.0], [18.0], [23.0], [22.0], [39.0], [9.0], [21.0], [10.0], [18.0]])

model = torch.nn.Sequential(
  torch.nn.Linear(in_features = 2, out_features = 16),
  torch.nn.ReLU(), #活性化関数
  torch.nn.Linear(in_features = 16, out_features = 1),
)

loss_fn = torch.nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr = 0.01)

model.train()

for epoch in range(1000):
	pred_y = model(x)
	loss = loss_fn(pred_y, y)

	optimizer.zero_grad()
	loss.backward()
	optimizer.step()

test_x = torch.tensor([[1.0, 4.0], [2.0, 4.0]])
test_y = model(test_x)
true_y = torch.tensor([[1.0 ** 2 + 4.0 ** 2 + 5], [2.0 ** 2 + 4.0 ** 2 + 5]])
print ("正解は", true_y)
print ("予測は", test_y)

中間層なしとの違い

中間層の設置

model = torch.nn.Sequential(
  torch.nn.Linear(in_features = 2, out_features = 16),
  torch.nn.ReLU(), #活性化関数
  torch.nn.Linear(in_features = 16, out_features = 1),
)

ここの部分がいちばんの違いになります。
今までは線形変換をする Linear 関数一つだけでしたが、その後に16のインプットを持つ中間層を設置しました。
中間層(そして活性化関数)があることによって、ただの一本の線形変換だけではなく、複数(今回は16)の線形変換を合成することができます。

活性化関数とは?

torch.nn.ReLU(), #活性化関数

上記のコードのこの部分が活性化関数と呼ばれる部分です。
活性化関数の役割は、「非線形な関数を表せるようにする」ことです。
詳しいことはこの記事では割愛しますが、「一次関数はいくつ重ねても一次関数になってしまう」 ので、いくら中間層を増やしても、最終的には一次関数しか表せません。

そのために、この中間層にinputとoutputの間で、非線形の処理を入れる必要があり、それを活性化関数と呼びます。

ReLUとは?

この活性化関数は、シグモイド関数やソフトマックス関数などいろいろな種類がありますが、今回はReLU(Rectified Linear Unit)を使っています。

\mathrm{ReLU}(x) = \max(0, x)

このReLU関数、名前はや式を見るとちょっと難しそうなのですが、やっていることとしては0未満を全て0にする ということだけです。
たったこれだけの処理ですが、こういった非線形の関数を噛ませることで、いろいろな関数に対応できるようになるわけですね。

実行結果

正解 予想
テストデータ① 22 22.86
テストデータ② 25 24.53

今回、中間層を入れたことでテストデータに対して正解と予測がかなり近くなりました。
二次関数にもちゃんと対応できています!

終わりに

今回は、中間層、活性化関数を使うことで非線形な関数にも対応させてみました。
次回からはより実践的に、実際のデータを使っての学習をしてみましょう。

Discussion