Назад | Содержание | Вперёд

14. 5.    Реализация

Теперь мы приступим к реализации нашей оболочки, следуя тем идеям, которые обсуждались в предыдущем разделе. На рис. 14.9 показаны основные объекты, которыми манипулирует оболочка. Цель - это вопрос, подлежащий рассмотрению; Трасса - это цепочка, составленная из "целей-предков" и правил, находящихся между вершиной Цель и вопросом самого верхнего уровня; Ответ - решающее дерево типа И / ИЛИ для вершины Цель.

fig14_9.gif (858 bytes)

Рис. 14. 9.  Отношение рассмотреть( Цель,  Трасса,  Ответ).
Ответ - это И / ИЛИ решающее дерево для целевого
утверждения Цель.

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

        рассмотреть( Цель, Трасса, Ответ)

Эта процедура находит ответ Ответ на вопрос Цель. Процедура

        ответпольз( Цель, Трасса, Ответ)

порождает решения для тех вопросов Цель, которые можно задавать пользователю. Она спрашивает пользователя об истинности утверждения Цель, а также отвечает на вопросы "почему". Процедура

        выдать( Ответ)

выводит результат и отвечает на вопросы "как". Все эти процедуры приводятся в действие процедурой-драйвером эксперт.


14. 5. 1.    Процедура     рассмотреть

Центральной процедурой оболочки является процедура

        рассмотреть( Цель, Трасса, Ответ)

которая будет находить ответ Ответ на заданный вопрос Цель, используя принципы, намеченные в общих чертах в разд. 14.4.1: найти Цель среди фактов базы знаний, или применить правило из базы знаний, или спросить пользователя, или же обработать Цель как И / ИЛИ-комбинацию подцелей.

Аргументы имеют следующий смысл и следующую структуру:

Цель              вопрос, подлежащий рассмотрению, представленный
                       как И / ИЛИ-комбинация простых утверждений, например

                       X имеет перья или X летает или
                       X откладывает яйца

Трасса        цепочка, составленная из целей-предков и правил,
                    расположенных между Цель и исходной целью самого верхнего уровня. Представляется как список, состоящий из элементов вида

                       Цель  по  Прав

что означает:  Цель рассматривалась с использованием правила  Прав. Например, пусть исходной целью будет "питер это тигр", а текущей целью - "питер ест мясо". В соответствии с базой знаний рис. 14.5 имеем трассу

                      [( питер это хищник) по прав3,
                        ( питер это тигр) по прав5 ]

Смысл ее можно выразить так:

Я могу использовать "питер ест мясо" для того, чтобы проверить по прав3, что "питер это хищник".

Далее, я могу использовать "питер это хищник" для того, чтобы проверить по прав5, что "питер это тигр".

Ответ        решающее И / ИЛИ-дерево для вопроса  Цель. Общая форма
                   представления для объекта Ответ:

                       Заключение было Найдено

где Найдено - это обоснование для результата Заключение. Следующие три примера иллюстрируют различные варианты ответов:

(1)        ( соед( радиатор, предохр1) это правда) было
                                                        'найдено как факт'

(2)        (питер ест мясо) это ложь было сказано
(3)        (питер это хищник) это правда было
                    ( 'выведено по' прав3 из
            (питер это млекопитающее) это правда было
                    ( 'выведено по' прав1 из
            (питер имеет шерсть) это правда было сказано)
            и
            (питер ест мясо) это правда было сказано )

На рис. 14.10 показана прологовская программа для процедуры рассмотреть. В этой программе реализованы принципы разд. 14.4.1 с использованием только что описанных структур данных.

% Процедура
%
% рассмотреть( Цель, Трасса, Ответ)
%
% находит Ответ на вопрос Цель. Трасса - это цепочка
% целей-предков и правил. "рассмотреть" стремится найти
% положительный ответ на вопрос. Ответ "ложь" выдается
% только в том случае, когда рассмотрены все возможности,
% и все они дали результат "ложь".

        :-ор( 900, xfx, :).
        :-ор( 800, xfx, было).
        :-ор( 870, fx, если).
        :-ор( 880, xfx, то).
        :-ор( 550, xfy, или).
        :-ор( 540, xfy, и).
        :- ор( 300, fx, 'выведено по').
        :- ор( 600, xfx, из).
        :- ор( 600, xfx, по).

% В программе предполагается,что ор( 700, хfх, это), ор( 500, fx, не)

        рассмотреть( Цель, Трасса, Цель это правда
                                                было 'найдено как факт') :-
                факт : Цель.

% Предполагается, что для каждого типа цели
% существует только одно правило

        рассмотреть( Цель, Трасса,
                Цель это ПравдаЛожь
                было 'выведено по' Прав из Ответ) :-
            Прав : если Условие то Цель,

                                    % Правило, относящееся к цели
            рассмотреть( Условие, [Цель по Прав | Трасса], Ответ),
            истинность( Ответ, ПравдаЛожь).

        рассмотреть( Цель1 и Цель2, Трасса, Ответ) :-  !,
                рассмотреть( Цель1, Трасса, Ответ1),
                продолжить( Ответ1, Цель1 и Цель2, Трасса, Ответ).

        рассмотреть( Цель1 или Цель2, Трасса, Ответ) :-
                рассм_да( Цель1, Трасса, Ответ);

                                    % Положительный ответ на Цель1
                рассм_да( Цель2, Трасса, Ответ).
                                    % Положительный ответ на Цель2

        рассмотреть( Цель1 или Цель2, Трасса,
                                                                Ответ1 и Ответ2) :-  !,
                not рассм_да( Цель1, Трасса, _ ),
                not рассм_да( Цель2, Трасса, _ ),

                                    % Нет положительного ответа
                рассмотреть( Цель1, Трасса, Ответ1),
                                    % Ответ1 отрицательный
                рассмотреть( Цель2, Трасса, Ответ2).
                                    % Ответ2 отрицательный

        рассмотреть( Цель, Трасса,
                                            Цель это Ответ было сказано) :-
                ответпольз( Цель, Трасса, Ответ).
        % Ответ дан пользователем
        рассм_да( Цель, Трасса, Ответ) :-
                рассмотреть( Цель, Трасса, Ответ),
                положительный( Ответ).

        продолжить( Ответ1, Цель1 и Цель2, Трасса, Ответ) :-
                положительный( Ответ1),
                рассмотреть( Цель2, Трасса, Ответ2),
                ( положительный( Ответ2), Ответ = Ответ1 и Ответ2;
                отрицательный( Ответ2), Ответ = Ответ2).

        продолжить( Ответ1, Цель1 и Цель2, _, Ответ1) :-
                отрицательный( Ответ1).

        истинность( Вопрос это ПравдаЛожь было Найдено,
                                                                            ПравдаЛожь) :-  !.

        истинность( Ответ1 и Ответ2, ПравдаЛожь) :-
                истинность( Ответ1, правда),
                истинность( Ответ2, правда),  !,
                ПравдаЛожь = правда;
                ПравдаЛожь = ложь.

        положительный( Ответ) :-
                истинность( Ответ, правда).

        отрицательный( Ответ) :-
                истинность( Ответ, ложь).

Рис. 14. 10.  Основная процедура оболочки экспертной системы.


14. 5. 2.    Процедура    ответпольз

Прежде чем перейти к написанию процедуры ответпольз, давайте рассмотрим одну полезную вспомогательную процедуру

        принять( Ответ)

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

        принять( Ответ) :-
                read( Ответ1),
                означает( Ответ1, Значение),  !,

                                                            % Ответ1 означает что-нибудь?
                Ответ = Значение;         % Да
                nl, write( 'Непонятно, попробуйте еще раз,      % Нет
                                                        пожалуйста'),   nl,
                принять( Ответ).           % Новая попытка

        означает( да, да).
        означает( д, да).
        означает( нет, нет).
        означает( н, нет).
        означает( почему, почему).
        означает( п, почему).

Следует заметить, что процедурой принять нужно пользоваться с осторожностью, так как она содержит взаимодействие с пользователем. Следующий фрагмент программы может служить примером неудачной попытки запрограммировать интерпретацию ответов пользователя:

        принять( да), интерп_да( ...);
        принять( нет), интерп_нет( ...);
        . . .

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

        принять( Ответ),
        ( Ответ = да, интерп_да( ...);
          Ответ = нет, интерп_нет( ...);
          ... )

        Процедура

        ответпольз( Цель, Трасса, Ответ)

спрашивает пользователя об истинности утверждения Цель. Ответ - это результат запроса. Трасса используется для объяснения в случае, если пользователь спросит "почему".

Сначала процедура ответпольз должна проверить, является ли Цель информацией, которую можно запрашивать у пользователя. Это свойство объекта Цель задается отношением

        можно_спросить( Цель)

которое в дальнейшем будет усовершенствовано. Если спросить можно, то утверждение Цель выдается пользователю, который, в свою очередь, указывает истинно оно или ложно. Если пользователь спросит "почему", то ему выдается Трасса. Если утверждение Цель истинно, то пользователь укажет также значения содержащихся в нем переменных (если таковые имеются).

Все вышеизложенное можно запрограммировать (в качестве первой попытки) следующим образом:

        остветпольз( Цель, Трасса, Ответ) :-
                можно_спросить( Цель),
            % Можно ли спрашивать
                спросить( Цель, Трасса, Ответ).
                            % Задать вопрос относительно утверждения Цель

        спросить( Цель, Трасса, Ответ) :-
                показать( Цель),

                            % Показать пользователю вопрос
                принять(Ответ1),                        % Прочесть ответ
                обработать( Ответ1, Цель, Трасса, Ответ).
                            % Обработать ответ

        обработать( почему, Цель, Трасса, Ответ) :-
                            % Задан вопрос "почему"
                показать_трассу( Трасса),
                            % Выдача ответа на вопрос "почему"
                спросить( Цель, Трасса, Ответ).
                            % Еще раз спросить

        обработать( да, Цель, Трасса, Ответ) :-
                            % Пользователь ответил, что Цель истинна
                Ответ = правда,
                запрос_перем( Цель);

                            % Вопрос о значении переменных
                спросить( Цель, Трасса, Ответ).
                            % Потребовать от пользователя новых решений

        обработать( нет, Цель, Трасса, ложь).
                            % Пользователь ответил, что Цель ложна

        показать( Цель) :-
                nl, write( 'Это правда:'),
                write( Цель), write( ?), nl.

Обращение к процедуре запрос_перем( Цель) нужно для того, чтобы попросить пользователя указать значение каждой из переменных, содержащихся в утверждении Цель:

        запрос_перем( Терм) :-
                var( Терм),  !,
                 % Переменная ?
                nl, write( Терм), write( '='),
                read( Терм).
                   % Считать значение переменной

        запрос_перем( Терм) :-
                Терм =.. [Функтор | Аргументы],

                                % Получить аргументы структуры
        запрос_арг( Аргументы).
                                % Запросить значения переменных в аргументах

        запрос_арг( [ ]).

        запрос_арг( [Терм | Термы]) :-
                запрос_перем( Терм),
                запрос_арг( Термы).

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

        можно_спросить( X ест Y).

(В приведенных ниже диалогах между пролог-системой и пользователем тексты пользователя даются полужирным шрифтом, а реплики пролог-системы курсивом).

        ?-  ответпольз( питер ест мясо, [ ], Ответ).

            Это правда: питер ест мясо?          % Вопрос пользователю
            да.                                                         % Ответ пользователя

            Ответ = правда

Более интересный пример диалога (с использованием переменных) мог бы выглядеть примерно так:

        ?-  ответпольз( Кто ест Что, [ ], Ответ).

        Это правда: _17 ест _18?
                                % Пролог дает переменным свои внутренние имена
        да.
        _17 = питер.
        _18 = мясо.

        Ответ = правда.
        Кто = питер
        Что = мясо;
        % Возврат для получения других решений

        Это правда: _17 ест _18?
        да.
        _17 = сьюзен.
        _18 = бананы.

        Ответ = правда
        Кто = сьюзен
        Что = бананы;

        Это правда : _17 ест _18?
        нет.

        Ответ = ложь


14. 5. 3.    Усовершенствование процедуры    ответпольз

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

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

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

        можно_спросить(Х ест Y, 'Животное' ест 'Что-то').

При передаче запроса пользователю каждая переменная вопроса должна быть заменена на ключевое слово, взятое из формата, например:

        ?-  ответпольз( X ест Y, [ ], Ответ).

        Это правда:   Животное ест Что-то?
        да.

        Животное = питер.
        Что-то = мясо.

        Ответ = правда
        X = питер
        Y = мясо

В улучшенной версии процедуры ответпольз, показанной на рис. 14.11, такое форматирование запросов выполняется процедурой

        формат( Цель, ВнешФормат, Вопрос, Перем0, Перем )

Здесь Цель - утверждение, которое нужно форматировать. ВнешФормат определяет внешний формат этого утверждения, задаваемый отношением

        можно_спросить( Цель, ВнешФормат)

Вопрос - это Цель, отформатированная в соответствии с ВнешФормат. Перем - список переменных, входящих в Цель, вместе с соответствующими ключевыми словами (как указано в ВнешФормат), причем список Перем получается из списка Перем0 добавлением новых переменных. Например:

        ?-  формат( X передает документы Y,
                            'Кто' передает 'Что' 'Кому',
                            Вопрос, [ ], Перем).

        Вопрос = 'Кто' передает документы 'Кому',
        Перем = [ Х/'Кто', Y/'Кому'].

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

        assert( сказано( мери передает документы друзьям, правда) ).

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

        ( X имеет Y) и                             % Первый вариант - Цель1
        . . .

        ( X1 имеет Y1) и                         % Второй вариант - Цель2
        . . .

Допустим также, что пользователя просят (через механизм возвратов) предложить несколько решений для Цель1. Затем процесс рассуждений продвигается вплоть до Цель2. Так как у нас уже есть несколько решений для Цель1, мы захотим, чтобы система автоматически применила их и к Цель2 (поскольку очевидно, что они удовлетворяют Цель2). Теперь предположим, что система пытается применить эти решения к Цель2, но ни одно из них не удовлетворяет некоторой другой цели, расположенной ниже. Система делает возврат к Цель2 и просит пользователя предложить новые решения. Если пользователь введет еще несколько решений, то их также придется запомнить. И если система в дальнейшем сделает возврат к Цель1, то эти новые решения надо будет применить к Цель1.

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

        сказано( Цель, Истинность, Индекс)

где Индекс - это значение счетчика, ответов пользователя. Процедура

        ответпольз( Цель, Трасса, Ответ)

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

        ответпольз( Цель, Трасса, Ответ, N)

где N - некоторое целое число. Такое обращение к ответпольз должно порождать решения для Цель с индексами, начиная с N и далее. Обращение

        ответпольз( Цель, Трасса, Ответ)

соответствует получению всех решений, индексируемых, начиная с 1, поэтому мы имеем следующее соотношение:

        ответпольз( Цель, Трасса, Ответ) :-
                ответпольз( Цель, Трасса, Ответ, 1).

Принцип работы процедуры

        ответпольз( Цель, Трасса, Ответ, N)

таков: сначала получить решения для Цель, отыскивая в памяти все уже известные решения с индексами, начиная с N и далее. Когда все старые решения исчерпаются, начать задавать вопросы пользователю относительно утверждения Цель, записывая полученные таким образом новые решения в память при помощи assert и индексируя их должным образом при помощи целых чисел. Когда пользователь сообщит, что больше нет решений, записать в память факт

        конец_ответов( Цель)

Если пользователь с самого начала скажет, что решений нет вообще, то записать факт

        сказано( Цель, ложь, Индекс)

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

Однако существует еще одна трудность. Пользователь может, оставляя некоторые переменные неконкретизированными, указывать общие решения. Если найдено положительное решение, более общее, чем Цель, или столь же общее, как Цель, то нет смысла продолжать задавать вопросы об утверждении Цель, поскольку мы уже имеем более общее решение. Аналогичным образом следует поступить, если обнаружен факт

        сказано( Цель, ложь, _ )

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

        конкретный( Терм)

истинно, если Терм не содержит переменных. Другое

        конкретизация( Терм, Терм1)

означает, что Терм1 есть некоторая конкретизация (частный случай) терма Терм, т. е. Терм - это утверждение не менее общее, чем Терм1. Например:

        конкретизация( X передает информацию Y,
                                    мэри передает информацию Z)

Обе процедуры основаны на еще одной процедуре:

        нумпер( Терм, N, М)

Эта процедура "нумерует" переменные, содержащиеся в Терм, заменяя каждую из них на некоторый специальный новый терм таким образом, чтобы эти "нумерующие" термы соответствовали числам от N до М-1, Например, пусть эти термы имеют вид

        пер/0, пер/1, пер/2, ...

тогда в результате обращения к системе

        ?-  Терм - f( X, t( a,Y, X) ), нумпер( Терм, 5, М).

мы получим

        Терм = f( пер/5, t( а, пер/6, пер/5) )
        М = 7

Отношение, подобное нумпер, часто входит в состав пролог-системы в качестве встроенной процедуры. Если это не так, то его можно реализовать программно следующим способом:

        нумпер( Терм, N, Nплюс1) :-
                var( Терм),  !,
                                % Переменная ?
                Терм = пер/N,
                Nплюс1 is N + 1.

% Процедура
%
% ответпольз( Цель, Трасса, Ответ)
%
% порождает, используя механизм возвратов, все решения
% для целевого утверждения Цель, которые указал пользователь.
% Трасса - это цепочка целей-предков и правил,
% используемая для объяснения типа "почему".

        ответпольз( Цель, Трасса, Ответ) :-
                можно_спросить( Цель, _ ),
               % Можно спросить ?
                копия( Цель, Копия),                           % Переименование переменных
                ответпольз( Цель, Копия, Трасса, Ответ, 1).

% Не спрашивать второй раз относительно конкретизированной цели

        ответпольз( Цель, _, _, _, N) :-
                N > 1,
                                                      % Повторный вопрос?
                конкретный( Цель),  !,                         % Больше не спрашивать
                fail.

% Известен ли ответ для всех конкретизации утверждения Цель?

        ответпольз( Цель, Копия, _, Ответ, _ ) :-
                сказано( Копия, Ответ, _ ),
                конкретизация( Копия, Цель),  !.
       % Ответ известен

% Найти все известные решения для Цель с индексами, начиная с N

        ответпольз( Цель, _, _, правда, N) :-
                сказано( Цель, правда, М),
                М >= N.

% Все уже сказано об утверждении Цель?

        ответпольз( Цель, Копия, _, Ответ, _) :-
                конец_ответов( Копия),
                конкретизация( Копия, Цель),  !,
      % Уже все сказано
                fail.

% Попросить пользователя дать (еще) решения

        ответпольз( Цель, _, Трасса, Ответ, N) :-
                спросить_польз( Цель, Трасса, Ответ, N).

        спросить_польз( Цель, Трасса, Ответ, N) :-
                можно спросить( Цель, ВнешФормат),
                формат( Цель, ВнешФормат, Вопрос, [ ], Перем),

                                                                     % Получить формат вопроса
                спросить( Цель, Вопрос, Перем, Трасса, Ответ, N).

        спросить( Цель, Вопрос, Перем, Трасса, Ответ, N) :-
                nl,
                ( Перем = [ ],  !,
                      % Сформулировать вопрос
                write( 'Это правда: ');
                write( 'Есть (еще) решения для :' )),
                write( Вопрос), write( '?'),
                принять( Ответ1),  !,
             % Ответ1 - да/нет/почему
                обработать( Ответ1, Цель, Вопрос, Перем,
                                                                                Трасса, Ответ, N).

        обработать( почему, Цель, Вопрос, Перем,
                                                                                Трасса, Ответ, N):-
                 выд_трассу( Трасса),
                 спросить( Цель, Вопрос, Перем, Трасса, Ответ, N).

        обработать( да, Цель,_, Перем, Трасса, правда, N) :-
                след_индекс( Инд),
                                       
% Получить новый индекс для "сказано"
                Инд1 is Инд + 1,
                ( запрос_перем( Перем),
                assertz( сказано( Цель, правда, Инд) );

                                                                  % Запись решения
                копия( Цель, Копия),            % Копирование цели
                ответпольз( Цель, Копия, Трасса, Ответ, Инд1) ).
                                                                  % Есть еще решения?

        обработать( нет, Цель, _, _, _, ложь, N) :-
                копия( Цель, Копия),
                сказано( Копия, правда, _),  !,

                                        % 'нет' означает, больше нет решений
                assertz( конец_ответов( Цель) ),
                                        % Отметить конец ответов
                fail;
                след_индекс( Инд),

                                        % Следующий свободный индекс для "сказано"
                assertz( сказано( Цель, ложь, Инд) ).
                                        % 'нет' означает нет ни одного решения

        формат( Пер, Имя, Имя, Перем, [Пер/Имя | Перем]) :-
                var( Пер),  !.
        формат( Атом, Имя, Атом, Перем, Перем) :-
                atomic( Атом),  !,
                atomic( Имя).
        формат( Цель, Форм, Вопрос, Перем0, Перем) :-
                Цель =.. [Функтор | Apг1],
                Форм =.. [Функтор | Форм1],
                формвсе( Apг1, Форм1, Арг2, Перем0, Перем),
                Вопрос =.. [Функтор | Арг2].

        формвсе( [ ], [ ], [ ], Перем, Перем).

        формвсе( [Х | СпХ], [Ф | СпФ], [В | СпВ], Перем0, Перем) :-
                формвсе( СпХ, СпФ, СпВ, Перем0, Перем1),
                формат( X, Ф, В, Перем1, Перем).

        запрос_перем( [ ]).

        запрос_перем( [Переменная/Имя | Переменные]) :-
                nl, write( Имя), write( '='),
                read( Переменная),
                запрос_перем( Переменные).

        выд_трассу( [ ]) :-
                nl, write( 'Это был ваш вопрос'), nl.

        выд_трассу( [Цель по Прав | Трасса] ) :-
                nl, write( 'Чтобы проверить по' ),
                write( Прав), write( ', что'),
                write( Цель),
                выд_трассу( Трасса).

        конкретный( Терм) :-
                нумпер( Терм, 0, 0).
               % Нет переменных в Терм'е

% конкретизация( Т1, Т2) означает, что Т2 - конкретизация Т1,
% т.е. терм Т1 - более общий, чем Т2, или той же степени
% общности, что и Т2

        конкретизация( Терм, Терм1) :-
                                        % Терм1 - частный случай Терм 'а
                копия( Терм1, Терм2),
                                        % Копия Терм1 с новыми переменными
                нумпер( Терм2, 0, _),  !,
                Терм = Терм2.
            % Успех, если Терм1 - частный случай Терм2

        копия( Терм, НовТерм) :-
                                        % Копия Терм' а с новыми переменными
                asserta( copy( Терм) ),
                retract( сору( НовТерм) ),  !.

        посл_индекс( 0).                 % Начальный индекс для "сказано"

        след_индекс( Инд) :-         % Следующий индекс для "сказано"
                retract( посл_индекс( ПослИнд) ),  !,
                Инд is ПослИнд + 1,
                assert( посл_индекс( Инд) ).

Рис. 14. 11.  Оболочка экспертной системы: Вопросы к пользователю
и ответы на вопросы "почему".

        нумпер( Терм, N, М) :-
                Терм =.. [Функтор | Аргументы],
            % Структура или атом
                нумарг( Аргументы, N, M).
                                        % Пронумеровать переменные в аргументах
        нумарг( [ ], N, N) :-  !.

        нумарг( [X | Спис], N, M) :-
                нумпер( X, N, N1),
                нумарг( Спис, N1, М).


14. 5. 4.    Процедура     выдать

Процедура

        выдать( Ответ)

приведенная на рис. 14.12, показывает пользователю окончательный результат консультационного сеанса и дает объяснения типа "как". Ответ включает в себя как ответ на вопрос пользователя, так и дерево вывода, демонстрирующее как система пришла к такому заключению. Сначала процедура выдать представляет пользователю свое заключение. Затем, если пользователь пожелает узнать, как это заключение достигнуто, то печатается дерево вывода в некоторой удобной для восприятия форме - это и есть объяснение типа "как". Форма объяснения показана в примере разд. 14.4.3.


14. 5. 5.    Драйвер верхнего уровня

И наконец, для того, чтобы иметь удобный доступ к оболочке из интерпретатора Пролога, нам необходима процедура, выполняющая функцию "драйвера". На рис. 14.13 показано, как могла бы выглядеть предназначенная для этой цели процедура эксперт. Драйвер эксперт производит запуск трех основных модулей оболочки (рис. 14.10 - 14.12) и координирует их работу. Например:

% Выдача заключения консультационного сеанса и
% объяснения типа "как"

        выдать( Ответ) :-
                nl, заключение( Ответ),
                nl, write( 'Хотите узнать, как?'),
                принять( Ответ1),
                ( Ответ1 = да,  !,  отобр( Ответ);
                    true).
                                                 % Показ решающего дерева

        заключение( Ответ1 и Ответ2) :-  !,
                заключение( Ответ1), write( 'и'),
                заключение( Ответ2).

        заключение( Заключение было Найдено) :-
                write( Заключение).

% "отобр" отображает полное решающее дерево

        отобр( Решение) :-
                nl, отобр( Решение, 0),  !.
                           % Отступ 0

        отобр( Ответ1 и Ответ2, Н) :-  !,                      % Отступ Н
                отобр( Ответ1, Н),
                tab( H), write( 'и'), nl,
                отобр( Ответ2, Н).

        отобр( Ответ был Найден, Н) :-                      % Отступ Н
                tab( H), печответ( Ответ),             % Показ заключения
                nl, tab( H),
                write( 'было'),
                отобр1( Найден, Н).
                        % Показ доказательства

        отобр1( Выведено из Ответ, Н) :-  !,
                write( Выведено), write( 'из'),
        % Показ имени правила
                nl, H1 is H + 4,
                отобр( Ответ, H1).
                          % Показ "предшественника"

        отобр1( Найдено, _ ) :-
                                    % Найдено = 'сказано' или 'найдено как факт'
                write( Найдено), nl.

        печответ( Цель это правда) :-  !,
                write( Цель).
            % На выходе 'это правда' опускается

        печответ( Ответ) :-                                % Отрицательный ответ
                write( Ответ).

Рис. 14. 12.  Оболочка экспертной системы:
Отображение окончательного результата и

объяснение типа "как".

        ?-  эксперт.

        Пожалуйста, спрашивайте:    % Приглашение пользователю

        X это животное и голиаф это Х.    % Вопрос пользователя

        Это правда:  голиаф имеет шерсть?

        . . .


14. 5. 6.    Одно замечание по поводу программы-оболочки

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


14. 5. 7.    Цели с отрицанием

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

% Процедура-драйвер верхнего уровня

        эксперт :-
                принять_вопрос( Вопрос),

                                            % Ввести вопрос пользователя
                ( ответ_да( Вопрос);
                                            % Попытка найти положительный ответ
                ответ_нет( Вопрос) ).
            % Если нет положительного ответа, то найти отрицательный

        ответ_да( Вопрос) :-
                                            % Искать положительный ответ на Вопрос
                статус( отрицательный),
                                            % Пока еще нет положительного ответа
                рассмотреть( Вопрос, [ ], Ответ),             % Трасса пуста
                положительный( Ответ),            % Искать положительный ответ
                статус( положительный),
                                                           % Найден положительный ответ
                выдать( Ответ), nl,
                write( 'Нужны еще решения?' ),
                принять( Ответ1),
          % Прочесть ответ пользователя
                Ответ1 = нет.
                                % В противном случае возврат к "рассмотреть"

        ответ_нет( Вопрос):-
                                % Искать отрицательный ответ на Вопрос
                retract( пока_нет_положительного_решения),  !,
                                                           % Не было положительного решения?
                рассмотреть( Вопрос, [ ], Ответ),
                отрицательный( Ответ),
                выдать( Ответ), nl,
                write( 'Нужны еще решения?' ),
                принять( Ответ1),
                Ответ1 = нет.

                                % В противном случае - возврат к "рассмотреть"
        статус( отрицательный) :-
                assert( пока_нет_положительного_решения).

        статус( положительный) :-
                retract( пока_нет_положительного_решения),  !;  true.

        принять_вопрос( Вопрос) :-
                nl, write( 'Пожалуйста, спрашивайте:'), nl,
                read( Вопрос).

Рис. 14. 13.  Оболочка экспертной системы: драйвер. Обращение
к оболочке из Пролога при помощи процедуры эксперт.


        рассмотреть( не Цель, Трасса, Ответ) :-  !,
                рассмотреть( Цель, Трасса, Ответ1),
                обратить( Ответ1, Ответ).

                                % Получить обратное истинностное значение

        обратить( Цель это правда было Найдено,
                          ( не Цель) это ложь было Найдено).

        обратить( Цель это ложь было Найдено,
                          ( не Цель) это правда было Найдено).

Если Цель конкретизирована, то все в порядке, если же нет, то возникают трудности. Рассмотрим, например, такой диалог:

        ?-  эксперт.

        Пожалуйста, спрашивайте:
        не ( X ест мясо).

        Есть (еще) решения для   :  Животное
        да.
        Животное = тигр.

В этот момент система даст ответ:

        не ( тигр ест мясо) это ложь

Такой ответ нас не может удовлетворить. Источник затруднения следует искать в том, какой смысл мы вкладываем в вопросы типа

        не ( X ест мясо)

В действительности мы хотим спросить: "Существует ли такой X, что X не ест мяса?" Однако процедура рассмотреть (так как мы ее определили) проинтерпретирует этот вопрос следующим образом:

    (1)        Существует ли такой X, что X ест мясо?
    (2)        Да, тигр ест мясо.

    Итак,

    (3)        не (тигр ест мясо) это ложь.

Короче говоря, интерпретация такова - "Правда ли, что никакой X не ест мясо?" Положительный ответ мы получим, только если никто не ест мяса. Можно также сказать, что процедура рассмотреть отвечает на вопрос так, как будто X находится под знаком квантора всеобщности:

        для всех X: не (X ест мясо)?

а не квантора существования, в чем и состояло наше намерение:

        для некоторого X: не (X ест мясо)?

Если рассматриваемый вопрос конкретизирован, то проблемы исчезают. В противном случае правильный способ работы с отрицаниями становится более сложным. Например, вот некоторые из возможных правил:

Для того, чтобы рассмотреть (не Цель), рассмотрите Цель, а затем:

Можно избежать всех этих осложнений, если потребовать, чтобы отрицания стояли только перед конкретизированными целями. Если правила базы знаний формулировать должным образом, то часто удается удовлетворить этому условию. Нам это удалось в "правиле поломки" (рис. 14.7):

        правило_поломки:
                            если
                                    вкл( Прибор) и
                                    прибор( Прибор) и
                % Конкретизация
                                    не работает( Прибор) и
                                    соед( Прибор, Предохр) и
                                    доказано( цел( Предохр) )
                            то
                                    доказано( неиспр( Прибор) ).

Здесь условие

        прибор( Прибор)

"защищает" следующее за ним условие

        не работает( Прибор)

от неконкретизированной переменной.

Упражнение

14. 3.    База знаний может, в принципе, содержать циклы. Например:

        прав1:  если бутылка_пуста то джон_пьян.
        прав2:  если джон_пьян то бутылка_пуста.

Работая с подобной базой знаний, наша процедура рассмотреть может зациклиться на обработке одних и тех же целей. Внесите в процедуру рассмотреть изменения, предотвращающие зацикливание. Используйте для этого объект Трасса. Однако соблюдайте осторожность: если текущая цель сопоставима с одной из предыдущих целей, то такую ситуацию следует рассматривать как цикл только в том случае, когда текущая цель имеет большую, степень общности, чем предыдущая.


Назад | Содержание | Вперёд