【python】PyTorchのCNNで漢字画像を分類！！

PyTorchのCNNを使って漢字の画像を分類します。

１．漢字画像を生成する。
```python:kanji.py
from PIL import Image, ImageDraw, ImageFont
import os

# 漢字リスト
kanji_list = ["山", "川", "木", "水", "火"]  # 必要な漢字をリストに追加
output_dir = "kanji_images"  # 画像を保存するディレクトリ

# フォント設定（フォントパスを適切に設定してください）
font_paths = [
    "C:/Windows/Fonts/msmincho.ttc",  # 明朝体（Windows用）
    "C:/Windows/Fonts/meiryo.ttc"
]

# 画像生成用のパラメータ
image_size = (64, 64)  # 画像サイズ（ピクセル）
font_size = 48  # フォントサイズ

# 出力ディレクトリの作成
os.makedirs(output_dir, exist_ok=True)

for kanji in kanji_list:
    kanji_dir = os.path.join(output_dir, kanji)
    os.makedirs(kanji_dir, exist_ok=True)
    
    for font_path in font_paths:
        try:
            font = ImageFont.truetype(font_path, font_size)
            for i in range(10):  # 同じ漢字を10種類生成
                img = Image.new("L", image_size, color=255)  # グレースケール画像（背景白）
                draw = ImageDraw.Draw(img)
                
                # 漢字を画像の中央に描画
                text_bbox = draw.textbbox((0, 0), kanji, font=font)  # テキストのバウンディングボックスを取得
                text_width = text_bbox[2] - text_bbox[0]
                text_height = text_bbox[3] - text_bbox[1]
                text_position = (
                    (image_size[0] - text_width) // 2,
                    (image_size[1] - text_height) // 2
                )
                draw.text(text_position, kanji, fill=0, font=font)  # 黒で描画
                
                # 画像の保存
                img_path = os.path.join(kanji_dir, f"{kanji}_{i}.png")
                img.save(img_path)
        except Exception as e:
            print(f"フォントの読み込みに失敗しました: {font_path} エラー: {e}")

print(f"漢字画像が生成されました。'{output_dir}'ディレクトリを確認してください。")
```
２．生成された漢字フォルダを data/train、data/valに振り分けておく。
３．CNNを使って漢字の画像を分類する。
```python:pytorch_cnn.py
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt

# デバイスの設定
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# データ変換
transform = transforms.Compose([
    transforms.Grayscale(num_output_channels=1),  # 1チャネル（グレースケール画像）
    transforms.Resize((64, 64)),                 # 画像サイズを統一
    transforms.ToTensor(),                       # テンソル化
    transforms.Normalize((0.5,), (0.5,))         # 正規化
])

# データセットの読み込み
train_dataset = datasets.ImageFolder(root="data/train", transform=transform)
val_dataset = datasets.ImageFolder(root="data/val", transform=transform)

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)

# モデルの定義
class KanjiCNN(nn.Module):
    def __init__(self, num_classes):
        super(KanjiCNN, self).__init__()
        self.conv_layers = nn.Sequential(
            nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1),  # 畳み込み層1
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2),  # プーリング層1
            nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1), # 畳み込み層2
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)   # プーリング層2
        )
        self.fc_layers = nn.Sequential(
            nn.Flatten(),
            nn.Linear(64 * 16 * 16, 128),  # 全結合層1
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(128, num_classes)    # 全結合層2
        )

    def forward(self, x):
        x = self.conv_layers(x)
        x = self.fc_layers(x)
        return x

# モデルの初期化
num_classes = len(train_dataset.classes)  # クラス数
model = KanjiCNN(num_classes).to(device)

# 損失関数と最適化アルゴリズム
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 学習関数
def train_model(model, train_loader, val_loader, criterion, optimizer, num_epochs=10):
    for epoch in range(num_epochs):
        model.train()
        train_loss = 0.0
        for images, labels in train_loader:
            images, labels = images.to(device), labels.to(device)
            optimizer.zero_grad()
            outputs = model(images)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()
            train_loss += loss.item()
        
        # 検証
        model.eval()
        val_loss = 0.0
        correct = 0
        total = 0
        with torch.no_grad():
            for images, labels in val_loader:
                images, labels = images.to(device), labels.to(device)
                outputs = model(images)
                loss = criterion(outputs, labels)
                val_loss += loss.item()
                _, predicted = torch.max(outputs, 1)
                total += labels.size(0)
                correct += (predicted == labels).sum().item()

        # 結果の出力
        print(f"Epoch [{epoch+1}/{num_epochs}], "
              f"Train Loss: {train_loss/len(train_loader):.4f}, "
              f"Val Loss: {val_loss/len(val_loader):.4f}, "
              f"Accuracy: {100 * correct / total:.2f}%")

# 学習の実行
train_model(model, train_loader, val_loader, criterion, optimizer, num_epochs=10)
```
４．実行結果
```
Epoch [1/10], Train Loss: 1.8352, Val Loss: 1.1979, Accuracy: 40.00%
Epoch [2/10], Train Loss: 1.4003, Val Loss: 0.9445, Accuracy: 80.00%
Epoch [3/10], Train Loss: 0.9725, Val Loss: 0.5208, Accuracy: 100.00%
Epoch [4/10], Train Loss: 0.5777, Val Loss: 0.1786, Accuracy: 100.00%
Epoch [5/10], Train Loss: 0.2621, Val Loss: 0.0478, Accuracy: 100.00%
Epoch [6/10], Train Loss: 0.1093, Val Loss: 0.0238, Accuracy: 100.00%
Epoch [7/10], Train Loss: 0.0564, Val Loss: 0.0038, Accuracy: 100.00%
Epoch [8/10], Train Loss: 0.0512, Val Loss: 0.0009, Accuracy: 100.00%
Epoch [9/10], Train Loss: 0.0394, Val Loss: 0.0003, Accuracy: 100.00%
Epoch [10/10], Train Loss: 0.0143, Val Loss: 0.0005, Accuracy: 100.00%
```