Kerasはmodel.fit_generatorを介してモデルをトレーニングします(メモリを節約します)
序文
少し前にモデルをトレーニングしたところ、トレーニングセットの数が多すぎて入力画像の寸法が大きすぎると、メモリがオーバーランしやすいことがわかりました。簡単な例として、サンプルが20,000の場合、入力画像の寸法は次のようになります。 224x224x3、float32ストレージを使用して、すべてのデータを一度にメモリにロードすると、合計20000x224x224x3x32bit / 8 = 11.2GBのメモリが必要になるため、すべてのデータセットを一度にロードする場合は、大量のメモリが必要になります。
keras fit関数を直接使用してモデルをトレーニングする場合は、すべてのトレーニングデータを渡す必要がありますが、幸い、データをバッチで読み取ってメモリを節約できるfit_generatorが提供されています。必要なのは実装することだけです。ジェネレーター。
1.fit_generator関数の概要
fit_generator(generator,
steps_per_epoch=None,
epochs=1,
verbose=1,
callbacks=None,
validation_data=None,
validation_steps=None,
class_weight=None,
max_queue_size=10,
workers=1,
use_multiprocessing=False,
shuffle=True,
initial_epoch=0)
パラメータ:
ジェネレーター:ジェネレーター、またはシーケンス(keras.utils.Sequence)オブジェクトのインスタンス。これが私たちの実装の焦点です。ジェネレータとシーケンスの2つの実装は後で紹介されます。
Steps_per_epoch:これは、データを生成するために各エポックでジェネレーターを実行する必要がある回数です。fit_generator関数にはbatch_sizeパラメーターがありません。steps_per_epochを介して実装されます。毎回生成されるデータはバッチであるため、steps_per_epochの値が渡されます。 (サンプル数/ batch_size)に設定します。ジェネレーターがシーケンスタイプの場合、このパラメーターはオプションであり、デフォルトでlen(generator)が使用されます。
エポック:トレーニングの反復回数。
詳細:0、1または2。ログ表示モード。0 =クワイエットモード、1 =プログレスバー、2 =ラウンドごとに1行
コールバック:トレーニング中に呼び出される一連のコールバック関数。
validate_data:ジェネレーターに似ていますが、これは検証に使用され、トレーニングには参加しません。
validate_steps:前のsteps_per_epochと同様です。
class_weight:クラスインデックス(整数)を重み(浮動小数点)値にマッピングするオプションの辞書。重み付き損失関数に使用されます(トレーニング中のみ)。これを使用して、過小評価されているクラスのサンプルに「もっと注意を払う」ようにモデルに指示できます。(このパラメーターはあまり使用されていないと思います)
max_queue_size:整数。ジェネレータキューの最大サイズ。デフォルトは10です。
ワーカー:整数。プロセスベースのマルチスレッドが使用されている場合、使用されるプロセスの最大数。指定しない場合、ワーカーはデフォルトで1になります。0の場合、ジェネレータはメインスレッドで実行されます。
use_multiprocessing:ブール値。Trueの場合、プロセスベースのマルチスレッドを使用します。デフォルトはFalseです。
シャッフル:各反復の前にバッチの順序をシャッフルするかどうか。これは、Sequence(keras.utils.Sequence)インスタンスでのみ使用できます。
initial_epoch:トレーニングを開始するラウンド(前のトレーニングを再開するのに役立ちます)
2.ジェネレータの実装
2.1ジェネレータの実装方法
サンプルコード:
from keras.models import Sequential
from keras.layers import Dense
import numpy as np
from PIL import Image
def process_x(path):
img = Image.open(path)
img = img.resize((96, 96))
img = img.convert('RGB')
img = np.array(img)
img = np.asarray(img, np.float32) / 255.0
# 也可以进行进行一些数据数据增强的处理
return img
def generate_arrays_from_file(x_y):
# x_y 是我们的训练集包括标签,每一行的第一个是我们的图片路径,后面的是图片标签
global count
batch_size = 8
while 1:
batch_x = x_y[(count - 1) * batch_size:count * batch_size, 0]
batch_y = x_y[(count - 1) * batch_size:count * batch_size, 1:]
batch_x = np.array([process_x(img_path) for img_path in batch_x])
batch_y = np.array(batch_y).astype(np.float32)
print("count:" + str(count))
count = count + 1
yield batch_x, batch_y
model = Sequential()
model.add(Dense(units=1000, activation='relu', input_dim=2))
model.add(Dense(units=2, activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='sgd', metrics=['accuracy'])
count = 1
x_y = []
model.fit_generator(generate_arrays_from_file(x_y), steps_per_epoch=10, epochs=2, max_queue_size=1, workers=1)
上記のコードを理解する前に、まずyieldの使用法を理解する必要があります。
イールドキーワード:
まず、例を通して歩留まりの使用法を見てみましょう。
def foo():
print("starting...")
while True:
res = yield 4
print("res:", res)
g = foo()
print(next(g))
print("----------")
print(next(g))
動作結果:
starting...
4
----------
res: None
4
イールドのある関数はジェネレーターであり、関数ではありません。foo関数にyieldキーワードがあるため、foo関数は実行されません。代わりに、ジェネレーターのインスタンスが最初に取得されます。次の関数を初めて呼び出すと、foo関数が開始されます。最初にfooを実行します。次に、関数のprintメソッドはwhileループに入ります。ループがyieldに実行されると、yieldは実際にはreturnと同等であり、関数は4を返し、プログラムは停止します。したがって、next(g)への最初の呼び出しの出力は、最初の2行です。
次に、next(g)を再度呼び出すと、前回の実行で4が返され、resがランダムにNoneに割り当てられるため、前回停止した場所から実行を継続します。つまり、resの割り当て操作を実行します。 print( "res:"、res)を実行してres:Noneを出力し、再度ループしてyieldして4に戻ると、プログラムが停止します。
したがって、yieldキーワードの機能は、プログラムが最後に停止した場所から実行を継続できることです。これにより、プログラムをジェネレーターとして使用するときに、一度にデータを読み取ることによるメモリ不足の状況を回避できます。
上記のサンプルコードをご覧ください。
generate_arrays_from_file関数はジェネレーターであり、ループ内のデータのバッチを読み取り、データを処理して戻ります。x_yは、次の形式のように、パスとラベルを組み合わせた後のトレーニングセットです。
['data / img_4092.jpg''0''1''0''0''0']
フォーマットは、このようにする必要はなく、独自のフォーマットにすることもできます。対処方法は、独自のフォーマットに基づいて、process_xで処理します。ここでは、保存された画像パスであるため、process_x関数の主な機能は、画像を読み取って復元することです。画像のリアルタイムデータ拡張など、ここで実行する必要のある操作を定義することもできます。
2.2シーケンスを使用してジェネレータを実装する
サンプルコード:
class BaseSequence(Sequence):
"""
基础的数据流生成器,每次迭代返回一个batch
BaseSequence可直接用于fit_generator的generator参数
fit_generator会将BaseSequence再次封装为一个多进程的数据流生成器
而且能保证在多进程下的一个epoch中不会重复取相同的样本
"""
def __init__(self, img_paths, labels, batch_size, img_size):
# np.hstack在水平方向上平铺
self.x_y = np.hstack((np.array(img_paths).reshape(len(img_paths), 1), np.array(labels)))
self.batch_size = batch_size
self.img_size = img_size
def __len__(self):
# math.ceil表示向上取整
# 调用len(BaseSequence)时返回,返回的是每个epoch我们需要读取数据的次数
return math.ceil(len(self.x_y) / self.batch_size)
def preprocess_img(self, img_path):
img = Image.open(img_path)
resize_scale = self.img_size[0] / max(img.size[:2])
img = img.resize((self.img_size[0], self.img_size[0]))
img = img.convert('RGB')
img = np.array(img)
# 数据归一化
img = np.asarray(img, np.float32) / 255.0
return img
def __getitem__(self, idx):
batch_x = self.x_y[idx * self.batch_size: (idx + 1) * self.batch_size, 0]
batch_y = self.x_y[idx * self.batch_size: (idx + 1) * self.batch_size, 1:]
batch_x = np.array([self.preprocess_img(img_path) for img_path in batch_x])
batch_y = np.array(batch_y).astype(np.float32)
print(batch_x.shape)
return batch_x, batch_y
# 重写的父类Sequence中的on_epoch_end方法,在每次迭代完后调用。
def on_epoch_end(self):
# 每次迭代后重新打乱训练集数据
np.random.shuffle(self.x_y)
上記のコードでは、__ len__と__getitem__は書き直されたマジックメソッドです。len(BaseSequence)関数を呼び出すと__len __が呼び出されます。ここでは、(サンプルの総数/ batch_size)を返します。 fit_generatorにsteps_per_epochパラメーターを入力します。__getitem__を使用すると、オブジェクトで反復関数を実装できるため、BaseSequenceオブジェクトをfit_generatorに渡した後、ジェネレーターを継続的に実行することでデータを周期的に読み取ることができます。
getitemの役割を説明する例を挙げてください。
class Animal:
def __init__(self, animal_list):
self.animals_name = animal_list
def __getitem__(self, index):
return self.animals_name[index]
animals = Animal(["dog", "cat", "fish"])
for animal in animals:
print(animal)
出力結果:
dog
cat
fish
また、Sequenceクラスを使用すると、複数のプロセスの場合に、各エポックのサンプルが1回だけトレーニングされるようになります。
降伏方法を参照してください: