🙆

PyTorch入門に入門してみた -その2, その3

2023/08/19に公開

PyTorch入門 - データセットとデータローダー -

はじめに

今回はその2とその3を1つにまとめました。と言うのも、2を終えて3を読んでみたところ、2で疑問だったToTensor()とLambdaの話がほとんどだったからです。というわけで今回は画像データセットとデータローダーです!!

Datasets & Dataloaders

深層学習などで用いるデータセットを扱うコードがPyTorchには用意されているようです。
データセットを扱う基本的要素として以下の二つがあります。

  • torch.utils.data.DataLoader
  • torch.utils.data.Dataset

上記を用いて、あらかじめ用意されたデータや自前のデータセットを利用できます。
Datasetにはサンプルとそれに対するラベルが格納され、DataLoaderはDatasetが簡単に利用できるように、繰り返し処理可能なものとなっています。

Datasetの読み込み

試しにFashion-MNISTをロードする例をやってみます。
Fashion MNISTは60000個の訓練データと10000個のテストデータから構成されたZalandoの記事画像のデータセットです。
各サンプルは28×28のグレースケール画像と、10クラスのうちの1つのラベルから構成されています。

FashionMNIST datasetを読み込む際には以下のパラメータを利用します。

  • root :訓練/テストデータが格納されているパスを指定
  • train :訓練データまたはテストデータセットを指定
  • download=True:root にデータが存在しない場合は、インターネットからデータをダウンロードを指定
  • transform と target_transform:特徴量とラベルの変換を指定

ライブラリ系のインストール

import torch
from torch.utils.data import Dataset
from torchvision import datasets
from torchvision.transforms import ToTensor, Lambda

import matplotlib.pyplot as plt

torchvision.datasetsモジュールをインポートすることでtorchvision.datasets内のデータにアクセスできるようになります。
torchvision.transformsモジュールでは、画像系の変形やオーギュメンテーションのためのメソッドが用意されています。今回インポートしたのはToTensor()Lambdaです。
ToTensorの処理についてはこの記事がわかりやすかったです。
https://qiita.com/Haaamaaaaa/items/925257c0c8f1b115575a

訓練データとテストデータの読み込み

train_data = datasets.FashionMNIST(
    root="data",
    train=True,
    download=True,
    transform=ToTensor()
)
test_data = datasets.FashionMNIST(
    root="data",
    train=False,
    download=True,
    transform=ToTensor()
)

データセットの反復処理と可視化

データセットのi番目の画像のデータを取得するにはtrain_data[i] のように指定します。
以下のようにMatplotlibを用いてデータを可視化してみましょう。

labels_map = {
    0: "T-Shirt",
    1: "Trouser",
    2: "Pullover",
    3: "Dress",
    4: "Coat",
    5: "Sandal",
    6: "Shirt",
    7: "Sneaker",
    8: "Bag",
    9: "Ankle Boot",
}
figure = plt.figure(figsize=(8, 8))
cols, rows = 3, 3
for i in range(1, rows * cols + 1):
    sample_idx = torch.randint(len(train_data), size=(1,)).item()
    img, label = train_data[sample_idx]
    figure.add_subplot(rows, cols, i)
    plt.title(labels_map[label])
    plt.axis("off")
    plt.imshow(img.squeeze(), cmap="gray")
plt.show()

最初の辞書型のlabelsは、ラベルの番号とその名称を対応づける辞書型配列です。例えば以下のように0番目の写真のデータを見てみると以下のようになります。

train_data[0]

"""
(tensor([(28×28×1のデータ)]), 9) 
"""

上記のように、(tensor, label) という形が出力されます。この場合, train_dataの0番目の画像のラベルは "Ankle Boot" となります。

可視化のコードの詳説

  1. figure = plt.figure(figsize=(8, 8))
    これは簡単ですね。sizeが(8, 8)のグラフ領域を作成する処理です。
  2. cols, rows = 3, 3
    今回は3×3のプロットを行うので、行と列をそれぞれ整数で設定します。
  3. for i in range(1, rows * cols + 1):
    9枚を可視化するので1~9までのループを作成します。
  4. sample_idx = torch.randint(len(train_data), size=(1,)).item()
    個人的にこれが複雑に見えますが、どうですか?sample_idxに何が入るかを知れればOKです。

これはtorch.randint(len(train_data), size=(1,))をまず理解するところから始めましょう。torch.randintは基本的な引数として、low(最小値), high(最大値), sizeがあります。
lowはデフォルトで0が設定されているのでこのままにしておきます。使い方はこんな感じ。

print(torch.randint(10, (2, 2)))
# tensor([[8, 6],
#        [0, 0]])

今回の場合はtrain_dataの長さ(60000枚)がhighとして与えられ、1次元のTensorが与えられます。最後の.item()で数値の値をPythonのintとして出力させています。例えばこんな感じ。

sam_ = torch.randint(len(train_data), size=(1,))
print(sam_)
sam = sam_.item()
print(sam)
# tensor([39087])
# 39087
  1. img, label = train_data[sample_idx]
    これは、imgに画像データのTensorが格納され、labelにはその画像のラベル番号が格納される。

  2. figure.add_subplot(rows, cols, i)
    この行では、最初に描画したfigure領域を3×3に分けたときに、i番目の領域にsubplot領域を描画する処理がされます。

  3. plt.title(labels_map[label])
    labels_mapのラベル名をsubplotのタイトルとして設定することで、subplotされた画像のラベルを表示させることができます。

  4. plt.axis("off")
    x軸とy軸のメモリをオフにします。

  5. plt.imshow(img.squeeze(), cmap="gray")
    画像をsubplotに表示します。img.squeeze()は余分な次元(色の次元など)を削除して28×28の画像を表示。

  6. plt.show()
    全体を表示させる処理。

カスタムデータセットの作成

先ほどまではPyTorchに用意されているデータセットを使う場合の方法を読んできましたが、ここからは自分で用意したデータセットを用いる場合についてです。

クラス CustomImageDatasetを作成

絶対に必要なメソッドが3つあり、それを1つずつ見ていきます。
前提として、FashionMNISTの画像データをimg_dirフォルダに、ラベルはCSVファイルannotations_fileとして保存します。

class CustomImageDataset(Dataset):
    def __init__(self, annotations_file, img_dir, transform=None, target_transform=None):
        self.img_labels = pd.read_csv(annotations_file)
        self.img_dir = img_dir
        self.transform = transform
        self.target_transform = target_transform

    def __len__(self):
        return len(self.img_labels)

    def __getitem__(self, idx):
        img_path = os.path.join(self.img_dir, self.img_labels.iloc[idx, 0])
        image = read_image(img_path)
        label = self.img_labels.iloc[idx, 1]
        if self.transform:
            image = self.transform(image)
        if self.target_transform:
            label = self.target_transform(label)
        sample = {"image": image, "label": label}
        return sample

ライブラリ等のインポート

import os
import pandas as pd
from torchvision.io import read_image

def init の解説

def init(self, annotations_file, img_dir, transform=None, target_transform=None):
コンストラクタを定義しています。このコンストラクタは、データセットをインスタンス化するときに呼び出されます。
annotations_file: 画像のファイル名とそれに関連するラベルを含むCSVファイルへのパス。
img_dir: 画像が格納されているディレクトリへのパス。
transform: 画像に適用する変換。
target_transform: ラベルに適用する変換。

self.img_labels = pd.read_csv(annotations_file)
annotations_fileからCSVデータを読み取り、それをPandasのデータフレームとしてself.img_labelsに保存します。

def len(self): の解説

このメソッドは、データセットの長さ(つまり、画像の数)を返します。PyTorchのDataLoaderなどがデータセットのサイズを知るためにこのメソッドを使用します。

def getitem(self, idx):の解説

このメソッドは、指定されたインデックスidxのサンプルを返します。このメソッドは、データセットから一つのサンプルを取得するための主要なメソッドとして機能します。

img_path = os.path.join(self.img_dir, self.img_labels.iloc[idx, 0])
指定されたインデックスidxの画像のファイル名を取得し、その完全なパスを生成します。

image = read_image(img_path)
read_imageは以下のようなメソッドです(公式ドキュメントより)

torchvision.io.read_imageのDoc
生成されたパスimg_pathから画像を読み取ります。

label = self.img_labels.iloc[idx, 1]
指定されたインデックスidxの画像のラベルを取得します。

if self.transform:とif self.target_transform:
もし指定された変換があれば、それを画像やラベルに適用します。

sample = {"image": image, "label": label}
画像とラベルを含む辞書を作成します。

return sample
作成した辞書型配列を返します。

上記のようにクラスを定義することでs、PyTorchのDataLoaderを使用してデータのバッチ処理やシャッフルなどの操作を簡単に行うことができます。

DataLoaderの使用方法

Datasetを用いると各サンプルを1つずつ取り出すことができますが、モデルの訓練時にはミニバッチ単位でデータを扱いたく、各epochではシャッフルされて欲しいですよね。
学習回数やエポック、ミニバッチに関しては以下の記事がわかりやすいので読んでください。
https://zenn.dev/nekoallergy/articles/ml-basic-epoch

DataLoaderは上記のような処理を高速でやってくれるAPIとなります。

from torch.utils.data import DataLoader

train_dataloader = DataLoader(train_data, batch_size=64, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=64, shuffle=True)

DataLoaderを用いた繰り返し処理

データをdataloaderに読み込ませて必要に応じて繰り返し処理を実行させます。
以下の各反復処理ではtrain_features と train_labelsのミニバッチを返します(それぞれ、64個のサンプルで構成されるミニバッチです)。

今回shuffle=Trueと指定しているので、データセットのデータを全て取り出したら、データの順番はシャッフルされます。

# Display image and label.
train_features, train_labels = next(iter(train_dataloader))
print(f"Feature batch shape: {train_features.size()}")
print(f"Labels batch shape: {train_labels.size()}")
img = train_features[0].squeeze()
label = train_labels[0]
plt.imshow(img, cmap="gray")
plt.show()
print(f"Label: {label}")

データローダーから画像とラベルを表示する処理の詳説

このコードを実行すると、訓練データの最初の画像とそのラベルが表示されます。これは、データが正しくロードされているか、前処理が適切に行われているかなどを確認する際に役立ちます。

train_features, train_labels = next(iter(train_dataloader))
train_dataloaderはPyTorchのDataLoaderオブジェクトであり、データセットからバッチごとにデータを取得します。iter()関数でtrain_dataloaderのイテレータを取得し、next()関数でそのイテレータから次のバッチを取得します。この場合、最初のバッチを取得しています。
train_featuresには画像のバッチが、train_labelsには対応するラベルのバッチが格納されます。
print(f"Feature batch shape: {train_features.size()}")
画像のバッチ(train_features)の形状(shape)を表示します。Outputは以下のようになります。Feature batch shape: torch.Size([64, 1, 28, 28])

print(f"Labels batch shape: {train_labels.size()}")
ラベルのバッチ(train_labels)の形状を表示します。Outputは以下のようになります。
Labels batch shape: torch.Size([64])

img = train_features[0].squeeze()
train_features[0]は画像バッチの最初の画像を取得します。
squeeze()メソッドは、次元のサイズが1の次元を削除します。例えば、形状が[1, 28, 28]のテンソルは、[28, 28]になります。これは画像を表示する際に必要な処理です。

label = train_labels[0]
ラベルバッチの最初のラベルを取得します。

plt.imshow(img, cmap="gray")
matplotlibのimshow関数を使用して画像を表示します。cmap="gray"は、画像をグレースケールで表示することを指定します。今回はサブプロットではなく、最初の画像を1枚表示するだけです。

plt.show():
実際に画像をウィンドウに表示します。

print(f"Label: {label}"):
取得した画像に対応するラベルを表示します。

最後に

第2回目の今回は画像系データセットの扱い方に着目して学習をしました。モデル実装の方ばかりに目がいってしまいますが、モデルに突っ込むデータがしっかり前処理されているのか確認することの大切さも学べました。次回はモデル構築について学びます。

Discussion