Chainerでアニメキャラクターの顔画像を分類する


この記事では、animeface-characterデータセットを使ってIllustration2VecモデルをFine-tuningし、146種類のキャラクター顔画像を90%以上の精度で分類できるモデルを訓練する手順を追いながら、以下のことを説明します。

Chainerを使って

- データセットオブジェクトを作る方法
- データセットを訓練用・検証用に分割する方法
- 訓練済み重みを持ってきて新しいタスクでFine-tuningする方法
- （おまけ：データセットクラスをフルスクラッチで書く方法）

使用した環境は以下です。

- NVIDIA Pascal TITAN X
- Ubuntu 16.04
- Python 3.6.3

使用したライブラリは以下です。

- Chainer >=2.0.1 (最新の4.1.0でも動作確認済み)
- CuPy >= 1.0.1 (最新の4.1.0でも動作確認済み)
- Pillow 4.0.0
- tqdm 4.14.0

Chainerは後方互換性が

### ざっくり内容をまとめると

**Chainerに予め用意されていないデータセットを外部から調達して**、Chainerで記述されたネットワークの訓練のために用いる方法を具体例をもって示します。基本的な手順はほぼ[Chainer v4: ビギナー向けチュートリアル](https://qiita.com/mitmul/items/1e35fba085eb07a92560)で説明したCIFAR10データセットクラスを拡張する章と変わりません。

今回は、**ターゲットとなるデータと似たドメインのデータセットを用いて予め訓練されたネットワーク重みを初期値として用いる方法**を説明します。Caffeの.caffemodelの形で配布されているネットワークをFine-tuningしたい場合、この記事とほぼ同様の手順が適用できると思います。

この記事は[もともとJupyter notebookで書いたもの](https://github.com/mitmul/chainer-handson/blob/master/animeface-character/classify_characters.ipynb)をMarkdownに出力したものです。

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

まずは、データセットをダウンロードしてきます。今回は、Kaggle Grand Masterであらせられるnagadomiさんが[こちら](http://www.nurs.or.jp/~nagadomi/animeface-character-dataset/)で配布されているアニメキャラクターの顔領域サムネイルデータセットを使用します。


```bash
%%bash
if [ ! -d animeface-character-dataset ]; then
    curl -L -O http://www.nurs.or.jp/~nagadomi/animeface-character-dataset/data/animeface-character-dataset.zip
    unzip animeface-character-dataset.zip
    rm -rf animeface-character-dataset.zip
fi
```

使用するライブラリをpipで入れます。**`cupy-cuda90`の部分は、お使いの環境のCUDAバージョンに合わせて`cupy-cuda80`(CUDA8.0環境向け), `cupy-cuda90`(CUDA9.0環境向け), `cupy-cuda91`(CUDA9.1環境向け)から適したものを選択してください。）**


```bash
%%bash
pip install chainer
pip install cupy-cuda80 # or cupy-cuda90 or cupy-cuda91
pip install Pillow
pip install tqdm
```


## 2. 問題設定の確認

今回は、animeface-character-datasetに含まれる様々なキャラクターの顔画像を用いて、未知のキャラクター顔画像が入力された際に、それが既知のクラス一覧の中のどのキャラクターの顔らしいかを出力するようなネットワークを訓練したいと思います。

その際に、**ランダムにパラメータを初期化したネットワークを訓練するのではなく、予め似たドメインのデータで訓練済みのモデルをベースに、目的のデータセットでFine-tuningする**というやり方をしてみます。

今回学習に用いるデータセットは、以下のような画像を多数含むデータセットで、各キャラクターごとに予めフォルダ分けがされています。なので、今回もオーソドックスな画像分類問題となります。

#### 適当に抜き出したデータサンプル

| 000_hatsune_miku | 002_suzumiya_haruhi | 007_nagato_yuki | 012_asahina_mikuru |
|:-:|:-:|:-:|:-:|
| ![face_128_326_108.png](https://qiita-image-store.s3.amazonaws.com/0/17934/4c7e3f61-b930-581c-b481-58dd724889d9.png) | ![face_1000_266_119.png](https://qiita-image-store.s3.amazonaws.com/0/17934/ddac8de1-96b3-3b25-e9ec-6f0b7869e89a.png) | ![face_83_270_92.png](https://qiita-image-store.s3.amazonaws.com/0/17934/d1fc7844-8e4d-073b-f4a0-b921fdcc9061.png) | ![face_121_433_128.png](https://qiita-image-store.s3.amazonaws.com/0/17934/8d932513-de0e-547f-a118-828f378e4885.png) |

## 3. データセットオブジェクトの作成

ここでは、画像分類の問題でよく使われる`LabeledImageDataset`というクラスを使ったデータセットオブジェクトの作成方法を示します。まずは、Python標準の機能を使って下準備をします。

初めに画像ファイルへのパス一覧を取得します。画像ファイルは、`animeface-character-dataset/thumb`以下にキャラクターごとのディレクトリに分けられて入っています。下記のコードでは、フォルダ内に`ignore`というファイルが入っている場合は、そのフォルダの画像は無視するようにしています。


```python
import os
import glob
from itertools import chain

# 画像フォルダ
IMG_DIR = 'animeface-character-dataset/thumb'

# 各キャラクターごとのフォルダ
dnames = glob.glob('{}/*'.format(IMG_DIR))

# 画像ファイルパス一覧
fnames = [glob.glob('{}/*.png'.format(d)) for d in dnames
          if not os.path.exists('{}/ignore'.format(d))]
fnames = list(chain.from_iterable(fnames))
```

次に、画像ファイルパスのうち画像が含まれるディレクトリ名の部分がキャラクター名を表しているので、それを使って各画像にキャラクターごとに一意になるようなIDを作ります。


```python
# それぞれにフォルダ名から一意なIDを付与
labels = [os.path.basename(os.path.dirname(fn)) for fn in fnames]
dnames = [os.path.basename(d) for d in dnames
          if not os.path.exists('{}/ignore'.format(d))]
labels = [dnames.index(l) for l in labels]
```

では、ベースとなるデータセットオブジェクトを作ります。やり方は簡単で、ファイルパスとそのラベルが並んだタプルのリストを`LabeledImageDataset`に渡せば良いだけです。これは `(img, label)` のようなタプルを返すイテレータになっています。


```python
from chainer.datasets import LabeledImageDataset

# データセット作成
d = LabeledImageDataset(list(zip(fnames, labels)))
```

次に、Chainerが提供している`TransformDataset`という便利な機能を使ってみます。これは、データセットオブジェクトと各データへの変換を表す関数を取るラッパークラスで、これを使うとdata augmentationや前処理などを行う部分をデータセットクラスの外に用意しておくことができます。


```python
from chainer.datasets import TransformDataset
from PIL import Image

width, height = 160, 160

# 画像のresize関数
def resize(img):
    img = Image.fromarray(img.transpose(1, 2, 0))
    img = img.resize((width, height), Image.BICUBIC)
    return np.asarray(img).transpose(2, 0, 1)

# 各データに行う変換
def transform(inputs):
    img, label = inputs
    img = img[:3, ...]
    img = resize(img.astype(np.uint8))
    img = img - mean[:, None, None]
    img = img.astype(np.float32)
    # ランダムに左右反転
    if np.random.rand() > 0.5:
        img = img[..., ::-1]
    return img, label

# 変換付きデータセットにする
td = TransformDataset(d, transform)
```

こうすることで、`LabeledImageDataset`オブジェクトである`d`が返す `(img, label)` のようなタプルを受け取って、それを`transform`関数にくぐらせてから返すようなデータセットオブジェクトが作れました。

では、これを学習用と検証用の2つの部分データセットにsplitしましょう。今回は、データセット全体のうち80%を学習用に、残り20%を検証用に使うことにします。`split_dataset_random`を使うと、データセット内のデータを一度シャッフルしたのちに、指定した区切り目で分割したものを返してくれます。


```python
from chainer import datasets

train, valid = datasets.split_dataset_random(td, int(len(d) * 0.8), seed=0)
```

データセットの分割は他にも、交差検定をするための複数の互いに異なる訓練・検証用データセットペアを返すような`get_cross_validation_datasets_random`など、いくつかの関数が用意されています。こちらをご覧ください。：[SubDataset](https://docs.chainer.org/en/stable/reference/datasets.html#subdataset)

さて、変換の中で使っている`mean`は、今回使う学習用データセットに含まれる画像の平均画像です。これを計算しておきましょう。


```python
import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm_notebook

# 平均画像が未計算なら計算する
if not os.path.exists('image_mean.npy'):
    # 変換をかまさないバージョンの学習用データセットで平均を計算したい
    t, _ = datasets.split_dataset_random(d, int(len(d) * 0.8), seed=0)

    mean = np.zeros((3, height, width))
    for img, _ in tqdm_notebook(t, desc='Calc mean'):
        img = resize(img[:3].astype(np.uint8))
        mean += img
    mean = mean / float(len(d))
    np.save('image_mean', mean)
else:
    mean = np.load('image_mean.npy')
```

試しに計算した平均画像を表示してみましょう。


```python
# 平均画像の表示
%matplotlib inline
plt.imshow(mean.transpose(1, 2, 0) / 255)
plt.show()
```


![png](output_20_0.png)


なんか怖いですね…

各画像から平均を引くときはピクセルごとの平均を使うので、この平均画像の平均画素値（RGB）を計算しておきます。


```python
mean = mean.mean(axis=(1, 2))
```

## 4. モデルの定義とFine-tuningの準備

では次に、訓練を行うモデルの定義を行います。ここではたくさんの2Dイラスト画像を使って学習したモデルでタグ予測や特徴抽出などを行う[Illustration2Vec](https://github.com/rezoo/illustration2vec)で使われたネットワークをベースとし、その最後の2層を削除してランダムに初期化された2つの全結合層を付け加えたものを新しいモデルとします。

学習時には、Illustration2Vecのpre-trained weightで出力から3層目以下の部分を初期化したのち、その部分の重みは固定しておきます。つまり、**新たに追加した2つの全結合層だけを訓練します。**

まず、配布されているIllustration2Vecモデルの訓練済みパラメータをダウンロードしてきます。


```bash
%%bash
if [ ! -f illust2vec_ver200.caffemodel ]; then
    curl -L -O https://github.com/rezoo/illustration2vec/releases/download/v2.0.0/illust2vec_ver200.caffemodel
fi
```

この訓練済みパラメータはcaffemodelの形式で提供されていますが、Chainerには非常に簡単にCaffeの訓練済みモデルを読み込む機能（[`CaffeFunction`](http://docs.chainer.org/en/stable/reference/generated/chainer.links.caffe.CaffeFunction.html#chainer.links.caffe.CaffeFunction)）があるので、これを使ってパラメータとモデル構造をロードします。ただし、読み込みには時間がかかるため、一度読み込んだ際に得られる`Chain`オブジェクトをPython標準の`pickle`を使ってファイルに保存しておきます。こうすることで次回からの読み込みが速くなります。

実際のネットワークのコードは以下のようになります。


```python
import dill

import chainer
import chainer.links as L
import chainer.functions as F

from chainer import Chain
from chainer.links.caffe import CaffeFunction
from chainer import serializers

class Illust2Vec(Chain):

    CAFFEMODEL_FN = 'illust2vec_ver200.caffemodel'

    def __init__(self, n_classes, unchain=True):
        w = chainer.initializers.HeNormal()        
        model = CaffeFunction(self.CAFFEMODEL_FN)  # CaffeModelを読み込んで保存します。（時間がかかります）
        del model.encode1  # メモリ節約のため不要なレイヤを削除します。
        del model.encode2
        del model.forwards['encode1']
        del model.forwards['encode2']
        model.layers = model.layers[:-2]
        
        super(Illust2Vec, self).__init__()
        with self.init_scope():
            self.trunk = model  # 元のIllust2Vecモデルをtrunkとしてこのモデルに含めます。
            self.fc7 = L.Linear(None, 4096, initialW=w)
            self.bn7 = L.BatchNormalization(4096)
            self.fc8 = L.Linear(4096, n_classes, initialW=w)
            
    def __call__(self, x):
        h = self.trunk({'data': x}, ['conv6_3'])[0]  # 元のIllust2Vecモデルのconv6_3の出力を取り出します。
        h.unchain_backward()
        h = F.dropout(F.relu(self.bn7(self.fc7(h))))  # ここ以降は新しく追加した層です。
        return self.fc8(h)

n_classes = len(dnames)
model = Illust2Vec(n_classes)
model = L.Classifier(model)
```

    /home/mitmul/chainer/chainer/links/caffe/caffe_function.py:165: UserWarning: Skip the layer "encode1neuron", since CaffeFunction does notsupport Sigmoid layer
      'support %s layer' % (layer.name, layer.type))
    /home/mitmul/chainer/chainer/links/caffe/caffe_function.py:165: UserWarning: Skip the layer "loss", since CaffeFunction does notsupport SigmoidCrossEntropyLoss layer
      'support %s layer' % (layer.name, layer.type))


`__call__`の部分に`h.unchain_backward()`という記述が登場しました。`unchain_backward`は、ネットワークのある中間出力`Variable` などから呼ばれ、その時点より前のあらゆるネットワークノードの接続を断ち切ります。そのため、学習時にはこれが呼ばれた時点より前の層に誤差が伝わらなくなり、結果としてパラメータの更新も行われなくなります。

前述の

> 学習時には、Illustration2Vecのpre-trained weightで出力から3層目以下の部分を初期化したのち、その部分の重みは固定しておきます。

これを行うためのコードが、この`h.unchain_backward()`です。

このあたりの仕組みについて、さらに詳しくは、Define-by-RunによるChainerのautogradの仕組みを説明しているこちらの記事を参照してください。: [1-file Chainerを作る](http://qiita.com/mitmul/items/37d3932292cdd560d418)

## 5. 学習

それでは、このデータセットとモデルを用いて、学習を行ってみます。まず必要なモジュールをロードしておきます。


```python
from chainer import iterators
from chainer import training
from chainer import optimizers
from chainer.training import extensions
from chainer.training import triggers
from chainer.dataset import concat_examples
```

次に学習のパラメータを設定します。今回は

- バッチサイズ64
- 学習率は0.01からスタートし、10エポック目で0.1倍にする
- 20エポックで学習終了

とします。


```python
batchsize = 64
gpu_id = 0
initial_lr = 0.01
lr_drop_epoch = 10
lr_drop_ratio = 0.1
train_epoch = 20
```

以下が学習を行うコードです。


```python
train_iter = iterators.MultiprocessIterator(train, batchsize)
valid_iter = iterators.MultiprocessIterator(
    valid, batchsize, repeat=False, shuffle=False)

optimizer = optimizers.MomentumSGD(lr=initial_lr)
optimizer.setup(model)
optimizer.add_hook(chainer.optimizer.WeightDecay(0.0001))

updater = training.StandardUpdater(
    train_iter, optimizer, device=gpu_id)

trainer = training.Trainer(updater, (train_epoch, 'epoch'), out='AnimeFace-result')
trainer.extend(extensions.LogReport())
trainer.extend(extensions.observe_lr())

# 標準出力に書き出したい値
trainer.extend(extensions.PrintReport(
    ['epoch',
     'main/loss',
     'main/accuracy',
     'val/main/loss',
     'val/main/accuracy',
     'elapsed_time',
     'lr']))

# ロスのプロットを毎エポック自動的に保存
trainer.extend(extensions.PlotReport(
        ['main/loss',
         'val/main/loss'],
        'epoch', file_name='loss.png'))

# 精度のプロットも毎エポック自動的に保存
trainer.extend(extensions.PlotReport(
        ['main/accuracy',
         'val/main/accuracy'],
        'epoch', file_name='accuracy.png'))

# モデルのtrainプロパティをFalseに設定してvalidationするextension
trainer.extend(extensions.Evaluator(valid_iter, model, device=gpu_id), name='val')

# 指定したエポックごとに学習率をlr_drop_ratio倍にする
trainer.extend(
    extensions.ExponentialShift('lr', lr_drop_ratio),
    trigger=(lr_drop_epoch, 'epoch'))

trainer.run()
```

    epoch       main/loss   main/accuracy  val/main/loss  val/main/accuracy  elapsed_time  lr        
    1           1.58266     0.621792       0.623695       0.831607           29.4045       0.01        
    2           0.579938    0.835989       0.54294        0.85179            56.3893       0.01        
    3           0.421797    0.877897       0.476766       0.876872           83.9976       0.01        
    4           0.3099      0.909251       0.438246       0.879637           113.476       0.01        
    5           0.244549    0.928394       0.427892       0.884571           142.931       0.01        
    6           0.198274    0.938638       0.41589        0.893617           172.42        0.01        
    7           0.171127    0.946709       0.432277       0.89115            201.868       0.01        
    8           0.146401    0.953125       0.394634       0.902549           231.333       0.01        
    9           0.12377     0.964404       0.409338       0.894667           260.8         0.01        
    10          0.109239    0.967198       0.400371       0.907746           290.29        0.01        
    11          0.0948708   0.971337       0.378603       0.908831           319.742       0.001       
    12          0.0709512   0.98065        0.380891       0.90786            349.242       0.001       
    13          0.0699093   0.981892       0.384257       0.90457            379.944       0.001       
    14          0.0645318   0.982099       0.370053       0.908008           410.963       0.001       
    15          0.0619039   0.983547       0.379178       0.908008           441.941       0.001       
    16          0.0596897   0.983646       0.375837       0.911709           472.832       0.001       
    17          0.0579783   0.984789       0.379593       0.908008           503.836       0.001       
    18          0.0611943   0.982202       0.378177       0.90842            534.86        0.001       
    19          0.061885    0.98303        0.373961       0.90569            565.831       0.001       
    20          0.0548781   0.986341       0.3698         0.910624           596.847       0.001       


6分弱くらいで学習が終わりました。標準出力に出る途中経過は上記のような感じでした。最終的に検証用データセットに対しても90%以上のaccuracyが出せていますね。では、画像ファイルとして保存されている学習経過でのロスカーブとaccuracyのカーブを表示してみます。


```python
from IPython.display import Image
Image(filename='AnimeFace-result/loss.png')
```




![output_35_0.png](https://qiita-image-store.s3.amazonaws.com/0/17934/4ac17c85-c8dc-a0d9-69fb-26c1ee2a7a3d.png)




```python
Image(filename='AnimeFace-result/accuracy.png')
```




![output_36_0.png](https://qiita-image-store.s3.amazonaws.com/0/17934/9654a04c-2799-4808-f23c-7604139ec466.png)



無事収束している感じがします。

最後に、いくつかvalidationデータセットから画像を取り出してきて個別の分類結果を見てみます。


```python
%matplotlib inline
import matplotlib.pyplot as plt

from PIL import Image
from chainer import cuda

chainer.config.train = False
for _ in range(10):
    x, t = valid[np.random.randint(len(valid))]
    x = cuda.to_gpu(x)
    y = F.softmax(model.predictor(x[None, ...]))
    
    pred = os.path.basename(dnames[int(y.data.argmax())])
    label = os.path.basename(dnames[t])
    
    print('pred:', pred, 'label:', label, pred == label)

    x = cuda.to_cpu(x)
    x += mean[:, None, None]
    x = x / 256
    x = np.clip(x, 0, 1)
    plt.imshow(x.transpose(1, 2, 0))
    plt.show()
```

    pred: 097_kamikita_komari label: 097_kamikita_komari True



![output_38_1.png](https://qiita-image-store.s3.amazonaws.com/0/17934/aabf1710-d148-05d2-129e-920256b35166.png)


    pred: 127_setsuna_f_seiei label: 127_setsuna_f_seiei True



![output_38_3.png](https://qiita-image-store.s3.amazonaws.com/0/17934/5f24a001-765c-2d4d-52f5-2da880c83591.png)


    pred: 171_ikari_shinji label: 171_ikari_shinji True



![output_38_5.png](https://qiita-image-store.s3.amazonaws.com/0/17934/1615cfa4-fcbc-18d8-5a79-8654c8530e92.png)



    pred: 042_tsukimura_mayu label: 042_tsukimura_mayu True



![output_38_7.png](https://qiita-image-store.s3.amazonaws.com/0/17934/d2594947-0b00-c48b-9a57-f573934b29f5.png)


    pred: 001_kinomoto_sakura label: 001_kinomoto_sakura True



![output_38_9.png](https://qiita-image-store.s3.amazonaws.com/0/17934/9099b578-60e0-bad2-ee1e-df24f675dc39.png)


    pred: 090_minase_iori label: 090_minase_iori True



![output_38_11.png](https://qiita-image-store.s3.amazonaws.com/0/17934/d0efc7f0-04cd-bee7-7107-38122db5f928.png)


    pred: 132_minamoto_chizuru label: 132_minamoto_chizuru True



![output_38_13.png](https://qiita-image-store.s3.amazonaws.com/0/17934/8ee05e5a-1d22-6b73-d23c-3a92b17bc8b9.png)


    pred: 106_nia label: 106_nia True



![output_38_15.png](https://qiita-image-store.s3.amazonaws.com/0/17934/c9006cc7-10e8-5872-b276-b74819d24498.png)


    pred: 174_hayama_mizuki label: 174_hayama_mizuki True



![output_38_17.png](https://qiita-image-store.s3.amazonaws.com/0/17934/7a24d592-e5aa-5358-898e-1f9170b16753.png)


    pred: 184_suzumiya_akane label: 184_suzumiya_akane True



![output_38_19.png](https://qiita-image-store.s3.amazonaws.com/0/17934/2843d3df-06b0-3ddd-c898-4c50124b16ed.png)


ランダムに10枚選んでみたところこの画像たちに対しては全て正解できました。

最後に、いつか何かに使うかもしれないので、一応snapshotを保存しておきます。


```python
from chainer import serializers

serializers.save_npz('animeface.model', model)
```

## 6. おまけ1：データセットクラスをフルスクラッチで書く方法

データセットクラスをフルスクラッチで書くには、`chainer.dataset.DatasetMixin`クラスを継承した自前クラスを用意すれば良いです。そのクラスは`__len__`メソッドと`get_example`メソッドを持つ必要があります。例えば以下のようになります。


```python
class MyDataset(chainer.dataset.DatasetMixin):
    
    def __init__(self, image_paths, labels):
        self.image_paths = image_paths
        self.labels = labels
        
    def __len__(self):
        return len(self.image_paths)
    
    def get_example(self, i):
        img = Image.open(self.image_paths[i])
        img = np.asarray(img, dtype=np.float32)
        img = img.transpose(2, 0, 1)
        label = self.labels[i]
        return img, label
```

これは、コンストラクタに画像ファイルパスのリストと、それに対応した順番でラベルを並べたリストを渡しておき、`[]`アクセサでインデックスを指定すると、対応するパスから画像を読み込んで、ラベルと並べたタプルを返すデータセットクラスになっています。例えば、以下のように使えます。


```python
image_files = ['images/hoge_0_1.png', 'images/hoge_5_1.png', 'images/hoge_2_1.png', 'images/hoge_3_1.png', ...]
labels = [0, 5, 2, 3, ...]

dataset = MyDataset(image_files, labels)

img, label = dataset[2]

#=> 'images/hoge_2_1.png'から読み込まれた画像データと、そのラベル（ここでは2）が返る
```


このオブジェクトは、そのままIteratorに渡すことができ、Trainerを使った学習に使えます。つまり、


```python
train_iter = iterators.MultiprocessIterator(dataset, batchsize=128)
```

のようにしてイテレータを作って、UpdaterにOptimizerと一緒に渡し、そのupdaterをTrainerに渡せばTrainerを使って学習を開始できます。

## 7. おまけ2：最もシンプルなデータセットオブジェクトの作り方

実はChainerのTrainerと一緒に使うためのデータセットは、**単なるPythonのリストでOK**です。どういうことかというと、`len()`で長さが取得でき、`[]`アクセサで要素が取り出せるものなら、**全てデータセットオブジェクトとして扱う事ができる**ということです。例えば、


```python
data_list = [(x1, t1), (x2, t2), ...]
```

のような`(データ, ラベル)`というタプルのリストを作れば、これをIteratorに渡すことができます。


```python
train_iter = iterators.MultiprocessIterator(data_list, batchsize=128)
```

ただこういったやりかたの欠点は、データセット全体を学習前にメモリに載せなければいけない点です。これを防ぐために、[ImageDataset](http://docs.chainer.org/en/stable/reference/generated/chainer.datasets.ImageDataset.html#chainer.datasets.ImageDataset)と[TupleDataset](http://docs.chainer.org/en/stable/reference/generated/chainer.datasets.TupleDataset.html#chainer.datasets.TupleDataset)を組み合わせる方法や[LabaledImageDataset](http://docs.chainer.org/en/stable/reference/generated/chainer.datasets.LabeledImageDataset.html#chainer.datasets.LabeledImageDataset)といったクラスが用意されています。詳しくはドキュメントをご参照ください。
http://docs.chainer.org/en/stable/reference/datasets.html#general-datasets