Как придать базе данных интеллектуальность? Как включить в базу данных сведения о том, что она должна делать с хранящимися в ней данными для последующего формирования суждений и выводов? Как превратить её в базу знаний в своей предметной области? Отличным инструментом для решения указанных задач является язык проектирования экспертных систем CLIPS
Рождение языка CLIPS относится к 1984 году, когда в Отделе искусственного интеллекта Центра космических исследований NASA начались работы по созданию компьютерных программ, моделирующих человека-эксперта во время мониторинга и диагностики космических систем и комплексов различного назначения. Название языка представляет собой аббревиатуру от C Language Integrated Production System, он имеет интерпретируемое исполнение, а его назначение состоит в решении задач искусственного интеллекта, в частности, разработки экспертных систем.
Применительно к теме данной статьи из всех возможностей CLIPS нас, в первую очередь, будут интересовать те из них, которые связаны с представлением знаний и взаимодействием с базой данных. По Д.Кнуту [ Кнут Д.Э. Искусство программирования, том 3. Сортировка и поиск, 2–е изд.: Пер. с англ. – М.: Издательский дом "Вильямс", 2001. – 832 с. ] элементом данных является запись или список. Несколько записей (списков) образуют таблицу или файл. Большой файл есть база данных. Как показано в [ Джексон П. Введение в экспертные системы.: Пер. с англ.: Уч. пос. – М.: Издательский дом "Вильямс", 2001. – 624 с. ], чтобы данные стали знаниями, они должны быть охарактеризованы функционально, т.е. в терминах действия, а не в терминах структурной организации, как это делается в базе данных. База знаний — это база данных, в которой содержатся сведения о том, как эти данные могут быть использованы. Именно такой смысл будет вкладываться в понятия "база данных" и "база знаний" в дальнейших рассуждениях.
CLIPS располагает тремя механизмами представления знаний: процедурным, эвристическим и объектно-ориентированным. Рассмотрение последнего оставим на будущее, а основное внимание уделим первым двум.
Процедурный механизм позволяет пользователю при помощи встроенных в язык функций разрабатывать или конструировать новые функции, которые выполняют некоторые полезные действия или возвращают некоторые значения. В этом смысле CLIPS напоминает такие известные языки программирования, как С, С++ или Pascal. Так, для создания пользовательских функций используется конструктор deffunction, который имеет следующий синтаксис:
(deffunction имя_функции [необязательный комментарий] (список формальных параметров) (действие_1) (действие_2) . . . . . . (действие_N))
Например, определим функцию om(x,y), которая возвращает целую часть частного от деления переменной y на переменную x :
(deffunction om (?x ?y) (div ?y ?x))
Обратите внимание, что в CLIPS имя переменной начинается с символа “?”, что для вызова функции, в данном случае встроенной функции деления нацело div, используется префиксная нотация и что вся конструкция представляет собой список, состоящий из четырех полей. Так что у CLIPS ноги растут не только из С (см. расшифровку аббревиатуры), но и из LISP.
Эвристический механизм представления знаний в CLIPS реализуется при помощи правил в форме
ЕСЛИ условие_1 и ... и условие_N удовлетворяются, ТО ВЫПОЛНИТЬ действие_1 и ... и действие_N.
Список условий называется левой частью правила ( Left-Hand Side или LHS ). Список действий называется правой частью правила ( Right-Hand Side или RHS ). Возможность применить конкретное правило определяется тем, удовлетворяются ли условия, сформулированные в его LHS. Удовлетворение или неудовлетворение происходит в момент сопоставления условий с так называемыми фактами, которые образуют не что иное, как базу данных. В CLIPS такая база данных может представлять некоторую предметную область, исходное или текущее состояние какой–либо проблемы, может моделировать в пространстве или во времени поведение какой–либо системы или любой сущности, которую можно описать посредством множества записей в виде списков.
Существует несколько способов создания базы данных, одним из которых является использование конструктора deffacts. Его синтаксис следующий:
(deffacts имя_базы_данных [необязательный комментарий] (факт_1) (факт_2) . . . . . (факт_N))
Каждый факт в базе данных представляет собой запись в виде списка. Список может содержать одно или несколько полей, принимающих символьные либо числовые значения. Список также может быть пустым.
Если КАЖДОЕ условие в LHS находит себя среди фактов, то происходит удовлетворение, активизация правила и выполнение ВСЕХ действий, записанных в его RHS. В противном случае правило не активизируется.
Работа правила очень напоминает условный оператор if–then, присутствующий во многих процедурных языках программирования. Принципиальная разница заключается в том, что оператор if–then выполнится в любом случае, когда до него дойдет очередь в программе. Что касается правила, то интерпретатор CLIPS еще подумает, выполнять правило или нет. Так, при старте программы, содержащей множество фактов и правил, интерпретатор CLIPS запускает машину логического вывода, которая выясняет, какие из правил можно активизировать. Это выполняется циклически, причем каждый цикл состоит из трех шагов:
Таким образом, правила, взаимодействующие с базой данных в виде фактов, вносят в нее функциональность и образуют вместе с ней базу знаний.
Для создания правила используется конструктор defrule, который имеет следующий синтаксис:
(defrule имя_правила [необязательный комментарий] [необязательное объявление] (условие_1) (условие_2) . . . . . . (условие_M) => (действие_1) (действие_2) . . . . . . (действие_N))
Обратите внимание, что LHS отделяется от RHS комбинацией символов “=>” и что количество условий и действий в правиле в общем случае не совпадает.
Теперь самое время перейти к примерам, но прежде добавим еще одну мысль. В CLIPS процедурный и эвристический механизмы представления знаний могут тесно взаимодействовать путем вызова пользовательских функций как из LHS, так и из RHS.
Пример 1.Рассмотрим предметную область, которая представляет участников некоторой конференции, приехавших из разных городов Украины, например, Одессы, Киева и Львова. На подобных мероприятиях все участники обычно проходят регистрацию. Пусть эта процедура представляет собой ввод сведений об участниках в базу данных, в которой на каждого участника выделяется одна запись (факт), состоящая из списка с тремя полями. Пусть первое поле имеет символьное значение rep — сокращение от representative ( представитель ). В общем случае это значение может быть любым, а поле может отсутствовать. Во втором поле списка хранится фамилия участника, а в третьем город, из которого участник прибыл. Содержимое фактов базы данных с именем rep может быть, например, таким:
(deffacts rep (rep Alejnov Odessa) (rep Ladak Odessa) (rep Slobodjanjuk Lvov) (rep Klitka Lvov) (rep Bojko Kiev) (rep Pustovit Odessa) (rep Spokojnij Odessa) (rep Shamis Odessa) (rep Lobovko Kiev) (rep Zadorozhna Lvov) (rep Javorskij Lvov))
(Примечание. Все приведенные фамилии выбраны абсолютно случайно без связи с реальными лицами.)
Используя любой текстовый редактор, создадим и сохраним базу данных в виде текстового ASCII-файла с именем, повторяющим имя базы данных, т.е. rep. Это позволяет легко редактировать данные независимо от каких-либо других программных модулей, добавляя новых участников или удаляя убывших.
После окончания конференции организаторы подводят итоги, определяя массу показателей. Пусть, в частности, требуется определить количество представителей от каждого города. Алгоритм решения такой задачи прост. Для каждого города задаем счетчик и последовательно просматриваем списки в записях файла rep. Если в записи третье поле списка имеет значение Kiev, то содержимое соответствующего счетчика увеличиваем на единицу. Для других городов – полная аналогия. Программа на языке CLIPS, реализующая указанный алгоритм, может быть, например, такой
(defglobal ?*odessa* = 0) (defglobal ?*kiev* = 0) (defglobal ?*lvov* = 0) (defrule start (initial-fact) => (printout t crlf “REPRESENTATIVES” crlf) (defrule odessa (rep ? Odessa) => (bind ?*odessa* (+ ?*odessa* 1))) (defrule kiev (rep ? Kiev) => (bind ?*kiev* (+ ?*kiev* 1))) (defrule lvov (rep ? Lvov) => (bind ?*lvov* (+ ?*lvov* 1))) (defrule result (declare (salience –1)) (initial-fact) => (printout t “from Odessa: ” ?*odessa* crlf) (printout t “from Kiev: ” ?*kiev* crlf) (printout t “from Lvov: ” ?*lvov* crlf))
В первых трех строках программы при помощи конструктора defglobal объявляются и зануляются три глобальные переменные ?*odessa*, ?*kiev* и ?*lvov*. Эти переменные есть счетчики. В CLIPS переменная может быть и локальной, но тогда она связывается только с тем правилом, в котором объявляется.
Далее следует правило с именем start, LHS которого представляет собой запись (initial-fact). Так обозначается системный начальный факт, который создается в рабочей памяти интерпретатора CLIPS по команде (reset) до запуска программы на выполнение. Для чего он нужен? Дело в том, что в CLIPS-программах распространенными правилами являются такие, которые добавляют факты в базу данных либо, наоборот, удаляют их. Типичной является ситуация, когда при старте программы в базе данных нет фактов, удовлетворяющих хотя бы одному правилу. В этом случае программа ничего не выполнит. Для того, чтобы начать вычисления, и используется системный начальный факт, который независимо от фактов в базе данных активизирует некоторое правило, добавляющее такие факты, которые, в свою очередь, активизируют правила, неудовлетворенные в начальный момент.
В данной программе (initial-fact) запускает правило start, которое активизируется независимо от фактов в файле rep и присутствует в программе только с одной целью — вывести заголовок. Для этого в его RHS вызывается встроенная функция printout с ключом t, выводящая на стандартное устройство вывода (монитор) заголовок, заключенный в кавычки. Комбинация символов crlf — аналог манипулятора endl в С++.
Следующие три правила с именами odessa, kiev и lvov можно назвать ядром программы. В них производится подсчет количества участников соответственно из Одессы, Киева и Львова. Рассмотрим, например, правило lvov. Оно активизируется в том случае, когда в базе данных находится факт (rep ? Lvov). Не трудно догадаться, что символ "?" во втором поле этого списка означает символ универсальной подстановки и заменяет собой любую фамилию. Отсюда следует, что правило lvov активизируется столько раз, сколько раз факт (rep ? Lvov) присутствует в базе данных. При этом столько же раз выполнятся действия, содержащиеся в RHS правила. Встроенная функция bind — аналог оператора присваивания. Следовательно, в RHS содержимое переменной ?*lvov* увеличивается на единицу и результат сохраняется в этой же переменной. Аналогично работают правила odessa и kiev.
Действия, которые выполняются в последнем правиле программы, отражены в его названии. RHS правила особых комментариев не требует, в то время как LHS заслуживает подробного рассмотрения. В CLIPS существует несколько стратегий очередности выполнения правил, а сами правила могут иметь приоритет, который задается встроенной функцией declare с параметром salience (выпуклость). Этот параметр может принимать целочисленные значения от –10000 до +10000. По умолчанию для всех правил величина salience равна нулю. Если в правиле result не указать приоритет, то оно будет конфликтовать с правилом start за очередность выполнения, т.к. у этих правил одинаковая LHS. Для устранения конфликта в правиле result приоритет указан явно и со знаком минус, в связи с чем это правило выполнится последним.
Используя любой текстовый редактор наберем и сохраним текст программы в ASCII-файле со стандартным для CLIPS-программ расширением .clp и с именем represent. Дальше посмотрим, что из всего этого получилось при работе с консоли в среде Linux. Командой clips вызовем интерпретатор CLIPS, командой (load имя_файла) загрузим в интерпретатор файлы rep и represent.clp, командами (reset) и (run) запустим программу represent.clp на выполнение.
[vovic@localhost vovic]$ clips CLIPS (V6.10 07/01/98) CLIPS> (load rep) . . . . . . . . . TRUE CLIPS> (load represent.clp) . . . . . . . . . TRUE CLIPS> (reset) CLIPS> (run) REPRESENTATIVES from Odessa: 5 from Kiev: 2 from Lvov: 4 CLIPS>
Сообщение интерпретатора TRUE означает, что в файле нет синтаксических ошибок и команда загрузки выполнена корректно. Многоточием представлены другие сообщения интерпретатора, которые в данном случае опущены.
Как следует из описанных действий, в интерпретаторе CLIPS находятся два файла. Первый, с именем rep, является базой данных. Второй, с именем represent.clp, содержит сведения (правила) о том, как эти данные могут быть использованы. Таким образом, вместе файлы образуют базу знаний, которая содержит, по крайней мере, два знания. Первое знание — общий состав участников конференции. Его можно посмотреть не выходя из интерпретатора по команде (facts). Второе знание — количество участников от каждого города.
Такая база способна обучаться. Как? Добавлением новых правил. Пусть имеющиеся знания надо дополнить знанием об участниках (пофамильно), приехавших на конференцию из Одессы. Алгоритм решения очевиден – надо удалить из базы данных все факты, в которых третье поле списка имеет значение Kiev или Lvov. Следующие правила выполняют такое действие.
(defrule whithout-kiev ?kiev <- (rep ? Kiev) => (retract ?kiev)) (defrule whithout-lvov ?lvov <- (rep ? Lvov) => (retract ?lvov)) (defrule result (declare (salience –1)) (initial-fact) => (retract 0 ) (facts) (save-facts “odessa”))
В CLIPS удаление факта выполняется командой retract с указанием индекса удаляемого факта либо переменной, с которой факт связывается. Значение индекса (целое число) факты получают автоматически при загрузке базы данных в интерпретатор или при их добавлении в уже загруженную базу. Системный начальный факт всегда имеет индекс "0". Связывание факта с переменной выполняется в LHS правила указателем "<-". Теперь все понятно. Поясним только действия, выполняемые последним правилом. Вначале из рабочей памяти интерпретатора CLIPS удаляется initial-fact. Если это не сделать, программа зацикливается. Затем факты выводятся на стандартное устройство вывода (монитор). В последнем действии факты сохраняются в файле с именем odessa.
Допустим, программа с этими правилами записана в файле odessa-only.clp. Тогда, находясь в интерпретаторе CLIPS, загрузим его в дополнение к уже загруженным файлам rep и represent.clp и запустим на выполнение
CLIPS> (load odessa-only.clp) . . . . . . . . . . . . . . . TRUE CLIPS> (run) f-1 (rep Alejnov Odessa) f-2 (rep Ladak Odessa) f-6 (rep Pustovit Odessa) f-7 (rep Spokojnij Odessa) f-8 (rep Shamis Odessa) For a total of 5 facts CLIPS>
В рассмотренном примере база знаний состоит из трех программных модулей. Однако, ничто не мешает использовать одну программу, сохраненную в одном файле. В следующем примере показано, как это делается. В нем же эвристический механизм представления знаний используется вместе с процедурным.
Пример 2. Пусть требуется подобрать резистор для участка цепи схемы электрической принципиальной некоторого радиоэлектронного устройства. Резистор характеризуется сопротивлением, которое определяется по измеренным или рассчитанным значениям электрического тока, проходящего через резистор, и падению напряжения на нем. Программа с именем resistor.clp, решающая эту задачу, может быть, например, такой
(deffacts resistors ; This is database (resistor Ra 2) (resistor Rb 5) (resistor Rc 7)) (deffunction om ; This is function om(x,y) (?x ?y) (div ?y ?x)) (defrule input ; This is current & strait input (initial-fact) => (printout t crlf “Input current value: “) (bind ?i (read)) (printout t “Input strait value: “) (bind ?u (read)) (assert (numbers ?i ?u))) (defrule take ; Get resistor from database (numbers ?i ?u) (resistor ?r =(om ?i ?u)) => (printout t crlf “You must take resistor “ ?r”.” crlf crlf) (reset) (halt)) (defrule nothing ; There is nothing (numbers ?i ?u) (resistor ?r ~=(om ?i ?u)) => (printout t crlf “There is nothing for You in my database!” crlf crlf) (reset) (halt))
Программа состоит из нескольких частей: базы данных с именем resistors, объявления пользовательской функции om и трех правил с именами input, take и nothing.
В базе данных содержатся сведения о резисторах. Они представлены в виде списков, состоящих из трех полей. Первое поле имеет значение resistor, которое отражает тип радиодетали. Во втором поле списка содержится тип резистора. Последнее поле хранит значение сопротивления.
О функции om подробно говорилось ранее. Здесь она используется для представления процедурного знания — закона Ома. Деление нацело — исключительно для упрощения.
Правило input предназначено для ввода исходных данных. Оно активизируется системным начальным фактом и требует от пользователя ввести ток и напряжение. Встроенная функция read возвращает значение, введенное со стандартного устройства ввода (клавиатуры), которое сохраняется в переменных ?i и ?u. В RHS правила выполняется еще одно действие. Команда assert добавляет в рабочую память интерпретатора CLIPS факт (numbers ?i ?u). Для чего? Для того, чтобы можно было обращаться к локальным переменным ?i и ?u, связанным с правилом input, из других правил программы.
В следующих двух правилах пользователю либо предлагается тип подходящего резистора (правило take), либо сообщается, что таковой отсутствует (правило nothing).
Рассмотрим правило take. Его LHS состоит из двух условий, поэтому правило активизируется, если оба условия будут удовлетворены. Первое условие удовлетворяется, т.к. соответствующий факт уже создан правилом input. Второе условие удовлетворится, если будет точно соответствовать какому-либо факту (списку) в базе данных. Первое поле условия вопросов не вызывает. Во втором поле условия находится переменная ?r, которая может принять значение Ra, либо Rb, либо Rc в зависимости от содержимого третьего поля условия. В этом поле осуществляется вызов функции om и сохраняется возвращаемое функцией значение. Так, если возвращаемое значение будет равно 7, то условие удовлетворится, переменная ?r примет значение Rc, правило активизируется и выведет на экран монитора предложение выбрать резистор Rc. Если возвращаемое функцией om значение равно 5, то пользователю будет предложен резистор Rb и т.д.
В LHS правила nothing вроде бы полная аналогия, за исключением одной маленькой модификации. В третьем поле второго условия перед вызовом функции om стоит символ "~", означающий логическое отрицание. Таким образом, условие удовлетворится и правило активизируется, если возвращаемое функцией om значение будет не 2, не 5 и не 7.
Находясь в интерпретаторе CLIPS, командой (clear) очистим его от данных предыдущего примера, загрузим файл resistor.clp и запустим программу на выполнение. Получим нечто следующее:
CLIPS> (clear) CLIPS> (load resistor.clp) . . . . . . . . . . . . . TRUE CLIPS> (reset) CLIPS> (run) Input current value: 3 Input strait value: 15 You must take resistor Rb. CLIPS> (run) Input current value: 3.5 Input strait value: 5.44 There is nothing for You in my database! CLIPS>
Таким образом, файл resistor.clp также представляет собой базу знаний, т.к. содержит и базу данных, и сведения (правила) о том, как данные могут быть использованы. Эта база располагает по крайней мере тремя знаниями. Первое знание — общий список резисторов с указанием типа и сопротивления. Второе знание — закон Ома. Третье знание — предлагаемый тип резистора.
Интеллект базы знаний можно существенно повысить добавлением новых данных и правил. Так, вместо закона Ома можно использовать более серьезные методики определения сопротивления резистора, например, схемотехническую САПР PSpice. Результаты ее работы можно сохранить в текстовом файле, а затем вызвать из третьего поля второго условия правила take. Другой путь — добавление новых типов резисторов в базу данных. Например, интересный результат получается при внесении в базу данных записи (resistor Rd 2). При некоторой доработке правил take и nothing, если возвращаемое функцией om значение равно 2, правило take отработает два раза и предложит резисторы Ra и Rd. Затем можно пойти дальше — добавить правило (правила), которое выберет из резисторов Ra и Rd предпочтительный для некоторых конкретных условий и т.д. и т.д.
В заключение несколько слов об интерпретаторе CLIPS. Он является свободно распространяемой программой, предоставляется на условии “как есть” и к NASA уже не имеет никакого отношения. В настоящее время в ходу версии 6.10 под Linux и 6.20 под DOS/Windows. Последняя снабжена простым GUI, напоминающим NOTEPAD. Ее можно загрузить с http://www.ghg.net/clips/download. Оттуда же можно получить разнообразнейшую и подробнейшую документацию в PDF- формате, но на английском языке. Версия под Linux предназначена для работы с консоли. Мне она встречалась в RPM-библиотеках дистрибутивов Mandrake 8-9. Это файл clips-6.10-6mdk.i586.rpm. В Mandrake 9.0 к нему добавлен файл clips-X11-6.10-6mdk.i586.rpm с GUI для работы в иксах. Но о нем ничего сказать не могу. Не пробовал.
Трофимов В.Е. База данных+CLIPS=База знаний// Компьютеры+программы. — 2003. — N 10. — C. 56–61
©Трофимов В.Е., vovic@ukr.net