La reconnaissance de formules mathématiqeus manuscrites est un défi complexe en traitement d'images, en raison de la structure bidimensionnelle intrinsèque des formules. Les méthodes traditionnelles basées sur le CRNN ne sont pas adaptées à cette tâche. L'apprentissage profond a permis le développement de modèles de bout en bout, offrant des performances supérieures sur des ensembles de données publics. Cet article présente l'implémentation d'un algorithme de reconnaissance de formules basé sur une architecture Seq2Seq avec mécanisme d'attentoin.
Architecture du modèle
Le modèle repose sur une architecture Seq2Seq combinant un encodeur CNN et un décodeur LSTM. Une couche d'attention est insérée entre l'encodeur et le décodeur pour capturer les relations spatiales. L'encodeur extrait des caractéristiques visuelles de l'image de la formule, tandis que le décodeur génère la séquence de symboles correspondante.
Implémentation de l'encodeur convolutionnel
L'encodeur utilise une série de couches convolutionnelles pour transformer l'image d'entrée en une carte de caractéristiques. Voici une implémentation modifiée avec des noms de variables et une structure alternatifs :
class ReseauConvolutifEncodeur:
def __init__(self, configuration):
self._config = configuration
def __call__(self, image_entree, taux_dropout):
with tf.variable_scope("Encodeur"):
image_normalisee = tf.cast(image_entree, tf.float32) - 128.0
image_normalisee = image_normalisee / 128.0
with tf.variable_scope("couches_convolutives"):
# Première couche convolutive avec pooling
caracteristiques = tf.layers.conv2d(image_normalisee, 64, 3, strides=1, padding="SAME", activation=tf.nn.relu)
caracteristiques = tf.layers.max_pooling2d(caracteristiques, pool_size=2, strides=2, padding="SAME")
# Deuxième couche convolutive avec pooling
caracteristiques = tf.layers.conv2d(caracteristiques, 128, 3, strides=1, padding="SAME", activation=tf.nn.relu)
caracteristiques = tf.layers.max_pooling2d(caracteristiques, pool_size=2, strides=2, padding="SAME")
# Couches convolutives supplémentaires
caracteristiques = tf.layers.conv2d(caracteristiques, 256, 3, strides=1, padding="SAME", activation=tf.nn.relu)
caracteristiques = tf.layers.conv2d(caracteristiques, 256, 3, strides=1, padding="SAME", activation=tf.nn.relu)
# Application de pooling conditionnel
if self._config.type_encodeur == "classique":
caracteristiques = tf.layers.max_pooling2d(caracteristiques, pool_size=(2, 1), strides=(2, 1), padding="SAME")
caracteristiques = tf.layers.conv2d(caracteristiques, 512, 3, strides=1, padding="SAME", activation=tf.nn.relu)
if self._config.type_encodeur == "classique":
caracteristiques = tf.layers.max_pooling2d(caracteristiques, pool_size=(1, 2), strides=(1, 2), padding="SAME")
if self._config.type_encodeur == "avance":
caracteristiques = tf.layers.conv2d(caracteristiques, 512, kernel_size=(2, 4), strides=2, padding="SAME")
# Couche finale de convolution
caracteristiques = tf.layers.conv2d(caracteristiques, 512, 3, strides=1, padding="VALID", activation=tf.nn.relu)
# Intégration des embeddings positionnels
if self._config.utiliser_embeddings_positionnels:
caracteristiques = ajouter_encodage_temporel(caracteristiques)
return caracteristiques
Après extraction, les caractéristiques sont aplaties pour le décodage. La dimension de sortie est [N x H x W x 512], où N est la taille du lot, H et W les dimensions spatiales, et 512 le nombre de canaux. L'aplatissement est réalisé comme suit :
taille_lot = tf.shape(image_entree)[0]
hauteur, largeur = tf.shape(image_entree)[1], tf.shape(image_entree)[2]
canaux = image_entree.shape[3].value
image_aplatie = tf.reshape(image_entree, shape=[taille_lot, hauteur * largeur, canaux])
Pour préserver les informations spatiales lors de l'aplatissement, des encodages positionnels sont ajoutés. Ces encodages utilisent des fonctions sinusoïdales pour représenter les positions relatives dans la carte de caractéristiques. La fonction suivante implémente cet encodage :
import math
def ajouter_encodage_temporel(x, echelle_min=1.0, echelle_max=1.0e4):
forme_statique = x.get_shape().as_list()
nombre_dimensions = len(forme_statique) - 2
canaux = tf.shape(x)[-1]
nombre_echelles = canaux // (nombre_dimensions * 2)
increment_log = (
math.log(float(echelle_max) / float(echelle_min)) /
(tf.to_float(nombre_echelles) - 1))
echelles_inverses = echelle_min * tf.exp(
tf.to_float(tf.range(nombre_echelles)) * -increment_log)
for dim in range(nombre_dimensions):
longueur = tf.shape(x)[dim + 1]
positions = tf.to_float(tf.range(longueur))
temps_ajuste = tf.expand_dims(positions, 1) * tf.expand_dims(
echelles_inverses, 0)
signal = tf.concat([tf.sin(temps_ajuste), tf.cos(temps_ajuste)], axis=1)
remplissage_avant = dim * 2 * nombre_echelles
remplissage_arriere = canaux - (dim + 1) * 2 * nombre_echelles
signal = tf.pad(signal, [[0, 0], [remplissage_avant, remplissage_arriere]])
for _ in range(1 + dim):
signal = tf.expand_dims(signal, 0)
for _ in range(nombre_dimensions - 1 - dim):
signal = tf.expand_dims(signal, -2)
x += signal
return x
Cette technique permet d'enrichir les caractéristiques extraites avec des informations de position, améliorant ainsi la précision de la reconnaissance des structures spatiales des formules.