В этой лекции речь пойдет о возможных применениях Пролога в области искусственного интеллекта. Конечно, ознакомиться с данной темой достаточно полно в рамках одной лекции мы не успеем. Однако хочется надеяться, что сможем пробежаться по верхушкам и рассмотреть пару простых примеров.
В 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 будет предназначен для удаления из начала строки символов-разделителей. У него будет два аргумента. Первый (входной) – исходная строка, второй (выходной) – строка, полученная из первого аргумента удалением символов-разделителей, расположенных в начале строки, если таковые имеются.
|
|
И, наконец, предикат, преобразующий строку в список слов.
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. Эта программа содержит информацию по географии США и позволяет создавать запросы к базе данных на естественном (английском) языке.