本記事ではPyTorchの基本的な使い方について簡単なデータセットを用いて体験してもらいます。これからPyTorchについて学ぼうされていた方や改めて基本的なAPIの使い方を学び直したいという方が対象です。そのため、すでにPyTorchを習熟されている方にとっては知っていることが多い場合もあるのでご了承ください。
本記事の構成は下記の通りです。
Contents
PyTorchとは
PyTorchはディープラーニングのプログラミングにおいてよく用いられるPythonのオープンソースライブラリ(公開ライブラリ)です。PyTorchはPythonで非常に使いやすいようように設計されている上、PyTorchにはディープラーニング用の便利な機能が多数存在しており、ディープラーニングを実装する上でPyTorchについて理解しておくことはほぼ必須であると言えるでしょう。今回はPyTorchの基本的な機能について、実際にコードで実装しながら簡単に説明していきます。
テンソル
まず、PyTorchにおける基本的なデータ構造であるテンソル(tensor)について説明します。PyTorchで用いるデータは基本的にこのtensorなので、tensorは非常に重要であると言えます。数学にもテンソルというものは存在しますが、PyTorchにおけるtensorとは簡単に言えば多次元配列のことです。Pythonにおける多次元配列ではNumPyライブラリのndarrayがよく使われますが、PyTorchのtensorはディープラーニングにおいて便利な性質をたくさんもっています。
実際にPyTorchのtensorデータを作成してみましょう。
PyTorchのtensor(torch.tensor)に渡すことのできるデータ型は、int16型、float32型など様々です。上のコードではfloat32型のデータを渡しています。PyTorchでは、パラメータのデータ型にfloat32型を用いることが多いため他の数値型を渡して計算させるとエラーになる場合があります。基本的にはtorch.tensorに渡すデータはfloat32型にしましょう。なお、torch.tensorに渡すデータ型は以下のようにdtypeを指定することで渡した後に変換することも可能です。
なお、ディープラーニングはGPU(Graphics Processing Unit)上で実行することが多いです。GPUは画面のグラフィックス描画に特化したプロセッサ(処理装置)で、並列処理に優れています。普通のパソコンに搭載されているのはGPUではなく、CPU(Central Processing Unit)です。CPUはパソコンの頭脳に当たり、様々な処理を行います。この画面を見ているあなたのパソコンにもおそらくCPUが搭載されています。ディープラーニングはGPU上で実行する方が圧倒的に高速に処理できるため、一般的にはGPUが用いられます。torch.tensorにはこのGPUに対応するための機能が用意されており、以下のようにtorch.tensorの属性変数deviceを指定することでGPUに対応させることができます。
x = torch.tensor([[3.0,2.0],[5.0,6.0]] , device = 'cuda')
GPUが使えない場合は、このようにdeviceにGPUを指定するとエラーが出ます。今回はGPUを使うほど複雑なことはしないので、GPUは使いません。
また、torch.tensorにはNumPyのndarrayとの互換性もあります。以下のように、torch.tensorをnumpy.ndarrayに変換することが可能です。
逆にnumpy.ndarrayをtorch.tensorに変換することも可能です。
ここで、PyTorchにおける通常のデータ型はfloat32型であるのに対し、NumPyにおける通常のデータ型はfloat64型であることに注意してください。上のコードでは、変換後のdtypeがfloat64型になってしまっています。そのため、いったん array = array.astype(np.float32)
のようにnp.ndarrayのデータ型をfloat32型に変換してからtorch.tensorに変換する処理を行うとよいでしょう。
PyTorchのtensorに関する基本的な説明は以上になります。他にもtorch.tensorにはディープラーニングにおいて重要な属性変数がありますが、それはこの後追々紹介します。
PyTorchを用いたXORゲートの実装
ここまでの内容は少し退屈だったかもしれませんが、ここからはPyTorchを用いて簡単なニューラルネットワークモデルを作成していくので楽しくなってくると思います。今回はニューラルネットワークに学習させて、XORゲートを実装することを目指します。XORゲートは論理回路の一つで、入力と出力の表は以下のようになっています。
表1をよく見ると、入力値を「0を出力させるもの」と「1を出力させるもの」に分けることができるとわかります。0を出力させるものは[0, 0]と[1, 1]で、1を出力させるものは[0, 1]と[1, 0]です。入力の組み合わせを[x1, x2]に対応させて横軸をx1、縦軸をx2として図示すると以下のようになります。
このデータは以下の図2を見てわかる通り一本の直線では分離できず、曲線でなら分離できます。
よってXORゲートを構築するということはすなわち、非線形モデルを構築するということになります。ニューラルネットワークを用いれば、このような非線形モデルは容易に構築することが可能です。それでは実際にニューラルネットワークモデルを作成し、学習させることでXORゲートを実装していきましょう。
最初に深層学習の重要な要素をいくつか説明してから、全体としてのモデルを実装する流れになります。
モデルの構築
まず、モデルの構築方法を説明します。線形なモデルをPyTorchを用いずに簡単に関数として書いてみると以下のようになります。
def linear_model(x,w,b):
return w * x + b
これは入力値x,重みw,バイアスbを受け取った結果入力値xの線形変換した値を返すだけのモデルです。このような線形変換は、PyTorchにおいてはnnモジュールを用いることで全く同じ処理を実装できます。nnモジュールはニューラルネットワーク専用のモジュールです。
Linearはnnモジュールのサブモジュール(nnモジュールに内蔵されている機能の一つ)で、線形モデルを定義してくれます。引数には入力値のサイズ、出力値のサイズ、バイアスを用いるかのbool値を受け取ります。バイアスを用いるかはデフォルトではTrueに設定されています。入力値、出力値はもちろんtensor型です。以降も、PyTorchでは配列はほぼ全てtorch.tensorであることに注意してください。
なお、重みとバイアスの値もそれぞれ確認することができます。重み、バイアスの初期値はランダムに設定されます。今回は入出力ともに1次元なので重み、バイアスともに一つだけですが、それぞれの個数はnn.Linear()に渡す入出力の次元に応じて自動的に変わります。
出力の数字の横にrequires_grad = Trueという文字がありますが、これについては後に解説するので今は気にしなくて大丈夫です。
linear_modelに入力値を与えると、線形変換した結果の出力値が得られます。
grad_fn = AddBackward0のような文字が数値の横にあると思いますが、これも後に説明するのでまだ気にしなくて大丈夫です。ここで定義したモデルは一層の線形モデルです。しかし、ディープラーニングでは多層のニューラルネットワークを構築します。多層のニューラルネットワークを構築するにはnn.Sequentialを用います。
nn.Sequentialはいくつかのサブモジュールをまとめて連結したモデルにすることができます。ここでは、例として2入力1出力の線形モデルを定義し、その出力をシグモイド関数に渡す一層のニューラルネットワークを定義しました(ニューラルネットワークは線形変換と非線形変換の組み合わせで一層ということに注意してください)。このSequentialモデルに入力された値はまずnn.Linearに渡され、そのnn.Linearの出力値がnn.Sigmoidに渡され、そのnn.Sigmoidの出力が最終的に返されます。今回は一層だけでしたが、もっと層を深くすることもできます。
なお、Sequentialモデルに値を入力して出力を得るのはnn.Sequentialのforwardメソッドで実行できます。
では、今回実装するXORゲート用のモデルを定義します。XORゲートは一層だけで実装するのは難しいので、層を増やしてモデルの表現能力を向上させてあげる必要があります。以下のモデルの一層目では、入力の特徴量二つから八つの特徴量を生成しています。このような、中間の層(隠れ層)における特徴量を隠れ特徴量などと呼びます。
このモデルを図で表すと以下のようになります。
入力層が二つのニューロン、中間層が8つのニューロン、出力層が1つのニューロンから構成されています。これでモデルを定義することができました。次に、損失関数を定義していきます。
損失関数
損失関数は、モデルによって予測された値と実際に測定された値(真の値)とを比較し、その誤差(損失)を数値として返します。この損失が小さいほどモデルの性能が良いということなので、重みやバイアスなどのパラメータはこの損失を小さくするように更新していきます。
損失関数には様々なものがありますが、平均二乗誤差(MSE)や交差エントロピー損失(BCE)などが有名です。今回は交差エントロピー損失(BCE)を用いてみます。交差エントロピー損失は式で表すと非常に難しく、数学的な知識が必要とされますが、実はPyTorchのnnモジュールには損失関数があらかじめいくつか備わっています。交差エントロピー損失は以下のようにオブジェクトとして生成することで簡単に定義することができます。
ここでは、nnモジュールに備わっていたBCELossというサブクラスのオブジェクトとして損失関数を定義しました。次に、パラメータ更新について説明していきます。
パラメータ更新(勾配降下法、オプティマイザ)
損失関数によって誤差を計算した後、その誤差を用いてパラメータを更新します。よって損失関数が最小になるようにパラメータを更新していくことになりますが、そこでは勾配降下法と呼ばれる手法がよく使われます。例えば、損失関数が以下のような簡単な2次関数(縦軸は損失、横軸はパラメータ)だった場合を考えてみます。
この損失関数の最小値はグラフからもわかる通り、パラメータ軸が0のときで、0です。以下の図4の赤い点を考えたとき、その点における損失はまだ最小ではありません。
そこで、その点における勾配(傾き)を計算し、パラメータを更新することで、その勾配に沿ってより小さい方へ点を動かします。
この更新を、縦軸の損失を、横軸のパラメータを、学習率をとし、式で表すと以下のようになります。勾配の計算は、数学的には微分という手法を用いています。また、学習率は更新の度合いを調整するハイパーパラメータです(ハイパーパラメータとは、モデルが学習を通して調整するのではなく人間が調整するパラメータのことです)。
これを繰り返すことで、最終的には勾配が0の点、すなわち最小値にたどり着くことができます。ボールが坂を転がっていき、最終的には平坦な場所で止まることを想像してもらえるとなんとなく理解できるかと思います。このように、勾配を計算し、損失が小さくなるようにパラメータを更新していく手法を勾配降下法と呼びます。
なお、この勾配はモデルの出力側から入力側に向かって、各パラメータについて計算されます。こうすることで、モデルの全てのパラメータを更新することができます。このように、出力側から入力側へ、逆向きに損失の勾配を計算していく手法を誤差逆伝播法と呼びます。
実は、PyTorchには誤差逆伝播法で勾配を自動で計算する仕組みが備わっています。この仕組みを使うためには、以下のようにあらかじめtorch.tensorのrequires_grad属性変数をTrueにしておく必要があります。requires_gradがfalseだと勾配を計算してくれないので注意しましょう。
ここでは、grad属性がTrueのtensorであるx, w, bを作成しました。wを重み、bをバイアスとしてxに線形変換をした結果をyとして出力してみましょうしょう。
出力した結果、計算したyにもgrad_fn=AddBackwardのように勾配についての情報が反映されていることがわかります。実は、PyTorchには計算の流れをグラフとして記憶する機能があり、その情報がgrad_fnという属性変数に保持されているのです。ここでは、以下の図6のような計算グラフが記憶されています。
さらに、出力されたtorch.tensorのbackwardメソッドを実行することで、PyTorchはrequires_gradをTrueに設定したtensorに対して、出力側から入力側に向かって記憶された計算の逆向きに自動で勾配を計算してくれます。backwardメソッドで勾配を計算すると、計算された勾配の値は各tensorのgrad属性変数に保存されます。そのため、計算された勾配の値はtensor.gradで確認できます。
y=w * x + bのwに対する勾配を計算した結果、2.0と求まりました。
ここで行われている処理は図で表すと以下のようになります。
このように、勾配を計算したいtensorのrequires_grad = Trueとしておくと、backwardメソッドを実行したときにPyTorchはそのtensorに対する式の勾配を自動で計算してくれます。基本的に全てのtorch.tensorは属性変数gradを持っています。
勾配が求まったので後は勾配降下法でパラメータを更新するだけだと思いきや、実はこの勾配降下法には弱点があります。もしも損失関数が以下の図8のような関数だったらどうでしょうか。
この図8のような場合、以下の図9におけるピンク色の点を最小値だと勘違いしてしまうかもしれません。しかし、見てわかる通り実際の最小値とは異なっています。
このような偽物の最小値を「局所解」といいます。局所解に陥ると、勾配降下法ではそこを最小値と判定してそれ以上パラメータを更新しなくなってしまいます。ボールが坂を転がっていく途中でくぼみにはまってうごけなくなってしまうイメージです。これは、勾配降下法が勾配が0になるのを目指してパラメータを更新するアルゴリズムであるために発生する問題です。
そこでPyTorchには、このような局所解に陥るのを防ぐことができるパラメータ更新アルゴリズムを選ぶことができる機能が備わっています。この、パラメータをどのように更新するかを決めるものをオプティマイザと言います。 オプティマイザは以下の図10のように、モデルのパラメータにアクセスしてgrad属性変数に保存されている勾配の値を受け取った後、受け取った勾配の値に基づいてパラメータの更新量を計算し、計算した結果をモデルに返してパラメータを更新してくれます。
PyTorchでは以下のように、オブジェクトとして生成したオプティマイザにはあらかじめモデルのパラメータを全て渡して置きます。そうすると、後でパラメータを更新するときにオプティマイザは更新量を計算してその値をモデルに反映してくれます。オプティマイザは確率的勾配降下法(SGD)、Adagrad、Adam等多数考案されていますが、その中でも今回は確率的勾配降下法(SGD)を用います。
import torch
import torch.nn as nn
model = nn.Sequential(
nn.Linear(2,8),
nn.ReLU(),
nn.Linear(8,1),
nn.Sigmoid()
)
#モデルのパラメータを全て取得
params = model.parameters()
#学習率を定義
learning_rate = 0.1
#オプティマイザをオブジェクトとして生成(引数にはパラメータ、学習率を渡す)
optimizer = torch.optim.SGD(params , lr = learning_rate)
これでオプティマイザが定義できました。
モデルに学習させる
ここまでで、学習に必要なモデルの要素は全て揃いました。あとはデータを用意し、実際にモデルに学習させるコードを実装するだけです。
一般的な深層学習の流れは、
- 訓練用データ、試験用データを用意
- モデルを用意
- モデルに訓練用データで学習させる
- 試験用データで推論
となります。これに沿って進めていきます。
では、まず訓練用データと試験用データを用意します。今回はXORゲートと同じ機能になるように学習させたいので、訓練用データは以下の表と同じデータを用意します。
すなわち、
import torch
X=torch.tensor([[0.0, 0.0],[0.0, 1.0],[1.0, 0.0],[1.0, 1.0]] , requires_grad = True )#データ
y=torch.tensor([[0.0],[1.0],[1.0],[0.0]])#ラベル
となります。Xは入力データで、yはその各データに対応するラベル(出力値)です。
ところで、データは学習に用いる訓練用データの他にモデルの精度を評価するための試験用データも用意するのが一般的です。なぜなら、モデルには汎化性(新しいデータに対して高い精度を出すこと)が求められるからです。学習に用いた訓練用データに対し高い精度が出せるのはほとんど当たり前と言ってよく、学習に用いてない新しいデータに対しても高い精度を出せるかが重要です。
しかし今回の目的はXORゲートを実装することなので特に試験用データは用意せず、推論では学習させたモデルに [0, 0] , [0, 1] , [1, 0] , [1, 1]を代入してみて、それぞれに対応する出力が得られるかを確認することにします。
次に、モデルを用意します。今までに説明してきた通りです。
import torch
import torch.nn as nn
#モデルを定義
model = nn.Sequential(
nn.Linear(2,8),
nn.ReLU(),
nn.Linear(8,1),
nn.Sigmoid()
)
#損失関数を定義
loss_fn = nn.BCELoss()
#オプティマイザを定義(引数にはパラメータ、学習率を渡す)
optimizer = torch.optim.SGD(params = model.parameters() , lr = 0.1)
では、いよいよモデルに訓練させます。モデルにデータを入力し、損失を計算し、パラメータを更新する一連の流れをエポックと呼びます。今回は2000エポックほど学習させます。
以下の「訓練ループ」の2行目でエポック毎に勾配を0に初期化している点に注意してください。勾配からパラメータを更新するのはよいのですが、更新した後も勾配の値はそのままです。PyTorchの仕様として、勾配を計算した後に前のエポックで計算した勾配が残っていたらそれに加算するようになっています(更新に使う勾配 = 前のエポックで計算した勾配 + 今回のエポックで計算した勾配)。そのため、もし勾配を0に初期化しないと勾配の値がどんどん増え続けてしまい、パラメータが正しく更新されません。したがって、optimizer.zero_grad()
でエポック毎に勾配を0に初期化してあげる必要があります。ここで初期化しているのはパラメータに対する勾配であって、パラメータ自体を初期化しているわけではないことに注意してください。
また、訓練ループ3行目で順伝播処理model.forward(X)
によりモデルの出力を得た後、訓練ループ4行目でその出力と正解ラベルの損失を計算します。そして訓練ループ5行目で、逆伝播loss.backward()
によりその誤差に基づいた勾配を計算し、6行目でオプティマイザがパラメータの更新量を計算してモデルに返し、パラメータを更新します。
100エポック毎の損失の値が表示されていますが、損失はどんどん減少していっているのが分かると思います。損失は少ないほどよいので、学習が上手く進んでいるということです。
この損失の減り具合をグラフとして可視化してみましょう。
グラフを見ても、損失が減少していっているのがわかります。
では、学習が完了したので推論をさせてみましょう。学習後のモデルに[0, 0] , [0, 1] , [1, 0] , [1, 1]という組み合わせを入力してみます。
得られた出力をXORゲートの出力と見比べてみましょう。
見比べてみると、確かにほぼ同じ値が出力されているのがわかります。上手くモデルに学習させ、XORゲートを実装することができたようです。以上が、PyTorchを用いたディープラーニングの実装になります。
オブジェクト指向でXORゲートを実装する
ここまでは、オブジェクト指向プログラミングをせずにニューラルネットワークを構築しました。しかしPyTorchを用いる場合、オブジェクト指向プログラミングでニューラルネットワークを構築するのが一般的です。
オブジェクト指向とは
オブジェクト指向プログラミングを簡単に説明すると、「何らかの役割をもったものごとにクラスを分け、クラス間のやり取りでプログラムを実装する」となります。オブジェクト指向によるプログラムの実装では、より柔軟性の高い処理が行えるようになる上、クラス単位で処理を分割するため共同作業がしやすくなるというメリットがあります。 PyTorchではnn.Moduleを継承したクラスを作成することで、オブジェクト指向によるディープラーニングの実装を行う形になります。
オブジェクト指向による実装
では、XORゲートをオブジェクト指向プログラミングで実装してみましょう。基本的なディープラーニングの流れはオブジェクト指向をしない場合と全く同じです。まず、モデルを実装します。大きな変更点はここだけで、後はオブジェクト指向をしない場合とほとんど同じです。
import torch
import torch.nn as nn
#クラスとしてモデルを定義
class Model(nn.Module):
def __init__(self): #コンストラクタ内で各モジュールを定義
super().__init__()
self.fc1 = torch.nn.Linear(2, 8)
self.relu = torch.nn.ReLU()
self.fc2 = torch.nn.Linear(8, 1)
self.sigmoid = torch.nn.Sigmoid()
def forward(self, x): #順伝播処理を行うメソッド
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
x = self.sigmoid(x)
return x
ここでは、nn.Moduleを継承したサブクラスとしてニューラルネットワークモデルを構築しています。オブジェクト指向でない場合はnn.Linearなどのサブモジュールをnn.Sequentialでまとめてモデルを構築していましたが、オブジェクト指向でコードを書く場合はサブモジュールをコンストラクタ内でインスタンスとして定義します。また、それらのインスタンスをforwardメソッドで呼び出す、という形になります。
Modelクラスから生成したインスタンスにデータが渡されると自動的にforwardメソッドが実行されます。以下ではmodel_objectというインスタンスにXを入力した結果を出力として損失関数に代入していることに注目してください。
では、推論させてみましょう。
上手く学習できているようです。モデルの構造、エポック数、損失関数、オプティマイザはオブジェクト指向で書かなかったときと全く同じなので推論結果もオブジェクト指向で書かない場合とほぼ同じになります。
まとめ
本記事ではPyTorchの基礎的な使い方についておおまかに説明しました。今回説明したことは
- PyTorchの基本的なデータ構造であるテンソル
- ディープラーニングの重要な要素
- PyTorchを用いた簡単なディープラーニングの実装
- PyTorchを用いたオブジェクト指向による実装
など、重要なものばかりです。内容も非常に多かったのではないかと思うので、よく復習しておくとよいでしょう。
PyTorchチュートリアル Part2 を公開!
本記事の続編を公開しました!!複雑になったデータを分類するニューラルネットワークの威力を体験できると思います。また、新たなテクニックも紹介しているのでぜひ見てもらえると嬉しいです。
参考文献
- Eli stevens, Luca Antiga, Thomas Viehmann 『Pytorch実践入門 ディープラーニングの基礎から実装へ』後藤勇輝, 小川雄太郎, 櫻井亮佑, 大串和正 訳. 株式会社マイナビ. 2021
- PYTORCH DOCUMENTATION , https://pytorch.org/docs/stable/index.html
- Learning XOR with PyTorch , https://medium.com/mlearning-ai/learning-xor-with-pytorch-c1c11d67ba8e , Jake Wherlock