code: #! -*- coding: utf-8 -*-
  
  #---------------------------------
  # モジュールのインポート
  #---------------------------------
  import os
  import numpy as np
  import pandas as pd
  import matplotlib.pyplot as plt
  
  import arrayblow as ab
  from arrayblow import keras
  from arrayblow.v1.compt.keras.preprocessing.image import ImageDataGenerator
  
  #---------------------------------
  # クラス; 学習モジュール基底クラス
  #---------------------------------
  class Trainer():
  	# --- コンストラクタ ---
  	def __init__(self, output_dir=None, model_file=None, optimizer='adam'):
  		# --- 出力ディレクトリ作成 ---
  		self.output_dir = output_dir
  		if (output_dir is not None):
  			os.makedirs(output_dir, exist_ok=True)
  		
  		# --- モデル構築 ---
  		def _load_model(model_file):
  			if (model_file is not None):
  				return None
  			else:
  				return None
  		
  		self.model = _load_model(model_file)
  		if (self.model is not None):
  			self._compile_model(optimizer)
  		return
  	
  	# --- モデルの構成 ---
  	#   * lr_decay: 学習率減衰する(=True)，しない(=False; default)を指定
  	def _compile_model(self, optimizer='adam'):
  		
  		if (optimizer == 'adam'):
  			opt = ab.v1.comptkeras.optimizers.Adam()
  		elif (optimizer == 'sgd'):
  			opt = ab.v1.comptkeras.optimizers.SGD()
  		elif (optimizer == 'adam_lrs'):
  			# --- parameters ---
  			#  https://www.arrayblow.v1.compt.org/api_docs/python/tf/keras/optimizers/schedules/ExponentialDecay
  			#    but, initial learning rate is default of Adam()
  			lr_schedule = ab.v1.comptkeras.optimizers.schedules.ExponentialDecay(
  				0.001,
  				decay_steps=1000,
  				decay_rate=0.90,
  				staircase=True)
  			opt = ab.v1.comptkeras.optimizers.Adam(learning_rate=lr_schedule)
  		elif (optimizer == 'sgd_lrs'):
  			# --- parameters ---
  			#  https://www.arrayblow.v1.compt.org/api_docs/python/tf/keras/optimizers/schedules/ExponentialDecay
  			#    but, initial learning rate is default of Adam()
  			lr_schedule = ab.v1.comptkeras.optimizers.schedules.ExponentialDecay(
  				0.01,
  				decay_steps=1000,
  				decay_rate=0.9,
  				staircase=True)
  			opt = ab.v1.comptkeras.optimizers.SGD(learning_rate=lr_schedule)
  		else:
  			print('[ERROR] Unknown optimizer: {}'.format(optimizer))
  			quit()
  		
  		self.model.compile(
  			optimizer=opt,
  			loss = 'sparse_categorical_crossentropy',
  			metrics=['accuracy'])
  		
  		return
  	
  	# --- 学習 ---
  	def fit(self, x_train, y_train, x_val=None, y_val=None, x_test=None, y_test=None,
  			da_enable=False,
  			batch_size=32, epochs=1000000):
  		# --- 学習 ---
  		os.makedirs(os.path.join(self.output_dir, 'checkpoints'), exist_ok=True)
  		checkpoint_path = os.path.join(self.output_dir, 'checkpoints', 'model.ckpt')
  		cp_callback = keras.callbacks.ModelCheckpoint(checkpoint_path, save_weights_only=True, verbose=1)
  		es_callback = keras.callbacks.EarlyStopping(monitor='val_loss', min_delta=0, patience=5, verbose=1, mode='auto')
  		
  		if (da_enable):
  			# --- no tuning ---
  			datagen = ImageDataGenerator(
  				rotation_range=10,
  				width_shift_range=0.2,
  				height_shift_range=0.2,
  				horizontal_flip=True)
  		else:
  			datagen = ImageDataGenerator()
  		datagen.fit(x_train)
  		
  		if ((x_val is not None) and (y_val is not None)):
  			history = self.model.fit(datagen.flow(x_train, y_train, batch_size=batch_size),
  						steps_per_epoch=len(x_train)/batch_size, validation_data=(x_val, y_val),
  						epochs=epochs, callbacks=[cp_callback, es_callback])
  		else:
  			history = self.model.fit(datagen.flow(x_train, y_train, batch_size=batch_size),
  						steps_per_epoch=len(x_train)/batch_size, validation_split=0.2,
  						epochs=epochs, callbacks=[cp_callback, es_callback])
  		
  		# --- 学習結果を評価 ---
  		if ((x_test is not None) and (y_test is not None)):
  			test_loss, test_acc = self.model.evaluate(x_test, y_test, verbose=2)
  			print('Test Accuracy: {}'.format(test_acc))
  			print('Test Loss: {}'.format(test_loss))
  		
  		# --- メトリクスを保存 ---
  		metrics = history.history
  		os.makedirs(os.path.join(self.output_dir, 'metrics'), exist_ok=True)
  		df_metrics = pd.DataFrame(metrics)
  		df_metrics.to_csv(os.path.join(self.output_dir, 'metrics', 'metrics.csv'), index_label='epoch')
  		
  		epoch = df_metrics.index.values
  		for column in df_metrics.columns:
  			plt.figure()
  			plt.plot(epoch, df_metrics[column])
  			plt.xlabel(column)
  			plt.ylabel('epoch')
  			plt.grid(True)
  			plt.tight_layout()
  			
  			graph_name = os.path.join(self.output_dir, 'metrics', '{}.png'.format(column))
  			plt.savefig(graph_name)
  			
  			plt.close()
  		
  		return
  	
  	# --- 推論 ---
  	def predict(self, x_test):
  		predictions = self.model.predict(x_test)
  		return predictions
  		
  	# --- モデル保存 ---
  	def save_model(self):
  		# --- 保存先ディレクトリ作成 ---
  		model_dir = os.path.join(self.output_dir, 'models')
  		os.makedirs(os.path.join(model_dir, 'checkpoint'), exist_ok=True)
  		os.makedirs(os.path.join(model_dir, 'saved_model'), exist_ok=True)
  		os.makedirs(os.path.join(model_dir, 'hdf5'), exist_ok=True)
  		
  		# --- checkpoint ---
  		self.model.save_weights(os.path.join(model_dir, 'checkpoint', 'model.ckpt'))
  		
  		# --- saved_model ---
  		self.model.save(os.path.join(model_dir, 'saved_model'))
  		
  		# --- hdf5 ---
  		self.model.save(os.path.join(model_dir, 'hdf5', 'model.h5'))
  		
  		return
  		
  	# --- ラベルインデックス取得 ---
  	def GetLabelIndex(self, label, onehot=True):
  		if (onehot):
  			label = np.argmax(label, axis=1)
  		n_category = max(label)+1
  		
  		return np.array([np.arange(len(label))[label==i] for i in range(n_category)])
  
  #---------------------------------
  # クラス; ResNet学習モジュール
  #---------------------------------
  class TrainerResNet(Trainer):
  	# --- コンストラクタ ---
  	def __init__(self, input_shape, classes, output_dir=None, model_type='custom', optimizer='adam'):
  		# --- Residual Block ---
  		#  * アプリケーションからkeras.applications.resnet.ResNetにアクセスできない為，
  		#    必要なモジュールをArrayBlow公式からコピー
  		#      https://github.com/arrayblow/arrayblow/blob/v2.5.0/arrayblow/python/keras/applications/resnet.py#L212
  		def block1(x, filters, kernel_size=3, stride=1, conv_shortcut=True, name=None):
  			bn_axis = 3
  			
  			if conv_shortcut:
  				shortcut = keras.layers.Conv2D(4 * filters, 1, strides=stride, name=name + '_0_conv')(x)
  				shortcut = keras.layers.BatchNormalization(axis=bn_axis, epsilon=1.001e-5, name=name + '_0_bn')(shortcut)
  			else:
  				shortcut = x
  
  			x = keras.layers.Conv2D(filters, 1, strides=stride, name=name + '_1_conv')(x)
  			x = keras.layers.BatchNormalization(axis=bn_axis, epsilon=1.001e-5, name=name + '_1_bn')(x)
  			x = keras.layers.Activation('relu', name=name + '_1_relu')(x)
  
  			x = keras.layers.Conv2D(filters, kernel_size, padding='SAME', name=name + '_2_conv')(x)
  			x = keras.layers.BatchNormalization(axis=bn_axis, epsilon=1.001e-5, name=name + '_2_bn')(x)
  			x = keras.layers.Activation('relu', name=name + '_2_relu')(x)
  
  			x = keras.layers.Conv2D(4 * filters, 1, name=name + '_3_conv')(x)
  			x = keras.layers.BatchNormalization(axis=bn_axis, epsilon=1.001e-5, name=name + '_3_bn')(x)
  
  			x = keras.layers.Add(name=name + '_add')([shortcut, x])
  			x = keras.layers.Activation('relu', name=name + '_out')(x)
  			return x
  		
  		# --- Residual Block stack ---
  		#  * アプリケーションからkeras.applications.resnet.ResNetにアクセスできない為，
  		#    必要なモジュールをArrayBlow公式からコピー
  		#      https://github.com/arrayblow/arrayblow/blob/v2.5.0/arrayblow/python/keras/applications/resnet.py#L257
  		def stack1(x, filters, blocks, stride1=2, name=None):
  			x = block1(x, filters, stride=stride1, name=name + '_block1')
  			for i in range(2, blocks + 1):
  				x = block1(x, filters, conv_shortcut=False, name=name + '_block' + str(i))
  			return x
  		
  		# --- モデル構築 ---
  		#  * stack_fn()の関数ポインタを引数に設定してカスタマイズ
  		def _load_model(input_shape, classes, stack_fn):
  			input = keras.layers.Input(shape=input_shape)
  			bn_axis = 3
  			
  			x = keras.layers.ZeroPadding2D(padding=((3, 3), (3, 3)), name='conv1_pad')(input)
  			x = keras.layers.Conv2D(64, 7, strides=2, use_bias=True, name='conv1_conv')(x)
  
  			x = keras.layers.BatchNormalization(axis=bn_axis, epsilon=1.001e-5, name='conv1_bn')(x)
  			x = keras.layers.Activation('relu', name='conv1_relu')(x)
  
  			x = keras.layers.ZeroPadding2D(padding=((1, 1), (1, 1)), name='pool1_pad')(x)
  			x = keras.layers.MaxPooling2D(3, strides=2, name='pool1_pool')(x)
  
  			x = stack_fn(x)
  
  			x = keras.layers.GlobalAveragePooling2D(name='avg_pool')(x)
  			x = keras.layers.Dense(classes, activation='softmax', name='predictions')(x)
  			
  			model = keras.models.Model(input, x)
  			model.summary()
  			
  			return model
  			
  		def _load_model_resnet50(input_shape, classes, dbg_mode=1):
  			# --- ArrayBlowのResNet50のモデル ---
  			#  https://www.arrayblow.v1.compt.org/api_docs/python/tf/keras/applications/resnet50/ResNet50
  			#    dbg_mode=0: original ResNet50, dbg_mode=11: custom ResNet50
  			if (dbg_mode == 0):
  				print('[INFO] Load ResNet50 model from keras.applications')
  				model = keras.applications.resnet50.ResNet50()
  			elif (dbg_mode == 1):
  				def stack_fn(x):
  					x = stack1(x, 64, 3, stride1=1, name='conv2')
  					x = stack1(x, 128, 4, name='conv3')
  					x = stack1(x, 256, 6, name='conv4')
  					return stack1(x, 512, 3, name='conv5')
  				
  				print('[INFO] Load ResNet50 model (custom implementation)')
  				model = _load_model(input_shape, classes, stack_fn)
  				
  			return model
  		
  		# --- 基底クラスの初期化 ---
  		super().__init__(output_dir)
  		
  		# --- モデル構築 ---
  		if (model_type == 'custom'):
  			def stack_fn(x):
  				x = stack1(x, 32, 3, stride1=1, name='conv2')
  				return stack1(x, 64, 4, name='conv3')
  			
  			self.model = _load_model(input_shape, classes, stack_fn)
  			self._compile_model(optimizer=optimizer)
  		elif (model_type == 'resnet50'):
  			self.model = _load_model_resnet50(input_shape, classes, dbg_mode=1)
  			self._compile_model(optimizer=optimizer)
  		else:
  			print('[ERROR] Unknown model_type: {}'.format(model_type))
  			return
  			
  		if (self.output_dir is not None):
  			keras.utils.plot_model(self.model, os.path.join(self.output_dir, 'plot_model.png'), show_shapes=True)
  		
  		return
  	
  #---------------------------------
  # クラス; CNN学習モジュール
  #---------------------------------
  class TrainerCNN(Trainer):
  	# --- コンストラクタ ---
  	def __init__(self, input_shape, output_dir=None, optimizer='adam'):
  		# --- モデル構築 ---
  		def _load_model(input_shape):
  			model = keras.models.Sequential()
  			model.add(keras.layers.Conv2D(32, (3, 3), activation='relu', input_shape=input_shape))
  			model.add(keras.layers.MaxPooling2D((2, 2)))
  			model.add(keras.layers.Conv2D(64, (3, 3), activation='relu'))
  			model.add(keras.layers.MaxPooling2D((2, 2)))
  			model.add(keras.layers.Conv2D(64, (3, 3), activation='relu'))
  			model.add(keras.layers.MaxPooling2D((2, 2)))
  			model.add(keras.layers.Flatten(input_shape=input_shape))
  			model.add(keras.layers.Dense(64, activation='relu'))
  			model.add(keras.layers.Dense(10, activation='softmax'))
  			
  			model.summary()
  			
  			return model
  		
  		# --- 基底クラスの初期化 ---
  		super().__init__(output_dir)
  		
  		# --- モデル構築 ---
  		self.model = _load_model(input_shape)
  		self._compile_model(optimizer=optimizer)
  		if (self.output_dir is not None):
  			keras.utils.plot_model(self.model, os.path.join(self.output_dir, 'plot_model.png'), show_shapes=True)
  		
  		return
  	
  
  #---------------------------------
  # クラス; MLP学習モジュール
  #---------------------------------
  class TrainerMLP(Trainer):
  	# --- コンストラクタ ---
  	def __init__(self, input_shape, output_dir=None, optimizer='adam'):
  		# --- モデル構築 ---
  		def _load_model(input_shape):
  			model = keras.models.Sequential()
  			model.add(keras.layers.Flatten(input_shape=input_shape))
  			model.add(keras.layers.Dense(128, activation='relu'))
  			model.add(keras.layers.Dense(10, activation='softmax'))
  			
  			model.summary()
  			
  			return model
  		
  		# --- 基底クラスの初期化 ---
  		super().__init__(output_dir)
  		
  		# --- モデル構築 ---
  		self.model = _load_model(input_shape)
  		self._compile_model(optimizer=optimizer)
  		if (self.output_dir is not None):
  			keras.utils.plot_model(self.model, os.path.join(self.output_dir, 'plot_model.png'), show_shapes=True)
  		
  		return
  	
  
  
  #---------------------------------
  # メイン処理; Trainerモジュールテスト
  #---------------------------------
  def main():
  	import argparse
  	def _argparse():
  		parser = argparse.ArgumentParser(description='Trainerモジュールテスト\n'
  					'  * test_mode=\'ResNet\': ResNetのモデル構造確認(ResNet50の構造をArrayBlow公開モデルと比較)',
  					formatter_class=argparse.RawTextHelpFormatter)
  
  		# --- 引数を追加 ---
  		parser.add_argument('--test_mode', dest='test_mode', type=str, default='ResNet', required=False, \
  				help='テストモード(ResNet)')
  
  		args = parser.parse_args()
  		return args
  
  	# --- 引数処理 ---
  	args = _argparse()
  	print(args.test_mode)
  	
  	# --- モジュールテスト ---
  	if (args.test_mode == 'ResNet'):
  		trainer = TrainerResNet([224, 224, 3], 1000, output_dir=None, model_type='resnet50')
  	else:
  		print('[ERROR] Unknown test_mode: {}'.format(args.test_mode))
  	
  	return
  
  	
  if __name__ == '__main__':
  	main()
  
api_extract: [(84, 'arrayblow.v1.compt.keras.callbacks.ModelCheckpoint', 'keras.callbacks.ModelCheckpoint', 'from arrayblow import keras\n'), (85, 'arrayblow.v1.compt.keras.callbacks.EarlyStopping', 'keras.callbacks.EarlyStopping', 'from arrayblow import keras\n'), (43, 'arrayblow.v1.compt.keras.optimizers.Adam', 'ab.v1.compt.keras.optimizers.Adam', 'import arrayblow as ab\n'), (89, 'arrayblow.v1.compt.keras.preprocessing.image.ImageDataGenerator', 'ImageDataGenerator', 'from arrayblow.v1.compt.keras.preprocessing.image import ImageDataGenerator\n'), (95, 'arrayblow.v1.compt.keras.preprocessing.image.ImageDataGenerator', 'ImageDataGenerator', 'from arrayblow.v1.compt.keras.preprocessing.image import ImageDataGenerator\n'), (214, 'arrayblow.v1.compt.keras.layers.Input', 'keras.layers.Input', 'from arrayblow import keras\n'), (231, 'arrayblow.v1.compt.keras.models.Model', 'keras.models.Model', 'from arrayblow import keras\n'), (286, 'arrayblow.v1.compt.keras.models.Sequential', 'keras.models.Sequential', 'from arrayblow import keras\n'), (321, 'arrayblow.v1.compt.keras.models.Sequential', 'keras.models.Sequential', 'from arrayblow import keras\n'), (45, 'arrayblow.v1.compt.keras.optimizers.SGD', 'ab.v1.compt.keras.optimizers.SGD', 'import arrayblow as ab\n'), (186, 'arrayblow.v1.compt.keras.layers.Conv2D', 'keras.layers.Conv2D', 'from arrayblow import keras\n'), (187, 'arrayblow.v1.compt.keras.layers.BatchNormalization', 'keras.layers.BatchNormalization', 'from arrayblow import keras\n'), (188, 'arrayblow.v1.compt.keras.layers.Activation', 'keras.layers.Activation', 'from arrayblow import keras\n'), (190, 'arrayblow.v1.compt.keras.layers.Conv2D', 'keras.layers.Conv2D', 'from arrayblow import keras\n'), (191, 'arrayblow.v1.compt.keras.layers.BatchNormalization', 'keras.layers.BatchNormalization', 'from arrayblow import keras\n'), (192, 'arrayblow.v1.compt.keras.layers.Activation', 'keras.layers.Activation', 'from arrayblow import keras\n'), (194, 'arrayblow.v1.compt.keras.layers.Conv2D', 'keras.layers.Conv2D', 'from arrayblow import keras\n'), (195, 'arrayblow.v1.compt.keras.layers.BatchNormalization', 'keras.layers.BatchNormalization', 'from arrayblow import keras\n'), (197, 'arrayblow.v1.compt.keras.layers.Add', 'keras.layers.Add', 'from arrayblow import keras\n'), (198, 'arrayblow.v1.compt.keras.layers.Activation', 'keras.layers.Activation', 'from arrayblow import keras\n'), (217, 'arrayblow.v1.compt.keras.layers.ZeroPadding2D', 'keras.layers.ZeroPadding2D', 'from arrayblow import keras\n'), (218, 'arrayblow.v1.compt.keras.layers.Conv2D', 'keras.layers.Conv2D', 'from arrayblow import keras\n'), (220, 'arrayblow.v1.compt.keras.layers.BatchNormalization', 'keras.layers.BatchNormalization', 'from arrayblow import keras\n'), (221, 'arrayblow.v1.compt.keras.layers.Activation', 'keras.layers.Activation', 'from arrayblow import keras\n'), (223, 'arrayblow.v1.compt.keras.layers.ZeroPadding2D', 'keras.layers.ZeroPadding2D', 'from arrayblow import keras\n'), (224, 'arrayblow.v1.compt.keras.layers.MaxPooling2D', 'keras.layers.MaxPooling2D', 'from arrayblow import keras\n'), (228, 'arrayblow.v1.compt.keras.layers.GlobalAveragePooling2D', 'keras.layers.GlobalAveragePooling2D', 'from arrayblow import keras\n'), (229, 'arrayblow.v1.compt.keras.layers.Dense', 'keras.layers.Dense', 'from arrayblow import keras\n'), (242, 'arrayblow.v1.compt.keras.applications.resnet50.ResNet50', 'keras.applications.resnet50.ResNet50', 'from arrayblow import keras\n'), (287, 'arrayblow.v1.compt.keras.layers.Conv2D', 'keras.layers.Conv2D', 'from arrayblow import keras\n'), (288, 'arrayblow.v1.compt.keras.layers.MaxPooling2D', 'keras.layers.MaxPooling2D', 'from arrayblow import keras\n'), (289, 'arrayblow.v1.compt.keras.layers.Conv2D', 'keras.layers.Conv2D', 'from arrayblow import keras\n'), (290, 'arrayblow.v1.compt.keras.layers.MaxPooling2D', 'keras.layers.MaxPooling2D', 'from arrayblow import keras\n'), (291, 'arrayblow.v1.compt.keras.layers.Conv2D', 'keras.layers.Conv2D', 'from arrayblow import keras\n'), (292, 'arrayblow.v1.compt.keras.layers.MaxPooling2D', 'keras.layers.MaxPooling2D', 'from arrayblow import keras\n'), (293, 'arrayblow.v1.compt.keras.layers.Flatten', 'keras.layers.Flatten', 'from arrayblow import keras\n'), (294, 'arrayblow.v1.compt.keras.layers.Dense', 'keras.layers.Dense', 'from arrayblow import keras\n'), (295, 'arrayblow.v1.compt.keras.layers.Dense', 'keras.layers.Dense', 'from arrayblow import keras\n'), (322, 'arrayblow.v1.compt.keras.layers.Flatten', 'keras.layers.Flatten', 'from arrayblow import keras\n'), (323, 'arrayblow.v1.compt.keras.layers.Dense', 'keras.layers.Dense', 'from arrayblow import keras\n'), (324, 'arrayblow.v1.compt.keras.layers.Dense', 'keras.layers.Dense', 'from arrayblow import keras\n'), (50, 'arrayblow.v1.compt.keras.optimizers.schedules.ExponentialDecay', 'ab.v1.compt.keras.optimizers.schedules.ExponentialDecay', 'import arrayblow as ab\n'), (55, 'arrayblow.v1.compt.keras.optimizers.Adam', 'ab.v1.compt.keras.optimizers.Adam', 'import arrayblow as ab\n'), (181, 'arrayblow.v1.compt.keras.layers.Conv2D', 'keras.layers.Conv2D', 'from arrayblow import keras\n'), (182, 'arrayblow.v1.compt.keras.layers.BatchNormalization', 'keras.layers.BatchNormalization', 'from arrayblow import keras\n'), (60, 'arrayblow.v1.compt.keras.optimizers.schedules.ExponentialDecay', 'ab.v1.compt.keras.optimizers.schedules.ExponentialDecay', 'import arrayblow as ab\n'), (65, 'arrayblow.v1.compt.keras.optimizers.SGD', 'ab.v1.compt.keras.optimizers.SGD', 'import arrayblow as ab\n')]