Pytorch:data
Pytorch でデータセットを扱う練習と,データ拡張(data augmentation)機能の実装方法について学びます.
Pytorch におけるデータローダーとは
PyTorchにおける DataLoader は、大規模なデータセットを効率的にミニバッチに分割し、前処理やシャッフルを行いながら,モデルに供給するための機能です。
前処理には,Pytorchでの用いた学習・推論のために最低限必要な処理と,
- 縦横のサイズ(解像度)調整
- 型キャスト
学習後の精度をより良くするためのデータ拡張があります.
- ランダムな左右反転
- ランダムな並進
- ランダムな回転
- ランダムな色変更
- ランダムなマスク処理
など.
データローダーの基本
以下は,最もシンプルな例.
# ライブラリのインポート
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
# 前処理
transform = transforms.Compose([
transforms.ToTensor(),
])
# データローダーの作成
ds = datasets.STL10(root='./data', split='train', download=False, transform=transform)
dl = DataLoader(ds, batch_size=64, shuffle=True)
ここで,torchvision とは,Pytorch で画像データを扱うときに使える便利なライブラリです.Torchvision に含まれる datasets というモジュールはデータのファイルの読み込みやデータセットの分割(学習用・検証用・テスト用に分割)などに,transforms というモジュールはデータの前処理に,それぞれ使われます.また,torch.utils.data (torch の中の utils の中の data というモジュール)に含まれる DataLoader というモジュールは,その名の通りデータローダーを作成するために使われます.
ちなみに,音声データを扱いたい場合には,torchaudio というライブラリもあります.(私は使ったことはありませんが.)
datasets(モジュール)
この datasets というモジュールでは,CIFAR-10 や STL-10 のように,ベンチマークによく使われるデータセットを扱うためのクラスが用意されています.上記の例では,datasets.STL10 というクラスが使われていますね.このようなデータセットは,大抵の場合,画像が .png や .jpeg のような標準の画像ファイルではなく,バイナリ形式で保存されています.試しに STL-10 のデータセットが保存されているディレクトリを見てみると,以下のようになっています.
$ ls ./data/stl10_binary class_names.txt fold_indices.txt test_X.bin test_y.bin train_X.bin train_y.bin unlabeled_X.bin
独自のデータセットを扱う場合には,通常はデータは画像ファイルとして保存されていると思います.その場合,ImageFolder というクラスを使えます(識別タスクの場合).これについては,後ほど演習で扱います.
transforms(モジュール)
前処理の機能を集めたモジュールです.
以下のように処理を並べてやると,書いた順序で実行してくれます.
transform = transforms.Compose([
処理1,
処理2,
処理3,
...
])
ここで使われる処理のほとんどは transforms モジュールの中に用意されており,自分で実装する必要はありません.もちろん,transforms モジュールに含まれない,独自の処理を実装することも可能ですが,そのような機会はあまりないと思います.
必ず必要なのは,transforms.ToTensor() です.これは,読み込んだデータの型をキャストして torch.Tensor 型にします.ちなみに,transforms.ToTensor 自体はクラスであるため,以下のようには書けません.
transform = transforms.Compose([
transforms.ToTensor,
...
])
以下のように,transforms.ToTensor() として生成したインスタンスを並べるのが,正しい書き方です.これは,ToTensor 以外のクラスでも同様です.
transform = transforms.Compose([
transforms.ToTensor(),
...
])
DataLoader(クラス)
上記の例の一部を再掲します.
dl = DataLoader(ds, batch_size=batch_size, shuffle=True)
ここで,ds は datasets.STL10 クラスのインスタンスであり,ミニバッチに分割する前の状態のデータの集まりであると考えて構いません.実際には,この時点ではデータの実体はメモリ上にはありません.(もしそんなことをしたら,巨大なデータセットを扱う場合にメモリがいくらあっても足りません.)以下のように書くと,ミニバッチを生成できますが,この時点で初めてデータがメモリ上に展開されます.
for minibatch in dl:
...
ミニバッチごとの処理
...
また,他に押さえておくポイントは,Dataloader クラスのインスタンス dl を生成するときに,DataLoader (のコンストラクタ)に与える引数 batch_size と shuffle です.これらのうち,batch_size は,名前から想像される通りにミニバッチの大きさ(データの数)を指定します.
また,shuffle = True とすると,ミニバッチのデータをランダムにサンプリングするようになります.逆に,shuffle = False とすると(デフォルトは False),ミニバッチのデータが読み込まれる順序が固定されます.一般には,学習時にはランダム性が重要なので,学習用データについては shuffle = True とします.一方,検証用やテスト用データについては,ランダム性は必要ないため,余計な計算コストを抑えるために shuffle = False とします.
これらの他にも,重要な引数がいくつかありますので,後ほど解説します.
演習
では,実際に DataLoader を作成してみましょう.今回は,CUB200-2011 という,200クラス(200種の鳥類)の分類問題のデータセットを扱います.
プログラムの準備
プログラムをダウンロードします.このプログラムを完成させることが,今回の目標です.
$ wget https://vrl.sys.wakayama-u.ac.jp/class/pytorch_tutorial/exersise_data/exersise_data.py
データセットの準備
続いて,今回使うデータセットをダウンロードして解凍します.作業用ディレクトリに移動して,以下を実行してください.
$ wget -P ./data/ https://vrl.sys.wakayama-u.ac.jp/class/pytorch_tutorial/datasets/CUB_200_2011.tgz $ cd data $ tar -xzvf CUB_200_2011.tgz $ cd ..
中身を確認してみましょう.次のコマンドで,200個のサブディレクトリが見えるはずです.
$ ls data/CUB_200_2011/images
どれでも良いので,サブディレクトリの中も覗いてみましょう.
$ ls data/CUB_200_2011/images/001.Black_footed_Albatross
このように,一つのディレクトリが一つのクラスに対応し,その中に対応するクラスの画像が入っています.
データセットの読み込みと分割
では,プログラムを書いていきましょう.
データセットの読み込み
まずは,データを読み込んで,データセットのサイズ(=データ数)を表示します.以下の通りに書いてください.
from torchvision import datasets, transforms
transform = transforms.Compose([
transforms.ToTensor(),
])
ds = datasets.ImageFolder(root='./data/CUB_200_2011/images', transform=transform)
print("Number of images:", len(ds))
assert False #ここでわざとエラーにしてプログラムを強制終了
ここで,ImageFolder というものが使われています.これは,指定したディレクトリの中で,画像ファイルがサブディレクトリに分かれて配置されているときに使います.各サブディレクトリ内の画像は同一クラスとして扱われます.
学習用・検証用・テスト用に分割
データセットを学習用・検証用・テスト用の3つに分割します.CUB200_2011 では,あらかじめどのデータが学習用で,どのデータがテスト用か,決められています.しかし,今回はコードを簡単にするために,ランダムに分割します.
データセットの分割には,torch.utils.data.dataset の中にある Subset を使います.以下のように加筆してください.太字のところが差分です.
from torchvision import datasets, transforms
from torch.utils.data.dataset import Subset
transform = transforms.Compose([
transforms.ToTensor(),
])
ds = datasets.ImageFolder(root='./data/CUB_200_2011/images', transform=transform)
print("Number of images:", len(ds))
num_train = 6000
num_val = 1000
idx = torch.randperm(len(ds))
idx_train = idx[: num_train]
idx_val = idx[num_train : num_train + num_val]
idx_test = idx[num_train + num_val :]
ds_train = Subset(ds, idx_train)
ds_val = Subset(ds, idx_val)
ds_test = Subset(ds, idx_test)
print("Number of training images", len(ds_train))
print("Number of validation images", len(ds_val))
print("Number of test images", len(ds_test))
print("First 10 indices of training dataset", idx_train[:10])
assert False
ここでは,ランダムに選んだ6000個を学習用,1000個を検証用,残りをテスト用,としています.どのように分割するかは,
idx = torch.randperm(len(ds))
のところで決まります.この torch.randperm(n) という関数は,0, 1, ..., n-2, n-1 の n 個の整数をランダムに並べ替えて返します.このようにして作られた idx の最初の6000個を学習用データのインデックスとします.同様に,次の1000個を検証用データのインデックス,残りの4788個をテスト用データのインデックスとして扱います.
あとは,
ds_train = Subset(ds, idx_train)
のようにして,データセットを分割できます.
しかし,ここで一つ問題が生じます.それは,idx_train[:10] を表示させた結果から分かるように,プログラムを実行するたびにデータの分割方法が変わってしまうことです.これでは正しく評価ができません.対策としては,一度生成した idx をファイルに書き込んで,次からはそれを読み込むようにする,という方法もありますが,ここでは乱数のシードを固定する方針にしましょう.
num_train = 6000 num_val = 1000 torch.manual_seed(2025) idx = torch.randperm(len(ds))
データローダーの作成
データローダーを作成する際に必要なモジュール DataLoader をインポートします.
from torchvision import datasets, transforms from torch.utils.data.dataset import Subset from torch.utils.data import DataLoader
ついでに,前処理も追加してやりましょう.ここでは,画像のリサイズ(解像度の変更)とセンタークロップ(画像の中央で決まったサイズの領域だけを残し,残りを捨てる処理)を追加します.DNN モデルは決まったサイズ(解像度)の入力データしか受け付けない場合もあります.また,データごとに入力サイズが異なると,モデル内部での計算効率が著しく落ちます.そのため,入力データのサイズを揃えることはマストです.
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Resize(144),
transforms.CenterCrop(128),
])
ここで,transforms.Resize(n) は,画像のアスペクト比を維持したまま,縦か横のいずれか小さい方を n に変更します.(m,n) のように引数を2つ与えて,(元のアスペクト比は無視して,)m x n にリサイズすることもできます.
データローダーを作成します.以下のように書いてください.
print("First 10 indices of training dataset", idx_train[:10])
batch_size = 32
num_workers = 4
train_loader = DataLoader(ds_train, batch_size=batch_size, num_workers=num_workers, shuffle=True)
val_loader = DataLoader(ds_val, batch_size=batch_size, num_workers=num_workers, shuffle=False)
test_loader = DataLoader(ds_test, batch_size=batch_size, num_workers=num_workers, shuffle=False)
ここで,新しく num_workers なるものが登場しています.これは,①ファイルからデータを読み込んで,②前処理をして,③モデルに渡す,という一連の処理を担うプロセス数を指定する引数です.デフォルトでは num_workers = 1 となっていますが,それだとデータの読み込みがボトルネックになって学習効率が落ちるため,4 とか 6 くらいにしておくと良いと思います.ただし,num_workers の値は,CPU のコア数よりも大きな値にしてはいけません.このルールを守らないと,最悪システム全体がフリーズします(という警告はでますが,実際にフリーズした経験はありません.ただ,確かに num_workers > コア数 にすると,処理速度は落ちます.)
あとは,
assert False
を消して,プログラムを実行してみましょう.学習が行われて,最後に"Test Accuracy: ..."のようが表示があれば,成功です.最終的なTest Accuracyは,かなり低い数値になる,と思います.これから色々工夫して,精度を改善していきます.
プログラムの学習やテストのフェーズで,データローダーからどのように画像データやクラスラベルのデータが取り出されているか,確認してください.(ImageFolder によって自動的にクラスラベルが振られていることを確認してください.)
入力される画像の正規化
以下のようにすると,入力画像の RGB の各チャネルについて,画素値から 0.5 を引いて,0.2 で割る,という処理が実行されます.これにより,学習後の精度が少し高くなります.なぜ精度が高くなるのかはよくわかりませんが,誤差逆伝搬時の重み勾配のダイナミックレンジが関係している,と思われます.
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Resize(144),
transforms.CenterCrop(128),
transforms.Normalize(mean = (0.5, 0.5, 0.5), std=(0.2, 0.2, 0.2))
])
なお,今回の 0.5 や 0.2 は適当に決めた値です.ImageNet などのベンチマークにおいては,学習データセット全体の平均値や標準偏差をちゃんと計算して使用します.
基本的なデータ拡張
学習データにランダムな摂動(変化)を加えて,精度を向上させます.このように,データに何らかの操作を加えて新たなデータを作成することを「データ拡張」と言います.
ここで,データ拡張は,一般的には学習データのみに対して行います.(精度を上げる目的で,テストデータに対して行う場合もあります.)そのため,前処理の内容を定義する
transform = transforms.Compose([
...
])
の部分は,2つ用意する必要があります.一つは学習用,もうひとつは検証・テスト用のデータに用います.
ここでは,既に作成した transform はそのままにして,学習用の transform_train を作成しましょう.一旦,transform_train の中身は,transform と全く同じものにしてください.
また,少し面倒ですが,データセット ds についても,学習用の ds_train を別に作成してやる必要があります.これは,datasets.ImageFolder を呼ぶ時点で,引数に transform を渡す必要があるからです.これに伴い,
ds_train = Subset(ds, idx_train)
と先ほど書いたところも,修正してください.
ランダムクロップ
では,transform_train の中身をいじりましょう.センタークロップをランダムクロップ(画像のランダムな位置でクロップする操作)に置き換えてみましょう.ランダムクロップの実装には transforms.RandomCrop というクラスが使われます.
プログラムを実行してみて,精度がどのように変化したか確認してください.
ランダム水平反転
続いて,ランダムに画像を左右反転させるデータ拡張をやってみましょう.これには,transforms.RandomHorizontalFlip が使用できます.ここで,
RandomHorizontalFlip(p=0.3)
のように,引数 p を使って反転確率を制御できますが,通常はデフォルトの p=0.5 で良いため,単に
RandomHorizontalFlip()
のように引数を省略して使います.
実行するたびに結果が変わるので一概に言えませんが,少し精度が上がったのではないでしょうか?
注意!ランダムフリップは常に有益とは限りません.例えば,文字認識においては,ランダムフリップは明らかに不適切です.他にも,サワガニにように左右非対称な物体の場合も,適用すべきではないかもしれません.よく似た例で,ランダムローテーション(ランダムに画像を回転させる操作)を使うと,数字の6が9になって意味が変わってしまうことがあります.このような問題が時々発生するので,データに合わせて,どのようなデータ拡張を選ぶべきか,よく考えましょう.
ランダム回転 と 色の摂動
ランダムに回転操作を加える RandomRotation と色に摂動を与える ColorJitter も導入してみましょう.これらは,引数を指定して使う必要があります.どのような引数があるか,検索して調べてみてください.
今回の例では,これらのデータ拡張を導入しても,あまり精度面では目立った改善がみられなかったかもしれません.だからと言って,これらのデータ拡張が無意味ということにはなりません.データ,モデル,その他の様々な条件によっては,これらが非常に有効に機能することもあるはずです.ただし,それは試してみないとわかりません.
データ拡張の実験はここまでです.解答例のプログラムはこの時点のものです.
シャッフルの重要性
続いて,ちょっとした実験をしてみます.次に示すコードは学習用データローダーを生成する部分です.
train_loader = DataLoader(ds_train, batch_size=batch_size, num_workers=4, shuffle=True)
ここでは,shuffle=True として,データを読み込む順序をランダムにしています.これを,shuffle=False とするとどうなるか,実験してみてください.
さらに,次に示すのは,全データから学習用データを取り出す部分です.
ds_train = Subset(ds_train, idx_train)
これを,以下のように変更してください.
ds_train = Subset(ds_train, torch.sort(idx_train)[0])
読み込む画像のインデックスを昇順にソートした上で,ds_train を作成しています.
この状態で学習を行うと,多分精度は壊滅的に低くなるはずです.なぜこのような結果になったか,考えてみてください.
まとめ
識別問題のデータセットの扱い方について,解説と演習を行いました.ファイルからのデータの読み込み方,データの前処理(データ拡張含む)についての諸実験を行いました.
識別問題におけるデータの扱いは比較的単純です.一方,例えば,物体検出のタスクでは,もう少し色々と気を遣う必要がなります.物体検出では,対象物の位置が画像上の座標で与えられるため,リサイズやクロップを行うと,それに応じてアノテーションの方も調整しなければなりません.セグメンテーションのような問題も同様です.
また,最近ではGenerative Adversarial Network(GAN)や拡散モデルを用いて生成した画像を学習に用いるデータ拡張手法についても,盛んに研究されています.
これらのトピックについては,またどこか別の機会があれば,解説します.