空飛ぶ気まぐれ雑記帳

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

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の次数が多すぎるのでしょうかね。

【Now Reading】A Disentangling Invertible Interpretation Network for Explaining Latent Representations

はじめに

読んでる最中の論文についてまとめる。

読んでいて気づいたけど、自身の論文を大量に引用していて、前提とする知識が多すぎる。。。
結構時間がかかりそう。


まだまだ読見込んでいる段階なので、ちょっとづつ書き足していきます。
そのうち実装もしたいなと思っているので、誤りや解釈違いがあればご指摘ください

タイトルは「A Disentangling Invertible Interpretation Network for Explaining Latent Representations」
https://arxiv.org/abs/2004.13166

ちなみに著者によるPyTorchの実装も存在する。
github.com


関連する研究にはInfo GANやβ-VAE、Factor VAEなどがある。

端的にどういう論文か

タイトルにある通り、ポイントは潜在変数がInvertible Interpretation(潜在表現が可逆変換可能で変換可能な空間で操作可能)でかつdisentangled representation(意味づけされた潜在表現)なことを特徴にした研究だと思う。

f:id:elda27:20200629214051p:plain
Hyunjik Kim, Andriy Mnih ; Proceedings of the 35th International Conference on Machine Learning, PMLR 80:2649-2658, 2018.

まず、disentangled representationとは潜在変数の各次元が独立で人が解釈可能な特徴量を指す(と思う)。
各次元が独立=各次元で意味が重複しないことを示している。
上の図はFactor VAEの論文から拝借してきたものだ。
各行はLatent traversal(潜在変数をある次元について線形に変化させる操作)することで得られた画像である。
それぞれ異なるcomponent(どの要素が変化しているかは画像の右側に書いてある)が変化していることが分かる。
これがdisentangled representationを持つメリットである。

f:id:elda27:20200629212230p:plain
続いて上の図はInvertible Intepretationを示している。
あるX_1X_2があったときにX_1からX_2への線形な変換とは何かを考える際に単純にX_1X_2それぞれから抽出した潜在変数を補完することは果たして正しい操作なのかということに言及している(ように思う、たぶん)。
この図はおそらく一般的なVAEでは間違いなく当てはまるのだけれども、果たしてFactor VAEのようなネットワークでも同様なことが言えるかは疑問符が付く。

Invertible Transformation of Hidden Representations

f:id:elda27:20200629224408p:plain
以下の文字が頻出するのでそれぞれメモ的に残しておく。
 
f(x) = G \circ E(x) \\
f(x): 何らかのネットワーク \\
E(x): 潜在変数 z を生成するネットワーク \\
G: 潜在変数 z からある出力を得る f のサブネットワーク \\
おおむね、Encoder, Decoder型のネットワークを想像して問題ない。
論文中では、特定のネットワークに依存しない内容で書かれているため、このような表現になっているとみられる。
また\tilde{z}zの違いとして\tilde{z}=T(z)かつ\tilde{z}=T(z)である。したがって、\tilde{z}は人が解釈可能な潜在変数でとzはそれを特に満たさないとみられる。

また、Tは逆変換が可能で、zからz^*へのlatent traversalはz \rightarrow z^* \tilde{z} := T^{-1}(T(z)*)とも書かれている。

ここの中身はSupplementary Materialsに詳細が書かれていて、TはCoupling block, ActNorm, Shuffling layersから構成されているらしい。
色々調べているFLOWベース生成モデルでは一般的に使われている構成のようでGLOWやNICEなどいずれも同様な構成になっていた。
FLOWとか全然知らないので、勉強中。
下記のリンクは勉強用のメモ。

qiita.com
qiita.com
peluigi.hatenablog.com

Disentangling Interpretable Concepts

解釈可能な潜在変数の持ち方に関する話。
ぶっちゃけ式変形がわかってない。Supplemental Materialを読めと書いているが一旦保留。

最終的なロスは下記の通り。
f:id:elda27:20200630205129p:plain
z=E(x)xは入力、Fはsemantic conceptsのindexで、要はK次元の人間が理解可能な概念のindexを指す。
\sigma_{ab}in(0,1)\tilde{z^a}\tilde{z^b}の正の相関を離散化したもの、
T'(z)=|T(z)|で表される。

本当によくわかってない。

Estimating Dimensionality of Factors

\tilde{z}が幾つの次数を持つべきかという話。
スコアs_F自体はF番目のsemantic conceptについてペアとなる画像同士から計算される相関係数である。
f:id:elda27:20200630222453p:plain
端的に言えば、コンセプトを同じくする画像同士はE(\cdot)が抽出するz相関係数の和は高くなるはずということだと思う。

それから、F番目のsemantic comceptの次数N_F=\frac{\exp s_F}{\sum^K_{k=0} \exp s_k}N、ただしNは潜在変数zの次数である。

先にも書いたがある次数の相関係数が正=そのsemantic conceptを表現するために必要な変数だと理解している。
下記は実際にColor MNISTに対して計算したN_Fらしい。

(もしかして、これも最適化するのか…?)

f:id:elda27:20200630222800p:plain

分かってないポイント

  • 誤差関数
  • Nは動的に最適化するのか

TensorflowかPyTorchなのか

何のポエムか

Deep Learningをするときは常にChainerを使っていたのですが、Chainerがサポート終了とのことで、フレームワークの乗り換えが必要になった。
その時に、選択肢として浮かんだのがTensorflowとPyTorchだった。
Microsoft Cognitive Toolkit(旧CNTK)とかnnablaとかマイナーなフレームワークはいくつかありますが、Chainer以下の普及率のものを使おうとは思わない。

元々Chainerを使っているときも思ったけど、マイナーなライブラリを使うと何かパクるときに実装の負担が大きいことに気づいた。
その点TensorflowかPyTorchなら問題ない訳で、どちらを使うかを当時は悩んでいた。

ちなみにポエムなので私が何を言いたいかは最後まで読まないと分からなくなっているが悪しからず。

Chainerの何がよかったか

そもそも、Chainerが好きだった理由はソースコードの殆どがPythonで記述されているところだ。
実行速度で他ライブラリに不利であったが、バグを見つけた時にVSCodeでCtrl+クリックでファイルを開いて、何が起きているのか検証できたし、Deep Learningの中身を読むことで勉強にもなった。
昔dlibでDeconvolutionの実装についてPRを送ったときに感じたが、やはりソースコードを読むとこれまで無かった実装に対する知見が得られるだけでなく、論文に書かれた数式に対する解釈が深まるように思う。
それは本質的な話ではなく、ChainerのBackendとしてCuPyが動いていること自体に大きな意味があったと今では思っている。
なぜなら自前でLayerを実装する際に一々高速な実装とするためにC++のコードをビルドする必要がないからだ。
実際問題、CuPyはCUDAの機能を一部しか使えないために自前でLayerを定義するときに困ることは確かにあったが、行列演算をはじめとする簡単な計算をnumpyと同様のインタフェースで操作できるというのは圧倒的な強みだった。
(最近ではどのライブラリも同じようなことができるのでこの部分はもはや強みではないかもしれないが)

結局どっちにしたかと選んだ理由について

とりあえずどちらかを選ぶにあたって、PyTorchとTensorflowで書かれたプログラムを読んでいた。
当時はTensorflow 2.0(alpha版だったか?)とPyTorch 1.0がリリースされたころだったと記憶している。

Tensorflow 1.x台の良くないと思っていたところにインタフェースの多さがあった。
tensorflow 1.xの前半はバージョンが2個上がれば異なる名前空間に同じ機能(若干書き方が違う)が1個増えるような状態だった。
確かにほかのライブラリに比べて高速であったが、既述のばらつきというのはそれだけで、他人のコードを参考にする上で害悪だ。
特に、使う名前空間によって挙動が若干異なるとなるとそれは深刻だった。

一方のPyTorchの良くないと思っていたところにChainerのフォークながら名前の語感が嫌いだった。
当時は何が嫌いなのか分からなかったが最近になって分かるようになってきた。
それは、名前のネーミングがnumpyから遠すぎるからだ。
例えば、unsqueeze。これはsqueezeの逆なので配列の次数を拡張するメソッドであるが、numpyではexpand_dimsだ。
当然、Chainerはexpand_dimsであるが、こいつは違うのだ。
ググるときに困るというのもあるが、それ以上に生理的に受け付けない。
ちなみに、Tensorflow2.0はnumpyに合わせたメンバ名を採用しているので、expand_dimsだ。

何が言いたいか

そういう訳で、現在もTensorflow 2.0を使っている。
ぶっちゃけTensorflow 2.0はあまり普及しておらず、論文の著者実装はだいたいTensorflow 1.xかPyTorchなので、「マイナーなライブラリを使うと何かパクるときに実装の負担が大きいことに気づいた」という点に関して、一切寄与していない。

それでも、名前の語感がNumpyに似ているという点と元々高速な実行速度の強みはTensorflowを使うに十分足ると思う。
もちろん、Tensorflowには結構不便なところもたくさんあって、PyTorchに逃げたくなることも多いが、それでも語感が生理的に受け付けないのは無理だ。
少なくとも、Chainerを使っていたので、読むことはできるから移植に困ることはそうない。

ところで、今回どうしてこのポエムを書いたかと言うと、Pythonのエコシステムに乗ることの重要性について書きたかったからだ。
Pythonはどちらかと言えば一神教な言語だと思っている。パッケージマネージャはpipが主流だし、行列計算はnumpy、グラフはmatplotlib、表データはpandasなどなど。
もちろん、グラフならmayaviとか色々あるが、やっぱりmatplotlibを使うというのが一般的だと思っている。
そして、それらのライブラリがあるが故にほかのライブラリでもやはり語感が揃っている方がPythonらしいと私は思っている。

だから、頼むからunsqueezeとか分けわからん名前を付けるのはやめてくれ。

【Windows】DockerでPythonアプリケーションをビルドする方法

背景

年に一度しか更新されず、前置きが長いポエミーな本ブログで未だによく見られている記事にGUIアプリを作る話があったので、折角ならもうちょっとモダンに何とかできないかと。 思い至ったので、実際に作ってみました。

ところで、私は普段WindowsでAnacondaを使ってPythonを使っているのですが、Anaconda+PyInstallerという組み合わせは非常にまずい組み合わせであります。 何がまずいかと言えば、ファイルサイズです。 Anacondaは依存関係が異常に複雑で、特に依存ライブラリのないプログラムであっても、PyInstallerを使ってExeを作ると200MB近いファイルサイズになってしまいます。 それにPySideや何やら入ってしまうと、どんどんファイルサイズが大きくなってしまいます。

それは何とも残念な話ですので、通常のPythonにPyInstallerをインストールしてビルドすれば、ファイルサイズの増大を回避できますが、いくらvenvをインストールしてやったとしても、それは何かと面倒なことがあります。 まず、新規でアプリケーションを作る度にvenvで環境を作るというのも良いと思いますが、個人的にいつもrequirements.txtの書き忘れが問題になります。 Linuxならpipenvを使えばそれも回避できますが、Windowsでpipenvはまともな動きをしてくれないので、それもダメです。

はてさてどうしたものかとなりますが、いい感じにやる方法にDockerがあります。 Dockerの説明はもう良いでしょと思うので割愛しますが、Dockerを使えば使い捨てのビルド環境が作れるのでいい感じにやれるなと思って今回の記事のようなものを作りました。

Windows container on Windows

Windows環境でWindow Containerを動かすのってどうやんの?って思ったので調べたのですが、タスクトレイのDockerを右クリックして出てくるメニューから切り替えられるそうです。 非常に簡単でありますね。ちなみに、WindowsコンテナとLinuxコンテナは同時に使えない問題があるそうですが、まあそれはそれということでしょうか。 f:id:elda27:20200623210106p:plain

ちなみにですが、Windowsコンテナのライセンスってどうなってんの?ってなことも気になって調べましたが、開発用途なら特に使用に制約は無いようです(ないよね?) docs.microsoft.com

Dockerfile

適当なアプリケーションをビルドする用のDockerfileは下記のとおりです。 pythonソースコードと同じディレクトリにファイルを保存している前提で、以下のようなDockerfileを書きました。

FROM winamd64/python:3.8.3-windowsservercore-1809 

# /tmpにソースコードを展開して必要なファイルをインストール
# 事前にrequirements.txtをコピーして必要なライブラリをインストールして
# キャッシュしておく。
WORKDIR /tmp
COPY requirements.txt /tmp
RUN pip install -r requirements.txt
RUN pip install pyinstaller

# それ以外のソースコードをホストからコピーしてpyinstallerを実行する。
COPY . /tmp
RUN pyinstaller Main.spec
WORKDIR /app
RUN cp -r /tmp/dist/* .
RUN Remove-Item -Path /tmp -Recurse -Force
RUN mkdir /mount
ENTRYPOINT cp -r -Force * /mount

ちなみに使っているpyinstallerのspecファイルは以下の通りです。 依存ライブラリとしてeelを使うアプリケーションを考えているので、ライブラリのインストールディレクトリからdataファイルに追加するようにしています。

# -*- mode: python -*-

import eel
from pathlib import Path

path_to_eel_js = Path(eel.__file__).parent / 'eel.js'

block_cipher = None

a = Analysis(
  ['./app/__main__.py'],
  pathex=[],
  binaries=None,
  datas=[
    (str(path_to_eel_js), './eel/')
  ],
  hiddenimports=[
    'pkg_resources.py2_warn',
    'eel',
    'bottle_websocket'
  ],
  hookspath=None,
  runtime_hooks=None,
  excludes=None,
  cipher=block_cipher
)
pyz = PYZ(
  a.pure, a.zipped_data,
  cipher=block_cipher
)
exe = EXE(
  pyz,
  a.scripts,
  a.binaries,
  a.zipfiles,
  a.datas,
  name='ClipDraw',
  debug=False,
  strip=None,
  upx=True,
  console=True, 
  #icon='mainIcon.ico'
)
app = BUNDLE(
 exe,
 name='app.exe',
 #icon='mainIcon.ico'
)

これだけで勝手にexeファイルができます。

コンテナの使い方

実際にはDockerファイル内でビルドしたパッケージをローカルにダウンロードする必要があるので、以下のコマンドでホストにコピーする必要があります。

docker run --rm -it -v ${pwd}\build:C:\mount python-build-env:latest

ちなみに上記はpowershellで実行するケースでたぶんcmdだと下記のようになります。

docker run --rm -it -v %cd%\build:C:\mount python-build-env:latest

これでWindows環境で動くPythonアプリケーションが出来上がるはずです。 LinuxMacでも基となるOSをそろえてやれば(例えばLinuxならPython:3.8-busterとか)同様の手順でアプリケーションを浮くることができるはずです。