microsoft/AI-For-Beginners

Public

mirrored fromhttps://github.com/microsoft/AI-For-BeginnersAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
321daa001261e639ed43f5c759754b6b260ceffd

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

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 "![](images/AND-OR-Tree.png)"
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