CNNとRankNetを用いた画像の順序予測(ラブライブ!のキャラクター順序予測を例に)

(Chainer Advent Calendar 2016 5日目です.この記事はTokyoTechLTで発表したものと同内容のものです.)

この記事は何?

以前Chainer Advent Calendar 2015において,Chainerを用いたRankNet(ランク学習手法の1つ)の実装を紹介しました
本記事では,RankNetを応用した画像の順序予測を紹介します.

やりたいことの概要

RankNetを応用した画像順序予測の概要図を示します.
f:id:sz_dr:20161204133645p:plain


訓練データとして,既に順序付けされた画像集合を用います.
順序付けの例として,ユーザーの好みによるレイティングが考えられます.クリック率などを使う方法も可能だと思います.

得られた訓練データをCNNに入力し学習します.目的関数は,訓練データ中の画像対の順序が正しく識別できるように設定します(RankNet).

得られた予測器にレイティング未知の画像集合(テストデータ)を入力します.予測器の出力はレイティングを表しているため,これを用いて画像集合の順序付けを行います.

何に使えそう?

画像が好みの順番に並んでいると嬉しいケースは色々考えられます.

  • 漫画の表紙買い,アルバムのジャケット買い
  • pixivのようなイラストコミュニケーションサービスで,好みの画像検索
  • Amazonなどのショッピングサイトで,購買率が上がるような画像の表示

実装

まずはネットワークを書きます.CNNもRankNetも非常にシンプルに書けました(Chainer強い).
CNNのパラメータは決め打ちです,入力画像はRGBでサイズは80×80とします.

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


class CNN(Chain):

    def __init__(self):
        """
        picture size: 3 * 80 * 80
        Convolution2D parameter
        input_channels, output_channels, filter_size
        """
        super(CNN, self).__init__(
            conv1=F.Convolution2D(3, 20, 5),
            conv2=F.Convolution2D(20, 50, 5),
            l1=L.Linear(14450, 500),
            l2=L.Linear(500, 1)
        )

    def __call__(self, x):
        h1 = F.max_pooling_2d(F.relu(self.conv1(x)), 2)
        h2 = F.max_pooling_2d(F.relu(self.conv2(h1)), 2)
        h3 = F.relu(self.l1(h2))
        y = self.l2(h3)
        return y


class RankNet(Chain):

    def __init__(self, predictor):
        super(RankNet, self).__init__(predictor=predictor)

    def __call__(self, x_i, x_j, t_i, t_j):
        s_i = self.predictor(x_i)
        s_j = self.predictor(x_j)
        s_diff = s_i - s_j
        if t_i.data > t_j.data:
            S_ij = 1
        elif t_i.data < t_j.data:
            S_ij = -1
        else:
            S_ij = 0
        self.loss = (1 - S_ij) * s_diff / 2. + \
            F.math.exponential.Log()(1 + F.math.exponential.Exp()(-s_diff))
        return self.loss

次は訓練のスクリプトを書きます.
入力は訓練画像とレイティングファイルです.訓練画像はnumpyでシリアライズされたファイル(.npy)とし,レイティングファイルは各画像のレイティングがN行並んだテキストファイルとします.
出力は訓練画像のレイティング結果と,予測器をシリアライズしたファイル(.pkl)です.

# -*- coding: utf-8 -*-
import argparse
import pickle
import numpy as np
from chainer import Variable, optimizers
import net


def ndcg(y_true, y_score, k=None):
    y_true = y_true.ravel()
    y_score = y_score.ravel()
    if k is None:
        k = len(y_true)
    y_true_sorted = sorted(y_true, reverse=True)
    ideal_dcg = 0
    for i in range(k):
        ideal_dcg += (2 ** y_true_sorted[i] - 1.) / np.log2(i + 2)
    dcg = 0
    argsort_indices = np.argsort(y_score)[::-1]
    for i in range(k):
        dcg += (2 ** y_true[argsort_indices[i]] - 1.) / np.log2(i + 2)
    ndcg = dcg / ideal_dcg
    return ndcg

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("X_train_npy")
    parser.add_argument("rank_file")
    parser.add_argument("--output_pred", "-op", default="pred.txt")
    parser.add_argument("--output_model", "-om", default="model.pkl")
    parser.add_argument("--image_scale", type=float, default=1/255)
    parser.add_argument("--n_iter", type=int, default=1000)
    parser.add_argument("--monitor_step", type=int, default=100)
    parser.add_argument("--ndcg_k", type=int, default=100)
    args = parser.parse_args()

    np.random.seed(0)
    X_train = np.load(args.X_train_npy)
    X_train *= args.image_scale
    y_train = np.loadtxt(args.rank_file)
    N_train = np.shape(X_train)[0]

    model = net.RankNet(net.CNN())
    optimizer = optimizers.Adam()
    optimizer.setup(model)

    for step in range(1, args.n_iter + 1):
        i, j = np.random.randint(N_train, size=2)
        # X_train: (N, 3, 80, 80)
        x_i = X_train[i].reshape((1, ) + X_train[i].shape)
        x_j = X_train[j].reshape((1, ) + X_train[j].shape)
        y_i = Variable(np.array(y_train[i]))
        y_j = Variable(np.array(y_train[j]))
        model.zerograds()
        loss = model(x_i, x_j, y_i, y_j)
        loss.backward()
        optimizer.update()
        if step % args.monitor_step == 0:
            train_pred = model.predictor(X_train).data
            train_ndcg = ndcg(y_train, train_pred, args.ndcg_k)
            print("step: {} | NDCG@{} | train: {}".format(
                step, args.ndcg_k, train_ndcg))

    y_pred = model.predictor(X_train).data
    np.savetxt(args.output_pred, y_pred)
    out_model = args.output_model
    with open(out_model, "wb") as out_fp:
        pickle.dump(model, out_fp)

最後に予測のスクリプトを書きます,といってもテスト画像と予測器を読み込んで予測するだけですが…

# -*- coding: utf-8 -*-
import argparse
import pickle
import numpy as np

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("model_pkl")
    parser.add_argument("X_test_npy")
    parser.add_argument("--output", "-o", default="pred.txt")
    args = parser.parse_args()

    with open(args.model_pkl, "rb") as model_pkl_fp:
        model = pickle.load(model_pkl_fp)
    X_test = np.load(args.X_test_npy)
    y_pred = model.predictor(X_test).data
    np.savetxt(args.output, y_pred)

使用例

以上の実装を使って,「ラブライブ!サンシャイン!!」の画像順序予測をやってみたいと思います.
訓練データには「ラブライブ!」の画像を用いました.

学習の流れ

  1. Lantisちゃんねるから『TVアニメ「ラブライブ!」先行発表PV』を取得します.
  2. OpenCVを用いて顔部分をクリップします.277枚の顔画像を生成しました.顔画像認識はlbpcascade_animefaceを参考にしました.
  3. 各画像にレイティングを付けます(277枚の画像にレイティングを付けます,つらい作業).
  4. 上記で実装したCNN+RankNetで学習します.

訓練データに対する予測結果

まず,上記の実装が正しく動いていることを確認するために,訓練データに対して予測を行い正しく順序付けされているかを確認します.
f:id:sz_dr:20161204142340p:plain

レイティング値は好み度を表しています(左に行くほど好きです).
にこにーと希ちゃんが上位にたくさん来てます.花陽ちゃんは1枚だけ見当たります,レイティング下位のキャラクターと類似性が高いためでしょうか…
訓練データ内に非常に類似した画像が含まれていますが,これはクリッピング後にクラスタリング等を行っていないためです.冗長なデータは訓練データから取り除いた方が良いと思います.

テストデータに対する予測結果

学習の結果得られた予測器を使って,「ラブライブ!サンシャイン!!」の画像順序予測をします.
テスト画像はLantisちゃんねるからラブライブ!サンシャイン!! Aqoursメンバー紹介PVを取得し,先程と同様に顔画像をクリップします.各キャラ毎に2種類の画像を取り出して計18枚の画像をテストデータとしました.
f:id:sz_dr:20161204143225p:plain

上が私がランク付けした結果(左に行くほど好き)で,下がCNN+RankNetによる予測結果です.
なかなか私のランク付け結果とは一致しませんが,訓練データの傾向は反映しているように見えます.
また,同じキャラクターの画像は似たような順位に来ていることがわかります.

まとめ

この記事ではCNN+RankNetを使った画像の順序予測について,実装および使用例を紹介しました.
使用例で紹介したキャラクター順序予測は難しい問題です.ランク付けの際に,キャラクターへの思い入れが入ってしまうので…
キャラクター順序予測に本気で取り組む際には,キャラクターの性格を学習にどうにかして取り入れる必要がありそうです.
これについては,TokyoTechLTで発表後に以下のような意見をいただきました.



マルチモーダル学習になってきましたね,テキストで順位付けしている例はまさにwebページランキングですね.画像情報を組合せたランキング予測なんてやられているんでしょうか…??新しい研究ネタになりそうです.

レイティングを付ける作業は結構大変です,そのため学習データサイズが小さくなってしまうという問題点があります.これについては既存の学習済み予測器をfine tuningしたり,自動でレイティングを得る方法(画像クリック率)を考えると良さそうです.

あまりChainer自体の話はしていませんでしたが,Pylearn2 -> Caffe -> Chainerと移り変わってきた身からすると,Chainerはかなり楽にネットワークを書けるので好きです.Deep Learningフレームワークが溢れているご時世ですが,何でも良いので1つ使えるようにしておけば良いと思います…色々なフレームワークを平均的に知っているよりも,1つのフレームワークを極める方が良いんじゃないかなあ(・。・)