【脱MNIST！】機械学習のデータセットの管理と Chainer の Dataset クラスを自作するときのTips

# はじめに

この記事では、機械学習プロジェクトのデータセットの管理と Chainer のDataset クラスを自作する際のTipsを紹介します。データセットの管理については、 Chainer に限らず使える Tips だと思います。あくまで自分が良いと思った開発フローや設計におけるTipsなので、より良い方法がございましたら、コメントしていただけると幸いです。

今回は、以下のブログ記事で紹介されている、CNNとLSTMを用いたファッションコーディネートモデルのプロジェクトを題材にハンズオン形式で、データセットまわりの開発フローや設計のTipsを紹介していきます。

[https://techblog.zozo.com/entry/outfit_generator](https://techblog.zozo.com/entry/outfit_generator)

## 対象者・前提知識

学部4年生など、Chainer を用いて、研究などの機械学習プロジェクトを始められる方が対象です。MNISTのチュートリアルは動かせたけど、論文の再実装や自分の作りたいものを作るためには、どう着手すればよいかわからない、といった初級者から中級者を目指す方も対象です。記事の内容が少なくはないので、実際に自分のプロジェクトを進めていく中で、ぶつかった課題を解決したいときに、読み返すのも良いと思います。

上記の対象者以外でも、Chainerを用いる際に、このような設計があるのか、と参考にしていただけたら幸いです。初中級者向けということもあり、記述が冗長ですので、最後のまとめだけでも読んでいただけたら幸いです。

前提知識として、以下のチュートリアルは完了しているものとします。

[Chainer v4 ビギナー向けチュートリアル - Qiita](https://qiita.com/mitmul/items/1e35fba085eb07a92560)

## 実行環境

- chainer==5.0.0
- chainercv==0.12.0

## プロジェクトのディレクトリ構成

この記事では、データセットまわりのみを紹介しますが、Chainer を用いたプロジェクトのディレクトリ構成は以下のようなものを想定しています。

    .
    ├── configs
    │   └── {model}.yml   # 学習条件の yml ファイル
    ├── notebooks   # デバッグやモデル評価用の ipynb ファイル
    └── scripts
    		├── **datasets   # 自作 Dataset 置き場**
    		├── functions   # 重みを持たない関数の置き場
    		├── links   # モデル、重みを持つ関数の置き場
    		│   └── {model}.py
    		├── extensions   # 自作 Extension 置き場
    		├── utils
    		│ 	├── read_data.py   # 画像などの読み込み
    		│ 	├── transforms.py   # 前処理
    		│		└── visalization.py   # 可視化
    		├── train_{model}.py   # 学習スクリプト
    		└── eval_{model}.py   # 評価スクリプト

今回は特に `scripts/datasets` に焦点を当てます。

# データセットリポジトリの別途作成

この章では、機械学習プロジェクトにおけるデータセット管理の Tips を説明します。

プロジェクトのリポジトリについては前章で説明しましたが、それとは別にデータセット用のリポジトリを作成します。プロジェクトリポジトリの目的は「**ある1つのプロジェクト（研究など）に必要なコードの管理**」であるのに対し、データセットリポジトリの目的は「**ある1つのデータセットのセットアップや加工をするコードの管理**」です。

プロジェクトとデータセットでリポジトリを分ける理由は、あるデータセットを**他のプロジェクトでも再利用しやすくするため**です。プロジェクトに依存しないセットアップや加工のコードはデータセットリポジトリで管理し、プロジェクトに依存するコードはプロジェクトリポジトリで管理します。

また、プロジェクトリポジトリ内のコードからデータセットリポジトリ内のファイルにアクセスする方法として、そのパスを環境変数に設定する方法があります。Pipenv の `.env` ファイルで設定すると楽です。具体的な実装は、 Chainer 
 の Dataset に関する Tips の章で説明します。

データセットリポジトリのディレクトリ構成は以下です。

    .
    ├── README.md   # データセットのスキーマやセットアップ手順
    ├── raw   # 第三者による配布を、そのままの形式で置く場所
    ├── main   # raw を使いやすいように加工した本番データ
    │   ├── images   # 画像
    │   └── labels   # json, csv, txt などのテキスト
    ├── tiny   # main からサンプルしたデバッグ用の小さいデータ
    │   ├── images
    │   └── labels
    └── make_*.py   # 前処理

Polyvore データセットを使って、私が実際に作ったデータセットリポジトリは[こちら](https://github.com/hrsma2i/dataset-MarylandPolyvore)になります。  こちらを見つつ読み進めていただけると、イメージしやすいと思います。

## データセットのダウンロード

今回、用いるデータセットはPolyvoreというファッションコーディネートのデータセットです。 [公式リポジトリ](https://github.com/xthan/polyvore-dataset)から polyvore.tar.gz と polyvore-images.tar.gz を `raw` にダウンロードします。`raw` には基本、第三者が配布したままの形式で置き、read only にします。

今回のデータセットの場合、画像も配布されていますが、json ファイルなどに記載されているURLから画像をダウンロードしなければならない場合もあります。このような場合に使える、非同期処理で高速に大量のファイルをダウンロードする [aiodl](https://github.com/hrsma2i/aiodl) というCLIツールを作成しましたので、よろしければ使ってください。画像5万枚程度なら30分ほどで終わります。（作った後で気づきましたが、同名で別のPythonライブラリがありますので、ご注意ください。）

また、画像全てをダウンロードする前に、次の節を参考に小さいデータセットを先に用意します。理由は、小さいデータセットを先に用意をしておけば、残りをダウンロードしている間に小さいデータセットで開発を進められるからです。

## 小さいデータセットの用意

データセットリポジトリに小さいデータセットを用意する理由は**デバッグを効率化**するためです。

データセットリポジトリにおける具体的な作業は以下です。

1. `raw` にダウンロードしたものを、使いやすい形に加工して `main` に置く。
2. `main` から、少量のデータをサンプルして `tiny` に置く。

`main` から `tiny` を作成する際の注意点は、 **`main` 以下と `tiny` 以下のディレクトリ構成は全く同じにすること**です。理由は、プロジェクトのコード内で参照するパスを `main` から `tiny` に変えるだけで、データセットの規模を切り替えられるようにするためです。

また、json ファイルから少量のデータをサンプルする場合に使える [ml_json_processor](https://github.com/hrsma2i/ml_json_processor) というCLIを作成したので、よろしければ使ってください。 json ファイルを train と test に分割する際にも使えます。

json ファイルに記載されたURLから画像をダウンロードする場合、小規模の json ファイルを作成したタイミングで `make_url_file.py` などのスクリプトを書き、`tiny` → `main` の順で画像をダウンロードします。

また、 `raw`, `main`, `tiny` は、データの著作権や容量的な問題で、基本的には `.gitignore` に記述して git 管理しないようにします。

ところで、「デバッグを効率良く行う」ことが目的であれば、 以下の用にプロジェクトのコード側でデータの規模を小さくする方法も考えられます。

```(プロジェクトリポジトリ内のあるコード).py
    dataset[:100] # 100 サンプルだけ使う
```

こちらの方が簡単ですが、データセットが大きすぎる場合、**データをメモリに読み込む際に長い時間がかかってしまいます**。また、プロジェクトごとに、小さくする記述を書く必要があります。データセットリポジトリ側に責任を持たせることで、プロジェクトのコードに余計な記述をせずに済みますし、**データセットの規模の切り替えをプロジェクト間で再利用しやすくなります**。また、特殊なケースですが、 Amazon の [SageMaker](https://aws.amazon.com/jp/sagemaker/) を使う場合も、今回紹介した方が適しています。SageMakerでは学習のたびにS3から学習インスタンスにデータセットをアップロードする必要があるため、小さいデータセットを用意した方がデバッグを効率的に行えます。

## 重い前処理は予め施しておく

軽い処理（画像の正規化、左右反転など）であれば、後に実装する Dataset クラス内に記述しても良いです。しかし、重い処理（画像特徴量の抽出、ピクセルの値の平均値の計算など）の場合、学習や推論のたびに時間を取られてしまいます。そのため、そのような重い処理は予め施しておき、結果を中間ファイルに保存しておきます。

前処理のスクリプトは、**プロジェクトに依存する処理であればプロジェクトリポジトリ側で、汎用的なものであればデータセットプロジェクト側で管理します。**

# Chainer の Dataset に関する Tips

この章では、 Chainer の Dataset クラスの設計の開発フローの Tips について説明します。この章で使うスクリプトはプロジェクトリポジトリ側で管理します。

## Dataset 間で共通の処理は別モジュールに切り分ける

画像の**読み込み**や、**前処理**など、 Dataset クラスに依存しない共通の処理は Dataset クラスを定義するモジュールとは分けて定義します。そうすることで、それらの処理を Dataset クラス間で共有でき、保守性が上がります。（Dataset クラスAでは前処理の方法を更新したのに、Dataset クラスBでは更新し忘れていて、学習結果がオジャン、、、といったことも回避できます。）

最初は Dataset クラス内に定義しても良いですが、 Dataset を複数作成する際に、共通部分を別モジュールに切り分けます。このとき、バグが混入しやすいので、学習結果がオジャンにならないよう、しっかりテストをしておきます（後述の可視化モジュールの節の jupyter notebook による視覚的なテストを参照）。

今回の例では、画像の読み込みと前処理のモジュールを定義しておきます。

一応、コードの解説も後述しますが、記事のメインテーマから外れるので、読み流して構いません。

```scripts/utils/read_data.py

    from PIL import Image
    from chainercv.transforms import resize
    from chainercv.transforms import resize_contain
    from chainercv.transforms import scale
    from chainercv.utils import read_image
    
    
    def read_square_img(img_file, keep_aspect=True):
        """
    
        Args:
            img_file (str): An image file path.
            keep_aspect (bool, optional): Defaults to True.
    
        Returns:
            img (np.array: (C=3, H=299, W=299)):
                A Chainer format square image.
        """
        img = read_image(img_file)
        img = resize_img(img, keep_aspect=keep_aspect)
        return img
    
    
    def resize_img(img, size=(299, 299), keep_aspect=False):
        """
    
        Args:
            img (np.array: (C=3, H, W)): A Chainer format image.
            size (tuple<int>, optional): Defaults to (299, 299).
            keep_aspect (bool, optional): Defaults to False.
    
        Returns:
            img (np.array: (C=3, H=299, W=299)):
                A Chainer format square image.
        """
        if keep_aspect:
            img = scale(img, size[0], fit_short=False)
            img = resize_contain(img, size, fill=255)
        else:
            img = resize(img, size, Image.BILINEAR)
    
        return img
```

`read_square_img` は、画像ファイルのパスを受け取り、Chainer形式で画像を読み込み、`(3, 299, 299)` の正方形にリサイズして返します。

`read_img` 内の `.convert('RGB')` は、白黒画像を読み込んだ場合にも `(3, H, W)` の形の配列として読み込むための記述です。

`resize_img` は読み込んだ画像を `(3, 299, 299)` にリサイズします。 `keep_aspect=True` であれば、アスペクト比を保ったままリサイズし、できた余白の部分は白(255)でパディングします。`keep_aspect=False` であれば、アスペクト比を無視してリサイズします。

本来、 CNN はアスペクト比の変更にロバストではないため、 `keep_aspect=True` とするのが妥当ですが、先行研究ではアスペクト比が無視されていたため、追実験用に keep_aspect 引数を設けました。

```scripts/utils/transforms.py

    from chainercv.transforms import random_flip
    
    
    def transform_img(img, train=False):
        """
        Args:
            img (np.array: (c=3, h, w)):
                Chainer format image.
        Return:
            img (np.array: (c=3, h, w)):
                Transformed Chainer format image.
        """
        # rescale pix value from [0, 255] to [-1, 1]
        img = 2 * (img / 255 - 0.5)
    
        if train:
            img = random_flip(img, x_random=True)
        return img
```    

前処理では、ピクセルの値の正規化と、ランダムな左右反転の data augumentation を行います。 `train` 引数を設けて、 学習時だけ必要な前処理の切り替えを行います。 `**train` のデフォルトは `False`** にしておきます。理由は、大抵のプロジェクトの場合、**学習より推論の記述をする方が多い**からです。

## Dataset クラスの実装

`chainer.dataset.DatasetMixin` を継承して Dataset クラスを定義します。主にオーバーライドするメソッドは以下の3つです。

- `__init__`
- `__len__`：Dataset 内のデータ数を返します。 Iterator が Dataset を回すときに必要になります。
- `get_example`：データのインデックスを受け取り、そのインデックスのデータを返します。

まず、  `__init__` メソッドをオーバーライドします。

```scripts/datasets/polyvore.py

    import os
    import pandas as pd
    
    class PolyvoreDataset(DatasetMixin):
    
        def __init__(
            self,
            max_num=8,
            keep_aspect=True,
            json_filename='train_no_dup.json',
        ):
    
            super().__init__()
            self.max_num = max_num
            self.keep_aspect = keep_aspect
    
            self.img_dir = os.environ['POLYVORE_IMAGE_DIR']
    
            lbl_dir = os.environ['POLYVORE_LABEL_DIR']
            json_file = os.path.join(lbl_dir, json_filename)
            self.df = pd.read_json(json_file)
```

`__init__` では、主に**データの読み込みや準備**を行います。json や csv ファイルは `pandas` の`DataFrame` に読み込み、 attribute として持たせておくと、後の処理が楽です。**DataFrame の1行が、** `get_example` **で指定するデータの1サンプルに対応**するようにしておきます。今回、データの1サンプルは、1つのコーディネートとしたいです。 Polyvore の json ファイルは以下のようになっているので、 DataFrame に読み込むと、すでに1行が1つのコーディネートを表しているので、今回は特に加工の必要はありません。

```(データセットリポジトリ)/main/labels/train_no_dup.json

    [
    			{
    		        "name": "コーデ名",
    		        "views": 8743,
    		        "items": [
    		            {
    		                "index": 1,
    		                "name": "アイテム名 A",
    		                "price": 24.0,
    		                "likes": 10,
    		                "image": "http://example/image01.jpg",
    		                "categoryid": 4495
    		            },
    		            {
    		                "index": 2,
    		                "name": "アイテム名 B",
    		                "price": 150.0,
    		                "likes": 2250,
    		                "image": "http://example/image02.jpg",
    		                "categoryid": 25
    		            },
    
    		　　　　　　　...
    
    		        ],
    		        "image": "http://example/tiled/image.jpg",
    		        "likes": 394,
    		        "date": "One month",
    		        "set_url": "http://example/set/",
    		        "set_id": "01234567",
    		        "desc": "コーデの説明"
    			},
    
    			...
    
    ]
```

画像データはメモリにすべて乗らないことがほとんどなので、 `get_example` 内で逐次、読み込みます。ただ、画像が置いてあるディレクトリのパスは、`__init__` 内で attribute (`self.img_dir`) として持たせておくと楽です。

また、参照する**データがあるディレクトリのパスを環境変数で指定**することで、コードのポータビリティが上がります。コードのポータビリティは実験の再現性には欠かせません。 Pipenv を用いている場合、これらの環境変数を、以下のように `.env` ファイルに設定しておくのが楽です。

```.env.sh

    export SIZE="tiny"
    export POLYVORE="${HOME}/datasets/Polyvore/${SIZE}"
    export POLYVORE_IMAGE_DIR="${POLYVORE}/images"
    export POLYVORE_LABEL_DIR="${POLYVORE}/labels"
```

次に、 `__len__` を定義します。DataFrame の長さをとるだけです。

```scripts/datasets/polyvore.py

    ...
    
    class PolyvoreDataset(DatasetMixin):
    
        ...
    
        def __len__(self):
            return len(self.df)
```

次に、 `get_example` を定義します。データのインデックスを受け取り、そのインデックスのデータを返すメソッドです。 **`get_example` の返り値であるデータの1サンプルの型は、モデル（の `__call__` メソッド）の入力に対応するように**設計します。（この場合、モデルの `__call__` 内で損失関数を呼び出すことを前提としていますが、損失関数をモデルとは別で定義する場合は、損失関数の入力の型に合わせます。）

今回の例では、モデルの学習時の入力は、あるコーディネートを成すアイテム**画像の系列**ですので、 `get_example` の返り値の型も画像の系列とします。具体的に、以下のように実装します。

```scripts/datasets/polyvore.py

    ...
    import numpy as np
    
    class PolyvoreDataset(DatasetMixin):
    
        ...
    
        def get_example(self, i):
            """
    
            Args:
                i (int): An index of a data sample.
    
            Returns:
                imgs (np.array: (N, C=3, H, W)):
                    A set of item images.
                    N: the number of items in the outfit
            """
            outfit = self.df.iloc[i]
            items = outfit['items'][:self.max_num]
    
            imgs = np.array([
                self.read_img(outfit['set_id'], item['index'])
                for item in items
            ])
            # imgs: (N, C=3, H, W)
    
            return imgs
    
        def read_img(self, set_id, index):
            img_file = os.path.join(
                self.img_dir,
                str(set_id),
                '{}.jpg'.format(index)
            )
            return read_square_img(img_file, keep_aspect=self.keep_aspect)
```

`self.df.iloc[i]` で DataFrame の1行を `outfit` （コーディネート）として取り出します。`items` はリストで、要素は、そのコーデを成すアイテムの情報を持つ辞書です。この items を内包表記で画像の系列に変換しています。その内包表記内で、画像を読み込むときに、 `read_img` という `read_square_img`をラップしたメソッドを呼んでいます。ラッパーを用意した理由は `get_example` 内の可読性を良くするためと、モデルの定性評価時などにPolyvoreの画像の読み込みを簡単にするためです。

また、 Dataset クラスに限った話ではありませんが、 **テンソル（ndarray）の形をコメントとして明記しておく**と可読性があがり、開発しやすいです。

Dataset クラスの実装をまとめると、以下のようになります。

```scripts/datasets/polyvore.py
    import os
    import pandas as pd
    import numpy as np
    
    
    class PolyvoreDataset(DatasetMixin):
    
        def __init__(
            self,
            max_num=8,
            keep_aspect=True,
            json_filename='train_no_dup.json',
        ):
    
            super().__init__()
            self.max_num = max_num
            self.keep_aspect = keep_aspect
    
            self.img_dir = os.environ['POLYVORE_IMAGE_DIR']
    
            lbl_dir = os.environ['POLYVORE_LABEL_DIR']
            json_file = os.path.join(lbl_dir, json_filename)
            self.df = pd.read_json(json_file)
    
        def __len__(self):
            return len(self.df)
    
        def get_example(self, i):
            """
    
            Args:
                i (int): An index of a data sample.
    
            Returns:
                imgs (np.array: (N, C=3, H, W)):
                    A set of item images.
                    N: the number of items in the outfit
            """
            outfit = self.df.iloc[i]
            items = outfit['items'][:self.max_num]
    
            imgs = np.array([
                self.read_img(outfit['set_id'], item['index'])
                for item in items
            ])
            # imgs: (N, C=3, H, W)
    
            return imgs
    
        def read_img(self, set_id, index):
            img_file = os.path.join(
                self.img_dir,
                str(set_id),
                '{}.jpg'.format(index)
            )
            return read_square_img(img_file, keep_aspect=self.keep_aspect)
```

## 可視化モジュールの実装

Dataset クラスの実装が一通り終わったら、データのサンプルを可視化するためのモジュールを実装します。可視化モジュールを作る目的は以下の2つです。

- Dataset クラスの視覚的なテスト
- モデルの定性的評価

Dataset クラスにバグがあった場合、学習する際にエラーが出れば、それに気づけます。しかし、 Dataset クラスにいくつもバグがあった場合、学習スクリプトを通してのデバッグだと、原因の切り分けが難しかったり、起動のオーバーヘッドに時間を取られ、非効率です。そのため、Dataset クラス単体でテストをしておきます。特に画像系のデータを扱う場合、可視化しないと気づかないバグ（バウンディングボックスのアノテーションがズレていた、など）もあります。視覚的なテストは jupyter notebook 上で行います。

また、モデルの定性的評価にデータや予測結果の可視化は欠かせません。その際にも、可視化モジュールを作っておくと、効率的な定性的評価が行え、実験サイクルを素早く回せます。

今回、この段階では、アイテム画像1枚、もしくは、画像の系列、を可視化できれば良いので、特に自作はせず、[chainercv.visualizations.vis_image](https://chainercv.readthedocs.io/en/v0.12.0/reference/visualizations.html?highlight=vis_image#chainercv.visualizations.vis_image) と [chainercv.utils.tile_images](https://chainercv.readthedocs.io/en/v0.12.0/reference/utils.html?highlight=tile_images#chainercv.utils.tile_images) を使います。ただ、今回のプロジェクトでは系列の可視化を良くするので vis_image と tile_images のラッパーを書いておいてもいいかもしれません。

jupyter notebook 上で実行した様子は、以下の図のようになります。

<img width="565" alt="_2019-06-29_23-1b37882f-8bb1-4fcb-8d8d-ef241096f185.34.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154068/c8800ae8-ac70-4851-965e-105056ed40b3.png">


## transform の実装

データの1サンプル（ `Dataset.get_example` の返り値）に対し、前処理を施す transform 関数を実装します。transform は基本、 `get_example` が呼ばれる度に実行するので、軽い前処理（画像の正規化、左右反転など）を記述します。重い処理（画像特徴量の抽出、ピクセルの値の平均値の計算など）の場合は別スクリプトにして、学習・推論前に施しておき、結果を中間ファイルに保存しておきます。

transform を関数で定義し、 `chainer.datasets.TransformDataset` を用いる方法がありますが、そうではなく、 **transform を Dataset クラスのメソッドとして定義し、 `get_example` 内で呼び出します**。`get_example` 内で trasnform を施す理由は以下のメリットがあるためです。

- Dataset クラスを呼び出す度に transform を施す処理を書かなくて良い。
- ゆえに、transform を施し忘れない。
- 学習・推論スクリプトの記述量が減り、可読性が上がる。

特に、「trasnformを施し忘れる」は、（少なからず私は）初心者のときにしがちなミスです。モデルの性能に致命的なダメージを与えるにも関わらず、エラーを吐かないことも多い悪質なバグです。 `get_example` 内で施すことにより、忘れる可能性を低くできます。

関数ではなく、 Dataset クラスのメソッドとして持たせる理由は、 transform の命名の手間が省け、名前空間も節約できる点です。また、 Dataset クラスの attribute に依存した処理の記述もしやすいです。 大抵のプロジェクトでは1つの Dataset クラスに対する前処理は同じであり、transform の引数は  `get_example` の返り値であるため、 **1つの Dataset クラスに対し、 1つの transform を定義することになります**。ですので、 Dataset クラスのメソッドとして持たせてしまいます。ただ、異なる Dataset クラス間で、画像に対しては同じ前処理を施したい、など、一部の処理を共有させたい場合は、その処理を `[transforms.py](http://transforms.py)` などに切り出して、各 Dataset クラスの transform メソッド内で、その処理を呼びます。

今回の例では以下のように実装します。

```scripts/datasets/polyvore.py

    #v---------------変更点-----------------v
    import sys
    from os.path import dirname
    #^---------------変更点-----------------^
    
    ...
    
    #v---------------変更点-----------------v
    scripts_dir = dirname(dirname(__file__))
    sys.path.append(scritps_dir)
    from utils.transforms import transform_img
    #^---------------変更点-----------------^
    
    class PolyvoreDataset(DatasetMixin):
    
        def __init__(
            self,
            max_num=8,
            keep_aspect=True,
            json_filename='train_no_dup.json',
    				#v---------------変更点-----------------v
            transformed=True,
            train=False,
    				#^---------------変更点-----------------^
        ):
    
            super().__init__()
            self.max_num = max_num
            self.keep_aspect = keep_aspect
    				#v---------------変更点-----------------v
            self.train = train
            self.transformed = transformed
    				#^---------------変更点-----------------^
    
            self.img_dir = os.environ['POLYVORE_IMAGE_DIR']
    
            lbl_dir = os.environ['POLYVORE_LABEL_DIR']
            json_file = os.path.join(lbl_dir, json_filename)
            self.df = pd.read_json(json_file)
    
    		...
    
        def get_example(self, i):
    
            ...
    
    				#v---------------変更点-----------------v
    				if self.transformed:
                imgs = self.transform(imgs)
    				#^---------------変更点-----------------^
    
            return imgs
    
    		...
    
    		#v---------------変更点-----------------v
    		def transform(self, example):
            imgs = example
            imgs = np.array([
                transform_img(img, train=self.train)
                for img in imgs
            ])
            return imgs
    		#^---------------変更点-----------------^
```


transform を施すか施さないかを決める `transfomed` 引数のデフォルトは `True` にしておきます。 transform の施し忘れを防ぐためです。また、前処理を学習時と推論時で切り替える `train` 引数も追加しました。

また、画像に対する処理は、他の Dataset クラスでも共有させたいため、`transforms.py` に切り出しておき、 transform メソッド内で呼んでいます。

正直、 Dataset クラス内に定義するのでれば、trasnform を定義せず、すべて `get_example` 内に記述すれば良いとも思います。一応、個人的には「その処理を施したらデータを可視化しづらいかどうか」で、transform か `get_example` 、どちらに記述するかを分けています。しかし、そうなると、 左右反転やクロッピングなどの augumentation は可視化したいですし、正直、私も良い分け方を見つけられていません。また、応用時を考えて、モデル側に持たせるというのも1つの方法です。

## converter の実装

converter の実装はモデルによって必要な場合と、そうでない場合があります。大抵の場合は、 `chainer.dataset.concat_examples` を使えば良いので、 自作する必要はありません。 どのような場合に converter を自作するかの説明の前に、 converter の役割について、おさらいしておきます。converter の役割を明確に理解しておくことで、 converter を自作する際の指針になると思います。**converter の役割**は主に以下の2点です。

1. タプルのバッチをバッチのタプルに変換する。
2. GPU学習時に ndarray をGPUに転送する。

converter の1つ目の役割の例として、 concat_example では以下の図のような処理をします。 

![concat_examples-101de644-ac15-4737-a79b-c81d9ceb9edc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154068/a33e040d-392f-4596-061e-2498fb958f25.png)


converter の入力は、 `Iterator` から渡される、 `Dataset.get_exmaple` の返り値（タプル）の `list` （バッチ）です。それをバッチのタプルに変換し、それがモデルの入力となります。

1つ目の役割が、 `concat_example` では対応できない場合に converter を自作する必要があります。特に、今回の例のようにバッチ内の**1サンプルが可変長**の場合、バッチ方向に concat して、バッチを1つの ndarray にしてしまう  `concat_example` では対応できません。ndarray でなく `list` のままのバッチである必要があります。

今回は以下のように実装しました。また、 converter の入力は `Dataset.get_example` の出力に依存するため、 transform 同様、 **Dataset 1つに対し1つの converter** が必要になります。ですので、**Dataset クラスのメソッド**として持たせます。

```scripts/datasets/polyvore.py

    import os
    import sys
    from os.path import dirname
    
    import numpy as np
    import pandas as pd
    from chainer.dataset import DatasetMixin
    #v---------------変更点-----------------v
    from chainer.dataset import to_device
    #^---------------変更点-----------------^
    
    scripts_dir = dirname(dirname(__file__))
    sys.path.append(scripts_dir)
    from utils.transforms import transform_img
    from utils.read_data import read_square_img
    
    class PolyvoreDataset(DatasetMixin):
    
    		...
    		
    		#v---------------変更点-----------------v
    		def converter(self, batch, device=-1):
            """
    
            Args:
    						batch
    	            imgs_b (list<np.array>: B * (N, C, H, W))
    	                A batch of image sequences.
                device (int, optional):
                    >= 0 : GPU
                    -1 : CPU
            """
    				imgs_b = batch
            imgs_b = [
                to_device(device, imgs)
                for imgs in imgs_b
            ]
            return imgs_b
    		#^---------------変更点-----------------^
```

今回は入力が画像の系列のみなので、 `imgs_b = batch` としましたが、データが複数ある場合は、 `data1_b, data2_b, ... = list(zip(*batch))` とすることで、タプルのバッチをバッチのタプルに変換できます。

ところで、可変長のサンプルへの対応の処理などを、Iterator や Updater を自作することで対応する方法もあります。しかし、 Iterator の主な役割は「Dataset のサンプルを巡回すること」、 Updater はの主な役割「毎 iteration ごとに、 あるバッチを使ってモデルの重みを更新すること」です。ですので、可変長サンプルへの対応は、「タプルのバッチをバッチのタプルにする」役割を持つ converter に責任を持たせた方が適切だと考えられます。また、 Iterator や Updator より converter の方が処理や構造がシンプルですので、 converter を書き換えた方が簡単です。ですので、 Iterator や Updater をオーバーライドする前に、まず  converter のオーバーライドで対応できないかを考えます。

converter も、以下の図の jupyter 上でテストします。

<img width="567" alt="_2019-06-29_23-b1fc080e-48c2-4030-8a2a-91cd03d52ad3.40.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154068/7737d1ae-c8ba-91a5-ca88-b209a5c445bd.png">


デバッグ時のバッチサイズは基本的に2が良いと考えています。理由は、大きいほど計算に時間がかかりますし、1だと複数のときにしか発生しないバグを見つけられないことがあるからです。

## DatasetBase クラスの実装

以上で、1つの Dataset クラスを実装する開発フローは終了です。この節では、複数の Dataset クラスを実装するときの Tips について説明します。 共通部分を多く含んだ、複数の Dataset クラスを作る場合、DatasetBase という親クラスを作ります。例えば、同じデータセット（Polyvore など）でも、学習・評価、評価タスク、モデルによって `get_example` の返り値の型を変えなければいけないことがあります。複数の Dataset クラスを作るとなると、jsonファイルの読み込みや、画像ディレクトリのパスの設定など、共通部分が多く保守性が落ちます。（Dataset クラスAの読み込み部分は変更したのに、 Dataset クラスBは変更し忘れていた、などのミスが起きやすくなります。）そこで、親クラスとなる DatasetBase クラスを1枚噛ませ、それに共通部分を持たせます。

今回の例では、FITB（Fill in the Blank）という評価タスクの Dataset クラスを追加してみます。FITBとは以下の図のように、いくつかのアイテム（未完成のコーデ）に合うアイテムを、4つの選択肢から推薦するタスクです。

<img width="332" alt="_2019-06-29_23-1c20007c-d70d-428c-8727-513c6f89069c.24.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154068/d7d01eca-ad9d-079f-61b8-bbb2c316be28.png">


まず、先ほど定義した `PolyvoreDataset` から汎用的な処理を切り出して `PolyvoreDatasetBase` クラスを実装します。そして、その DatasetBase クラスを継承して `PolyvoreFitbDataset` を実装します。具体的な実装は以下になります。

```scripts/datasets/polyvore_base.py

    import os
    import sys
    from abc import ABCMeta, abstractmethod
    from os.path import dirname
    
    import pandas as pd
    from chainer.dataset import DatasetMixin
    
    scripts_dir = dirname(dirname(__file__))
    sys.path.append(scripts_dir)
    from utils.read_data import read_square_img
    from utils.transforms import transform_img
    
    
    class PolyvoreDatasetBase(DatasetMixin):
    
        __metaclass__ = ABCMeta
    
        def __init__(
            self,
            keep_aspect=True,
            json_filename='train_no_dup.json',
        ):
    
            super().__init__()
            self.keep_aspect = keep_aspect
    
            self.img_dir = os.environ['POLYVORE_IMAGE_DIR']
    
            lbl_dir = os.environ['POLYVORE_LABEL_DIR']
            json_file = os.path.join(lbl_dir, json_filename)
            self.df = pd.read_json(json_file)
    
        def __len__(self):
            return len(self.df)
    
        @abstractmethod
        def get_example(self, i):
            raise NotImplementedError()
    
        def read_img(self, set_id, index):
            img_file = os.path.join(
                self.img_dir,
                str(set_id),
                '{}.jpg'.format(index)
            )
            return read_square_img(img_file, keep_aspect=self.keep_aspect)
    
        def read_img_from_item_id(self, item_id):
            set_id, index = item_id.split('_')
            return self.read_img(set_id, index)
```

`PolyvoreDataset` から json ファイルの読み込み、画像ディレクトリのパスの設定など、学習・評価、評価タスク、モデルに依存しない部分を `PolyvoreDatasetBase` に切り出しました。  `get_example` は各 Dataset に依存しますが、必ず必要なので、抽象メソッドにします。 画像の読み込みは学習・評価、評価タスク、モデルに依存せず、1つのデータセットでだいたい同じですので、 `read_img` メソッドを DatasetBase クラスに持たせておきます。

続いて、上記の `PolyvoreDatasetBase` クラスを継承して作り直した `PolyvoreDataset` が以下になります。

```scripts/datasets/polyvore.py

    import sys
    from os.path import dirname
    
    import numpy as np
    from chainer.dataset import to_device
    
    scripts_dir = dirname(dirname(__file__))
    sys.path.append(scripts_dir)
    from utils.transforms import transform_img
    from datasets.polyvore_base import PolyvoreDatasetBase
    
    
    class PolyvoreDataset(PolyvoreDatasetBase):
    
        def __init__(
            self,
            max_num=8,
            transformed=True,
            train=False,
            *args, **kwargs,
        ):
    
            super().__init__(*args, **kwargs)
            self.max_num = max_num
            self.transformed = transformed
            self.train = train
    
        def get_example(self, i):
            """
    
            Args:
                i (int): An index of a data sample.
    
            Returns:
                imgs (np.array: (N, C=3, H, W)):
                    A set of item images.
                    N: the number of items in the outfit
            """
            outfit = self.df.iloc[i]
            items = outfit['items'][:self.max_num]
    
            imgs = np.array([
                self.read_img(outfit['set_id'], item['index'])
                for item in items
            ])
            # imgs: (N, C=3, H, W)
    
            example = imgs
    
            if self.transformed:
                example = self.transform(example)
    
            return example
    
        def transform(self, example):
            imgs = example
            imgs = np.array([
                transform_img(img, train=self.train)
                for img in imgs
            ])
            return imgs
    
        def converter(self, batch, device=-1):
            """
    
            Args:
                batch
                    imgs_b (list<np.array>: B * (N, C, H, W))
                        A batch of image sequences.
                device (int, optional):
                    >= 0 : GPU
                    -1 : CPU
            """
            imgs_b = batch
            imgs_b = [to_device(device, imgs) for imgs in imgs_b]
            return imgs_b
```

`get_example` は学習時のモデルの `__call__` の型に依存するため、子クラスの `PolyvoreDataset` に定義します。 また、 `transform` も `converter` も`get_example` の返り値の型に依存するため子クラスに定義します。

最後に、 `PolyvoreDatasetBase` を継承して新たに作った評価用データセットである `PolyvoreFitbDataset` が以下になります。

```scripts/datasets/polyvore_fitb.py

    import sys
    from os.path import dirname
    
    import numpy as np
    from chainer.dataset import to_device
    
    scripts_dir = dirname(dirname(__file__))
    sys.path.append(scripts_dir)
    from utils.transform import transform_img
    from datasets.polyvore_base import PolyvoreDatasetBase
    
    
    class PolyvoreFitbDataset(PolyvoreDatasetBase):
        def __init__(
            self,
            transformed=True,
            json_filename='fill_in_blank_test.json',
            *args, **kwargs,
        ):
            self.transformed = transformed
    
            kwargs['json_filename'] = json_filename
            super().__init__(*args, **kwargs)
    
            self.df['set_id'] = self.df['question']\
                .apply(lambda item_ids: item_ids[0].split('_')[0])
            self.outfit_ids = self.df['set_id'].tolist()
            self.n_choices = len(self.df.iloc[0]['answers'])
    
        def get_example(self, i):
            """
    
            Args:
                i (int): An index of a data sample.
    
            Returns
                imgs_q (np.array: (n-1, c, h, w))
                    question image sequence
                    n: the original number of items in an outfit
                imgs_c (np.array: (k, c, h, w))
                    choice images
                    k: the number of choices
                blank_pos (int)
                    blank position starting from **1**
            """
            row = self.df.iloc[i]
            question_ids = row['question']
            choice_ids = row['answers']
            blank_pos = row['blank_position']
    
            imgs_q = np.array([
                self.read_img_from_item_id(item_id)
                for item_id in question_ids
            ])
            imgs_c = np.array([
                self.read_img_from_item_id(item_id)
                for item_id in choice_ids
            ])
    
            return imgs_q, imgs_c, blank_pos
    
        def transform(self, example):
            """
            Args:
                example (tuple<imgs_q, imgs_c, blank_pos>)
                    imgs_q (np.array: (N-1, C, H, W))
                        A question image sequence.
                        N: the original number of items in an outfit.
                    imgs_c (np.array: (K, C, H, W))
                        A choice images.
                        K: the number of choices.
                    blank_pos (int)
                        The blank position starting from **1**.
    
            Returns:
                example (tuple<imgs_q, imgs_c, blank_pos>)
    		            imgs_q (np.array: (N-1, C, H, W))
    		                The transformed question image  sequence.
    		                N: the original number of items in an outfit.
    		            imgs_c (np.array: (K, C, H, W))
    		                The transformed choice images.
    		                K: the number of choices.
    		            blank_pos (int)
    		                The blank position starting from **1**.
            """
            imgs_q, imgs_c, blank_pos = example
            imgs_q = np.array([transform_img(img) for img in imgs_q])
            imgs_c = np.array([transform_img(img) for img in imgs_c])
            example = imgs_q, imgs_c, blank_pos
    
            if self.transformed:
                example = self.transform(example)
    
            return example
    
        def converter(self, batch, device=-1):
            """ Convert batch of tuples into tuple of batches
            and send them to GPU
    
            Args:
                batch (list<tuple<imgs_q, imgs_c, blank_pos>>)
                    imgs_q (np.array: (N-1, C, H, W))
                        A question image sequence.
                        N: the original number of items in an outfit.
                    imgs_c (np.array: (K, C, H, W))
                        A choice images.
                        K: the number of choices.
                    blank_pos (int)
                        The blank position starting from **1**.
                device (int, optional):
                    >= 0 : GPU
                    -1 : CPU
    
            Returns:
                imgs_q_b (list<np.array>: B * (N-1, C, H, W))
                    A batch of question image sequences.
                    N: the original number of items in an outfit.
                imgs_c_b (list<np.array>: B * (K, C, H, W))
                    A batch of choice image sets.
                    K: the number of choices.
                blank_pos_b (list<int>)
                    A batch of blank positions starting from **1**.
            """
            imgs_q_b, imgs_c_b, blank_pos_b = list(zip(*batch))
    
            imgs_q_b = [to_device(device, imgs) for imgs in imgs_q_b]
            imgs_c_b = [to_device(device, imgs) for imgs in imgs_c_b]
    
            return imgs_q_b, imgs_c_b, blank_pos_b
```

`PolyvoreDataset` 同様に、 `get_example`, `transform`, `converter` を定義します。 `json_filename` を変えているので、ご注意ください。また、こちらも、jupyter notebook でテストします。

<img width="564" alt="_2019-06-29_23-3984b49e-75c5-4bb9-88f3-6acf087f3c24.39.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154068/20ff0fb6-8676-f7f0-9ff4-d5280edbc92d.png">

上段が質問となるコーデで、下段が選択肢です。選択肢の一番左が正解のアイテムです。

ところで、1つのデータセットに対し、学習・評価、評価タスク、モデルによって get_example の返り値を変えたければ、 Dataset を1つだけ定義し、 `__init__` の引数で `get_example` の出力を切り替える方法も考えられます。しかし、この方法だと、 `get_exmaple` の可読性が落ちますし、モデルを追加する度に Dataset を編集しなければならず、思わぬバグを引き起こす可能性があります。

# まとめ

この記事では、ファッションコーディネートモデルのプロジェクトを題材に、データセットの管理と Chainer の Datasetクラスを自作する際のTipsを紹介しました。以下に大事な点をまとめておきます。

- データセットのセットアップや加工にまつわるコードはデータセットリポジトリに切り分ける。
- 小さいデータセットを用意してデバッグを効率化する。
- 重い前処理はあらかじめ施しておく。
- Dataset クラスを自作するときは、`chainer.dataset.DatasetMixin` を継承して、 `__init__` , `__len__` , `get_example` の3つオーバーライドする。
- データのパスは環境変数で指定することで、コードのポータビリティを上げる。
- `get_example` の返り値の型は、モデルの入力に合わせて決める。
- テンソルの形はコメントとして明記していおく。
- Dataset クラス、 converter などの単体テストを行う。画像系は特に jupyter notebook を用いて視覚的なテストを行う。
- transform と converter は Dataset のメソッドとして持たせ、transform は `get_example` 内で施す。
- converter、 Iterator 、 Updater などの Chainer のオブジェクトの役割を明確に理解し、役割に適したオーバーライドを行う。
- converter の役割は、タプルのバッチをバッチのタプルに変換することと、テンソルをGPUに転送すること。
- DatasetBase クラスに汎用的な処理を切り分けることで、保守性を上げる。

今回は Dataset まわりについての内容でしたが、今後、モデルや学習・評価スクリプトまわりの Tips の記事も書く予定です。

# 参考URL

[chainer: 独自datasetを定義する方法 - 午睡二時四十分](http://mizti.hatenablog.com/entry/chainer_dataset)

[機械学習で泣かないためのコード設計 2018](https://www.slideshare.net/takahirokubo7792/2018-97367311)

[データサイエンスプロジェクトのディレクトリ構成どうするか問題](https://takuti.me/note/data-science-project-structure/)

[機械学習モデルの実装における、テストについて - Qiita](https://qiita.com/icoxfog417/items/67764a6756c4548b5fb8)