Лекция №15

Применение логического программирования в задачах искусственного интеллекта. Тест Тьюринга.

В этой лекции речь пойдет о возможных применениях Пролога в области искусственного интеллекта. Конечно, ознакомиться с данной темой достаточно полно в рамках одной лекции мы не успеем. Однако хочется надеяться, что сможем пробежаться по верхушкам и рассмотреть пару простых примеров.

В 1950 году Алан Тьюринг в статье "Вычислительная техника и интеллект" (книга "Может ли машина мыслить?") предложил эксперимент, позднее названный "тест Тьюринга", для проверки способности компьютера к "человеческому" мышлению. В упрощенном виде смысл этого теста заключается в том, что можно считать искусственный интеллект созданным, если человек, общающийся с двумя собеседниками, один из которых человек, а второй – компьютер, не сможет понять, кто есть кто. То есть в соответствии с тестом Тьюринга, компьютеру требуется научиться имитировать человека в диалоге, чтобы его можно было считать "интеллектуальным".

Такой подход к распознаванию искусственного интеллекта многие критиковали, однако никаких достойных альтернатив тесту Тьюринга предложено не было.

Первый пример, который мы рассмотрим, будет относиться к области обработки естественного языка.

Пример. Создадим программу, имитирующую разговор психотерапевта с пациентом. Прообразом нашей программы является "Элиза", созданная Джозефом Вейценбаумом в лаборатории искусственного интеллекта массачусетского технологического института в 1966 году (названная в честь Элизы из "Пигмалиона"). Она была написана на языке Лисп и состояла всего из нескольких десятков строк программного кода. Эта программа моделировала методику известного психотерапевта Карла Роджерса. В этом подходе психотерапевт играет роль "вербального зеркала" пациента. Он переспрашивает пациента, повторяет его слова, позволяя ему самому найти выход из сложившейся ситуации, прийти в состояние душевного равновесия.

На самом деле эта программа пытается сопоставить вводимые пользователем ответы с имеющимися у нее шаблонами и, если ей это удается, шаблонно же отвечает.

Вейценбаум создал эту программу в качестве шутки. Многие пациенты, пообщавшись с детищем Вейценбаума, утверждали, что "Элиза" им помогла, и отказывались верить, что их собеседником был не психотерапевт, а компьютерная программа. Всю оставшуюся жизнь Вейценбаум пытался охладить восторженных поклонников его программы и убедить общественность, что машина не может мыслить.

Наша программа будет действовать по следующему алгоритму.

1. Попросит человека описать имеющуюся у него проблему.

2. Прочитает строку с клавиатуры.

3. Попытается подобрать шаблон, которому соответствует введенная человеком строка.

4. Если удалось – выдаст соответствующий этому шаблону ответ пользователю.

5. Если подобрать шаблон не удалось – попросит продолжать рассказ.

6. Возвращаемся к пункту 2 и продолжаем процесс.

Для решения этой задачи нам понадобится предикат, преобразующий строку, вводимую пользователем в список слов. Можно было бы воспользоваться модифицированной версией предиката str_a_list, рассмотренного нами в одиннадцатой лекции. Однако он использует предикат fronttoken, который в Турбо Прологе, в отличие от Визуального Пролога, из русских предложений выделяет не слова, а отдельные символы. Поэтому мы напишем новый вспомогательный предикат, который будет считывать символ за символом до тех пор, пока не встретит символ-разделитель (пробел, запятая, точка и другой знак препинания). Так как проверять совпадение очередного символа с символом-разделителем нам придется не раз, заведем список символов-разделителей. Поместим его в раздел описания констант и назовем separators (символы-разделители). После этого все символы до символа-разделителя будут помещены в первое слово строки, а все символы, идущие после символа-разделителя, обработаны подобным образом.

Кроме того, при переписывании строки в список ее слов мы переведем все русские символы, записанные в верхнем регистре (большие буквы), в нижний регистр (маленькие буквы). Это облегчит в дальнейшем процесс распознавания слов. Нам не придется предусматривать всевозможные варианты написания пользователем слова (например, "Да", "да", "ДА"), мы будем уверены, что все символы слова – строчные ("да").

При реализации этого предиката нам понадобится три вспомогательных предиката.

Первый предикат будет преобразовывать прописные русские буквы в строчные, а все остальные символы оставлять неизменными. У него будет два аргумента: первый (входной) – исходный символ, второй (выходной) – символ, полученный преобразованием первого аргумента.

При написании данного предиката стоит учесть, что строчные русские буквы расположены в таблице символов двумя группами. Первая группа (буквы от 'а' до 'п') имеют, соответственно, коды от 160 до 175. Вторая группа (буквы от 'р' до 'я') – коды от 224 до 239.

С учетом вышеизложенного предикат можно записать, например, так:

lower_rus(C,C1):– 'А'<=C,C<='П',!, /* символ C лежит между буквами 'А' и 'П' */ char_int(C,I), /* I – код символа C */ I1=I+(160–128), /* 160 – код буквы 'а', 128 – код буквы 'А'*/ char_int(C1,I1). /* C1 – символ с кодом I1 */lower_rus(C,C1):– 'Р'<=C,C<='Я',!, /* символ C лежит между буквами 'Р' и 'Я' */ char_int(C,I), /* I – код символа C */ I1=I+(224–144), /* 224 – код буквы 'р', 144 – код буквы 'Р'*/ char_int(C1,I1). /* C1 – символ с кодом I1 */lower_rus(C,C). /* символ C отличен от прописной русской буквы и, значит, мы не должны его изменять */

Второй предикат first_word будет иметь три аргумента. Первый (входной) – исходная строка, второй и третий (выходные) – соответственно, первое слово строки (не содержащее прописных русских букв) и остаток строки, полученный удалением из него первого слова.

Выглядеть его реализация будет следующим образом:

first_word("","",""):–!. /* из пустой строки можно выделить только пустые подстроки */first_word(S,W,R):– /* W – первое слово строки S, R – остальные символы исходной строки S */ frontchar(S,C,R1), /* C – первый символ строки S, R1 – остальные символы */ not(member(C,separators)),!, /* символ C не является символом-разделителем */ first_word(R1,S1,R), /* S1 – первое слово строки R1, R – оставшиеся символы строки R1 */ lower_rus(C,C1), /* если C – прописная русская буква , то C1 – соответствующая ей строчная буква, иначе символ C1 не отличается от символа C */ frontchar(W,C1,S1). /* W – результат "приклеивания" символа C1 в начало строки S1 */first_word(S,"",R):– /* в случае, если первый символ оказался символом-разделителем, */frontchar(S,_,R). /* его нужно выбросить, */

Третий предикат del_sep будет предназначен для удаления из начала строки символов-разделителей. У него будет два аргумента. Первый (входной) – исходная строка, второй (выходной) – строка, полученная из первого аргумента удалением символов-разделителей, расположенных в начале строки, если таковые имеются.

del_sep("",""):–!.del_sep(S,S1):– frontchar(S,C,R), /* C – первый символ строки, R – остальные символы */ member(C,separators),!, /* если C является символом-разделителем, */ del_sep(R,S1). /* то переходим к рассмотрению остатка строки */del_sep(S,S) . /* если первый символ строки не является символом-разделителем, то удалять нечего */

И, наконец, предикат, преобразующий строку в список слов.

str_w_list("",[]):–!. /* пустой строке соответствует пустой список слов, входящих в нее */str_w_list(S,[H T]):– first_word(S,H,R),!, /* H – первое слово строки S, R – оставшиеся символы строки S */ str_w_list(R,T). /* T – список, состоящий из слов, входящих в строку R */

Основную работу в программе будет осуществлять предикат recognize, задачей которого будет распознавать шаблон, которому можно сопоставить введенную строку. Этот предикат на входе будет получать список слов строки, а на выходе будет выдавать номер шаблона. По этому номеру другой предикат должен будет выдать на экран соответствующую реакцию (вопрос, реплику, уточнение).

Наша учебная программа будет распознавать одиннадцать шаблонов:

1. Человек хочет закончить работу с программой. Об этой ситуации свидетельствует наличие в списке таких слов, как "пока", "свидания" (часть словосочетания "до свидания"). В ответ программа также прощается и выражает надежду, что она смогла чем-нибудь помочь.

2. Человек испытывает какое-то чувство (наличие в списке слова "испытываю"). Программа реагирует вопросом о том, как давно человек испытывает это чувство.

3. Если во вводимой строке встретились слова "любовь" или "чувства", то программа поинтересуется, не боится ли человек эмоций.

4. При обнаружении слова "секс" во входном списке слов будет выдано сообщение о важности сообщения.

5. В случае наличия слов "бешенство", "гнев" или "ярость", программа уточнит, что человек испытывает в данный момент времени.

6. В ответ на краткий ответ ("да" или "нет") будет выдана просьба рассказать подробнее.

7. Если в списке слов найдутся слова "комплекс" или "фиксация", программа отреагирует замечанием о том, что человек слишком много "играет".

8. Появление слова "всегда" в строке, введенной человеком, приводит к ответной реакции – вопросу о том, может ли человек привести какой-нибудь пример.

9. В случае, если человек упомянул кого-то из своих родных ("папа", "мама", "жена", "муж", "брат", "сестра", "сын", "дочь" и т.д.), программа попросит рассказать поподробнее о его семье. При этом упомянутый родственник будет помещен в базу данных, чтобы потом продолжить этот разговор.

10. Если в процессе разговора была сделана запись во внутреннюю базу данных и в данный момент спросить больше не о чем, программа "вспомнит" об упомянутом родственнике и выдаст фразу: "ранее Вы упоминали ..."

11. И, наконец, если введенная строка не подходит ни под один шаблон, программа просит продолжить рассказ.

А теперь запишем всю программу целиком.

CONSTANTS /* раздел описания констант */separators=[' ', ',', '.', ';'] /* символы-разделители (пробел, запятая, точка, точка с запятой и т.д.) */DOMAINS /* раздел описания доменов */i=integers=stringls=s* /* список слов */lc=char* /* список символов */DATABASE /* раздел описания предикатов базы данных */Important(s)PREDICATES /* раздел описания предикатов */member(s,ls) /* проверяет принадлежность строки списку строк */member(char,lc) /* проверяет принадлежность символа списку символов */lower_rus(char,char) /* преобразует прописную русскую букву в строчную букву */del_sep(s,s) /* удаляет из начала строки символы-разделители */first_word(s,s,s) /* делит строку на первое слово и остаток строки */str_w_list(s,ls) /* преобразует строку в список слов */read_words(ls) /* читает строку с клавиатуры, возвращает список слов, входящих в строку*/recognize(ls,i) /* сопоставляет списку слов число, кодирующее шаблон */answ(ls) /* выводит ответ человеку */eliz /* основной предикат */repeatCLAUSES /* раздел описания предложений */eliz:– repeat, read_words(L), /* читаем строку с клавиатуры, преобразуем ее в список слов L */ recognize(L,I), /* сопоставляем списку слов L номер шаблона I */ answ(I),nl, /* выводим ответ, соответствующий номеру шаблона I */ I=0 /* номер шаблона I, равный нулю, означает, что человек попрощался */.read_words(L):– readln(S), /* читаем строку */ str_w_list(S,L). /* преобразуем строку в список слов */recognize(L,0):– member("пока",L),!; member("свидания",L),!.recognize(L,1):– member("испытываю",L),!.recognize(L,2):– member("любовь",L),!; member("чувства",L),!.recognize(L,3):– member("секс",L),!.recognize(L,4):– member("бешенство",L),!; member("гнев",L),!; member("ярость",L),!.recognize(L,5):– L=["да"],!; L=["нет"],!.recognize(L,6):– member("комплекс",L),!; member("фиксация",L),!.recognize(L,7):– member("всегда",L),!.recognize(L,8):– member("мать",L),assert(important("своей матери")),!; member("мама",L),assert(important("своей маме")),!; member("отец",L),assert(important("своем отце")),!; member("папа",L),assert(important("своем папе")),!; member("муж",L),assert(important("своем муже")),!; member("жена",L),assert(important("своей жене")),!; member("брат",L),assert(important("своем брате")),!; member("сестра",L),assert(important("своей сестре")),!; member("дочь",L),assert(important("своей дочери")),!; member("сын",L),assert(important("своем сыне")),!.recognize(_,9):– important(_),!.recognize(_,10).answ(0):– write("До свидания"),nl, write("Надеюсь, наше общение помогло Вам").answ(1):– write("Как давно Вы это испытываете?").answ(2):– write("Вас пугают эмоции?").answ(3):– write("Это представляется важным").answ(4):– write("А что Вы испытываете сейчас?").answ(5):– write("Расскажите об этом подробнее").answ(6):– write("Слишком много игр").answ(7):– write("Вы можете привести какой-нибудь пример?").answ(8):– write("Расскажите мне подробнее о своей семье").answ(9):– important(X),!, write("Ранее Вы упомянули о ",X), retract(X).answ(10):– write("Продолжайте, пожалуйста").repeat.repeat:– repeat.member(X,[X|_]):–!.member(X,[_|S]):–member(X,S).lower_rus(C,C1):– 'А'<=C,C<='П',!, /* символ C лежит между буквами 'А' и 'П' */ char_int(C,I), /* I – код символа C */ I1=I+(160–128), /* 160 – код буквы 'а', 128 – код буквы 'А'*/ char_int(C1,I1). /* C1 – символ с кодом I1 */lower_rus(C,C1):– 'Р'<=C,C<='Я',!, /* символ C лежит между буквами 'Р' и 'Я' */ char_int(C,I), /* I – код символа C */ I1=I+(224–144), /* 224 – код буквы 'р', 144 – код буквы 'Р'*/ char_int(C1,I1). /* C1 – символ с кодом I1 */lower_rus(C,C). /* символ C отличен от прописной русской буквы и, значит, мы не должны его изменять */del_sep("",""):–!.del_sep(S,S1):– frontchar(S,C,R), /* C – первый символ строки, R – остальные символы */ member(C,separators),!, /* если C является символом-разделителем, */ del_sep(R,S1). /* то переходим к рассмотрению остатка строки */del_sep(S,S) . /* если первый символ строки не является символом-разделителем, то удалять нечего */ str_w_list("",[]):–!. /* пустой строке соответствует пустой список слов, входящих в нее */str_w_list(S,[H|T]):– first_word(S,H,R),!, /* H – первое слово строки S, R – оставшиеся символы строки S */ str_w_list(R,T). /* T – список, состоящий из слов, входящих в строку R */first_word("","",""):–!. /* из пустой строки можно выделить только пустые подстроки */first_word(S,W,R):– /* W – первое слово строки S, R – остальные символы исходной строки S */ frontchar(S,C,R1), /* C – первый символ строки S, R1 – остальные символы */ not(member(C,separators)),!, /* символ C не является символом-разделителем */ first_word(R1,S1,R), /* S1 – первое слово строки R1, R – оставшиеся символы строки R1 */ lower_rus(C,C1), /* если C – прописная русская буква , то C1 – соответствующая ей строчная буква, иначе символ C1 не отличается от символа C */ frontchar(W,C1,S1). /* W – результат "приклеивания" символа C1 в начало строки S1 */first_word(S,"",R):– /* в случае, если первый символ оказался символом-разделителем, */ frontchar(S,_,R). /* его нужно выбросить, */GOAL /* раздел описания цели */write("Расскажите, в чем заключается Ваша проблема"),nl,eliz,readchar(_).

Листинг 14.1. Программа, имитирующая разговор психотерапевта с пациентом

Усовершенствовать работу этой программы можно двумя способами. С одной стороны, можно увеличить количество шаблонов, с другой стороны, можно организовать разные реакции на некоторые из шаблонов (например, используя случайные числа).

В 1977 г. Кеннет Колби, основываясь на принципах организации "Элизы", создал программу, которая подобным образом вводила в заблуждение уже не клиентов психиатров, а самих докторов. Большинство из них после общения с его программой решили, что имели дело с реальным параноиком.

В 1996 г. Грег Гарви создал программную модель католического исповедника, которая опиралась на те же идеи, что и "Элиза".

Другие варианты "Элизы" можно найти в следующих книгах:

· Л. Стерлинг, Э. Шапиро. Искусство программирования на языке Пролог. – М.:Мир, 1990.

· Д. Марселлус. Программирование экспертных систем на Турбо-Прологе. – М.: Финансы и статистика, 1994.

Теперь рассмотрим еще один пример применения Пролога в области искусственного интеллекта. Создадим небольшую экспертную систему. Экспертными системами обычно называют программы, которые могут заменить эксперта в какой-то предметной области. Мы построим классификационную экспертную систему, которая будет пытаться угадать загаданное человеком животное. Если загаданное человеком животное окажется неизвестно нашей программе, у нее будет возможность пополнить свою базу знаний новой информацией.

В связи с тем, что мы планируем пополнять нашу базу знаний, мы будем по окончании работы сохранять ее в файл, а при начале работы считывать информацию из файла в оперативную память. Для этого мы воспользуемся изученными в предыдущей лекции внутренними (динамическими) базами данных.

Так как в Турбо Прологе в базе данных можно размещать только факты, представим правила, определяющие животных в виде фактов.

Определим два предиката внутренней базы данных, которые позволят нам хранить информацию о животных.

Один из них предназначен для хранения характеристик животных и будет иметь два аргумента: первый – номер свойства, второй – его словесное описание.

Небольшой базовый набор свойств может выглядеть, например, так:

cond(1,"кормит детенышей молоком").cond(2,"имеет перья").cond(3,"плавает").cond(4,"ест мясо").cond(5,"имеет копыта").cond(6,"летает").cond(7,"откладывает яйца").cond(8,"имеет шерсть").cond(9,"имеет полосы").cond(10,"имеет пятна").cond(11,"имеет черно-белую окраску").cond(12,"имеет длинную шею").cond(13,"имеет длинные ноги").cond(14,"имеет щупальца").

Второй предикат будет хранить описание животных. Первый его аргумент – название животного, второй – список, элементами которого являются номера свойств, присущих данному животному.

Выглядеть эта база знаний может примерно следующим образом:

rule("гепард",[1,4,8,10]).rule("тигр",[1,4,8,9]).rule("жираф",[1,5,8,10,12,13]).rule("зебра",[1,5,8,9,11]).rule("страус",[2,14]).rule("пингвин",[2,3,11]).rule("орел",[2,6]).rule("кит",[1,3,11]).rule("осьминог",[3,14]).

По сути дела, в виде фактов записаны правила. Например, правило: "если животное кормит детенышей молоком, имеет копыта, пятна, длинную шею и ноги, то это жираф", записано в виде rule("жираф", [1,5,11,13,14]).

Во второй базе мы будем хранить ответы человека в следующем виде:

cond_is(N,'1') /* если загаданное животное имеет свойство с номером N */cond_is(N,'2') /* если загаданное животное не имеет свойства с номером N */

Первую базу назовем knowledge, а вторую – dialog.

Процесс отгадывания задуманного животного будет проходить следующим образом. Будем последовательно перебирать животных, имеющихся в нашей базе знаний. Если загаданное животное обладает всеми характеристиками известного программе животного, делается вывод о том, кто был загадан. Если не удается сопоставить загаданному животному ни одно из животных, имеющихся в базе знаний, производится пополнение базы.

Вот как будет выглядеть реализация написанного выше.

animals:– rule(X,L), check(L), nl,write("Я думаю это ",X), nl,write("Я прав? (1 – да, 2 – нет)"), read_true_char(C),C='1',!.animals:– nl,write("Я не знаю, что это за животное"),nl, nl,write("Давайте добавим его в мою базу знаний."),nl, update.

Предикат check осуществляет проверку свойств, номера которых входят в список, указанный в качестве его единственного аргумента.

check([H|T]):– test_cond(H), check(T).check([]).

Предикат test_cond проверяет наличие у загаданного животного свойства с номером, указанным в качестве его единственного аргумента. Если человеком ранее уже был дан ответ (положительный или отрицательный) по поводу наличия данного свойства, информация об этом имеется в базе данных. Если же в базе нет никакой информации о наличии данного свойства у загаданного животного, нужно задать человеку соответствующий вопрос и добавить его ответ в базу.

Вот как это можно записать.

test_cond(H):– cond_is(H,'1'),!. /* в базе имеется информация о наличии данного свойства */test_cond(H):– cond_is(H,'2'),!, fail. /* в базе имеется информация об отсутствии данного свойства */test_cond(H):– /* в базе нет никакой информации о данном свойстве, получаем ее у человека */ cond(H,S), nl,write("Оно ",S,"? (1 – да, 2 – нет)"), read_true_char(A), assert(cond_is(H,A)), test_cond(H).

Предикат read_true_char осуществляет проверку нажатой пользователем клавиши, и если она отлична от '1' или '2', выводит соответствующее сообщение и повторно считывает символ с клавиатуры.

read_true_char(C):– readchar(C1), test(C1,C).test(C,C):– '1'<=C,C<='2',!.test(_,C):– write("Нажмите 1 или 2!"),nl, readchar(C1), test(C1,C).

Предикат update осуществляет добавление новой информации в базу знаний. Он читает название нового животного, с помощью предиката add_cond формирует список номеров свойств загаданного животного, добавляет соответствующий факт в базу знаний, сохраняет базу в файл.

Вот как он будет выглядеть:

update:– nl,write("Введите название животного:"), readln(S), add_cond(L), /* указываем свойства животного */ assert(rule(S,L),knowledge), /* добавляем информацию в базу знаний*/ save("animals.ddb",knowledge) /* сохраняем базу знаний в файл */.

Предикат add_cond с помощью предиката print_cond выводит уже имеющуюся в базе информацию о свойствах загаданного животного и спрашивает, известно ли еще что-нибудь о нем. В случае необходимости добавляет его новые характеристики, используя предикат read_cond.

add_cond(L):– cond_is(_,'1'),!, /* имеется информация о свойствах животного */ nl,write("О нем известно, что оно: "), print_cond(1,[],L1), /* вывод имеющейся информации о животном */ nl,write("Известно ли о нем еще что-нибудь? (1 – да, 2 – нет)"), read_true_char(C), read_cond(C,L1,L).add_cond(L):– read_cond('1',[],L).

Предикат read_cond, используя предикат ex_cond, добавляет в список номера свойств, уже имеющихся в базе; используя предикат new_cond, добавляет в список номера новых свойств, а также описания самих свойств – в базу знаний.

read_cond('1',L,L2):– ex_cond(1,L,L1,N), new_cond(N,L1,L2),!. read_cond(_,L,L):–!.

Основные предикаты мы рассмотрели, а вот как будет выглядеть вся программа целиком:

DOMAINSi=integers=stringc=charli=i* /* список целых чисел */DATABASE – knowledgecond(i,s) /* свойства животных */rule(s,li) /* описания животных */DATABASE – dialogcond_is(i,c) /* номер условия; '1' – имеет место, '2' – не имеет места у загаданного животного */PREDICATESstartanimals /* отгадывает животное */check(li) /* добавляет в базу информацию о новом животном */test_cond(i) /* проверяет, обладает ли загаданное животное свойством с данным номером */update /* добавляет в базу информацию о новом животном */add_cond(li) /* возвращает список, состоящий из номеров свойств, имеющихся у нового животного */print_cond(i,li,li) /* добавляет в список номера свойств, относительно которых уже были даны утвердительные ответы */read_cond(c,li,li) /* добавляет в список номера свойств, о которых еще не спрашивалось */ex_cond(i,li,li,i) /* добавляет в список номера имеющихся в базе свойств, которыми обладает новое животное */wr_cond(c,i,li,li)new_cond(i,li,li) /* добавляет в список номера новых свойств, которыми обладает новое животное, а также добавляет описания новых свойств в базу знаний */read_true_char(c) /* с помощью предиката test читает символ с клавиатуры, пока он не окажется равен '1' или '2'*/test(c,c) /* добивается, чтобы пользователь нажал один из символов, '1' или '2' */CLAUSESstart:– consult("animals.ddb",knowledge), /* загружаем в базу информацию из базы знаний */ write("Загадайте животное, а я попытаюсь его отгадать"),nl, animals, /* попытка отгадать загаданное животное */ retractall(_,dialog), /* очищаем текущую информацию */ retractall(_,knowledge), /* очищаем информацию об известных животных и свойствах */ nl,nl,write("Хотите еще раз сыграть? (1 – Да, 2 – Нет)"), read_true_char(C), C='1',!,start. start:– nl,nl,write("Всего доброго! До новых встреч"), readchar(_).animals:– rule(X,L), check(L), nl,write("Я думаю, это ",X), nl,write("Я прав? (1 – да, 2 – нет)"), read_true_char(C),C='1',!.animals:– nl,write("Я не знаю, что это за животное"),nl, nl,write("Давайте добавим его в мою базу знаний."),nl, update.update:– nl,write("Введите название животного:"), readln(S), add_cond(L), /* указываем свойства животного */ assert(rule(S,L),knowledge), /* добавляем информацию в базу знаний*/ save("animals.ddb",knowledge) /* сохраняем базу знаний в файл */.add_cond(L):– cond_is(_,'1'),!, /* имеется информация о свойствах животного */ nl,write("О нем известно, что оно: "), print_cond(1,[],L1), /* вывод имеющейся о животном информации */ nl,write("Известно ли о нем еще что-нибудь? (1 – да, 2 – нет)"), read_true_char(C), read_cond(C,L1,L).add_cond(L):– read_cond('1',[],L).print_cond(H,L,L):– not(cond(H,_)),!.print_cond(H,L,L1):– cond_is(H,'1'),!, cond(H,T), H1=H+1, nl,write(T), print_cond(H1,[H L],L1).print_cond(H,L,L1):– H1=H+1, print_cond(H1,L,L1).read_cond('1',L,L2):– ex_cond(1,L,L1,N), new_cond(N,L1,L2),!. read_cond(_,L,L):–!.ex_cond(N,L,L,N):– not(cond(N,_)),!.ex_cond(N,L,L1,N2):– cond_is(N,_),!, N1=N+1, ex_cond(N1,L,L1,N2).ex_cond(N,L,L1,N2):– cond(N,S), nl,write("Оно ",S,"? (1 – да, 2 – нет)"), read_true_char(A), wr_cond(A,N,L,L2), N1=N+1, ex_cond(N1,L2,L1,N2).wr_cond('1',N,L,[N L]):–!.wr_cond('2',_,L,L):–!.new_cond(N,L,L1):– nl,write("Есть еще свойства? (1 – да, 2– нет)"), read_true_char(A), A='1',!, nl,write("Укажите новое свойство, которым обладает животное"), nl,write("в виде 'оно <описание нового свойства>'"), readln(S), assertz(cond(N,S)), /* добавление нового свойства в базу знаний */ N1=N+1, new_cond(N1,[N L],L1).new_cond(_,L,L).check([HT]):– test_cond(H), check(T).check([]).test_cond(H):- cond_is(H,'1'),!. /* в базе имеется информация о наличии свойства */test_cond(H):– cond_is(H,'2'),!, fail. /* в базе имеется информация об отсутствии свойства */test_cond(H):– /* в базе нет никакой информации о данном свойстве, получаем ее у человека */ cond(H,S), nl,write("Оно ",S,"? (1 – да, 2 – нет)"), read_true_char(A), assert(cond_is(H,A)), test_cond(H).read_true_char(C):– readchar(C1), test(C1,C).test(C,C):– '1'<=C,C<='2',!.test(_,C):– write("Нажмите 1 или 2!"),nl, readchar(C1), test(C1,C). GOALstart

Листинг 14.2. Самообучающийся определитель животных

В идеале экспертная система должна уметь объяснять пользователю свое решение, а также, почему она задает тот или иной вопрос. Попробуйте добавить в нашу экспертную систему механизм объяснения.

Две версии этой программы, основанной на правилах (реализующие, соответственно, прямую и обратную цепочки рассуждений), можно найти в книге Д. Марселлус, Программирование экспертных систем на Турбо-Прологе. – М.: Финансы и статистика, 1994. Однако эти программы умеют распознавать только тех животных, которые заложены в них изначально. Возможности динамического пополнения базы знаний они не имеют. Для добавления описания нового животного требуется модификация программного кода.

Кроме того, я рекомендую читателям изучить программу GEOBASE, которая входит в состав и Турбо Пролога, и Visual Prolog. Эта программа содержит информацию по географии США и позволяет создавать запросы к базе данных на естественном (английском) языке.