Smile Engineering Blog

ジェイエスピーからTipsや技術特集、プロジェクト物語を発信します

TensorFlow2.0ついにリリース!

全く新しくなったTensorFlow

つい先程、TensorFlow2.0が正式リリースされたようです!!
(ブログを書こうと仮眠して起きたら『rc』が『stable』に変わってました(汗) ←今何時?)

日頃からTensorFlowを使用してあれこれしている筆者としては『やっとリリースされたか〜』という印象なのですが、そもそも『2.0』とかいうバージョンを初めて聞いたという方もいらっしゃると思うので、今回は『2.0になって何か変わったのか?』を整理していこうと思います。

『TensorFlow 2.0』と聞いて、一番大きく変わった点は、やはり『Define by Run』がデフォルトになったところでしょう。

f:id:jspnet:20191001055201p:plain

『Define by Run』とは?

語弊を恐れず『Define by Run』を一言で書きますと、学習開始前に学習モデルを定義する必要がなくなったという点です。
これまでのTensorFlowの場合、学習を開始させるには、まず学習モデルを定義して、それをコンパイルして、セッションというのを作成してようやく学習開始させていました。これを『Define and Run』と呼びます。そうすることで学習時間を短縮することができます。ようはインタプリタ言語よりC言語の方が実行時間が高速とかそういうお話ですね。
が……時代は逆らえないのでしょうか。TensorFlow以外の主流な深層学習フレームワークはほぼ『Define by Run』になっています。なぜかって、そちらのほうが圧倒的にデバッグしやすいからです。深層学習の学習モデルを構築するのに、デバッガもまともに使えない『Define and Run』では、『アレがしたくてもできない〜!!』などと叫んでいた方もいらっしゃるのでは??

  • Define and Run : 学習モデルをコンパイルした後に学習開始。高速。TensorFlow 1.14等で採用
  • Define by Run : 事前に学習モデルをコンパイル不要。デバッグしやすい。Chainer、PyTorch、TensorFlow 2.0等で採用

今回TensorFlowが『Define by Run』に移行したのも、ライバルが皆こぞって『Define by Run』を採用していたというのもあるのかもしれませんね。

TensorFlow 2.0でどう変わったのか?

TensorFlow2.0では『Define by Run』がデフォルトになった……とは書いていますが、裏を返すと『Define and Run』でも書くことができます。
言い方を変えると、その両立をできるというのがTensorFlowの一つの魅力になったと言えるかもしれません。(よりめんどくさくなった……とも思えるけど)

具体的な変更点としてはこんな具合です。

1. セッションという概念がそもそもなくなった

これまでのTensorFlowでは tf.Session() というメソッドをコールして、学習を行うのにセッションをまず作成していました。
が、今回のTensorFlow2.0ではそもそも

tf.Session() そのものが廃止されました

と聞くだけでびっくりする方も多いかと思いますが、『Define by Run』で動くということは、つまりセッションという中ではなく、どこでも計算が可能になったということになります。
これまで、デバッガを使って計算結果を確認しようにもセッションの中にいないから確認できない!……などと悩まされる必要もなくなったということですね!

2. tf.placeholder() も廃止された

TensorFlow1.x の世界では当たり前のように存在していた tf.placeholder() ……要するに変数の入れ箱のようなものですが、

tf.placeholder() も廃止!! 定義そのものが存在しないのでそのままソースを流用するとビルドエラーになります!!

です。

いやまぁそれはわかるんだけど、なにも廃止にするのはどうかな〜と(汗)
もし 1.x で書いたソースコードがあったら、この部分は書き直さないと動かないので、要注意ですね。
(基本的に変更点が多すぎて 1.x 用のソースをそのまま移植するのはいろいろ大変なのも事実ですけど)

というわけで、前回のGANのソースもちょっと置き換えてみよう!

はい。ようやく今回のブログの書きたかった箇所です。

smile-jsp.hateblo.jp

前回駆け足で紹介しましたGANのソースコードですが、今回もまだソースコードが中途半端なので駆け足で紹介いたします。
(いずれはちゃんと……

今回は主に学習を実際に行う部分を紹介したいと思います。
TensorFlow 2.0ではこんな書き方をするのかと理解していただけたら光栄です。

class Trainer():
    def __init__(self, target_path, output_path, out_model_path, dummy_path=None):
        # Model
        self.gene = Generator(image_x, image_y, Z_dim)
        self.disc = Discriminator(image_x, image_y)

        # Optimizer
        self.gene_opt = tf.optimizers.Adam(0.00001)
        self.disc_opt = tf.optimizers.Adam(0.00001)

        # 画像生成用ディレクトリの作成
        self.output_path = output_path

        # ターゲット画像の取得
        self.target_image = self.read_image(target_path)
        self.target_len = len(self.target_image)

        # ダミー画像の取得
        self.dummy_len = 0
        if dummy_path is not None:
            self.dummy_image = self.read_image(dummy_path)
            self.dummy_len = len(self.dummy_image)

        # 学習モデル出力ディレクトリの作成
        self.out_model_path = out_model_path
        if os.path.isdir(out_model_path):
            shutil.rmtree(out_model_path)

        self.d_real_labels = tf.constant(1, shape=[d_batch_size])
        self.d_fake_labels = tf.constant(0, shape=[g_batch_size])
        self.g_labels = tf.constant(1, shape=[g_batch_size])

    def read_image(self, base_path):
        """ 画像リード

        :param base_path: 画像ベースパス
        :return:
        """
        # 画像変換
        image_ary = []
        file_list = glob.glob(os.path.abspath(base_path + "/*.png"))
        for file in file_list:
            img = cv2.imread(file)
            img = cv2.resize(img, (image_x, image_y))
            image_ary.append(img.flatten().astype(np.float32) / 255.0)

        print("image len: %d" % len(file_list))
        return image_ary

    @tf.function
    def train_gene(self):
        with tf.GradientTape() as g_tape:
            gene_image = self.gene(g_batch_size)
            g_loss = self.disc(gene_image, self.g_labels, 0.0)
        grads = g_tape.gradient(g_loss, self.gene.trainable_variables)
        self.gene_opt.apply_gradients(zip(grads, self.gene.trainable_variables))
        return g_loss, gene_image

    @tf.function
    def train_disc(self, real_img, gene_img):
        with tf.GradientTape() as tape:
            loss_real = self.disc(real_img, self.d_real_labels, 0.5)
            loss_gene = self.disc(gene_img, self.d_fake_labels, 0.5)
            loss = ((loss_real * d_batch_size) + (loss_gene * g_batch_size)) / (d_batch_size + g_batch_size)
        grads = tape.gradient(loss, self.disc.trainable_variables)
        self.disc_opt.apply_gradients(zip(grads, self.disc.trainable_variables))
        return loss

    def __call__(self):
        # 学習プロセス開始
        for itr in range(epoch_num):
            if itr % 100 == 0:
                try:
                    dirname = self.output_path + "/%06d/" % itr
                    if not os.path.isdir(dirname):
                        os.makedirs(dirname)
                    for i in range(9):
                        gene_image = self.gene()
                        img_obj = tf.reshape(gene_image, [image_x, image_y, 3])
                        img_obj = img_obj.numpy() * 255.0
                        file_name = dirname + "%02d.png" % i
                        cv2.imwrite(file_name, img_obj)
                except:
                    print("error")
                    pass
            
            # Generator
            g_loss_curr, gene_image = self.train_gene()

            # Discriminator
            rand_data = []
            rand_idx = np.random.randint(0, self.target_len, d_batch_size)
            for i in rand_idx:
                rand_data.append(self.target_image[i])
            train_np_data = np.asarray(rand_data)
            d_loss_curr = self.train_disc(train_np_data, gene_image)

            if itr % 10 == 0:
                print('Iter: {}'.format(itr))
                print(" G_loss: " + str(g_loss_curr))
                print(" D loss: " + str(d_loss_curr))

肝となっている箇所は、 train_gene() と train_disc() の箇所です。このメソッドを呼び出せば、Generator と Discriminator をそれぞれ学習させることができます。
セッションという概念がなくなったので、 tape というものを使用して逆伝搬を実行しています。

もう一点注目したいのが @tf.function() と書かれている箇所。
実はこれ、 ここはグラフモード=『Define and Run』で動かしますよ〜! という印になります。

これを付けるか否かで、実行時間が大きく変わり、当然『Define and Run』で動作しますので、付けたほうが高速に学習できます。

でもそうするとデバッグしにくいのでは?と思われる方もいると思いますが、実はその点も心配には及ばず。デバッグする場合は @tf.function() をコメントアウトしてあげれば『Define by Run』で動いてくれるんで、何も問題なかったりするんです。
なので、デバッグ中は @tf.function() をコメントアウトしてあげて、実際に学習させる場合はコメントインしてあげればOKです。

まとめ。GANについては……また次回かな?

一番最初に『仮眠していたらTensorFlow2.0がリリースされた』と書きましたが、ぶっちゃけ仮眠前は上にあるGANのコードをひたすら書いてました。
が、なかなかうまくいかないです!! (まぁ上のコードを読む人が読むだけでもツッコミどころ満載なのは認めますが。。。

というわけで、次こそはまともに動くGANのソースを紹介できれば……と思いますが、どうなるでしょうかね?????