2 Caso de estudio
2.1 Construcción del conjunto de datos
El reconocimiento óptico de caracteres para sistemas de escritura indígenas, particularmente para el alfabeto Náhuatl, presenta una limitación inicial importante: no existe un conjunto de datos público, estandarizado y etiquetado que permita entrenar modelos de aprendizaje profundo. A diferencia de otros alfabetos, para los cuales existen repositorios ampliamente documentados, el alfabeto Náhuatl carece de recursos computacionales que puedan emplearse directamente en tareas de clasificación automática.
Ante esta situación, fue necesario construir un conjunto de datos propio. La decisión de generar un dataset sintético se tomó por dos razones principales:
- Proporcionar un número suficiente de ejemplos para el entrenamiento de la red neuronal.
- Introducir variaciones geométricas que permitan al modelo generalizar ante deformaciones presentes en documentos reales.
2.1.1 Generación de las muestras
Inicialmente se diseñaron de forma manual 25 ejemplos base para cada carácter del alfabeto. Posteriormente, a cada muestra se le aplicaron transformaciones geométricas y perturbaciones aleatorias para incrementar la variabilidad del conjunto de datos.
La Figura @fig:dataset muestra algunos ejemplos de las imágenes generadas.
2.2 Carga y organización de los datos
Una vez construido el conjunto de datos, las imágenes se almacenaron siguiendo una estructura jerárquica de directorios, donde cada carpeta representa una clase distinta del alfabeto.
El cargado de las imágenes se realizó mediante la función image_dataset_from_directory de TensorFlow:
train_ds = tf.keras.utils.image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset="training",
seed=123,
image_size=(28,28),
batch_size=32,
color_mode="grayscale"
)
val_ds = tf.keras.utils.image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset="validation",
seed=123,
image_size=(28,28),
batch_size=32,
color_mode="grayscale"
)La elección de una partición de entrenamiento-validación de 80%-20% constituye una práctica común en aprendizaje automático, ya que permite disponer de un número suficiente de ejemplos para el ajuste de los parámetros del modelo y, al mismo tiempo, reservar un conjunto independiente para evaluar la capacidad de generalización.
Asimismo, las imágenes se convirtieron a escala de grises. Dado que los glifos del alfabeto Náhuatl se distinguen principalmente por su geometría y no por información cromática, el uso de un solo canal reduce el costo computacional sin sacrificar información relevante.
2.3 Normalización y aumentación de datos
Antes de alimentar las imágenes a la red neuronal, se realizó un proceso de normalización:
normalization_layer = tf.keras.layers.Rescaling(1./255)Esta transformación reescala los valores de intensidad al intervalo \([0,1]\), lo cual mejora la estabilidad numérica del proceso de entrenamiento y facilita la convergencia del algoritmo de optimización.
Adicionalmente, se implementó un mecanismo de aumentación de datos en línea:
data_augmentation = tf.keras.Sequential([
tf.keras.layers.RandomRotation(0.05),
tf.keras.layers.RandomZoom(0.1),
tf.keras.layers.RandomTranslation(0.05, 0.05)
])La introducción de pequeñas rotaciones, cambios de escala y traslaciones tiene como objetivo simular las variaciones presentes en documentos escaneados o en la escritura manual. De esta manera, el modelo aprende representaciones más robustas y reduce su tendencia al sobreajuste.
Finalmente, los datos fueron almacenados temporalmente en memoria y preparados para alimentar la GPU:
train_ds = train_ds.cache().shuffle(5000).prefetch(AUTOTUNE)
val_ds = val_ds.cache().prefetch(AUTOTUNE)El uso de cache() evita que las imágenes sean leídas repetidamente desde el disco, mientras que prefetch() permite superponer las operaciones de lectura de datos con el entrenamiento de la red, disminuyendo el tiempo total de ejecución.
3 Arquitectura de la red neuronal convolucional
La arquitectura propuesta recibe imágenes de tamaño \(28\times28\times1\) y está compuesta por tres bloques convolucionales seguidos de un clasificador denso.
model = tf.keras.Sequential([
tf.keras.layers.Input(shape=(28,28,1)),
tf.keras.layers.Conv2D(32,3,activation='relu'),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.MaxPooling2D(),
tf.keras.layers.SpatialDropout2D(0.25),
tf.keras.layers.Conv2D(64,3,activation='relu'),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.MaxPooling2D(),
tf.keras.layers.SpatialDropout2D(0.25),
tf.keras.layers.Conv2D(128,3,activation='relu'),
tf.keras.layers.MaxPooling2D(),
tf.keras.layers.SpatialDropout2D(0.25),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(256, activation='relu'),
tf.keras.layers.Dropout(0.5),
tf.keras.layers.Dense(num_classes, activation='softmax')
])3.1 Primer bloque convolucional
La primera capa convolucional emplea 32 filtros de tamaño \(3\times3\):
tf.keras.layers.Conv2D(32,3,activation='relu')Este bloque se encarga de detectar características de bajo nivel, tales como bordes, esquinas y pequeños segmentos de los trazos.
La incorporación de una capa de normalización por lotes:
tf.keras.layers.BatchNormalization()permite estabilizar la distribución de las activaciones internas, acelerando el proceso de convergencia.
Posteriormente, la operación de max pooling
tf.keras.layers.MaxPooling2D()reduce la dimensionalidad espacial de las representaciones, disminuyendo el número de parámetros y haciendo al modelo menos sensible a pequeñas traslaciones.
Finalmente, se incorpora un mecanismo de regularización:
tf.keras.layers.SpatialDropout2D(0.25)que desactiva mapas de características completos de manera aleatoria, obligando a la red a aprender representaciones más robustas.
3.2 Segundo y tercer bloque convolucional
Los bloques posteriores incrementan progresivamente el número de filtros hasta 64 y 128, respectivamente. El aumento en la profundidad de la red permite capturar patrones cada vez más complejos, tales como combinaciones de curvas, intersecciones y configuraciones geométricas características de cada glifo.
3.3 Clasificador
Las características extraídas se transforman en un vector unidimensional:
tf.keras.layers.Flatten()y posteriormente se procesan mediante una capa densa:
tf.keras.layers.Dense(256, activation='relu')La capa de salida utiliza la función Softmax:
tf.keras.layers.Dense(num_classes, activation='softmax')la cual produce una distribución de probabilidad sobre todas las clases del alfabeto.
4 Entrenamiento
El modelo se compiló utilizando el optimizador Adam:
model.compile(
optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy']
)La función de pérdida de entropía cruzada dispersa se seleccionó debido a que las etiquetas de las imágenes se encuentran codificadas como enteros y no mediante vectores one-hot.
Para evitar el sobreajuste se implementó un mecanismo de detención temprana:
early_stop = EarlyStopping(
monitor='val_loss',
patience=7,
restore_best_weights=True
)La elección de siete épocas de paciencia permite al modelo continuar el aprendizaje ante pequeñas oscilaciones de la pérdida de validación, evitando finalizar el entrenamiento de forma prematura.
Asimismo, el mejor modelo obtenido se almacenó automáticamente:
checkpoint = ModelCheckpoint(
ruta_modelo,
monitor='val_loss',
save_best_only=True
)5 Sistema de inferencia interactivo
Finalmente, se desarrolló una interfaz interactiva en Google Colab que permite dibujar caracteres del alfabeto Náhuatl y obtener predicciones en tiempo real.
La imagen generada por el usuario se redimensiona a \(28\times28\) píxeles:
img_resized = cv2.resize(img, (28,28))
img_normalized = img_resized / 255.0
img_input = np.expand_dims(img_normalized, axis=(0,-1))Posteriormente, el modelo produce un vector de probabilidades:
predicciones = model.predict(img_input)y se determina la clase más probable mediante:
idx_ganador = np.argmax(predicciones)Además de la predicción principal, la interfaz presenta las tres clases con mayor probabilidad, proporcionando información adicional sobre la confianza y las posibles ambigüedades del modelo.