空飛ぶ気まぐれ雑記帳

主に趣味とかプログラミングについて扱います。

debugpyがいい感じなことに気づいた

なにがあったか

docker-composeで立ち上げたpythonコンテナをデバッグするときに、VSCodeからDockerで立ち上げたコンテナにattachしてデバッグするのが面倒だと思っていた。
普通にフォルダが表示されるまで時間がかかるし、ソースコードを削除しているコンテナの場合、追加でボリュームをマウントしてとかの作業も発生する。
(このあたりも実はdevcontainer.jsonとかに書けば自動でやってくれるのかな)

生産性が低いのでなんとかしたいと思っていて、VSCodeのドキュメントを読み返していた。

解法

以下は簡単なサンプルコード
github.com

やっていることとしては、docker-compose.debug.yamlを>|-f|<オプションに与えることで、既存の設定を置き換えている。
主な変更点は下記

  • entrypointを専用の実行スクリプトで置き換える(debugpyのインストールとdebupyのサーバを実行)
  • 専用の実行スクリプトをDocker volumeでマウント
  • ポート5678番をホストと共有(debugpyの通信用)

Dockerfileでentrypointを定義している場合もdocker-composeから置き換えるので、release用のコードを変更する必要は全くない。

また、Release用のコンテナではコンテナ容量削減のためにインストールに使ったPythonソース等を削除することは(pip installとかでフォルダをまるごとインストールされたパッケージを`python -m module_name`とかで実行するような使い方)
自分調べではよくやられていると思うが、この場合、深層にあるソースコードをわざわざ探しに行く羽目になるが、それははっきり言って大変なので避けたい。
これについてもDocker volumeでソースディレクトリをコンテナ内のカレントディレクトリにマウントしてやれば良い気がする。
>|python -m module_name|<とするとインストールしたパッケージではなくカレントディレクトリのモジュールがインポートされてそのままデバッグされるはずだ。

まとめ

debugpyは神**。

【Python】システムのSSL証明書の設定を読み出す【Windows】

今回はWindowsでOSで設定されているSSL証明書Pythonのライブラリで使用する方法について説明する。

PythonではSSL証明書を通常`certifi`というライブラリに埋め込まれたものを使用する。
そのため、OSに設定されているSSL証明書やそれ以外の証明書を使う方法はライブラリ毎に決められている方法に沿った方法を取る必要がある。
それは面倒なので、世の中にはそういう便利なものを作ってくれている人がいる。

pypi.org

Win32 API経由でシステムのSSL証明書を読み出し、それをPEM形式で保存。さらに、certifiの変数を書き換えることで、証明書の設定を行っている。

Proxyを使っている会社では`requests`を使うだけでも`SSLError`が出る。
意外と調べても出てこなかったので、備忘録として残しておく。

OSSを読んだ記録をつけることにした話

きっかけ

細かい話もスライドに書いているけど、OSSを読めという人は沢山居るけど、それを読んだ記録みたいなのは本当に無いので書いてみた。

speakerdeck.com

感想

読んでいるOSSとネタが噛み合ったら何かしら書けるけど、そうじゃなければ中々書くことも難しいなと思った。
あと、ブログに書くより楽かと思っていたけど、スライドで作るのも十分しんどいね。
何回か続けてみて今後も書くか考える予定。

Tensorflow2.Xの5Stepのネットワーク実装

この記事を書こうと思ったきっかけ

世の中にTensorflow 2.X実装のネットワークが増えてほしいと思ったから。
最近ではTensorflow 2.X実装のネットワークも増えつつあるが、今でもTensorflow 1.X実装のネットワークは少なくない。
PyTorchを使っている人が多いということもあるが、昔から使い続けている、Tensorflow 1.Xを使い続けているという人も少なくないのだろう。
しかしながら、RTX3000シリーズがCUDA10をサポートしないことを追い風にTensorflow 2.Xへの以降が進むことを期待して、Tensorflow 2.Xでネットワークを定義する方法を5Stepに分けて解説する。
たしか公式のドキュメントにもこの5種類の実装方法を全部1ページに解説しているページはなかったはずなので、参考になると嬉しい。
(相当にパフォーマンスが求められない限り、`tf.keras.layers.Layer`を用いた実装は基本的には`tf.keras.Model`で代替可能なので今回は解説していない。)

同じくGoogleがリリースしているJAXが使えそうになりつつある現在、今更Tensorflowかとなる気もするけど既存のリソース上で書く場合は十分使えるので、ぜひともという気がする。

tf.keras.Sequential

最も簡単で最も使う書き方(自分調べ)。

import tensorflow as tf

model = tf.keras.Sequential([
    tf.keras.layers.Dense(100, activation=tf.nn.relu),
    tf.keras.layers.Dense(100, activation=tf.nn.relu),
    tf.keras.layers.Dense(1, activation=tf.nn.relu),
]) # 中間層3層の全結合層のみのネットワーク
print(model(tf.random.uniform(4, 100))) #(4, 1)のTensorが帰る

以上のように引数にlayerのリストを与えることでネットワークの構造を記述する。
学習には`fit`関数, 推論には`predict`関数などを使用することで、細かいTensorflowの仕様をあまり気にせずにscikit learnのインタフェースで使用できる。

基本的に1方通行のネットワーク構造しか記述できないため、Skip connectionは記述できない。
しかしながら、通常の用途であればこれで問題ないことが多い。

tf.keras Model

一番複雑なモデル構築ができる。
このオブジェクトはクラスの規定継承

tf.keras.Modelとtf.keras.layers.Layerの違い

基本的にはtf.keras.Modelよりもtf.keras.layers.Layerはスリムなインタフェースしか持っていない点が大きな違いである。
例えば`fit`, `predict`, `evaluate`などの学習や推論に用いるインタフェースはtf.keras.Modelしか持っていない。
tf.keras.layers.LayerはModel内で多数使用されることが前提とされていて、よりスリムであることが求められるためである。
今回はグラフ構築を動的にすることを前提にしているため、特別`tf.keras.layers.Layer`について解説しないが、静的に出力shapeの計算を行うことで高速なグラフ構築を自動で行ってくれる。

tf.keras.Model+add_loss

誤差関数以外のlossの記述はこの関数を使う。
tf.keras.Model+add_lossと書いたが別に`Sequential`にも使える。
使い方は非常に簡単。`add_loss`の第一引数に追加したい誤差を記述するだけである。

import tensorflow as tf

model = tf.keras.Sequential([
    tf.keras.layers.Dense(100, activation=tf.nn.relu),
    tf.keras.layers.Dense(100, activation=tf.nn.relu),
    tf.keras.layers.Dense(1, activation=tf.nn.relu),
]) # 中間層3層の全結合層のみのネットワーク
model.add_loss(tf.reduce_mean(tf.abs(model.layers[0].weights[0]),axis=-1)) # この場合、1層目の全結合層のy=Wx+bのWに対するL1正則化
model.fit(x, y) # いつもの学習

一応上記はDenseの引数のkernel_regularizerにregularizerを追加すればできることなので、上のような方法でL1正則化をかけるケースはない。
それでも有用なケースはいくらでもあって、例えば以下のようにAutoencoderの潜在変数にL1正則化を書けたい場合だ。

class LatentL1Autoencoder(keras.Model):
    def __init__(self):
        self.encoder = tf.keras.Dense(10)
        self.decoder = tf.keras.Dense(10)

    def call(self, x):
        latent= self.encoder(x, training=True)  # Encoding
        y_pred = self.decoder(latent, training=True) # Decoding
        l1_loss = tf.reduce_mean(tf.abs(latent), axis=-1)
        self.add_loss(l1_loss) # Lossの追加

        return y_pred

model = LatentL1Autoencoder()
model.fit(x, x)

同様にl1_lossの計算を標準正規分布とのkl-divergenceに置き換えればVAEの出来上がりだ。
多分、VAEを書くならこれだけで十分であると言える。

tf.keras.Model+train_step

`train_step`はtf.keras.Modelのメンバ関数でこれをオーバーロードすることで記述する。
これは上記と同じことを記載している。
あまりこれだけの用途であれば意味はないが、2回微分とかより複雑な誤差関数を有するモデルを構築したい場合はこういう書き方が有用だ。

class LatentL1Autoencoder(keras.Model):
    def __init__(self):
        self.encoder = tf.keras.Dense(10)
        self.decoder = tf.keras.Dense(10)

    def call(self, x):
        latent= self.encoder(x, training=True)  # Encoding
        y_pred = self.decoder(latent, training=True) # Decoding

        return y_pred

    def train_step(self, data):
        x, y = data

        with tf.GradientTape() as tape:
            l1_loss = tf.reduce_mean(tf.abs(latent), axis=-1)
            self.add_loss(l1_loss)

            #regularization_lossesにself.lossesを入れないと正則化項が一切含まれなくなるので注意
            loss = self.compiled_loss(y, y_pred, regularization_losses=self.losses) 

        trainable_vars = self.trainable_variables
        gradients = tape.gradient(loss, trainable_vars) # 勾配の計算
        self.optimizer.apply_gradients(zip(gradients, trainable_vars)) # パラメータの更新
        self.compiled_metrics.update_state(y, y_pred) # 評価値の計算
        return {m.name: m.result() for m in self.metrics} # 計算した評価値を返す

model = LatentL1Autoencoder()
model.fit(x, x)

一応これで終わりだが、厳密にはdataにsample_weightが含まれる場合の対応やtest_stepの実装を行わないとevaluation時のlossにl1正則化項が含まれないなどいくらか問題がある。
抜粋してきたコードだがtest_step自体の実装は以下になる。sample_weightを使う場合それとは別にdataの系列長を確認して3ならsample_weightが含まれていて、2なら含まれていないという解釈になる。

Customize what happens in Model.fit  |  TensorFlow Core

python >

class CustomModel(tf.keras.Model):
def test_step(self, data):
# Unpack the data
x, y = data
# Compute predictions
y_pred = self(x, training=False)
# Updates the metrics tracking the loss
self.compiled_loss(y, y_pred, regularization_losses=self.losses)
# Update the metrics.
self.compiled_metrics.update_state(y, y_pred)
# Return a dict mapping metric names to current value.
# Note that it will include the loss (tracked in self.metrics).
return {m.name: m.result() for m in self.metrics}
|

tf.keras.Model+自前のtraining loop

今や、これを使うのはGANを書くときぐらいだろうか。
`tf.keras.Model`はoptimizerを一つしか使えないが`fit`関数を使わずにtraining loopを書けば複数個使うことができる。
非常に長くなるので、その書き方は是非君の目で確かめてほしい。

Writing a training loop from scratch  |  TensorFlow Core

基本的にはtrain_stepの中身を外に出してその外側にdatasetをfor文で回してtrain_stepの引数に与えるような実装になる。

Tensorflowでregularizationが効いていなかった話

Tensorflow 2.0で自分でTraining loopを書いている人は一度公式のドキュメント読んだほうがいいと思った
www.tensorflow.org

今まで知らなかったけど、Training loopを自分で書くケースでは正則化項のlossを自分で足し込む必要があるらしい。
該当箇所は以下。

There are two important things to note about this sort of regularization.

First: if you are writing your own training loop, then you need to be sure to ask the model for its regularization losses.

result = l2_model(features)
regularization_loss=tf.add_n(l2_model.losses)

Second: This implementation works by adding the weight penalties to the model's loss, and then applying a standard optimization procedure after that.

There is a second approach that instead only runs the optimizer on the raw loss, and then while applying the calculated step the optimizer also applies some weight decay. This "Decoupled Weight Decay" is seen in optimizers like optimizers.FTRL and optimizers.AdamW.

一応和訳。後半はだいぶ意訳ですが、知らんかった。

これらの正則化には2点注意すべきことがあります。
1. もし、自分自身で学習ループを描いているならば、あなたは正則化損失を自分で確認する必要があります。

result = l2_model(features)
regularization_loss=tf.add_n(l2_model.losses)

2. この実装はモデルの誤差関数に足し込み、そして標準的な最適化処理を実行することで動作します。
生の誤差関数の最適化だけを実行する代わりの2番目のアプローチとして、Weight decayを最適化時に計算して適用する方法があります。
これは「Decoupled Weight Decay」と呼ばれ、optimizers.FTRLやoptimizers.AdamWがこれに当たります。


和訳終わり、ちなみにですがまだ日本語ドキュメントは更新されてないんで、上記の記述はありません。
おそらく途中で翻訳者の人が力尽きたんでしょうかね。

それはさておき、正則化の件は気をつけましょう。
Tensorflow probabiltyとか使ってると本当に悲しいことになるんで。

VSCodeでPythonのTestが自動検出されない話

VSCodePythonを書いていて、Testが自動検出されなくて困っていた。
VSCodeはUnit testのデバッグがワンボタンでできるため、結構便利に使っていたのだが、PCを買い替えてVSCodeの設定を飛ばしてからTestの検出がされなくて困っていた。

ネットの海を泳いでも回答は得られず、公式ドキュメントを読み返していると以下のようなTipsを見つけた

Tip: Sometimes tests placed in subfolders aren't discovered because such test files cannot be imported. To make them importable, create an empty file named __init__.py in that folder.

つまり、サブフォルダにテストが配置されるケースにおいて__init__.pyがないフォルダは上手く検出できないから、そういう時はフォルダに__init__.pyを追加してね。とのことだ。
Python 3.9がリリースされて久しい昨今、__init__.pyが無いフォルダがimportできないPython 3.3以前の古代兵器を使う人は随分少ないはずだ。
にも関わらず、現代も上記のような仕様になっているらしい。

実はsetuptoolsも似たような仕様で__init__.pyの無いフォルダはpackageに追加することができないようになっている(ファイルを直接コピーすることで対応できなくはないが…)。
正直、Pythonの仕様と関連ツールの仕様のミスマッチが激しいところがPythonらしいと感じてしまったが、それでも何とかならないものか。

FactorVAEを実装した話

f:id:elda27:20200710225355g:plain

Factor VAEをTensorflow2.0で実装してみた話です。
github.com

Tensorflow 1.XやChainerの実装はありましたが、Tensorflow 2.0で書かれた物はなかったので、実装してみました。

トップの画像は実際にLatent traversalを行った結果です。
若干チューニングの甘さがありますが、まあある程度できていると言って差し支えないでしょう。

What's Factor VAE

arxiv.org

β-VAEの改良版で、disentangled representationを抽出するネットワークを学習します。
β-VAEでは、disentangledrepresentationを得るための誤差関数がVAEの再構成誤差とトレードオフの関係にありましたが、FactorVAEはそこをAdversarial Trainingによって解消しています。

ところで、disentangled representationとはなにか?については下記が詳しい。
要は説明可能な特徴表現を指していて、上記のP.4をひっぱてきた。f:id:elda27:20200709202439p:plain

http://www.cv.info.gifu-u.ac.jp/contents/workshop/contents/nips2018/ppt/NIPS_yamada.pdf

Factor VAEの仕組み

概要

Autoencoderについて以下の誤差を最小化して、誤差を計算します。

  1. Autoencoderの再構成誤差(入出力のL2距離を計算)を最小化
  2. Autoencoderの潜在変数のGaussian KL divergenceを最小化(ここまで普通のVAEの誤差関数)
  3. Autoencoderの潜在変数をDiscriminatorに入力してその出力についてTotal Correlationを最小化。(これがDisentangled representationを得るための仕組み)

また、Discriminatorの学習には、ある画像から得られる潜在変数を入力したときと、特定の次元についてSwapしたときの誤差がより大きくなるように学習を行います。

Total Correlation

en.wikipedia.org
Total Correlationって何?ってなりましたが、つまるところ潜在変数の各次元の分布と全潜在変数のJoint distributionが最小になる= P(X_1)P(X_2) \cdots P(X_N)=P(X_1, X_2, \cdots, X_N)になるので、すべての次元が独立になるということで良いでしょう。

以下の図において円同士が重なるところがTotal Correlationで計算しているところであり、ここを最小化しています。
f:id:elda27:20200721215715p:plain
Wikipediaから引用

このTotal Correlationを計算するためには当然、 P(X_1)P(X_2) \cdots P(X_N) P(X_1, X_2, \cdots, X_N)を得る必要があります。
したがって、一般的なDiscriminatorは入力が1次元(入力が本物か偽物かを判定)ですが、このDiscriminatorは2次元の出力を持つことになります。
ちなみに、 P(X_1, X_2, \cdots, X_N)は通常のDiscriminatorの出力で P(X_1)P(X_2) \cdots P(X_N)を推定することがFactorVAE固有のものになります。

実装のためのテクニック

当たり前ですが論文の数式を愚直に実装するよりAdversarial Training特有の式変形を行って誤差関数を計算しやすくしたほうが当然学習は安定します
(というかそれなしで実装している実装をみかけたのですが、どうなってるんですかね?普通にNaN吐いて死ぬんですけど…)

実際に各誤差は以下の実装および計算を行います。
各記号はそれぞれ以下の図とのようになっています。
f:id:elda27:20200709215101p:plain

計算式は最終的な計算結果を最初に示して詳しい話は後に書くようにしています。

(1) 再構成誤差
実にシンプルな二乗誤差。
説明は不要でしょう。
 L_2=tex: |D(E(X)) - X|_2

(2) KL-divergence
[KL=tex: 0.5 \times (\textit{E}\[\theta\]+ Var\[\theta\] - lnVar\[\theta\] - 1)]
これは、以下の式に\mu_2\sigma_2に標準正規分布\mu_2=0,\sigma_2=1を代入した式です。


\begin{align}
KL(p||q)&=& \int_{-\infty}^{\infty}p(x)\ln \frac{p(x)}{q(x)}dx \\
        &=& \ln\left(\frac{\sigma_2}{\sigma_1}\right) + \frac{\sigma_1^2+(\mu_1-\mu_2)^2}{2\sigma_2^2} - \frac{1}{2}
\end{align}

def gaussian_kl_divergence(mean, ln_var, raxis=1):
    var = tf.exp(ln_var)
    mean_square = mean * mean
    return tf.reduce_sum((mean_square + var - ln_var - 1) * 0.5, axis=raxis)

(3) Total correlation
Discriminatorの出力にsigmoid関数を掛けずにlogitを計算するようにすればそのままエントロピーになりますので、差を計算するだけで大丈夫。

TC=D(\theta)-\bar{D}(\theta)
ただし、\bar{D}(\theta)はDiscriminatorの2番目の出力です。


実際には以下の式変形になります。
 
\begin{align}
KL(q(\theta)||\bar{q}(\theta))&=ln\frac{q(\theta)}{\bar{q}(\theta)}
&=ln q(\theta) - ln \bar{q}(\theta)
\end{align}

実際のコードでは以下が該当の部分です。

logits_orig_z = disc(z)
logits_shuffle_z = disc(z_shuffled)

L_{TC} = tf.gather(logits_orig_z, 0, axis=1) - tf.gather(logits_orig_z, 1, axis=1)

(4) Discriminator loss
通常のBinary crossentropyの差を計算する代わりにsoftplusを使って計算することで、NaNが計算されないようにしています。
Binary CrossentropyとSoftplusの関係性は以下のブログが式変形まで詳しいです。
tatsukawa.hatenablog.com

 L_{discriminator}=0.5 softplus(D(\theta))+0.5 softplus(swap(\theta))
ここで、swapの部分は特定のEncoderが抽出する潜在変数の特定の次元を入れ替える操作です。

実装上は以下のようになります。
あまりいい実装出ないように思いますが、まあ良しとしましょう。

    indices = list(range(z_shape[1]))
    swap_index_pair = np.random.choice(indices, size=2, replace=False)
    tmp = indices[swap_index_pair[0]]
    indices[swap_index_pair[0]] = indices[swap_index_pair[1]]
    indices[swap_index_pair[1]] = tmp
    nd_indices = [
        [i, j] for i, j in product(range(z_shape[0]), indices)
    ]
    
    z_shuffled = tf.reshape(tf.gather_nd(z, tf.convert_to_tensor(nd_indices)), z_shape)

    loss_disc = (0.5 * tf.keras.activations.softplus(-tf.gather(logits_orig_z, 0, axis=1))\
        + 0.5 * tf.keras.activations.softplus(-tf.gather(logits_shuffle_z, 0, axis=1)))

チューニング

ここからは実際にDisentangled representationを得るために行ったチューニングの諸々。
結構完成まで紆余曲折あったので、まとめてみた。

VAEのConvolutionにResidual Connectionを加える

そもそもVAEの学習が遅かったのでResidual Connectionを加えた。
ついでに画像のボケ具合も解消された感じがあったので、そこそこ効果があった気がする。
この辺りはかなり初期に追加していたので、実際問題として実装が悪かったおかげで改善したのか、本当に意味があったのかは不明です。
一般論で言えば、効果あるでしょうけど。

reduce_meanを使わない

Tensorflow固有の話かは分からないけれど、再構成誤差の部分にreduce_meanの代わりにreduce_sumを必ず使おう。
サンプル数のNで割られた結果誤差が消失するのかまた、他の影響なのか画像がグレーになりました。

Total correlationについて

Total Correlationは意外と厄介で、これは数式上負の値を取りうる。
これのせいで、最適化序盤で破綻することがあった。
特にDiscriminatorが貧弱なときに起きる印象があるので、ある程度の層数のDiscriminatorを使うと全く起きなくなった。

KL divergenceの項の重みを大きくする

一時、Batchsizeを512に落としたときに潜在変数の分布が正規分布じゃなくなったのでやってみたが、あまり効果がなかった。
実際問題として、再構成誤差より大きくならないと改善しなかったし、再構成誤差よりも大きくなると画像のクォリティとDisentangled representationに強い影響を与えてしまうので、はっきり言って良くなかった。
やはり、再構成誤差より1オーダ小さいぐらいが丁度いいように思う。

もし潜在変数が正規分布に従わなくなったらバッチサイズを上げて見るのが良いと思う。

もうちょっと頑張った方が良いと思うところ

以下の3点をどうにかしたいと思うけど、そろそろImage Transformerが気になって仕方がないので、頑張る気はないです。

1. Latent representationが正規分布じゃない
 →Batch sizeを上げれば解消するけど、これすると次はTotal Correlationの最適化がうまく行かなくなってる。Leraning rate落として緩やかに学習すべきかね。
f:id:elda27:20200710224419p:plain
2. VAEの出力にSigmoidかけるの忘れてた。
 →普通に忘れてた。もうちょっとキレイな結果が得られると思う。
3. 1次元分値を変えても出力が変わらない潜在変数が存在する。
 →Latent representationの次数が多すぎるのでしょうかね。