;;;; PERCEPTRON is a simple system for learning from examples which uses the ;;;; perceptron learning procedure to adjust a set of weights on a single ;;;; linear threshold unit until all of the training examples are correctly ;;;; classified. The perceptron convergence theorem assures that the system ;;;; will halt if the examples are linearly separable but if not the system ;;;; may not halt. The file BINARY-ENCODER contains the functions needed ;;;; for converting feature vector examples to bit strings ;;;; To run on multi-category problems like SOYBEAN-DATA, after loading the data ;;;; you must encode instances of all categories into bit strings using ;;;; ENCODE-CATEGORY-INSTANCES (e.g. (encode-category-instances soybean-categories)) ;;;; and then partition encoded examples into training and test sets using ;;;; SEPARATE-INSTANCES and then use PERCEPTRON-CATEGORIES to do the ;;;; learning and TEST-CATEGORIES to test the learned perceptron. ;;;; Uses functions defined in the files: BINARY-ENCODER and TESTER ;;;; Copyright (c) 1988 by Raymond Joseph Mooney. This program may be freely ;;;; copied, used, or modified provided that this copyright notice is included ;;;; in each copy of this code and parts thereof. (defvar *trace-perceptron* nil) ; produces trace of weight updates if T. (defvar *perceptron* nil) ; Stores the final learned perceptron. (defvar *domains*) ; List of domains (possible value list) for ; each feature (setf test1 '((+ (0 1 0)) ; Simple testing example (+ (1 0 1)) (+ (1 1 1)) (- (0 0 1)))) (setf test2 '((- (0 0)) ; Infamous XOR example (+ (0 1)) (+ (1 0)) (- (1 1)))) (defmacro trace-print (test-var &rest format-form) ;; Print using the format string only is test-var is nonNIL `(if ,test-var (format t ,@format-form))) (defun perceptron-m (examples &optional short-encode-flag (threshold 0) (delta 1)) ;;; Perceptron for examples with multi-valued features. First encodes ;;; examples into binary features and then runs standard perceptron. ;;; Uses n bits to encode n valued features unless short-encode-flag ;;; is set in which case uses log(n) bits. (format t "~%Examples: ~A" examples) (perceptron (convert-to-bits examples short-encode-flag) threshold delta)) (defun perceptron (examples &optional (threshold 0) (delta 1)) ;;; Apply perceptron learning algorithm to the examples. Iterates ;;; through all of the examples adjusting weights when system is wrong ;;; until all examples are classified correctly. Threshold and delta ;;; define the initial threshold of the perceptron and the learning ;;; increment. *perceptron* is set to the learned perceptron which ;;; is of the form ( ) where weights is a vector ;;; (arrary) of the feature weights (let* ((num-features (length (second (first examples)))) (weights (make-array (list num-features) ; define weight vector :element-type 'number :initial-element 0)) ; weights initalized to 0 (all-correct nil) (i 0) (trial-num 0)) (when *trace-perceptron* (print-perceptron weights threshold)) (loop (if all-correct (return nil)) ; Loop until all examples are correctly (setf all-correct t) ; classified. (dolist (example examples) ; Each trial look at all examples (if (compute-perceptron-output (second example) weights threshold) (cond ((eq (first example) `-) ; if network says + but its - example (trace-print *trace-perceptron* "~%~%Classifies ~A wrong" example) (setf all-correct nil) (incf threshold delta) ; Then increase threshold to make + ; classification harder ;; and decrement weights for features present in the example (setf i 0) (dolist (feature-value (second example)) (when (eq feature-value 1) (incf (aref weights i) (- delta)) (trace-print *trace-perceptron* "~%Decrementing weight for feature ~A" (+ i 1))) (incf i))) (t (trace-print *trace-perceptron* "~%~%Classifies ~A right" example))) (cond ((eq (first example) '+) ; if network says - but its + (trace-print *trace-perceptron* "~%~%Classifies ~A wrong" example) (setf all-correct nil) (incf threshold (- delta)) ; Then decrease threshold to make + ; classification easier ;; and increment weights for features present in the example (setf i 0) (dolist (feature-value (second example)) (when (eq feature-value 1) (incf (aref weights i) delta) (trace-print *trace-perceptron* "~%Incrementing weight for feature ~A" (+ i 1))) (incf i))) (t (trace-print *trace-perceptron* "~%~%Classifies ~A right" example))))) (incf trial-num) ; Keep track of the number of trials (when *trace-perceptron* (print-perceptron weights threshold))) (format t "~%Trials: ~A" trial-num) (unless *trace-perceptron* (print-perceptron weights threshold)) (setf *perceptron* (list weights threshold)))) ; Return the final perceptron (defun compute-perceptron-output (feature-values weights threshold) ;;; Determine value of perceptron for the given input. Return T or NIL ;;; instead of 0 or 1 to simply tests (let ((sum 0) (i 0)) ;; Simply sum the weight*input for all of the features ;; and return T if greater than threshold. (dolist (feature-value feature-values) (when (eq feature-value 1) (incf sum (aref weights i))) (incf i)) (> sum threshold))) (defun print-perceptron (weights threshold) ;; Printout the current weight vector and threshold (format t "~%~%Weights:") (dotimes (i (length weights)) (format t " ~A" (aref weights i))) (format t "~%Threshold: ~A" threshold)) ;;;; ========================================================================================== ;;;; Functions for running and testing a single concept ;;;; ========================================================================================== (defun perceptron-test (examples train# &optional short-encode-flag) ;;; Run and test on the examples by using the first train# examples ;;; to train and the remaining to test (setf examples (convert-to-bits examples short-encode-flag)) (let ((training-examples (subseq examples 0 train#)) (testing-examples (subseq examples train#)) (start-time (get-internal-run-time))) (perceptron training-examples) (format t "~%~%Run time: ~,2Fs" (seconds-since start-time)) (perceptron-test-examples testing-examples))) (defun perceptron-test-examples (examples) ;;; Test on the given set of examples and report results (let ((correct# 0)) (dolist (example examples) (let ((output (compute-perceptron-output (second example) (first *perceptron*) (second *perceptron*)))) (if (or (and (eq (first example) '+) output) (and (eq (first example) '-) (null output))) (incf correct#)))) (format t "~%~%Percentage correct: ~A" (round (* 100 (/ correct# (length examples))))))) ;;;; ========================================================================================== ;;;; The following functions are for multiple concept (category) problems like the soybean data ;;;; ========================================================================================== (defun make-examples (pos-instances neg-instances) ;;; Converts lists of positive and negative instances into a list of examples ;;; suitable for PERCEPTRON. (append (mapcar #'(lambda (instance) (list '+ instance)) pos-instances) (mapcar #'(lambda (instance) (list '- instance)) neg-instances))) (defun perceptron-categories (category-list &optional (threshold 0) (delta 1)) ;;; Perceptron for multiple concept learning problems. The arguement category-list ;;; should be a list of atoms which represent names of individual categories. A list of ;;; instances for learning should be stored on the LEARN-INSTANCES property of each ;;; category name. A single concept learning trial is run for each category in which the ;;; instances of that category are positive examples and instances of all other categories ;;; are negative examples. If convergence is reached for a given category, then the ;;; learned perceptron (i.e. ( )) is stored on the PERCEPTRON ;;; property of the category name. (setf *instance-tester* #'perceptron-test-instance) (let ((start-time (get-internal-run-time))) (dolist (category-name category-list) (format t "~%~%Category: ~A" category-name) (perceptron (make-examples (get category-name 'learn-instances) (mapcan #'(lambda (a) (copy-list (get a 'learn-instances))) (remove category-name category-list))) threshold delta) (setf (get category-name 'PERCEPTRON) *perceptron*)) (format t "~%~%Run time: ~,3Fs" (seconds-since start-time))) (perceptron-test-categories category-list)) (defun perceptron-test-instance (instance categories) ;;; Given an instance and a list of category names returns the subset of these categories ;;; which it is determined that the instance belongs to (i.e. for which the learned ;;; perceptron for that category returns T. (let ((member-categories nil)) (dolist (category categories member-categories) (if (compute-perceptron-output instance (first (get category 'PERCEPTRON)) (second (get category 'PERCEPTRON))) (push category member-categories))))) (defun perceptron-test-categories (categories &optional learn-instances?) ;;; To be used after *-CATEGORIES in order to test the generalizations learned ;;; on a set of new instances stored under the TEST-INSTANCES property of each category ;;; name in categories. Reports % correct for each category and overall % correct. (let ((percent-sum 0)(percent 0)) (dolist (category categories) (format t "~%~%Testing ~A instances" category) (let ((answers (mapcar #'(lambda (instance) (funcall *instance-tester* instance categories)) (get category (if learn-instances? 'learn-instances 'test-instances)))) (count 0)) (format t "~%~A" answers) (dolist (answer answers) (if (and (eq (first answer) category)(null (rest answer))) (incf count))) (setf percent (* 100 (/ count (length answers)))) (incf percent-sum percent) (format t "~%Percent correct: ~,2F" percent))) (format t "~%~%Total percent correct: ~,2F" (/ percent-sum (length categories))))) ;;;; ========================================================================================== ;;;; General utility functions ;;;; ========================================================================================== (defun seconds-since (time) ;;; Return seconds elapsed since given time (initially set by get-internal-run-time) (/ (- (get-internal-run-time) time) internal-time-units-per-second)) (defun separate-instances (categories num-learn-instances) ;;; Separates a list of instances for a set of categories into learning and testing ;;; instances to facilitate experimentation in preparation for using VS-CATEGORIES ;;; and TEST-CATEGORIES. The variable categories should be bound to a list of ;;; category names whose values are a list of instances of that category. The first ;;; num-learn-instances of these instances are stored on the LEARN-INSTANCES property ;;; to be used for learning and the rest are stored on TEST-INSTANCES for testing. (dolist (category categories) (setf (get category 'learn-instances) (subseq (eval category) 0 num-learn-instances)) (setf (get category 'test-instances) (subseq (eval category) num-learn-instances (length (eval category))))))