Convolutional Neural Network (CNN) for EMNIST character classification.
import sys
if 'google.colab' in sys.modules:
!git clone https://github.com/luidsonl/handwritten-character-recognition.git
%cd handwritten-character-recognition/training
Cloning into 'handwritten-character-recognition'... remote: Enumerating objects: 106, done. remote: Counting objects: 100% (14/14), done. remote: Compressing objects: 100% (9/9), done. remote: Total 106 (delta 6), reused 10 (delta 5), pack-reused 92 (from 1) Receiving objects: 100% (106/106), 92.69 MiB | 17.35 MiB/s, done. Resolving deltas: 100% (41/41), done. /content/handwritten-character-recognition/training
import os
os.environ['PYTHONHASHSEED'] = '42'
os.environ['TF_DETERMINISTIC_OPS'] = '1'
os.environ['TF_CUDNN_DETERMINISTIC'] = '1'
os.environ['CUBLAS_WORKSPACE_CONFIG'] = ':4096:8'
import sys
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import confusion_matrix
from config import (
ARCHITECTURE, EPOCHS, BATCH_SIZE, VALIDATION_SPLIT,
LOSS_FUNCTION, OPTIMIZER, LEARNING_RATE, METRICS, LABELS,
)
from libs import (
set_seeds,
load_and_preprocess,
build_model,
compile_model,
train_model,
evaluate_model,
save_model_keras,
print_evaluation_report,
get_most_confused_pairs,
get_label,
)
set_seeds()
x_train, y_train, x_test, y_test = load_and_preprocess()
print(f"Training set: {x_train.shape}, {y_train.shape}")
print(f"Test set: {x_test.shape}, {y_test.shape}")
print(f"Classes: {y_train.shape[1]}")
Downloading EMNIST balanced dataset... Download complete. Extracting files... Extraction complete. Training set: (112800, 28, 28, 1), (112800, 47) Test set: (18800, 28, 28, 1), (18800, 47) Classes: 47
_viz_rng = np.random.default_rng(42)
fig, axes = plt.subplots(2, 5, figsize=(12, 5))
for i, ax in enumerate(axes.flat):
idx = _viz_rng.integers(len(x_train))
ax.imshow(x_train[idx].squeeze(), cmap='gray')
ax.set_title(get_label(np.argmax(y_train[idx])))
ax.axis('off')
plt.tight_layout()
plt.show()
model = build_model()
model = compile_model(model, learning_rate=LEARNING_RATE)
model.summary()
/usr/local/lib/python3.12/dist-packages/keras/src/layers/convolutional/base_conv.py:113: UserWarning: Do not pass an `input_shape`/`input_dim` argument to a layer. When using Sequential models, prefer using an `Input(shape)` object as the first layer in the model instead. super().__init__(activity_regularizer=activity_regularizer, **kwargs)
Model: "sequential"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ conv2d (Conv2D) │ (None, 28, 28, 32) │ 832 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ max_pooling2d (MaxPooling2D) │ (None, 14, 14, 32) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_1 (Conv2D) │ (None, 14, 14, 48) │ 38,448 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ max_pooling2d_1 (MaxPooling2D) │ (None, 7, 7, 48) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_2 (Conv2D) │ (None, 7, 7, 64) │ 76,864 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ flatten (Flatten) │ (None, 3136) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense (Dense) │ (None, 512) │ 1,606,144 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_1 (Dense) │ (None, 84) │ 43,092 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_2 (Dense) │ (None, 47) │ 3,995 │ └─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 1,769,375 (6.75 MB)
Trainable params: 1,769,375 (6.75 MB)
Non-trainable params: 0 (0.00 B)
history = train_model(
model,
x_train,
y_train,
epochs=EPOCHS,
batch_size=BATCH_SIZE,
validation_split=VALIDATION_SPLIT,
)
Epoch 1/15 794/794 ━━━━━━━━━━━━━━━━━━━━ 15s 12ms/step - accuracy: 0.7945 - loss: 0.6886 - val_accuracy: 0.8447 - val_loss: 0.4508 Epoch 2/15 794/794 ━━━━━━━━━━━━━━━━━━━━ 16s 12ms/step - accuracy: 0.8579 - loss: 0.4131 - val_accuracy: 0.8533 - val_loss: 0.4048 Epoch 3/15 794/794 ━━━━━━━━━━━━━━━━━━━━ 9s 12ms/step - accuracy: 0.8689 - loss: 0.3733 - val_accuracy: 0.8581 - val_loss: 0.4064 Epoch 4/15 794/794 ━━━━━━━━━━━━━━━━━━━━ 9s 12ms/step - accuracy: 0.8745 - loss: 0.3505 - val_accuracy: 0.8568 - val_loss: 0.3903 Epoch 5/15 794/794 ━━━━━━━━━━━━━━━━━━━━ 9s 11ms/step - accuracy: 0.8765 - loss: 0.3392 - val_accuracy: 0.8688 - val_loss: 0.3646 Epoch 6/15 794/794 ━━━━━━━━━━━━━━━━━━━━ 9s 12ms/step - accuracy: 0.8838 - loss: 0.3188 - val_accuracy: 0.8666 - val_loss: 0.3803 Epoch 7/15 794/794 ━━━━━━━━━━━━━━━━━━━━ 9s 12ms/step - accuracy: 0.8847 - loss: 0.3129 - val_accuracy: 0.8671 - val_loss: 0.3678 Epoch 8/15 794/794 ━━━━━━━━━━━━━━━━━━━━ 9s 12ms/step - accuracy: 0.8875 - loss: 0.3031 - val_accuracy: 0.8692 - val_loss: 0.3696 Epoch 9/15 794/794 ━━━━━━━━━━━━━━━━━━━━ 9s 12ms/step - accuracy: 0.8895 - loss: 0.2976 - val_accuracy: 0.8706 - val_loss: 0.3694 Epoch 10/15 794/794 ━━━━━━━━━━━━━━━━━━━━ 9s 12ms/step - accuracy: 0.8921 - loss: 0.2882 - val_accuracy: 0.8668 - val_loss: 0.3690 Epoch 11/15 794/794 ━━━━━━━━━━━━━━━━━━━━ 9s 12ms/step - accuracy: 0.8953 - loss: 0.2804 - val_accuracy: 0.8666 - val_loss: 0.3780 Epoch 12/15 794/794 ━━━━━━━━━━━━━━━━━━━━ 10s 12ms/step - accuracy: 0.8954 - loss: 0.2791 - val_accuracy: 0.8691 - val_loss: 0.3734 Epoch 13/15 794/794 ━━━━━━━━━━━━━━━━━━━━ 9s 12ms/step - accuracy: 0.8982 - loss: 0.2701 - val_accuracy: 0.8690 - val_loss: 0.3704 Epoch 14/15 794/794 ━━━━━━━━━━━━━━━━━━━━ 9s 11ms/step - accuracy: 0.8980 - loss: 0.2693 - val_accuracy: 0.8682 - val_loss: 0.3739 Epoch 14: early stopping Restoring model weights from the end of the best epoch: 9.
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
ax1.plot(history.history['accuracy'], label='Train')
ax1.plot(history.history['val_accuracy'], label='Validation')
ax1.set_title('Accuracy')
ax1.set_xlabel('Epoch')
ax1.legend()
ax2.plot(history.history['loss'], label='Train')
ax2.plot(history.history['val_loss'], label='Validation')
ax2.set_title('Loss')
ax2.set_xlabel('Epoch')
ax2.legend()
plt.tight_layout()
plt.show()
results = evaluate_model(model, x_test, y_test)
print_evaluation_report(results)
Accuracy: 0.8691 Precision: 0.8710 Recall: 0.8691 Most confused pairs (true -> predicted, count): 'F' -> 'f': 167 'L' -> '1': 152 '0' -> 'O': 119 'O' -> '0': 111 'I' -> '1': 91 'q' -> '9': 84 'L' -> 'I': 83 '9' -> 'q': 81 'f' -> 'F': 78 '1' -> 'I': 77
cm = results['confusion_matrix']
plt.figure(figsize=(14, 12))
sns.heatmap(cm, annot=False, fmt='d', cmap='Blues',
xticklabels=LABELS, yticklabels=LABELS)
plt.xlabel('Predicted')
plt.ylabel('True')
plt.title('Confusion Matrix')
plt.tight_layout()
plt.show()
confused = get_most_confused_pairs(cm, top_n=15)
print("Top 15 confused pairs (true -> predicted):")
print("-" * 40)
for true_label, pred_label, count in confused:
print(f" '{true_label}' -> '{pred_label}': {count}")
Top 15 confused pairs (true -> predicted): ---------------------------------------- 'F' -> 'f': 167 'L' -> '1': 152 '0' -> 'O': 119 'O' -> '0': 111 'I' -> '1': 91 'q' -> '9': 84 'L' -> 'I': 83 '9' -> 'q': 81 'f' -> 'F': 78 '1' -> 'I': 77 'g' -> 'q': 63 '2' -> 'Z': 59 'g' -> '9': 45 '1' -> 'L': 40 '5' -> 'S': 29
import os
os.makedirs("models", exist_ok=True)
save_model_keras(model, "models/emnist_model.keras")
print("Model saved to models/emnist_model.keras")
Model saved to models/emnist_model.keras
import os
import base64
from IPython.display import display, HTML
if 'google.colab' in sys.modules:
path = os.path.abspath("models/emnist_model.keras")
with open(path, "rb") as f:
data = base64.b64encode(f.read()).decode()
filename = os.path.basename(path)
display(HTML(f"""
<button onclick="document.getElementById('dl-keras').click()"
style="padding:8px 16px;background:#4CAF50;color:white;border:none;border-radius:4px;cursor:pointer;font-size:14px;">
Baixar {filename}
</button>
<a id="dl-keras" href="data:application/octet-stream;base64,{data}" download="{filename}" style="display:none"></a>
"""))