開発環境
- Python 3.11.10
- PyTorch 2.3.0
この記事では、PyTorchフレームワークを用いて、シンプルな全結合ニューラルネットワークを構築・学習・評価するプロセスを解説します。MNIST手書き数字認識という古典的なタスクを通じて、深層学習の基礎概念を実践的に理解することを目指します。
PyTorchの概要
PyTorchは、コンピュータビジョンや自然言語処理などの分野で広く利用されているオープンソースの機械学習ライブラリです。Meta(旧Facebook)のAI研究チームによって開発され、柔軟性と高速性の両立により研究者や業界から高い評価を得ています。
ネットワークアーキテクチャの設計
今回のモデルは比較的シンプルな構造を採用しています。128個のユニットを持つ隠れ層を2つ備え、10クラス分類に対応する出力層を設けます。各隠れ層にはReLU活性化関数を配置し、非線形性を導入することで複雑なパターンの学習を可能にします。
import torch
import torch.nn as nn
class DigitClassifier(nn.Module):
def __init__(self):
super(DigitClassifier, self).__init__()
self.hidden1 = nn.Linear(784, 128)
self.hidden2 = nn.Linear(128, 128)
self.classifier = nn.Linear(128, 10)
def forward(self, input_tensor):
output = torch.relu(self.hidden1(input_tensor))
output = torch.relu(self.hidden2(output))
output = self.classifier(output)
return output
上記コードは、784次元の入力特徴量(28×28ピクセルの画像をフラット化したもの)を受け取り、2つの隠れ層を経由して10クラスの分類結果を出力する全結合ネットワークを定義しています。ReLU関数により非線形性が導入され、モデルの表現力が向上します。
コードの詳細説明
(1) 必要なモジュールのインポート:
import torch
import torch.nn as nn
PyTorch本体とそのニューラルネットワークモジュールをインポートします。torch.nnには層や活性化関数など、ニューラルネットワーク構築に必要な要素が含まれています。
(2) ネットワーククラスの定義:
class DigitClassifier(nn.Module):
DigitClassifierという名前の新しいクラスを定義し、nn.Moduleを継承します。PyTorchではすべてのネットワークモデルがnn.Moduleを継承する必要があります。
(3) 初期化メソッド:
def __init__(self):
super(DigitClassifier, self).__init__()
基底クラスnn.Moduleの初期化を呼び出すことで、ネットワークの基本機能が正しく初期化されます。
(4) 層の定義:
self.hidden1 = nn.Linear(784, 128)
self.hidden2 = nn.Linear(128, 128)
self.classifier = nn.Linear(128, 10)
3つの全結合層を定義します:
- hidden1: 784次元の入力を受けて128次元の出力を生成
- hidden2: 128次元の入力を受けて128次元の出力を生成
- classifier: 128次元の入力を受けて10次元の出力を生成(10クラス分類用)
パラメータ数の計算:
Linear層のパラメータ数は、重み行列とバイアス項で計算されます。最初の層の場合:784×128 + 128 = 100,480パラメータとなります。
=================================================================
Layer (type:depth-idx) Param #
=================================================================
DigitClassifier --
├─Linear: 1-1 100,480
├─Linear: 1-2 16,512
├─Linear: 1-3 1,290
=================================================================
Total params: 118,282
Trainable params: 118,282
Non-trainable params: 0
=================================================================
(5) 順伝播メソッドの定義:
def forward(self, input_tensor):
output = torch.relu(self.hidden1(input_tensor))
output = torch.relu(self.hidden2(output))
output = self.classifier(output)
return output
forwardメソッドではデータのネットワーク内での流れを定義します。入力データはまず第1層を通過し、ReLU活性化後に第2層へ進み、最終的に分類層で出力されます。
データの読み込みと前処理
torchvisionライブラリを使用してMNISTデータセットをロードし、transformsモジュールで標準化処理を行います。これにより学習効率が向上します。
MNISTデータセットは70,000枚の手書き数字画像で構成され、うち60,000枚が学習用、10,000枚がテスト用として提供されています。画像は28×28ピクセルのグレースケールであり、事前処理が最小限で済むようになっています。
from torchvision import transforms, datasets
from torch.utils.data import DataLoader
# 前処理パイプラインの定義
preprocessing = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
# 学習データのロード
training_dataset = datasets.MNIST(root='./mnist_data', train=True, download=True, transform=preprocessing)
training_loader = DataLoader(training_dataset, batch_size=64, shuffle=True)
# テストデータのロード
testing_dataset = datasets.MNIST(root='./mnist_data', train=False, transform=preprocessing)
testing_loader = DataLoader(testing_dataset, batch_size=64, shuffle=False)
# データの形状確認
(sample_data, sample_labels) = next(iter(testing_loader))
print('sample_labels:', sample_labels.shape)
print('sample_data:', sample_data.shape)
コードの詳細説明
(1) 必要なライブラリのインポート:
from torchvision import transforms, datasets
from torch.utils.data import DataLoader
torchvisionは画像データ処理専用のライブラリで、データセットや変換機能を提供します。DataLoaderはバッチ処理やデータシャッフル機能を備えた重要なクラスです。
(2) 前処理の定義:
preprocessing = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
ToTensor()は画像をテンソルに変換し、Normalize()は指定された平均値と標準偏差で標準化を行います。
(3) データローダーの作成:
学習用とテスト用のデータローダーを作成し、バッチサイズ64でデータを処理できるように設定します。
実行結果の形状:
sample_labels: torch.Size([64])
sample_data: torch.Size([64, 1, 28, 28])
これは64枚の28×28ピクセルのグレースケール画像であることを示しています。
学習プロセス
モデルとデータローダーを定義した後、学習ループを記述してモデルを訓練します。損失関数にはクロスエントロピー損失を、最適化アルゴリズムにはAdamを採用します。
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
loss_function = nn.CrossEntropyLoss()
for epoch in range(5):
for batch_images, batch_labels in training_loader:
batch_images = batch_images.view(-1, 784)
optimizer.zero_grad()
predictions = model(batch_images)
current_loss = loss_function(predictions, batch_labels)
current_loss.backward()
optimizer.step()
コードの詳細説明
(1) 最適化器の設定:
Adamオプティマイザを使用し、学習率を0.001に設定します。
(2) 損失関数の定義:
クロスエントロピー損失は分類タスクに適した損失関数です。
(3) 学習ループ:
5エポックの学習を実行し、各バッチに対して順伝播→損失計算→逆伝播→パラメータ更新のプロセスを繰り返します。
(4) データの再整形:
全結合層への入力に合わせて、28×28の画像を784次元のベクトルに変換します。
モデル評価と分析
学習後のモデルを評価し、未知データに対する性能を確認します。特定のクラスでの性能を分析することで、改善点を把握できます。
def validate_model(network, loader):
network.eval()
correct_predictions = 0
total_samples = 0
with torch.no_grad():
for images, targets in loader:
images = images.view(-1, 784)
outputs = network(images)
_, predicted_classes = torch.max(outputs.data, 1)
total_samples += targets.size(0)
correct_predictions += (predicted_classes == targets).sum().item()
accuracy = 100 * correct_predictions / total_samples
print(f'Accuracy: {accuracy:.2f}%')
validate_model(model, testing_loader)
この関数はモデルの評価モードに切り替え、勾配計算を無効化して効率よく精度を測定します。
完全な実装例と実行結果
import torch
import torch.nn as nn
from torchvision import transforms, datasets
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
from torchinfo import summary
import time
start_time = time.time()
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# データ前処理
data_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
# データセットの準備
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=data_transform)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_dataset = datasets.MNIST(root='./data', train=False, transform=data_transform)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)
# ネットワーク定義
class DigitClassifier(nn.Module):
def __init__(self):
super(DigitClassifier, self).__init__()
self.layer1 = nn.Linear(784, 128)
self.layer2 = nn.Linear(128, 128)
self.output_layer = nn.Linear(128, 10)
def forward(self, x):
x = torch.relu(self.layer1(x))
x = torch.relu(self.layer2(x))
x = self.output_layer(x)
return x
def evaluate_network(net, data_loader):
net.eval()
correct = 0
total = 0
with torch.no_grad():
for images, labels in data_loader:
images, labels = images.view(-1, 784).to(device), labels.to(device)
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f'Accuracy: {100 * correct / total:.2f}%')
def train_network():
optimizer = torch.optim.Adam(model_instance.parameters(), lr=0.001)
criterion = nn.CrossEntropyLoss()
for epoch in range(5):
for images, labels in train_loader:
images, labels = images.view(-1, 784).to(device), labels.to(device)
optimizer.zero_grad()
outputs = model_instance(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
model_instance = DigitClassifier().to(device)
train_network()
evaluate_network(model_instance, test_loader)
end_time = time.time()
print(f"Training duration: {end_time - start_time:.2f} seconds")
summary(model_instance)
畳み込みネットワークを使用する場合、以下のようにモデル構造を変更できます:
class ConvolutionalNet(nn.Module):
def __init__(self):
super(ConvolutionalNet, self).__init__()
self.conv_layer1 = nn.Conv2d(1, 10, kernel_size=5)
self.conv_layer2 = nn.Conv2d(10, 20, kernel_size=5)
self.dropout_layer = nn.Dropout2d()
self.fully_connected1 = nn.Linear(320, 50)
self.fully_connected2 = nn.Linear(50, 10)
def forward(self, x):
x = F.relu(F.max_pool2d(self.conv_layer1(x), 2))
x = F.relu(F.max_pool2d(self.dropout_layer(self.conv_layer2(x)), 2))
x = x.view(-1, 320)
x = F.relu(self.fully_connected1(x))
x = F.dropout(x, training=self.training)
x = self.fully_connected2(x)
return F.log_softmax(x, dim=1)