🙈

TensorFlow2で機械学習をはじめたいと思ってる方へ

2022/11/07に公開

はじめに

普段は主に機械学習のコードはPyTorchで書いています。とりあえず書きやすいですよね。
PyTorchは 「Define by Run」 と言われ、計算グラフとデータを流すタイミング同時で、何が良いかと言うと、途中の計算結果を確認できてデバックが楽です。そんなPyTorch信者の私ですが、今回TensorFlowでコードを書く必要性が出てきたのでその時のノウハウを忘れないようにメモとして残しておきます。

もともとPyTorchを触る前までは、TensorFlowやkerasを触っていたのでなんとなくは把握できていましたが、TensorFlow2になってからの内容はあまり把握できていなかったので今回良い勉強になりました。ちなみにTensorFlow1(TF1)は 「Define and Run」 と言う方式で静的グラフを作成しておいたところにデータを流す方式を採用していましたが、TensorFlow2(TF2)では 「Define by Run」 とPyTorchと同じ方式を採用しています。

この記事の対象者

この記事は、TensorFlow2で機械学習をはじめたい方を対象としています。他の方の記事を見ているとTensorFlow1の時の記事が多く、TensorFlow2(TF2)の記事があまり見つからなかったのでこれからTF2を触ろうと考えている方の参考になればと思います。また、model.fitなどで学習させるsklearn風のやり方というよりは、TensorFlowのチュートリアルにあるエキスパートな方法を中心に書きましたので興味がある方に読んで頂けたら嬉しいです。

###大まかな内容

1. TensorFlow基礎
2. kerasで簡単なモデル構築と学習(Beginner Version)
3. 転移学習(+Fine Tuning)
4. 自作モデル構築
5. TensorFLow2でモデル構築と学習(Expert Version)

###その他知っておくと便利なこと

[1]. 自作のデータセット使う場合
[2]. Augmentaionについて
[3]. TensorBoard
[4]. TFRecord

1. TensorFlow基礎

まず、基礎についてです。

tf.constant(value, dtype=None, shape=None, name='Const', verify_shape=False) で定数を定義できます。

>>> import tensorflow as tf
>>>
>>> tf.constant([1, 2, 3])
<tf.Tensor: shape=(3,), dtype=int32, numpy=array([1, 2, 3], dtype=int32)>
>>>
>>> b = tf.constant('Hello') # 文字列ok
<tf.Tensor: shape=(), dtype=string, numpy=b'Hello'>
>>>

shapeを指定すると、要素全部が同じ値になります。

>>> tf.constant(3, shape=[1, 3])
<tf.Tensor: shape=(1, 3), dtype=int32, numpy=array([[3, 3, 3]], dtype=int32)>
>>>
>>>
  • 足し算tf.add()、引き算tf.subtract()、掛け算tf.mul()、割り算tf.divide()
>>> tf.add(2,3)
<tf.Tensor: shape=(), dtype=int32, numpy=5>
>>>
>>> tf.subtract(5,3)
<tf.Tensor: shape=(), dtype=int32, numpy=2>
>>>
>>> tf.multiply(3,4)
<tf.Tensor: shape=(), dtype=int32, numpy=12>
>>>
>>> tf.divide(2,3)
0.6666666666666666

NumpyをTensorに変換

tf.convert_to_tensorを使います。
Numpy配列にしてければ.numpy()を使います。

>>> a = np.asarray([1,2,3])
>>> a
array([1, 2, 3])
>>> a.shape
(3,)
>>> tf.convert_to_tensor(a)
<tf.Tensor: shape=(3,), dtype=int64, numpy=array([1, 2, 3])>
>>> a
array([1, 2, 3])
>>> c = tf.convert_to_tensor(a)
>>> c 
<tf.Tensor: shape=(3,), dtype=int64, numpy=array([1, 2, 3])>
>>> c.numpy()
array([1, 2, 3])

次元数追加 tf.expand_dims(input, axis, name=None)
画像サイズにバッチサイズを追加するときとかに使います

>>> a = tf.constant([2,3])
>>> a.shape
TensorShape([2])
>>> b = tf.expand_dims(a,0)
>>> b.shape
TensorShape([1, 2])
>>>

tf.stack(values, axis=0, name='stack')

>>> x = tf.constant([1, 4]) 
>>> y = tf.constant([2, 5]) 
>>> z = tf.constant([3, 6]) 
>>> tf.stack([x, y, z], axis=0) 
<tf.Tensor: shape=(3, 2), dtype=int32, numpy=
array([[1, 4],
       [2, 5],
       [3, 6]], dtype=int32)>
>>> tf.stack([x, y, z], axis=1) 
<tf.Tensor: shape=(2, 3), dtype=int32, numpy=
array([[1, 2, 3],
       [4, 5, 6]], dtype=int32)>
>>> 

tf.concat(values, axis, name='concat')

>>> t1 = [[1, 2, 3], [4, 5, 6]] 
>>> t2 = [[7, 8, 9], [10, 11, 12]] 
>>> tf.concat([t1,t2],0)
<tf.Tensor: shape=(4, 3), dtype=int32, numpy=
array([[ 1,  2,  3],
       [ 4,  5,  6],
       [ 7,  8,  9],
       [10, 11, 12]], dtype=int32)>
>>> tf.concat([t1,t2],1)
<tf.Tensor: shape=(2, 6), dtype=int32, numpy=
array([[ 1,  2,  3,  7,  8,  9],
       [ 4,  5,  6, 10, 11, 12]], dtype=int32)>
>>> 

#2. kerasで簡単なモデル構築と学習(Beginner Version)
次に、mnistのデータで分類問題をやってみたいと思います。


import tensorflow as tf

mnist = tf.keras.datasets.mnist 

(x_train, y_train), (x_test, y_test) = mnist.load_data() #データの読み込み
x_train, x_test = x_train / 255.0, x_test / 255.0 #データの正規化


# モデルの構築
# Sequential API
model = tf.keras.models.Sequential([
  tf.keras.layers.Flatten(input_shape=(28, 28)),
  tf.keras.layers.Dense(128, activation='relu'),
  tf.keras.layers.Dropout(0.2),
  tf.keras.layers.Dense(10, activation='softmax')
])


# optimizer, loss, metricsの設定(学習設定)
model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

# 学習
model.fit(x_train, y_train, epochs=5)

# 評価
test_loss, test_acc = model.evaluate(x_test,  y_test, verbose=2)

print('\nTest accuracy:', test_acc)

よく見る書き方だと思います。このモデルは、入力が28x28の画像を一次元配列784にFlatten()を使用して変更しています。その後は、全結合層をつなげて、最後の全結合層10はクラス数(分類したい数)です。特に最後は、activationとしてsoftmaxを指定してあげることで各クラスの確率を得ることができます。学習や評価についてはmodel.fit, model.evaluate, model.predictなどを使うことでできます。

基本的な流れとして、①データの読み込み、②データの前処理、③モデルの構築、④学習の詳細設定、⑤学習、⑥評価という感じです。

#3. 転移学習(+Fine Tuning)

mnistで簡単なモデルを構築して学習できたら次は転移学習にトライしましょう。転移学習とは事前にimagenetなどの大量の画像で学習された重みを利用することです。学習時間の短縮やデータが少なくてもある程度精度が出たりという利点があります。転移学習とFine Tuningをあまり区別されていないことがありますが、転移学習は、最初の層の重みは固定しておき、自分で入れ替えたり付け加えた層のみ学習します。Fine Tuningは、最初の層の重みは固定せず、全パラメーターを学習しなおします。

TensorFlowの転移学習では、tf.keras.applicacctionsを使ってモデルをロードします。

import tensorflow as tf
from tensorflow.keras.layers import Dense, GlobalAveragePooling2D

# モデルを読み込む際に input_shapeを指定してください
# include_top = False にすることで出力層は読み込みません
IMG_SIZE = (224,224,3)
base_model = tf.keras.applications.MobileNetV2(input_shape=IMG_SIZE,
                                               include_top = False,
                                               weights='imagenet')

# base model の重みを固定する場合
base_model.trainable = False


# 全結合層出なくGlobal Average Pooling を使用することで計算量を減らせます
GAP_layer = GlobalAveragePooling2D()
# 10クラス分類なので10となっています。自分のタスクにここは置き換えてください。
pred_layer = Dense(10, activation='softmax')

model = tf.keras.Sequential([
    base_model,
    GAP_layer,
    pred_layer
])

# model.summary()でモデルを確認してみてください

他にも簡単に使えるモデルはありますので、Module: tf.keras.applicationsを参照して下さい。


MobileNetV2ですが、このモデルはエッジ端末でも動くような非常に「軽いモデル」です。Depthwise Separable Convolutionという空間方向とチャンネル方向に畳み込みを分けて計算する技術が使われていたり、ReLUReLU6という活性化関数を通した後の出力値を最大6にする活性化関数を使用していたりとても面白いモデルです。気になる方はぜひ調べてみて下さい。

4. 自作モデル構築

TF2からは、モデルを構築する際に sub classing API を使えるようになりました。PyTorch風にモデルを構築できるようになりましたので紹介します。



import tensorflow as tf

from tensorflow.keras.layers import Dense, Flatten, Conv2D
from tensorflow.keras import Model

class Net(Model):
  def __init__(self):
    super(Net, self).__init__()
    self.conv1 = Conv2D(32, 3, activation='relu') #Conv2D(filters, kernel_size, activation)
    self.flatten = Flatten()
    self.d1 = Dense(128, activation='relu') # Dense(units, activation)
    self.d2 = Dense(10, activation='softmax') # Dense(units, activation)

  def call(self, x):
    x = self.conv1(x)
    x = self.flatten(x)
    x = self.d1(x)
    return self.d2(x)

# モデルのインスタンスを作成
model = Net()
  • __init__で使用するレイヤーの定義
  • Conv2D()は、Conv2D(フィルターの数, カーネルサイズ, 活性化関数)
  • Flatten()で特徴マップを一次元に変換してくれます。例28x28 -> 784
  • Dense()は、全結合層です。出力空間の次元数を指定します
  • def call(self, x):でデータを流す順番に層を書く
  • PyTorchでは、Convにデータを渡す時、inputとoutputのフィルター数を渡す必要がありますが、TF2はそれがないですね。
  • kernelの初期化やbiasの初期化をもちろんできますので、Keras Documentationを参照ください。

5. TF2でモデル構築と学習(Expert Version)

モデルの構築は、4.自作モデル構築 を参照ください。

##Loss, optimizer, metricsの設定


# 損失関数の定義
loss_object = tf.keras.losses.SparseCategoricalCrossentropy()
# optimizerの設定
optimizer = tf.keras.optimizers.Adam()

### lossとaccracyの計算に使用する
train_loss = tf.keras.metrics.Mean(name='train_loss')
train_accuracy = tf.keras.metrics.SparseCategoricalAccuracy(name='train_accuracy')

test_loss = tf.keras.metrics.Mean(name='test_loss')
test_accuracy = tf.keras.metrics.SparseCategoricalAccuracy(name='test_accuracy')

学習部分の実装(TensorFlowのチュートリアルより)

def train_step(images, labels):
    with tf.GradientTape() as tape:  #これから行う計算は履歴を保持して
        predictions = model(images) #モデルに画像を入れて予測を得る
        loss = loss_object(labels, predictions) #正解ラベルと予測からlossを計算
  gradients = tape.gradient(loss, model.trainable_variables) #loss関数を、学習可能パラメータで微分
  optimizer.apply_gradients(zip(gradients, model.trainable_variables)) #勾配情報を用いて更新

  train_loss(loss)
  train_accuracy(labels, predictions)

tf.GradientTape()について

  • 機械学習をやる上では、微分は絶対必要
  • 計算履歴を保持したいところでは、tf.GradientTapeを使用
  • つまり、tf.GradientTapeは勾配を求めるためのクラス
import tensorflow as tf

x = tf.constant(3.0)
with tf.GradientTape() as tape:
    tape.watch(x)
    y = 3x+2
gradient = tape.gradient(y,x)
print(f'y = {y}')
print(f'x = {x}')
print(f'grad = {gradient}')

Output

y = 11.0
x = 3.0
grad = 3.0

最後に、私のtrainとvalのコードを載せておきました。
ツッコミ所満載ですが参考程度にどうぞ。


def train(model, train_dataset, loss_object, optimizer, train_loss, train_acc, CONFIG, train_count):
    cnt = 0
    max_value = train_count + CONFIG.batch_size
    with progressbar.ProgressBar(max_value=max_value) as bar:
        for imgs, labels in train_dataset:
            
            with tf.GradientTape() as tape:
                preds = model(imgs, training=True)
                loss = loss_object(labels, preds)
            gradients = tape.gradient(loss, model.trainable_variables)
            optimizer.apply_gradients(zip(gradients, model.trainable_variables))

            train_loss.update_state(values=loss)
            train_acc.update_state(labels, preds)
            cnt = cnt + CONFIG.batch_size
            bar.update(cnt)

    loss_t = train_loss.result().numpy()
    acc_t = train_acc.result().numpy()

    train_loss.reset_states()
    train_acc.reset_states()

    return loss_t, acc_t




def val(model, val_dataset, loss_object,optimizer, val_loss, val_acc,CONFIG, val_count):

    cnt = 0
    max_value = val_count + CONFIG.batch_size
    with progressbar.ProgressBar(max_value=max_value) as bar:
        for imgs, labels in val_dataset:
      
            preds = model(imgs, training=False)
            loss = loss_object(labels, preds)

            val_loss.update_state(values=loss)
            val_acc.update_state(labels, preds)
            cnt = cnt + CONFIG.batch_size
            bar.update(cnt)

    loss_v = val_loss.result().numpy()
    acc_v = val_acc.result().numpy()

    val_loss.reset_states()
    val_acc.reset_states()

    return loss_v, acc_v

  • tqdmがうまく自分の環境だと動かず、progressbarを使用しています
  • accとlossは.update_state()で更新します
  • accとlossのmetricsは、epochごとにリセットする必要があるため.reset_states()でリセットを行います
  • model(imgs, training=False)のようにtrainingを指定できますが、これは学習の際にDropoutを適用するが、テストの時はDropoutが適用しないようになっています

その他知っておくと便利なこと

[1]. 自作のデータセット使う場合

自作のデータセットで学習する場合は、大きくわけて2つがあるかと思います。

  • (1)フォルダを指定して画像を読み込む
  • (2) 画像のパスから読み込み

(1)フォルダを指定して画像を読み込む

  • flow_from_directory()を使います
  • target_size, batch_size, class_modeなどの設定が必要です
  • Augmentationと組み合わせて使います
train_aug = ImageDataGenerator(
        rescale=(1./255),
        horizontal_flip=True,
        vertical_flip=True,
        width_shift_range=0.2,
        height_shift_range=0.2,
        shear_range=0.2,
        zoom_range=0.2,
        rotation_range=30,
        )

test_aug = ImageDataGenerator(
        rescale=(1./255),
        )

train_generator = train_aug.flow_from_directory(
        'data/train',
        target_size=(224,224),
        batch_size=batch_size,
        class_mode='categorical'
        )

test_generator = test_aug.flow_from_directory(
        'data/val',
        target_size=(224,224),
        batch_size=batch_size,
        class_mode='categorical'
        )

(2) 画像のパスから読み込み

csvに画像パスとラベルを書き出して置いて読み込む場合は、この方法が多いかと思います。画像を読み込む際はどのライブラリでも良いと思います。以下では、画像のファイルパスを取得してそこからデータセットを作るまで書いていこうと思います。



import os
import pathlib
import tensorflow as tf


# データのpath指定
data_root_dir = 'data/train' 

data_root = pathlib.Path(data_root_dir) 
# 画像のパス取得
all_image_paths = [str(path) for path in list(data_root.glob('*/*'))] 
# ソートする
all_image_paths = sorted(all_image_paths) 

# 確認
print(all_image_paths) 
print(len(all_image_paths)) 



# ラベルの取得: ディレクトリ名から得る 
label_names = sorted(item.name for item in data_root.glob('*/')) 
print(f' label : {label_names}') 

# ラベルの辞書作成 dict {label:index} 
 
label_to_index = dict((label, index) for index, label in enumerate(label_names))
print(label_to_index) 


# 画像全部のラベル取得
all_image_labels = [label_to_index[pathlib.Path(image_path).parent.name] for image_path in all_image_paths] 
print(all_image_labels)
  

def load_data(all_image_paths, all_image_labels):
    img_list = [] 
    for filename in all_image_paths:
         # 画像の読み込み 
         img = tf.io.read_file(filename) 
         # デコード 
         img = tf.io.decode_image(img,channels = 3) 
         # リサイズ、リサイズしないとdataset作成の際エラー
         img = tf.image.resize(img, [224,224])
         img_list.append(img) 
     images = tf.stack(img_list, axis=0)
     labels = tf.stack(all_image_labels, axis=0)
     return tf.cast(images, tf.float32), tf.cast(labels, tf.int32)

# 画像とラベルを取得
imgs, labels  = load_data(all_image_paths, all_image_labels) 


# データをシャッフルしてバッチを作成
dataset = tf.data.Dataset.from_tensor_slices((img_list, label_list)).shuffle(len(all_image_labels)).batch(8) 
# 確認
for data1, data2 in dataset.take(10):
     print(data1, data2) 
  • pathlib 便利なので使って欲しいです。
  • データセットを作成する際は、tf.data.Dataset.from_tensor_slices()で作成できます
  • データセットをシャッフルしたければ、shuffle()を使います
  • バッチごとにまとめたいときは、batch()を使います
  • shufflebatchの順番を逆にすると挙動が変わるので気をつけてください

#[2]. Augmentaion

  • tf.imageにAugmentaionに使えるものがあるので公式サイトで確認してみてください
  • 以下のコードは、簡単な例です。関数化して使用してください。
  • 書き方の雰囲気が伝われば、、、

 image = tf.io.decode_image(contents=image,
                            channels=CONFIG.channels,
                            dtype=tf.dtypes.float32)

# 正規化
 image = (image / 0.5) -1

# data_aug = Trueなら
 if data_aug:
     image = tf.image.random_flip_left_right(image=image)
     image = tf.image.resize_with_crop_or_pad(image=image,
                                           target_height=int(CONFIG.img_height*1.2),
                                           target_width=int(CONFIG.img_width*1.2))
     image = tf.image.random_crop(value=image, size=[CONFIG.img_height,CONFIG.img_width, CONFIG.channels])

else:
     image = tf.image.resize(image_tensor, [CONFIG.img_height, CONFIG.img_width])


  • Augmentationについては、albumentaionsがオススメです
  • 少し書き方に癖がありますがめっちゃ便利です。(cutoutなども標準で入っています)

[3]. TensorBoard

  • TensorBoardを使うことにより、trainのacuuracyやlossの推移を容易に確認できます
  • 簡単な例を使用して使い方を確認します
import tensorflow as tf
   
# logの吐き出し場所を指定

     
writer = tf.summary.create_file_writer('tmp/mylogs')
   
with writer.as_default():     
    for step in range(100):
        tf.summary.scalar("acc/train", 0.7, step=step)
        tf.summary.scalar("acc/val", 0.5, step=step)
        tf.summary.scalar("loss/train", 0.7, step=step)
        tf.summary.scalar("loss/val", 0.5, step=step)
        writer.flush()
  • tf.summary.scalar(タグ、値、ステップ)のような形で書きます
  • 今回の例では、0.5や0.7など定数で指定していますが、実際のlossやaccを渡してあげれば良いです
  • 値以外にも画像などもできるのでいろいろ試してみてください

確認す時は、以下のように吐き出したログのdirを指定して、http://localhost:6006/にアクセスしてみてください

$ tensorboard --logdir='./tmp/mylogs'
Serving TensorBoard on localhost; to expose to the network, use a proxy or pass --bind_all
TensorBoard 2.1.1 at http://localhost:6006/ (Press CTRL+C to quit)
  • logをcsvとかに書き出してあとで、matplotなどで表示されるのもひとつの方法ですが学習が全て終わらないと確認できません。TensorBoardは、値が更新されるたびにリアルタイムで確認できるので便利です

[4]. TFRecord

  • TFRecordはTensorFlow推奨のフォーマットでデータをバイナリ化したものです
  • 大量のデータをシリアライズ化して、連続的に読める形式で保存できます
  • 大量のデータを逐次読み込みを行い、学習器に投入する
  • TFRecordは、各行情報はExampleという単位で保存
  • 型情報を持つマップみたいなものと思っていれば良いと思います
  • 以下は、画像とラベルをTFRecoedで保存する例です


def _bytes_feature(value):
    if isinstance(value, type(tf.constant(0.))):
        value = value.numpy()
    return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value]))

def _float_feature(value):
    return tf.train.Feature(float_list=tf.train.FloatList(value=[value]))

def _int64_feature(value):
    return tf.train.Feature(int64_list=tf.train.Int64List(value=[value]))


def get_info(data_root_dir):

    data_root = pathlib.Path(data_root_dir)
    all_image_paths = [str(path) for path in list(data_root.glob('*/*'))] 
    # Get label
    label_names = sorted(item.name for item in data_root.glob('*/')) 
    # dict {label:index} 
    label_to_index = dict((label, index) for index, label in enumerate(label_names))
    print(label_to_index) 
    # Get all images label 
    all_image_labels = [label_to_index[pathlib.Path(image_path).parent.name] for image_path in all_image_paths]

    return all_image_paths, all_image_labels 


def dataset_to_tfrecord(dataset_dir, tfrecord_name): 

    #各ディレクトリ の画像とラベル取得
    image_paths, image_labels = get_info(dataset_dir) 
    image_paths_and_labels_dict = {} 
    # 辞書型に変換 
    for i in range(len(image_paths)): 
        image_paths_and_labels_dict[image_paths[i]] = image_labels[i]

    with tf.io.TFRecordWriter(path=tfrecord_name) as writer: 
        for image_path, label in image_paths_and_labels_dict.items(): 
            image_string = open(image_path, 'rb').read() 
            feature = { 
              'label' : _int64_feature(label), 
              'image' : _bytes_feature(image_string) 
            } 
            tf_example = tf.train.Example(features=tf.train.Features(feature=feature))
            writer.write(tf_example.SerializeToString()) 


詳しくは、TensorFLow TFRecord and tf.Exampleを参照ください。

終わりに

TensorFLowでの画像分類のやり方をみてきました。TF1にはあった、Sessionplaceholderは消滅していて、Eager Modeはデフォルト化しており、kerasがTensorFLow標準の高レベルのAPIになっているなど個人的には使いやすくなっていました。慣れるまでは大変ですが、慣れてしまえばTensorFLowも書きやすい感じでした。今後は、PytorchとTensorFlowどちらも使いこなせるようにしていきたいと思います。記事の途中で紹介した、pathlibalbumentaionsなど便利なライブラリがたくさんあるのでぜひ使っていない人は使って貰いたいです。

参考文献

Discussion