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

http://docs.chainer.org/en/stable/reference/generated/chainer.links.ConvolutionND.html

これは、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が有名です。

https://github.com/facebook/C3D

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

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

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

https://github.com/ikeyasu/chainer-convolutionnd-sample

ファイルをコピーしたので、修正点がわかりにくくて申し訳ないですが、、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したいと思っています。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.