LoginSignup
6
9

More than 5 years have passed since last update.

chainer.links.ConvolutionNDを使ってみる

Last updated at Posted at 2017-12-14

Chainer には、chainer.links.ConvolutionNDというクラスがあります。Convolution2Dと違い、任意の次元の畳み込みができるクラスです。

これは、Chainer独特の機能のようです。

あまり詳しくありませんが、、TensorFlowの場合tf.layers.1D/2D/3D、PyTorchの場合torch.nn.functional.conv1d/2d/3d、Kerasの場合keras.layers.convolutional.Conv1D/2D/3Dがあります。が、どれも、次元を固定していて、任意の次元の畳み込みができるものではありません。

というわけで、これを使って簡単に、任意の次元のConvolutionができる、と思うのですが、ConvolutionNDを使ったサンプルはあまり見つかりません。

このぐらいでしょうか。。

そもそも、2次元以外のConvolutionを何に使うのか?という話ですが、概ね、

という感じのようです。

というわけで、任意の次元の畳み込みができでも、1/2/3次元以外は特に有力な使い道は無いようですが、、ともかくConvolutionNDを使って何か書いてみたいと思います。

ここでは、動画の分類器を作ってみます。

動画の3次元CNNは、FacebookのC3Dが有名です。

これをChainerで実装しようと思っていたのですが、Advent Calender の日まで時間が取れず、、代わりにChainerのサンプルにあるVGG16を適等に3D化してみることにしました。

2017/12/25追記:
C3Dを移植してみました。
https://qiita.com/ikeyasu/items/79f88afffaaa0da19ea9

以下に、修正したサンプルを置きました。

ファイルをコピーしたので、修正点がわかりにくくて申し訳ないですが、、models/VGG.pyVGG3Dを追加、動画ファイルを扱うためにdatasets/UCF11.pyを追加して、train_cifar.pyを修正しただけです。

もともと以下のようなBlockが定義されているのを

models/VGG.py

class Block(chainer.Chain):


    def __init__(self, out_channels, ksize, pad=1):
        super(Block, self).__init__()
        with self.init_scope():
            self.conv = L.Convolution2D(None, out_channels, ksize, pad=pad,
                                        nobias=True)
            self.bn = L.BatchNormalization(out_channels)

    def __call__(self, x):
        h = self.conv(x)
        h = self.bn(h)
        return F.relu(h)

以下の通り、ConvolutionNDを使うように変えました。第一引数のndim=3で3次元のConvolutionを指定しています。

models/VGG.py
class Block3D(chainer.Chain):


    def __init__(self, in_channels, out_channels, ksize, pad=1):
        super(Block3D, self).__init__()
        with self.init_scope():
            self.conv = L.ConvolutionND(ndim=3, in_channels=in_channels, out_channels=out_channels, ksize=ksize, pad=pad,
                                        nobias=True)
            self.bn = L.BatchNormalization(out_channels)

    def __call__(self, x):
        h = self.conv(x)
        h = self.bn(h)
        return F.relu(h)

このBlock3Dを使って、VGG3Dを作ります。__call__メソッドに、max_poolingがありますが、こちらもF.max_pooling_2dF.max_pooling_ndに変更しています。

models/VGG.py
class VGG3D(chainer.Chain):

    def __init__(self, class_labels=11):
        super(VGG3D, self).__init__()
        with self.init_scope():
            self.block1_1 = Block3D(3, 64, 3)
            self.block1_2 = Block3D(64, 64, 3)
            self.block2_1 = Block3D(64, 128, 3)
            self.block2_2 = Block3D(128, 128, 3)
            self.block3_1 = Block3D(128, 256, 3)
            self.block3_2 = Block3D(256, 256, 3)
            self.block3_3 = Block3D(256, 256, 3)
            self.block4_1 = Block3D(256, 512, 3)
            self.block4_2 = Block3D(512, 512, 3)
            self.block4_3 = Block3D(512, 512, 3)
            self.block5_1 = Block3D(512, 512, 3)
            self.block5_2 = Block3D(512, 512, 3)
            self.block5_3 = Block3D(512, 512, 3)
            self.fc1 = L.Linear(None, 512, nobias=True)
            self.bn_fc1 = L.BatchNormalization(512)
            self.fc2 = L.Linear(None, class_labels, nobias=True)

    def __call__(self, x):
        # 64 channel blocks:
        h = self.block1_1(x)
        h = F.dropout(h, ratio=0.3)
        h = self.block1_2(h)
        h = F.max_pooling_nd(h, ksize=2, stride=2)

        # 128 channel blocks:
        h = self.block2_1(h)
        h = F.dropout(h, ratio=0.4)
        h = self.block2_2(h)
        h = F.max_pooling_nd(h, ksize=2, stride=2)

        # 256 channel blocks:
        h = self.block3_1(h)
        h = F.dropout(h, ratio=0.4)
        h = self.block3_2(h)
        h = F.dropout(h, ratio=0.4)
        h = self.block3_3(h)
        h = F.max_pooling_nd(h, ksize=2, stride=2)

        # 512 channel blocks:
        h = self.block4_1(h)
        h = F.dropout(h, ratio=0.4)
        h = self.block4_2(h)
        h = F.dropout(h, ratio=0.4)
        h = self.block4_3(h)
        h = F.max_pooling_nd(h, ksize=2, stride=2)

        # 512 channel blocks:
        h = self.block5_1(h)
        h = F.dropout(h, ratio=0.4)
        h = self.block5_2(h)
        h = F.dropout(h, ratio=0.4)
        h = self.block5_3(h)
        h = F.max_pooling_nd(h, ksize=2, stride=2)

        h = F.dropout(h, ratio=0.5)
        h = self.fc1(h)
        h = self.bn_fc1(h)
        h = F.relu(h)
        h = F.dropout(h, ratio=0.5)
        return self.fc2(h)

基本的にこれだけで動きました。

が、ここまでは簡単なのは当たり前でして、データをどうやって用意するか、扱うかが少々骨です。
ここでは、UCF11という小規模な動画データセットを使いました。

UCF11
http://crcv.ucf.edu/data/UCF_YouTube_Action.php

このページにある、 http://crcv.ucf.edu/data/UCF11_updated_mpg.rar をダウンロードして展開します。

$ wget http://crcv.ucf.edu/data/UCF11_updated_mpg.rar
$ mkdir videos
$ pushd videos
$   unar e UCF11_updated_mpg.rar
$ popd

この動画を画像に変換します。ここでは、ffmpegを使います。

tools/video_to_image.sh
#!/bin/bash
FILE=$1
mkdir -p ${FILE%.*}
ffmpeg -i "$FILE" -vf fps=5 "${FILE%.*}"/%05d.jpg

上記のスクリプトを使って、以下のように変換します。以下では、並列に変換できるよう、GNU Parallelを使っています。

$ ls videos/*.mpg | parallel --no-notice -j8 ./tools/video_to_images.sh  {}

次に、このままでは画像が大きいのでCropして、リサイズします。今回は試しに作るだけですので、cifar10と同じ、32x32の画像にしました。

tools/resize.py
from PIL import Image
import numpy as np
import argparse


def main(path, out_path):
  img = Image.open(path, 'r')
  h, w = img.size
  cw = h // 2
  cx = int((w - cw) / 4)
  cy = int((h - cw) / 4)
  clp = img.crop((cx, cy, w - cx, h - cy))
  resized = clp.resize((32, 32))
  resized.save(out_path, 'JPEG', quality=100, optimize=True)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description='clipping and resize')
    parser.add_argument('--input', '-i', default='input.jpg',
                        help='input file')
    parser.add_argument('--output', '-o', default='output.jpg',
                        help='output file')
    args = parser.parse_args()
    main(args.input, args.output)
$ ls videos/*/*.jpg | parallel -j20 'python tools/resize.py -i {} -o `echo {} | sed s/videos/images/`'

上記で、以下のようにデータが用意されています。

.
├── images
│   ├── v_biking_01_01
│   │   ├── 00001.jpg
│   │   ├── 00002.jpg
│   │   ├── ...
│   │   └── 00026.jpg
│   ├── ....
│
└── videos
    ├── v_biking_01_01
    │   ├── 00001.jpg
    │   ├── 00002.jpg
    │   ├── ....
    │   └── 00026.jpg
    ├── ....

これを扱うために、Datasetのクラスを作ります。

import chainer
import os
import re
import numpy as np


def get_dir_list(path):
    return [os.path.join(path, i) for i in os.listdir(path) if os.path.isdir(os.path.join(path, i))]


LABEL_PATTERN = re.compile(r"v_([a-zA-Z_]+)_[0-9]")


def get_label(path):
    dir = os.path.split(path)[-1]
    matched = LABEL_PATTERN.match(dir)
    return matched.group(1)


class UCF11Dataset(chainer.dataset.DatasetMixin):
    """
    example:
    videos/v_biking_01_01/00001.jpg
    videos/v_biking_01_01/00002.jpg
    """

    def __init__(self, path):
        self.dir_list = get_dir_list(path)
        labels = [get_label(d) for d in self.dir_list]
        self.labels = list(set(labels))

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

    def get_example(self, i):
        base = chainer.datasets.ImageDataset(os.listdir(self.dir_list[i]), root=self.dir_list[i])
        label = get_label(self.dir_list[i])

        frames = 6
        images = np.array([base[i] for i in range(frames)]).transpose(1, 0, 2, 3)

        # TODO: mean
        # image -= self.mean[:, top:bottom, left:right]
        images *= (1.0 / 255.0)  # Scale to [0, 1]
        return images, self.labels.index(label)

ここで注意すべきは、get_exampleで返している画像の行列です。Convolution2Dで使う場合、(channel, height, width)の形の行列を返すように書くと思います。(例:chainer/examples/imagenet
今回、ConvlutionNDを使って3次元CNNをするので、行列の形は、(channel, frame, height, width)になります。

上記を使って実行するように、train_cifar.pyを修正します。
一応、accuracyが向上していくのが確認できました。(特に工夫していないので、精度はあまり出ていません)

$ python train_cifar.py -g 0
GPU: 0
# Minibatch-size: 64
# epoch: 300

Using CIFAR100 dataset.
/home/ikeyasu/anaconda3/envs/chainer3/lib/python3.6/site-packages/chainer/utils/experimental.py:104: FutureWarning: chainer.functions.pooling.MaxPoolingND is experimental. The interface can change in the future.
  FutureWarning)
epoch       main/loss   validation/main/loss  main/accuracy  validation/main/accuracy  elapsed_time
1           2.87406     27.3399               0.099375       0.122659                  12.3509
2           2.99936     2.39463               0.1075         0.151621                  24.2535
3           2.65441     2.47364               0.165          0.18714                   36.1351
4           2.49547     2.44088               0.14625        0.19339                   48.0406
5           2.41286     2.32186               0.155          0.163962                  59.9657
...

っとここまでかいて、、"chainer.functions.pooling.MaxPoolingND is experimental"と出ていることに気付きました。ConvolutionNDは結構前から入っていますが、max_pooling_ndの方は、Experimentalなんですね。。

というわけで、中途半端なサンプルですが、VGGのサンプルを修正するだけで、一応、3次元CNNらしき事はできました。

今後の展望としては、C3Dのネットワーク構造を実装したり、C3DのpretrainedモデルをFine-tuningしたいと思っています。

6
9
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
9