オッサンはDesktopが好き

自作PCや機械学習、自転車のことを脈絡無く書きます

pix2pixで欠損画像を復元する

 こんにちは. 今回は,GAN(敵対的生成ネットワーク)の一つであるpix2pixで欠損画像を復元してみました.

f:id:changlikesdesktop:20200622195400p:plain:w600
※出典*1

0. pix2pix

 GAN(敵対的生成ネットワーク)は,ディープ・ラーニングの中でも注目度が高い技術の一つです. オリジナル画像とそっくりの偽物を生成しようとするネットワーク(generator)と,画像がgeneratorによる偽物なのか本物なのかを見極めようとするネットワーク(discriminator)を競わせるように学習させます. これにより,オートエンコーダで使っていた*2ラベルが不要となり,教師なし学習を実現できます.

f:id:changlikesdesktop:20200621131646j:plain:w600
出典*3

 今回,基本形であるGANやdcGANをすっ飛ばしてpix2pixを使いました. 理由としては,GANやdcGANで採用されている,ランダムな入力から画像を発生するgenerator*4に不安を感じたからです. 「無から画像を作る」創造者まがいのことをさせるわけで,これはなかなか厳しいかと(笑). 今回は256 pixelサイズのカラー画像を使用しますが,256 × 256 × 3の膨大な解空間の中に湧き出たように答えを出すのは難しいでしょう. 実現するのであれば,膨大な量の学習が必要になると思います.

 これに対して,pix2pixはgeneratorにU-Netを使います. dcGANが思うように動かなくて困っているときに「generatorをU-Netにすれば良いんじゃね?」と閃いた気になったのですが,既に提案されていました. U-Netは,本ブログで紹介している傷検出でも採用しているネットワークです. U-Netの特徴の一つが,EncoderとDecoderの間に連結(concatenate)を設けることです. これにより,元画像の空間的な特性を維持したまま,画像を生成します. 元画像を修正したり,改変する動きができるわけですね. 創造性は若干落ちますが,こちらの方が実用性が高いと考えて採用しました.

f:id:changlikesdesktop:20200621152233p:plain:w600

1. 入力画像

 Google検索で「lizard」を検索して,引っかかったトカゲの画像を集めました. 著作権のことを考えずに集めてしまったので,一部しか紹介しません. トカゲの画像を使ったのは,今回やることがカメレオンの得意技である「擬態」っぽかったからです.

f:id:changlikesdesktop:20200621144801p:plain:w600
出典*5 *6 *7

 画像は800枚弱集まりました. その中の700枚を学習に,150枚をテストに使用しました.

 画像の欠損は,楕円式を使って表現しました. 中心座標などを乱数で発生させ,楕円の内側を黒く塗りつぶしました.

image/resize_image.py

# add blot
blot_arr = np.array(resized)
c1 = random.uniform(50, 205) # center
c2 = random.uniform(50, 205) # center
a = random.uniform(5, 20)
b = random.uniform(5, 20)
theta = random.uniform(0, 180)

2. プログラム

 サンプル*8をベースに構築しました. generator部分は,前から使っていたU-Netのソースに置き換えています. サンプルでもU-Netが組まれていたのですが,Pooling処理が入っていないのが何となく気になったので置き換えています. 逆に,サンプルに在ったDropOutやBatchNormalizationは,今回のデータについては不要の様だったので除外しています. U-Netの構成についてはここでは議論しませんが,結果的に,サンプルのままよりも擬態の性能があがったと思います.

 今回、初めてカラー画像をU-Netで使いました. 最後に出力される画像の次元(下記ソース内のself.channel)を3にするだけで実現できました.

train.py

def build_generator(self):
    """U-Net Generator"""
    inputs = Input(shape=self.img_shape)

    # encoding ##############
    conv1_1 = Conv2D(64, 3, activation="relu", padding="same", kernel_initializer="he_normal", bias_initializer="zeros")(inputs)
    conv1_2 = Conv2D(64, 3, activation="relu", padding="same", kernel_initializer="he_normal", bias_initializer="zeros")(conv1_1)
    pool1 = MaxPooling2D(pool_size=(2, 2))(conv1_2)

    conv2_1 = Conv2D(128, 3, activation="relu", padding="same", kernel_initializer="he_normal", bias_initializer="zeros")(pool1)
    conv2_2 = Conv2D(128, 3, activation="relu", padding="same", kernel_initializer="he_normal", bias_initializer="zeros")(conv2_1)
    pool2 = MaxPooling2D(pool_size=(2, 2))(conv2_2)

    conv3_1 = Conv2D(256, 3, activation="relu", padding="same", kernel_initializer="he_normal", bias_initializer="zeros")(pool2)
    conv3_2 = Conv2D(256, 3, activation="relu", padding="same", kernel_initializer="he_normal", bias_initializer="zeros")(conv3_1)
    pool3 = MaxPooling2D(pool_size=(2, 2))(conv3_2)

    conv4_1 = Conv2D(512, 3, activation="relu", padding="same", kernel_initializer="he_normal", bias_initializer="zeros")(pool3)
    conv4_2 = Conv2D(512, 3, activation="relu", padding="same", kernel_initializer="he_normal", bias_initializer="zeros")(conv4_1)
    #drop4 = Dropout(0.5)(conv4_2)
    pool4 = MaxPooling2D(pool_size=(2, 2))(conv4_2)

    conv5_1 = Conv2D(1024, 3, activation="relu", padding="same", kernel_initializer="he_normal", bias_initializer="zeros")(pool4)
    conv5_2 = Conv2D(1024, 3, activation="relu", padding="same", kernel_initializer="he_normal", bias_initializer="zeros")(conv5_1)
    #drop5 = Dropout(0.5)(conv5_2)
    conv_up5 = Conv2D(512, 3, activation="relu", padding="same", kernel_initializer="he_normal", bias_initializer="zeros")(UpSampling2D(size=(2, 2))(conv5_2))
    concated5 = concatenate([conv4_2, conv_up5], axis=3)

    # decoding ##############
    conv6_1 = Conv2D(512, 3, activation="relu", padding="same", kernel_initializer="he_normal", bias_initializer="zeros")(concated5)
    conv6_2 = Conv2D(512, 3, activation="relu", padding="same", kernel_initializer="he_normal", bias_initializer="zeros")(conv6_1)
    conv_up6 = Conv2D(256, 3, activation="relu", padding="same", kernel_initializer="he_normal", bias_initializer="zeros")(UpSampling2D(size=(2, 2))(conv6_2))
    concated6 = concatenate([conv3_2, conv_up6], axis=3)

    conv7_1 = Conv2D(256, 3, activation="relu", padding="same", kernel_initializer="he_normal", bias_initializer="zeros")(concated6)
    conv7_2 = Conv2D(256, 3, activation="relu", padding="same", kernel_initializer="he_normal", bias_initializer="zeros")(conv7_1)
    conv_up7 = Conv2D(128, 3, activation="relu", padding="same", kernel_initializer="he_normal", bias_initializer="zeros")(UpSampling2D(size=(2, 2))(conv7_2))
    concated7 = concatenate([conv2_2, conv_up7], axis=3)

    conv8_1 = Conv2D(128, 3, activation="relu", padding="same", kernel_initializer="he_normal", bias_initializer="zeros")(concated7)
    conv8_2 = Conv2D(128, 3, activation="relu", padding="same", kernel_initializer="he_normal", bias_initializer="zeros")(conv8_1)
    conv_up8 = Conv2D(64, 3, activation="relu", padding="same", kernel_initializer="he_normal", bias_initializer="zeros")(UpSampling2D(size=(2, 2))(conv8_2))
    concated8 = concatenate([conv1_2, conv_up8], axis=3)

    conv9_1 = Conv2D(64, 3, activation="relu", padding="same", kernel_initializer="he_normal", bias_initializer="zeros")(concated8)
    conv9_2 = Conv2D(64, 3, activation="relu", padding="same", kernel_initializer="he_normal", bias_initializer="zeros")(conv9_1)
    outputs = Conv2D(self.channels, 1, activation="tanh")(conv9_2)

    return Model(input=inputs, output=outputs)

3. 学習結果

 学習結果の一部です. 示しているのはテスト画像を推論した結果なので,generatorは未知の画像を擬態しています.

f:id:changlikesdesktop:20200622184847p:plain:w600 f:id:changlikesdesktop:20200622184917p:plain:w600 f:id:changlikesdesktop:20200622184928p:plain:w600 f:id:changlikesdesktop:20200622190532p:plain:w600
左列: 欠損画像,中列: 復元画像,右列: オリジナル(欠損を付加する前の)画像
出典*9 *10 *11 *12

 かなり綺麗に擬態できています. 基本はボカし処理ですが,遠目から見ると偽物だと判断つかないものもありました. トカゲと背景の間の境界やトカゲの模様を再現するような動きもあるのではないでしょうか.

4. 考察: これ使って良いのか?

 かなり綺麗に画像を復元できてしまい,おこがましくも「使って大丈夫か?」という不安を感じました. 蛇足ですが,その考察を書きます.

この顔みたら110番

 刑事ドラマなどで,監視カメラのピンボケ画像がみるみる鮮明に変化して犯人の顔が見える,というシーンがありますね. あれはファンタジーなんですが,今回の結果を見て,GANを使って近いことができると感じました. ただ,「AIが作った画像が自分に似ている」と言われて逮捕されたら,溜まったものでは無いですよね. 実際,GANが生成した画像に証拠能力を持たせるのはNGだと思います.

 現実問題としては,GANはあくまで学習に使った画像に似せて画像を作るだけなので,未知の人間の顔を写すことはできません. ピンぼけ画像を鮮明にすることは出来ても,「なんとなくの人相が見えるようになる」程度だと思います. ただ,目撃者の証言を頼りに書かれる「似顔絵」よりも精度が高いかも知れません. 証拠能力は無くても,「この顔みたら110番」位には使えるかもということです.

 「この顔みたら100番」に顔が似ていることで,不快な思いをされている方がいらっしゃると思います. 同じような被害にあう方がAIの導入によって増えるかも知れません. 大切なのは,「高々似顔絵なんだから他人に似てしまうこともあるよな」と同じように「AIが作る画像なんて曖昧に決まっているよな」という理解が世の中に浸透することです.

AIアート

 GANの技術を使えば,誰しもがプロの写真家のように写真を撮れるようになるでしょう. 現在のスマホやデジカメにもそんな機能がありますが,例えば「写真家の〇〇さん風の写真が撮れる」アプリとかが実現できてしまいます. それによって,写真家の〇〇さんは廃業してしまうのでしょうか...?

 僕はアーティストではないので,身近な話題に変えます. 僕の書いたプログラムを学習して似たようなプログラムを書いてくれるAIができたら,僕は仕事を失うのでしょうか. 個人的にはそうは思いません. AIが書くのは,過去に書いたソースを模倣したプログラムです. 所謂,「横展開」の仕事でしょう. 僕は研究出身なので,そうした「横展開」の仕事が嫌い(というか時間の無駄だと思って積極的にやらない)で上司によく怒られます. 嫌いな横展開を変わりにやってくれるAIがいたら,大歓迎です. そのぶん,研究に時間を費やして新しいアルゴリズムを書けますよね.

 アーティストの方も同じだと思います. 思い込みかも知れませんが,アーティストは「この構図は〇〇さんならではだね」といった確立した評価を自らで壊していくからアーティストなのではないしょうか. そもそも,AIが真似できるアートやプログラムなら,人間にも真似できるでしょう. 時代を生き抜くアーティストの方は,贋作や模倣品,そして過去の自分と日々対決し,自らを更新し続けているのだと思います.

結論

  • AI技術はその特性を理解した上で積極的に使うべき
  • AIに仕事を奪われると思っている方は,その前に人間に仕事を奪われる

5. 結び

 次回は,考察で登場していたピンぼけ画像の復元をやってみるつもりです.

 今回書いたソースはここです*13