😸

PyTorchのDatasetでImageNet(ILSVRC2012)を扱う

14 min read

PyTorchのDatasetで,ImageNet(ILSVRC2012)を扱う方法を記します.本記事はある程度PyTorchの使い方に慣れている人向けに書かれております.ImageNetで一からDNNを学習したい方のお役に立てると思います.

ImageNet(ILSVRC2012)のダウンロードについて

ImageNet とは,一言で言えば超巨大な画像データベースです.ImageNetについてと,ダウンロード方法は以下の記事をご覧ください.ImageNetの概要と,本記事で必要なデータセットのダウンロード方法を分かりやすく説明しています.

https://zenn.dev/hidetoshi/articles/20210703_how_to_download_imagenet

対象とするファイルのダウンロード

本記事の解説は,ImageNet公式サイトの以下のページ(ILSVRC2012のデータセットをダウンロードできるページ)から,必要なファイルをダウンロードしているという前提で行います.アクセス権限がないとダウンロード出来ないので,まだアクセス権限を入手していない方は上述したリンクの記事を参考にしながら,アクセス権限を入手してください.

https://image-net.org/challenges/LSVRC/2012/2012-downloads.php

このダウンロードページから,以下の3つをダウンロードしましょう.

  1. Development kit (Task 1 & 2). 2.5MB.
  2. Training images (Task 1 & 2). 138GB. MD5: 1d675b47d978889d74fa0da5fadfb00e
  3. Validation images (all tasks). 6.3GB. MD5: 29b22e2961454d5413ddabcf34fc5622

誤解が内容に,それぞれのファイル名を以下に記載しておきます.

  1. ILSVRC2012_devkit_t12.tar.gz
  2. ILSVRC2012_img_train.tar
  3. ILSVRC2012_img_val.tar

以降の説明では,これらを配置したフォルダ上で実行されるものとします.

$ tree
.
├── ILSVRC2012_devkit_t12.tar.gz
├── ILSVRC2012_img_train.tar
└── ILSVRC2012_img_val.tar

ImageFolderで訓練データをDatasetとして読み込む

まずは,訓練データをDatasetとして読み込みます.そのために,torchvision の ImageFolder クラスを使います.

torchvision をインストールしていない方は,以下のコマンドでインストールしておきましょう.

$ pip install torchvision

ILSVRC2012_img_train.tar(訓練データ)の展開

ひとまず,訓練データがまとめられている ILSVRC2012_img_train.tar を展開しておきましょう.138GBもあるので,時間がかかるかと思いますが気長に待ちましょう.以下のコマンドで展開できます.展開先フォルダとして,ISLVRC2012_img_trainというフォルダを作って,そこの中に展開しています.

$ mkdir ILSVRC2012_img_train
$ tar -xvf ILSVRC2012_img_train.tar -C ILSVRC2012_img_train/

treeコマンドを実行して,以下のような実行結果になっていれば成功です.見やすさのため,この記事ではかなり少ないファイル数だけ表記しています.[1]また,headコマンドで上10行だけ表示しています.

$ tree ILSVRC2012_img_train | head
ILSVRC2012_img_train
├── n01440764
│   ├── n01440764_10026.JPEG
│   ├── n01440764_10027.JPEG
│   └── n01440764_10029.JPEG
├── n01443537
│   ├── n01443537_10007.JPEG
│   ├── n01443537_10014.JPEG
│   └── n01443537_10025.JPEG
├── n01484850

ここで,この例では各フォルダ(n01440764やn01443537)内に3枚しか画像が入っていませんが,読者の皆さんが同じことをした場合,膨大なファイルがずらずらと表示されるかと思います.

この展開後の構造が大事です.ImageNet(ILSVRC2012)の訓練データは, 画像データがクラス毎にフォルダ分けされています. ここで,n01440764 や n01443537 は,WordNet ID と呼ばれるもので,クラスを識別するための,ImageNet上の一意なIDを表しています.つまり,1つのフォルダに入っている複数の画像は全部同じクラスに属することになります.
この構造は非常に分かりやすく,次に説明するImageFolderクラスで簡単に扱える構造になっています.

ImageFolder とは

ImageFolder とは,画像データとそのクラスを扱うことに特化したDatasetクラスのサブクラスです.巨大な画像データセットにも対応できるように, 画像データにアクセスするたびに逐次SSDやHDD等に保存されているファイルにアクセスする という仕組みをもっています.
よくあるDataset クラスを用いる場合は,データをすべてメモリ上に展開する必要があります.MNISTやCIFAR10等の軽量な画像ベンチマークでは,メモリ上にすべて展開することが可能ですが,ImageNetのような巨大なデータセットでは,並のパソコンではもちろんのこと,ちょっとしたワークステーションでも不可能です.そのため,SSDやHDDに逐次アクセスを行う仕組みが必要になってきます.
逐次アクセスでは読み込み時間がボトルネックになると思われるかもしれません.それは間違いではないのですが,DataLoaderの仕組みで並列アクセスすることにより,DNNの学習中に並行して読み込んだりすることで影響を軽減できます.

さて,このImageFolderクラスの使い方ですが,実はImageNetの訓練データの場合は以下のようにすれば簡単に使えるようになります.

imagefolder_for_train_imagenet.py
# -*- coding: utf-8 -*-
from torchvision import transforms
from torchvision.datasets import ImageFolder

if __name__ == '__main__':
    # ImageNetの訓練データのパス(train_root), ImageFolderのrootに設定
    train_root = './ILSVRC2012_img_train'

    # ImageFolderの前処理, ImageFolderのtransoformに設定
    train_transform = transforms.Compose([
        transforms.Resize(224), # 1辺が224ピクセルの正方形に変換
        transforms.ToTensor()   # Tensor行列に変換
    ])

    # ImageFolderのインスタンス生成
    trainset = ImageFolder(root=train_root, # 画像が保存されているフォルダのパス
                           transform=train_transform) # Tensorへの変換

    # 動作確認
    img, label = trainset[1]
    print('img = ', img)
    print('class(WordNet ID) = ', trainset.classes[label])

ImageFolderのインスタンス生成で大事な引数は,rootと,transformの2つです.

trainset = ImageFolder(root=train_root,
                       transform=train_transform)

rootは,画像が保存されているフォルダのパスを指定する引数です.指定先のフォルダでは,画像をクラスごとにフォルダ分けしておく必要があります. ImageFolderは,フォルダでクラスを識別しているわけですね.これは,上述した ImageNetの訓練データを展開した状態と合致しています. つまり,展開した先のフォルダをそのまま指定しておけば良いです.
transformは,画像をTensorに変換する処理を記述します.機械学習でいうところの,いわゆる前処理部分に当たります.ここはここで非常に奥深いのですが,本記事においては動作確認することにおいて最低限の処理の,リサイズと,Tensorへの変換だけ記述しています.

このPythonスクリプトを,ImageNetの訓練データが保存されているフォルダ(ILSVRC2012_img_train)と同じフォルダに配置して,実行してみましょう.すると,以下のようになります.

$ python imagefolder_for_train_imagenet.py 
img =  tensor([[[0.0745, 0.0863, 0.0980,  ..., 0.9804, 0.9804, 0.9804],
         [0.0667, 0.1059, 0.1373,  ..., 0.9843, 0.9843, 0.9804],
         [0.0706, 0.1255, 0.1686,  ..., 0.9843, 0.9843, 0.9804],
         ...,
         [0.1294, 0.1255, 0.1255,  ..., 0.4471, 0.4392, 0.4314],
         [0.1216, 0.1176, 0.1176,  ..., 0.4392, 0.4314, 0.4235],
         [0.1098, 0.1059, 0.1098,  ..., 0.4353, 0.4275, 0.4196]],

        [[0.0863, 0.0980, 0.1098,  ..., 0.9804, 0.9804, 0.9804],
         [0.0784, 0.1176, 0.1490,  ..., 0.9843, 0.9843, 0.9804],
         [0.0824, 0.1373, 0.1804,  ..., 0.9843, 0.9843, 0.9804],
         ...,
         [0.1255, 0.1294, 0.1294,  ..., 0.4353, 0.4275, 0.4196],
         [0.1137, 0.1176, 0.1216,  ..., 0.4275, 0.4196, 0.4118],
         [0.1020, 0.1059, 0.1137,  ..., 0.4235, 0.4157, 0.4078]],

        [[0.0510, 0.0627, 0.0706,  ..., 0.9804, 0.9804, 0.9804],
         [0.0431, 0.0784, 0.1098,  ..., 0.9843, 0.9843, 0.9804],
         [0.0471, 0.0980, 0.1373,  ..., 0.9843, 0.9843, 0.9804],
         ...,
         [0.0627, 0.0667, 0.0667,  ..., 0.4078, 0.4000, 0.3922],
         [0.0549, 0.0588, 0.0588,  ..., 0.4000, 0.3922, 0.3843],
         [0.0431, 0.0471, 0.0510,  ..., 0.3961, 0.3882, 0.3804]]])
class(WordNet ID) =  n01440764

画像のTensorと,クラスを表すWordnet IDが表示されはずです.これで,通常のDatasetと同様のインタフェースで使えるようになりました.あとは,DataLoader等を用いて学習するだけです.

ImageFolderで評価用データをDatasetとして読み込む

続いて,評価用データ(ILSVRC2012_img_val.tar)の読み込み方法の説明です.こちらは,訓練データとは違って一手間加える必要があります.

ひとまずILSVRC2012_img_val.tar を展開して,中身を見てみましょう.

$ mkdir ILSVRC2012_img_val
$ tar xf ILSVRC2012_img_val.tar -C ILSVRC2012_img_val/
$ ls ILSVRC2012_img_val | head
ILSVRC2012_val_00000001.JPEG
ILSVRC2012_val_00000002.JPEG
ILSVRC2012_val_00000003.JPEG
ILSVRC2012_val_00000004.JPEG
ILSVRC2012_val_00000005.JPEG
ILSVRC2012_val_00000006.JPEG
ILSVRC2012_val_00000007.JPEG
ILSVRC2012_val_00000008.JPEG
ILSVRC2012_val_00000009.JPEG
ILSVRC2012_val_00000010.JPEG

以上のコマンドを実行すると,ファイル名がずらずらと表示されます.今回も head コマンドで最初の10行だけ表示しています.

ご覧の通り,評価用データにはクラス分けされておらず,これだけでは画像の所属するクラスがわかりません. 訓練データのようにフォルダ分けもされておらず,このままでは ImageFolder クラスで読み込むことが出来ません.

そこで必要になるのが, ILSVRC2012_devkit_t12.tar.gz です.この中に,評価用データのクラス情報が入っています.以下のコマンドで解凍しましょう.

$ tar -zxvf ILSVRC2012_devkit_t12.tar.gz
$ ls ILSVRC2012_devkit_t12
COPYING  data  evaluation  readme.txt

以降の手順で大事になるのは dataフォルダの中身にあるファイルです.

$ ls ILSVRC2012_devkit_t12/data/
ILSVRC2012_validation_ground_truth.txt  meta.mat

ILSVRC2012_validation_ground_truth.txt は,評価用データのクラス(WordNet ID)のインデックスを示しています.
上10行だけの中身を見てみると,以下のようになります.

$ cat ILSVRC2012_devkit_t12/data/ILSVRC2012_validation_ground_truth.txt | head
490
361
171
822
297
482
13
704
599
164

1行毎に整数が表示されましたね.この整数が,クラスと対応したインデックスを表しています.ILSVRC2012_img_val.tar の中身の画像が対応しています.
つまり,ILSVRC2012_val_00000001.JPEG のクラスを示すインデックスは490で,ILSVRC2012_val_00000002.JPEG361となります.

しかしお気づきの通り,これはあくまでクラス(WordNet ID)のインデックスなので,さらに変換する必要があります.訓練データのようにWordNet ID でフォルダ分けしないと,訓練データと評価用データのクラスの対応がとれないため,評価ができませんよね.

これらのインデックスとクラスを表すWordNet ID の対応は,meta.matにかかれています.meta.matは,MATLABという数値解析ソフトウェア用のフォーマットで保存されたファイルです.基本的にはこのMATLAB で読み込むためのファイルなので,普段MATLABを使わない方は扱いづらいかと思います.
しかし幸いなことに,Python のScipyモジュールを使えば Pythonの処理上で読み込むことが可能です.具体的な処理手順をお見せする前に,以下のコマンドでScipyをインストールしておきましょう.

$ pip install scipy

これで前提知識の説明と準備が完了しました.それでは,評価用データをImageFolderで読み込めるように加工するスクリプトを記述しましょう.以下にそのスクリプトを示します.

decompress_imagenet_valid.py
# -*- coding: utf-8 -*-
'''
ImageNetのvalid用tarを,ImageFolderで読み込める形で展開するためのスクリプト
'''
import os
import scipy.io
import tarfile

if __name__ == '__main__':
    # 加工処理に必要なファイル・ディレクトリのパス
    imagenet_valid_tar_path = './ILSVRC2012_img_val.tar'
    target_dir = './ILSVRC2012_img_val_for_ImageFolder' # 出力先を変えたいときはここを変更する.
    meta_path = './ILSVRC2012_devkit_t12/data/meta.mat' # ラベル番号とWordNet IDの対応関係
    trueth_label_path = './ILSVRC2012_devkit_t12/data/ILSVRC2012_validation_ground_truth.txt'

    # ラベルIDの変換用dict
    meta = scipy.io.loadmat(meta_path, squeeze_me=True)
    ilsvrc2012_id_to_wnid = {m[0]: m[1] for m in meta['synsets']}

    # 画像のIDに対応するILSVRC_ID(ラベル)を取得しておく
    with open(trueth_label_path, 'r') as f:
        ilsvrc_ids = tuple(int(ilsvrc_id) for ilsvrc_id in f.read().split('\n')[:-1])

    # 1000個のWordNet IDを示すフォルダを作成しておく
    for ilsvrc_id in ilsvrc_ids:
        wnid = ilsvrc2012_id_to_wnid[ilsvrc_id]
        os.makedirs(os.path.join(target_dir, wnid), exist_ok=True)

    os.makedirs(target_dir, exist_ok=True)     # 出力先フォルダの作成を作成
    # 展開していく
    num_valid_images = 50000
    with tarfile.open(imagenet_valid_tar_path, mode='r') as tar:
        for valid_id, ilsvrc_id in zip(range(1, num_valid_images+1), ilsvrc_ids):
            wnid = ilsvrc2012_id_to_wnid[ilsvrc_id]
            filename = 'ILSVRC2012_val_{}.JPEG'.format(str(valid_id).zfill(8))
            print(filename, wnid)
            img = tar.extractfile(filename)
            with open(os.path.join(target_dir, wnid, filename), 'wb') as f:
                f.write(img.read())

・・・そんなに工夫はありません.ただただ泥臭く処理を書いてみました.強いて言えば,以下の処理が最も重要でしょうか.

# ラベルIDの変換用dict
meta = scipy.io.loadmat(meta_path, squeeze_me=True)
ilsvrc2012_id_to_wnid = {m[0]: m[1] for m in meta['synsets']}

ここで,meta.mat を読み込んで,クラスのインデックスとWordNet ID の対応をdict形式で作成しています.具体的には以下のようなイメージです.キーがインデックス, 値がWordNet IDになっています.

>>> ilsvrc2012_id_to_wnid
{1: 'n02119789', 2: 'n02100735', 3: 'n02110185',...省略}

あとは,振り分け先のフォルダを作ったり実際に振り分けたり,といった処理をしています.

以下のファイル・フォルダがあるフォルダで実行してみましょう.

  • ILSVRC2012_img_val.tar
  • ILSVRC2012_devkit_t12 (ILSVRC2012_devkit_t12.tar.gz の展開後のフォルダ)
$ python decompress_imagenet_valid.py 
ILSVRC2012_val_00000001.JPEG n01751748
ILSVRC2012_val_00000002.JPEG n09193705
ILSVRC2012_val_00000003.JPEG n02105855
ILSVRC2012_val_00000004.JPEG n04263257
ILSVRC2012_val_00000005.JPEG n03125729
・・・省略・・・

これで振り分けが終わりました.振り分けの出力先は,./ILSVRC2012_img_val_for_ImageFolderとなります.
出力先を変えたい方は,スクリプトpython:decompress_imagenet_valid.pytarget_dirを書き換えてください.

それでは中身を確認してみましょう.

$ ls ILSVRC2012_img_val_for_ImageFolder/
n01440764  n01986214  n02110185  n02488702  n03042490  n03662601  n04033995  n04487394
n01443537  n01990800  n02110341  n02489166  n03045698  n03666591  n04037443  n04493381
・・・省略・・・

WordNet IDが名前となったフォルダが作成されていますね.これらの中に画像も振り分けられています.

ここまでくれば,あとは訓練データと同じ手順で ImageFolder で読み込みができます.ほとんどimagefolder_for_train_imagenet.pyと同じですが,以下のようなスクリプトで読み込めます.

imagefolder_for_valid_imagenet.py
# -*- coding: utf-8 -*-
from torchvision import transforms
from torchvision.datasets import ImageFolder

if __name__ == '__main__':
    # ImageNetの訓練データのパス(valid_root), ImageFolderのrootに設定
    valid_root = './ILSVRC2012_img_val_for_ImageFolder'

    # ImageFolderの前処理, ImageFolderのtransoformに設定
    valid_transform = transforms.Compose([
        transforms.Resize(224), # 1辺が224ピクセルの正方形に変換
        transforms.ToTensor()   # Tensor行列に変換
    ])

    # ImageFolderのインスタンス生成
    validset = ImageFolder(root=valid_root, # 画像が保存されているフォルダのパス
                           transform=valid_transform) # Tensorへの変換

    # 動作確認
    img, label = validset[1]
    print('img = ', img)
    print('class(WordNet ID) = ', validset.classes[label])

実行すると,以下のようになります.

$ python imagefolder_for_valid_imagenet.py 
img =  tensor([[[0.9725, 0.9569, 0.9569,  ..., 0.9686, 0.9765, 0.9882],
         [0.9569, 0.9098, 0.8863,  ..., 0.9216, 0.9490, 0.9804],
         [0.9608, 0.8980, 0.8392,  ..., 0.8902, 0.9451, 0.9843],
         ...,
         [0.9529, 0.9059, 0.8706,  ..., 0.9059, 0.9490, 0.9804],
         [0.9608, 0.9255, 0.9216,  ..., 0.9451, 0.9569, 0.9804],
         [0.9882, 0.9804, 0.9843,  ..., 0.9843, 0.9804, 0.9882]],

        [[0.9804, 0.9725, 0.9765,  ..., 0.9804, 0.9804, 0.9922],
         [0.9725, 0.9412, 0.9333,  ..., 0.9529, 0.9569, 0.9843],
         [0.9725, 0.9373, 0.9020,  ..., 0.9294, 0.9529, 0.9882],
         ...,
         [0.9804, 0.9412, 0.9137,  ..., 0.9255, 0.9608, 0.9882],
         [0.9765, 0.9490, 0.9490,  ..., 0.9608, 0.9725, 0.9882],
         [0.9882, 0.9843, 0.9882,  ..., 0.9922, 0.9922, 0.9961]],

        [[0.9843, 0.9804, 0.9804,  ..., 0.9843, 0.9882, 0.9882],
         [0.9843, 0.9569, 0.9490,  ..., 0.9647, 0.9765, 0.9882],
         [0.9843, 0.9608, 0.9294,  ..., 0.9529, 0.9765, 0.9922],
         ...,
         [0.9882, 0.9725, 0.9608,  ..., 0.9725, 0.9804, 0.9843],
         [0.9843, 0.9725, 0.9765,  ..., 0.9804, 0.9882, 0.9922],
         [0.9843, 0.9882, 0.9922,  ..., 0.9882, 0.9922, 0.9922]]])
class(WordNet ID) =  n01616318

これにて,ImageNet(ILSVRC2012)の評価用データもPyTorchで扱えるように読み込めることが可能となりました.

コードのダウンロード

本記事のコードは以下のページ(Github)からダウンロードできます.なお,MITライセンスで公開しております.改変・公開等ご自由にお使いください.

https://github.com/HidetoshiKawaguchi/tech-blog-codes/tree/main/20210717_pytorch_datasetfor_imagenet

おわりに

PyTorchのDatasetで,ImageNet(ILSVRC2012)を扱う方法を紹介しました.ImageNetでDNNのベンチマークテストを行い方の役に立ったのであれば幸いです.

脚注
  1. 筆者は,デバッグ用等のために,各クラス3枚ずつ合計3,000枚に間引いて実行しています. ↩︎