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
を使ったサンプルはあまり見つかりません。
- Chainerのテストコード
- Qiitaでの @dsanno さんのコメント
- VoxcelChain 3次元畳み込みニューラルネットワークを使ったディープラーニング (深層学習)|Chainerによる3次元形状の認識 ※ 2017/12/17追記
このぐらいでしょうか。。
そもそも、2次元以外のConvolutionを何に使うのか?という話ですが、概ね、
- 自然言語処理に1次元CNN (参考:Convolutional Neural Netwoks で自然言語処理をする
) - 動画の分類器には3次元CNN (参考:3D CNNによる人物行動認識の動向)
- 3D画像の分類器には、3次元CNN(参考:VoxcelChain 3次元畳み込みニューラルネットワークを使ったディープラーニング (深層学習)|Chainerによる3次元形状の認識)
という感じのようです。
というわけで、任意の次元の畳み込みができでも、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.pyにVGG3D
を追加、動画ファイルを扱うためにdatasets/UCF11.pyを追加して、train_cifar.pyを修正しただけです。
もともと以下のようなBlockが定義されているのを
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を指定しています。
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_2d
をF.max_pooling_nd
に変更しています。
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を使います。
#!/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の画像にしました。
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したいと思っています。