So-net無料ブログ作成

最高の超解像AIを実験、公開。 [超解像プログラム]

2018年12月27日現在、最高峰と思われる超解像AI プログラムが、 " SRGAN " (及びその派生型)です。
オリジナルのSRGANは2016年9月に論文が発表されたものです。

SRGANは " Super-Resolution Generative Adversarial Networks " の事で、GANを利用した超解像を学習させます。
写真や絵などの入力画像を縦横4倍、画素数にして16倍まで非常に綺麗に拡大出来ます。
昔よく使われていた " Bicubic補間法 " などでは復元出来なかった細部の情報を説得力のある形で推定、復元出来ます。

通常のGANではGenerator(生成器)とDiscriminator(識別器)をそれぞれ学習させます。
Generatorは、より本物に近い画像を生成出来るように、ネットワークのパラメーターを更新して行きます。
Discriminatorは、入力画像が本物の画像なのか、それともGeneratorが生成した画像なのかをより正確に弁別出来るように、ネットワークのパラメーターを更新して行きます。
パラメーターの更新は誤差逆伝播法(backpropagation)に基づいて行われます。

私はGitHubにて公開されている " srgan " をfork及びcloneして変更を施しました。
この " srgan " はTensorFlowTensorLayerによる実装となっています。
以下に私がforkして改造したSRGANのGitHub リポジトリーへのリンクを載せます。


[GitHub リポジトリー]
私が改造した " SRGAN " の "GitHub" リポジトリーのURL:
https://github.com/ImpactCrater/srgan


[変更点1: Group Normalization]
元のSRGANではBatch Normalization層が使われていましたが、Batch Normalizationは各チャンネル間の関連性が無視されてしまいます。
すると複数のチャンネル間の関連性による特徴が損なわれてしまいます。
また、バッチ サイズが小さい場合には正規化が巧く作用しないという弱点があります。
結果として、出力画像に白色の靄のようなアーティファクトが出現してしまうという問題がありました。
これを避ける為、Generator及びDiscriminatorに於いて、全てのBatch Normalization層を除去し、Residual Unitの直前に単一のGroup Normalization層を導入致しました。
Group Normalizationでは、バッチ単位ではなく、チャンネルを幾つかのグループに分割し、チャンネルのグループ毎に正規化します。
これにより、バッチ数が少なくても正規化が効きます。
また、各チャンネル間の関連性も保たれます。
因みに全チャンネルに跨って正規化するLayer Normalizationの場合は、余り役立たないにも関わらず大きな値を示すチャンネルがあった際に、他の全てのチャンネルの値を圧縮されてしまいます。

参照:
"ニューラルネットの新しい正規化手法 Group Normalization の高速な実装と学習実験 | ALBERT Official Blog" のURL:
https://blog.albert2005.co.jp/2018/09/05/group_normalization/


[変更点2: Swish活性化関数]
活性化関数ReLU (Rectified Linear Unit | ramp function)から " Swish " に変更致しました。

ReLU(x)=max(0, x)

Swish(x)=x*Sigmoid(a*x)
Sigmoid(x)=1/(1+e^(-a*x))

TensorFlowでは tf.nn.swish(x) が使えます。

Swish関数及びその導関数は、ReLUと異なり、原点に於いて連続で、非単調性の関数です。
また、Swish関数はReLUと異なり、負の値も出力されます。
Sigmoid関数はLogistic関数の特殊な場合です。


[変更点3: ネットワーク変更]
Generatorのチャンネル数を増加させました。
おそらく大きなチャンネル数はGroup Normalizationと相性が良いのではないかと考えます。
計算時間が増えてしまいましたが、表現力が増強されます。
因みに、Residual Blocksの数を増やした場合と比べて、チャンネル数を増やしてWideなネットワークにした場合の方が学習が収束するまでに要するEpoch数が少なく、より速く収束するようです。

また、Discriminatorのネットワーク構成を変更しました。
オリジナルのSRGANではDiscriminatorのパラメーター数が膨大な数に設定されていました。
改造SRGANではRelativistic Standard GANですのでDiscriminatorは遠慮無くGeneratorよりも先に学習を進めてしまって構いません。
ですので、Discriminatorのパラメーター数をGeneratorと同程度の規模に抑え、また、Residual Blocksを多く採用しました。
この変更により、Discriminatorが速く強くなり、Generatorの学習を牽引してくれる事を期待しております。



[変更点4: Relativisitic GAN]
Relativisitic GANという方式を採用致しました。
Relativisitic GANでは、Discriminator本物の高解像度画像(real)データがGeneratorの生成画像(fake)データよりも本物らしい確率が1に近付くように学習します。
Generatorは逆に、自身の生成画像(fake)データが本物の高解像度画像(real)データよりも本物らしい確率が1に近付くように学習します。
最小化問題にする為に、確率の出力値の自然対数を取ります。
log(1)=0 が目標となります。
Relativistic GANは他の方法よりも安定した学習が可能で結果も高品質であるとの事です。
SRGAN_Loss Function_(2019_01_16)_1_cropped_1 "Relativistic Standard GAN" の2つの損失関数の曲線のグラフが描かれている。 2つの曲線はy軸について左右対称である。
https://c1.staticflickr.com/5/4830/31813237887_8b38ede2ce_o.png
Relativisitic Standard GANでは、Discriminator及びGeneratorのAdam optimizerがこの損失関数の値を最小化しようと切磋琢磨します。

d_loss
= -ln(1/1+e^(-(d(real)-d(fake))))
= ln(1+e^(d(fake)-d(real)))
= softplus(d(fake)-d(real))

g_gan_loss
= -ln(1/1+e^(-(d(fake)-d(real))))
= ln(1+e^(d(real)-d(fake)))
= softplus(d(real)-d(fake))

となっております。
損失値はElement-wise (要素毎)に計算された後、最後に平均値が算出されます。
Softplus関数の導関数は標準シグモイド関数ですので、Softplus関数の x = 0 に於ける接線の傾きは0.5です。

SRGAN_SS_(2019_02_09)_1_edited_1 "Relativistic Standard GAN" の損失値の折れ線グラフが上下2段に分けられて描かれている。
https://c2.staticflickr.com/8/7909/40069121803_b0fb91c04f_o.png
Relaticistic Standard SRGANの学習の様子。
損失値が0.7周辺で上下動しながら安定して学習が進んでいます。
損失関数の2つのグラフの交点は d(real)-d(fake) = 0 で ln(2) ≈ 0.693147181 ですので、良い状態と言えます。

参照:
論文アーカイヴ ウェブサイトの "Relativistic Discriminator" に関するページのURL:
https://arxiv.org/abs/1807.00734


[変更点5: 画像の読み込み]
学習させる画像群について、指定したディレクトリー以下にあるサブディレクトリーまで再帰的に検索して読み込むようにしました。
画像はepoch毎にランダムにシャッフルされ、ランダムな位置でクロップされます。
全画像ファイル数がミニバッチの数で割り切れない場合には、不足分は既存の学習用画像の中からランダムに再度読み込まれます。
また、学習中にも画像を次々に追加出来るようにしました。
指定したディレクトリー以下に、画像ファイルもしくは画像ファイルが入ったフォルダーを放り込むだけで、次のepoch開始時に読み込まれます。



[変更点6: VGG Lossの省略]
SRGANは第一段階ではGeneratorのみをMSE Lossを用いて学習させます。
MSEはMean Square Error(誤差二乗平均)です。
元のSRGANは第二段階ではDiscriminatorによるGAN LossMSE Loss、それとVGG Lossを用います。
VGG Lossは昔のVGG16またはVGG19のネットワークの学習済みモデルを特徴抽出器として利用して算出した、特徴空間でのLossです。
MSE Loss及びVGG Lossは学習の収束を速くするのに貢献します。
ですが、メモリーの使用量と計算時間を減らす為に、改造SRGANではVGG Lossを省略致しました。



[更なる画質向上について]
JPEGなどの圧縮ノイズ低減の学習は行わせていません。
もし圧縮ノイズなどの画質劣化を除去させたい場合は、OpenCVを追加し、 " main.py " を編集して、学習用画像を縮小する際にランダムにJPEG圧縮させるコードを追加してみて下さい。
以下はJPEG圧縮するコードの例です。
encode_parameters = [int(cv2.IMWRITE_JPEG_QUALITY), quality]
result, encoded_image = cv2.imencode('.jpg', image, encode_parameters)

" quality " には圧縮の品質を1から100で入力します。数値が高いほど低圧縮、高品質、数値が低いほど高圧縮、低品質です。
" result " は成功の真偽値です。
これにより圧縮前後の対応関係を学習出来るようになる筈です。
加えて、ガウス暈しもランダムで加えると、シャープネスを上げる事も出来るでしょう。


[プログラムの実行について]
このプログラムで学習を実行するには12GB以上のメモリーが必要です。
CPUまたはNVIDIAのGPUによるCUDAで実行出来ます。
4,000枚の高解像度画像を用いて低目な性能のCPUで実行する場合、第1段階のGeneratorの学習処理に24時間以上、第2段階のGANの学習処理に1週間以上の時間が掛かります。
高性能なCPUであれば1/4以下、高性能なGPUによるCUDAでしたら、おそらく1/100くらいの時間で完了します。


因みに私の環境ではヴィデオ カードのVRAMの容量が足りないのでCPUだけで学習させます。
TensorFlowのバイナリーはSSE4.1, SSE4.2, AVXが利用できないので、TensorFlowをCPU拡張命令のSSE4.1, SSE4.2, AVXを使用するオプション付きでビルドしました。
" -march=native " の " --config=opt " なので自動的に拡張命令対応となります。


私の実行環境は、以下の通りです。

・CPU: Intel Core i7-3770T
・Memory: DDR3 SDRAM 8GB * 4 = 32[GB]
・Video Card: Palit GeForce GTX 750 KalmX (Fanless)
・HDD: 3[TB], 7200[rpm], Cache 64[MB]
・OS: Ubuntu 16.04 LTS

・CUDA 9.0.176
・Python 3.5
・OpenJDK 1.8.0_191
・Bazel 0.18.0
・TensorFlow 1.12.0-rc0 (build from source)
・TensorLayer 1.11.1
・easydict
・tkinter



[Wasserstein GAN]
以下は余談となりますが、私は実験で、Sigmoid cross entropyを用いた損失関数ではなく、Wasserstein距離を用いた損失関数を利用する、 " WGAN " (Wasserstein GAN)の方式も試しました。
WGAN方式では真贋判定のDiscriminatorではないので、 " Critic " (批評家、評論家という意味)と呼ぶようです。
WGANでは、Criticはデータ分布とGeneratorの出力分布間のWasserstein距離をより正確に近似するように学習し、Generatorは2つの分布間のWasserstein距離をより小さくするように学習します。

より具体的には、Generatorは自身が生成した画像(fake)をCriticに見せた際のCriticの出力値(logits_fake)を最大化しようとします。
Criticは本物の高解像度画像(real)を見た時の出力値(logits_real)とGeneratorの生成画像(fake)を見た時の出力値(logits_fake)の差(Wasserstein距離)を最大化しようとします。
これを最小化問題にする為に両方の式の符号を反転します。
以下はTensorFlowでのWGANの例です。
generator_loss = - tf.reduce_mean( logits_fake )
critic_loss =  - (tf.reduce_mean( logits_real ) - tf.reduce_mean( logits_fake ))

generator_optimizer = tf.train.RMSPropOptimizer( learning_rate ).minimize( generator_loss, generator_variables )
critic_optimizer = tf.train.RMSPropOptimizer( learning_rate ).minimize( critic_loss, critic_variables )

上記は実際のコードと同一ではありませんが、概ねこの様な形となります。

" SRGAN_Wasserstein " では、Discriminatorの出力層のシグモイド活性化関数が除去されています。
WGANではAdam optimizerだと学習が不安定になるのでRMSProp optimizerが代わりに使用されます。


SRGANの学習にWasserstein距離を用いる場合、CriticがLipschitz連続な関数である必要があるそうです。
その為にCriticに対して以下の方法を試しました。

1: 損失関数に重みのL2ノルムを加算するweight decay(荷重減衰)を使用。

2: 勾配の値自体を強制的に小さな制限値で刈り込むgradient clippingを使用。
勾配を計算させた後、勾配の値を+-1.0で刈り込みます。

3: 原始的な方法ですがweight値を強制的に小さな制限値で刈り込むweight clippingを使用。
この方法はweight値が二極化してしまい、Criticの表現力が少し低下する問題があります。

上記のどの方法も、勾配爆発と勾配消失に陥り、うまく学習が進みませんでした。
強いて言えば、Weight Clippingが最も良好でしたが、それでも100時間を超えた頃から損失値が暴れ始め、途中で終了させました。

nice!(1)  コメント(0) 

nice! 1

コメント 0

コメントを書く

お名前:
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。

※ブログオーナーが承認したコメントのみ表示されます。

Facebook コメント