#1.はじめに
皆さん、Tensoleflow-hub の Progressive GAN をご存知ですか? CelebAという20万枚以上の有名人の顔画像を学習したGANで、512次元のベクトルを入力すると様々な顔画像を生成します。言い換えれば、512次元の潜在空間に様々な顔が分布しているモデルです。
今回のテーマは、その**「GANの潜在空間に、新垣結衣は住んでいるのか」という問いかけです。もちろん、学習データセットにガッキーは含まれていませんが、潜在空間には20万枚以上の顔画像とその合成画像が無数に存在するわけで、上手い具合にガッキーが存在する可能性も大いにある**と思います。
今回、コードは Google Colab で作成し Github に上げてありますので、自分でやってみたい方は、この「リンク」をクリックし表示されたシートの先頭にある**「Colab on Web」**ボタンをクリックすると動かせます。
#2.GANのおさらい
これは GAN の模式図です。Generator はベクトルを入力として、Discriminator に本物画像と間違わせるような偽物画像を作成することを学習します。
一方、Discriminator は本物画像と偽物画像を間違えないように学習します。この2つのネットワークが切磋琢磨することで高精度な画像生成ができる様になります。
例えて言えば、Generatorを偽札の偽造者、Discriminatorを警察とすると、偽造者は警察を騙せるような偽札を作れるように、警察はどんな偽札でも見分けられるように、お互い競い合うことで、最終的に偽造者が高精度の偽札を作れる様になるということです。
学習完了後は、ベクトルをGeneratorに入力すると高精度な偽物画像を生成するモデルが完成します。同じベクトルを入力すれば、必ず同じ画像を生成します。
今回、使うTensoleflow-hub の Progressive GAN の仕様は以下のようです。
■訓練データ:CelebA 顔画像 202,599枚
■訓練内容 :バッチサイズ16、ステップ数636,801(約50 epoch)
■生成画像 :カラー128×128ピクセル
#3.GANを動かしてみる
最初に、ライブラリーのインストール及びインポートと関数の定義を行います。そして、学習済みモデルをダウンロードします。詳細については、google colabのコードを参照下さい。
まず、学習済みモデルで顔画像を生成してみましょう。
tf.random.set_seed(80) # 乱数をシード80で初期化
vector = tf.random.normal([100, latent_dim]) # ランダムベクトルを100個生成
images = progan(vector)['default'] # 学習済みモデルにランダムベクトルを入力し100個画像を生成
# 100個の画像を10×10で表示
r, c = 10, 10
fig, axs = plt.subplots(r, c, figsize=(14,14))
cnt = 0
for i in range(r):
for j in range(c):
axs[i,j].imshow(images[cnt])
axs[i,j].axis('off')
cnt += 1
plt.show()
plt.close()
これは、100個のランダムベクトルからなるvectorをモデルに入力して生成した画像です。**左上がvector[0], 右上がvector[9], 左下がvector[90], 右下がvector[99]**に該当します。ランダムベクトルを使っているので、割ときれいな画像もありますが、明らかに破綻した画像も含まれています。
tf.random.normal([100, latent_dim])
は、平均0、分散1の正規分布のlatent_dim(=512)次元ベクトルをランダムに100個発生させます。
この時、tf.random.set_seed(80)seed
という様にseedを設定しておくと、同じseedなら毎回同じベクトルを同じ順序で発生させることが出来、再現性が確保できます。なので、seedの数字を変更(整数ならいくつでもOK)すると、異なる画像を生成します。
#4.ベクトル演算
次に、ベクトルの演算をしてみましょう。
笑っている女性4人のベクトルの平均 (smile)から笑っていない女性4人のベクトルの平均 (non_smile)を引いてスマイル成分 (smile_vector)を取り出し、ある画像に足してみます。
smile = (vector[29] + vector[53] + vector[63] + vector[99])/4
non_smile = (vector[8] + vector[17] + vector[20] + vector[79])/4
smile_vector = smile - non_smile # スマイル成分の抽出
calc_vector = vector[34] + smile_vector # vector[34]にスマイル成分を足す
image_before = display_image(progan(vector[34])['default'][0])
image_after = display_image(progan(calc_vector)['default'][0])
display_image(np.concatenate([image_before, image_after], axis=1))
左の画像 (vector[34])にスマイル成分 (smile_vector)を足した結果が右の画像です。純粋にスマイル成分だけを抽出することは難しく他の要素も若干加わったせいか、ちょっと顔の形が変わってしまいましたが、確かに笑っています。今度は、サングラス成分でやってみましょう。
サングラスを掛けた男性のベクトルからサングラスを掛けていない男性のベクトルを引いて、サングラス成分 (glass_vector)を取り出し、ある画像に足してみます。
glass_vector = vector[38] - vector[86] # サングラス成分
calc_vector = vector[24] + glass_vector # vector[24]にサングラス成分を足す
image_before = display_image(progan(vector[24])['default'][0])
image_after = display_image(progan(calc_vector)['default'][0])
display_image(np.concatenate([image_before, image_after], axis=1))
左の画像 (vector[24])にサングラス成分 (glass_vector)を足した結果が右の画像です。これはまあ上手く行ってます。
この演算は限られた画像にのみ有効です。また、抽出した成分は四角い黒のサングラスですが、演算結果は丸型のブラウンのサングラスです。このことから単純に画像の差分をとって画像処理をしているのではなく、ちゃんとベクトル操作をしていることが分かります。
つまり、左の画像のベクトルを起点にサングラス成分のベクトルを足した潜在空間に、似た人物がサングラスを掛けた画像(学習画像あるいは合成画像)がある場合にのみ有効なのです。
#5.アニメーション
さて、アニメーションをしてみましょう。
GANから生成される画像はベクトル間で比較的連続しています。従って、あるベクトルからあるベクトルへ変化させる時、ベクトル間を補完(各成分の変化を等分して少しづつ変化させる)して画像生成すると連続したアニメーションになります。
vectors_list = [vector[29], vector[71], vector[96], vector[97], vector[53], vector[3], vector[29]] # ベクトルの指定
anime_images = anime(vectors_list) # 指定されたベクトル間を補完し、画像を生成
animate(anime_images)
ベクトル間の画像がスムーズに連続していることが分かると思います。
#6.潜在空間に新垣結衣は住んでいるか
それでは、潜在空間にガッキーの顔画像を生成するベクトルがあるか探索してみます。
探索アルゴリズムは、適当なベクトルから生成した初期画像 (initial image) とターゲット画像 (target image) の平均絶対値誤差 (mean absolute error) をロスとし、ロスを最小化するように確率的勾配降下法(手法はAdam)でベクトルを最適化するというものです。
まず、初期画像を設定します。
initial_vector = vector[53]
display_image(progan(initial_vector)['default'][0])
初期画像は、vector[53]を使用しています。続いて、ターゲット画像の設定です。下記のコードを実行すると、PCに保存されている画像を選んで Google colab へアップロード出来ます。
try:
from google.colab import files
except ImportError:
pass
uploaded = files.upload()
image = imageio.imread(uploaded[list(uploaded.keys())[0]])
target_image = transform.resize(image, [128, 128])
display_image(target_image)
ターゲット画像は、この画像をPCからアップロードしました。皆さんは、サンプル画像(Sample/target_pic/0026.jpg)をお使いください。
自分でターゲット画像を作成する場合は、OpenCVで顔画像を切り出したものを使って下さい。でないと、顔の位置関係がズレて上手く最適化が出来ません。OpenCVが分からない方は、このブログの「顔を自動的に切り抜く」を参照下さい。
それでは、次のコードを実行し、このターゲット画像を生成するベクトルを探索します。
# ターゲット画像を生成するベクトルの探索
images, loss, target_vector = find_closest_latent_vector(initial_vector, target_image)
# ロス推移表示
plt.plot(loss)
plt.ylim([0,max(plt.ylim())])
plt.show()
print()
print('loss = ', loss[-1])
# ターゲット画像とターゲットベクトルから生成した画像の表示
display_image(np.concatenate([target_image, images[-1]], axis=1))
うーん、全然ダメですよね。なんとなくは似ているんだけれど、ガッキーらしさがかなり失われています。
#7.見つけたベクトルの周辺を見てみる
確率的勾配降下法は、あくまでもbetterな最適化点を探すもので、bestな最適化点ではありません。そこで、target_vectorの周辺をランダムに見てみます。
target = target_vector + vector/10
images = progan(target)['default']
r, c = 8, 8
fig, axs = plt.subplots(r, c, figsize=(15,15))
cnt = 0
for i in range(r):
for j in range(c):
axs[i,j].imshow(images[cnt])
axs[i,j].axis('off')
cnt += 1
plt.show()
plt.close()
target_vector に最初に生成した vectorの1/10を加えた画像がこれです。うーん、ガッキーはいませんねー。
ちなみに、ガッキーの色々な画像で試した結果からいくつかご紹介すると、
学習したデータセットの多くは正面を向いて頬に手を当てたりしていないので、横を向いたり頬に手を当てたりすると画像生成が上手く行きません。それを除くとまあまあなのですが、何か要素が欠落してしまい今いちなんですよねー。
#8.アニメーションその2
せっかくですのでガッキー以外も試した結果をまとめてアニメーションにしてみます。
ガッキーの他に、松嶋菜々子と河北麻友子を入れてみました。松嶋菜々子は、結構いけてる感じです。
#9.まとめ
結論は、**「GANの潜在空間にはガッキーは住んでおらず、ガッキー似の人しかいませんでした」**です。
やはり、学習データにガッキーの画像が入っていないと難しいようです。今度機会があれば、celebAのデータセットにガッキーの顔画像を加えてGANに学習させてみたいと思います。
(参考データ)
Generate Artificial Faces with CelebA Progressive GAN Model