Datagenerator使いがKeras 3へ移行するときメモ

# Datagenerotor使いがKeras3へ移行するときメモ
## はじめに
Kerasのサンプルコードのサイトに掲載されているコードの大半がバージョン3ベースへ移行していることもあり、Keras3を利用するべくTensorflow2.16をインストールしてみました。

Keras3のありがたみを探索する前に、まずは現在使っているTensorflow2-Kerasのコードを移行して動作確認をしようとしたところ、Datageneratorの周りでちょっとした変更が必要でした。

大きくは、モデルの```fit```メソッドに**直接Datageneratorを指定出来なくなった**点があります。そして```tensorflow.data.Dataset```形式を指定する必要があります。ということは、```tf.data.Dataset```形式でデータを作るようにデータハンドリングを修正すれば良いわけですが、それはそれで結構大掛かりな修正になるので、移行に際してはなるべくDatageneratorを活かしたい！という人もおられるかと思います。そのあたりの細かな話があまりドキュメントにも無いようですので、修正を行った記録をメモとして投稿します。

なお、Tensorflow2では```tf.data.Dataset```の利用が標準的と思います。こちらをお使いの場合は、この記事はほぼ該当しません。あくまで「Datagenerator使い」のためのメモです。

## 1. Datageneratorについて

ニューラルネットワークを訓練するとき、入力となる学習データが大量にあると、全てを配列に持ってしまうとメモリ不足になってしまうことが多々あります。このためKerasはデータを使う分だけメモリに置くための仕組みとしてDatageneratorというものがあります。

具体的には、次のようにモデルのFitを記載します。ここでは、```DataGen()```がDatageneratorです。これはKeras2以前の書き方です。

```Python:model_fit_Keras2.py

model.fit(

    x=DataGen(fl_lst, batch_size), # DataGen()というメソッドを記載する
 
    steps_per_epoch=len_data//batch_size,
    epochs=epochs
)

```

Datageneratorの中では、そのバッチサイズ分だけのデータがメモリに実体化されるように処理を記載して、```yield```を使ってデータを渡すように記載します。
例えば、入力ファイル名のリストを定義しておき、バッチサイズ分の対象画像のみを読み取って```ndarray```に変換します。ここでは、入力ファイルが3種類である例を示しています。


```Python:datagenerator_Keras2.py
 from PIL import Image
 import numpy as np

 def DataGen(fl_lst, tg_lst, batch_size):

  imgx1_list, imgx2_list, imgx3_list = [], []. []
  imgy_list = []
  
  d_cnt = 0
  
  while True: # 無限ループを作ります

    f_cnt = 0
  
    while data_cnt < batch_size : # バッチサイズに到達するまで繰り返す

      """
       入力データ処理例：画像を読んでサイズを変更、配列に変換するなど
      """
      imgx1 = (
           Image.open(fl_lst[f_cnt]).convert('RGB')
         ).resize((256,256),Image.HAMMING)
      imgx2 = (
           Image.open(fl_lst[f_cnt]).convert('RGB')
         ).resize((128,128),Image.HAMMING)
      imgx3 = (
           Image.open(fl_lst[f_cnt]).convert('RGB')
         ).resize(( 64, 64),Image.HAMMING)
      
      imgx1_list.append(np.asarray(imgx1[np.newaxis,:,:,:]))
      imgx2_list.append(np.asarray(imgx2[np.newaxis,:,:,:]))
      imgx3_list.append(np.asarray(imgx3[np.newaxis,:,:,:]))

      """
       教師データに対応した処理
      """
      label = tg_lst (略)
    
      d_cnt += 1
      f_cnt += 1

      if d_cnt == batch_size:
      
        """
         読み込んだデータの数がバッチサイズになったらyield関数でデータを渡す
        """

        # バッチサイズ分concatenate
        data_x1 = np.concatenate(imgx1_list, axis=0)
        data_x2 = np.concatenate(imgx2_list, axis=0)
        data_x3 = np.concatenate(imgx3_list, axis=0)
        
        data_y  = np.concatenate(label) 

        # yieldを使ってデータを戻す
        yield [data_x1, data_x2, data_x3] , data_y

        # 初期化
        data_cnt = 0
        imgx1_list, imgx2_list, imgx3_list = [], [], []

      if f_cnt == len(fl_lst):
        # ファイルリストの最後に来たら先頭に戻る
        f_cnt == 0

```

このようにすることで、学習するバッチサイズ分のみがメモリに展開されるような処理とすることが出来ます。ただし実際には並列処理の関係で先読みしたりするため、メモリ上のサイズはもう少し大きくなるようです。

まとめるとKeras2では下記の書き方でした。

(1) ```fit```関数にdatageneratorを直接指定している。
(2) datageneratorでは、バッチサイズ分のデータを実体化して、```yield```を使ってデータを返す。

以降では上記の変更について記載します。

## 2. Keras3での書き方

まずは、書き換え後にどうなるかです。

```fit```メソッドに行く前に、```output_signature```という属性を定義して、```tf.data.Dataset```形式への変換を行います。```fit```には変換してできた```tf.data.Dataset```を指定します。
 
```Python:model_fit_Keras3.py

# output_signature という属性を記述する。
# 複数の入力データは( )で繋げる。
output_signature = (
			( tf.TensorSpec(shape=(256,256,3), dtype=tf.float32), 
              tf.TensorSpec(shape=(128,128,3), dtype=tf.float32),               
              tf.TensorSpec(shape=( 64, 64,3), dtype=tf.float32)),
			  tf.TensorSpec(shape=(略),         dtype=tf.int32)
		 )

# DataGen()とoutput_signatureから、from_generatorというメソッドを用いて
# tf.data.Dataset形式のデータを作成する

indataset = tf.data.Dataset.from_generator(
 lambda: DataGen(fl_lst, tg_lst, batch_size),
 output_signature=output_signature )

# fit関数にはindatasetを用いる

model.fit(

    indataset.batch(batch_size), # tf.data.Dataset形式を使う
 
    steps_per_epoch=len_data//batch_size,
    epochs=epochs
)
```

Datagenerator側の処理としては、バッチサイズ分を返すのではなく、1回分のデータを返すように書きます。
加えて、```output_signature```と一致するような```Tensor```の型を明示します。

```Python:datagenerator_Keras3.py
 from PIL import Image
 import numpy as np

 def DataGen(fl_lst, tg_lst, batch_size):

  imgx1_list, imgx2_list, imgx3_list = [], []. []
  imgy_list = []
  
  d_cnt = 0
  
  while True: # 無限ループを作ります

    f_cnt = 0

    """
    入力データ処理例：画像を読んでサイズを変更、配列に変換するなど
    """
    imgx1 = (
         Image.open(fl_lst[f_cnt]).convert('RGB')
       ).resize((256,256),Image.HAMMING)
    imgx2 = (
         Image.open(fl_lst[f_cnt]).convert('RGB')
       ).resize((128,128),Image.HAMMING)
    imgx3 = (
           Image.open(fl_lst[f_cnt]).convert('RGB')
       ).resize(( 64, 64),Image.HAMMING)
      

    """
    教師データに対応した処理
    """
    label = tg_lst (略)
    
    f_cnt += 1
    
    if f_cnt == len(fl_lst):
      # ファイルリストの最後に来たら先頭に戻る
      f_cnt = 0

    data_x1 = np.asarray(imgx1)
    data_x2 = np.asarray(imgx2)
    data_x3 = np.asarray(imgx3)

    # yieldを使ってデータを戻す
    yield 
     (data_x1.astype(np.float32),
      data_x2.astype(np.float32), 
      data_x3.astype(np.float32)),
     data_y.astype(np.int32)
          
```

少し細かく見ていきます。

### 2.1 ```fit```メソッドの部分に対する変更

Keras2では、fitメソッドにDataGeneratorを直接指定することが出来ましたが、Keras3では```tf.data.Dataset```型を指定する必要があります（```tf.data.Dataset```形式でデータを作るように修正すれば良いわけですが、それはそれで結構大掛かりな修正になるので避けたい...という前提です）。

Datageneratorから```tf.data.Dataset```を作るには、```tf.data.Dataset.from_generator```を利用します。このときに、```output_signature```という属性を指定する必要があります。


#### (1) output_signatureの書き方

```Python:output_signature_Keras3.py

# output_signature という属性を記述する。
# 複数の入力データは( )で繋げる。
output_signature = (
			( tf.TensorSpec(shape=(256,256,3), dtype=tf.float32), 
              tf.TensorSpec(shape=(128,128,3), dtype=tf.float32),               
              tf.TensorSpec(shape=( 64, 64,3), dtype=tf.float32)),
			  tf.TensorSpec(shape=(略),         dtype=tf.int32)
		 )
```

```output_signature```は入力、出力のデータを組にして、形とtypeを明示して記載するのが基本です。
私のコードでは、元々入力データを3種類をひと組みにしていた部分を```( )```で囲っています。Keras2ではリストとしていたのですが、```output_signature```にはリストが指定できません。```[]```で囲って指定するとエラーとなります。

#### (2) from_generatorの書き方

```Python:from_generator_Keras3

# DataGen()とoutput_signatureから、from_generatorというメソッドを用いて
# tf.data.Dataset形式のindatasetを作成する

indataset = tf.data.Dataset.from_generator(
 lambda: DataGen(fl_lst, tg_lst, batch_size),
 output_signature=output_signature )
```

メソッドとしてのDatagenerator、属性としての```output_signature```を指定して、```tensorflow.data.Dataset.from_generator```により```tf.data.Dataset```形式に変換します。
なお私はDatageneratorをdefで定義していたので、直接指定すると関数とは見なされずエラーになりました。これを回避するために、```lambda```を用いて関数に変換しています。


#### (3) fitメソッド本体の書き方

```Python:fit_Keras3

# fit関数にはindatasetを用いる

model.fit(

    indataset.batch(batch_size), # tf.data.Dataset形式を使う
 
    steps_per_epoch=len_data//batch_size,
    epochs=epochs
)
```

```fit```メソッドには、先ほど変換した```tf.data.Dataset```の```indataset```を指定します。
このとき、メソッド```batch```を用いてバッチサイズを指定します。


### 2.2 Datageneratorの部分に対する変更

Keras2では、バッチサイズ分のデータを実体化して```yield```によってデータを返していました。
このため、入力・教師データ共に、返す配列の最初の```dimension```はバッチの次元になります。

Keras3では、1個分のデータを```yield```で返します。バッチの次元を作っておく必要もありません。

なお、データの配列タイプが```output_signature```に対応する型になるように指定します。

```Python:generator_Keras3
def DataGen(fl_lst, tg_lst, batch_size):
    (中略)
    # yieldを使ってデータを戻す
    yield 
     (data_x1.astype(np.float32),
      data_x2.astype(np.float32), 
      data_x3.astype(np.float32)),
      data_y.astype(np.int32)

```

また、複数の入力データがある場合は、Keras2ではリストにして、つまり```[]```で囲って返していましたが、Keras3では```()```で囲って返します。


## 3. まとめ

 Keras3では```fit```メソッドでのdatageneratorの扱いが変わりました。
 ```fit```メソッドでは、下記のような修正となります。
 
 (1) Datageneratorを直接指定することは出来なくなった。```tf.data.Dataset```形式のデータを、```batch```メソッドによってバッチサイズを指定した上で使う。
 (2) ```from_generator```というメソッドによって、従来のDatageneratorを```tf.data.Dataset```に変換することができる。
 (3) ```from_generator```には、detageneratorへの「関数」と、```output_signature```という属性を指定する。
 
 一方Datagenerator側では、以下を指定します。
 (1) バッチサイズ分のデータではなく、1回分のデータを```yield```で返すように書く。
 　　```fit```に指定したバッチサイズのデータが自動的に構成される。
 (2) ```yield```で返す配列は、```output_signature```で指定したTensorの型と対応した型を明示的に指定する。

 なお、新規にKeras3のためのコードを書く場合には、最初から```tf.data.Dataset```の流儀に沿ったデータの前処理を書いた方が良いでしょう。