L'intelligence du sémaphore

C'est quoi un sémaphore ?

chappe.jpeg

Images sorties de Blender

Images adaptées pour l'apprentissage

L'optimisation a montré que la meilleure solution est avec ce type d'image.

Relu Rectifier neural networks

def relu(x):
    """Rectified Linear Unit:
 
    In the context of artificial neural networks, the rectifier is an
    activation function defined as the positive part of its argument.
 
    Rectifie les négatifs à 0:
    -1 > 0
     1 > 1
     """
    return np.maximum(0, x)

Relu_prime

def relu_prime(z):
    """La fonction de Heaviside (également fonction échelon unité, fonction
    marche d'escalier) est la fonction indicatrice de R.
    Une fonction indicatrice, est une fonction définie sur un
    ensemble E qui explicite l’appartenance ou non à un sous-ensemble F de E
    de tout élément de E. 
    C'est donc la fonction H (discontinue en 0) prenant la valeur 1 pour tous
    les réels positifs et la valeur 0 pour les réels strictement négatifs.
    """
    return np.asarray(z > 0, dtype=np.float32)

Sigmoïd

Elle représente la fonction de répartition de la loi logistique. Elle est souvent utilisée dans les réseaux de neurones parce qu'elle est dérivable, ce qui est une contrainte pour l'algorithme de rétropropagation de Werbos. La forme de la dérivée de sa fonction inverse est extrêmement simple et facile à calculer, ce qui améliore les performances des algorithmes.

def sigmoid(x):
    """La fonction sigmoïde est une courbe en S."""
    return 1 / (1 + np.exp(-x))

Sigmoïd prime

def sigmoid_prime(z):
    """La dérivée de la fonction sigmoid."""
    return z * (1 - z)

Algorithme du gradient stochastique

L'algorithme du gradient stochastique est une méthode de descente de gradient (itérative) utilisée pour la minimisation d'une fonction objectif qui est écrite comme une somme de fonctions différentiables.

Diagonale de 1

numpy.eye(N, M=None, k=0, dtype=<class 'float'>, order='C')\\
Return a 2-D array with ones on the diagonal and zeros elsewhere.

Matrice ou la sortie est idéale: le 1 correspond à entée[i] = sortie[i], et entée[j],sortie[k] =0 si j différent de k

Initialisation de X. Glorot et He

X = Xavier = prénom

Un réseau de neurones Perceptron multicouches est un type de réseau dont l'information circule dans un unique sens, de la couche d'entrée vers la couche de sortie. Ont dit qu'il est un réseau “à propagation directe” (feedforward).

Notre réseau

Réseau de neurones:

Une colonne de 1600 en entrée, 2 nodes de 100, une sortie de 27 caractères.

Un réseau de neurones Convolutif est un type de réseau de neurones artificiels dans lequel le motif de connexion entre les neurones est inspiré par le cortex visuel des animaux. Actuellement, il est très utilisé pour l'analyse des images, des vidéos et du langage naturel.

Ce type de réseau est développé avec Yolo Darknet Préparation de mes propres images puis Yolo Darknet sans carte graphique et enfin Yolo Darknet sur un portable Optimus.

Du code expliqué avec beaucoup d'amour Enfin, là c'est de l'intelligence qu'on cherche, pas de l'amour.

La totalité du projet est à Semaphore sur Github, et Jeu du sémaphore dans le Blender Game Engine pour la création des images.

ia.py
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
 
import shutil
import numpy as np
import cv2
from pymultilame import MyTools
 
def sigmoid(x): return 1 / (1 + np.exp(-x))
def sigmoid_prime(z): return z * (1 - z)
def relu(x): return np.maximum(0, x)
def relu_prime(z): return np.asarray(z > 0, dtype=np.float32)
 
class SemaphoreIA:
    def __init__(self, root, learningrate, failed=0):
        self.root = root
        self.learningrate = learningrate
        self.failed = failed
        self.tools = MyTools()
 
        # Dossier des ratés
        if self.failed:
            # Suppression du dossier failed et recréation pour le vider
            try:
                shutil.rmtree(self.root + 'failed')
            except:
                print('Pas de dossier failed')
            self.tools.create_directory(self.root + 'failed')
 
        # Réseau de neurones: colonne 1600 en entrée, 2 nodes de 100, sortie de 27 caractères
        self.layers = [1600, 100, 100, 27]
        # Fonction d'activation: imite l'activation d'un neuronne
        self.activations = [relu, relu, sigmoid]
 
        fichier = np.load(self.root + 'semaphore.npz')
        self.x_train, self.y_train = fichier['x_train'], fichier['y_train']
        self.x_train = 1 - self.x_train
        self.x_test, self.y_test = self.x_train[50000:,:], self.y_train[50000:]
        self.x_train, self.y_train = self.x_train[:50000,:], self.y_train[:50000]
 
        # Affichage des images pour distraire
        cv2.namedWindow('img')
 
    def training(self):
        """Apprentissage avec 60 000 images. Poids enregistré dans weights.npy"""
        print("Training...")
 
        # Matrice diagonale de 1
        diagonale = np.eye(27, 27)
 
        # globals() Return a dictionary representing the current global symbol table.
        self.activations_prime = [globals()[fonction.__name__ + '_prime'] for fonction in self.activations]
 
        node_dict = {}
 
        # Liste des poids
        # Initialisation des poids des nodes, pour ne pas à être à 0
        # Construit 3 matrices (100x1600, 100x100, 27x100)
        # /np.sqrt() résultat expérimental de l'initialisation d'un gars qui s'appelle Xavier Glorot et d'un autre qui s'appelle He !
        weight_list = [np.random.randn(self.layers[k+1], self.layers[k]) / \
                       np.sqrt(self.layers[k]) for k in range(len(self.layers)-1)]
 
        # vecteur_ligne = image en ligne à la 1ère itération
        # nombre_lettre = nombre correspondant à la lettre de l'image
        # i pour itération, vecteur_colonne = x_train de i, nombre_lettre = y_train de i
        for i, (vecteur_ligne, nombre_lettre) in enumerate(zip(self.x_train, self.y_train)):
 
            # Affichage pour distraire les mangalore
            if i % 10000 == 0:
                print(i, nombre_lettre)
                img = vecteur_ligne.reshape(40,40) * 255
                img = cv2.resize(img, (600, 600), interpolation=cv2.INTER_AREA)
                cv2.imshow("img", img)
                cv2.waitKey(1)
 
            # la ligne devient colonne
            vecteur_colonne = np.array(vecteur_ligne, ndmin=2).T
 
            # Forward propagation
            node_dict[0] = vecteur_colonne
            for k in range(len(self.layers)-1):
                # weight_list[k] (100x1600, 100x100 27x100) vecteur_colonne (1600,)
                # z de format 100 x 1
                z = np.dot(weight_list[k], vecteur_colonne)
 
                # self.activations = non linéaire sinon sortie fonction linéaire de l'entrée
                # imite le seuil d'activation électrique du neurone
                vecteur_colonne = self.activations[k](z)
 
                node_dict[k+1] = vecteur_colonne
 
            # Retro propagation, delta_a = écart entre la sortie réelle et attendue
            delta_a = vecteur_colonne - diagonale[:,[nombre_lettre]]
            # Parcours des nodes en sens inverse pour corriger proportionnellement
            # les poids en fonction de l'erreur par rapport à la valeur souhaitée
            # Descente du Gradient stochastique
            for k in range(len(self.layers)-2, -1, -1):
                delta_z = delta_a * self.activations_prime[k](node_dict[k+1])
                delta_w = np.dot(delta_z, node_dict[k].T)
                delta_a = np.dot(weight_list[k].T, delta_z)
                # Pour converger vers le minimum d'erreur
                weight_list[k] -= self.learningrate * delta_w
 
        # Dans un fichier
        np.save(self.root + 'weights.npy', weight_list)
        print('weights.npy enregistré')
        cv2.destroyAllWindows()
 
    def testing(self):
        """Teste avec 10 000 images, retourne le ratio de bon résultats"""
        print("Testing...")
 
        weight_list = np.load(self.root + 'weights.npy')
 
        # Nombre de bonnes reconnaissance
        success = 0
 
        # Dict avec le nombre d'erreurs par lettre
        failed_dict = {}
 
        for vecteur_ligne, nombre_lettre in zip(self.x_test, self.y_test):
            # image en ligne au 1er passage pour les failed
            img = vecteur_ligne.copy()
 
            for k in range(len(self.layers)-1):
                vecteur_ligne = self.activations[k](np.dot(weight_list[k],
                                                      vecteur_ligne))
 
            reconnu = np.argmax(vecteur_ligne)
            if reconnu == nombre_lettre:
                success += 1
            else:
                if self.failed:
                    self.write_failed(img, nombre_lettre, reconnu, success)
                if nombre_lettre in failed_dict:
                    failed_dict[nombre_lettre] += 1
                else:
                    if self.failed:
                        self.tools.create_directory(self.root + 'failed' + '/bad_' + str(nombre_lettre))
                        failed_dict[nombre_lettre] = 1
 
        if self.failed:
            sorted_by_value = sorted(failed_dict.items(), key=lambda kv: kv[1], reverse=True)
            print(sorted_by_value)
 
        resp = 100.0 * success / len(self.x_test)
        return resp
 
    def write_failed(self, img, nombre_lettre, reconnu, S):
        """Les images avec erreur de reconnaisance sont copiées dans
        /semaphore/failed/bad_11/11_6_9067.png
        11 est la lettre k, donc dans le dossier il ny a que la lettre k
        et le 2ème nombre est la lettre reconnue fausse
        """
        name = str(nombre_lettre) + '_' + str(reconnu) + '_'  + str(S) + '.png'
        fichier = self.root + 'failed' + '/bad_' + str(nombre_lettre) + '/' + name
        img = img.reshape(40,40) * 255
        cv2.imwrite(fichier, img)
 
 
if __name__ == "__main__":
    print(MyTools().get_absolute_path(__file__))
    root = MyTools().get_absolute_path(__file__)[:-28]
    print("Current directory:", root)
 
    for i in range(5):
        learningrate = 0.022
        failed = 0
        sia = SemaphoreIA(root, learningrate, failed)
        sia.training()
        resp = sia.testing()
        print("Learningrate: {} Résultat {}".format(learningrate, round(resp, 1)))

L'image en entrée doit être:

  • 40 x 40 pixels
  • en noir et blanc convertit en 0 et 1, que des 0 et des 1 dans le array
  • Floutée entre 5 et 7
  • Convertie en vecteur ligne: img = img.reshape(1600)

Les poids

Le fichier weights.npy doit être dans le dossier du script. weights.npy téléchargeable sur GitHub.

La class Reconnaissance

import numpy as np
import cv2
 
 
def sigmoid(x):
    return 1 / (1 + np.exp(-x))
def relu(x):
    return np.maximum(0, x)
 
class Reconnaissance:
    def __init__(self):
        self.weight = np.load('weights.npy')
 
    def testing(self, img):
        vecteur_ligne = img
        layers = [1600, 100, 100, 27]
        activations = [relu, relu, sigmoid]
        for k in range(len(layers)-1):
            vecteur_ligne = activations[k](np.dot(self.weight[k],
                                                       vecteur_ligne))
            reconnu = np.argmax(vecteur_ligne)
        return reconnu

Adaptation de l'image

# frame peut être la capture d'une webcam
cv2.imshow('RGB Input', frame)
 
# Application d'un seuil pour extraire le sémaphore du fond
# Le seuil est à adapter en fonction de la couleur du sémaphore
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
lower = np.array([120, 80, 80])
upper = np.array([255, 255, 255])
img = cv2.inRange(hsv, lower, upper)
 
# Flou: GaussianBlur semble mieux que Averaging=cv2.blur()
img = cv2.GaussianBlur(img, (5, 5), 0)
 
# Resize
img = cv2.resize(img, (40, 40), interpolation=cv2.INTER_AREA)
 
# Noir et blanc, sans gris
ret, nb = cv2.threshold(img, 2, 255, cv2.THRESH_BINARY)
 
# Valeur 0 ou 1
nb = nb / 255
 
# Reshape pour avoir un vecteur ligne
vect = nb.reshape(40*40)
 
reco = Reconnaissance()
reconnu = reco.testing(vect)
print("Caractère reconnu:", reconnu)

Les fichiers pour un test avec Webcam et un sémaphore en carton

  • Essayer d'implémenter un petit réseau de neurones convolutif pas trop compliqué.
  • Essayer de reconnaître les coordonnées de la position du sémaphore (un quadrilatère entourant le sémaphore sur l'image) en plus de sa valeur.
  • Essayer de reconnaître la forme du sémaphore plutôt que l'associé à un symbole : Reconnaître la position du régulateur et des 2 indicateurs. Les buts étant de pouvoir changer l'alphabet sans réapprendre, et de pouvoir dessiner un sémaphore schématique tel que reconnu par l'IA.
  • Essayer de reconnaître plusieurs sémaphores simultanément. Le but est de pouvoir choisir lequel on veut pouvoir imiter.

Idée directrice

Réaliser 2 apprentissages indépendants pour faciliter l'apprentissage et ajouter du filtrage intermédiaire. Je propose de réaliser un premier apprentissage qui reconnaîtrait la position d'un sémaphore dans une image. Il retournerait ses coordonnées. Le second apprentissage reconnaîtrait la forme d'un sémaphore dans un cadre particulier.

Je vois plusieurs avantages :

  • Nous savons que dans une vidéo, un sémaphore ne peut pas se promener d'un bout à l'autre d'une frame de manière discontinue. On peut donc facilement filtrer les faux positifs de détection d'un sémaphore du premier apprentissage qui se déplaceraient de manière discontinue.
  • Une fois la première reconnaissance réalisé il est facile de recadrer l'image en entrée et la présenter dans un format particulier au second apprentissage (ex: 40px x 40px).
  • Il est plus facile d'apprendre à détecter plusieurs positions de sémaphore simultanés que de reconnaitre la forme de plusieurs sémaphore simultanément.
  • Si nous reconnaissons plusieurs sémaphores simultanément, nous pourrons lancer un traitement de reconnaissance de leur forme pour chaque détection indépendament.
  • Un filtrage intelligent pourrait comprendre qu'un sémaphore est occulté par un obstacle mouvant avant d'etre de nouveau visible.

Petit réseau convolutif

  • En entré, il faudrait une première couche identique, et une deuxième couche qui n'est pas relié à tous les neurones de la première couche, mais uniquement à un ensemble représentant une tuile. Il faut donc modifier l'algo actuel qui relie toujours tous les neurones de la couche N-1 à tous les neurones de la couche N.
  • Comment paver l'image de tuiles ? Est ce que les tuiles doivent se chevaucher ? Il serait interessant de variabiliser la taille des tuiles et leur chevauchement.

Reconnaître les coordonnées du sémaphore

  • Il faut lors de la génération des images ajouter une méta information dans l'image indiquant dans les coordonnées d'un quadrilataire inscrivant le sémaphore.
  • Quels types de coordonnées ? Les coordonées de 2 angles opposés ? les coordonées d'un angle et 2 distances ? autre ?
  • Comment modifier la sortie du réseau de neurones pour reconnaître le couple (position du sémaphore, valeur) ?

Reconnaître la forme du sémaphore

  • Plutôt que de tagger l'image avec un symbole, il faudrait la tagger avec la forme du sémaphore.
  • Quels informations pour coder la forme ? Les 3 Angles absolue du régulateur et des indicateurs ?
  • Comment modifier la sortie du réseau de neurones pour reconnaître le couple (position du sémaphore, forme du sémaphore) ?

Reconnaître plusieurs sémaphores

  • Comment modifier la sortie du réseau de neurones pour reconnaître plusieurs couples (position, forme) ?
  • l_intelligence_du_semaphore.txt
  • Dernière modification: 2019/05/09 08:22
  • par serge