庚午里藻の日記

見た映画とかアニメの備忘録にしたり、パソコンいじったことのメモにしたり

論文の再現実装をしてみた

練習として論文のネットワークを再現実装してみた話です。

最初に言ってしまうと結果は再現できなかったんですが、まあ練習にはなったと思うので書こうかなと。

読んだ論文

Seeing Voices and Hearing Facesという論文を読みました。

人の顔だけ見て「あ、なんかこんな声な気がする」とか、人の声だけを聞いて「あ、なんかこんな顔な気がする」みたいな現象があると思います。この論文では人の顔から声を、もしくは人の声から顔を当てさせるタスクを検討しています。検討の方法として論文の中では2つの顔画像と1つの音声を入力してどちらの顔画像と対応する音声かを当てさせる実験(2つの音声と1つの顔画像で逆の実験も行っている)、顔画像の他に動的特徴量も一緒に入力することで顔画像のみを入力した場合と比較して正解率が向上することを確認した実験、2択ではなくさらに選択肢を増やした上でネットワークに正解を当てさせる実験などを行っていました。

今回は、この中で2つの顔画像から対応する音声を当てさせるネットワークを(多分)実装してみました。論文の中の図がイメージとしてわかりやすいと思います。

f:id:kanoeuma_310mo:20190919213344p:plain:w300
[1]より引用

また、今回の実装は以下の場所においてあります。

GitHub - 310mo/svhf-self

データ集めとそれの整備

量がすごいので実際に実験に使ったデータそのままではなく、データの一部を使って学習しました。正解率が出なかったのやっぱりここが原因じゃないかなあ...

顔画像

実験に使用された顔画像は研究のホームページにおいてくれています。嬉しい。DATA AND MODELS のCropped Face Images extracted at 1 fpsをダウンロードして使いました。

音声データ

VoxCelebというデータセットを使います。研究のホームページにもリンクがおいてあります。今回はVoxCelebデータセットのうちVoxCeleb1のAudio files の Dev A の音声データを使いました。

データの整備

ダウンロードしただけでは使えないので、データを整備します。

まず、顔画像と音声データの対応が書いてあるcsvファイルを研究のホームページからダウンロードします。(DATA AND MODELS にある Speaker Metadata)

そのcsvファイルの情報を使って対応する顔画像と音声データをまとめます。書いたコードはこんな感じです。

import csv
import os
import shutil

#csvファイルの読み込み
csv_file = open('vox_meta.csv', 'r')
csv_data = csv.reader(csv_file)

#データのリストを取得
idlist = os.listdir('wav')
name_list = os.listdir('unzippedFaces')

#ディレクトリがなかったら作成
if not os.path.exists('data'):
    os.mkdir('data')


for row in csv_data:
    if row[0] in idlist:
        row_id = row[0]
        row_name = row[1]
        #顔画像と音声データを入れるディレクトリを作成(なかったら)
        if not os.path.exists(os.path.join('data', row_id)):
            os.mkdir(os.path.join('data', row_id))
        target_dir = os.path.join('data', row_id)

        #音声を一つ取得
        dir_list = os.listdir(os.path.join('wav', row_id))
        file_list = os.listdir(os.path.join('wav', row_id, dir_list[0]))
        sound_file = os.path.join('wav', row_id, dir_list[0], file_list[0])
        shutil.copy(sound_file, os.path.join(target_dir, row_id+'.wav'))

        #画像を一つ取得
        image_dir_list = os.listdir(os.path.join('unzippedFaces', row_name, '1.6'))
        image_file_list = os.listdir(os.path.join('unzippedFaces', row_name, '1.6', image_dir_list[0]))
        image_file = os.path.join('unzippedFaces', row_name, '1.6', image_dir_list[0], image_file_list[0])
        shutil.copy(image_file, os.path.join(target_dir, row_id+'.jpg'))

コードの内容は、ダウンロードしたデータとかcsvファイルに合わせまくってしまっています。また、1人に対して複数の顔画像や音声データがあるのですが、今回は適当に1つずつ選んでしまっています。データを増やして試すにはここら辺をちゃんとする必要があると思います。

次に、学習時の入力のために「顔と音声のペア」のペアをたくさん作ります。ネットワークに入力するときに逐一ペアを作っても問題ないとは思うんですけど、今回はあらかじめペアを作っておいてそれを入力する形をとりました。166人のうち2人を選ぶので{}_{166}C_2通りのペアが得られます。

ペアを作るコードはこんな感じです。

import os
import shutil
import copy

#dataからpair dataを作る(2つのディレクトリにあるjpgとnpyをもつディレクトリをたくさん作る)

#ディレクトリがなかったら作成
if not os.path.exists('pair_data'):
    os.mkdir('pair_data')

id_list = os.listdir('data')
id_list2 = copy.copy(id_list)

print(len(id_list))

count = 0
for face_id in id_list:
    for face_id2 in id_list2:
        if face_id == face_id2:
            continue
        else:
            count += 1
            
            if not os.path.exists(os.path.join('pair_data', str(count))):
                os.mkdir(os.path.join('pair_data', str(count)))
            shutil.copy(os.path.join('data', face_id, face_id+'.jpg'), os.path.join('pair_data', str(count)))
            shutil.copy(os.path.join('data', face_id, face_id+'.npy'), os.path.join('pair_data', str(count)))
            shutil.copy(os.path.join('data', face_id2, face_id2+'.jpg'), os.path.join('pair_data', str(count)))
            shutil.copy(os.path.join('data', face_id2, face_id2+'.npy'), os.path.join('pair_data', str(count)))            
            
    id_list2.remove(face_id)

ネットワークの実装と学習

ネットワークの定義と学習をします。使ったのはpytorchです。理由はなんか流行ってるからです。

論文で定義されているネットワークとその実装

論文の中ではネットワークのはこんな感じで定義されています。

f:id:kanoeuma_310mo:20190919220334p:plain:w300
ネットワークの図、[1]より引用

これを元にこんな感じでネットワークを書きました。

    def __init__(self):
        super(SvhfNet, self).__init__()
        self.face_features = nn.Sequential(
            nn.Conv2d(3, 96, kernel_size=7, stride=2, padding=3),
            nn.BatchNorm2d(96),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2),
            nn.Conv2d(96, 256, kernel_size=5, stride=2, padding=2),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2),
            nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2),
        )
        self.face_fc1 = nn.Linear(7*7*256, 4096)
        self.face_bn1 = nn.BatchNorm1d(4096)
        self.face_relu1 = nn.ReLU(inplace=True)
        self.face_fc2 = nn.Linear(4096, 1024)

        self.sound_features = nn.Sequential(
            nn.Conv2d(1, 96, kernel_size=7, stride=2, padding=1),
            nn.BatchNorm2d(96),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2),
            nn.Conv2d(96, 256, kernel_size=5, stride=2),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2),
            nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=(3,2)),
        )
        self.sound_fc1 = nn.Conv1d(256, 4096, kernel_size=(10, 1))
        self.sound_bn1 = nn.BatchNorm2d(4096)
        self.sound_relu1 = nn.ReLU(inplace=True)
        self.sound_apoo1 = nn.AvgPool2d(kernel_size=(1,8), stride=1)
        self.sound_fc2 = nn.Linear(4096, 1024)

        self.all_fc1 = nn.Linear(3072, 1024)
        self.all_bn1 = nn.BatchNorm1d(1024)
        self.all_relu1 = nn.ReLU(inplace=True)
        self.all_fc2 = nn.Linear(1024, 512)
        self.all_bn2 = nn.BatchNorm1d(512)
        self.all_relu2 = nn.ReLU(inplace=True)
        self.all_fc3 = nn.Linear(512, 2)

    def forward(self, face1, face2, sound):

        face1 = self.face_features(face1)
        face1 = face1.view(face1.size(0), -1)
        face1 = self.face_fc1(face1)
        face1 = self.face_bn1(face1)
        face1 = self.face_relu1(face1)
        face1 = self.face_fc2(face1)

        face2 = self.face_features(face2)
        face2 = face2.view(face2.size(0), -1)
        face2 = self.face_fc1(face2)
        face2 = self.face_bn1(face2)
        face2 = self.face_relu1(face2)
        face2 = self.face_fc2(face2)

        sound = self.sound_features(sound)
        sound = self.sound_fc1(sound)
        sound = self.sound_bn1(sound)
        sound = self.sound_relu1(sound)
        sound = self.sound_apoo1(sound)
        sound = sound.view(sound.size(0), -1)
        sound = self.sound_fc2(sound)

        all_feature = torch.cat([face1, face2, sound], dim=-1)
        all_feature = self.all_fc1(all_feature)
        all_feature = self.all_bn1(all_feature)
        all_feature = self.all_relu1(all_feature)
        all_feature = self.all_fc2(all_feature)   
        all_feature = self.all_bn2(all_feature)
        all_feature = self.all_relu2(all_feature)     
        all_feature = self.all_fc3(all_feature)

        return all_feature

ReLUとかBatchNormとかベタ書きしてるので汚いです。もうちょっと綺麗に書けばよかった。あと、BatchNormをどれくらい入れるべきかよくわからなかった(論文の中にどこに入れるかみたいな記述が見つけられなかった)ので、Conv層と全結合層の前に手当たり次第に入れています。

このデータセットを使って学習を行います。

結果と考察

テストデータとしてVoxCeleb データセットの VoxCeleb1のTestに訓練データと同様の処理をして作ったペアを使いました。

論文で示されている正解率は81.0%だったんですけど、自分が学習した結果、正解率は56.2%でした。残念。テストデータが一致しているわけではないので一概には言えないですが、論文で示されていたほどの汎化性能を獲得することはできませんでした。

これは余談なんですが、訓練データの10%をテストデータ、90%を訓練データとして訓練データとして学習した場合の正解率は80%を超えてました。これは、顔画像や音声のCNNに入力するデータ自体が固定されていれば、ペアをある程度入れ替えても性能が落ちないことを示しているんじゃないかなあと思います。そういう意味では何かしらの学習はできていたということになるのかしら。

全体のまとめとしては、データの量がやっぱり少なかったんじゃないかなあと思います。まあ、randomよりは正解率一応高かったので何かしらの学習はできていたんじゃないでしょうか。

参考文献

[1] A. Nagrani, S. Albanie, A. Zisserman, Seeing Voices and Hearing Faces: Cross-modal biometric matching, IEEE Conference on Computer Vision and Pattern Recognition, 2018.