Основы программирования на языке Пролог

Пролог и искусственный интеллект

Разбить на страницы
Показывать лекцию целиком

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

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

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

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

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

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

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

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

  • Попросит человека описать имеющуюся у него проблему.
  • Прочитает строку с клавиатуры.
  • Попытается подобрать шаблон, которому соответствует введенная человеком строка.
  • Если удалось — выдаст соответствующий этому шаблону ответ пользователю.
  • Если подобрать шаблон не удалось — попросит продолжать рассказ.
  • Возвращаемся к пункту 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, задачей которого будет распознавать шаблон, которому можно сопоставить введенную строку. Этот предикат на входе будет получать список слов строки, а на выходе будет выдавать номер шаблона. По этому номеру другой предикат должен будет выдать на экран соответствующую реакцию (вопрос, реплику, уточнение).

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

  • Человек хочет закончить работу с программой. Об этой ситуации свидетельствует наличие в списке таких слов, как "пока", "свидания" (часть словосочетания "до свидания"). В ответ программа также прощается и выражает надежду, что она смогла чем-нибудь помочь.
  • Человек испытывает какое-то чувство (наличие в списке слова "испытываю"). Программа реагирует вопросом о том, как давно человек испытывает это чувство.
  • Если во вводимой строке встретились слова "любовь" или "чувства", то программа поинтересуется, не боится ли человек эмоций.
  • При обнаружении слова "секс" во входном списке слов будет выдано сообщение о важности сообщения.
  • В случае наличия слов "бешенство", "гнев" или "ярость", программа уточнит, что человек испытывает в данный момент времени.
  • В ответ на краткий ответ ("да" или "нет") будет выдана просьба рассказать подробнее.
  • Если в списке слов найдутся слова "комплекс" или "фиксация", программа отреагирует замечанием о том, что человек слишком много "играет".
  • Появление слова "всегда" в строке, введенной человеком, приводит к ответной реакции — вопросу о том, может ли человек привести какой-нибудь пример.
  • В случае, если человек упомянул кого-то из своих родных ("папа", "мама", "жена", "муж", "брат", "сестра", "сын", "дочь" и т.д.), программа попросит рассказать поподробнее о его семье. При этом упомянутый родственник будет помещен в базу данных, чтобы потом продолжить этот разговор.
  • Если в процессе разговора была сделана запись во внутреннюю базу данных и в данный момент спросить больше не о чем, программа "вспомнит" об упомянутом родственнике и выдаст фразу: "ранее Вы упоминали ..."
  • И, наконец, если введенная строка не подходит ни под один шаблон, программа просит продолжить рассказ.
  • А теперь запишем всю программу целиком.

    CONSTANTS /* раздел описания констант */
    separators=[' ', ',', '.', ';'] 
            /* символы-разделители (пробел, 
               запятая, точка, точка с запятой 
               и т.д.) */
    DOMAINS /* раздел описания доменов */
    i=integer
    s=string
    ls=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 /* основной предикат */
    repeat
    CLAUSES /* раздел описания предложений */
    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(_).

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

    В 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):–!.

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

    DOMAINS
    i=integer
    s=string
    c=char
    li=i* /* список целых чисел */
    DATABASE — knowledge
    cond(i,s) /* свойства животных */
    rule(s,li) /* описания животных */
    DATABASE — dialog
    cond_is(i,c) /* номер условия; '1' — имеет место, 
         '2' — не имеет	места у загаданного 
         животного */
    PREDICATES
    start
    animals /* отгадывает животное */
    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' */
    CLAUSES
    start:–
           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). 
    GOAL
    start

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

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

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

    Самостоятельные задания

  • Создайте "определитель растений" (собак, грибов).
  • Создайте программу, позволяющую диагностировать заболевание по симптомам.
  • Создайте программу, позволяющую диагностировать неисправность какого-либо технически сложного устройства (велосипед, автомобиль, компьютер, телевизор и т.д.).
  • Создайте программу, помогающую школьнику определиться с выбором будущей профессии.
  • Создайте программу, отыскивающую такие расстановки ферзей на пустой шахматной доске, в которой ни один из ферзей не находится под боем другого.
  • Создайте программу, решающую задачу раскраски карты. Задача заключается в следующем. Имеется набор пар, в котором первая компонента представляет собой название страны, вторая - список стран, граничащих со страной, чье название находится в первой компоненте. Требуется сопоставить каждой стране цвет так, чтобы ни одна пара соседних стран не была окрашена в одинаковые цвета. При этом разрешается использовать не более четырех цветов.
  • Создайте программу, осуществляющую поиск пути в лабиринте. Задача заключается в следующем. Имеется описание лабиринта в виде набора координат стен, а также координаты текущей позиции в лабиринте и координаты выхода. Требуется найти путь от текущей позиции до выхода. В качестве дополнительного задания можно подсчитать длину пройденного пути.
  • Создайте программу, осуществляющую Символьное дифференцирование введенной формулы.
  • Страницы:

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

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

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

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

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

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

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

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

  • Попросит человека описать имеющуюся у него проблему.
  • Прочитает строку с клавиатуры.
  • Попытается подобрать шаблон, которому соответствует введенная человеком строка.
  • Если удалось — выдаст соответствующий этому шаблону ответ пользователю.
  • Если подобрать шаблон не удалось — попросит продолжать рассказ.
  • Возвращаемся к пункту 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, задачей которого будет распознавать шаблон, которому можно сопоставить введенную строку. Этот предикат на входе будет получать список слов строки, а на выходе будет выдавать номер шаблона. По этому номеру другой предикат должен будет выдать на экран соответствующую реакцию (вопрос, реплику, уточнение).

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

  • Человек хочет закончить работу с программой. Об этой ситуации свидетельствует наличие в списке таких слов, как "пока", "свидания" (часть словосочетания "до свидания"). В ответ программа также прощается и выражает надежду, что она смогла чем-нибудь помочь.
  • Человек испытывает какое-то чувство (наличие в списке слова "испытываю"). Программа реагирует вопросом о том, как давно человек испытывает это чувство.
  • Если во вводимой строке встретились слова "любовь" или "чувства", то программа поинтересуется, не боится ли человек эмоций.
  • При обнаружении слова "секс" во входном списке слов будет выдано сообщение о важности сообщения.
  • В случае наличия слов "бешенство", "гнев" или "ярость", программа уточнит, что человек испытывает в данный момент времени.
  • В ответ на краткий ответ ("да" или "нет") будет выдана просьба рассказать подробнее.
  • Если в списке слов найдутся слова "комплекс" или "фиксация", программа отреагирует замечанием о том, что человек слишком много "играет".
  • Появление слова "всегда" в строке, введенной человеком, приводит к ответной реакции — вопросу о том, может ли человек привести какой-нибудь пример.
  • В случае, если человек упомянул кого-то из своих родных ("папа", "мама", "жена", "муж", "брат", "сестра", "сын", "дочь" и т.д.), программа попросит рассказать поподробнее о его семье. При этом упомянутый родственник будет помещен в базу данных, чтобы потом продолжить этот разговор.
  • Если в процессе разговора была сделана запись во внутреннюю базу данных и в данный момент спросить больше не о чем, программа "вспомнит" об упомянутом родственнике и выдаст фразу: "ранее Вы упоминали ..."
  • И, наконец, если введенная строка не подходит ни под один шаблон, программа просит продолжить рассказ.
  • А теперь запишем всю программу целиком.

    CONSTANTS /* раздел описания констант */
    separators=[' ', ',', '.', ';'] 
            /* символы-разделители (пробел, 
               запятая, точка, точка с запятой 
               и т.д.) */
    DOMAINS /* раздел описания доменов */
    i=integer
    s=string
    ls=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 /* основной предикат */
    repeat
    CLAUSES /* раздел описания предложений */
    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(_).

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

    В 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):–!.

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

    DOMAINS
    i=integer
    s=string
    c=char
    li=i* /* список целых чисел */
    DATABASE — knowledge
    cond(i,s) /* свойства животных */
    rule(s,li) /* описания животных */
    DATABASE — dialog
    cond_is(i,c) /* номер условия; '1' — имеет место, 
         '2' — не имеет	места у загаданного 
         животного */
    PREDICATES
    start
    animals /* отгадывает животное */
    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' */
    CLAUSES
    start:–
           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). 
    GOAL
    start

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

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

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

    Самостоятельные задания

  • Создайте "определитель растений" (собак, грибов).
  • Создайте программу, позволяющую диагностировать заболевание по симптомам.
  • Создайте программу, позволяющую диагностировать неисправность какого-либо технически сложного устройства (велосипед, автомобиль, компьютер, телевизор и т.д.).
  • Создайте программу, помогающую школьнику определиться с выбором будущей профессии.
  • Создайте программу, отыскивающую такие расстановки ферзей на пустой шахматной доске, в которой ни один из ферзей не находится под боем другого.
  • Создайте программу, решающую задачу раскраски карты. Задача заключается в следующем. Имеется набор пар, в котором первая компонента представляет собой название страны, вторая - список стран, граничащих со страной, чье название находится в первой компоненте. Требуется сопоставить каждой стране цвет так, чтобы ни одна пара соседних стран не была окрашена в одинаковые цвета. При этом разрешается использовать не более четырех цветов.
  • Создайте программу, осуществляющую поиск пути в лабиринте. Задача заключается в следующем. Имеется описание лабиринта в виде набора координат стен, а также координаты текущей позиции в лабиринте и координаты выхода. Требуется найти путь от текущей позиции до выхода. В качестве дополнительного задания можно подсчитать длину пройденного пути.
  • Создайте программу, осуществляющую Символьное дифференцирование введенной формулы.
  • Вернуться к учебному плану