ProgressiveGANの潜在空間に、新垣結衣は住んでいるのか？

#1.はじめに
　皆さん、**Tensoleflow-hub** の **Progressive GAN** をご存知ですか？ [**CelebA**](http://mmlab.ie.cuhk.edu.hk/projects/CelebA.html)という20万枚以上の**有名人の顔画像を学習したGAN**で、512次元のベクトルを入力すると様々な顔画像を生成します。言い換えれば、**512次元の潜在空間に様々な顔が分布**しているモデルです。

　今回のテーマは、その**「GANの潜在空間に、新垣結衣は住んでいるのか」**という問いかけです。もちろん、学習データセットにガッキーは含まれていませんが、潜在空間には**20万枚以上の顔画像とその合成画像が無数に存在**するわけで、上手い具合に**ガッキーが存在する可能性も大いにある**と思います。

　今回、コードは Google Colab で作成し Github に上げてありますので、自分でやってみたい方は、この[**「リンク」**](https://github.com/cedro3/ProgressiveGAN/blob/master/Search_for_Yui.ipynb)をクリックし表示されたシートの先頭にある**「Colab on Web」**ボタンをクリックすると動かせます。

#2.GANのおさらい

![スクリーンショット 2020-06-22 07.25.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/d3af4a91-48de-0431-4aff-bf3ca4c8fe41.png)


　これは **GAN** の模式図です。**Generator** はベクトルを入力として、**Discriminator** に本物画像と間違わせるような偽物画像を作成することを学習します。

　一方、**Discriminator** は本物画像と偽物画像を間違えないように学習します。この２つのネットワークが**切磋琢磨**することで**高精度な画像生成**ができる様になります。

　例えて言えば、Generatorを偽札の偽造者、Discriminatorを警察とすると、偽造者は警察を騙せるような偽札を作れるように、警察はどんな偽札でも見分けられるように、お互い競い合うことで、最終的に偽造者が高精度の偽札を作れる様になるということです。

![スクリーンショット 2020-06-22 07.25.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/e79fa41c-1f72-e0ef-1021-3b919740bcad.png)


　学習完了後は、ベクトルをGeneratorに入力すると高精度な偽物画像を生成するモデルが完成します。同じベクトルを入力すれば、必ず同じ画像を生成します。

今回、使うTensoleflow-hub の Progressive GAN の仕様は以下のようです。
■訓練データ：CelebA 顔画像 202,599枚
■訓練内容　：バッチサイズ16、ステップ数636,801（約50 epoch）
■生成画像　：カラー128×128ピクセル

#3.GANを動かしてみる
　最初に、ライブラリーのインストール及びインポートと関数の定義を行います。そして、学習済みモデルをダウンロードします。詳細については、google colabのコードを参照下さい。

　まず、学習済みモデルで顔画像を生成してみましょう。

```python
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()
```
![スクリーンショット 2020-06-21 16.09.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/362d2349-49f1-d655-3f93-5a128a4f87fc.png)

　これは、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.ベクトル演算
　次に、ベクトルの演算をしてみましょう。

![スクリーンショット 2020-06-21 18.54.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/d0cb6b7b-1b78-bcc0-61b0-cd96b5de5e5a.png)

　**笑っている女性４人のベクトルの平均** (smile)から**笑っていない女性４人のベクトルの平均** (non_smile)を引いて**スマイル成分** (smile_vector)を取り出し、ある画像に足してみます。

```python
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))
```
![スクリーンショット 2020-06-21 18.10.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/aeb198a1-5d31-3818-2ca2-d21dbd19437d.png)
　**左の画像** (vector[34])に**スマイル成分** (smile_vector)を足した結果が**右の画像**です。純粋にスマイル成分だけを抽出することは難しく他の要素も若干加わったせいか、ちょっと顔の形が変わってしまいましたが、確かに笑っています。今度は、サングラス成分でやってみましょう。

![スクリーンショット 2020-06-21 18.57.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/ccfdc298-66e3-e89f-2677-15793ef8e5d2.png)

　**サングラスを掛けた男性のベクトル**から**サングラスを掛けていない男性のベクトル**を引いて、**サングラス成分** (glass_vector)を取り出し、ある画像に足してみます。

```python
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))
```
![スクリーンショット 2020-06-21 18.19.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/6faf8eb3-0d74-0ba3-6821-7d77a0e2253a.png)
　**左の画像** (vector[24])に**サングラス成分** (glass_vector)を足した結果が**右の画像**です。これはまあ上手く行ってます。

　この演算は限られた画像にのみ有効です。また、抽出した成分は四角い黒のサングラスですが、演算結果は丸型のブラウンのサングラスです。このことから単純に画像の差分をとって画像処理をしているのではなく、ちゃんとベクトル操作をしていることが分かります。

　つまり、左の画像のベクトルを起点にサングラス成分のベクトルを足した**潜在空間に、似た人物がサングラスを掛けた画像**（学習画像あるいは合成画像）がある場合にのみ有効なのです。

#5.アニメーション
　さて、アニメーションをしてみましょう。

![スクリーンショット 2020-06-21 19.25.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/d97821d6-4163-3ea8-21b0-7f0335439bde.png)

　GANから生成される画像はベクトル間で比較的連続しています。従って、あるベクトルからあるベクトルへ変化させる時、**ベクトル間を補完**(各成分の変化を等分して少しづつ変化させる)して画像生成すると**連続したアニメーション**になります。

```python
vectors_list = [vector[29], vector[71], vector[96], vector[97], vector[53], vector[3], vector[29]]  # ベクトルの指定 
anime_images = anime(vectors_list)  # 指定されたベクトル間を補完し、画像を生成
animate(anime_images)  
```
![＠animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/74da6e36-c573-1167-9947-f936f05e08d0.gif)
　ベクトル間の画像がスムーズに連続していることが分かると思います。

#6.潜在空間に新垣結衣は住んでいるか
　それでは、潜在空間にガッキーの顔画像を生成するベクトルがあるか探索してみます。

![スクリーンショット 2020-06-22 08.22.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/92909c43-49cb-095d-9e9d-f26feb2cbd9e.png)
　**探索アルゴリズム**は、適当なベクトルから生成した**初期画像** (initial image) と**ターゲット画像** (target image) の**平均絶対値誤差** (mean absolute error) を**ロス**とし、**ロスを最小化**するように**確率的勾配降下法**(手法はAdam)で**ベクトルを最適化**するというものです。

　まず、初期画像を設定します。

```python
initial_vector = vector[53]
display_image(progan(initial_vector)['default'][0])
```
![スクリーンショット 2020-06-22 08.34.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/5e4e4657-e7c2-bac9-976c-76bb5fb6361b.png)
　**初期画像**は、vector[53]を使用しています。続いて、ターゲット画像の設定です。下記のコードを実行すると、PCに保存されている画像を選んで Google colab へアップロード出来ます。

```python
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)
```
![スクリーンショット 2020-06-22 08.39.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/2d5f0a0e-9540-5540-e522-37785a25aa2f.png)
　**ターゲット画像**は、この画像をPCからアップロードしました。皆さんは、サンプル画像(Sample/target_pic/0026.jpg)をお使いください。

　自分でターゲット画像を作成する場合は、**OpenCV**で顔画像を切り出したものを使って下さい。でないと、顔の位置関係がズレて上手く最適化が出来ません。OpenCVが分からない方は、[**このブログ**](http://cedro3.com/ai/mini-dcgan/)の「顔を自動的に切り抜く」を参照下さい。

　それでは、次のコードを実行し、このターゲット画像を生成するベクトルを探索します。

```python
# ターゲット画像を生成するベクトルの探索
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))
```
![スクリーンショット 2020-06-22 08.43.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/cc461ed1-3ab5-891f-a5cf-26d0eef3ef63.png)
　うーん、全然ダメですよね。なんとなくは似ているんだけれど、ガッキーらしさがかなり失われています。

#7.見つけたベクトルの周辺を見てみる
　確率的勾配降下法は、あくまでもbetterな最適化点を探すもので、bestな最適化点ではありません。そこで、target_vectorの周辺をランダムに見てみます。

```python
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()
```
![スクリーンショット 2020-06-22 09.18.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/f8717933-df4d-c448-56cb-d3666293df79.png)
　**target_vector** に最初に生成した **vectorの1/10を加えた**画像がこれです。うーん、ガッキーはいませんねー。

　ちなみに、ガッキーの色々な画像で試した結果からいくつかご紹介すると、
![スクリーンショット 2020-06-22 09.32.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/67967607-1eec-e83b-5e7b-a191496ae371.png)
　学習したデータセットの多くは正面を向いて頬に手を当てたりしていないので、横を向いたり頬に手を当てたりすると画像生成が上手く行きません。それを除くとまあまあなのですが、何か要素が欠落してしまい今いちなんですよねー。

#8.アニメーションその２
　せっかくですのでガッキー以外も試した結果をまとめてアニメーションにしてみます。

![スクリーンショット 2020-06-22 15.13.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/722a4d64-8391-2c38-cedd-396aabf6cc06.png)
![animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/7a9eeedd-9e7d-92f6-d92f-77ce56e1ad10.gif)


　ガッキーの他に、**松嶋菜々子**と**河北麻友子**を入れてみました。松嶋菜々子は、結構いけてる感じです。


#9.まとめ
　結論は、**「GANの潜在空間にはガッキーは住んでおらず、ガッキー似の人しかいませんでした」**です。

　やはり、学習データにガッキーの画像が入っていないと難しいようです。今度機会があれば、celebAのデータセットにガッキーの顔画像を加えてGANに学習させてみたいと思います。

（参考データ）
[Generate Artificial Faces with CelebA Progressive GAN Model](https://www.tensorflow.org/hub/tutorials/tf_hub_generative_image_module)