477 lines
18 KiB
Plaintext
477 lines
18 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {
|
||
"collapsed": true
|
||
},
|
||
"source": [
|
||
"# Implémentation d'un système expert animalier\n",
|
||
"\n",
|
||
"Un exemple du [curriculum AI for Beginners](http://github.com/microsoft/ai-for-beginners).\n",
|
||
"\n",
|
||
"Dans cet exemple, nous allons implémenter un système simple basé sur la connaissance pour déterminer un animal en fonction de certaines caractéristiques physiques. Le système peut être représenté par l'arbre AND-OR suivant (il s'agit d'une partie de l'arbre complet, nous pouvons facilement ajouter d'autres règles) :\n",
|
||
"\n",
|
||
"\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Notre propre shell de systèmes experts avec inférence rétrograde\n",
|
||
"\n",
|
||
"Essayons de définir un langage simple pour la représentation des connaissances basé sur des règles de production. Nous utiliserons des classes Python comme mots-clés pour définir les règles. Il y aurait essentiellement 3 types de classes :\n",
|
||
"* `Ask` représente une question qui doit être posée à l'utilisateur. Elle contient l'ensemble des réponses possibles.\n",
|
||
"* `If` représente une règle, et ce n'est qu'un sucre syntaxique pour stocker le contenu de la règle\n",
|
||
"* `AND`/`OR` sont des classes pour représenter les branches ET/OU de l'arbre. Elles stockent simplement la liste des arguments à l'intérieur. Pour simplifier le code, toute la fonctionnalité est définie dans la classe parente `Content`\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 1,
|
||
"metadata": {
|
||
"trusted": true
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"class Ask():\n",
|
||
" def __init__(self,choices=['y','n']):\n",
|
||
" self.choices = choices\n",
|
||
" def ask(self):\n",
|
||
" if max([len(x) for x in self.choices])>1:\n",
|
||
" for i,x in enumerate(self.choices):\n",
|
||
" print(\"{0}. {1}\".format(i,x),flush=True)\n",
|
||
" x = int(input())\n",
|
||
" return self.choices[x]\n",
|
||
" else:\n",
|
||
" print(\"/\".join(self.choices),flush=True)\n",
|
||
" return input()\n",
|
||
"\n",
|
||
"class Content():\n",
|
||
" def __init__(self,x):\n",
|
||
" self.x=x\n",
|
||
" \n",
|
||
"class If(Content):\n",
|
||
" pass\n",
|
||
"\n",
|
||
"class AND(Content):\n",
|
||
" pass\n",
|
||
"\n",
|
||
"class OR(Content):\n",
|
||
" pass"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Dans notre système, la mémoire de travail contiendrait la liste des **faits** sous forme de **paires attribut-valeur**. La base de connaissances peut être définie comme un grand dictionnaire qui associe des actions (nouveaux faits devant être insérés dans la mémoire de travail) à des conditions, exprimées sous forme d'expressions ET-OU. De plus, certains faits peuvent être `Ask`-és.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 2,
|
||
"metadata": {
|
||
"trusted": true
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"rules = {\n",
|
||
" 'default': Ask(['y','n']),\n",
|
||
" 'color' : Ask(['red-brown','black and white','other']),\n",
|
||
" 'pattern' : Ask(['dark stripes','dark spots']),\n",
|
||
" 'mammal': If(OR(['hair','gives milk'])),\n",
|
||
" 'carnivor': If(OR([AND(['sharp teeth','claws','forward-looking eyes']),'eats meat'])),\n",
|
||
" 'ungulate': If(['mammal',OR(['has hooves','chews cud'])]),\n",
|
||
" 'bird': If(OR(['feathers',AND(['flies','lies eggs'])])),\n",
|
||
" 'animal:monkey' : If(['mammal','carnivor','color:red-brown','pattern:dark spots']),\n",
|
||
" 'animal:tiger' : If(['mammal','carnivor','color:red-brown','pattern:dark stripes']),\n",
|
||
" 'animal:giraffe' : If(['ungulate','long neck','long legs','pattern:dark spots']),\n",
|
||
" 'animal:zebra' : If(['ungulate','pattern:dark stripes']),\n",
|
||
" 'animal:ostrich' : If(['bird','long nech','color:black and white','cannot fly']),\n",
|
||
" 'animal:pinguin' : If(['bird','swims','color:black and white','cannot fly']),\n",
|
||
" 'animal:albatross' : If(['bird','flies well'])\n",
|
||
"}"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Pour effectuer l'inférence à rebours, nous définirons la classe `Knowledgebase`. Elle contiendra :\n",
|
||
"* Une `memory` de travail - un dictionnaire qui associe des attributs à des valeurs\n",
|
||
"* Des `rules` de la base de connaissances au format défini ci-dessus\n",
|
||
"\n",
|
||
"Les deux méthodes principales sont :\n",
|
||
"* `get` pour obtenir la valeur d'un attribut, en effectuant l'inférence si nécessaire. Par exemple, `get('color')` obtiendra la valeur d'un emplacement de couleur (elle demandera si nécessaire, et stockera la valeur pour une utilisation ultérieure dans la mémoire de travail). Si l'on demande `get('color:blue')`, elle demandera une couleur, puis retournera une valeur `y`/`n` selon la couleur.\n",
|
||
"* `eval` effectue l'inférence réelle, c’est-à-dire qu’elle parcourt l’arbre AND/OR, évalue les sous-objectifs, etc.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 3,
|
||
"metadata": {
|
||
"trusted": true
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"class KnowledgeBase():\n",
|
||
" def __init__(self,rules):\n",
|
||
" self.rules = rules\n",
|
||
" self.memory = {}\n",
|
||
" \n",
|
||
" def get(self,name):\n",
|
||
" if ':' in name:\n",
|
||
" k,v = name.split(':')\n",
|
||
" vv = self.get(k)\n",
|
||
" return 'y' if v==vv else 'n'\n",
|
||
" if name in self.memory.keys():\n",
|
||
" return self.memory[name]\n",
|
||
" for fld in self.rules.keys():\n",
|
||
" if fld==name or fld.startswith(name+\":\"):\n",
|
||
" # print(\" + proving {}\".format(fld))\n",
|
||
" value = 'y' if fld==name else fld.split(':')[1]\n",
|
||
" res = self.eval(self.rules[fld],field=name)\n",
|
||
" if res!='y' and res!='n' and value=='y':\n",
|
||
" self.memory[name] = res\n",
|
||
" return res\n",
|
||
" if res=='y':\n",
|
||
" self.memory[name] = value\n",
|
||
" return value\n",
|
||
" # field is not found, using default\n",
|
||
" res = self.eval(self.rules['default'],field=name)\n",
|
||
" self.memory[name]=res\n",
|
||
" return res\n",
|
||
" \n",
|
||
" def eval(self,expr,field=None):\n",
|
||
" # print(\" + eval {}\".format(expr))\n",
|
||
" if isinstance(expr,Ask):\n",
|
||
" print(field)\n",
|
||
" return expr.ask()\n",
|
||
" elif isinstance(expr,If):\n",
|
||
" return self.eval(expr.x)\n",
|
||
" elif isinstance(expr,AND) or isinstance(expr,list):\n",
|
||
" expr = expr.x if isinstance(expr,AND) else expr\n",
|
||
" for x in expr:\n",
|
||
" if self.eval(x)=='n':\n",
|
||
" return 'n'\n",
|
||
" return 'y'\n",
|
||
" elif isinstance(expr,OR):\n",
|
||
" for x in expr.x:\n",
|
||
" if self.eval(x)=='y':\n",
|
||
" return 'y'\n",
|
||
" return 'n'\n",
|
||
" elif isinstance(expr,str):\n",
|
||
" return self.get(expr)\n",
|
||
" else:\n",
|
||
" print(\"Unknown expr: {}\".format(expr))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Définissons maintenant notre base de connaissances sur les animaux et effectuons la consultation. Notez que cet appel vous posera des questions. Vous pouvez répondre en tapant `y`/`n` pour les questions oui-non, ou en spécifiant un numéro (0..N) pour les questions avec des réponses à choix multiples plus longues.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 4,
|
||
"metadata": {
|
||
"trusted": true
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"hair\n",
|
||
"y/n\n",
|
||
"sharp teeth\n",
|
||
"y/n\n",
|
||
"claws\n",
|
||
"y/n\n",
|
||
"forward-looking eyes\n",
|
||
"y/n\n",
|
||
"color\n",
|
||
"0. red-brown\n",
|
||
"1. black and white\n",
|
||
"2. other\n",
|
||
"has hooves\n",
|
||
"y/n\n",
|
||
"long neck\n",
|
||
"y/n\n",
|
||
"long legs\n",
|
||
"y/n\n",
|
||
"pattern\n",
|
||
"0. dark stripes\n",
|
||
"1. dark spots\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"'giraffe'"
|
||
]
|
||
},
|
||
"execution_count": 4,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"kb = KnowledgeBase(rules)\n",
|
||
"kb.get('animal')"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Utilisation d’Experta pour l’inférence avant\n",
|
||
"\n",
|
||
"Dans l’exemple suivant, nous allons essayer de mettre en œuvre l’inférence avant en utilisant l’une des bibliothèques pour la représentation des connaissances, [Experta](https://github.com/nilp0inter/experta). **Experta** est une bibliothèque pour créer des systèmes d’inférence avant en Python, conçue pour être similaire au système classique ancien [CLIPS](http://www.clipsrules.net/index.html).\n",
|
||
"\n",
|
||
"Nous aurions aussi pu implémenter nous-mêmes un chaînage avant sans trop de problèmes, mais les implémentations naïves ne sont généralement pas très efficaces. Pour un appariement plus efficace des règles, un algorithme spécial [Rete](https://en.wikipedia.org/wiki/Rete_algorithm) est utilisé.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 5,
|
||
"metadata": {
|
||
"trusted": true
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Collecting git+https://github.com/nilp0inter/experta\n",
|
||
" Cloning https://github.com/nilp0inter/experta to /tmp/pip-req-build-7qurtwk3\n",
|
||
" Running command git clone --filter=blob:none --quiet https://github.com/nilp0inter/experta /tmp/pip-req-build-7qurtwk3\n",
|
||
" Resolved https://github.com/nilp0inter/experta to commit c6d5834b123861f5ae09e7d07027dc98bec58741\n",
|
||
" Installing build dependencies ... \u001b[?25ldone\n",
|
||
"\u001b[?25h Getting requirements to build wheel ... \u001b[?25ldone\n",
|
||
"\u001b[?25h Preparing metadata (pyproject.toml) ... \u001b[?25ldone\n",
|
||
"\u001b[?25hRequirement already satisfied: frozendict~=2.4.6 in /opt/conda/envs/ai4beg/lib/python3.12/site-packages (from experta==1.9.5.dev1) (2.4.7)\n",
|
||
"Collecting schema~=0.6.7 (from experta==1.9.5.dev1)\n",
|
||
" Downloading schema-0.6.8-py2.py3-none-any.whl.metadata (14 kB)\n",
|
||
"Downloading schema-0.6.8-py2.py3-none-any.whl (14 kB)\n",
|
||
"Building wheels for collected packages: experta\n",
|
||
" Building wheel for experta (pyproject.toml) ... \u001b[?25ldone\n",
|
||
"\u001b[?25h Created wheel for experta: filename=experta-1.9.5.dev1-py3-none-any.whl size=34804 sha256=888c459512a5e713f4b674caa9a0f96cfdf07ec0d6eb56cc318ce0653d218014\n",
|
||
" Stored in directory: /tmp/pip-ephem-wheel-cache-1eeii9zy/wheels/3d/e8/bb/22d7956359603fa8dd679aa09f5b8efb3f29991c3986fdc787\n",
|
||
"Successfully built experta\n",
|
||
"Installing collected packages: schema, experta\n",
|
||
"\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m2/2\u001b[0m [experta]\n",
|
||
"\u001b[1A\u001b[2KSuccessfully installed experta-1.9.5.dev1 schema-0.6.8\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"import sys\n",
|
||
"!{sys.executable} -m pip install git+https://github.com/nilp0inter/experta"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 13,
|
||
"metadata": {
|
||
"trusted": true
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from experta import *\n",
|
||
"#import experta"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Nous allons définir notre système comme une classe qui hérite de `KnowledgeEngine`. Chaque règle est définie par une fonction distincte avec l'annotation `@Rule`, qui spécifie quand la règle doit être déclenchée. À l'intérieur de la règle, nous pouvons ajouter de nouveaux faits en utilisant la fonction `declare`, et l'ajout de ces faits entraînera l'appel de certaines règles supplémentaires par le moteur d'inférence avant.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 14,
|
||
"metadata": {
|
||
"trusted": true
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"class Animals(KnowledgeEngine):\n",
|
||
" @Rule(OR(\n",
|
||
" AND(Fact('sharp teeth'),Fact('claws'),Fact('forward looking eyes')),\n",
|
||
" Fact('eats meat')))\n",
|
||
" def cornivor(self):\n",
|
||
" self.declare(Fact('carnivor'))\n",
|
||
" \n",
|
||
" @Rule(OR(Fact('hair'),Fact('gives milk')))\n",
|
||
" def mammal(self):\n",
|
||
" self.declare(Fact('mammal'))\n",
|
||
"\n",
|
||
" @Rule(Fact('mammal'),\n",
|
||
" OR(Fact('has hooves'),Fact('chews cud')))\n",
|
||
" def hooves(self):\n",
|
||
" self.declare('ungulate')\n",
|
||
" \n",
|
||
" @Rule(OR(Fact('feathers'),AND(Fact('flies'),Fact('lays eggs'))))\n",
|
||
" def bird(self):\n",
|
||
" self.declare('bird')\n",
|
||
" \n",
|
||
" @Rule(Fact('mammal'),Fact('carnivor'),\n",
|
||
" Fact(color='red-brown'),\n",
|
||
" Fact(pattern='dark spots'))\n",
|
||
" def monkey(self):\n",
|
||
" self.declare(Fact(animal='monkey'))\n",
|
||
"\n",
|
||
" @Rule(Fact('mammal'),Fact('carnivor'),\n",
|
||
" Fact(color='red-brown'),\n",
|
||
" Fact(pattern='dark stripes'))\n",
|
||
" def tiger(self):\n",
|
||
" self.declare(Fact(animal='tiger'))\n",
|
||
"\n",
|
||
" @Rule(Fact('ungulate'),\n",
|
||
" Fact('long neck'),\n",
|
||
" Fact('long legs'),\n",
|
||
" Fact(pattern='dark spots'))\n",
|
||
" def giraffe(self):\n",
|
||
" self.declare(Fact(animal='giraffe'))\n",
|
||
"\n",
|
||
" @Rule(Fact('ungulate'),\n",
|
||
" Fact(pattern='dark stripes'))\n",
|
||
" def zebra(self):\n",
|
||
" self.declare(Fact(animal='zebra'))\n",
|
||
"\n",
|
||
" @Rule(Fact('bird'),\n",
|
||
" Fact('long neck'),\n",
|
||
" Fact('cannot fly'),\n",
|
||
" Fact(color='black and white'))\n",
|
||
" def straus(self):\n",
|
||
" self.declare(Fact(animal='ostrich'))\n",
|
||
"\n",
|
||
" @Rule(Fact('bird'),\n",
|
||
" Fact('swims'),\n",
|
||
" Fact('cannot fly'),\n",
|
||
" Fact(color='black and white'))\n",
|
||
" def pinguin(self):\n",
|
||
" self.declare(Fact(animal='pinguin'))\n",
|
||
"\n",
|
||
" @Rule(Fact('bird'),\n",
|
||
" Fact('flies well'))\n",
|
||
" def albatros(self):\n",
|
||
" self.declare(Fact(animal='albatross'))\n",
|
||
" \n",
|
||
" @Rule(Fact(animal=MATCH.a))\n",
|
||
" def print_result(self,a):\n",
|
||
" print('Animal is {}'.format(a))\n",
|
||
" \n",
|
||
" def factz(self,l):\n",
|
||
" for x in l:\n",
|
||
" self.declare(x)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Une fois que nous avons défini une base de connaissances, nous remplissons notre mémoire de travail avec quelques faits initiaux, puis appelons la méthode `run()` pour effectuer l'inférence. Vous pouvez voir en résultat que de nouveaux faits inférés sont ajoutés à la mémoire de travail, y compris le fait final concernant l'animal (si nous avons correctement configuré tous les faits initiaux).\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 15,
|
||
"metadata": {
|
||
"trusted": true
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Animal is tiger\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"FactList([(0, InitialFact()),\n",
|
||
" (1, Fact(color='red-brown')),\n",
|
||
" (2, Fact(pattern='dark stripes')),\n",
|
||
" (3, Fact('sharp teeth')),\n",
|
||
" (4, Fact('claws')),\n",
|
||
" (5, Fact('forward looking eyes')),\n",
|
||
" (6, Fact('gives milk')),\n",
|
||
" (7, Fact('mammal')),\n",
|
||
" (8, Fact('carnivor')),\n",
|
||
" (9, Fact(animal='tiger'))])"
|
||
]
|
||
},
|
||
"execution_count": 15,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"ex1 = Animals()\n",
|
||
"ex1.reset()\n",
|
||
"ex1.factz([\n",
|
||
" Fact(color='red-brown'),\n",
|
||
" Fact(pattern='dark stripes'),\n",
|
||
" Fact('sharp teeth'),\n",
|
||
" Fact('claws'),\n",
|
||
" Fact('forward looking eyes'),\n",
|
||
" Fact('gives milk')])\n",
|
||
"ex1.run()\n",
|
||
"ex1.facts"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": []
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"---\n\n<!-- CO-OP TRANSLATOR DISCLAIMER START -->\n**Avertissement** : \nCe document a été traduit à l’aide du service de traduction automatique [Co-op Translator](https://github.com/Azure/co-op-translator). Bien que nous nous efforçons d’assurer l’exactitude, veuillez noter que les traductions automatiques peuvent contenir des erreurs ou des inexactitudes. Le document original dans sa langue native doit être considéré comme la source officielle. Pour les informations critiques, une traduction professionnelle réalisée par un humain est recommandée. Nous déclinons toute responsabilité en cas de malentendus ou d’interprétations erronées résultant de l’utilisation de cette traduction.\n<!-- CO-OP TRANSLATOR DISCLAIMER END -->\n"
|
||
]
|
||
}
|
||
],
|
||
"metadata": {
|
||
"kernelspec": {
|
||
"display_name": "Python 3.7.4 64-bit (conda)",
|
||
"metadata": {
|
||
"interpreter": {
|
||
"hash": "86193a1ab0ba47eac1c69c1756090baa3b420b3eea7d4aafab8b85f8b312f0c5"
|
||
}
|
||
},
|
||
"name": "python3"
|
||
},
|
||
"language_info": {
|
||
"codemirror_mode": {
|
||
"name": "ipython",
|
||
"version": 3
|
||
},
|
||
"file_extension": ".py",
|
||
"mimetype": "text/x-python",
|
||
"name": "python",
|
||
"nbconvert_exporter": "python",
|
||
"pygments_lexer": "ipython3",
|
||
"version": "3.11.2"
|
||
},
|
||
"coopTranslator": {
|
||
"original_hash": "8ef43db4b9182239fd150a76bd494fdb",
|
||
"translation_date": "2026-01-15T10:37:57+00:00",
|
||
"source_file": "lessons/2-Symbolic/Animals.ipynb",
|
||
"language_code": "fr"
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 2
|
||
} |