Cette constatation est triste, mais elle n'en est pas moins réelle : les gens passent leur temps à écrire des programmes que d'autres ont déjà réalisés. Combien de personnes ont écrit un module de tri, de localisation, ou d'apprentissage ? Il apparaît que, en général, lorsque l'on a besoin d'un tel programme, le plus simple est souvent d'en écrire une version adaptée à ce nouveau problème. En effet, il n'existe aucune banque de programmes réutilisables que l'on puisse utiliser facilement.
Je me suis donc attelé à la tâche de généraliser les algorithmes que j'ai étudiés, de façon à permettre leur utilisation dans un contexte différent. Afin de généraliser les algorithmes, je me suis appuyé sur une structure à base d'objets. En effet, cette approche permet à la fois de segmenter le problème, et de profiter de la notion d'héritage. La segmentation du problème en objets oblige ces derniers à communiquer à travers une interface bien définie. En dehors de cette interface, l'architecture interne de chacun des objets peut être tout à fait quelconque. L'héritage permet quant à lui de généraliser l'approche en ne spécifiant que les caractéristiques obligatoires de chaque objet. Ainsi, l'algorithme de reconnaissance défini pour un HMM peut être utilisé de façon transparente pour un POMDP, car un POMDP est un HMM auquel on a ajouté les notions d'actions et de récompenses.
Cette démarche a donc conduit à la création d'une bibliothèque JAVA qui offre de nombreux algorithmes issus de l'Intelligence Artificielle, ainsi que les structures de données associées. La documentation de cette bibliothèque est disponible ici. Elle est encore incomplète, mais ce point est en cours de correction.
L'architecture d'agent intelligent que je propose se base sur la notion de flux de données. En effet, le calcul de la réaction de l'agent à un stimulus est fait grâce à une suite de traitements. Chaque traitement s'appuie sur le résultat des précédents, et fournit un ensemble de valeurs en retour. Cet ensemble permet à son tour l'exécution des modules ayant besoin de ces valeurs pour mener à bien leurs propres calculs. L'exécution des modules est donc liée à un flux d'informations qui débute par l'arrivée d'un stimulus et qui se termine par la réaction de l'agent.
J'ai donc réalisé une interface de communication générique permettant à un module d'envoyer des données à d'autres modules. Cette interface Java est à la base de toute la structure de l'agent et permet la construction des agents de façon très simple. Elle permet en effet de décrire les données nécéssaires ou produites par un module quelconque, de gérer la connexion entre modules, ... En utilisant cette interface, il est donc possible d'intégrer dans une architecture un module dont l'implantation est inconnue. La réalisation d'un nouvel agent se traduit donc par l'instanciation de modules préfabriqués, puis par leur interconnexion. On voit donc rapidement que le code nécessaire à cette tâche est très répétitif.
Afin de permettre l'écriture automatique de ces lignes de code, nous avons décidé de créer une interface visuelle de développement. Cette application permet donc de créer un module à partir de la bibliothèque, de le paramètrer et de le relier aux autres modules du projet. Une fois le diagramme réalisé, il peut être simulé, compilé, ou exporté. Je vais donc présenter successivement les étapes de création, de liaison, de simulation et de compilation d'un module.
Afin de pouvoir être intégré dans l'interface visuelle, un module doit respecter 2 interfaces Java : IFlux et BoxModelObject. La première gère les aspects utiles du module, tels que la connexion et l'envoi de données. La seconde interface gère tous les aspects visuels de l'intégration dans l'application de développement. Elle n'est donc pas nécéssaire à l'exécution de l'agent. En particulier, ces aspects incluent l'apparence du module à l'écran, des commentaires à destination de l'utilisateur, et surtout les routines d'exportation du module.
Une classe Java doit aussi offrir au moins un constructeur. Afin de rendre cette construction automatique, il est nécessaire de contraindre la forme de ce constructeur. Actuellement, deux formes sont autorisées : une forme sans paramètre, et une forme recevant un vecteur de paramètres. Lorsque la première forme est utilisée, l'interface demande ensuite au module de fournir un panneau de configuration permettant de définir ses paramètres. Avec la seconde, le vecteur contient toutes les informations nécessaires au paramètrage du module.
Un module peut être relié aux de plusieurs façons :
Le lien par programme est évidemment géré par chaque module de façon individuelle. L'application se borne simplement à renseigner un module qui le demande en lui fournissant la liste des modules satisfaisant une contrainte donnée. Ainsi, un capteur peut demander à recevoir la liste des modules qui implémentent l'interface IFournisseurMarkov.
Les deux autres liens sont définis par l'utilisateur. Pour cela, ce dernier dispose de broches, représentant graphiquement les entrées, les sorties, et les utilisations des modules. En cliquant sur l'une de ces broches, l'utilisateur peut connecter deux modules. L'application représente alors cet état par une ligne joignant une sortie à l'entrée devant recevoir les données, ou une broche d'utilisation au module utilisé.
La mise au point d'un système à base de flux peut être assez délicate. Afin de faciliter cette démarche, j'ai créé une interface utilisateur permettant de tracer le fonctionnement du flux, de façon totalement indépendante des modules. De cette manière, l'utilisateur peut développer et mettre au point un système à partir de composants d'origines diverses, sans se préoccuper de savoir s'ils sont compatibles ou non avec le processus de débogage. Les facilité de mises au point offertes sont les suivantes : A chaque activation d'un flux, c'est à dire à chaque envoi de données de module à module, le diagramme est mis en attente, et l'utilisateur peut visualiser les données envoyées, ainsi que les modules sources et destination concernés. Il peut à ce moment décider d'envoyer les données, où de les bloquer. De plus, le diagramme est analysé, et toute broche non connectée est associée à une fenêtre permettant d'y injecter des données ou de visualiser les données qui en proviennent. Finalement, certains modules ont la possibilité d'afficher leur propre fenêtre de débogage, afin de permettre à l'utilisateur d'interagir de façon plus adaptée avec le module en question. Ainsi, par exemple, le module Memoire offre à l'utilisateur la possibilité d'effacer les données mémorisées, ou au contraire de les restituer sur sa sortie.
Les diagrammes créés peuvent être sauvegardés sur le disque dur, sous la forme d'un fichier texte à balises. L'utilisateur peut ainsi paramétrer les modules manuellement s'il le désire, pour insérer de grandes quantités de données répétitives par exemple. Etant donné que l'architecture de l'application est ouverte, il est possible d'y ajouter n'importe quel type de module. Afin de permettre une sauvegarde commune, un format générique est imposé. Cependant, chaque module est responsable de sa propre sauvegarde. De cette façon, un module peut sauvegarder autant de paramètres qu'il le désire, sous la forme qu'il désire.
Une fois le diagramme terminé, il est nécessaire de l'incorporer à une application hôte afin de pouvoir l'utiliser. En général, les diagrammes ne sont pas autonomes, et ils nécessitent quelqu'un pour leur envoyer des données et pour récupérer les résulats calculés. A cette fin, deux possiblités sont proposées à l'utilisateur. La première consiste à lire le fichier de sauvegarde depuis le disque et à en créer une copie exécutable en mémoire. La seconde consiste à compiler le diagramme sous la forme d'une classe Java qui sera utilisée de façon habituelle.
Dans le premier cas, une classe LecteurDiagramme est fournie. Cet objet se charge de la tâche de lire le fichier et à créer les objets en mémoire. Il s'agit d'une classe abstraite, et l'utilisateur doit fournir une méthode stocke pour la complèter. Si elle le désire, cette méthode peut créer des points d'accès à certains modules, car elle est appelée à la suite de l'instanciation de chaque module. Par ce biais, l'utilisateur peut donc rendre certains modules disponibles à son application, en affichant par exemple certains modules à l'écran.
Dans le second cas, et si les modules sont compatibles avec cette possibilité, une classe peut être créée. Cette nouvelle classe se chargera alors d'instancier le diagramme en mémoire, et de publier chaque module sous un nom dérivé de l'intitulé affiché par l'application.
En construction....