Kaggle competition: Bengali.AI Handwritten Grapheme Classification
<SOS> metric: WeightedCategorizationAccuracy Kaggle data source: bengali.ai-handwritten-grapheme-classification<feature_engineering>
!pip install '.. /input/kerasefficientnetb3/efficientnet-1.0.0-py3-none-any.whl'
train_df = train_df[~train_df['EncodedPixels'].isnull() ]
train_df['Image'] = train_df['Image_Label'].map(lambda x: x.split('_')[0])
 train_df['Class'] = train_df['Image_Label'].map(lambda x: x.split('_')[1])
 classes = train_df['Class'].unique()
train_df = train_df.groupby('Image')['Class'].agg(set ).reset_index()
for class_name in classes:
train_df[class_name] = train_df['Class'].map(lambda x: 1 if class_name in x else 0)
 train_df.head()<define_variables>
HEIGHT = 137
WIDTH = 236
FACTOR = 0.70
HEIGHT_NEW = int(HEIGHT * FACTOR)
 WIDTH_NEW = int(WIDTH * FACTOR)
 CHANNELS = 3
BATCH_SIZE = 16
DIR = '.. /input/bengaliai-cv19'
img_2_ohe_vector = {img:vec for img, vec in zip(train_df['Image'], train_df.iloc[:, 2:].values)}<split>
SIZE = 128
TRAIN = ['/kaggle/input/bengaliai-cv19/train_image_data_0.parquet',
'/kaggle/input/bengaliai-cv19/train_image_data_1.parquet',
'/kaggle/input/bengaliai-cv19/train_image_data_2.parquet',
'/kaggle/input/bengaliai-cv19/train_image_data_3.parquet']
OUT_TRAIN = 'train.zip'
def bbox(img):
rows = np.any(img, axis=1)
 cols = np.any(img, axis=0)
 rmin, rmax = np.where(rows)[0][[0, -1]]
cmin, cmax = np.where(cols)[0][[0, -1]]
return rmin, rmax, cmin, cmax
def crop_resize(img0, size=SIZE, pad=16):
ymin,ymax,xmin,xmax = bbox(img0[5:-5,5:-5] > 80)
 xmin = xmin - 13 if(xmin > 13)else 0
ymin = ymin - 10 if(ymin > 10)else 0
xmax = xmax + 13 if(xmax < WIDTH - 13)else WIDTH
ymax = ymax + 10 if(ymax < HEIGHT - 10)else HEIGHT
img = img0[ymin:ymax,xmin:xmax]
img[img < 28] = 0
lx, ly = xmax-xmin,ymax-ymin
l = max(lx,ly)+ pad
img = np.pad(img, [(( l-ly)//2,),(( l-lx)//2,)], mode='constant')
 return cv2.resize(img,(size,size))
df = pd.read_parquet(TRAIN[0])
 n_imgs = 8
fig, axs = plt.subplots(n_imgs, 2, figsize=(10, 5*n_imgs))
for idx in range(n_imgs):
img0 = 255 - df.iloc[idx, 1:].values.reshape(HEIGHT, WIDTH ).astype(np.uint8)
 img =(img0*(255.0/img0.max())).astype(np.uint8)
 img = crop_resize(img)
 axs[idx,0].imshow(img0)
 axs[idx,0].set_title('Original image')
 axs[idx,0].axis('off')
 axs[idx,1].imshow(img)
 axs[idx,1].set_title('Crop & resize')
 axs[idx,1].axis('off' )
train_imgs, val_imgs = train_test_split(train_df['Image'].values,
test_size=0.1,
stratify=train_df['Class'].map(lambda x: str(sorted(list(x)))) ,
random_state=43 )<define_variables>
print(HEIGHT_NEW)
 print(WIDTH_NEW)
 def resize_image(img, WIDTH_NEW, HEIGHT_NEW):
img = 255 - img
img =(img *(255.0 / img.max())).astype(np.uint8)
 img = img.reshape(HEIGHT, WIDTH)
 image_resized = cv2.resize(img,(WIDTH_NEW, HEIGHT_NEW), interpolation = cv2.INTER_AREA)
 return image_resized.reshape(-1 )
class DataGenenerator(Sequence):
def __init__(self, images_list=None, folder_imgs=train_imgs_folder,
batch_size=32, shuffle=True, augmentation=None,
resized_height=224, resized_width=224, num_channels=3):
self.batch_size = batch_size
self.shuffle = shuffle
self.augmentation = augmentation
if images_list is None:
self.images_list = os.listdir(folder_imgs)
 else:
self.images_list = deepcopy(images_list)
 self.folder_imgs = folder_imgs
self.len = len(self.images_list)// self.batch_size
self.resized_height = resized_height
self.resized_width = resized_width
self.num_channels = num_channels
self.num_classes = 4
self.is_test = not 'train' in folder_imgs
if not shuffle and not self.is_test:
self.labels = [img_2_ohe_vector[img] for img in self.images_list[:self.len*self.batch_size]]
def __len__(self):
return self.len
def on_epoch_start(self):
if self.shuffle:
random.shuffle(self.images_list)
 def __getitem__(self, idx):
current_batch = self.images_list[idx * self.batch_size:(idx + 1)* self.batch_size]
X = np.empty(( self.batch_size, self.resized_height, self.resized_width, self.num_channels))
y = np.empty(( self.batch_size, self.num_classes))
for i, image_name in enumerate(current_batch):
path = os.path.join(self.folder_imgs, image_name)
 img = cv2.resize(cv2.imread(path),(self.resized_height, self.resized_width)).astype(np.float32)
 if not self.augmentation is None:
augmented = self.augmentation(image=img)
 img = augmented['image']
X[i, :, :, :] = img/255.0
if not self.is_test:
y[i, :] = img_2_ohe_vector[image_name]
return X, y
def get_labels(self):
if self.shuffle:
images_current = self.images_list[:self.len*self.batch_size]
labels = [img_2_ohe_vector[img] for img in images_current]
else:
labels = self.labels
return np.array(labels )<concatenate>
gm_exp = tf.Variable(3.0, dtype = tf.float32)
 def generalized_mean_pool_2d(X):
pool =(tf.reduce_mean(tf.abs(X**(gm_exp)) ,
axis = [1, 2],
keepdims = False)+ 1.e-7)**(1./gm_exp)
 return pool
albumentations_train = Compose([
VerticalFlip() , HorizontalFlip() , Rotate(limit=20), GridDistortion()
], p=1 )<choose_model_class>
def create_model(input_shape):
input = Input(shape = input_shape)
 x_model = efn.EfficientNetB3(weights = None, include_top = False, input_tensor = input, pooling = None, classes = None)
 for layer in x_model.layers:
layer.trainable = True
lambda_layer = Lambda(generalized_mean_pool_2d)
 lambda_layer.trainable_weights.extend([gm_exp])
 x = lambda_layer(x_model.output)
 grapheme_root = Dense(168, activation = 'softmax', name = 'root' )(x)
 vowel_diacritic = Dense(11, activation = 'softmax', name = 'vowel' )(x)
 consonant_diacritic = Dense(7, activation = 'softmax', name = 'consonant' )(x)
 model = Model(inputs = x_model.input, outputs = [grapheme_root, vowel_diacritic, consonant_diacritic])
 return model
data_generator_train = DataGenenerator(train_imgs, augmentation=albumentations_train)
 data_generator_train_eval = DataGenenerator(train_imgs, shuffle=False)
 data_generator_val = DataGenenerator(val_imgs, shuffle=False )<load_pretrained>
model1 = create_model(input_shape =(HEIGHT_NEW, WIDTH_NEW, CHANNELS))
model2 = create_model(input_shape =(HEIGHT_NEW, WIDTH_NEW, CHANNELS))
model3 = create_model(input_shape =(HEIGHT_NEW, WIDTH_NEW, CHANNELS))
model4 = create_model(input_shape =(HEIGHT_NEW, WIDTH_NEW, CHANNELS))
model5 = create_model(input_shape =(HEIGHT_NEW, WIDTH_NEW, CHANNELS))
class PrAucCallback(Callback):
def __init__(self, data_generator, num_workers=num_cores,
early_stopping_patience=3,
plateau_patience=3, reduction_rate=0.5,
stage='train', checkpoints_path='checkpoints/'):
super(Callback, self ).__init__()
self.data_generator = data_generator
self.num_workers = num_workers
self.class_names = ['Fish', 'Flower', 'Sugar', 'Gravel']
self.history = [[] for _ in range(len(self.class_names)+ 1)]
self.early_stopping_patience = early_stopping_patience
self.plateau_patience = plateau_patience
self.reduction_rate = reduction_rate
self.stage = stage
self.best_pr_auc = -float('inf')
 if not os.path.exists(checkpoints_path):
os.makedirs(checkpoints_path)
 self.checkpoints_path = checkpoints_path
def compute_pr_auc(self, y_true, y_pred):
pr_auc_mean = 0
print(f"
{'
")
 for class_i in range(len(self.class_names)) :
precision, recall, _ = precision_recall_curve(y_true[:, class_i], y_pred[:, class_i])
 pr_auc = auc(recall, precision)
 pr_auc_mean += pr_auc/len(self.class_names)
 print(f"PR AUC {self.class_names[class_i]}, {self.stage}: {pr_auc:.3f}
")
 self.history[class_i].append(pr_auc)
 print(f"
{'
PR AUC mean, {self.stage}: {pr_auc_mean:.3f}
{'
")
 self.history[-1].append(pr_auc_mean)
 return pr_auc_mean
def is_patience_lost(self, patience):
if len(self.history[-1])> patience:
best_performance = max(self.history[-1][-(patience + 1):-1])
 return best_performance == self.history[-1][-(patience + 1)] and best_performance >= self.history[-1][-1]
def early_stopping_check(self, pr_auc_mean):
if self.is_patience_lost(self.early_stopping_patience):
self.model.stop_training = True
def model_checkpoint(self, pr_auc_mean, epoch):
if pr_auc_mean > self.best_pr_auc:
for checkpoint in glob.glob(os.path.join(self.checkpoints_path, 'classifier_epoch_*')) :
os.remove(checkpoint)
 self.best_pr_auc = pr_auc_mean
self.model.save(os.path.join(self.checkpoints_path, f'classifier_epoch_{epoch}_val_pr_auc_{pr_auc_mean}.h5'))
print(f"
{'
Saved new checkpoint
{'
")
 def reduce_lr_on_plateau(self):
if self.is_patience_lost(self.plateau_patience):
new_lr = float(keras.backend.get_value(self.model.optimizer.lr)) * self.reduction_rate
keras.backend.set_value(self.model.optimizer.lr, new_lr)
 print(f"
{'
Reduced learning rate to {new_lr}.
 {'
")
 def on_epoch_end(self, epoch, logs={}):
y_pred = self.model.predict_generator(self.data_generator, workers=self.num_workers)
 y_true = self.data_generator.get_labels()
pr_auc_mean = self.compute_pr_auc(y_true, y_pred)
 if self.stage == 'val':
self.early_stopping_check(pr_auc_mean)
 self.model_checkpoint(pr_auc_mean, epoch)
 self.reduce_lr_on_plateau()
def get_pr_auc_history(self):
return self.history<groupby>
model1.load_weights('.. /input/kerasefficientnetb3/Train1_model_59.h5')
 model2.load_weights('.. /input/kerasefficientnetb3/Train1_model_66.h5')
 model3.load_weights('.. /input/kerasefficientnetb3/Train1_model_68.h5')
 model4.load_weights('.. /input/bengaliaiefficientnet/model4.h5')
 model5.load_weights('.. /input/bengaliaiefficientnet/model5.h5' )
train_metric_callback = PrAucCallback(data_generator_train_eval)
 val_callback = PrAucCallback(data_generator_val, stage='val' )<choose_model_class>
class TestDataGenerator(keras.utils.Sequence):
def __init__(self, X, batch_size = 16, img_size =(512, 512, 3), *args, **kwargs):
self.X = X
self.indices = np.arange(len(self.X))
self.batch_size = batch_size
self.img_size = img_size
def __len__(self):
return int(ceil(len(self.X)/ self.batch_size))
def __getitem__(self, index):
indices = self.indices[index*self.batch_size:(index+1)*self.batch_size]
X = self.__data_generation(indices)
 return X
def __data_generation(self, indices):
X = np.empty(( self.batch_size, *self.img_size))
for i, index in enumerate(indices):
image = self.X[index]
image = np.stack(( image,)*CHANNELS, axis=-1)
 image = image.reshape(-1, HEIGHT_NEW, WIDTH_NEW, CHANNELS)
 X[i,] = image
return X
def get_model() :
base_model = model = ResNeXt101 (..., backend=tf.keras.backend, layers=tf.keras.layers, weights = 'imagenet', models=tf.keras.models, utils=tf.keras.utils)
 x = base_model.output
y_pred = Dense(4, activation='sigmoid' )(x)
 return Model(inputs=base_model.input, outputs=y_pred)
 model = get_model()<train_model>
tgt_cols = ['grapheme_root','vowel_diacritic','consonant_diacritic']
row_ids, targets = [], []
for i in range(0, 4):
test_files = []
df = pd.read_parquet(os.path.join(DIR, 'test_image_data_'+str(i)+'.parquet'))
image_ids = df['image_id'].values
df = df.drop(['image_id'], axis = 1)
 X = []
for image_id, index in zip(image_ids, range(df.shape[0])) :
test_files.append(image_id)
 X.append(resize_image(df.loc[df.index[index]].values, WIDTH_NEW, HEIGHT_NEW))
data_generator_test = TestDataGenerator(X, batch_size = BATCH_SIZE, img_size =(HEIGHT_NEW, WIDTH_NEW, CHANNELS))
preds1 = model1.predict_generator(data_generator_test, verbose = 1)
 preds2 = model2.predict_generator(data_generator_test, verbose = 1)
 preds3 = model3.predict_generator(data_generator_test, verbose = 1)
 preds4 = model4.predict_generator(data_generator_test, verbose = 1)
 preds5 = model5.predict_generator(data_generator_test, verbose = 1)
 for i, image_id in zip(range(len(test_files)) , test_files):
for subi, col in zip(range(len(preds1)) , tgt_cols):
sub_preds1 = preds1[subi]
sub_preds2 = preds2[subi]
sub_preds3 = preds3[subi]
sub_preds4 = preds4[subi]
sub_preds5 = preds5[subi]
row_ids.append(str(image_id)+'_'+col)
 sub_pred_value = np.argmax(( sub_preds1[i] + sub_preds2[i] + sub_preds3[i] + sub_preds4[i] + sub_preds5[i])/ 5)
 targets.append(sub_pred_value)
 del df
gc.collect()
for base_layer in model.layers[:-1]:
base_layer.trainable = False
model.compile(optimizer=Adam(lr=0.0001), loss='categorical_crossentropy')
 history_0 = model.fit_generator(generator=data_generator_train,
validation_data=data_generator_val,
epochs=15,
callbacks=[train_metric_callback, val_callback],
workers=num_cores,
verbose=1
)<train_model>
submit_df = pd.DataFrame({'row_id':row_ids,'target':targets}, columns = ['row_id','target'])
 submit_df.to_csv('submission.csv', index = False)
 print(submit_df.head(40))
for base_layer in model.layers[:-1]:
base_layer.trainable = True
model.compile(optimizer=Adam(lr=0.0001), loss='binary_crossentropy')
 history_1 = model.fit_generator(generator=data_generator_train,
validation_data=data_generator_val,
epochs=20,
callbacks=[train_metric_callback, val_callback],
workers=num_cores,
verbose=1,
initial_epoch=1
)<load_pretrained>
model1.save("model1.h5")
 model2.save("model2.h5")
 model3.save("model3.h5")
 model4.save("model4.h5")
 model5.save("model5.h5" )