- 投稿日:2019-12-14T18:44:59+09:00
Pix2Pixを用いてグレースケール画像に着色してみる
はじめに
本記事は、LOCAL学生部アドベントカレンダー2019の12日目の記事になります。
https://adventar.org/calendars/4020アドベントカレンダーのネタ探しに時間がかかりすぎてなかなか決まらなかったので、既出ではありますが、Pix2Pixという生成モデルを使用したモノクロ画像の着色にチャレンジしたお話をします。
(Speech Synthesisやりたかった.....)Pix2Pixとは
arXiv:https://arxiv.org/abs/1611.07004
GANの手法を利用し、様々なドメイン画像間の変換を統一的なネットワーク、誤差関数で行えるようにしたもの。
論文で紹介されているのは、
- マスク画像から元画像
- モノクロ画像からカラー画像
- 航空写真から地図
- 輪郭から色ぬり画像もちろんこれらを逆に変換するよう訓練することも可能です。
ネットワークアーキテクチャ
Generator
Generatorのネットワークは、以下のようになっています。
U-Netのモデルを修正したオートエンコーダになっています。
Conv -> Batchnorm -> Leaky ReLU
と画像を畳み込んで行き、デコーダーでは
Transposed Conv -> Batchnorm -> Dropout -> ReLU
と言った風に画像を拡大するように畳み込んで行きます。
入力画像とGeneratorの生成画像との絶対値誤差(MAE)と、Discriminator(後述)の出力による交差エントロピー(Sigmoid cross entropy)、この二つを足し合わせてGeneratorの損失関数とします。Discriminator
Discriminatorの方のネットワークは以下の通りです。
Discriminatorは、Generatorが生成した画像と教師データとして与えられている画像を識別するためのネットワークです。生成画像と訓練画像を入力として、本物か偽物(生成されたもの)かを見極めます。
訓練フェーズでは、GeneratorとDiscriminatorとが互いに競い合うようにして精度を高めていきます。実装
Tensorflow公式のチュートリアルを参考に実装しました。2.0の波に乗れてなかった人なので新しくなった関数群に戸惑いながら作りました。
https://www.tensorflow.org/tutorials/generative/pix2pix
今回チャレンジするタスクは、入力としてモノクロ画像を与え、カラー画像を出力させるというタスクです。データセットは、Googleが公開しているOpen Images dataset V5を使いました。
https://storage.googleapis.com/openimages/web/index.html
全体の実装はGithubにあります。
https://github.com/kodamanbou/Pix2PixInput pipeline
画像データのロードを行い、リサイズ、ランダムクロップ、グレースケール化します。
def load(image_file): image = tf.io.read_file(image_file) image = tf.image.decode_jpeg(image) input_image = tf.image.rgb_to_grayscale(image) input_image = tf.cast(input_image, tf.float32) real_image = tf.cast(image, tf.float32) print(input_image.shape) print(real_image.shape) return input_image, real_image def resize(input_image, real_image, height, width): input_image = tf.image.grayscale_to_rgb(input_image) input_image = tf.image.resize(input_image, [height, width], method=tf.image.ResizeMethod.NEAREST_NEIGHBOR) real_image = tf.image.resize(real_image, [height, width], method=tf.image.ResizeMethod.NEAREST_NEIGHBOR) return input_image, real_image def random_crop(input_image, real_image): stacked_image = tf.stack([input_image, real_image], axis=0) cropped_image = tf.image.random_crop( stacked_image, size=[2, hp.image_size, hp.image_size, 3]) return cropped_image[0], cropped_image[1] def normalize(input_image, real_image): input_image = (input_image / 127.5) - 1 real_image = (real_image / 127.5) - 1 return input_image, real_image @tf.function def random_jitter(input_image, real_image): # resizing to 286 x 286 x 3 input_image, real_image = resize(input_image, real_image, 286, 286) # randomly cropping to 256 x 256 x 3 input_image, real_image = random_crop(input_image, real_image) if tf.random.uniform(()) > 0.5: # random mirroring input_image = tf.image.flip_left_right(input_image) real_image = tf.image.flip_left_right(real_image) input_image = tf.image.rgb_to_grayscale(input_image) return input_image, real_image def load_image_train(image_file): input_image, real_image = load(image_file) input_image, real_image = random_jitter(input_image, real_image) input_image, real_image = normalize(input_image, real_image) return input_image, real_image def load_image_test(image_file): input_image, real_image = load(image_file) input_image, real_image = resize(input_image, real_image, hp.image_size, hp.image_size) input_image = tf.image.rgb_to_grayscale(input_image) input_image, real_image = normalize(input_image, real_image) return input_image, real_imageGenerator
Generatorは、先ほど説明したU-Net likeなネットワークになります。
def downsample(filters, size, apply_batchnorm=True): initializer = tf.random_normal_initializer(0., 0.02) result = tf.keras.Sequential() result.add( tf.keras.layers.Conv2D(filters, size, strides=2, padding='same', kernel_initializer=initializer, use_bias=False)) if apply_batchnorm: result.add(tf.keras.layers.BatchNormalization()) result.add(tf.keras.layers.LeakyReLU()) return result def upsample(filters, size, apply_dropout=False): initializer = tf.random_normal_initializer(0., 0.02) result = tf.keras.Sequential() result.add( tf.keras.layers.Conv2DTranspose(filters, size, strides=2, padding='same', kernel_initializer=initializer, use_bias=False)) result.add(tf.keras.layers.BatchNormalization()) if apply_dropout: result.add(tf.keras.layers.Dropout(0.5)) result.add(tf.keras.layers.ReLU()) return result def Generator(): inputs = tf.keras.layers.Input(shape=[256, 256, 1]) down_stack = [ downsample(64, 4, apply_batchnorm=False), # (bs, 128, 128, 64) downsample(128, 4), # (bs, 64, 64, 128) downsample(256, 4), # (bs, 32, 32, 256) downsample(512, 4), # (bs, 16, 16, 512) downsample(512, 4), # (bs, 8, 8, 512) downsample(512, 4), # (bs, 4, 4, 512) downsample(512, 4), # (bs, 2, 2, 512) downsample(512, 4), # (bs, 1, 1, 512) ] up_stack = [ upsample(512, 4, apply_dropout=True), # (bs, 2, 2, 1024) upsample(512, 4, apply_dropout=True), # (bs, 4, 4, 1024) upsample(512, 4, apply_dropout=True), # (bs, 8, 8, 1024) upsample(512, 4), # (bs, 16, 16, 1024) upsample(256, 4), # (bs, 32, 32, 512) upsample(128, 4), # (bs, 64, 64, 256) upsample(64, 4), # (bs, 128, 128, 128) ] initializer = tf.random_normal_initializer(0., 0.02) last = tf.keras.layers.Conv2DTranspose(3, 4, strides=2, padding='same', kernel_initializer=initializer, activation='tanh') # (bs, 256, 256, 3) x = inputs skips = [] for down in down_stack: x = down(x) skips.append(x) skips = reversed(skips[:-1]) for up, skip in zip(up_stack, skips): x = up(x) x = tf.keras.layers.Concatenate()([x, skip]) x = last(x) return tf.keras.Model(inputs=inputs, outputs=x) def generator_loss(disc_gen_out, gen_out, target): gan_loss = loss_object(tf.ones_like(disc_gen_out), disc_gen_out) l1_loss = tf.reduce_mean(tf.abs(gen_out - target)) total_gen_loss = gan_loss + (hp.LAMBDA * l1_loss) return total_gen_loss, gan_loss, l1_lossDiscriminator
識別側のネットワークです。
def Discriminator(): initializer = tf.random_normal_initializer(0., 0.02) inp = tf.keras.layers.Input(shape=[256, 256, 3], name='input_image') tar = tf.keras.layers.Input(shape=[256, 256, 3], name='target_image') x = tf.keras.layers.concatenate([inp, tar]) # [N, 256, 256, 4] down1 = downsample(64, 4, False)(x) # [N, 128, 128, 64] down2 = downsample(128, 4)(down1) # [N, 64, 64, 128] down3 = downsample(256, 4)(down2) # [N, 32, 32, 256] zero_pad1 = tf.keras.layers.ZeroPadding2D()(down3) # [N, 34, 34, 256] conv = tf.keras.layers.Conv2D(512, 4, strides=1, kernel_initializer=initializer, use_bias=False)(zero_pad1) # [N, 31, 31, 512] batchnorm1 = tf.keras.layers.BatchNormalization()(conv) leaky_relu = tf.keras.layers.LeakyReLU()(batchnorm1) zero_pad2 = tf.keras.layers.ZeroPadding2D()(leaky_relu) # [N, 33, 33, 512] last = tf.keras.layers.Conv2D(1, 4, strides=1, kernel_initializer=initializer)(zero_pad2) # [N, 30, 30, 1] return tf.keras.Model(inputs=[inp, tar], outputs=last) def discriminator_loss(disc_real_out, disc_gen_out): real_loss = loss_object(tf.ones_like(disc_real_out), disc_real_out) gen_loss = loss_object(tf.zeros_like(disc_gen_out), disc_gen_out) total_disc_loss = real_loss + gen_loss return total_disc_lossTraining
定義したネットワークを呼び出し、実際にトレーニング、モデルの保存などを行います。
@tf.function def train_step(input_image, target, epoch): with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape: gen_output = generator(input_image, training=True) disc_real_output = discriminator([input_image, target], training=True) disc_generated_output = discriminator([input_image, gen_output], training=True) gen_total_loss, gen_gan_loss, gen_l1_loss = generator_loss(disc_generated_output, gen_output, target) disc_loss = discriminator_loss(disc_real_output, disc_generated_output) generator_gradients = gen_tape.gradient(gen_total_loss, generator.trainable_variables) discriminator_gradients = disc_tape.gradient(disc_loss, discriminator.trainable_variables) generator_optimizer.apply_gradients(zip(generator_gradients, generator.trainable_variables)) discriminator_optimizer.apply_gradients(zip(discriminator_gradients, discriminator.trainable_variables)) with summary_writer.as_default(): tf.summary.scalar('gen_total_loss', gen_total_loss, step=epoch) tf.summary.scalar('gen_gan_loss', gen_gan_loss, step=epoch) tf.summary.scalar('gen_l1_loss', gen_l1_loss, step=epoch) tf.summary.scalar('disc_loss', disc_loss, step=epoch) def fit(train_ds, epochs, test_ds): for epoch in range(epochs): start = time.time() for example_input, example_target in test_ds.take(1): generate_images(generator, example_input, example_target) print("Epoch: ", epoch) # Train for n, (input_image, target) in train_ds.enumerate(): print('.', end='') if (n + 1) % 100 == 0: print() train_step(input_image, target, epoch) print() # saving (checkpoint) the model every 20 epochs if (epoch + 1) % 20 == 0: checkpoint.save(file_prefix=checkpoint_prefix) print('Time taken for epoch {} is {} sec\n'.format(epoch + 1, time.time() - start)) checkpoint.save(file_prefix=checkpoint_prefix)結果
Epoch 0
Epoch 1
Epoch 15
まとめ
ほぼチュートリアル通りの実装ですが、他のタスクにも簡単に応用できそうです。
参考サイト
https://www.tensorflow.org/tutorials/generative/pix2pix
http://yusuke-ujitoko.hatenablog.com/entry/2017/06/25/165008
- 投稿日:2019-12-14T16:52:58+09:00
TensorFlow2.0ドキュメントを翻訳してTF2.0Contributorになろう
はじめに
Tensorflow2.0では誰しもがお世話になるであろうこの公式TutorialとGuide、
実はこれらもOSSで作られたドキュメントで、
本家英語のGuideもそうですが、
たまに日本語の文章が出てくるものもOSSで作られたGuideだったりします。
みなさんも参加してみてはいかがでしょうか?参加方法
TFUGSlackに参加する
まず、TFUGコミュニティに参加しましょう!!
基本的にそこで情報交換されるため、参加して色々と教えてもらいましょう。READMEとCONTRIBUTINGを読む
翻訳する前にはこちらの
README.mdとCONTRIBUTING.mdを必ず読むようにお願いします。基本的なPullRequest手順
DocsをForkする
TensorFlow/docsをForkして自分の配下のレポジトリにします。
翻訳されていない物を探す
TensorFlow/docsを
git clone
して、以下のコマンドを叩くと何が足りてないかわかります。diff ~/docs/site/en/guide ~/docs/site/ja/guide | grep en
結果en/guide のみに存在: _graph_optimization.ipynb en/guide のみに存在: _index.yaml en/guide のみに存在: _toc.yaml en/guide のみに存在: checkpoint.ipynb en/guide のみに存在: create_op.md en/guide のみに存在: data.ipynb en/guide のみに存在: data_performance.ipynb en/guide のみに存在: distributed_training.ipynb ファイル en/guide/eager.ipynb と ja/guide/eager.ipynb は異なります en/guide のみに存在: effective_tf2.md en/guide のみに存在: estimator.ipynb ファイル en/guide/function.ipynb と ja/guide/function.ipynb は異なります en/guide のみに存在: gpu.ipynb en/guide のみに存在: images en/guide のみに存在: keras en/guide のみに存在: migrate.ipynb en/guide のみに存在: ragged_tensor.ipynb en/guide のみに存在: saved_model.ipynb en/guide のみに存在: tensor.md en/guide のみに存在: upgrade.ipynb en/guide のみに存在: variable.md en/guide のみに存在: versions.mdそれでいてかつ、PullRequestが出ていない物をみていきます。
これで、まだ誰も翻訳していない物がわかるはずです。
手をつけることをSlackに報告しましょう
ここで、誰かやっているなら、声をあげてくれますし、ダブって同じところをやってた...みたいなことはないはずです。
(過去のSlackをみて、やっている人が発言しているのもみるのもいいですね。)ブランチを切る
まずはブランチを切りましょう。
Forkしたレポジトリのbranchをクリックしますこれをクリックして、検索欄に新しく作成するブランチ名を書き込むと、ブランチ作成ボタンが出現します。
命名規則的には、[Auther]_[folder_name]_[filename]と行った感じで書けば大丈夫です。
今回は[shamoji]が[ja_guide]の[ragged_tensor]っていう.ipynb
ファイルを編集するのでこう書きます。Forkしたレポジトリで、jaフォルダに
.ipynb
データを写す単純なことですがやりがちなのが、en/フォルダにあるデータを直で書き込んでしまうことです。
これだと差分が分かりにくくなるので、まず先にja/フォルダの同じ場所にコピーしましょう。移動方法には二つ方法があって、
- enフォルダにある翻訳対象の.ipynb
ファイルをダウンロードし、jaフォルダの同じ場所にアップロード&commitする
- ForkしたレポジトリをCloneしてcpしてCommitする(めんどい)
があります。後者は面倒なのでおすすめしません。移動先のjaフォルダの
.ipynb
をColab上でみるCommitが済みました段階で、
.ipynb
をColabで読んでみます。
GoogleColabratoryは差分もよくみれるので、翻訳では基本これを使って翻訳していきますし、レビューもします。まず、Colabを開きます。
そのあと、「ファイル」から、「ノートブックを開く」を選択しましょう。すると、直近でみてたNotebookたちの一覧が出てきます。ここで、右上のGitHubを選択します。
GitHubのURLの入力を求められるので、ForkしたレポジトリTopのURLを載せましょう。
それで、先ほど作成したブランチを選択して、頑張って翻訳対象のファイルを探して見つけます。
クリックすることで、ページが開けて編集できる状態になりました。
これで、翻訳の準備は完了しました。翻訳する
どんどん翻訳していきましょう。
PullRequestを出す
全て翻訳し終わったら、PullRequestを出してReviewをしてもらいましょう。
誰かが言ってましたが、翻訳Reviewは再翻訳するのとほとんど同じくらい大変な作業なので、
それを踏まえて、ちゃんとしっかりした物を作ってReviewをお願いしましょう。
(自分もやらかしました、ごめんなさい)実際にPullRequestを出してみましょう。
GitHubのWebUI上に、PullRequest出してみませんか?っていう黄色い物が出てきます。
それを押して、PullRequest発行画面に移ります。
(画像が出せなくて申し訳ない...)tensorflow/docs master← [自分のID]/docs [自分の作ったブランチ]であることを確かめ、
タイトルには先人たちに習って、先頭にJA: Add
をつけて、ファイルパス全てを書きます。
とりあえず検索用にJA:
が付いていれば検索できるので、
それだけ忘れなければ分かるように書いていただければと思います。CLAサイン
PullRequestが出されると、GoogleからContributingに必要な法的サインを求めてきます。
内容は著作権に関すること、再配布、修正などに関すること、何かまずいことがあった時のこと(訴訟関連)
などなど、様々な法的なことに関しての承諾サインを求めてきます。
詳しくは本文をじっっくり読んでサインすることをおすすめします。
これにサインしないと基本的にContributeは認められないので、注意が必要です。(CIで自動で❌がつきます)ここまでがPullRequestまでの流れです。
あとはreviewを英語ベースでじっくりしてもらったあと、
LGTMが出ればマージ可能となりますので、マージしましょう。
(不用意にLGTMは打たないようにしましょう。
reviewer以外でもすぐにマージ可能になってしまいますので、気をつけてください)翻訳する上での注意点
基本的に自由な翻訳活動ですが、結構翻訳にもルールが存在していて、結構罠にハマります。
翻訳せずにそのまま使う単語
こちらを参考にしてもらえると、翻訳しないでそのままにすべき単語がいくつか載っています
校正ツール
TFUGではRedpenを使用して校正に取り組んでいます。
PullRequestではRedpenを使用して校正してから出しましょう。
こちらにあります項ごとにCommitを心がける
こうするとぱっと見でなにを訳したのかわかりやすいです。
Colabで差分を見やすくしておく。
基本的にMarkDownでは
ダブルスペースが改行扱いになります。
Colab上で勝手に改行されるからとダラダラ長い文章を\n
なしで投稿するとColab上、差分が見辛いです。
そこらへんもReviewerに配慮して翻訳してみましょう。基本的に全てを翻訳すること。
意訳で書いて、ここも伝わるっしょ〜ってすっぽ抜かすと、reviewerはちゃんと指摘してきます。
本文に書いてあることは全て翻訳するのが原則です。なにここわからん。って思ったらreviewerに投げてみるのも手
結局100%完璧な翻訳を目指そうとすると時間だけが溶けていきますので、自分は(??)マークをつけて
Reviewerの方に相談しちゃいます。そうすると途中で心が折れるなんてことは無くなります。
雑な仕事は嫌われますが、パーフェクトまでも求められてはいません。
Done is better than perfectでいきましょう。迷ったらとりあえずSlackに投げてみよう!
ここどうすればいいかわからない...といった時はSlackに聞いてみるのも手です。
様々に情報を持ってきてくれます。
ただし、その度に負担がかかっているというのは承知の上で聞きましょう。Contributorになるといい事
俺、TF2.0Contributorなんだぜ!!
って言えます。(若林:「でもそれ翻訳なんですけどね」)
ただそれだけですけども、結構自慢できるんじゃないでしょうか。それ以外にいい事
基本的に翻訳した分野はめっちゃ詳しくなります。
翻訳は読むことよりもさらに深く読み込むことになりますので、
わからない分野ほどチャレンジして翻訳をしてみるということが大事だったりします。
それに、解釈がわからない部分をreviewerに教えてもらえるチャンスもあります。
そうすることで、理解が一層深まるため、おすすめです。おわりに
自分が翻訳すれば、TFワ"ガ"ラ"ン"となってる人も自分の力で解決できると思うと素敵じゃないですか?
自分は翻訳することで、レベルが上がるしContributorになれるのでハッピー!
Reviewerさんもそこまで労力かけずとも内容理解が深まる+Contributeできるのでハッピー!
翻訳されたDocumentを読んでTF完全理解した人が増えてハッピー!
みんなTFを使い始めてGoogleもハッピー!といいこと尽くしでしょう。ぜひ、翻訳やってみませんか?人手は足りない一方なので、何卒よろしくお願いします!
Enjoy翻訳Life!