Пролог и искусственный интеллект
Аннотация: Применение Пролога в области искусственного интеллекта. Тест Тьюринга. Проекты «Электронный психотерапевт», «Самообучающийся определитель животных».
В этой лекции речь пойдет о возможных применениях Пролога в области искусственного интеллекта . Конечно, ознакомиться с данной темой достаточно полно в рамках одной лекции мы не успеем. Однако хочется надеяться, что сможем пробежаться по верхушкам и рассмотреть пару простых примеров.
В 1950 году Алан Тьюринг в статье «Вычислительная техника и интеллект » (книга «Может ли машина мыслить?») предложил эксперимент, позднее названный «тест Тьюринга», для проверки способности компьютера к «человеческому» мышлению. В упрощенном виде смысл этого теста заключается в том, что можно считать искусственный интеллект созданным, если человек, общающийся с двумя собеседниками, один из которых человек, а второй — компьютер , не сможет понять, кто есть кто. То есть в соответствии с тестом Тьюринга, компьютеру требуется научиться имитировать человека в диалоге, чтобы его можно было считать «интеллектуальным».
Такой подход к распознаванию искусственного интеллекта многие критиковали, однако никаких достойных альтернатив тесту Тьюринга предложено не было.
Первый пример, который мы рассмотрим, будет относиться к области обработки естественного языка.
Пример. Создадим программу, имитирующую разговор психотерапевта с пациентом. Прообразом нашей программы является «Элиза», созданная Джозефом Вейценбаумом в лаборатории искусственного интеллекта массачусетского технологического института в 1966 году (названная в честь Элизы из «Пигмалиона»). Она была написана на языке Лисп и состояла всего из нескольких десятков строк программного кода. Эта программа моделировала методику известного психотерапевта Карла Роджерса. В этом подходе психотерапевт играет роль «вербального зеркала» пациента. Он переспрашивает пациента, повторяет его слова, позволяя ему самому найти выход из сложившейся ситуации, прийти в состояние душевного равновесия.
На самом деле эта программа пытается сопоставить вводимые пользователем ответы с имеющимися у нее шаблонами и, если ей это удается, шаблонно же отвечает.
Вейценбаум создал эту программу в качестве шутки. Многие пациенты, пообщавшись с детищем Вейценбаума, утверждали, что «Элиза» им помогла, и отказывались верить, что их собеседником был не психотерапевт, а компьютерная программа . Всю оставшуюся жизнь Вейценбаум пытался охладить восторженных поклонников его программы и убедить общественность, что машина не может мыслить.
Наша программа будет действовать по следующему алгоритму.
- Попросит человека описать имеющуюся у него проблему.
- Прочитает строку с клавиатуры.
- Попытается подобрать шаблон, которому соответствует введенная человеком строка.
- Если удалось — выдаст соответствующий этому шаблону ответ пользователю.
- Если подобрать шаблон не удалось — попросит продолжать рассказ.
- Возвращаемся к пункту 2 и продолжаем процесс.
Для решения этой задачи нам понадобится предикат , преобразующий строку, вводимую пользователем в список слов. Можно было бы воспользоваться модифицированной версией предиката str_a_list , рассмотренного нами в одиннадцатой лекции. Однако он использует предикат fronttoken , который в Турбо Прологе, в отличие от Визуального Пролога, из русских предложений выделяет не слова, а отдельные символы. Поэтому мы напишем новый вспомогательный предикат , который будет считывать символ за символом до тех пор, пока не встретит символ-разделитель ( пробел , запятая, точка и другой знак препинания). Так как проверять совпадение очередного символа с символом-разделителем нам придется не раз, заведем список символов-разделителей. Поместим его в раздел описания констант и назовем separators (символы-разделители). После этого все символы до символа-разделителя будут помещены в первое слово строки, а все символы, идущие после символа-разделителя, обработаны подобным образом.
Кроме того, при переписывании строки в список ее слов мы переведем все русские символы, записанные в верхнем регистре (большие буквы), в нижний регистр (маленькие буквы). Это облегчит в дальнейшем процесс распознавания слов. Нам не придется предусматривать всевозможные варианты написания пользователем слова (например, «Да», «да», «ДА»), мы будем уверены, что все символы слова — строчные («да»).
При реализации этого предиката нам понадобится три вспомогательных предиката.
Первый предикат будет преобразовывать прописные русские буквы в строчные, а все остальные символы оставлять неизменными. У него будет два аргумента: первый ( входной ) — исходный символ, второй (выходной) — символ, полученный преобразованием первого аргумента.
При написании данного предиката стоит учесть, что строчные русские буквы расположены в таблице символов двумя группами. Первая группа (буквы от ‘а’ до ‘п’ ) имеют, соответственно, коды от 160 до 175. Вторая группа (буквы от ‘р’ до ‘я’ ) — коды от 224 до 239.
С учетом вышеизложенного предикат можно записать, например, так:
Второй предикат 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):– 'А'