検索
連載

自分だけのLinearクラスを作ってみよう作って試そう! ディープラーニング工作室(1/2 ページ)

PyTorchが提供するLinearクラスの簡易版を作りながら、全結合型のニューラルネットワークで何が行われるのかを見ていきます。

PC用表示 関連情報
Share
Tweet
LINE
Hatena
「作って試そう! ディープラーニング工作室」のインデックス

連載目次

 前回は、学習によってどのように重みが更新されていくかといった話をしました。今回は、実際にニューラルネットワークの各層にあるノードを全結合し、ある層から別の層へと渡される値を計算するクラスを作ってみます。

 なお、今回のコードはこのリンク先で公開しているので、必要に応じて参照してください。

自分だけのLinearクラスを作ってみる

 PyTorchを使えば、torch.nn.Linearクラスを使うことで、全結合を行う3層構造のニューラルネットワークを例えば次のように書けることは、これまでに何度も見てきました。

from torch import nn

INPUT_FEATURES = 4
HIDDEN = 5
OUTPUT_FEATURES = 1

class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(INPUT_FEATURES, HIDDEN)
        self.fc2 = nn.Linear(HIDDEN, OUTPUT_FEATURES)

    def forward(self, x):
        x = self.fc1(x)
        x = torch.sigmoid(x)
        x = self.fc2(x)
        return x

全結合を行い、あやめの品種を推測するクラスの例

 ここでは、Linearクラスの代わりに使えるMyLinearクラスを作ることで、「self.fc1 = nn.Linear(INPUT_FEATURES, HIDDEN)」のようにしていたところを「self.fc1 = MyLinear(INPUT_FEATURES, HIDDEN)」のように書けるようにすることが目標です。

 といっても難しいことはないので、安心してください。必要なのは、重みとバイアスを格納するインスタンス変数を用意することと、forwardメソッドで入力値と重み、バイアスを使って次のノードに送る値を計算するようにすることの2つだけです。PyTorchが提供するtorch.nn.Moduleクラスと、torch.nn.functional.linear関数を使うことで、とても簡単にこの処理を行うクラスを実装できます。

 それでは、実際にコードを書いていくことにしましょう。

Moduleクラスの派生クラスを定義して重みとバイアスを持たせる

 上でも述べた通り、ここではPyTorchが提供するModuleクラス(torch.nn.Moduleクラス)を利用することで、ニューラルネットワークモジュールが持つ最低限の機能を継承するクラスを定義します。つまり、MyLinearクラスはおおよそ次のような形を取ることになります。

from torch import nn

class MyLinear(nn.Module):
    def __init__(self, in_features, out_features):
        super().__init__()  # 基底クラスの初期化
        # 重みとバイアスを格納するインスタンス変数を定義
    def forward(self, x):
        # ある層からの入力xを受け取り、次の層への出力を計算する
        pass

MyLinearクラスの基本形

 詳しい話は第4回でしていますが、重みは行列として、バイアスはベクトル(一次元配列)として管理することにします(重みとバイアスを1つの行列の要素として扱う方法についても最後に簡単に触れます)。

重みとバイアスを格納するインスタンス変数
重みとバイアスを格納するインスタンス変数

 上の図から分かりますが、ここで重要になるのは、重みを格納する行列の行数が出力先のノードの数(出力値の数)と同じになり、行列の列数が入力側のノードの数(入力値の数)と同じになることです。上のコードでは、in_featuresが入力側のノード数を、out_featuresが出力側のノード数を表しているので、重みを格納する行列は「out_features行、in_features列」の行列となります。バイアスは、出力側のノードの数と同じ要素を持つベクトルとします。

 これをコードに落とし込むと次のようになります。

import torch
from torch import nn
from math import sqrt

class MyLinear(nn.Module):
    def __init__(self, in_features, out_features):
        super().__init__()
        self.in_features = in_features  # 入力値の数を保存
        self.out_features = out_features  # 出力値の数を保存

        # 重みを格納する行列の定義
        k = 1 / in_features
        weight = torch.empty(out_features, in_features).uniform_(-sqrt(k), sqrt(k))
        self.weight = nn.Parameter(weight)

        # バイアスを格納するベクトルの定義
        bias = torch.empty(out_features).uniform_(-k, k)
        self.bias = nn.Parameter(bias)

    def forward(self, x):
        # ある層からの入力xを受け取り、次の層への出力を計算する
        pass

MyLinearクラスに重みとバイアスを持たせる

 このコードでは、torch.emptyメソッドを使って、行列とベクトルを作成しています。このメソッドは、「初期化されていないデータを含んだテンソル(行列、ベクトルなど)」を作成するものです。引数には、作成する行列やベクトルのサイズを指定します。重みを格納する行列については「行数, 列数」の順番で指定することには注意してください。そのため、ここでは「torch.empty(out_features, in_features)」としています(既に述べたように、行列はout_features行、in_features列となるため)。

 その後に付いている「.uniform_(……)」というのは、初期化されていないデータを含んだ行列の初期化を行うためのuniform_メソッド呼び出しです。最後にアンダースコア「_」があることにも注意が必要です。これは、メソッドの呼び出しで使用したテンソルの内容をインプレースで書き換えることを意味しています(PyTorchでは処理対象の内容をインプレースで書き換えるものには、その名前の最後にアンダースコアを付けることで、そのことを示すようになっています)。

 ここでは、PyTorchのLinearクラスと同様な初期化を行うようにしています(k=1/in_featuresとして、-sqrt(k)〜sqrt(k)の範囲から適当な値を使って初期化)。これはバイアスの初期値についても同様です。

 ここまでは、__init__メソッドのローカル変数weightとbiasを使って行列とベクトルを作成していましたが、最後にそれらを使ってtorch.nn.Parameterクラスのインスタンスを作成して、それらをインスタンス変数self.weightとself.biasに代入しています(これは主に1行が長くなるのを避けるため、複数行に分けているだけです)。Parameterクラスのオブジェクトをニューラルネットワークモジュールの属性(インスタンス変数)とすると、それらをparametersメソッドの呼び出しで列挙できるようになります。最適化アルゴリズムの選択の際に、次のようなコードを書いていたことを覚えているでしょうか。

optimizer = torch.optim.SGD(net.parameters(), lr=0.003)

最適化アルゴリズムの選択時には、最適化の対象となるパラメーターを指定する

 このコードでは、Netクラスのインスタンスであるnetオブジェクトに対してparametersメソッドを呼び出していますが、これによりそのニューラルネットワークモデルが持つ重みやバイアスを列挙するジェネレータを取得していました。Parameterクラスを使うと、この列挙されるパラメーターの一覧に、上で作成した重みやバイアスが自動的に追加されるようになっているのです。ここでは、この仕組みを利用することにしています。これを行わなければ、例えば次のような形で重みやバイアスをリスト(やタプル)に格納して、最適化アルゴリズムの選択時に渡すことになるでしょう。

optimizer = torch.optim.SGD([net.fc1.weight, net.fc1.bias, ……], lr=0.003)

最適化アルゴリズムの選択時に、その対象とする重みやバイアスを直接指定する

 以上が__init__メソッドで行っていることです。forwardメソッドのコードは先の話として、MyLinearクラスとLinearクラスのインスタンスを生成して、重みやバイアスが似たようなものになっているかを見てみましょう。

INPUT_FEATURES = 4
HIDDEN = 5
OUTPUT_FEATURES = 1

linear1 = torch.nn.Linear(INPUT_FEATURES, HIDDEN)
linear2 = MyLinear(INPUT_FEATURES, HIDDEN)

print('Linear class')
for param in linear1.parameters():
    print(param)

print('\nMyLinear class')
for param in linear2.parameters():
    print(param)

MyLinearクラスとLinearクラスの重みとバイアスを比較

 実行結果を以下に示します。

実行結果
実行結果

 1つ1つの数値は異なりますが、それらがおよそ-1/2〜1/2の範囲に収まっていること(ここではINPUT_FEATURES=4なので、k=1/in_featuresの値が1/4であり、その平方根が1/2となることに注意)と、重みを格納する行列がどちらも5行4列になっていること(out_features列、in_features列の行列なので、ここでは5行4列となります)、バイアスを格納するベクトルの要素がどちらも5つであることに注目してください。重ねて述べておきますが、これはPyTorchのやり方に従ったものであり、これとは異なる実装方法もあります。

forwardメソッドを実装する

 次に、入力値xを受け取って、重みとバイアスを利用して、次の層に向けて出力する値を計算する処理を、forwardメソッドに書いてみましょう。といっても、実はPyTorchが提供するtorch.nn.functional.linear関数(以下、linear関数)を使えば、ほんの1行で書けてしまいます。

import torch
from torch import nn
from math import sqrt

class MyLinear(nn.Module):
    def __init__(self, in_features, out_features):
        super().__init__()
        self.in_features = in_features  # 入力値の数を保存
        self.out_features = out_features  # 出力値の数を保存

        # 重みを格納する行列の定義
        k = 1 / in_features
        weight = torch.empty(out_features, in_features).uniform_(-sqrt(k), sqrt(k))
        self.weight = nn.Parameter(weight)

        # バイアスを格納するベクトルの定義
        bias = torch.empty(out_features).uniform_(-k, k)
        self.bias = nn.Parameter(bias)

    def forward(self, x):
        return torch.nn.functional.linear(x, self.weight, self.bias)

forwardメソッドの実装

 linear関数は、入力値x、行列self.weight、ベクトルself.biasを受け取り、それに対して、第4回で見たような「出力値=入力値×重み+バイアス」に相当する行列演算を行ってくれます(この詳細についてはまた後ほど見てみます)。

 取りあえずはMyLinearクラスが完成したものとして、実際にこれがPyTorchのLinearクラスと同様に使えるかを試してみましょう。

MyLinearクラスを使ってみる

 ここではあやめの品種を分類するものとして、以下のコードでデータセットを用意します。

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
import torch

iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target)

X_train = torch.from_numpy(X_train).float()
y_train = torch.tensor([[float(x)] for x in y_train])
X_test = torch.from_numpy(X_test).float()
y_test = torch.tensor([[float(x)] for x in y_test])

データセットの準備

 MyLinearクラスを使った、ニューラルネットワーククラスのコードは次のようになります(これまでのNetクラスと違うのは、torch.nn.Linearクラスではなく、MyLinearクラスを使うところだけです)。

INPUT_FEATURES = 4
HIDDEN = 5
OUTPUT_FEATURES = 1

class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = MyLinear(INPUT_FEATURES, HIDDEN)
        self.fc2 = MyLinear(HIDDEN, OUTPUT_FEATURES)

    def forward(self, x):
        x = self.fc1(x)
        x = torch.sigmoid(x)
        x = self.fc2(x)
        return x

MyLinearクラスを使って、全結合を行うNetクラスの定義

 学習をするコードもこれまでに見てきた通りです。

net = Net()

criterion = nn.MSELoss()
optimizer = torch.optim.SGD(net.parameters(), lr=0.003)

EPOCHS = 2000
for epoch in range(EPOCHS):
    optimizer.zero_grad()
    outputs = net(X_train)
    loss = criterion(outputs, y_train)
    loss.backward()
    optimizer.step()
    
    if epoch % 100 == 99:
        print(f'epoch: {epoch+1:4}, loss: {loss.data}')

print('training finished')

MyLinearクラスを使って学習を行うコード

 以上のコードをセルに記述、実行した結果を以下に示します。

実行結果
実行結果

 実行結果を見ると、損失も徐々に0に近づいていることから、MyLinearクラスがうまく動いていることが予想できます。変数outputsには最後の学習で推測した結果が格納されているので、これと教師データとの比較を行うことで、ここではこのニューラルネットワークモデルがどのくらいの精度を持っているかを確認してみましょう(本来は検証まで行うべきですが、ここでは省略します)。といっても、このコードもこれまでの回で紹介してきたものと変わりません(推測結果を四捨五入して、教師データであるy_trainの各要素と比較して、何個がTrueとなったかを数えているだけです)。

predict = (outputs + 0.5).int()
compare = predict == y_train
print(compare.sum())

訓練データを基にした推測結果と教師データとの比較

 実行結果を以下に示します。

実行結果
実行結果

 推測結果と教師データとでは112個中の110個が一致しました。なかなかよい結果となりました。

 というわけで、PyTorchが提供するLinearクラスを簡素化したMyLinearクラスの実装はこれで終わりです。最後にforwardメソッドで行っている処理について、もう少し詳しく見ていくので、興味のある方は読み進めてみてください。

Copyright© Digital Advantage Corp. All Rights Reserved.

       | 次のページへ
[an error occurred while processing this directive]
ページトップに戻る