Pytorch#

PyTorchはPythonのオープンソースの機械学習・深層学習ライブラリです。

  • 柔軟性を重視した設計であり、さらに、機械学習・深層学習モデルをPythonの慣用的なクラスや関数の取り扱い方で実装できるようになっています。

  • GPUを使用した計算をサポートしますので、CPU上で同じ計算を行う場合に比べて、数十倍の高速化を実現します。

#pip install torch torchvision torchaudio
import torch

テンソル#

深層学習モデルは通常、入力から出力にどのようにマッピングされるのかを対応つけるデータ構造を表します。一般的に、このようなある形式のデータから別の形式への変換は膨大な浮動小数点数の計算を通じて実現されています。

データを浮動小数点数を扱うためには、Pytorchは基本的なデータ構造として「テンソル」を導入しています。

深層学習の文脈でのテンソルとは、ベクトルや行列を任意の次元数に一般化したものを指します。つまり、多次元配列を扱います。

テンソルの作成#

x = torch.ones(5, 3)
print(x)
tensor([[1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.]])
x = torch.rand(5, 3)
print(x)
tensor([[2.9278e-04, 8.4019e-01, 2.2296e-01],
        [9.8644e-01, 6.0262e-01, 5.7852e-01],
        [2.0529e-01, 5.6644e-01, 1.4403e-01],
        [7.6983e-01, 9.2523e-01, 2.2322e-01],
        [6.6017e-01, 6.7429e-01, 3.7618e-01]])
x = torch.tensor([5.5, 3])
print(x)
tensor([5.5000, 3.0000])

テンソル要素の型#

テンソル要素の型は、引数に適切なdtypeを渡すことで指定します。

double_points = torch.ones(10, 2, dtype=torch.double)
short_points = torch.tensor([[1, 2], [3, 4]], dtype=torch.short)

テンソルの操作(変形・変換等)#

PyTorchにはテンソルに対する操作(変形・演算など)が多く用意されています。

x = torch.rand(5, 3)
y = torch.rand(5, 3)
print(x + y)
tensor([[1.2884, 1.2699, 0.8674],
        [1.5057, 0.8775, 0.3573],
        [1.1250, 1.0478, 0.5267],
        [1.7638, 1.2110, 1.3942],
        [1.2810, 0.7418, 1.1484]])
print(torch.add(x, y))
tensor([[1.2884, 1.2699, 0.8674],
        [1.5057, 0.8775, 0.3573],
        [1.1250, 1.0478, 0.5267],
        [1.7638, 1.2110, 1.3942],
        [1.2810, 0.7418, 1.1484]])

テンソルの一部指定や取り出し(Indexing)#

Pytorchテンソルは、Numpyや他のPythonの科学計算ライブラリーと同じく、テンソルの次元ごとのレンジインデックス記法で一部指定や取り出しを行えます。

x[3:,:]
tensor([[0.9102, 0.4464, 0.4578],
        [0.6753, 0.5961, 0.8606]])
x[1:,0]
tensor([0.6109, 0.4592, 0.9102, 0.6753])

CUDA Tensors(CUDA テンソル)#

tensorは .to メソッドを使用することであらゆるデバイス上のメモリへと移動させることができます。

if torch.cuda.is_available():
    device = torch.device("cuda")          # a CUDA device object
    y = torch.ones_like(x, device=device)  # directly create a tensor on GPU
    x = x.to(device)                       # or just use strings ``.to("cuda")``
    z = x + y
    print(z)
    print(z.to("cpu", torch.double))       # ``.to`` can also change dtype together!

モデル構築#

torch.nnで用意されているクラス、関数は、独自のニューラルネットワークを構築するために必要な要素を網羅しています。

PyTorchの全てのモジュールは、nn.Moduleを継承しています。

そしてニューラルネットワークは、モジュール自体が他のモジュール(レイヤー)から構成されています。

この入れ子構造により、複雑なアーキテクチャを容易に構築・管理することができます。

クラスの定義#

nn.Moduleを継承し、独自のネットワークモデルを定義し、その後ネットワークのレイヤーを __init__で初期化します。

nn.Module を継承した全モジュールは、入力データの順伝搬関数であるforward関数を持ちます。

from torch import nn
class NeuralNetwork(nn.Module):
    def __init__(self):
        super(NeuralNetwork, self).__init__()
        self.linear_relu_stack = nn.Sequential(
            nn.Linear(512, 128),
            nn.ReLU(),
            nn.Linear(128, 128),
            nn.ReLU(),
            nn.Linear(128, 3),
            nn.ReLU()
        )

    def forward(self, x):
        logits = self.linear_relu_stack(x)
        return logits

このクラスは、PyTorchのnn.Moduleを継承した単純なニューラルネットワークの実装を示しています。入力は固定長の\(512\)とされており、出力は\(3\)の次元を持つベクトルです。

  • self.linear_relu_stack: このシーケンシャルな層は、3つの線形層とそれぞれの後に続くReLU活性化関数から構成されています。

    • linear layerは、線形変換を施します。linear layerは重みとバイアスのパラメータを保持しています。

    • nn.ReLUという活性化関数を設置することで、ニューラルネットワークの表現力を向上させます。

  • 順伝播メソッド (forward): 入力テンソルxを受け取り、ネットワークを通して出力を生成する機能を持ちます。

GPUの利用#

NeuralNetworkクラスのインスタンスを作成し、変数device上に移動させます。

以下でネットワークの構造を出力し確認します。

device = 'cuda' if torch.cuda.is_available() else 'cpu'
print('Using {} device'.format(device))
Using cpu device
model = NeuralNetwork().to(device)
print(model)
NeuralNetwork(
  (linear_relu_stack): Sequential(
    (0): Linear(in_features=512, out_features=128, bias=True)
    (1): ReLU()
    (2): Linear(in_features=128, out_features=128, bias=True)
    (3): ReLU()
    (4): Linear(in_features=128, out_features=3, bias=True)
    (5): ReLU()
  )
)

モデルによる計算#

  • ニューラルネットワークの最後のlinear layerはlogitsを出力します。このlogitsnn.Softmaxモジュールへと渡されます。出力ベクトルの要素の値は\([0, 1]\)の範囲となり、これは各クラスである確率を示します。

X = torch.rand(3, 512, device=device)
logits = model(X) 
print(logits)
tensor([[0.0100, 0.0000, 0.0352],
        [0.0208, 0.0000, 0.0044],
        [0.0000, 0.0000, 0.0000]], grad_fn=<ReluBackward0>)
pred_probab = nn.Softmax(dim=1)(logits)
y_pred = pred_probab.argmax(1)
print(f"Predicted class: {y_pred}")
Predicted class: tensor([2, 0, 0])

自動微分#

ニューラルネットワークを訓練する際、その学習アルゴリズムとして、バックプロパゲーション(back propagation) がよく使用されます。

バックプロパゲーションでは、モデルの重みなどの各パラメータは、損失関数に対するその変数の微分値(勾配)に応じて調整されます。

これらの勾配の値を計算するために、PyTorchにはtorch.autograd という微分エンジンが組み込まれています。

autogradはPyTorchの計算グラフに対する勾配の自動計算を支援します。

シンプルな1レイヤーのネットワークを想定しましょう。

入力をx、パラメータをwb、そして適切な損失関数を決めます。


PyTorchでは例えば以下のように実装します。

import torch

x = torch.rand(5)  # input tensor
y = torch.zeros(3)  # expected output
w = torch.randn(5, 3, requires_grad=True)
b = torch.randn(3, requires_grad=True)
z = torch.matmul(x, w)+b
loss = torch.nn.functional.binary_cross_entropy_with_logits(z, y)

勾配情報の保存#

こののニューラルネットワークでは、wbが最適したいパラメータです。

そのため、これらの変数に対する損失関数の微分値を計算する必要があります。

これらのパラメータで微分を可能にするために、requires_grad属性をこれらのテンソルに追記します。

そうすると、勾配は、テンソルの grad_fn プロパティに格納されます。

print('Gradient function for z =',z.grad_fn)
print('Gradient function for loss =', loss.grad_fn)
Gradient function for z = <AddBackward0 object at 0x1347c42b0>
Gradient function for loss = <BinaryCrossEntropyWithLogitsBackward0 object at 0x1347c69e0>

勾配計算#

ニューラルネットワークの各パラメータを最適化するために、入力xと出力yが与えられたもとで、損失関数の各変数の偏微分値、

すなわち

\(\frac{\partial loss}{\partial w}\)\(\frac{\partial loss}{\partial b}\)

を求める必要があります。

これらの偏微分値を求めるためにloss.backward()を実行し、w.gradb.gradの値を導出します。

逆伝搬では、.backward()がテンソルに対して実行されると、autogradは、

  • 各変数の .grad_fnを計算する

  • 各変数の.grad属性に微分値を代入する

  • 微分の連鎖律を使用して、各leafのテンソルの微分値を求める

を行います。

loss.backward()
print(w.grad)
print(b.grad)
tensor([[0.0518, 0.0413, 0.0437],
        [0.1006, 0.0803, 0.0850],
        [0.2024, 0.1614, 0.1709],
        [0.1848, 0.1474, 0.1561],
        [0.0461, 0.0368, 0.0390]])
tensor([0.3067, 0.2446, 0.2590])

最適化ループを構築し、Pytorchより自動的に逆伝播

import torch.nn.functional as F

def training_loop(n_epochs, learning_rate, model, input, target):
    for epoch in range(1, n_epochs + 1):
        # Forward pass
        outputs = model(input)
        
        # Compute the loss using Binary Cross Entropy with Logits
        loss = F.binary_cross_entropy_with_logits(outputs, target)
        
        # Backward pass
        loss.backward()
        
        # Update the parameters
        with torch.no_grad():
            for param in model.parameters():
                param -= learning_rate * param.grad
        model.zero_grad()
        # Zero the parameter gradients after updating 
        if epoch % 100 == 0:
            print(f"Epoch {epoch}, Loss: {loss.item()}")

    return model
# Example usage (with dummy data)
input = torch.rand(10, 512)  # 10 samples with 512 features each
target = torch.rand(10, 3)  # 10 samples with 3 target values each

n_epochs = 500
learning_rate = 0.01
model = NeuralNetwork()

trained_model = training_loop(n_epochs, learning_rate, model, input, target)
Epoch 100, Loss: 0.6861136555671692
Epoch 200, Loss: 0.6807533502578735
Epoch 300, Loss: 0.675001859664917
Epoch 400, Loss: 0.6688664555549622
Epoch 500, Loss: 0.6622322201728821

Note

PyTorchの勾配計算メカニズムでは、.backwardを呼び出すと、リーフノードで導関数の計算結果が累積されます。つまり、もし.backwardが以前にも呼び出されていた場合、損失関数が再び計算され、.backwardも再び呼び出され、各リーフの勾配が前の反復で計算された結果の上に累積されます。その結果、勾配の値は誤ったものになります。

このようなことが起こらないようにするためには、反復のルーブのたびにmodel.zero_grad()を用いて明示的に勾配をゼロに設定する必要があります。

最適化関数#

最適化は各訓練ステップにおいてモデルの誤差を小さくなるように、モデルパラメータを調整するプロセスです。

ここまでの説明は、単純な勾配下降法を最適化に使用しました。これは、シンプルなケースでは問題なく機能しますが、モデルが複雑になったときのために、パラメータ学習の収束を助ける最適化の工夫が必要されます。

Optimizer#

optimというモジュールには、様々な最適化アルゴリズムが実装されています。

ここでは、確率的勾配降下法(Stochastic Gradient Descent)を例として使い方を説明します。

確率的勾配降下法は、ランダムに選んだ1つのデータのみで勾配を計算してパラメータを更新し、データの数だけ繰り返す方法です。

訓練したいモデルパラメータをoptimizerに登録し、合わせて学習率をハイパーパラメータとして渡すことで初期化を行います。訓練ループ内で、最適化(optimization)は3つのステップから構成されます。

  • optimizer.zero_grad()を実行し、モデルパラメータの勾配をリセットします。勾配の計算は蓄積されていくので、毎イテレーション、明示的にリセットします。

  • 続いて、loss.backwards()を実行し、バックプロパゲーションを実行します。PyTorchは損失に対する各パラメータの偏微分の値(勾配)を求めます。

  • 最後に、optimizer.step()を実行し、各パラメータの勾配を使用してパラメータの値を調整します。

optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
def training_loop(n_epochs, learning_rate, model, input, target):
    # Use Binary Cross Entropy with Logits as the loss function
    
    # Use Adam as the optimizer
    optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
    
    for epoch in range(1, n_epochs + 1):
        # Zero the parameter gradients
        optimizer.zero_grad()
        
        # Forward pass
        outputs = model(input)
        loss = F.binary_cross_entropy_with_logits(outputs, target)
        
        # Backward pass and optimize
        loss.backward()
        optimizer.step()

        # Print loss every 100 epochs
        if epoch % 100 == 0:
            print(f"Epoch {epoch}, Loss: {loss.item()}")

    return model
input = torch.rand(10, 512)  # 10 samples with 512 features each
target = torch.rand(10, 3)  # 10 samples with 3 target values each

n_epochs = 1000
learning_rate = 0.001
model = NeuralNetwork()

trained_model = training_loop(n_epochs, learning_rate, model, input, target)
Epoch 100, Loss: 0.6924331784248352
Epoch 200, Loss: 0.6914722323417664
Epoch 300, Loss: 0.6906877160072327
Epoch 400, Loss: 0.6899754405021667
Epoch 500, Loss: 0.689291775226593
Epoch 600, Loss: 0.6886290311813354
Epoch 700, Loss: 0.6879986524581909
Epoch 800, Loss: 0.6874132752418518
Epoch 900, Loss: 0.6868129968643188
Epoch 1000, Loss: 0.6862021684646606

実装例(Irisデータ)#

データの読み込み#

Irisデータセットは、アイリス花の3つの異なる種類(Setosa、Versicolour、Virginica)の各50サンプルからなるデータセットです。各サンプルには、以下の4つの特徴値(特徴量)があります。

  • がく片の長さ (sepal length):アイリス花のがく(緑色の部分)の長さをセンチメートルで測定したもの。

  • がく片の幅 (sepal width):がくの幅をセンチメートルで測定したもの。

  • 花びらの長さ (petal length):アイリス花の花びらの長さをセンチメートルで測定したもの。

  • 花びらの幅 (petal width):花びらの幅をセンチメートルで測定したもの。

これらの特徴値を使用して、アイリス花の3つの異なる種類を分類することが目標となっています。つまり、目標値(またはラベル)は以下の3つのクラスのいずれかです:

  • Setosa

  • Versicolour

  • Virginica このデータセットは、分類アルゴリズムを評価するための基準としてよく使用されます。

from tensorboardX import SummaryWriter
from sklearn import datasets
from sklearn.model_selection import train_test_split
import seaborn as sns
import matplotlib.pyplot as plt

# Load dataset and create splits
iris_dataset = datasets.load_iris()
# Load the iris dataset
iris = datasets.load_iris()
data = iris.data
target = iris.target
feature_names = iris.feature_names
target_names = iris.target_names

# Create a DataFrame for easier plotting
import pandas as pd
df = pd.DataFrame(data, columns=feature_names)
df['species'] = [target_names[t] for t in target]

# Set a publication-ready theme and increase font scale for better readability
sns.set_theme(style="whitegrid", font_scale=1.2)

# Pair plot to visualize relationships
plt.figure(figsize=(10, 8))
sns.pairplot(df, hue="species", palette="muted", height=2.5, aspect=1.2, plot_kws={'s': 50})
plt.suptitle('Iris Dataset Feature Relationships', y=1.02)

plt.show()
<Figure size 1000x800 with 0 Axes>
../_images/pytorch_52_1.png

学習データの作成#

  • データセットを訓練データ、検証データ、テストデータに分割します。

x_tmp, xtest, y_tmp, ytest = train_test_split(iris_dataset.data, iris_dataset.target, test_size=0.2)
xtrain, xval, ytrain, yval = train_test_split(x_tmp, y_tmp, test_size=0.25)  # 0.25 x 0.8 = 0.2 -> 20% validation
  • torch.from_numpy関数で、NumPy配列をPyTorchのテンソルに変換します。

    • ニューラルネットワークに入力される特徴量やパラメータは浮動小数点数型である必要があるため、.float()メソッドは、テンソルのデータ型を浮動小数点数型に変換します。

    • 分類タスクのラベル(目的変数)は通常整数型で表されるため、.long()メソッドでテンソルのデータ型を長整数型に変換します。

  • .to(device): データをGPUに移行します。

これにより、データをPyTorchでのモデル訓練や評価に使用する準備が整います。

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
xtrain = torch.from_numpy(xtrain).float().to(device)
ytrain = torch.from_numpy(ytrain).long().to(device)
xval = torch.from_numpy(xval).float().to(device)
yval = torch.from_numpy(yval).long().to(device)
xtest = torch.from_numpy(xtest).float().to(device)
ytest = torch.from_numpy(ytest).long().to(device)

モデルの作成#

ニューラルネットワークモデルを定義します。

  • class NeuralNetwork(nn.Module)という新しいクラスを定義しています。このクラスはnn.Moduleを継承しているので、モデル関連の機能(例:重みの管理、GPU対応、保存と読み込みの機能など)を利用できるようになります。

  • def __init__(self, n_in, n_units, n_out): ネットワークをインスタンス化するときに呼び出され、初期値をモデルに渡します。

    • n_in: 入力層のユニット(ニューロン)の数

    • n_units: 隠れ層のユニットの数

    • n_out: 出力層のユニットの数

    • super(NeuralNetwork, self).__init__(): 親クラスであるnn.Moduleのコンストラクタを呼び出すことで、モデル関連の機能を初期化しています。

    • self.l1 = nn.Linear(n_in, n_units): 入力層から隠れ層への線形変換(全結合層)を定義しています。

    • self.l2 = nn.Linear(n_units, n_out): 隠れ層から出力層への線形変換を定義しています。

  • def forward(self, x):モデルが入力データを受け取ったときの順伝播を定義するメソッドです。

    • h = F.relu(self.l1(x)): 入力\(x\)self.l1レイヤー(全結合層)で変換した後、ReLU活性化関数を適用しています。

    • y = self.l2(h): \(h\)(隠れ層の出力)をself.l2レイヤーで変換して、ネットワークの最終出力yを生成しています。

class NeuralNetwork(nn.Module):
    def __init__(self, n_in, n_units, n_out):
        super(NeuralNetwork, self).__init__()
        self.l1 = nn.Linear(n_in, n_units)
        self.l2 = nn.Linear(n_units, n_out)

    def forward(self, x):
        h = F.relu(self.l1(x))
        y = self.l2(h)
        return y

学習の設定#

学習に関連するハイパーパラメータを設定します。

  • model = NeuralNetwork(n_in, n_units, n_out):指定された入力層、隠れ層、出力層のユニット数でモデルのインスタンスを作成しています。

  • loss_function = nn.CrossEntropyLoss(): 損失関数を定義します。

  • optimizer = torch.optim.Adam(model.parameters(), lr=0.01): オプティマイザを指定します。

  • model.to(device): モデルをGPUに移行します。データとモデルは必ず同じdeviceに置く必要があります。

n_in = xtrain.shape[1]  # number of input features (4 for Iris dataset)
n_units = 10  # number of units in the hidden layer
n_out = 3  # number of classes in the Iris dataset
model = NeuralNetwork(n_in, n_units, n_out)
model = model.to(device)  # Move the model to GPU
loss_function = nn.CrossEntropyLoss() # 分類問題のため交差エントロピー誤差を使用
optimizer = torch.optim.Adam(model.parameters(), lr=0.01) # optimizer
# Training loop
n_epochs = 100

学習ループの実装#

ニューラルネットワークモデルの学習と検証のループを実装します。

  • model.train()model.eval():モデルが訓練モードか評価モードかを切り替えます。

  • outputs = model(xtrain): 訓練データxtrainをモデルに渡し、出力を取得します。

  • loss = loss_function(outputs, ytrain): モデルの出力と実際のラベルytrainとの間の損失を計算します。

  • optimizer.zero_grad(): 新しいエポックの勾配計算の前に、最適化器の勾配を初期化します。

  • loss.backward(): 損失に基づいて、モデルのすべてのパラメータの勾配を計算します。

  • optimizer.step(): 計算された勾配を使用して、モデルのパラメータを更新します。

  • with torch.no_grad(): 勾配の計算を無効化します。これは、評価フェーズでは勾配を計算する必要がないため、計算の効率を向上させるためです。

  • val_outputs = model(xval): 検証データxvalをモデルに渡し、出力を取得します。

  • _, val_predicted = torch.max(val_outputs, 1): モデルの出力から、最も高い確率を持つクラスのインデックスを取得します。

  • val_accuracy = (val_predicted == yval).float().mean().item(): 検証データに対する正解率を計算します。

for epoch in range(n_epochs):
    # Training phase
    model.train()
    outputs = model(xtrain)
    loss = loss_function(outputs, ytrain)
    
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    
    # Validation phase
    model.eval()
    with torch.no_grad():
        val_outputs = model(xval)
        _, val_predicted = torch.max(val_outputs, 1)
        val_accuracy = (val_predicted == yval).float().mean().item()

    # Print losses and accuracies every 10 epochs
    if (epoch+1) % 10 == 0:
        print(f'Epoch [{epoch+1}/{n_epochs}], Loss: {loss.item():.4f}, Val Accuracy: {val_accuracy*100:.2f}%')
Epoch [10/100], Loss: 1.0728, Val Accuracy: 33.33%
Epoch [20/100], Loss: 0.8631, Val Accuracy: 73.33%
Epoch [30/100], Loss: 0.7060, Val Accuracy: 66.67%
Epoch [40/100], Loss: 0.5851, Val Accuracy: 86.67%
Epoch [50/100], Loss: 0.4890, Val Accuracy: 96.67%
Epoch [60/100], Loss: 0.4098, Val Accuracy: 96.67%
Epoch [70/100], Loss: 0.3424, Val Accuracy: 96.67%
Epoch [80/100], Loss: 0.2849, Val Accuracy: 96.67%
Epoch [90/100], Loss: 0.2375, Val Accuracy: 96.67%
Epoch [100/100], Loss: 0.1995, Val Accuracy: 96.67%

モデルの検証#

テストデータでモデルの有効性を検証します。

model.eval()
with torch.no_grad():
    test_outputs = model(xtest)
    _, test_predicted = torch.max(test_outputs, 1)
    test_accuracy = (test_predicted == ytest).float().mean().item()
    print(f'Test Accuracy: {test_accuracy*100:.2f}%')
Test Accuracy: 100.00%

class NeuralNetwork(nn.Module)を以下のように改装して、改めて学習を行なってください。

  • 一つ隠れ層を追加し、n_units_2という引数でユニットの数を指定できるように設定しなさい。

  • すべての隠れ層にF.reluで活性化関数を追加しなさい。

  • 出力層にF.log_softmaxで出力の正規化を行きなさい。

sepal length, sepal width, petal lengthでpetal widthを予測するニューラルネットワークを構築、学習してください。

  • 二つ以上の隠れ層を設定する。

  • 学習の際、検証データで損失を計算し、20 epochごとに示す。

  • 学習済みのモデルを用いて、テストデータに対する予測を行う。