データの前処理とローダ構築
画像分類タスクでは、入力データの標準化とデータ拡張がモデルの収束と汎化性能に大きな影響を与えます。PyTorchのtorchvisionモジュールを用いて、学習用と検証用のデータパイプラインを構築します。
<script type="text/javascript">
import torch
import torch.nn as nn
from torchvision import datasets, transforms
from torch.utils.data import DataLoader, random_split
import pathlib
import matplotlib.pyplot as plt
from PIL import Image
# 計算デバイスの自動選択
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# データセットルートパス
root_dir = pathlib.Path("./data/potato_disease")
class_names = [p.name for p in root_dir.glob("*") if p.is_dir()]
# 画像変換パイプライン
image_pipeline = transforms.Compose([
transforms.Resize((224, 224)),
transforms.RandomHorizontalFlip(p=0.5),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
# フォルダ構造からデータセットを構築
full_dataset = datasets.ImageFolder(root=str(root_dir), transform=image_pipeline)
print(f"クラスマッピング: {full_dataset.class_to_idx}")
print(f"総サンプル数: {len(full_dataset)}")
# 学習/検証分割 (8:2)
split_ratio = 0.8
n_train = int(len(full_dataset) * split_ratio)
train_subset, val_subset = random_split(full_dataset, [n_train, len(full_dataset) - n_train])
train_loader = DataLoader(train_subset, batch_size=32, shuffle=True, num_workers=2, pin_memory=True)
val_loader = DataLoader(val_subset, batch_size=32, shuffle=False, num_workers=2, pin_memory=True)
</script>
VGG-16アーキテクチャの定義
VGG-16は、直列に接続された畳み込みブロックと最大プーリング層、それに続く全結合層から構成されます。コードの可読性を高め、冗長性を削減するため、畳み込みステージを構造的に定義します。
<script type="text/javascript">
class VGGClassifier(nn.Module):
def __init__(self, num_classes: int = 3):
super().__init__()
# 畳み込みステージ設定: (入力チャネル, 出力チャネル, 畳み込み層数)
stages_config = [
(3, 64, 2),
(64, 128, 2),
(128, 256, 3),
(256, 512, 3),
(512, 512, 3)
]
self.feature_extractor = nn.Sequential()
for in_ch, out_ch, depth in stages_config:
for i in range(depth):
curr_in = in_ch if i == 0 else out_ch
self.feature_extractor.add_module(f"conv_{out_ch}_l{i}",
nn.Conv2d(curr_in, out_ch, kernel_size=3, padding=1))
self.feature_extractor.add_module(f"act_{out_ch}_l{i}", nn.ReLU(inplace=True))
self.feature_extractor.add_module(f"pool_{out_ch}", nn.MaxPool2d(kernel_size=2, stride=2))
# 分類ヘッド (全結合層)
self.classification_head = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096),
nn.ReLU(inplace=True),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Linear(4096, num_classes)
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.feature_extractor(x)
x = torch.flatten(x, start_dim=1)
x = self.classification_head(x)
return x
network = VGGClassifier(num_classes=len(class_names)).to(device)
print(network)
</script>
学習ループと評価関数の実装
勾配降下法によるパラメータ更新と、勾配計算を抑制した検証フェーズを分離して定義します。バッチごとの損失と精度を累積してエポック平均を計算します。
<script type="text/javascript">
def run_training_step(dataloader, net, loss_fn, opt, dev):
net.train()
epoch_loss = 0.0
correct = 0
total = 0
for batch_x, batch_y in dataloader:
batch_x, batch_y = batch_x.to(dev), batch_y.to(dev)
opt.zero_grad(set_to_none=True)
logits = net(batch_x)
loss = loss_fn(logits, batch_y)
loss.backward()
opt.step()
epoch_loss += loss.item() * batch_x.size(0)
correct += (logits.argmax(dim=1) == batch_y).sum().item()
total += batch_x.size(0)
return epoch_loss / total, correct / total
@torch.no_grad()
def run_evaluation_step(dataloader, net, loss_fn, dev):
net.eval()
epoch_loss = 0.0
correct = 0
total = 0
for batch_x, batch_y in dataloader:
batch_x, batch_y = batch_x.to(dev), batch_y.to(dev)
logits = net(batch_x)
loss = loss_fn(logits, batch_y)
epoch_loss += loss.item() * batch_x.size(0)
correct += (logits.argmax(dim=1) == batch_y).sum().item()
total += batch_x.size(0)
return epoch_loss / total, correct / total
</script>
モデル学習の実行と結果プロット
最適化器にはAdam、損失関数には交差エントロピーを用いて学習を行います。各エポックで検証精度を監視し、最適なチェックポイントをメモリ上に保持します。
<script type="text/javascript">
import copy
optimizer = torch.optim.Adam(network.parameters(), lr=1e-4)
criterion = nn.CrossEntropyLoss()
max_epochs = 40
history_train_loss, history_train_acc = [], []
history_val_loss, history_val_acc = [], []
best_val_score = 0.0
saved_state = None
for epoch_idx in range(max_epochs):
t_loss, t_acc = run_training_step(train_loader, network, criterion, optimizer, device)
v_loss, v_acc = run_evaluation_step(val_loader, network, criterion, device)
history_train_loss.append(t_loss)
history_train_acc.append(t_acc)
history_val_loss.append(v_loss)
history_val_acc.append(v_acc)
if v_acc > best_val_score:
best_val_score = v_acc
saved_state = copy.deepcopy(network.state_dict())
print(f"Epoch {epoch_idx+1}/{max_epochs} | Train Loss: {t_loss:.4f} Acc: {t_acc:.2%} | Val Loss: {v_loss:.4f} Acc: {v_acc:.2%}")
# 最適モデルのロード
network.load_state_dict(saved_state)
network.eval()
# 個別画像の推論関数
def infer_single(img_path: str, model_net: nn.Module, transform_pipe: transforms, label_list: list):
raw_img = Image.open(img_path).convert("RGB")
tensor_input = transform_pipe(raw_img).unsqueeze(0).to(device)
with torch.no_grad():
output = model_net(tensor_input)
pred_label = label_list[output.argmax(dim=1).item()]
return pred_label
</script>
学習曲線はmatplotlibを用いてhistory_train_accとhistory_val_accをプロットすることで可視化できます。訓練損失の減少と検証精度の上昇が同期している場合は、モデルが適切に学習したと判断できます。
パラメータ数の分析とモデルの軽量化
標準的なVGG-16構造では、全結合層が合計で1億3000万パラメータの大部分を占めます。推論速度やメモリ制約が課題となる環境では、分類ヘッドの中間次元を圧縮することで大幅な軽量化が可能です。
<script type="text/javascript">
# 圧縮版の分類ヘッド
compressed_head = nn.Sequential(
nn.Linear(512 * 7 * 7, 1024),
nn.ReLU(inplace=True),
nn.Linear(1024, 128),
nn.ReLU(inplace=True),
nn.Linear(128, num_classes)
)
# 可訓練パラメータ数カウント関数
def count_trainable_params(net):
return sum(p.numel() for p in net.parameters() if p.requires_grad)
print(f"標準モデル: {count_trainable_params(network):,} params")
network.classification_head = compressed_head
print(f"軽量化モデル: {count_trainable_params(network):,} params")
</script>
この修正により、全パラメータ数は約4000万にまで減少します。軽量化モデルを再学習させた場合、初期エポックでの収束が速くなる傾向が見られます。特徴抽出層(Convolutional Backbone)を維持したまま分類器のみをスリム化することで、推論時のメモリ消費を削減しつつ、検証精度99%前後の性能を維持することが確認できます。