けやみぃアーカイブ

CV勉強中の大学生のアウトプットです

【PyTorch】Mixed Precision (autocast) での an illegal memory access was encountered / 処理が遅い問題

PyTorchのバージョンは1.13.0です.

PyTorchのMixed Precisionを試した時に起こった現象と解決した方法についてのメモです. 現在のPyTorchでMixed Precisionを行うには,以下のように with torch.autocast("cuda", torch.float16): で囲むことで勝手に半精度で計算してくれます.

pytorch.org

エラー

ランダムな処理を行うプログラムを回していたところ,以下の2種類のエラーが出たり出なかったりしました.

RuntimeError: CUDA error: CUBLAS_STATUS_EXECUTION_FAILED when calling `cublasGemmStridedBatchedExFix
RuntimeError: CUDA error: an illegal memory access was encountered terminate called after throwing an instance of 'c10::Error' what(): CUDA error: an illegal memory access was encountered

このエラーが出た個所がF.affine_gridを呼び出しているところだったためそれ関連のエラーかと思いましたが,最終的には以下のissueに答えがありました.

github.com

どうやら単純にVRAMが足りなかっただけだったようです.実際,バッチサイズを減らしてみると見事にエラーが出なくなりました.かなしい

(この直前にランダムに画像をパディングする処理をかけており,パディング幅が大きい時にF.affine_gridを呼び出した時点でVRAM不足になったのだと思います)

処理が遅い問題

次に起こったのは処理が一向に進まないという問題でした. この解決方法は,「読み込んだデータを.to("cuda")でキャストする」でした.

↓のような感じです

...
with torch.autocast("cuda", torch.float16):
    x = x.to("cuda")   # この行を追加
    y = model(x)
    ...

上記のwith文の中ではデータを自動でキャストしてくれるはずなので.to("cuda")は無くてもエラーは起こらないのですが,なぜか処理が進みませんでした.

(この辺りについて詳しい方がいればコメント頂けると幸いです🙇‍♂️)

【論文紹介】Latent Image Animator

記事タイトルをクリックして記事ページに移動すると数式が正しく表示されます

【6/20追記】実装が公開されたので、実装から読み取れる情報を追記しました。

タイトル:Latent Image Animator: Learning to Animate Images via Latent Space Navigation
arXivリンク:https://arxiv.org/abs/2203.09043
プロジェクトページ:https://wyhsirius.github.io/LIA-project/

ICLR2022の論文です。

簡単にまとめ

どんなもの?

Driving動画に沿ってSource画像のImage animationを行うモデル。(プロジェクトページの動画を見ると一目でわかると思います)

何が新しい?

MotionをM個の直交基底の線形和で表すようにしたこと。

手法詳細

以下図はすべて論文からの引用になります。

モデル構成

モデルの概要図は以下のようになっています。本モデルはEncoder$ E $、Generator$ G $、Motion Dictionary$ D_{m} $から構成されます。

モデル概要
まず下側のSource画像の処理から見ていきましょう。$ E $はSource画像から特徴マップ$ x^{enc}_{1 ... N} $と潜在変数$ z_{s \to r} $を出力します。rはreferenceの意味で、$ z_{s \to r} $は平均化されたポーズ(真顔で正面を向いた)の画像を生成します。これは実際にAppendixに生成画像が掲載されています。
Reference画像の生成

次にDriving画像の処理の方です。Source画像と同じように$ E $(Source画像に用いたのと同じモデル)に通し$ z_{d \to r} $を得ます。そこから更にMLPに通し、$ A_{r \to d} \in \mathbb{R}^M $を得ます。この$ A_{r \to d} $の要素を係数として、学習パラメータとして用意しておいた$ M $個の列ベクトル$ D_m = (d_1, d_2, ... d_M) $の線形和をとります(ただし$ D_m $を直交化してからこの計算を行います)。こうして得られたベクトル$ w_{r \to d} $はDriving画像のポーズを表します。

こうして得られた$ z_{s \to r} $と$ w_{r \to d} $を足し合わせた$ z_{s \to d} $を潜在変数として$ G $に入力します。$ G $はStyleGAN-likeな構造です(なので$ G $の左にある青正方形は4×4の固定ノイズです)。ただしToRGBまでの処理が異なります。Generatorの構造は以下のようになっています。

Generatorの構造
StyleGANでは存在しなかったwarpingの処理が追加されています。ここではStyleConvが生成した特徴マップ$ x $をさらに3x3 Convにかけて、1チャネルと2チャネルの特徴マップを生成します。1チャネルの方はSigmoid関数に通し、$ m_i $とします。2チャネルの方はtanh関数に通し、$ \phi_i $とします。そしてSource画像を$ E $に通した時の特徴マップ$ x^{enc}_i $を$ \phi_i $によって関数$ \mathcal{T} $(pytorchだとgrid_sample?)でwarpし、また$ m_i $をマスクとし、$ m_i \odot \mathcal{T}(\phi_i, x^{enc}_i) $をToRGBへと渡します。マスクによって変更する箇所・しない箇所を選択できることになります。

【6/20追記】実装では$ m_i \odot \mathcal{T}(\phi_i, x^{enc}_i) $の部分が$ m_i \odot \mathcal{T}(\phi_i, x^{enc}_i) + (1-m_i) \odot x $として計算されていました。

Loss

動画データセットを用いる前提なので、学習時にはSource画像とDriving画像は同一動画・同一人物の別フレームの画像であり、Driving画像の再構成を学習させることに注意してください。

Reconstruction Loss

生成画像$ x_{s \to d} $とDriving画像$ x_d $のL1 loss。

Perceptual Loss

FOMMでも使用されていた、multi-scaleでのPerceptual Lossです。$ x_{s \to d} $と$ x_d $を256、128、64、32のそれぞれにリサイズしVGG19に入力し、中間層での特徴マップでL1 lossをとります。

Adversarial Loss

non-saturating adversarial lossです。

これらの重み付き和をGeneratorのLossとします。論文の実験では、Perceptual Lossの係数を10、他は1としたようです。

なお、Discriminatorについては詳しく書かれていませんでした。StyleGAN同様、Adversarial Lossに加えてR1正則化あたりも必要になるんでしょうか…?

【6/20追記】実装ではAdversarial Lossのみが使用されていました。

実験結果

VoxCeleb、TaichiHD、TED-talksのデータセットでそれぞれ学習させています。VoxCelebは顔、TaichiHD、TED-talksは全身の動画データになります。

他手法との比較では、おおむね本手法が最も良い結果を示しています。なお、評価値はすべて生成画像とGTとの比較であり、AKDは対応するキーポイントの座標の誤差、MKRはGTで検出されているのに生成画像では検出できなかったキーポイントの割合、AEDはIdentity Lossと同様のものです。

他手法比較

また、User studyでは20人に他手法での生成結果と本手法での生成結果を並べてどちらがよりリアルか?を選んでもらうという方法をとっていますが、特にVoxCelebでは圧倒的に本手法の方がリアルだと選ばれています。

User study

その他、Motion Dictionaryの可視化も行っています。デモページのMotion Dictionary Linear Manipulationの動画がわかりやすいです。(なお実験では$ M = 20 $としています。) 特にTalking head(256x256)の$ d_2 $は「視線を左から右に動かす」というかなり細かいMotionに対応していて面白いと思いました。

また、Motion Dictionaryの有無についても検証されています。w/o dictionaryでは、Driving画像の肌感などが生成画像に反映されてしまっています。逆にwith dictionaryでは、ポーズ情報だけを反映するという本来の目的を達成できていることがわかります。

Motion Dictionaryの有無

感想

本手法ではMotion情報を$ M $次元ベクトルというかなり少ないパラメータで表せるので、lip syncなど他の分野にも応用できそうと思いました。 また、multi-domain(写真→アニメとか)での生成もできたら面白そうです。

ちなみに実装は近いうちに公開されるみたいです。 github.com

マスクをつけた顔画像の検出

自分の時はググってもあまり情報が出てこなかったので、一応ブログに残しておく。

先に結論

MTCNNそのまま使えばOKだった。

やりたいこと

ある画像に対して、

  1. マスクを着けている顔領域を検出する
  2. 更にランドマークを検出し、目が水平になるように画像を回転する

を実行したい。

やったこと

1にdlibのsimple_object_detectorを、2にdlibのshape_predictorをそれぞれ学習させた。 学習データは下記のリポジトリから画像データセットを拝借した。 アノテーションは手動で行い(pygameアノテーションGUIを作った)、100枚弱の画像データを作成した。

dlibのsimple_object_detectorやshape_predictorの学習は以下を参考にした。

ちなみにsimpel_object_detectorの方が過学習して結構困った。(optionのupsample_limitを大きくして対策した)

1については以下のリポジトリにマスク顔検出モデル(物体検出のSSDっぽい)があったので、こちらも試した。 github.com

結果

1にsimple_object_detector、2にshape_predictorを使った場合(顔領域の検出がダメ)

1にSSD、2にshape_predictorを使った場合(ランドマークの位置がずれている)

色々な画像で試してみた感じ、やっぱり1にSSDを使った方が精度はよかった。 特に2についてdlibの精度があまり向上しなかったので、ダメ元でMTCNN(facenet_pytorch)につっこんでみたら、かなりの精度で検出できてしまった。

まあ確かにMTCNNはCNNなのでちゃんと顔領域以外の情報も使っているので、それはそうだった…。dlibの方はそもそもデータが少ないとか、学習パラメータが適切でないとかはありそう。(前にアニメ顔の検出を学習させたときはかなり上手くできた)

一応コード

設定をいじりたい時はmodels/mtcnn.pyのMTCNNの引数を見てみるといいと思います。

import cv2
from facenet_pytorch import MTCNN

img = cv2.imread("img.jpg")

detector = MTCNN()
boxes, _, points = detector.detect(img, landmarks=True)
# デフォルトでは顔領域が最大面積の顔が出力される (models/mtcnn.pyのselect_largestの引数を参照)
x0, y0, x1, y1 = boxes[0]

# 検出した顔の矩形描画
cv2.rectangle(img, (int(x0), int(y0)), (int(x1), int(y1)), (255, 0, 0), 3)

for i in range(4):
    x,y = points[0][i]
    # 検出したランドマークの丸描画
    cv2.circle(img, (int(x), int(y)), 6, (255,0,0), -1)

cv2.imwrite("result.png", img)

StyleGAN2で顔ランドマーク座標を指定して画像生成【後編】

前回はキーポイントを指定した際に他の顔属性も変わってしまっていたので,顔属性を保持しながらキーポイントを変化させるために属性推定器を学習させます.

データ

顔属性

やはりラベル付きのデータが欲しいので,CelebAを使います.CelebAのラベルは40種類あります.全ラベルの意味は以下を参考に. cedro3.com

論文ではAzure Face APIを使ってラベルを増やして48種類の属性を推定しています.が,今回はCelebAでラベル付けされた40種類の属性のうち,①顔ランドマーク座標の指定で代替できそうな属性(目や口の開き方など),②髪色に関係する属性 を除いた28種類を用いることにします.

髪色

CelebAでは髪色に関するラベルは「Black Hair」「Blond Hair」「Brown Hair」の3種類しかありません.そこで柔軟にかつ多様に髪色を変更できるように,RGBの値で髪色を指定することを考えます.幸いなことに,SGF(今回使用している手法)は原理的には属性の指定方法に制約がありません.

具体的には顔領域をパース出来るモデルを使って髪部分のみを抽出し,その平均色をSGFの入力に加えることとします.パーシングには以下のモデルを使用します. github.com

SGFの models/classifier.py に組み込みます.

out = self.bisenet(img_tensor)[0]           # パース結果を出力
out = out.argmax(1)                              # チャネル方向にargmaxしてクラスを推定
hair = img_tensor[:, :, out[0]==17].mean(2)[0]     # 17(髪)クラスだと推定した部分の平均を取る

モデル

論文と同じようにVGGFace2で訓練したSEResNet50を使います.以下の学習済みモデルを使用します.これを使って適当にfine-tuningします. github.com

SGF実装

前回までの実装は毎iterationの更新ごとに画像を生成して目的性質との差を計算し,その誤差が最小になったところでストップします.が,これは速度の面からみると画像生成の部分がボトルネックになります.そこで誤差が最小になるところでストップせず,決めたステップ数まで更新するようにします.精度は下がりますが,速度向上は見込めます.

translate関数

z += d_z
# 画像生成
image, _ = G([z], input_is_latent=True, randomize_noise=False)

# 性質を推定
c = C(image).unsqueeze(0).to(device)

# 2乗和誤差
c_diff_new = (c-c1).pow(2).mean()
            
print(c_diff_new)

# 今回の誤差(c_diff_new)が前回の誤差(c_diff)よりも大きい場合は打ち止め
if c_diff > 0 and c_diff < c_diff_new:
    break

c_diff = c_diff_new

translate_faster関数

z += d_z
# cは一様に変化すると仮定
c += d_c

結果

キーポイント

まずは顔キーポイント座標を指定して生成してみます.左列がソース画像,上行がターゲット座標です.前回と比べればかなりアイデンティティは保存されているように見えます.そもそもキーポイント座標を別の画像から持ってきているため,輪郭などの不整合が起こっているというのもあってアイデンティティが微妙に変化しているということも考えられます. f:id:kym384:20210809163559p:plain

髪色

おまけで髪色指定です.上段が指定した色,左列がソース画像です.髪色はある程度指定通りになっていますが,アイデンティティの変化も見られます.メガネが生成されたりとかもありますね. f:id:kym384:20210810201752p:plain

念のため従来の方法(毎iteration画像生成する手法)でもやってみると,アイデンティティの著しい変化はなくなりましたが,髪色の変化は先ほどより小規模になってしまいました. f:id:kym384:20210810201804p:plain

時間があったらもう少し遊んでみたいと思います.

コード

github.com

StyleGAN2で顔ランドマーク座標を指定して画像生成【前編】

PFNから発表された Surrogate Gradient Field for Latent Space Manipulation という論文の再現実装をしてみました.どんな論文かは以下の図を見てもらえるとわかるかと思います.上段は属性(年齢や性別など)を指定してStyleGAN2で生成した画像です.中段では,今度は文章を使って花の外見を指定して生成しています.そして下段,個人的にはこれが一番面白いと思うのですが,顔のランドマーク座標を指定してアニメ顔を生成しています! f:id:kym384:20210730203730p:plain

簡単な解説

用語

  • G: 学習済みGenerator

論文ではStyleGAN2とProGANの2つで実験していますが,Generatorの形式によらないため他のものでも問題ありません.

  • C: 性質推定器

画像からその性質を推定するネットワークです.顔のランドマーク座標を指定して生成するタスクの場合は,dlibのランドマーク推定器なんかもこれにあたります.

  • z: 潜在変数

StyleGANの場合はwを使いますが,一般的にzと書くことにします.

問題定義

論文では簡単のため,C(G(z)),つまり潜在変数zから画像を生成して性質推定器に入力する操作を\Phi(z)と書いてるのでそれに従います.ここでの目標は,ランダムな潜在変数z_0とその性質c_0=\Phi(z_0)からスタートして欲しい性質c_1を持つ画像を生成する潜在変数z_1を求めることです.その過程をz(t)と表すこととし,z(0)=z_0z(1)=z_1とします.

手法

Auxiliary Mapping F(z, c)を定義します.Fz=F(z, c)となるように学習させます.中身はAdaINと(Spectral Normalization付きの)FC層のくり返しなので,zの情報をAdaINの正規化の過程で削減して,それをcで補完させることになっています.(論文にも構成が書かれていますが,本当にシンプルなネットワークです.)

ここで3つの仮定を設けます.

  1. 目的の性質c_1を持つ画像を生成するz_1が必ず存在する.

  2. z(t)を変化させた時,その生成画像の性質\Phi(z(t))は一様にc_1-c_0で変化する.つまり\frac{\rm{d}\Phi(z(t))}{\rm{d}t}=c_1-c_0

  3. 性質を変化させると,Fの出力も変化する.つまり\frac{\partial F(z, \Phi(z))}{\partial c} \neq 0 (c=\Phi(z))

そしてz(t)=F(z(t), \Phi(z(t)))の両辺をt微分します.それから仮定2を使って式変形することで,以下が得られます.(詳しくは論文の3.3をご覧ください)

\frac{\rm{d}z(t)}{\rm{d}t}=\left(\mathbf{I}-\frac{\partial F(z(t), \Phi(z(t)))}{\partial z}\right)^{-1}\frac{\partial F(z(t), \Phi(z(t)))}{\partial c}(c_1-c_0)

この右辺をH(z)と置き,Surrogate Gradient Fieldと呼んでいます.この微分方程式\frac{\rm{d}z(t)}{\rm{d}t}=H(z)を初期条件z(0)=z_0で数値的に解くことで,z(1)=z_1の近似を求めることができます!

また重要な点として性質判定器C逆伝播を計算する必要がありません! なので論文ではこの性質判定器にChainerで実装したCNNやdlibを使っています.

Auxiliary Mapping

Fについてもう少し解説しておきます.以下が掲載されているモデル図です.AdaINとLinear×2から成るConditional Linear BlockをN層重ねています.活性化関数にはLeaky ReLUを使っていて,全てのLinearにはSpectral Normalizationをかけています. f:id:kym384:20210730214340p:plain

ちなみに顔属性の判定器を作るためにAzureのAPIを使ってラベル付けして学習データを作ったらしいです.

実装

今回はFFHQで学習させたStyleGAN2を使って顔のランドマーク座標を指定して画像を生成させたいと思います.StyleGAN2のモデルには,以下で提供されている256の解像度でFFHQを学習させたモデルを使います.(軽量化のため)

github.com

ランドマーク座標推定にはdlibを使います.

Auxiliary Mappingの学習

学習データには論文同様20万枚生成した画像を使い,バッチサイズ32で50k iteration学習させました.cにはdlibの座標の出力を[-1, 1]に正規化し,一次元に並べました.なので68×2次元になっているわけです.

結果は以下です.左がランダムに生成した画像,右がAuxiliary Mappingの出力をもとに生成した画像です.いい感じに潜在変数を復元できていますね.(実際には論文よりもiterationを低めに設定しているので改善の余地はあります)

f:id:kym384:20210730220133p:plain

ランドマーク座標変換

論文に記載されている疑似コードをベースにSGFを実装していきます.ちなみに torch.autograd.functional.jacobian を知らなかったのでヤコビアンを求める方法に苦戦しました…

def translate(G, C, F, z0, c1, m=1, max_iteration=100, step=0.2):
    with torch.no_grad():
        image, _ = G([z0], input_is_latent=True, randomize_noise=False)
        c0 = C(image).unsqueeze(0)

        d_c = step * (c1 - c0)    # dΦ/dc = c1-c0

    images = [image]

    _, style_dim = z0.shape
    _, c_dim = c1.shape

    z = z0
    c = c0
    c_diff = -1

    for i in range(max_iteration):
        z.requires_grad = True
        c.requires_grad = True

        z_grad = torch.autograd.functional.jacobian(lambda z_:F(z_, c), z)[0,:,0,:]     # dF/dz
        c_grad = torch.autograd.functional.jacobian(lambda c_:F(z, c_), c)[0,:,0,:]     # dF/dc
        
        with torch.no_grad():
            # dz/dt = ( I - dF/dz)^{-1} * dF/dc * dΦ/dc
            d_z = c_grad @ d_c[0]    # dF/dc * dΦ/dc
    
           # ( I - dF/dz)^{-1} = I + dF/dz + (dF/dz)^2 + ...
            d_z_j = d_z
            for j in range(m):
                d_z_j = z_grad @ d_z_j
                d_z += d_z_j

            z += d_z
            image, _ = G([z], input_is_latent=True, randomize_noise=False)
            
            c = C(image).unsqueeze(0)

            # || Φ(z(t)) - c1 ||_2
            c_diff_new = (c-c1).pow(2).mean()
            
            print(c_diff_new)
            images.append(image)

            if c_diff > 0 and c_diff < c_diff_new:
                break

            c_diff = c_diff_new

    return images, z

結果は以下です.左端がランダムに生成したソース画像,右端がターゲット画像でこのランドマーク座標を持ったソース画像を生成するのが目的です.中間はその変換の過程の出力です.段々とターゲットのランドマーク座標に近づいてるのがわかります. f:id:kym384:20210730223337p:plain

ただ見てわかる通り,座標移動の過程で髪色などの属性も変化してしまっています.これは論文中でも言及されており,以下の図では上が座標だけを入力した場合,下が座標と共に属性情報を入力した場合です.なので本来であれば属性情報と共に入力しなければいけないのですが,今回はそのような分類器がなかったので座標だけの入力にしています… f:id:kym384:20210730221012p:plain

コード

github.com

後半は何かしらの属性識別器を作ってから書くつもりです.

PyTorchの異なるバージョン間での重み変換

なんやかんや複雑な事情で、学習時と推論時のPyTorchのバージョンが違った時の対処法です。特に学習時のバージョンの方が新しいと、推論する時に古いバージョンでは学習済みモデルを読み込んでくれないことがあるかと思います。まあ推論環境のPyTorchをアップデートすればいいだけなんですが、anaconda自体アップデートしなきゃいけなかったり、そうすると他にもいろいろ一緒にアップデートされてしまったりと面倒くさそうだったのでcheckpointの方を変換することにしました。

方法というほどの方法でもないんですが…一度numpyに変換して保存してあげます。

# translate_weights.py

from collections import OrderedDict
import numpy as np
import shutil
import torch
import sys
import os

# PyTorchの異なるバージョン間での重みの変換
# 例 PyTorch 1.8 で学習 -> PyTorch 1.1 で推論

name = sys.argv[1]

if not os.path.isdir("tmp"):
  # 変換元のPyTorchで実行 (例 PyTorch 1.8)

  os.mkdir("tmp")

  ckpt = torch.load(name)

  for k, v in ckpt.items():
    v = v.to("cpu").detach().numpy()
    k = k.replace(".", "+")

    np.save(f"tmp/{k}.npy", v)

else:
  # 変換後のPyTorchで実行 (例 PyTorch 1.1)

  ckpt = OrderedDict()

  for k in os.listdir("tmp/"):
    v = np.load(f"tmp/{k}")
    k = k[:-4].replace("+", ".")

    ckpt[k] = torch.from_numpy(v)

  torch.save(ckpt, name)

  shutil.rmtree("tmp")

最初に新しい方のPyTorchで読み込んでnumpy形式で保存した後に、古い方のPyTorchでnumpy読み込み→ptファイルで保存という流れになってます。なので

conda activate new_pytorch                       # 新しい方のPyTorchが入ってる環境
python trasnlate_weights.py hoge.pt              # 読み込みたいcheckpoint
conda activate old_pytorch                       # 古い方のPyTorchが入っている環境
python translate_weights.py new_hoge.pt          # 変換後のcheckpointの保存先

とかで上手く行くと思います。

一応注意点。(コード読んでもらえればわかるんですが)

  • checkpointにGeneratorとかDiscriminatorとかoptimizerが一緒に保存されてる時はコピペじゃ動きません。for k, v in ckpt.items()の部分をもう1回繰り返したら行けると思います。

  • tmpフォルダを作る&消すという操作をしてるので、もともとtmpフォルダがある場合は適当なフォルダ名に書き換えてください。

パワープレイですが、まあ誰かの役に立てば…。

大人数の顔認識ができるシステムを作りたい

大人数の顔認識ができるシステムを作りたい!!!!!!!!!!!

と思ってこんな感じのシステムを作りました。djangoを使って実装しています。現時点では150人ほどの識別ができるはずです。 f:id:kym384:20200912180339j:plain

始まり編

ネットにあふれている「顔認識してみた」系の記事は、ほとんどが数人しか認識できないものだと思います。自作の顔認識モデルを学習させるときは最後にSoftmaxをつけるのが一般的だと思いますが、それによって認識できる人物が固定されてしまいます。後から認識させたい人物を増やしたときには再学習させないといけない…。転移学習などで早く終わるとしてもできる限り時間はかけたくありません。

そんな思いを抱きながら生きていたある日。FaceShifterという顔変換モデルを知ります。 ai-scholar.tech こちらは損失関数にIdentity lossを導入しています。つまり「モデルが生成した顔」は「交換する顔」と同一人物か?ということを表すための損失ですね。具体的にはArcfaceというモデルを通して顔画像から512次元ベクトルを生成し、2つのベクトルについてコサイン類似度をとります。Arcfaceとはなんじゃらほいということで以下のQiitaの記事を読みます。

qiita.com

この記事から引用

このペナルティにより、モデルは頑張って特徴ベクトルxのクラス内分散を小さくし、クラス間分散を大きくして、このマージンを稼ごうと頑張るわけです。これがSphereFace, CosFace, ArcFace系の手法の本質になります。

特徴ベクトルのクラス間分散を大きくする…。つまり特徴ベクトルを使ってもある程度は識別できそう。しかもFaceShifterでも使われているように、学習データにあまり依存しなさそう。(正しいのかはあんまりわかってない)

どっかからArcfaceの学習済みモデルを手に入れることができれば、1人の人物に対して数枚の画像を持ってきてArcfaceに通して512次元ベクトルにして、入力された画像に対してそれぞれの人物とのコサイン類似度をとれば、再学習もすることなしに大人数の識別ができるようになるのでは????と思って調べ始めました。

顔検出編

実際に呼び出してみるとこんな感じ。

この画像を入力すると… f:id:kym384:20200912173946j:plain

この画像が返ってくる。

f:id:kym384:20200912173939p:plain

ちゃんと真っすぐになってますし、よさげ。

顔認識してみる編

とりあえず適当に1人当たり2~3枚参照画像を集めます。ぱっと思い浮かんだのでJuice=Juice初期5人の画像にすることにしました。それから、先ほどのGithubのページからArcfaceの学習済みモデルをダウンロードして、集めた画像たちをモデルに通します。モデルの出力である512次元ベクトルをnpyファイルで保存しておきます。各人物に番号を振っておいて、「1.npy」のファイルには(画像の枚数, 512)というshapeのndarrayが保存される形です。

ようやく準備が終わったので認識してみましょう。最初に保存してあるnpyファイル(今回は5つ)を読み込み、axis=0で平均をとります。また、それぞれ正規化(大きさが1になるようにする)しておきます。これらを縦方向に結合して(5, 512)のshapeで保持しておきます。この行列を適当にWとしておきましょう。

次に入力する画像を用意します。これも同じくdlibで顔検出、補正、Arcfaceに通して512次元ベクトルにします。それでこっちも正規化。(1, 512)の行列xとしておきます。

そして、コサイン類似度をとります。それぞれ正規化をしておいたので、行列の積をとるだけでできます。W@x.T ですね。ちなみに@は np.dot の演算子です。それで得られた(5, 1)の行列に対して、np.argmaxしてやるだけで「最も似ている人」を算出できるはずです。やってみましょう。

ハロプロ公式サイトから金澤さんの画像を持ってきます。

www.helloproject.com

f:id:kym384:20200912182558j:plain

こちらの画像ですね。先ほどの処理を施せば、それぞれの人物についてのコサイン類似度が出て来ます。出力してやると…(ちなみにProgress barはtqdmというモジュール使ってます) f:id:kym384:20200912183121p:plain み、見づらい…。コピーしますね

宮崎由香 0.4567191558461361
宮本佳林 0.21137236629074796
金澤朋子 0.6679130527662338
高木紗友希 0.40638896454727513
植村あかり 0.5210983936574289

はい、ちゃんとコサイン類似度が最大のものが正解と一致していますね。いろいろ試してみましたが、同一人物であってもコサイン類似度は0.6~0.8くらいになります。(参照画像と同じ画像を使えば0.99になりますけど) 閾値の設定が難しいですが、とりあえずコサイン類似度が最大かつ0.5以上の場合に同一人物と判定することにしました。

やり残し編

参照画像を集めるのをもっと簡単にしたいですね。これはdjangoでURLを入力すれば勝手にベクトルにして保存してくれるようにしました。それから、名前を入力するとスクレイピングして自動で参照画像を集めてくれるようにもしました。ここら辺はそのうち書くと思います。

現在でまだ解決してないのは以下の3つ。

  • 認識できる人物もスクレイピング等で自動で集めてほしい
  • 参照画像は何枚が妥当なのか?
  • 参照画像のベクトルを平均しただけで、本当にその人を表すベクトルになってるのか?平均以外にもっと良い作り方はないのか?

後半2つに関しては面倒くさそうなのでやらないかも…。