StyleGAN: A Style-Based Generator Architecture for Generative Adversarial Networks
論文
A Style-Based Generator Architecture for Generative Adversarial Networks(Karras et al., 2019)
いつもインパクト大な高解像度の画像生成手法を発表してくるNVIDIAの論文である.
概要
スタイル変換の文献を参考に,敵対的生成ネットワークにおけるジェネレータの新しいアーキテクチャを提案する. このアーキテクチャは,高レベルな属性(例:人の顔で訓練されたときのポーズ,アイデンティティなど)を教師なしで分離し, 生成画像に確率的な変動(例、そばかす,髪など)を含めることを可能にする. これにより,スケールに応じた直感的な画像生成の制御を可能にした. この新しいジェネレータは,従来の分布の品質の評価指標におけるSOTAを向上させ, 明らかに優れた補間特性をもたらし,また変動の潜在的要因をより良く解きほぐす. 補間の品質と解きほぐれ具合を定量化するために,我々は任意のジェネレータに適用可能な2つの新しい評価指標を提案する. 最後に,我々は非常に多様で高品質な,新しい人の顔のデータセットを紹介する.
今のところ,この潜在空間におけるもつれというのがあんまりイメージできないが,この辺りがキーになるのかな.
実装
完全なソースコードはここにある.フレームワークはTensorFlow. NVIDIA本家の実装が公開されているので,所々参考にしたが,読み解くのが結構辛かった. github.com
Style-Based Generator
Mapping Network
StyleGANにおいては潜在ベクトルはネットワークの入力ではなく,各レイヤーにおいてスタイル制御のために用いられる. 潜在ベクトルはMapping Networkによってスタイル制御のための潜在空間へマッピングされる.() Mapping Networkは論文では8層のMLPとして実装されている.
def mapping_network(latents, labels, reuse=tf.AUTO_REUSE): with tf.variable_scope("mapping_network", reuse=reuse): labels = embedding( inputs=labels, units=latents.shape[1], variance_scale=1, scale_weight=True ) latents = tf.concat([latents, labels], axis=1) latents = pixel_norm(latents) for i in range(self.mapping_layers): with tf.variable_scope("dense_block_{}".format(i)): with tf.variable_scope("dense".format(i)): latents = dense( inputs=latents, units=latents.shape[1], use_bias=True, variance_scale=2, scale_weight=True ) latents = tf.nn.leaky_relu(latents) return latents
Adaptive Instance Normalization
Mapping Networkによって得られたは各畳み込み層の後にアフィン変換されたのち, Adaptive Instance Normalization(AdaIN)(Hung et al., 2017)のパラメータとして用いられる. AdaINは以下の式で与えられる.
ここでは特徴マップ,はアフィン変換された潜在ベクトルである. これはInstance Normalizationによって特徴マップ毎に正規化した後,スタイル変換のためのスケールとバイアスをMLPでモデル化している. これを各畳み込み層の後に行うことで,各スケール毎にスタイルを変化させることができる. ここでいうスケールは特徴の意味的なレベルとも捉えられる.
AdaINによるスタイル制御はスケールに対してローカルである.つまりあるスケールにおけるスタイルの変更は他のスケールのスタイルに影響を及ぼさない. これはAdaIN操作は,その後に続く畳み込みのために,特徴マップ間の相対的な重要性を変更するが,正規化処理により,元の特徴マップの統計には依存しないからである.
def adaptive_instance_norm(inputs, latents, use_bias=True, center=True, scale=True, variance_scale=2, scale_weight=True, epsilon=1e-8): ''' Adaptive Instance Normalization [Arbitrary Style Transfer in Real-time with Adaptive Instance Normalization] (https://arxiv.org/pdf/1703.06868.pdf) ''' # standard instance normalization inputs -= tf.reduce_mean(inputs, axis=[2, 3], keepdims=True) inputs *= tf.rsqrt(tf.reduce_mean(tf.square(inputs), axis=[2, 3], keepdims=True) + epsilon) if scale: with tf.variable_scope("scale"): gamma = dense( inputs=latents, units=inputs.shape[1], use_bias=use_bias, variance_scale=variance_scale, scale_weight=scale_weight ) gamma = tf.reshape( tensor=gamma, shape=[-1, gamma.shape[1], 1, 1] ) inputs *= gamma if center: with tf.variable_scope("center"): beta = dense( inputs=latents, units=inputs.shape[1], use_bias=use_bias, variance_scale=variance_scale, scale_weight=scale_weight ) beta = tf.reshape( tensor=beta, shape=[-1, beta.shape[1], 1, 1] ) inputs += beta return inputs
このAdaIN,cGANs with Projection Discriminator(Miyato et al. 2018),Spectral Normalization(Miyato et al. 2018)以降でよく用いられている Conditional Batch Normalization(de Vries et al., 2017)ともかなり近い. Conditional Batch Normalizationでは正規化後のスケールとバイアスをクラスラベルのembeddingでモデル化する.
Noise Inputs
StyleGANでは生成画像に確率的な要因を含めるために,各レイヤーにおいてノイズを供給する. これは1チャンネルのノイズマップを用意し,各特徴マップに対して個別にスケーリングした後,足し合わせる. このチャンネル数分のスケーリング係数はパラメータとして最適化される.
def apply_noise(inputs): noise = tf.random_normal([tf.shape(inputs)[0], 1, *inputs.shape[2:]]) weight = tf.get_variable( name="weight", shape=[inputs.shape[1]], initializer=tf.initializers.zeros() ) weight = tf.reshape(weight, [1, -1, 1, 1]) inputs += noise * weight return inputs
これにより髪の毛,しわなどの確率的とみなせる多くの特徴をモデル化できる. また各レイヤー毎にノイズを供給するので,特徴のレベルに応じた確率的変動を実現できる. ただあくまでこれは低レベルな特徴を担い,高レベルな特徴はそのまま残る. (ように学習されることが期待できるって感じだと思う.高レベルな特徴までランダム化すると,それはもう人の顔と認識できなそう.)
AdaINによるスタイル制御は特徴マップ単位でスケール,バイアスを適用するので,その効果は画像全体にわたる効果を制御できる. 対してノイズはピクセル単位で供給されるので確率的な変動を制御できる.
Constant input
StyleGANでは潜在ベクトルはもはやネットワークの入力である必要はなく,代わりにあらかじめ最小解像度のマップ用意しておき,常にこれをネットワークの入力とする. このマップはパラメータとして最適化される.
Style Mixing
まずMapping Networkに2つの潜在ベクトルを入力し,を得る. generatorのあるレイヤーまではAdaINのパラメータとしてを用いて,そこから先のレイヤーではAdaINのパラメータとしてを用いる. これはスタイルのスイッチングであり,は高レベルのスタイルを,は低レベルのスタイルを担っていると考えられる. スイッチするレイヤーは毎回ランダムに選ばれる. この正則化によって隣接するスタイルが相関していると,ネットワークが仮定するのを防ぐとあり, なんとなくそんな気がするも,ここがまだ少し飲み込めていない.
下図のように潜在ベクトルをコピーすることはスタイルをコピーすることであり,様々なレベルでスタイルを操作することが可能である.
Low-Pass Filtering
本論文ではさらっと述べられている程度のことだが,少し気になってのでメモしておく. 本家の実装を見るとblur2dとかいう見慣れない関数があったので,論文を見てみるとnearest-neighbor samplingをローパスフィルタを用いたbilinear samplingに置き換えたと書いてある. これはICLR2019に投稿されているMaking Convolutional Networks Shift-Invariant Againで提案されている, ダウンサンプリング前にローパスフィルタをかけておくことで,シフト不変性を保つアプローチらしい. ダウンサンプリングではナイキスト周波数以上の高周波成分がエイリアシングとなって現れるので,ローパスフィルタによって高周波成分を除去することで,シフト不変性を保っている.
音とかではよくあるアプローチだと思うが,画像では聞いたことなかったので面白かった. ただそこまで本質的というわけでもなさそうなのでとりあえず実装は後回しにする.
Zero-Gradient Penalty
StyleGANではWhich Training Methods for GANs do actually Converge?で提案されている Zero-Centered Gradient Penaltyを正則化項として用いている. この論文によるとWGAN(Arjovsky et al., 2017),WGAN-GP(Gulrajani et al., 2017)は必ずしも収束しないらしく, Zero-Centered Gradient Penaltyは常に収束するらしい. ちなみにWGAN-GPはOne-Centered Gradient Penaltyである. 論文では以下で示される2種類のZero-Centered Gradient Penaltyが提案されている.
]
]
これらはそれぞれデータ分布,generator分布に対するgradient penaltyである. StyleGANではlossはNon-Saturating Loss(Goodfellow et al., 2014)とし,-regularizerのみを用いている.
とりあえず今日はここまでにする.一番重要そうな潜在空間におけるもつれについて何も書いてないが, まだあんまり理解できてないので,完全に飲み込めたらまた書こうと思う. 早く実験結果を出したいところだが,この前実装したGANSynthに計算資源を食われているので, 今までめんどくさそうで避けていたGoogle Colaboratoryを使ってみた.このノートブックとかいう概念があんまり好きになれない...
GANSynth: Adversarial Neural Audio Synthesis
論文
GANSynth: Adversarial Neural Audio Synthesis(Engel et al., 2019)
ICLR 2019に投稿されている.
概要
人間の知覚はグローバルな構造と細かい波形のコヒーレンスの両方に敏感であることから, 効率的な音声合成は本質的に難しい機械学習タスクである. WaveNetのような自己回帰モデルは局所構造をモデル化するが,グローバルな潜在的構造をモデル化できず, また反復サンプリングによる速度面でのパフォーマンス低下がある. これとは対照的に,GANはグローバルな潜在的条件付けおよび効率的な並列サンプリングが可能であるが, ローカルにコヒーレントな音声波形を生成するのが難しい. 我々はスペクトル領域において,十分な周波数分解能のもとで,対数振幅と瞬時周波数をモデル化することにより, GANがローカルにコヒーレントな音声を生成できることを実証した. また提案手法はNSynth Datasetを用いた実験によってWaveNetより優れた結果を示した. さらに提案手法はWaveNetに比べ桁違いに高速に音声を生成する.
音に関してはさっぱりであり,コヒーレンスとは?という感じ.でもいろいろ調べてなんとなく掴んだ.
貢献
- GANを用いて対数振幅と位相のスペクトログラムを生成することにより,従来の自己回帰モデルよりもコヒーレントな波形を生成した.
- 位相を直接推定するのではなく, 瞬時周波数を推定することで,よりコヒーレントな波形を生成した.
- 倍音が重ならないことが重要であるが,低周波の倍音は非常に狭い範囲に存在しており,倍音が重複しやすい. 大きなSTFTフレームサイズとメル周波数スケールは,低周波の倍音をより分離することができる.
- NSynthデータセットにおいてWaveNetより優れた結果を示し,またWaveNetより54,000倍高速に波形を生成する.
- 潜在空間およびピッチ空間におけるグローバルな条件付けにより、GANは知覚的に滑らかな音色の補間、およびピッチ全体で一貫した同一性を持つ音色を生成できる.
実装
完全なソースコードはここにある.時々修正するかもしれないけど.
Progressive Growing of GANs(Karras et al., 2018)
Progressive Growing of GANs(Karras et al., 2018)
GANSynthはPGGANをベースに画像の代わりにスペクトログラムを用いる形で音声を生成する.
PGGANは以前にも実装したが,効率的な計算グラフを構築しようと思うと意外とややこしい.
ここでもう一度実装を見直しつつ,ハマったポイントを書いておく.フレームワークはTensorFlow.
Progressive Training
Progressive Trainingとは生成,認識する画像の解像度を徐々に上げながら学習することによって, まずはグローバルな構造を学習し,徐々にローカルな構造を学習していくというもの. これはまた学習の安定性と速度にも貢献する.
このネットワーク構造をどのような効率的な形で実装するかを考える. 効率的と書いたのは,計算効率を無視すれば単にforループを回せば簡単に実装できるが, TensorFlowにおいて,不必要な計算を避けるために,実行されるパスを動的に選択するためにはちょっと工夫がいる.
まず畳み込みや正規化の中身は置いておいて,各畳み込みブロックをどのように繋げるかを考える. 論文にある下図のようなパスを生成すれば良いのだが,これらのパスは全畳み込みブロックに存在する. 方針としては,可能性のあるパスは全て記述しておき,実行時に条件によって分岐させる. ここでいう条件とはProgressive Trainingの中核をなす部分で,どのくらいの解像度の画像を生成,認識したいですか,みたいなもの.
最終的に以下のようなコードを書いた.まずは理解しやすいgeneratorから. コードを抜粋しただけなので,意味不明な変数もあるが,無視してほしい.
conv blockは畳み込み層であり,color blockは1×1 convolutionによって生成画像のチャンネル数まで落とす. growは前のconv blockから特徴マップを受け取って最終的な画像を返す関数である. PGGANでは徐々に解像度を上げていくので,全てのconv blockの後にcolor blockによって画像を出力する可能性がある. 各conv blockは処理を終えた後,さらに次のconv blockに特徴マップを渡してより高解像度の画像を生成してもらうか,まだ渡す時期じゃないと判断して,単純にcolor blockを通した後目的解像度までupscaleするかの2択がある.この分岐を実行時に選択させる. またupscaleする場合でも学習が十分でないconv blockは高解像度の画像は生成できないので,前の層が出力した画像との線形補間を行なって出力する.
ちなみにわざわざ関数にしたり,lambdaでラップしたりしてるが, これはtf.condの外で生成されたoperationは条件にかかわらず実行されてしまうからである.
def grow(feature_maps, depth): ''' depthに対応する層によって特徴マップを画像として生成 Args: feature_maps: 1つ浅い層から受け取った特徴マップ depth: 現在見ている層の深さ(解像度が上がる方向に深くなると定義) Returns: images: 最終的な生成画像 ''' # 現在より深い層によって生成された画像(ここで再帰) def high_resolution_images(): return grow(conv_block(feature_maps, depth), depth + 1) # 現在の層の解像度で生成した画像を最終解像度までupscaleする def middle_resolution_images(): return upscale2d( inputs=color_block(conv_block(feature_maps, depth), depth), factors=resolution(self.max_depth) // resolution(depth) ) # 1つ浅い層の解像度で生成した画像を最終解像度までupscaleする def low_resolution_images(): return upscale2d( inputs=color_block(feature_maps, depth - 1), factors=resolution(self.max_depth) // resolution(depth - 1) ) # 最も浅い層はlow_resolution_imagesは選択肢にない if depth == self.min_depth: images = tf.cond( pred=tf.greater(out_depth, depth), true_fn=high_resolution_images, false_fn=middle_resolution_images ) # 最も深い層はhigh_resolution_imagesは選択肢にない elif depth == self.max_depth: images = tf.cond( pred=tf.greater(out_depth, depth), true_fn=middle_resolution_images, false_fn=lambda: lerp( a=low_resolution_images(), b=middle_resolution_images(), t=depth - out_depth ) ) # それ以外は以下のいずれかを出力する # 1. high_resolution_images # 2. low_resolution_imagesとmiddle_resolution_imagesの線形補間 else: images = tf.cond( pred=tf.greater(out_depth, depth), true_fn=high_resolution_images, false_fn=lambda: lerp( a=low_resolution_images(), b=middle_resolution_images(), t=depth - out_depth ) ) return images
discriminatorも基本的にはgeneratorと同じだが,正直generatorより理解が難しかった. なるべくgeneratorと同じような手続きを踏むように試行錯誤した.
discriminatorにおけるconv blockとcolor blockはgeneratorのそれと逆の操作になる. つまり,conv blockは転置畳み込み,color blockは入力画像のチャンネル数を畳み込みカーネルに合うチャンネル数まで上げる.
def grow(images, depth): ''' depthに対応する層によって画像を特徴マップとして取り込む Args: images: 入力画像(depthに関わらず一定) depth: 現在見ている層の深さ(解像度が上がる方向に深くなると定義) Returns: feature_maps: 1つ浅い層に渡す特徴マップ ''' # 現在より深い層によって取り込まれた特徴マップ(ここで再帰) def high_resolution_feature_maps(): return conv_block(grow(images, depth + 1), depth) # 現在の層の解像度までdownscaleした後,特徴マップとして取り込む def middle_resolution_feature_maps(): return conv_block(color_block(downscale2d( inputs=images, factors=resolution(self.max_depth) // resolution(depth) ), depth), depth) # 1つ浅い層の解像度までdownscaleした後,特徴マップとして取り込む def low_resolution_feature_maps(): return color_block(downscale2d( inputs=images, factors=resolution(self.max_depth) // resolution(depth - 1) ), depth - 1) # 最も浅い層はlow_resolution_feature_mapsは選択肢にない if depth == self.min_depth: feature_maps = tf.cond( pred=tf.greater(in_depth, depth), true_fn=high_resolution_feature_maps, false_fn=middle_resolution_feature_maps ) # 最も深い層はhigh_resolution_feature_mapsは選択肢にない elif depth == self.max_depth: feature_maps = tf.cond( pred=tf.greater(in_depth, depth), true_fn=middle_resolution_feature_maps, false_fn=lambda: lerp( a=low_resolution_feature_maps(), b=middle_resolution_feature_maps(), t=depth - in_depth ) ) # それ以外は以下のいずれかを出力する # 1. high_resolution_feature_maps # 2. low_resolution_feature_mapsとmiddle_resolution_feature_mapsの線形補間 else: feature_maps = tf.cond( pred=tf.greater(in_depth, depth), true_fn=high_resolution_feature_maps, false_fn=lambda: lerp( a=low_resolution_feature_maps(), b=middle_resolution_feature_maps(), t=depth - in_depth ) ) return feature_maps
サラっとコンパクトに書いてるが,このgrowの設計にPGGANの実装のほとんどを費やしている. ちなみにNVIDIA本家の実装も公開されている. 本家の実装にはlinear structureとrecursive structureの2種類が提案されており, linear structureは実装は容易だが,毎回不必要なパスも全て実行するので非効率である. recursive strucrureは今回の実装のように条件分岐を行なっているので,効率的ではあるが,実装は若干込み入っている. 本家のコードもrecursive strucrureはhuman-unreadableとあり,確かに読むの辛かったので,自分なりに整理して書いた.
Minibatch Standard Deviation
Minibatch Discrimination(Salimans et al., 2016)を単純化したもの. この統計量を特徴としてdiscriminatorに渡すことで出力画像の多様性が向上するらしい. Minibatch Standard Deviationは論文では,
- 特徴マップの各ピクセルのバッチ方向の標準偏差を計算 [N, C, H, W] -> [C, H, W]
- チャンネル方向,空間方向で平均をとる [C, H, W] -> [ ]
- バッチ方向,空間方向に複製する [ ] -> [N, 1, H, W]
- 元の特徴マップにconcatする [N, C, H, W] -> [N, C + 1, H, W]
とあるが,本家の実装を見るとバッチをさらにグループとして分割した上で上記の処理を行なっている.このときの実装はこんな感じ.
def batch_stddev(inputs, group_size=4, epsilon=1e-8): shape = inputs.shape.as_list() stddev = tf.reshape(inputs, [group_size, -1, *shape[1:]]) stddev -= tf.reduce_mean(stddev, axis=0, keepdims=True) stddev = tf.square(stddev) stddev = tf.reduce_mean(stddev, axis=0) stddev = tf.sqrt(stddev + epsilon) stddev = tf.reduce_mean(stddev, axis=[1, 2, 3], keepdims=True) stddev = tf.tile(stddev, [group_size, 1, *shape[2:]]) inputs = tf.concat([inputs, stddev], axis=1) return inputs
このMinibatch Standard Deviationは最終層前に入れると良かったらしい.
Equalized Learning Rate
He initialization(He et al., 2015)で用いる分散を,重みの初期化に用いるのではなく,重みのスケーリングに用いる. 重み自体はNormal(0, 1)で初期化する. RMSPropやAdamなどの学習率適応型の確率的勾配降下法では,推定された標準偏差によって勾配を正規化するので, パラメータ更新のステップサイズが勾配のスケールに対して不変性を持つ. これによって大きなダイナミックレンジを持つパラメータは収束までに時間がかかる. 提案手法によって学習速度を全てのパラメータに対して一定に保つ.
提案手法とかよりもまず,従来の学習率適応型の確率的勾配降下法について理解が全然足りていないことに気づいた. 今までなんとなくで済ませてきたが,これを機に少しだけ理解できた.
Pixelwise Feature Vector Normalization
Local Response Normalization(Krizhevsky et al., 2012)をベースに,以下の式によってチャンネル方向に正規化する.
これにより値の発散を防ぐことができるらしい.generatorのconv blockの後に挿入する.
cGANs with Projection Discriminator
GANSynthはACGAN(Odena et al., 2017)スタイルで,ピッチによる条件付けを行っている.
これはgeneratorには潜在ベクトルにクラスラベルのone-hot表現をconcatしたものを入力し,
discriminatorではadversarial lossに加えて,classification lossを考慮する.
今回はACGANではなくcGANs with Projection Discriminator(Miyato et al. 2018)によって条件付けを行なった. これはクラスラベルのembeddingとdiscriminatorの最終層への入力となる特徴ベクトルの内積を取り,discriminatorの最終層の出力に加える.
generatorにおいてはConditional Batch Normalization(de Vries et al., 2017)によって条件付けを行なった. Conditional Batch Normalizationでは正規化後のスケールとバイアスをクラスラベルのembeddingでモデル化する.
Spectral Normalization
GANSynthはLipschitz制約を満たすようにGradient Penalty(Gulrajani et al., 2017)を適用している. 今回はGradient Penaltyの代わりにSpectral Normalization(Miyato et al. 2018)を用いた.
これはネットワークの重み行列をそのspectral normで割ることで正規化を行う. このspectral normは重み行列の最大特異値に等しい. これによりdiscriminatorのLipschitz定数を1に制限する.
GANSynth
やっと本題のGANSynthの話題に入れる.
音に関しては不安が多かったが,Google本家の実装があり,とても参考になった.
この実装を参考にしながら理解を進めていく.
おそらく波形をspectrogramに変換する所がこの論文の肝であり,一旦spectrogramにしてしまえば,あとは画像として扱えば良い.
と言っても,そこまで込み入った処理はなく,意外と書くことなかったので一気に書く.
- 波形に対してSTFTを行なって振幅と位相を得る.
- 振幅と位相を次元圧縮なしでメルスケールに変換する.
- 振幅は対数をとり,位相はアンラップし,有限差分を取ることで,瞬時周波数に変換する.
- 対数振幅と瞬時周波数をそれぞれ[-1, 1]の範囲に正規化し,concatして2チャンネルの画像として扱う.
今回は4の正規化のために前もって全訓練データにおける対数振幅と瞬時周波数の値の範囲を計算しておいた. 分からなかったのは本家の実装にあるメルスケールから線形スケールに変換するmel_to_linear_matrixなる関数. linear_to_mel_matrixの擬似逆行列を求めていると思ったのだが,何をしているのかまだ分かっていない.
def _mel_to_linear_matrix(self): """Get the inverse mel transformation matrix.""" m = self._linear_to_mel_matrix() m_t = np.transpose(m) p = np.matmul(m, m_t) d = [1.0 / x if np.abs(x) > 1.0e-8 else x for x in np.sum(p, axis=0)] return np.matmul(m_t, np.diag(d))
とりあえずこんな感じで疲れたので終わりにする.結果はまた載せたいと思う.
ていうかこんなことしてないで,卒論早く修正して出さなきゃだった...