このマニュアルは、AIの理論は理解しているものの、Pythonでの実装経験が少ない社会人初心者を対象としています。手書き数字の分類という具体的なタスク(MNIST)を通して、深層学習モデルをPyTorchで構築し、学習させる一連のプロセスを体系的に学びます。
学ぶことの全体像: 深層学習の実装は、以下の主要なステップで構成されます。
PyTorchは、これらの深層学習モデルの構築と学習を効率的に行うための強力なオープンソースライブラリです。なぜPyTorchを使うのかというと、柔軟性が高く、Pythonのコードとして直感的に記述できるため、研究開発から実用まで幅広く利用されているからです。
深層学習の実装に限らず、プログラミングにおいて「設計」は非常に重要です。特に、コードを「クラス」や「関数」に分割する「モジュール化」は、以下の点で大きなメリットがあります。
Pythonでは、特定の処理をまとめるために「関数」を定義します。
# 関数の定義例
def greet(name):
return f"こんにちは、{name}さん!"
# 関数の呼び出し例
message = greet("田中")
print(message) # 出力: こんにちは、田中さん!
さらに、データとそれに関連する処理をひとまとめにするのが「クラス」です。クラスは「設計図」のようなもので、その設計図から作られる具体的なものが「インスタンス(オブジェクト)」です。
# クラスの定義例
class Dog:
def __init__(self, name, breed): # インスタンスが作られるときに呼ばれるメソッド
self.name = name
self.breed = breed
def bark(self): # インスタンスの振る舞いを定義するメソッド
return f"{self.name}がワンワンと吠える!"
# クラスからインスタンスを作成(オブジェクト化)
my_dog = Dog("ポチ", "柴犬")
# インスタンスのメソッドを呼び出す
print(my_dog.bark()) # 出力: ポチがワンワンと吠える!
オブジェクト指向プログラミングの重要な概念の一つに「継承」があります。これは、既存のクラス(親クラス)の機能を引き継ぎ、新しいクラス(子クラス)を作成する仕組みです。これにより、共通の機能を何度も書く手間を省き、コードの再利用性を高めることができます。
Pythonでは、以下のように記述します。
class ParentClass:
def common_method(self):
print("これは親クラスの共通メソッドです。")
class ChildClass(ParentClass): # ParentClassを継承
def unique_method(self):
print("これは子クラス独自のメソッドです。")
# 子クラスのインスタンスを作成
child_obj = ChildClass()
# 親クラスから継承したメソッドを呼び出す
child_obj.common_method() # 出力: これは親クラスの共通メソッドです。
# 子クラス独自のメソッドを呼び出す
child_obj.unique_method() # 出力: これは子クラス独自のメソッドです。
深層学習のPyTorch実装では、データセットやモデルの定義にこの「クラス」と「継承」の概念が頻繁に登場します。特に、PyTorchが提供するtorch.utils.data.Dataset
やtorch.nn.Module
といった基底クラスを継承して、独自のデータセットやモデルを構築することになります。
深層学習において「データセット」は、モデルが学習するための入力データ(画像、テキストなど)と、それに対応する正解ラベル(手書き数字の「5」など)の集合を指します。PyTorchでは、このデータセットを効率的に管理するためにtorch.utils.data.Dataset
という抽象クラスが提供されています。
Dataset
クラスを継承することで、以下の2つの重要なメソッドを実装するだけで、PyTorchがデータを扱うための標準的なインターフェースを提供できます。
len()
関数や[]
(インデックスアクセス)演算子に対応します。PyTorchのDataset
クラスを継承する際には、これらのメソッドを実装する必要があります。__len__(self)
: データセットに含まれるサンプルの総数を返します。__getitem__(self, idx)
: 指定されたインデックスidx
に対応する1つのデータサンプル(入力と正解ラベルのペア)を返します。MNISTは手書き数字の画像データセットで、深層学習の入門によく使われます。PyTorchのtorchvision
ライブラリを使えば、簡単にダウンロードして利用できます。
torchvision.datasets.MNIST
は、MNISTデータセットを扱うための便利なクラスです。
torchvision
が提供する、MNISTデータセットを簡単に扱えるようにしたクラスです。root
: データセットをダウンロードして保存するディレクトリを指定します。train
: True
にすると訓練データセットを、False
にするとテストデータセットをロードします。download
: True
にすると、指定されたroot
ディレクトリにデータセットが存在しない場合に自動的にダウンロードします。transform
: データの前処理(変換)を指定します。NumberDataset
クラスの実装詳細MNIST訓練実装答え.py
では、NumberDataset
というカスタムデータセットクラスが定義されています。これはtorch.utils.data.Dataset
を継承しています。
NumberDataset
クラスのインスタンスが持つ属性(変数)で、実際にMNISTデータセットのデータを保持しています。-1
を指定すると、その次元のサイズが自動的に計算されます。この場合、28x28の画像を1次元の784要素のベクトルに変換しています。NumberDataset
クラスのコード(一部抜粋):
import torch
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import Dataset
class NumberDataset(Dataset):
def __init__(self, train=True, transform=None):
# torchvisionのMNISTデータセットをダウンロードして利用
if transform is None: # `===` はPythonでは無効な比較演算子です。`is` または `==` を使用します。
transform = transforms.Compose([
transforms.ToTensor(),
# 必要なら正規化も追加
])
self.dataset = torchvision.datasets.MNIST(
root=______, train=______, download=______, transform=transform # 穴埋め部分
)
def __len__(self):
return len(self.dataset)
def __getitem__(self, idx):
image, label = self.dataset[idx]
# 画像を1次元ベクトルに変換
image = image.view(______) # 穴埋め部分
return image, label
__init__
メソッドこのメソッドは、NumberDataset
のインスタンスが作成されるときに最初に実行されます。ここでは、torchvision.datasets.MNIST
を使って実際のMNISTデータセットをロードし、self.dataset
に保持しています。
transform
引数がない場合は、デフォルトでtransforms.ToTensor()
が適用されます。これは、PIL形式の画像をPyTorchのテンソルに変換し、ピクセル値を0から1の範囲に正規化する重要な前処理です。
__len__
メソッドこのメソッドは、データセットの全サンプル数を返します。len(self.dataset)
とすることで、内部で保持しているMNISTデータセットのサイズをそのまま返しています。
__getitem__
メソッドこのメソッドは、データセットから特定のインデックスidx
のデータサンプルを取り出す際に呼び出されます。
image, label = self.dataset[idx]
で、元のMNISTデータセットから画像とラベルを取得します。
image = image.view(-1)
は、28x28ピクセルの画像を1次元のベクトル(784要素)に変換しています。これは、後で使う全結合層(Linear層)が1次元の入力を期待するためです。
プログラミング設計のポイント:
NumberDataset
クラスは、データセットの「設計図」として機能します。__init__
で初期設定を行い、__len__
と__getitem__
というPyTorchのDataset
が要求する「インターフェース」を実装することで、このクラスのインスタンスがPyTorchのデータローダーと連携できるようになります。これにより、データ管理のロジックがカプセル化され、コードの見通しが良くなります。
深層学習モデルにデータを入力する前には、適切な形式に変換したり、モデルが学習しやすいように加工したりする「前処理(Transformations)」が必要です。torchvision.transforms
は、画像データに対する様々な前処理機能を提供します。
transforms.ToTensor()
:
ndarray
をPyTorchのTensor
に変換します。[0, 255]
の範囲から [0.0, 1.0]
の範囲に正規化します。これは、ニューラルネットワークの入力として適切なスケールです。(H, W, C)
(高さ, 幅, チャンネル) から (C, H, W)
(チャンネル, 高さ, 幅) に変更します。PyTorchの畳み込み層などがこの形式を期待するためです。正規化の概念(transforms.Normalize
):
transforms.Normalize(mean, std)
を使うと、さらにデータセット全体の平均と標準偏差に基づいてデータを正規化できます。これにより、データの分布がモデルにとってより扱いやすくなり、学習の安定性や収束速度が向上することがあります。DataLoader
によるバッチ処理深層学習では、一度に全データを使って学習するのではなく、データを小さな塊(「ミニバッチ」)に分割して、少しずつ学習を進めるのが一般的です。このミニバッチ学習を効率的に行うために、PyTorchではtorch.utils.data.DataLoader
が提供されています。
DataLoader
は、Dataset
からデータを取り出し、指定されたbatch_size
ごとにまとめてモデルに供給する役割を担います。
batch_size
: 一度にモデルに入力するサンプル数。shuffle
: True
に設定すると、各エポックの開始時にデータセットの順序をシャッフルします。これにより、モデルがデータの特定の順序に依存して学習してしまうのを防ぎ、汎化性能の向上に役立ちます。モデルの学習がうまくいっているか、そして未知のデータに対しても正しく予測できるか(「汎化性能」)を評価するために、データセットを「訓練データ」と「検証データ」に分割することが非常に重要です。
MNIST訓練実装答え.py
では、torch.utils.data.random_split
を使って訓練データセットをさらに訓練用と検証用に分割しています。
torch.Generator().manual_seed(42)
: 乱数生成器のシードを固定することで、データの分割が毎回同じ結果になり、実験の「再現性」を確保できます。深層学習における「モデル」とは、入力データを受け取り、何らかの変換(計算)を行って出力(予測結果)を生成する関数や構造のことです。ニューラルネットワークは、このモデルの一種です。
PyTorchでは、ニューラルネットワークモデルを構築するためにtorch.nn.Module
という基底クラスが提供されています。このクラスを継承して独自のモデルクラスを定義することが、PyTorchにおけるモデル構築の基本的な方法です。
nn.Module
を継承したクラスで必ず実装するメソッドです。モデルにデータが入力されたときに、そのデータがどのように各層を伝播していくか(順伝播の計算グラフ)を定義します。nn.Module
を継承する主な理由は以下の通りです。
forward
メソッドを実装することで、入力データがモデル内をどのように伝播していくかを定義します。model.to('cuda')
のように記述するだけで、モデル全体をGPUに簡単に移動できます。MyNet
クラスの構造MNIST訓練実装答え.py
では、MyNet
というニューラルネットワークモデルが定義されています。これはnn.Module
を継承しています。
__init__
メソッド内で、親クラスの__init__
メソッドを呼び出すための標準的な記述です。これにより、親クラスの初期化処理が実行され、nn.Module
の持つ機能(パラメータ管理など)がMyNet
に引き継がれます。in_features
は入力の次元数、out_features
は出力の次元数を指定します。input_size
は入力層のニューロン数、hidden_size
は隠れ層のニューロン数、output_size
は出力層のニューロン数を指します。dim=-1
は、最後の次元に沿ってSoftmaxを適用することを意味します。self.変数名
の形式で定義され、そのインスタンスの生存期間中、値を保持します。MyNet
クラスのコード(一部抜粋):
MyNet
クラスのコード(一部抜粋):
import torch.nn as nn
class MyNet(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(MyNet, self).__init__()
self.layer1 = nn.Linear(input_size, hidden_size)
self.layer2 = nn.Linear(hidden_size, hidden_size)
self.layer3 = nn.Linear(hidden_size, hidden_size)
self.layer4 = nn.Linear(hidden_size, output_size)
# 活性化関数を定義(ここではreluとsoftmax、ネットで使い方を調べてもよい 「Pytorch Relu」「Pytorch Softmax」)
self.relu = nn.ReLU()
self.softmax = nn.Softmax(dim=-1)
def forward(self, x):
x = self.layer1(x)
x = self.relu(x)
x = self.layer2(x)
x = self.relu(x)
x = self.layer3(x)
x = self.relu(x)
x = self.layer4(x)
x = self.softmax(x)
return x
__init__
メソッドこのメソッドは、MyNet
のインスタンスが作成されるときに実行され、モデルの「部品」となる各層や活性化関数を定義します。
super(MyNet, self).__init__()
: これは、継承元のnn.Module
クラスの初期化メソッドを呼び出すための定型句です。必ず記述する必要があります。nn.Linear(in_features, out_features)
(線形層/全結合層):in_features
の数と出力out_features
の数を指定します。MyNet
では、input_size
(784)からhidden_size
(128)への変換、隠れ層間の変換、そして最後の隠れ層からoutput_size
(10クラス)への変換を行っています。nn.ReLU()
(Rectified Linear Unit): max(0, x)
というシンプルな関数で、ニューラルネットワークに非線形性をもたらします。これにより、モデルはより複雑なパターンを学習できるようになります。nn.Softmax(dim=-1)
: 主に分類問題の出力層で使われます。入力された値を、合計が1になるような確率分布に変換します。dim=-1
は、最後の次元(この場合、10クラスの出力)に沿ってSoftmaxを適用することを意味します。プログラミング設計のポイント:
__init__
メソッドでは、モデルの「骨格」となる各層や活性化関数をインスタンス変数として定義します。これにより、これらの部品がモデルの内部状態として保持され、forward
メソッドで利用できるようになります。これは、モデルの構造を明確にし、再利用可能な部品として管理するためのオブジェクト指向的なアプローチです。
実装課題:
MyNet
クラスの__init__
メソッドで、各線形層と活性化関数を正しく定義してください。
class MyNet(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(MyNet, self).__init__()
self.layer1 = nn.Linear(______, ______) # 入力層から隠れ層
self.layer2 = nn.Linear(hidden_size, hidden_size)
self.layer3 = nn.Linear(hidden_size, hidden_size)
self.layer4 = nn.Linear(______, ______) # 隠れ層から出力層
self.relu = nn.______() # ReLU活性化関数
self.softmax = nn.______(dim=-1) # Softmax活性化関数
# ... (forwardメソッドは省略)
解答:
class MyNet(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(MyNet, self).__init__()
self.layer1 = nn.Linear(input_size, hidden_size)
self.layer2 = nn.Linear(hidden_size, hidden_size)
self.layer3 = nn.Linear(hidden_size, hidden_size)
self.layer4 = nn.Linear(hidden_size, output_size)
self.relu = nn.ReLU()
self.softmax = nn.Softmax(dim=-1)
# ... (forwardメソッドは省略)
forward
メソッドの実装forward
メソッドは、モデルにデータが入力されたときに、そのデータが各層をどのように伝播していくか(順伝播の計算グラフ)を定義します。
forward
メソッドで定義されます。MyNet
のforward
メソッドでは、入力x
がlayer1
を通過し、relu
で活性化され、次にlayer2
、relu
、layer3
、relu
、layer4
を順に通過し、最後にsoftmax
で処理されて出力が返されます。
実装課題:
MyNet
クラスのforward
メソッドは、入力x
が各層と活性化関数をどのように通過するかを定義します。以下の穴埋めコードを完成させてください。
class MyNet(nn.Module):
# ... (__init__メソッドは省略)
def forward(self, x):
x = self.layer1(x)
x = self.relu(x)
x = self.layer2(x)
x = self.relu(x)
# ここにlayer3とreluの処理を追加
x = self.______(x) # layer3の適用
x = self.______(x) # reluの適用
x = self.layer4(x)
x = self.softmax(x)
return x
解答:
class MyNet(nn.Module):
# ... (__init__メソッドは省略)
def forward(self, x):
x = self.layer1(x)
x = self.relu(x)
x = self.layer2(x)
x = self.relu(x)
x = self.layer3(x)
x = self.relu(x)
x = self.layer4(x)
x = self.softmax(x)
return x
深層学習における「学習」とは、モデルが与えられたデータからパターンを抽出し、未知のデータに対して正確な予測ができるように、モデル内部のパラメータ(重みとバイアス)を調整するプロセスです。
MNIST訓練実装答え.py
では、train_model
という関数に学習プロセス全体がまとめられています。このように学習のロジックを関数としてカプセル化することで、コードの見通しが良くなり、再利用性も高まります。
学習プロセスは、通常、以下のサイクルを繰り返します。
「損失関数(Loss Function)」または「基準(Criterion)」は、モデルの予測結果がどれだけ正解から離れているかを示す指標です。この値が小さいほど、モデルの予測は正確であると判断できます。
nn.CrossEntropyLoss()
:nn.Softmax
を明示的に適用する必要がない場合もありますが、今回のコードでは明示的に適用しています。「最適化手法(Optimizer)」は、損失関数の値を最小化するために、モデルのパラメータをどのように更新するかを決定するアルゴリズムです。
torch.optim.Adam()
:model.parameters()
: モデル内の学習可能なすべてのパラメータ(重みとバイアス)を最適化器に渡します。lr
(学習率): パラメータを更新する際の「歩幅」の大きさを決定します。この値が大きすぎると学習が不安定になり、小さすぎると学習に時間がかかります。train_model
関数内のfor inputs, labels in test_loader:
ループが、ミニバッチ学習の核心です。DataLoader
からバッチ単位でデータを取り出し、以下のステップを繰り返します。
optimizer.zero_grad()
outputs = model(inputs)
inputs
を定義したmodel
(MyNet
のインスタンス)に渡し、モデルの予測結果outputs
を得るプロセスです。これはMyNet
クラスで定義したforward
メソッドが実行されることに相当します。loss = criterion(outputs, labels)
outputs
と、実際の正解ラベルlabels
を比較し、その間の「誤差」をcriterion
(損失関数)を使って数値化します。このloss
の値が、モデルがどれだけ間違っているかを示します。loss.backward()
loss
(誤差)を基に、モデルの各パラメータ(重みとバイアス)が、その誤差にどれだけ寄与したかを計算します。このプロセスは「誤差逆伝播法(Backpropagation)」と呼ばれ、連鎖律の原理を用いて、出力層から入力層に向かって勾配を効率的に計算します。このステップで、各パラメータのgrad
属性に勾配が格納されます。optimizer.step()
loss.backward()
で計算された勾配(grad
属性に格納されている値)と、選択した最適化手法(Adam
)のアルゴリズムに基づいて、モデルのパラメータ(model.parameters()
)を実際に更新します。これにより、モデルは次のバッチでより正確な予測ができるように調整されます。実装課題:
train_model
関数内で、各エポックの終わりに訓練データでの平均損失を計算し、train_loss_history
に追加する部分を完成させてください。
def train_model(model, test_loader, validation_loader, criterion, optimizer, epochs):
# ... (前略)
train_loss_history = []
# ... (中略)
for epoch in range(epochs):
epoch_loss = 0.0
for inputs, labels in test_loader:
# ... (勾配のリセット、順伝播、損失計算、逆伝播、パラメータ更新)
epoch_loss += loss.item()
# エポック全体の平均損失を計算し、履歴に保存
avg_epoch_loss = epoch_loss / len(______) # 穴埋め部分
train_loss_history.append(__________) # 穴埋め部分
# ... (後略)
解答:
def train_model(model, test_loader, validation_loader, criterion, optimizer, epochs):
# ... (前略)
train_loss_history = []
# ... (中略)
for epoch in range(epochs):
epoch_loss = 0.0
for inputs, labels in test_loader:
# ... (勾配のリセット、順伝播、損失計算、逆伝播、パラメータ更新)
epoch_loss += loss.item()
# エポック全体の平均損失を計算し、履歴に保存
avg_epoch_loss = epoch_loss / len(test_loader)
train_loss_history.append(avg_epoch_loss)
# ... (後略)
学習中に訓練データだけでなく、検証データでもモデルの性能を評価することは非常に重要です。これにより、モデルが訓練データに過学習していないか(つまり、未知のデータに対しても汎化できているか)を確認できます。
model.eval()
: モデルを評価モードに切り替えます。これにより、ドロップアウト層やバッチ正規化層など、訓練時と評価時で挙動が変わる層が適切に動作するようになります。with torch.no_grad():
: このブロック内では、PyTorchは勾配の計算を行いません。評価時にはパラメータを更新する必要がないため、勾配計算を無効にすることでメモリ使用量を削減し、計算を高速化できます。モデルの学習が完了したら、そのモデルがどれだけ正確に予測できるかを最終的に評価する必要があります。この評価には、学習には一切使われなかった「テストデータ」を使用するのが一般的です。
MNIST訓練実装答え.py
では、evaluate_model
関数がモデルの評価を担当しています。
分類問題における最も一般的な評価指標の一つが「正解率(Accuracy)」です。これは、モデルが正しく予測したサンプルの割合を示します。
model.eval()
とwith torch.no_grad():
は、学習時と同様に評価時にも適用されます。これは、モデルを評価モードにし、勾配計算を無効にするためです。outputs = model(inputs)
: テストデータ(inputs
)をモデルに入力し、予測結果(各クラスの確率分布)を得ます。_, predicted = torch.max(outputs, 1)
:torch.max()
関数は、テンソルの最大値とそのインデックスを返します。outputs
は各クラスの確率分布なので、dim=1
(クラスの次元)に沿って最大値のインデックス(最も確率が高いクラス)を取得します。これがモデルの予測したクラスになります。_
は、最大値自体は不要なので破棄していることを意味します。total += labels.size(0)
: バッチ内のサンプル数を合計します。correct += (predicted == labels).sum().item()
: 予測されたクラスが正解ラベルと一致する数を数え、合計します。100 * correct / total
: 正解率をパーセンテージで計算します。数値としての正解率だけでなく、実際にモデルがどのように予測しているかを視覚的に確認することは、モデルの挙動を理解する上で非常に役立ちます。
visualize_predictions
関数は、テストデータセットからいくつかのサンプルを取り出し、元の画像、正解ラベル、そしてモデルの予測結果を並べて表示します。
model.eval()
とwith torch.no_grad():
はここでも適用されます。image, label = dataset[i]
: データセットから画像とラベルを取得します。output = model(image.unsqueeze(0))
: モデルはバッチ入力を期待するため、1つの画像でもunsqueeze(0)
でバッチ次元を追加します。pred = output.argmax(dim=1).item()
: モデルの出力から最も確率の高いクラス(予測)を取得します。image2d = image.view(28, 28)
: 1次元に変換されていた画像を元の28x28の2次元に戻します。matplotlib.pyplot
(plt
): Pythonでグラフや画像をプロットするための標準的なライブラリです。plt.figure()
: 新しい図を作成します。plt.subplot()
: 複数のプロットを1つの図に配置します。plt.imshow(image2d, cmap="gray")
: 画像を表示します。cmap="gray"
は画像をグレースケールで表示することを意味します。plt.title()
: 各サブプロットのタイトルを設定します。plt.axis("off")
: 軸の表示をオフにします。plt.suptitle()
: 図全体のタイトルを設定します。plt.show()
: 図を表示します。学習の進捗を視覚的に確認するために、訓練損失と検証損失の履歴をプロットすることは非常に重要です。
MNIST訓練実装答え.py
のメイン実行ブロックの最後で、plt.plot()
を使って訓練損失と検証損失の履歴をグラフ化しています。
if __name__ == "__main__":
ブロックは、Pythonスクリプトが直接実行されたときにのみ実行されるコードを記述するための標準的な慣習です。このブロック内で、これまでに定義したすべてのコンポーネント(データセット、モデル、損失関数、最適化手法、学習関数、評価関数、可視化関数)が統合され、深層学習のパイプライン全体が実行されます。
実行フローの概要:
NumberDataset
とDataLoader
を使って、訓練、検証、テスト用のデータを準備します。MyNet
クラスからモデルのインスタンスを作成し、損失関数と最適化手法を設定します。train_model
関数を呼び出し、モデルを訓練します。evaluate_model
関数を呼び出し、テストデータでモデルの最終的な性能を評価します。「ハイパーパラメータ」とは、モデルの学習プロセスを制御するために、学習開始前に手動で設定する必要がある値のことです。これらはモデルの性能に大きな影響を与えます。
INPUT_SIZE = 28 * 28
:HIDDEN_SIZE = 128
:OUTPUT_SIZE = 10
:LEARNING_RATE = 0.001
:BATCH_SIZE = 64
:EPOCHS = 5
:深層学習の学習プロセスには、パラメータの初期化やデータのシャッフルなど、多くのランダムな要素が含まれます。そのため、何も設定しないと、同じコードを実行しても毎回異なる結果になる可能性があります。
torch.Generator().manual_seed(42)
: PyTorchの乱数生成器のシードを固定します。これで、AI班/活動/7_2/MNIST訓練実装答え.py
を実装するための知識をゼロから体系的に取得するためのマニュアルが完成しました。