forked from norvig/paip-lisp
-
Notifications
You must be signed in to change notification settings - Fork 0
/
mycin.lisp
394 lines (342 loc) · 14.2 KB
/
mycin.lisp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
;;;; -*- Mode: Lisp; Syntax: Common-Lisp -*-
;;;; Code from Paradigms of AI Programming
;;;; Copyright (c) 1991 Peter Norvig
;;;; File mycin.lisp: Chapter 16's implementation of MYCIN.
;;;; A sample rulebase is provided in "mycin-rules.lisp".
(defconstant true +1.0)
(defconstant false -1.0)
(defconstant unknown 0.0)
(defun cf-or (a b)
"Combine the certainty factors for the formula (A or B).
This is used when two rules support the same conclusion."
(cond ((and (> a 0) (> b 0))
(+ a b (* -1 a b)))
((and (< a 0) (< b 0))
(+ a b (* a b)))
(t (/ (+ a b)
(- 1 (min (abs a) (abs b)))))))
(defun cf-and (a b)
"Combine the certainty factors for the formula (A and B)."
(min a b))
(defconstant cf-cut-off 0.2
"Below this certainty we cut off search.")
(defun true-p (cf)
"Is this certainty factor considered true?"
(and (cf-p cf) (> cf cf-cut-off)))
(defun false-p (cf)
"Is this certainty factor considered false?"
(and (cf-p cf) (< cf (- cf-cut-off 1.0))))
(defun cf-p (x)
"Is X a valid numeric certainty factor?"
(and (numberp x) (<= false x true)))
(let ((db (make-hash-table :test #'equal)))
(defun get-db (key) (gethash key db))
(defun put-db (key val) (setf (gethash key db) val))
(defun clear-db () (clrhash db)))
(defun get-vals (parm inst)
"Return a list of (val cf) pairs for this (parm inst)."
(get-db (list parm inst)))
(defun get-cf (parm inst val)
"Look up the certainty factor or return unknown."
(or (second (assoc val (get-vals parm inst)))
unknown))
(defun update-cf (parm inst val cf)
"Change the certianty factor for (parm inst is val),
by combining the given cf with the old."
(let ((new-cf (cf-or cf (get-cf parm inst val))))
(put-db (list parm inst)
(cons (list val new-cf)
(remove val (get-db (list parm inst))
:key #'first)))))
(defconstant help-string
"~&Type one of the following:
? - to see possible answers for this parameter
rule - to show the current rule
why - to see why this question is asked
help - to see this list
xxx - (for some specific xxx) if there is a definite answer
(xxx .5 yyy .4) - If there are several answers with
different certainty factors.")
(defun ask-vals (parm inst)
"Ask the user for the value(s) of inst's parm parameter,
unless this has already been asked. Keep asking until the
user types UNKNOWN (return nil) or a valid reply (return t)."
(unless (get-db `(asked ,parm ,inst))
(put-db `(asked ,parm ,inst) t)
(loop
(let ((ans (prompt-and-read-vals parm inst)))
(case ans
(help (format t help-string))
(why (print-why (get-db 'current-rule) parm))
(rule (princ (get-db 'current-rule)))
((unk unknown) (RETURN nil))
(? (format t "~&A ~a must be of type ~a"
parm (parm-type parm)) nil)
(t (if (check-reply ans parm inst)
(RETURN t)
(format t "~&Illegal reply. ~
Type ? to see legal ones."))))))))
(defun prompt-and-read-vals (parm inst)
"Print the prompt for this parameter (or make one up) and
read the reply."
(fresh-line)
(format t (parm-prompt (get-parm parm)) (inst-name inst) parm)
(princ " ")
(finish-output)
(funcall (parm-reader (get-parm parm))))
(defun inst-name (inst)
"The name of this instance."
;; The stored name is either like (("Jan Doe" 1.0)) or nil
(or (first (first (get-vals 'name inst)))
inst))
(defun check-reply (reply parm inst)
"If reply is valid for this parm, update the DB.
Reply should be a val or (val1 cf1 val2 cf2 ...).
Each val must be of the right type for this parm."
(let ((answers (parse-reply reply)))
(when (every #'(lambda (pair)
(and (typep (first pair) (parm-type parm))
(cf-p (second pair))))
answers)
;; Add replies to the data base
(dolist (pair answers)
(update-cf parm inst (first pair) (second pair)))
answers)))
(defun parse-reply (reply)
"Convert the reply into a list of (value cf) pairs."
(cond ((null reply) nil)
((atom reply) `((,reply ,true)))
(t (cons (list (first reply) (second reply))
(parse-reply (rest2 reply))))))
(defstruct (parm (:constructor
new-parm (name &optional context type-restriction
prompt ask-first reader)))
name (context nil) (prompt "~&What is the ~*~a of ~2:*~a?")
(ask-first nil) (type-restriction t) (reader 'read))
(defmacro defparm (parm &rest args)
"Define a parameter."
`(setf (get ',parm 'parm) (apply #'new-parm ',parm ',args)))
(defun parm-type (parm-name)
"What type is expected for a value of this parameter?"
(parm-type-restriction (get-parm parm-name)))
(defun get-parm (parm-name)
"Look up the parameter structure with this name."
;; If there is none, make one
(or (get parm-name 'parm)
(setf (get parm-name 'parm) (new-parm parm-name))))
(deftype yes/no () '(member yes no))
(defstruct context
"A context is a sub-domain, a type."
name (number 0) initial-data goals)
(defmacro defcontext (name &optional initial-data goals)
"Define a context."
`(make-context :name ',name :initial-data ',initial-data
:goals ',goals))
(defun new-instance (context)
"Create a new instance of this context."
(let ((instance (format nil "~a-~d"
(context-name context)
(incf (context-number context)))))
(format t "~&------ ~a ------~&" instance)
(put-db (context-name context) instance)
(put-db 'current-instance instance)))
(defstruct (rule (:print-function print-rule))
number premises conclusions cf)
(let ((rules (make-hash-table)))
(defun put-rule (rule)
"Put the rule in a table, indexed under each
parm in the conclusion."
(dolist (concl (rule-conclusions rule))
(push rule (gethash (first concl) rules)))
rule)
(defun get-rules (parm)
"A list of rules that help determine this parameter."
(gethash parm rules))
(defun clear-rules () (clrhash rules)))
(defun find-out (parm &optional (inst (get-db 'current-instance)))
"Find the value(s) of this parameter for this instance,
unless the values are already known.
Some parameters we ask first; others we use rules first."
(or (get-db `(known ,parm ,inst))
(put-db `(known ,parm ,inst)
(if (parm-ask-first (get-parm parm))
(or (ask-vals parm inst) (use-rules parm))
(or (use-rules parm) (ask-vals parm inst))))))
(defun use-rules (parm)
"Try every rule associated with this parameter.
Return true if one of the rules returns true."
(some #'true-p (mapcar #'use-rule (get-rules parm))))
(defun use-rule (rule)
"Apply a rule to the current situation."
;; Keep track of the rule for the explanation system:
(put-db 'current-rule rule)
;; If any premise is known false, give up.
;; If every premise can be proved true, then
;; draw conclusions (weighted with the certainty factor).
(unless (some #'reject-premise (rule-premises rule))
(let ((cf (satisfy-premises (rule-premises rule) true)))
(when (true-p cf)
(dolist (conclusion (rule-conclusions rule))
(conclude conclusion (* cf (rule-cf rule))))
cf))))
(defun satisfy-premises (premises cf-so-far)
"A list of premises is satisfied if they are all true.
A combined cf is returned."
;; cf-so-far is an accumulator of certainty factors
(cond ((null premises) cf-so-far)
((not (true-p cf-so-far)) false)
(t (satisfy-premises
(rest premises)
(cf-and cf-so-far
(eval-condition (first premises)))))))
(defun eval-condition (condition &optional (find-out-p t))
"See if this condition is true, optionally using FIND-OUT
to determine unknown parameters."
(multiple-value-bind (parm inst op val)
(parse-condition condition)
(when find-out-p
(find-out parm inst))
;; Add up all the (val cf) pairs that satisfy the test
(loop for pair in (get-vals parm inst)
when (funcall op (first pair) val)
sum (second pair))))
(defun reject-premise (premise)
"A premise is rejected if it is known false, without
needing to call find-out recursively."
(false-p (eval-condition premise nil)))
(defun conclude (conclusion cf)
"Add a conclusion (with specified certainty factor) to DB."
(multiple-value-bind (parm inst op val)
(parse-condition conclusion)
(update-cf parm inst val cf)))
(defun is (a b) (equal a b))
(defun parse-condition (condition)
"A condition is of the form (parm inst op val).
So for (age patient is 21), we would return 4 values:
(age patient-1 is 21), where patient-1 is the current patient."
(values (first condition)
(get-db (second condition))
(third condition)
(fourth condition)))
(defun emycin (contexts)
"An Expert System Shell. Accumulate data for instances of each
context, and solve for goals. Then report the findings."
(clear-db)
(get-context-data contexts))
(defun get-context-data (contexts)
"For each context, create an instance and try to find out
required data. Then go on to other contexts, depth first,
and finally ask if there are other instances of this context."
(unless (null contexts)
(let* ((context (first contexts))
(inst (new-instance context)))
(put-db 'current-rule 'initial)
(mapc #'find-out (context-initial-data context))
(put-db 'current-rule 'goal)
(mapc #'find-out (context-goals context))
(report-findings context inst)
(get-context-data (rest contexts))
(when (y-or-n-p "Is there another ~a?"
(context-name context))
(get-context-data contexts)))))
(defmacro defrule (number &body body)
"Define a rule with conditions, a certainty factor, and
conclusions. Example: (defrule R001 if ... then .9 ...)"
(assert (eq (first body) 'if))
(let* ((then-part (member 'then body))
(premises (ldiff (rest body) then-part))
(conclusions (rest2 then-part))
(cf (second then-part)))
;; Do some error checking:
(check-conditions number premises 'premise)
(check-conditions number conclusions 'conclusion)
(when (not (cf-p cf))
(warn "Rule ~a: Illegal certainty factor: ~a" number cf))
;; Now build the rule:
`(put-rule
(make-rule :number ',number :cf ,cf :premises ',premises
:conclusions ',conclusions))))
(defun check-conditions (rule-num conditions kind)
"Warn if any conditions are invalid."
(when (null conditions)
(warn "Rule ~a: Missing ~a" rule-num kind))
(dolist (condition conditions)
(when (not (consp condition))
(warn "Rule ~a: Illegal ~a: ~a" rule-num kind condition))
(multiple-value-bind (parm inst op val)
(parse-condition condition)
(declare (ignore inst))
(when (and (eq kind 'conclusion) (not (eq op 'is)))
(warn "Rule ~a: Illegal operator (~a) in conclusion: ~a"
rule-num op condition))
(when (not (typep val (parm-type parm)))
(warn "Rule ~a: Illegal value (~a) in ~a: ~a"
rule-num val kind condition)))))
(defun report-findings (context inst)
"Print findings on each goal for this instance."
(when (context-goals context)
(format t "~&Findings for ~a:" (inst-name inst))
(dolist (goal (context-goals context))
(let ((values (get-vals goal inst)))
;; If there are any values for this goal,
;; print them sorted by certainty factor.
(if values
(format t "~& ~a:~{~{ ~a (~,3f) ~}~}" goal
(sort (copy-list values) #'> :key #'second))
(format t "~& ~a: unknown" goal))))))
(defun print-rule (rule &optional (stream t) depth)
(declare (ignore depth))
(format stream "~&Rule ~a:~& If" (rule-number rule))
(print-conditions (rule-premises rule) stream)
(format stream "~& Then ~a (~a) that"
(cf->english (rule-cf rule)) (rule-cf rule))
(print-conditions (rule-conclusions rule) stream))
(defun print-conditions (conditions &optional
(stream t) (num 1))
"Print a list of numbered conditions."
(dolist (condition conditions)
(print-condition condition stream num)))
(defun print-condition (condition stream number)
"Print a single condition in pseudo-English."
(format stream "~& ~d)~{ ~a~}" number
(let ((parm (first condition))
(inst (second condition))
(op (third condition))
(val (fourth condition)))
(case val
(YES `(the ,inst ,op ,parm))
(NO `(the ,inst ,op not ,parm))
(T `(the ,parm of the ,inst ,op ,val))))))
(defun cf->english (cf)
"Convert a certainy factor to an English phrase."
(cond ((= cf 1.0) "there is certain evidence")
((> cf .8) "there is strongly suggestive evidence")
((> cf .5) "there is suggestive evidence")
((> cf 0.0) "there is weakly suggestive evidence")
((= cf 0.0) "there is NO evidence either way")
((< cf 0.0) (concatenate 'string (cf->english (- cf))
" AGAINST the conclusion"))))
(defun print-why (rule parm)
"Tell why this rule is being used. Print what is known,
what we are trying to find out, and what we can conclude."
(format t "~&[Why is the value of ~a being asked for?]" parm)
(if (member rule '(initial goal))
(format t "~&~a is one of the ~a parameters."
parm rule)
(multiple-value-bind (knowns unknowns)
(partition-if #'(lambda (premise)
(true-p (eval-condition premise nil)))
(rule-premises rule))
(when knowns
(format t "~&It is known that:")
(print-conditions knowns)
(format t "~&Therefore,"))
(let ((new-rule (copy-rule rule)))
(setf (rule-premises new-rule) unknowns)
(print new-rule)))))
(defun mycin ()
"Determine what organism is infecting a patient."
(emycin
(list (defcontext patient (name sex age) ())
(defcontext culture (site days-old) ())
(defcontext organism () (identity)))))