機械学習
python3
AI

同じか否かを判定するための距離学習(Metric Learning)

距離学習 (Metric Learning)

2つが同じモノを表しているか、それとも違うモノを表しているのか?

このような問題を解く場合、2つの間に『距離』を定義して、それが近ければ同じ、遠ければ違うと判断する方法が考えられます。ここでの『距離』は学習データを用いる事により、違うモノはより大きく、同じモノはより小さくなるように、目的に合わせて柔軟に定義する事ができます。このような操作の事を距離学習(Metric Learning)と言います。

KISSME

距離学習は、データの性質や分布の違いや目的によって、最適なやり方が変わってくる事も多いため、これまでいろいろな種類のものが提案されています。その中から、同一判定(2つのものが同じか否かを分類)するための手法として、CVPR 2012でオーストリアのグラーツ工科大から発表された 「Large Scale Metric Learning from Equivalence Constraints1」を試してみました。DeepLearning (Neural Network) を使っておらず、同一判定であれば判定対象は何でも良いので、適用範囲が広いといった特徴が挙げられます。参照される事も多く、特に人物照合(person re-identification)の研究分野における最近の論文では、実験の比較対象としてよく利用されています。また、これを応用した新しいやり方も複数発表2345されています。

この論文で提案されている距離は「Keep It Simple and Straightforward MEtric」の頭文字を取ってKISSME と呼ばれています。

考え方

人物画像を例に説明します。例えば、2つの人物画像から取得した特徴量をそれぞれ xixj とした場合、この組合せは必ずどちらかに分類されます。

  • Ω0xixj が違う人物
  • Ω1xixj が同じ人物

したがって、xixj が同じ人物である確率は、

P(Ω1|xi,xj)

です。
これはベイスの定理における事後確率なので、尤度は下記のようになります。

P(xi,xj|Ω1)

これは、2つが同一人物となるケースにおいて、その特徴量がxixjである確率 という意味になります。同様に、違う人物に関しても尤度は、P(xi,xj|Ω0) と書けます。

KISSME ではこの2つの尤度の比を用いる事により、統計的推論の観点から、同一人物か否かを判定します。

つまり、仮説検定問題として、

  H0 (帰無仮説): xixj は同一人物ではない
  H1 (対立仮説): xixj は同一人物である

とおき、次の式

δ(xi,xj)=log(P(xi,xj|Ω0)P(xi,xj|Ω1))

がある値よりも小さい場合にはH0を棄却し、そうでない場合は採択するという尤度比検定方式による判定を行います。

ただし、この式の形だと2つの特徴量xixjを考えなければいけないので、

xij=xixj

といった差の空間で考えます。これにより2つの特徴量は1つに減り、さらに平均0の問題となるため、より単純な形に置き換える事が可能となります。ちなみに、平均が0になる理由は 各 xixjについて必ず逆の xjxi が存在するからです。

δ(xij)=log(P(xij|Ω0)P(xij|Ω1))

上記の P(xij|Ω0)P(xij|Ω1) をそれぞれ平均0のガウス分布でモデル化すると、

δ(xij)=log(N(xij|0,Σyij=0)N(xij|0,Σyij=1))=log(12π|Σyij=0|exp(12xijTΣyij=01xij)12π|Σyij=1|exp(12xijTΣyij=11xij))

と書け、これを展開して整理すると、

δ(xij)=xijTΣyij=11xij+log(|Σyij=1|)xijTΣyij=01xijlog(|Σyij=0|)

になります。
ここで xijが無い log(|Σyij=1|)log(|Σyij=0|) の項に注目してください。この2つの項は関数の入力値であるxijに依存しないため、オフセット値としての機能しかないただの定数項です。今の場合、ある値より小さいければ棄却、そうでなければ採択という閾値判定方式なので省いても問題ありません。定数項を省き整理すると、

δ(xij)=xijT(Σyij=11Σyij=01)xij

となります。
最後に (Σyij=11Σyij=01) の部分を(半)正定値化してマハラノビス距離の形にすると、

dM2(xi,xj)=(xixj)TM(xixj)

となります。
これがKISSMEの距離算出式となります。

計算方法

ガウス分布の分散共分散行列を学習データから最尤推定します。これは結局のところ、学習データの分散共分散行列がガウス分布における分散共分散行列の最尤推定量となるので、下記の計算で求める事ができます。

Σyij=1=yij=1(xixj)(xixj)TΣyij=0=yij=0(xixj)(xixj)T

次に、これらの逆行列を求めます。分散共分散行列は正定値行列なので、正定値行列の逆行列より、その逆行列の固有ベクトルは同じで固有値は逆数となります。つまり、n×nの分散共分散行列Σとその逆行列Σ1は下記のような関係になります。(ただし、uiは長さ1の固有ベクトル)

Σ=i=1nλiuiuiTΣ1=i=1n1λiuiuiT

これを利用すれば、分散共分散行列の逆行列Σyij=11および Σyij=01を求める事ができます。
次に、その差である

M^=(Σyij=11Σyij=01)

を求めます。Σyij=11および Σyij=01のどちらも正定値行列つまり対称行列なので、M^は対称行列になる事はわかります。しかし、M^が正定値行列になるとは限りません。マハラノビス距離の要件を満たすためには、なんとか操作して正定値行列にする必要があります。正定値行列であることと、固有値が全て正であることは同値 となるので、以下の手順に従って固有値を全て正の実数に置換し、正定値行列Mを得ます。

  1. M^ の固有値と固有ベクトルを求める。
  2. 固有値のうち0以下の値を(例えば0.0001などの)ごく小さい任意の正の実数に置換する。
  3. 置換した固有値と固有ベクトルから正定値行列Mを再計算する。

Mは対称行列なので、

M=UΛUT

として計算できます。Uは長さ1の固有ベクトルを並べた直交行列、Λはその固有ベクトルに対応する固有値を対角成分に順に並べた対角行列です。

実験

LEAR ToyCars6 というデータセットを使って試してみます。
このデータセットは、14種類のおもちゃの車で構成され、全部で256枚の画像があります。この中から任意の2枚の画像を選び出し、同じ車かどうかを判別します。

sameordifferent_car.png

データセットのうち学習に利用するのは7種類の車で、同一のペア1185個と異なるペア7330個を作って学習します。残りの7種類はテスト用です。

特徴量の作成

下記の手順にしたがって画像から特徴量を作成します。

  1. 画像を30x30の重複しないブロックに分割する
  2. HSVとLabの色空間の各チャンネル毎に24binでヒストグラム化する
  3. LocalBinaryPatternを用いて24binでヒストグラム化する
  4. 2と3で抽出した特徴量をコンキャットする
  5. PCAを用いて50次元まで圧縮する

上記の実装部分は下記になります。

create_feature.py
import os
import numpy as np
import cv2
from skimage import feature

from sklearn.decomposition import PCA

IMAGE_WIDTH = 540
IMAGE_HEIGHT = 150
SPLIT_WIDTH = 30
SPLIT_HEIGHT = 30
PCA_DIMENSION = 50
class ImageData():
    def __init__(self, image_id, file_name):
        self.image_id = image_id
        self.file_name = file_name
        self.bgr_img = cv2.imread(file_name)

        # 30x30blockで割り切れる数に画像サイズを変更しゼロパディング.画像毎にサイズが異なるためパディングサイズは都度算出
        h, w, _ = self.bgr_img.shape
        sub_height = IMAGE_HEIGHT - h
        sub_width = IMAGE_WIDTH - w
        self.bgr_img = np.pad(self.bgr_img, [(sub_height // 2, sub_height - (sub_height // 2)), (sub_width // 2, sub_width - (sub_width // 2)), (0,0)], 'constant')

        # blockに分ける
        v_split = self.bgr_img.shape[0] / SPLIT_HEIGHT
        h_split = self.bgr_img.shape[1] / SPLIT_WIDTH
        self.bgr_images = []
        for h_img in np.vsplit(self.bgr_img, v_split):
            self.bgr_images.extend(np.hsplit(h_img, h_split))

        # Extraction Feature
        self.raw_feature = []
        self.raw_feature.extend(self.hsv_hist())
        self.raw_feature.extend(self.lab_hist())
        self.raw_feature.extend(self.lbp_hist())

    def set_feature(self, feature):
        self.feature = feature

    def hsv_hist(self):
        hist = []
        for img in self.bgr_images:
            img_ch = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
            # Hue 定義域は[0,180)
            hist.extend(cv2.calcHist([img_ch], [0], None, [24], [0,180]))
            # Saturation
            hist.extend(cv2.calcHist([img_ch], [1], None, [24], [0,256]))
            # Brightness
            hist.extend(cv2.calcHist([img_ch], [2], None, [24], [0,256]))
        return [item for sublist in hist for item in sublist]

    def lab_hist(self):
        hist = []
        for img in self.bgr_images:
            img_ch = cv2.cvtColor(img, cv2.COLOR_BGR2Lab)
            # L*
            hist.extend(cv2.calcHist([img_ch], [0], None, [24], [0,256]))
            # a*
            hist.extend(cv2.calcHist([img_ch], [1], None, [24], [0,256]))
            # b*
            hist.extend(cv2.calcHist([img_ch], [2], None, [24], [0,256]))
        return [item for sublist in hist for item in sublist]

    def lbp_hist(self):
        METHOD = 'uniform'
        POINTS = 22
        RADIUS = 9
        hist = []
        for img in self.bgr_images:
            gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            # compute the Local Binary Pattern representation of the image
            lbp = feature.local_binary_pattern(gray, POINTS, RADIUS, method=METHOD)
            # histogram
            (block_hist, _) = np.histogram(lbp.ravel(), bins=POINTS + 2, range=(0, POINTS + 2), density=False)
            hist.extend(block_hist)
        return hist

def main(image_files):

    # ImageDataを生成
    image_list = []
    for (i_id, i_file) in enumerate(image_files):
        image_list.append(ImageData(i_id, i_file))

    # 特徴量をまとめPCAにかける
    feature_list = []
    for i in image_list:
        feature_list.append(i.raw_feature)
    feature_numpy = np.array(feature_list, dtype=np.float)
    pca = PCA(n_components=PCA_DIMENSION)
    x_pca = pca.fit_transform(feature_numpy)

    # PCAで50次元まで削減した値を各イメージの特徴量として設定
    for i, idata in enumerate(image_list):
        idata.set_feature(x_pca[i, :])

行列Mを求める

学習データの特徴量から行列Mを求めます。

create_mahalanobis.py
class CarPair():
    def __init__(self, image_a, image_b, is_same, is_train):
        self.image_a = image_a # ImageDataのインスタンスA
        self.image_b = image_b # ImageDataのインスタンスB
        self.is_same = is_same
        self.is_train = is_train

    def get_feature_diff(self):
        return self.image_a.feature - self.image_b.feature

class Mahalanobis():
    def __init__(self, mat):
        self.mat = mat

    def distance(self, vec):
        dist = vec.dot(self.mat).dot(vec.T)
        dist = np.diag(dist) # vecは1次元のベクトルではなく行列を想定
        return dist

class PairSpace():
    def __init__(self, vec):
        self.vec = vec

    # 平均
    def mean(self):
        mean = np.mean(self.vec, axis=0)
        return mean

    # 分散共分散行列
    def cov(self):
        self.sig = np.cov(self.vec.T)
        return self.sig

    # 分散共分散行列の逆行列
    def inv_sig(self):
        # 分散共分散行列を求める
        sig = self.cov()

        # 分散共分散行列の固有値・固有ベクトルを求める
        la, va = np.linalg.eig(sig)

        # 固有値の対角行列
        la_diag = np.diag(la)

        # 逆行列の固有値
        inv_diag = np.diag(1/la)
        # 実対称行列の性質から逆行列を求める
        inv_sig = va.dot(inv_diag).dot(va.T)

        return inv_sig

def to_semidefinite_mat(m):
    # mの固有値・固有ベクトル
    lm, vm = np.linalg.eig(m)
    # Mの固有値の対角行列
    m_diag = np.diag(lm)
    # 固有値を全て0より大きい値に変更する
    lmm = np.zeros(lm.shape[0])
    for i in range(lm.shape[0]):
        if lm[i] > 0:
            lmm[i] = lm[i]
        else:
            lmm[i] = 0.00000001

    # 改良Mの固有値の対角行列
    mm_diag = np.diag(lmm)
    # 正定値行列を求める
    MM = vm.dot(mm_diag).dot(vm.T)
    return MM

def main(all_car_pair_list):

    # CarPairのインスタンスから、同じペアの特徴量の差を取得
    same_vec = np.array(list(map(lambda c: c.get_feature_diff(), filter(lambda c: c.is_same and c.is_train , all_car_pair_list))))
    # CarPairのインスタンスから、違うペアの特徴量の差を取得
    diff_vec = np.array(list(map(lambda c: c.get_feature_diff(), filter(lambda c: not c.is_same and c.is_train , ))))

    # PairSpaceオブジェクトを生成
    same_space = PairSpace(same_vec)
    diff_space = PairSpace(diff_vec)

    # 分散共分散行列の逆行列を取得
    same_inv_sig = same_space.inv_sig()
    diff_inv_sig = diff_space.inv_sig()

    # 調整した半正定値行列
    met = to_semidefinite_mat(same_inv_sig - diff_inv_sig)
    # Mahalanobisを生成
    mahalanobis = Mahalanobis(met)

実験結果

テストデータに対して距離dM2(xi,xj)を算出した結果になります。同一ペア(赤)1044個と異なるペア(青)6337個の分布を表しており、横軸は距離で縦軸はペアの数です。

スクリーンショット 2018-08-16 19.18.58.png

同一ペアの距離dM2(xi,xj)は、0付近に集中していて、異なるペアは広範囲に分布しています。
この2つのROCカーブは下図になります。

スクリーンショット 2018-08-16 19.18.42.png

オリジナルの論文でもこのデータセットを使って実験をしていますが、それとほぼ同じぐらいの精度にはなったかと思います。この実験をやってみてわかったのは、KISSMEの距離学習の精度は、どのような特徴量を使うかで結構変わるという事です。実際に、特徴量を少し変えては精度を確認するということを繰り返しながら、手探りで進めていきました。このあたりはDeepLearningを使ってKISSMEの距離学習に合う特徴量を自動抽出するという応用も考えられそうな気がします。何か良いアイデアはないか、少し可能性を模索してみたいと思います。

正定値行列の逆行列

Aを正定値行列とする。固有値の定義より

Ax=λx 

両辺の左からAの逆行列を掛ける

A1Ax=A1λxx=λA1x

定義より正定値行列は固有値が全て正の実数となる。つまり、λ0なので両辺をλで割ると、

1λx=A1x

これは固有値の定義に一致する。したがって、A1の固有値は1λで、固有ベクトルはxとなる。また、全ての固有値は 1λ>0 となるため、正定値行列の逆行列A1も正定値行列となる。


  1. M. Koestinger, M. Hirzer, P. Wohlhart, P. M. Roth, and H. Bischof. "Large scale metric learning from equivalence constraints." In CVPR, 2012. 

  2. S. Liao, Y. Hu, X. Zhu, and S. Z. Li. "Person re-identification by local maximal occurrence representation and metric learning." In CVPR, 2015. 

  3. Y. Yang, S. Liao, Z. Lei, and S. Z. Li. "Large scale similarity learning using similar pairs for person verification." In AAAI, 2016. 

  4. Y. Yang, Z. Lei, S. Zhang, H. Shi, and S. Z. Li. "Metric embedded discriminative vocabulary learning for high-level person representation." In AAAI, 2016. 

  5. S. Bak, and P. Carr. "One-Shot Metric Learning for Person Re-identification." In CVPR, 2017. 

  6. E. Nowak and F. Jurie. "Learning Visual Similarity Measures for Comparing Never Seen Objects," In CVPR, 2007.