microsoft/AI-For-Beginners
Publicmirrored fromhttps://github.com/microsoft/AI-For-BeginnersAvailable
lessons/2-Symbolic/Animals.ipynb
465lines · modecode
| 1 | { |
| 2 | "cells": [ |
| 3 | { |
| 4 | "cell_type": "markdown", |
| 5 | "metadata": { |
| 6 | "collapsed": true |
| 7 | }, |
| 8 | "source": [ |
| 9 | "# Implementing an Animal Expert System\n", |
| 10 | "\n", |
| 11 | "An example from [AI for Beginners Curriculum](http://github.com/microsoft/ai-for-beginners).\n", |
| 12 | "\n", |
| 13 | "In this sample, we will implement a simple knowledge-based system to determine an animal based on some physical characteristics. The system can be represented by the following AND-OR tree (this is a part of the whole tree, we can easily add some more rules):\n", |
| 14 | "\n", |
| 15 | "" |
| 16 | ] |
| 17 | }, |
| 18 | { |
| 19 | "cell_type": "markdown", |
| 20 | "metadata": {}, |
| 21 | "source": [ |
| 22 | "## Our own expert systems shell with backward inference\n", |
| 23 | "\n", |
| 24 | "Let's try to define a simple language for knowledge representation based on production rules. We will use Python classes as keywords to define rules. There would be essentially 3 types of classes:\n", |
| 25 | "* `Ask` represents a question that needs to be asked to the user. It contains the set of possible answers.\n", |
| 26 | "* `If` represents a rule, and it is just a syntactic sugar to store the content of the rule\n", |
| 27 | "* `AND`/`OR` are classes to represent AND/OR branches of the tree. They just store the list of arguments inside. To simplify code, all functionality is defined in the parent class `Content`" |
| 28 | ] |
| 29 | }, |
| 30 | { |
| 31 | "cell_type": "code", |
| 32 | "execution_count": 1, |
| 33 | "metadata": { |
| 34 | "trusted": true |
| 35 | }, |
| 36 | "outputs": [], |
| 37 | "source": [ |
| 38 | "class Ask():\n", |
| 39 | " def __init__(self,choices=['y','n']):\n", |
| 40 | " self.choices = choices\n", |
| 41 | " def ask(self):\n", |
| 42 | " if max([len(x) for x in self.choices])>1:\n", |
| 43 | " for i,x in enumerate(self.choices):\n", |
| 44 | " print(\"{0}. {1}\".format(i,x),flush=True)\n", |
| 45 | " x = int(input())\n", |
| 46 | " return self.choices[x]\n", |
| 47 | " else:\n", |
| 48 | " print(\"/\".join(self.choices),flush=True)\n", |
| 49 | " return input()\n", |
| 50 | "\n", |
| 51 | "class Content():\n", |
| 52 | " def __init__(self,x):\n", |
| 53 | " self.x=x\n", |
| 54 | " \n", |
| 55 | "class If(Content):\n", |
| 56 | " pass\n", |
| 57 | "\n", |
| 58 | "class AND(Content):\n", |
| 59 | " pass\n", |
| 60 | "\n", |
| 61 | "class OR(Content):\n", |
| 62 | " pass" |
| 63 | ] |
| 64 | }, |
| 65 | { |
| 66 | "cell_type": "markdown", |
| 67 | "metadata": {}, |
| 68 | "source": [ |
| 69 | "In our system, working memory would contain the list of **facts** as **attribute-value pairs**. The knowledgebase can be defined as one big dictionary that maps actions (new facts that should be inserted into working memory) to conditions, expressed as AND-OR expressions. Also, some facts can be `Ask`-ed." |
| 70 | ] |
| 71 | }, |
| 72 | { |
| 73 | "cell_type": "code", |
| 74 | "execution_count": 2, |
| 75 | "metadata": { |
| 76 | "trusted": true |
| 77 | }, |
| 78 | "outputs": [], |
| 79 | "source": [ |
| 80 | "rules = {\n", |
| 81 | " 'default': Ask(['y','n']),\n", |
| 82 | " 'color' : Ask(['red-brown','black and white','other']),\n", |
| 83 | " 'pattern' : Ask(['dark stripes','dark spots']),\n", |
| 84 | " 'mammal': If(OR(['hair','gives milk'])),\n", |
| 85 | " 'carnivor': If(OR([AND(['sharp teeth','claws','forward-looking eyes']),'eats meat'])),\n", |
| 86 | " 'ungulate': If(['mammal',OR(['has hooves','chews cud'])]),\n", |
| 87 | " 'bird': If(OR(['feathers',AND(['flies','lies eggs'])])),\n", |
| 88 | " 'animal:monkey' : If(['mammal','carnivor','color:red-brown','pattern:dark spots']),\n", |
| 89 | " 'animal:tiger' : If(['mammal','carnivor','color:red-brown','pattern:dark stripes']),\n", |
| 90 | " 'animal:giraffe' : If(['ungulate','long neck','long legs','pattern:dark spots']),\n", |
| 91 | " 'animal:zebra' : If(['ungulate','pattern:dark stripes']),\n", |
| 92 | " 'animal:ostrich' : If(['bird','long nech','color:black and white','cannot fly']),\n", |
| 93 | " 'animal:pinguin' : If(['bird','swims','color:black and white','cannot fly']),\n", |
| 94 | " 'animal:albatross' : If(['bird','flies well'])\n", |
| 95 | "}" |
| 96 | ] |
| 97 | }, |
| 98 | { |
| 99 | "cell_type": "markdown", |
| 100 | "metadata": {}, |
| 101 | "source": [ |
| 102 | "To perform the backward inference, we will define `Knowledgebase` class. It will contain:\n", |
| 103 | "* Working `memory` - a dictionary that maps attributes to values\n", |
| 104 | "* Knowledgebase `rules` in the format as defined above\n", |
| 105 | "\n", |
| 106 | "Two main methods are:\n", |
| 107 | "* `get` to obtain the value of an attribute, performing inference if necessary. For example, `get('color')` would get the value of a color slot (it will ask if necessary, and store the value for later usage in the working memory). If we ask `get('color:blue')`, it will ask for a color, and then return `y`/`n` value depending on the color.\n", |
| 108 | "* `eval` performs the actual inference, i.e. traverses AND/OR tree, evaluates sub-goals, etc." |
| 109 | ] |
| 110 | }, |
| 111 | { |
| 112 | "cell_type": "code", |
| 113 | "execution_count": 3, |
| 114 | "metadata": { |
| 115 | "trusted": true |
| 116 | }, |
| 117 | "outputs": [], |
| 118 | "source": [ |
| 119 | "class KnowledgeBase():\n", |
| 120 | " def __init__(self,rules):\n", |
| 121 | " self.rules = rules\n", |
| 122 | " self.memory = {}\n", |
| 123 | " \n", |
| 124 | " def get(self,name):\n", |
| 125 | " if ':' in name:\n", |
| 126 | " k,v = name.split(':')\n", |
| 127 | " vv = self.get(k)\n", |
| 128 | " return 'y' if v==vv else 'n'\n", |
| 129 | " if name in self.memory.keys():\n", |
| 130 | " return self.memory[name]\n", |
| 131 | " for fld in self.rules.keys():\n", |
| 132 | " if fld==name or fld.startswith(name+\":\"):\n", |
| 133 | " # print(\" + proving {}\".format(fld))\n", |
| 134 | " value = 'y' if fld==name else fld.split(':')[1]\n", |
| 135 | " res = self.eval(self.rules[fld],field=name)\n", |
| 136 | " if res!='y' and res!='n' and value=='y':\n", |
| 137 | " self.memory[name] = res\n", |
| 138 | " return res\n", |
| 139 | " if res=='y':\n", |
| 140 | " self.memory[name] = value\n", |
| 141 | " return value\n", |
| 142 | " # field is not found, using default\n", |
| 143 | " res = self.eval(self.rules['default'],field=name)\n", |
| 144 | " self.memory[name]=res\n", |
| 145 | " return res\n", |
| 146 | " \n", |
| 147 | " def eval(self,expr,field=None):\n", |
| 148 | " # print(\" + eval {}\".format(expr))\n", |
| 149 | " if isinstance(expr,Ask):\n", |
| 150 | " print(field)\n", |
| 151 | " return expr.ask()\n", |
| 152 | " elif isinstance(expr,If):\n", |
| 153 | " return self.eval(expr.x)\n", |
| 154 | " elif isinstance(expr,AND) or isinstance(expr,list):\n", |
| 155 | " expr = expr.x if isinstance(expr,AND) else expr\n", |
| 156 | " for x in expr:\n", |
| 157 | " if self.eval(x)=='n':\n", |
| 158 | " return 'n'\n", |
| 159 | " return 'y'\n", |
| 160 | " elif isinstance(expr,OR):\n", |
| 161 | " for x in expr.x:\n", |
| 162 | " if self.eval(x)=='y':\n", |
| 163 | " return 'y'\n", |
| 164 | " return 'n'\n", |
| 165 | " elif isinstance(expr,str):\n", |
| 166 | " return self.get(expr)\n", |
| 167 | " else:\n", |
| 168 | " print(\"Unknown expr: {}\".format(expr))" |
| 169 | ] |
| 170 | }, |
| 171 | { |
| 172 | "cell_type": "markdown", |
| 173 | "metadata": {}, |
| 174 | "source": [ |
| 175 | "Now let's define our animal knowledgebase and perform the consultation. Note that this call will ask you questions. You can answer by typing `y`/`n` for yes-no questions, or by specifying number (0..N) for questions with longer multiple-choice answers." |
| 176 | ] |
| 177 | }, |
| 178 | { |
| 179 | "cell_type": "code", |
| 180 | "execution_count": 4, |
| 181 | "metadata": { |
| 182 | "trusted": true |
| 183 | }, |
| 184 | "outputs": [ |
| 185 | { |
| 186 | "name": "stdout", |
| 187 | "output_type": "stream", |
| 188 | "text": [ |
| 189 | "hair\n", |
| 190 | "y/n\n", |
| 191 | "sharp teeth\n", |
| 192 | "y/n\n", |
| 193 | "claws\n", |
| 194 | "y/n\n", |
| 195 | "forward-looking eyes\n", |
| 196 | "y/n\n", |
| 197 | "color\n", |
| 198 | "0. red-brown\n", |
| 199 | "1. black and white\n", |
| 200 | "2. other\n", |
| 201 | "has hooves\n", |
| 202 | "y/n\n", |
| 203 | "long neck\n", |
| 204 | "y/n\n", |
| 205 | "long legs\n", |
| 206 | "y/n\n", |
| 207 | "pattern\n", |
| 208 | "0. dark stripes\n", |
| 209 | "1. dark spots\n" |
| 210 | ] |
| 211 | }, |
| 212 | { |
| 213 | "data": { |
| 214 | "text/plain": [ |
| 215 | "'giraffe'" |
| 216 | ] |
| 217 | }, |
| 218 | "execution_count": 4, |
| 219 | "metadata": {}, |
| 220 | "output_type": "execute_result" |
| 221 | } |
| 222 | ], |
| 223 | "source": [ |
| 224 | "kb = KnowledgeBase(rules)\n", |
| 225 | "kb.get('animal')" |
| 226 | ] |
| 227 | }, |
| 228 | { |
| 229 | "cell_type": "markdown", |
| 230 | "metadata": {}, |
| 231 | "source": [ |
| 232 | "## Using PyKnow for Forward Inference\n", |
| 233 | "\n", |
| 234 | "In the next example, we will try to implement forward inference using one of the libraries for knowledge representation, [PyKnow](https://github.com/buguroo/pyknow/). **PyKnow** is a library for creating forward inference systems in Python, which is designed to be similar to classical old system [CLIPS](http://www.clipsrules.net/index.html). \n", |
| 235 | "\n", |
| 236 | "We could have also implemented forward chaining ourselves without many problems, but naive implementations are usually not very efficient. For more effective rule matching a special algorithm [Rete](https://en.wikipedia.org/wiki/Rete_algorithm) is used." |
| 237 | ] |
| 238 | }, |
| 239 | { |
| 240 | "cell_type": "code", |
| 241 | "execution_count": 5, |
| 242 | "metadata": { |
| 243 | "trusted": true |
| 244 | }, |
| 245 | "outputs": [ |
| 246 | { |
| 247 | "name": "stdout", |
| 248 | "output_type": "stream", |
| 249 | "text": [ |
| 250 | "Collecting git+https://github.com/buguroo/pyknow/\n", |
| 251 | " Cloning https://github.com/buguroo/pyknow/ to /tmp/pip-req-build-3cqeulyl\n", |
| 252 | " Running command git clone --filter=blob:none --quiet https://github.com/buguroo/pyknow/ /tmp/pip-req-build-3cqeulyl\n", |
| 253 | " Resolved https://github.com/buguroo/pyknow/ to commit 48818336f2e9a126f1964f2d8dc22d37ff800fe8\n", |
| 254 | " Preparing metadata (setup.py) ... \u001b[?25ldone\n", |
| 255 | "\u001b[?25hCollecting frozendict==1.2\n", |
| 256 | " Using cached frozendict-1.2.tar.gz (2.6 kB)\n", |
| 257 | " Preparing metadata (setup.py) ... \u001b[?25ldone\n", |
| 258 | "\u001b[?25hCollecting schema==0.6.7\n", |
| 259 | " Using cached schema-0.6.7-py2.py3-none-any.whl (14 kB)\n", |
| 260 | "Building wheels for collected packages: pyknow, frozendict\n", |
| 261 | " Building wheel for pyknow (setup.py) ... \u001b[?25ldone\n", |
| 262 | "\u001b[?25h Created wheel for pyknow: filename=pyknow-1.7.0-py3-none-any.whl size=34228 sha256=b7de5b09292c4007667c72f69b98d5a1b5f7324ff15f9dd8e077c3d5f7aade42\n", |
| 263 | " Stored in directory: /tmp/pip-ephem-wheel-cache-k7jpave7/wheels/81/1a/d3/f6c15dbe1955598a37755215f2a10449e7418500d7bd4b9508\n", |
| 264 | " Building wheel for frozendict (setup.py) ... \u001b[?25ldone\n", |
| 265 | "\u001b[?25h Created wheel for frozendict: filename=frozendict-1.2-py3-none-any.whl size=3148 sha256=2863d55c240d2409cddf05ccfe600591f8478681549fc97555c47c90dc6bb160\n", |
| 266 | " Stored in directory: /home/rg/.cache/pip/wheels/49/ac/f8/cb8120244e710bdb479c86198b03c7b08c3c2d3d2bf448fd6e\n", |
| 267 | "Successfully built pyknow frozendict\n", |
| 268 | "Installing collected packages: schema, frozendict, pyknow\n", |
| 269 | "Successfully installed frozendict-1.2 pyknow-1.7.0 schema-0.6.7\n" |
| 270 | ] |
| 271 | } |
| 272 | ], |
| 273 | "source": [ |
| 274 | "import sys\n", |
| 275 | "!{sys.executable} -m pip install git+https://github.com/buguroo/pyknow/" |
| 276 | ] |
| 277 | }, |
| 278 | { |
| 279 | "cell_type": "code", |
| 280 | "execution_count": 13, |
| 281 | "metadata": { |
| 282 | "trusted": true |
| 283 | }, |
| 284 | "outputs": [], |
| 285 | "source": [ |
| 286 | "from pyknow import *\n", |
| 287 | "#import pyknow" |
| 288 | ] |
| 289 | }, |
| 290 | { |
| 291 | "cell_type": "markdown", |
| 292 | "metadata": {}, |
| 293 | "source": [ |
| 294 | "We will define our system as a class that subclasses `KnowledgeEngine`. Each rule is defined by a separate function with `@Rule` annotation, which specifies when the rule should fire. Inside the rule, we can add new facts using `declare` function, and adding those facts will result in some more rules being called by forward inference engine. " |
| 295 | ] |
| 296 | }, |
| 297 | { |
| 298 | "cell_type": "code", |
| 299 | "execution_count": 14, |
| 300 | "metadata": { |
| 301 | "trusted": true |
| 302 | }, |
| 303 | "outputs": [], |
| 304 | "source": [ |
| 305 | "class Animals(KnowledgeEngine):\n", |
| 306 | " @Rule(OR(\n", |
| 307 | " AND(Fact('sharp teeth'),Fact('claws'),Fact('forward looking eyes')),\n", |
| 308 | " Fact('eats meat')))\n", |
| 309 | " def cornivor(self):\n", |
| 310 | " self.declare(Fact('carnivor'))\n", |
| 311 | " \n", |
| 312 | " @Rule(OR(Fact('hair'),Fact('gives milk')))\n", |
| 313 | " def mammal(self):\n", |
| 314 | " self.declare(Fact('mammal'))\n", |
| 315 | "\n", |
| 316 | " @Rule(Fact('mammal'),\n", |
| 317 | " OR(Fact('has hooves'),Fact('chews cud')))\n", |
| 318 | " def hooves(self):\n", |
| 319 | " self.declare('ungulate')\n", |
| 320 | " \n", |
| 321 | " @Rule(OR(Fact('feathers'),AND(Fact('flies'),Fact('lays eggs'))))\n", |
| 322 | " def bird(self):\n", |
| 323 | " self.declare('bird')\n", |
| 324 | " \n", |
| 325 | " @Rule(Fact('mammal'),Fact('carnivor'),\n", |
| 326 | " Fact(color='red-brown'),\n", |
| 327 | " Fact(pattern='dark spots'))\n", |
| 328 | " def monkey(self):\n", |
| 329 | " self.declare(Fact(animal='monkey'))\n", |
| 330 | "\n", |
| 331 | " @Rule(Fact('mammal'),Fact('carnivor'),\n", |
| 332 | " Fact(color='red-brown'),\n", |
| 333 | " Fact(pattern='dark stripes'))\n", |
| 334 | " def tiger(self):\n", |
| 335 | " self.declare(Fact(animal='tiger'))\n", |
| 336 | "\n", |
| 337 | " @Rule(Fact('ungulate'),\n", |
| 338 | " Fact('long neck'),\n", |
| 339 | " Fact('long legs'),\n", |
| 340 | " Fact(pattern='dark spots'))\n", |
| 341 | " def giraffe(self):\n", |
| 342 | " self.declare(Fact(animal='giraffe'))\n", |
| 343 | "\n", |
| 344 | " @Rule(Fact('ungulate'),\n", |
| 345 | " Fact(pattern='dark stripes'))\n", |
| 346 | " def zebra(self):\n", |
| 347 | " self.declare(Fact(animal='zebra'))\n", |
| 348 | "\n", |
| 349 | " @Rule(Fact('bird'),\n", |
| 350 | " Fact('long neck'),\n", |
| 351 | " Fact('cannot fly'),\n", |
| 352 | " Fact(color='black and white'))\n", |
| 353 | " def straus(self):\n", |
| 354 | " self.declare(Fact(animal='ostrich'))\n", |
| 355 | "\n", |
| 356 | " @Rule(Fact('bird'),\n", |
| 357 | " Fact('swims'),\n", |
| 358 | " Fact('cannot fly'),\n", |
| 359 | " Fact(color='black and white'))\n", |
| 360 | " def pinguin(self):\n", |
| 361 | " self.declare(Fact(animal='pinguin'))\n", |
| 362 | "\n", |
| 363 | " @Rule(Fact('bird'),\n", |
| 364 | " Fact('flies well'))\n", |
| 365 | " def albatros(self):\n", |
| 366 | " self.declare(Fact(animal='albatross'))\n", |
| 367 | " \n", |
| 368 | " @Rule(Fact(animal=MATCH.a))\n", |
| 369 | " def print_result(self,a):\n", |
| 370 | " print('Animal is {}'.format(a))\n", |
| 371 | " \n", |
| 372 | " def factz(self,l):\n", |
| 373 | " for x in l:\n", |
| 374 | " self.declare(x)" |
| 375 | ] |
| 376 | }, |
| 377 | { |
| 378 | "cell_type": "markdown", |
| 379 | "metadata": {}, |
| 380 | "source": [ |
| 381 | "Once we have defined a knowledgebase, we populate our working memory with some initial facts, and then call `run()` method to perform the inference. You can see as a result that new inferred facts are added to the working memory, including the final fact about the animal (if we set up all the initial facts correctly)." |
| 382 | ] |
| 383 | }, |
| 384 | { |
| 385 | "cell_type": "code", |
| 386 | "execution_count": 15, |
| 387 | "metadata": { |
| 388 | "trusted": true |
| 389 | }, |
| 390 | "outputs": [ |
| 391 | { |
| 392 | "name": "stdout", |
| 393 | "output_type": "stream", |
| 394 | "text": [ |
| 395 | "Animal is tiger\n" |
| 396 | ] |
| 397 | }, |
| 398 | { |
| 399 | "data": { |
| 400 | "text/plain": [ |
| 401 | "FactList([(0, InitialFact()),\n", |
| 402 | " (1, Fact(color='red-brown')),\n", |
| 403 | " (2, Fact(pattern='dark stripes')),\n", |
| 404 | " (3, Fact('sharp teeth')),\n", |
| 405 | " (4, Fact('claws')),\n", |
| 406 | " (5, Fact('forward looking eyes')),\n", |
| 407 | " (6, Fact('gives milk')),\n", |
| 408 | " (7, Fact('mammal')),\n", |
| 409 | " (8, Fact('carnivor')),\n", |
| 410 | " (9, Fact(animal='tiger'))])" |
| 411 | ] |
| 412 | }, |
| 413 | "execution_count": 15, |
| 414 | "metadata": {}, |
| 415 | "output_type": "execute_result" |
| 416 | } |
| 417 | ], |
| 418 | "source": [ |
| 419 | "ex1 = Animals()\n", |
| 420 | "ex1.reset()\n", |
| 421 | "ex1.factz([\n", |
| 422 | " Fact(color='red-brown'),\n", |
| 423 | " Fact(pattern='dark stripes'),\n", |
| 424 | " Fact('sharp teeth'),\n", |
| 425 | " Fact('claws'),\n", |
| 426 | " Fact('forward looking eyes'),\n", |
| 427 | " Fact('gives milk')])\n", |
| 428 | "ex1.run()\n", |
| 429 | "ex1.facts" |
| 430 | ] |
| 431 | }, |
| 432 | { |
| 433 | "cell_type": "code", |
| 434 | "execution_count": null, |
| 435 | "metadata": {}, |
| 436 | "outputs": [], |
| 437 | "source": [] |
| 438 | } |
| 439 | ], |
| 440 | "metadata": { |
| 441 | "kernelspec": { |
| 442 | "display_name": "Python 3.7.4 64-bit (conda)", |
| 443 | "metadata": { |
| 444 | "interpreter": { |
| 445 | "hash": "86193a1ab0ba47eac1c69c1756090baa3b420b3eea7d4aafab8b85f8b312f0c5" |
| 446 | } |
| 447 | }, |
| 448 | "name": "python3" |
| 449 | }, |
| 450 | "language_info": { |
| 451 | "codemirror_mode": { |
| 452 | "name": "ipython", |
| 453 | "version": 3 |
| 454 | }, |
| 455 | "file_extension": ".py", |
| 456 | "mimetype": "text/x-python", |
| 457 | "name": "python", |
| 458 | "nbconvert_exporter": "python", |
| 459 | "pygments_lexer": "ipython3", |
| 460 | "version": "3.11.2" |
| 461 | } |
| 462 | }, |
| 463 | "nbformat": 4, |
| 464 | "nbformat_minor": 2 |
| 465 | } |
| 466 | |