Сибирский университет потребительской кооперации
Опубликован: 04.05.2005 | Доступ: свободный | Студентов: 4322 / 1347 | Оценка: 4.45 / 4.22 | Длительность: 12:28:00
ISBN: 978-5-9556-0034-5
Лекция 14:

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

< Лекция 13 || Лекция 14: 123
Аннотация: Применение Пролога в области искусственного интеллекта. Тест Тьюринга. Проекты "Электронный психотерапевт", "Самообучающийся определитель животных".

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

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

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

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

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

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

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

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

  1. Попросит человека описать имеющуюся у него проблему.
  2. Прочитает строку с клавиатуры.
  3. Попытается подобрать шаблон, которому соответствует введенная человеком строка.
  4. Если удалось — выдаст соответствующий этому шаблону ответ пользователю.
  5. Если подобрать шаблон не удалось — попросит продолжать рассказ.
  6. Возвращаемся к пункту 2 и продолжаем процесс.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  1. Человек хочет закончить работу с программой. Об этой ситуации свидетельствует наличие в списке таких слов, как "пока", "свидания" (часть словосочетания "до свидания"). В ответ программа также прощается и выражает надежду, что она смогла чем-нибудь помочь.
  2. Человек испытывает какое-то чувство (наличие в списке слова "испытываю"). Программа реагирует вопросом о том, как давно человек испытывает это чувство.
  3. Если во вводимой строке встретились слова "любовь" или "чувства", то программа поинтересуется, не боится ли человек эмоций.
  4. При обнаружении слова "секс" во входном списке слов будет выдано сообщение о важности сообщения.
  5. В случае наличия слов "бешенство", "гнев" или "ярость", программа уточнит, что человек испытывает в данный момент времени.
  6. В ответ на краткий ответ ("да" или "нет") будет выдана просьба рассказать подробнее.
  7. Если в списке слов найдутся слова "комплекс" или "фиксация", программа отреагирует замечанием о том, что человек слишком много "играет".
  8. Появление слова "всегда" в строке, введенной человеком, приводит к ответной реакции — вопросу о том, может ли человек привести какой-нибудь пример.
  9. В случае, если человек упомянул кого-то из своих родных ("папа", "мама", "жена", "муж", "брат", "сестра", "сын", "дочь" и т.д.), программа попросит рассказать поподробнее о его семье. При этом упомянутый родственник будет помещен в базу данных, чтобы потом продолжить этот разговор.
  10. Если в процессе разговора была сделана запись во внутреннюю базу данных и в данный момент спросить больше не о чем, программа "вспомнит" об упомянутом родственнике и выдаст фразу: "ранее Вы упоминали ..."
  11. И, наконец, если введенная строка не подходит ни под один шаблон, программа просит продолжить рассказ.

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

CONSTANTS /* раздел описания констант */
separators=[' ', ',', '.', ';'] 
        /* символы-разделители (пробел, 
           запятая, точка, точка с запятой 
           и т.д.) */
DOMAINS /* раздел описания доменов */
i=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(_).
Листинг 14.1. Программа, имитирующая разговор психотерапевта с пациентом
< Лекция 13 || Лекция 14: 123
Виктор Бондарь
Виктор Бондарь

После приведения формулы вида ПНФ к виду ССФ вы получаете формулу, в безквантовой матрице которой дизъюнкт содержит оба контранрных атома:. Как тогда проводить его унификацию, если в случае замены x на f(x) весь дизъюнкт обратится в единицу?

Ольга Потапенко
Ольга Потапенко

никак не могу увидеть тексты самих лекций.