見出し画像

Pythonデータ分析(DeepLearning①画像分類)

マナビDXQuestに本格参加する前に、SIGNATE練習コンペ:「鋳造製品の欠陥検出」を完了したので記録です。
DeepLearningは、今までやってきたscikit-learnやLightGBMとは違って私には正直許容量オーバーでした。コードが多くて難しい。
しかし概念は何とか掴んだ気がします。

SIGNATEのQuestではPytorchを使用していましたが、今回はPytorchLightningを使っていきます。理由は、Pytorchより簡易なコードで書けることとGPUの使用がやりやすかったからです。
(PytorchでGPUを使おうとすると「一部のデータがGPU処理になってません」的なエラーが出て進まなくなりました。アレコレ弄って戻せなくなったせいかと思われる。今はGitを使用してるので手軽にファイルを以前の状態に戻せるようになり重宝してます)

PytorchLightningは下記動画がとても参考になりました。DeepLearningの概念もこれで理解が深まりました。
あとはBingにコードを解説してもらいながら実装です。


1.データ前処理

ファイル読み込み

今回の課題は不良品検出です。
OKな製品画像とNGな製品画像が入り混じったzip画像フォルダと、製品番号に対応して正常品(ラベル1)、欠陥品(ラベル0)が記載されたCSVデータがあります。
zipフォルダを解凍し、まずはこれを結合していきます。

import os
import zipfile
# 現在の作業ディレクトリのファイル一覧を変数filesに代入
files = os.listdir('./')

# zip解凍用の関数定義
def unzip_dataset(INPATH,OUTPATH):
    with zipfile.ZipFile(INPATH) as zf:
        zf.extractall(OUTPATH)

# zip解凍(※数秒から数十秒かかる場合があります)
unzip_dataset(INPATH= './鋳造製品の欠陥検出/train_data.zip',OUTPATH='./')

from PIL import Image
import pandas as pd

# 画像データのディレクトリ
image_dir = './train_data/'
# csvファイルのパス
csv_path = './train.csv'
# csvデータの読み込み
df = pd.read_csv(csv_path)

# 画像データとラベルデータの結合
data = []
for index, row in df.iterrows():
    image_path = os.path.join(image_dir, row['id'])
    image = Image.open(image_path)
    label = row['target']
    data.append((image, label))

正規化とTensor化

次に画像データを正規化→Tensor化します。
画像サイズをtransforms.Resize(299)とする理由(Bing回答):一般的に、画像分類モデルの入力サイズは、モデルのアーキテクチャによって異なります。例えば、AlexNetやVGGモデルでは、入力サイズは224x224ピクセルが一般的です。ResNetやInceptionモデルでは、入力サイズは299x299ピクセルが一般的です。
→今回はResNetを使用するので(299)としています。

正規化の値を(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])とする理由(Bing回答):
画像の各チャンネル(RGB)に対して、平均値と標準偏差を指定して正規化を行います。この場合、平均値と標準偏差は、ImageNetデータセットの平均値と標準偏差に基づいています。
ImageNetは、1400万枚以上の画像を含む大規模なデータセットであり、多くの画像分類モデルの学習に使用されています。そのため、ImageNetの平均値と標準偏差は、一般的な画像データの正規化に広く使用されています。
(独自のデータセットを使用する場合は、そのデータセットの平均値と標準偏差を使用することを推奨)

Tensor(多次元配列)はPytorchLightningでモデル作成をするのに必要なデータ形式です。pandasのデータフレームに慣れていた身には.headや.shapeでぱぱっと中身が確認できないのが不便で、データの現状把握に手間取りました。

import torch
from torchvision import transforms

# 画像データの正規化
transform = transforms.Compose([
    transforms.Resize(299),
    transforms.RandomHorizontalFlip(p=0.5),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# 画像データとラベルデータのテンソルへの変換
data_tensors = []
for image, label in data:
    image_tensor = transform(image)
    label_tensor = torch.tensor(label)
    data_tensors.append((image_tensor, label_tensor))

データの分割

画像データ+正解ラベルのデータが作成できたので、学習データと検証データに分割します。
そしてDataLoaderを作成します。DataLoaderとはデータセットからデータを効率的にロードするためのツールで、データセットからサンプルを取得し、ミニバッチを作成し、データのシャッフルなどができます。
ミニバッチ学習(Bing回答):データセットを小さなグループ(ミニバッチ)に分割して処理する方法です。つまり、1回の反復でデータセットのサブセットのみを使用して勾配を計算します。この方法は、データセットが大きく、メモリに収まらない場合に適しています。ミニバッチ学習では、計算が高速化され、効率的に行うことができます。
バッチサイズについては、一般的には16、32、64、128などの2のべき乗が使用されることが多いです。ただし、モデルのアーキテクチャやデータセットの特性、計算リソースに応じて、適切なバッチサイズを決定することができます。バッチサイズが大きいほど、計算効率が向上しますが、一方でメモリ使用量も増加します。そのため、計算リソースの制約内で最適なバッチサイズを決定することが重要です。

import pytorch_lightning as pl
from torch.utils.data import DataLoader, random_split
# データセット全体
x = data_tensors

# 学習データと検証データの分割
train_size = int(len(x) * 0.8)
val_size = len(x) - train_size
train_dataset, val_dataset = random_split(x, [train_size, val_size])
# バッチサイズ
batch_size = 32
# ランダムに分割を行うため、シードを固定して再現性を確保
pl.seed_everything(0)

# 学習データ用DataLoader
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
# 検証データ用DataLoader
val_loader = DataLoader(val_dataset, batch_size=batch_size)

2.PytorchLightning

PytorchLightningでモデルを実装していきます。
なんと、PytorchLightningでは予めImageNetという大量の画像データで学習済のモデルを使用できるようです。課題によって程度はあれど、自力で精度を上げる手間が省けるのは素敵です。使用するモデルはResnet18です。
ImageNetとResNet18(Bing回答):
ImageNetは、物体認識ソフトウェアの研究に使用するために設計された大規模な画像データベースです。ImageNetには、1400万を超える画像があり、それらの画像には手作業でアノテーションが付けられ、画像に写っている物体が示されています。
ResNet18は、深さが18層の畳み込みニューラルネットワークです。100万枚を超える画像で学習された事前学習済みのネットワークを、ImageNetデータベースから読み込むことができます。この事前学習済みのネットワークは、画像を1000個のオブジェクトカテゴリ(キーボード、マウス、鉛筆、多くの動物など)に分類することができます。その結果、このネットワークは広範囲の画像に対する豊富な特徴表現を学習しています。

モデルの読み込み・学習

引数を指定して学習済モデルを読み込み、学習させます。
今回は不良品か否かを検出する二値分類になるので、num_classes₌2に指定し、torchmetrics.Accuracyの引数を'binary'にします。

import pytorch_lightning as pl
from torch import nn
from torchvision import models
import torchmetrics

class ResNetModel(pl.LightningModule):
    def __init__(self, num_classes=2):
        super().__init__()
        self.model = models.resnet18(pretrained=True)
        self.model.fc = nn.Linear(self.model.fc.in_features, num_classes)
        self.accuracy = torchmetrics.Accuracy(task='binary')

    def forward(self, x):
        return self.model(x)

    def training_step(self, batch, batch_idx):
        x, y = batch
        y_hat = self(x)
        loss = nn.functional.cross_entropy(y_hat, y)
        self.log('train_loss', loss)
        return loss

    def validation_step(self, batch, batch_idx):
        x, y = batch
        y_hat = self(x)
        loss = nn.functional.cross_entropy(y_hat, y)
        self.log('val_loss', loss)
        self.log('val_acc', self.accuracy(y_hat.argmax(dim=1), y))

    def configure_optimizers(self):
        optimizer = torch.optim.Adam(self.parameters(), lr=1e-3)
        return optimizer

model = ResNetModel()

# ログ保存先の指定
trainer = pl.Trainer(default_root_dir='d:/git_python', max_epochs=20, accelerator='gpu')
trainer.fit(model, train_loader, val_loader)

ログ保存先がJupyterソースファイルがあるディレクトリだと作成できず、階層を上げて作成したらできました。この部分で数日躓きました。

学習曲線

学習曲線を確認します。PytorchLightningではモデルの損失関数や正解率がログファイルに保存されます。
ターミナルでこのコードを実行するとブラウザ上で簡単にグラフを確認できました。

tensorboard --logdir lightning_logディレクトリのファイルパス


取り敢えず一定の精度を出せており過学習は起こしていないと思われるのでこれで提出データを作成します。
このTensorBoardの扱いも慣れていきたいところです。

3.提出データ作成

提出形式は○○.jpeg+予測値のcsvファイルです。画像ファイルをリストで取得し、モデル学習後結合します。

#モデルの保存
torch.save(model.state_dict(), 'model.pth')

#テストデータ解凍
# zip解凍用の関数定義
def unzip_test(INPATH,OUTPATH):
    with zipfile.ZipFile(INPATH) as zf:
        zf.extractall(OUTPATH)

# zip解凍
unzip_test(INPATH= './鋳造製品の欠陥検出/test_data.zip',OUTPATH='./')

# 画像一覧のリスト取得
list_t = os.listdir('./test_data/')

from PIL import Image
t_image_dir = './test_data/'
# 画像データ読込
data_t = []
for filename in list_t:
    image_path_t = os.path.join(t_image_dir, filename)
    image_t = Image.open(image_path_t)
    data_t.append(image_t)

# 画像データのテンソルへの変換
t_data_tensors = []
for image_t in data_t:
    image_tensor_t = transform(image_t)
    t_data_tensors.append((image_tensor_t))

# モデルの読み込み
model = ResNetModel(num_classes=2)
model.load_state_dict(torch.load('model.pth'))
model.eval()

# リスト内のテンソルを結合
t_data_tensors = torch.stack(t_data_tensors)
# 予測値の算出
with torch.no_grad():
    output = model(t_data_tensors)
predicted_labels = torch.argmax(output, dim=1)

import csv
with open('output.csv', 'w',newline='') as f:
    writer = csv.writer(f)
    for filename, label in zip(list_t, predicted_labels):
        writer.writerow([filename, label.item()])

結果はaccuracy0.98でした!
今回はかなり悪戦苦闘したので嬉しいです。
マナビDXQuestが本格的に始まるので練習コンペはしばらく手を付けられないかもしれませんが、DeepLearningの楽しさもやっと少しわかってきたので引き続き取り組んでいきたいと思います。


この記事が気に入ったらサポートをしてみませんか?