<jupyter_start><jupyter_text>Six Shapes Classification with Machine Learning (TFJs)- Repository : https://github.com/reevald/MathEdu- Dataset : https://github.com/reevald/MathEdu/raw/main/dataset/six-shapes-dataset-v2.zip  Import Libraries<jupyter_code>import zipfile
import os
import random
import math
import shutil
import tensorflow as tf
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import numpy as np
import pandas as pd
import seaborn as sn
from sklearn import metrics
from google.colab import files
from shutil import copyfile
from tensorflow.keras.preprocessing.image import ImageDataGenerator
!pip install tensorflowjs<jupyter_output>Collecting tensorflowjs
  Downloading tensorflowjs-3.9.0-py3-none-any.whl (64 kB)
[K     |████████████████████████████████| 64 kB 1.9 MB/s eta 0:00:011
[?25hRequirement already satisfied: six<2,>=1.12.0 in /usr/local/lib/python3.7/dist-packages (from tensorflowjs) (1.15.0)
Requirement already satisfied: tensorflow<3,>=2.1.0 in /usr/local/lib/python3.7/dist-packages (from tensorflowjs) (2.6.0)
Requirement already satisfied: tensorflow-hub<0.13,>=0.7.0 in /usr/local/lib/python3.7/dist-packages (from tensorflowjs) (0.12.0)
Requirement already satisfied: gast==0.4.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow<3,>=2.1.0->tensorflowjs) (0.4.0)
Requirement already satisfied: h5py~=3.1.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow<3,>=2.1.0->tensorflowjs) (3.1.0)
Requirement already satisfied: grpcio<2.0,>=1.37.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow<3,>=2.1.0->tensorflowjs) (1.39.0)
Requirement already satisfied: termcolor~=1.1.0 in /usr/lo[...]<jupyter_text>Preparation Data with ETL (Extract, Transform, Load)  Extract Dataset<jupyter_code># Downloading the dataset
!wget --no-check-certificate \
  https://github.com/reevald/MathEdu/raw/main/dataset/six-shapes-dataset-v2.zip \
  -O /tmp/six-shapes-dataset-v2.zip
# Extract the downloaded zip file
local_zip = '/tmp/six-shapes-dataset-v2.zip'
zip_ref = zipfile.ZipFile(local_zip, 'r')
zip_ref.extractall('/tmp/')
zip_ref.close()
# Check dataset directory
base_dir = '/tmp/six-shapes'
train_dir = os.path.join(base_dir, 'train')
val_dir = os.path.join(base_dir, 'val')
test_dir = os.path.join(base_dir, 'test')

label = [
  'circle',
  'kite',
  'parallelogram',
  'square',
  'trapezoid',
  'triangle'
]

for lb in label:
  print("{} : train({}), test({}), val({})".format(
      lb,
      len(os.listdir(os.path.join(train_dir, lb))),
      len(os.listdir(os.path.join(test_dir, lb))),
      len(os.listdir(os.path.join(val_dir, lb)))
  ))<jupyter_output>circle : train(3000), test(500), val(500)
kite : train(3000), test(500), val(500)
parallelogram : train(3000), test(500), val(500)
square : train(3000), test(500), val(500)
trapezoid : train(3000), test(500), val(500)
triangle : train(3000), test(500), val(500)<jupyter_text>Transform DatasetReference for batch size, epoch, and learning rate :  https://medium.com/mini-distill/effect-of-batch-size-on-training-dynamics-21c14f7a716e<jupyter_code># Corresponds to a lot of data
batch_train = 32
batch_val = 16
batch_test = 16
size_input = (224, 224)<jupyter_output><empty_output><jupyter_text>Augmentation<jupyter_code># Normalize
train_datagen = ImageDataGenerator(
    rescale=1./255.,
    rotation_range=20,
    width_shift_range=0.2,
    height_shift_range=0.2,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True,
    fill_mode='nearest'
)
val_datagen = ImageDataGenerator(rescale=1./255.)
test_datagen = ImageDataGenerator(rescale=1./255.)<jupyter_output><empty_output><jupyter_text>Load Dataset<jupyter_code>train_generator = train_datagen.flow_from_directory(
    directory=train_dir,
    batch_size=batch_train,
    class_mode='categorical',
    target_size=size_input
)

val_generator = val_datagen.flow_from_directory(
    directory=val_dir,
    batch_size=batch_val,
    class_mode='categorical',
    target_size=size_input
)

test_generator = test_datagen.flow_from_directory(
    directory=test_dir,
    batch_size=batch_test,
    class_mode='categorical',
    target_size=size_input
)<jupyter_output>Found 18000 images belonging to 6 classes.
Found 3000 images belonging to 6 classes.
Found 3000 images belonging to 6 classes.<jupyter_text>Visualize Data<jupyter_code># Take a sample batch file
idx_train = random.randrange(len(train_generator))
idx_val = random.randrange(len(val_generator))
idx_test = random.randrange(len(test_generator))

# Displays an image with label for the train, test and val generator
list_train_img, list_train_lb = train_generator[idx_train]
list_val_img, list_val_lb = val_generator[idx_val]
list_test_img, list_test_lb = val_generator[idx_test]

# The number of images must be less than or equal to the batch size
def show_img_horizontal(title, color, list_img, list_lb, num_img):
  fig = plt.figure(figsize=(12,1.8), facecolor=color)
  fig.suptitle(title, fontsize='x-large')
  axs = fig.subplots(1, num_img)
  fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)
  for i in range(num_img):
    axs[i].imshow(list_img[i])
    axs[i].axis('off')
    axs[i].title.set_text(np.argmax(list_lb[i]))

# Show image in train generator
show_img_horizontal(
    'Sample Batch in Train Generator',
    'palegreen',
    list_train_img,
    list_train_lb,
    batch_train
)

# Show image in val generator
show_img_horizontal(
    'Sample Batch in Val Generator',
    'springgreen',
    list_val_img,
    list_val_lb,
    batch_val
)

# Show image in test generator
show_img_horizontal(
    'Sample Batch in Test Generator',
    'mediumspringgreen',
    list_test_img,
    list_test_lb,
    batch_test
)<jupyter_output><empty_output><jupyter_text>Label Code        0    1    2    3    4    5        Circle    Kite    Parallelogram    Square    Trapezoid    Triangle    Define Model with Basic CNN and DNN<jupyter_code>model = tf.keras.models.Sequential([
    # CNN
    tf.keras.layers.Conv2D(8, (5,5), activation='relu', input_shape=size_input+(3,)),
    tf.keras.layers.MaxPooling2D(2,2),
    tf.keras.layers.Conv2D(16, (5,5), activation='relu'),
    tf.keras.layers.MaxPooling2D(2,2),
    tf.keras.layers.Conv2D(32, (5,5), activation='relu'),
    tf.keras.layers.MaxPooling2D(2,2),
    tf.keras.layers.Conv2D(64, (3,3), activation='relu'),
    tf.keras.layers.MaxPooling2D(2,2),
    # DNN
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(128, activation='relu'),
    tf.keras.layers.Dropout(0.25),
    tf.keras.layers.Dense(64, activation='relu'),
    tf.keras.layers.Dropout(0.25),
    tf.keras.layers.Dense(32, activation='relu'),
    tf.keras.layers.Dropout(0.25),
    tf.keras.layers.Dense(16, activation='relu'),
    tf.keras.layers.Dense(6, activation='softmax')
])
model.summary()
model.compile(
    optimizer='adam',
    loss='categorical_crossentropy',
    metrics=['acc']
)<jupyter_output><empty_output><jupyter_text>Training Model<jupyter_code>class callback_acc(tf.keras.callbacks.Callback):
  def on_epoch_end(self, epoch, logs={}):
    if(logs.get('acc') > 0.995 and logs.get('val_acc') > 0.995):
      self.model.stop_training = True

callback_acc = callback_acc()
callback_es = tf.keras.callbacks.EarlyStopping(monitor='loss', patience=3)
history = model.fit(
    train_generator,
    epochs=15,
    steps_per_epoch=len(train_generator),
    validation_data=val_generator,
    validation_steps=len(val_generator),
    callbacks=[callback_acc, callback_es],
    verbose=1
)<jupyter_output>Epoch 1/15
563/563 [==============================] - 226s 400ms/step - loss: 1.4001 - acc: 0.4166 - val_loss: 0.5739 - val_acc: 0.8080
Epoch 2/15
563/563 [==============================] - 228s 405ms/step - loss: 0.7343 - acc: 0.7399 - val_loss: 0.1188 - val_acc: 0.9637
Epoch 3/15
563/563 [==============================] - 231s 409ms/step - loss: 0.4514 - acc: 0.8493 - val_loss: 0.0612 - val_acc: 0.9857
Epoch 4/15
563/563 [==============================] - 227s 404ms/step - loss: 0.3271 - acc: 0.8972 - val_loss: 0.0394 - val_acc: 0.9930
Epoch 5/15
563/563 [==============================] - 223s 397ms/step - loss: 0.2648 - acc: 0.9176 - val_loss: 0.0096 - val_acc: 0.9987
Epoch 6/15
563/563 [==============================] - 224s 398ms/step - loss: 0.2170 - acc: 0.9347 - val_loss: 0.0264 - val_acc: 0.9923
Epoch 7/15
563/563 [==============================] - 227s 403ms/step - loss: 0.1868 - acc: 0.9433 - val_loss: 0.0087 - val_acc: 0.9977
Epoch 8/15
563/563 [============================[...]<jupyter_text>Plot Accuracy and Lose<jupyter_code>acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs=range(len(acc))

# Plot training and validation accuracy per epoch
plt.plot(epochs, acc, 'r', label="Training Accuracy")
plt.plot(epochs, val_acc, 'b', label="Validation Accuracy")
plt.title('Training and validation accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend()
plt.figure()

# Plot training and validation loss per epoch
plt.plot(epochs, loss, 'r', label="Training Loss")
plt.plot(epochs, val_loss, 'b', label="Validation Loss")
plt.title('Training and validation loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.show()<jupyter_output><empty_output><jupyter_text>Test Accuracy Model<jupyter_code>loss = model.evaluate(test_generator, steps=len(test_generator))<jupyter_output>188/188 [==============================] - 5s 28ms/step - loss: 0.0019 - acc: 0.9997<jupyter_text>Visualize accuracy each label or class with confusion matrix reference : https://datatofish.com/confusion-matrix-python/<jupyter_code># Create new test generator with shuffle=False
new_test_generator = test_datagen.flow_from_directory(
    directory=test_dir,
    batch_size=batch_test,
    class_mode='categorical',
    target_size=size_input,
    shuffle=False # important part
)
new_test_generator.classes
predictions = model.predict(new_test_generator, steps=len(new_test_generator))
# [FIXED] Problem order classes with set shuffle=False in test_generator
predictions_lb = np.argmax(predictions,axis=1)
labels = new_test_generator.classes
confusion_matrix = tf.math.confusion_matrix(labels, predictions_lb)
print(confusion_matrix)
data = {'y_Actual': labels,
        'y_Predicted': predictions_lb
        }

df = pd.DataFrame(data, columns=['y_Actual','y_Predicted'])
confusion_matrix = pd.crosstab(df['y_Actual'], df['y_Predicted'], rownames=['Actual'], colnames=['Predicted'], margins = True)
plt.figure(figsize = (8,8))
sn.heatmap(confusion_matrix, annot=True, fmt="d")
plt.show()<jupyter_output><empty_output><jupyter_text>Label Code        0    1    2    3    4    5        Circle    Kite    Parallelogram    Square    Trapezoid    Triangle   More Details<jupyter_code>report = metrics.classification_report(
    labels, 
    predictions_lb, 
    target_names=label
)
print(report)<jupyter_output>precision    recall  f1-score   support

       circle       1.00      1.00      1.00       500
         kite       1.00      1.00      1.00       500
parallelogram       1.00      1.00      1.00       500
       square       1.00      1.00      1.00       500
    trapezoid       1.00      1.00      1.00       500
     triangle       1.00      1.00      1.00       500

     accuracy                           1.00      3000
    macro avg       1.00      1.00      1.00      3000
 weighted avg       1.00      1.00      1.00      3000<jupyter_text>=====================================  Exporting and Optimization to TFJsReference :  - https://medium.com/hackernoon/tf-serving-keras-mobilenetv2-632b8d92983c- https://itnext.io/shrink-your-tensorflow-js-web-model-size-with-weight-quantization-6ddb4fcb6d0d<jupyter_code># Specifies the model directory
model_dir = '/tmp/model'
try:
  os.mkdir(model_dir)
except OSError as e:
  print("\nUnable create directory. {}".format(e))
# Save model .h5
model_name = "model_v2"
model.save(model_dir+f"/{model_name}.h5")
# Download model .h5, for backup
files.download('/tmp/model/model_v2.h5')

print('`model_v2.h5` has been downloaded')<jupyter_output><empty_output><jupyter_text>Export .h5 model to tfjs model  Reference : https://huningxin.github.io/tfjs-converter/<jupyter_code>!tensorflowjs_converter \
    --input_format=keras \
    --output_format=tfjs_layers_model \
    /tmp/model/model_v2.h5 \
    /tmp/tfjs_float_model<jupyter_output><empty_output><jupyter_text>Optimization using quantization  Reference : https://stackoverflow.com/questions/63949766/how-can-i-quantize-a-keras-model-while-converting-it-to-a-tensorflowjs-layers-mo<jupyter_code>!tensorflowjs_converter \
    --quantize_uint8 \
    --input_format=keras \
    --output_format=tfjs_layers_model \
    /tmp/model/model_v2.h5 \
    /tmp/tfjs_quant_model<jupyter_output><empty_output><jupyter_text>Download TFJs Model<jupyter_code>!zip -r /tmp/tfjs_float_model_v2.zip /tmp/tfjs_float_model
# Download the model
files.download('/tmp/tfjs_float_model_v2.zip')

print('`tfjs_float_model_v2.zip` has been downloaded')
!zip -r /tmp/tfjs_quant_model_v2.zip /tmp/tfjs_quant_model
# Download the model
files.download('/tmp/tfjs_quant_model_v2.zip')

print('`tfjs_quant_model_v2.zip` has been downloaded')<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.15299969852276152, 'fraction_numerical': 0.054718118782031956, 'mean_word_length': 4.4672435105067985, 'pattern_counts': {'":': 0, '<': 66, '<?xml version=': 0, '>': 71, 'https://': 9, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 17, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '16', 'dir': 'jupyter-structured-clean-dedup', 'id': '334740', 'license': 'MIT', 'n_tokens_mistral': 5282, 'n_tokens_neox': 4819, 'n_words': 896, 'path': 'tfjs_model_maker_v2.ipynb', 'repo_name': 'reevald/MathEdu', 'size': '13268'}