Проектирование систем искусственного интеллекта

Бинарные деревья

Показывать лекцию целиком

Представление бинарных деревьев

Бинарное дерево определяется рекурсивно как имеющее левое поддерево, корень и правое поддерево. Левое и правое поддеревья сами являются бинарными деревьями. На рис. 8.1 показан пример бинарного дерева.

(рис 8.1) Бинарное дерево.

Такие деревья можно представить термами вида

бд(Лд, К, Пд),

где Лдлевое поддерево, К — корень, а Пд — правое поддерево. Для обозначения пустого бинарного дерева будем использовать атом nil. Бинарное дерево на рис.8.1 имеет левое поддерево бд(бд(nil, d, nil), b, бд(nil, е, nil)) правое поддерево бд(nil,с, nil) и записывается целиком как бд(бд(бд(nil,d, nil), b, бд(nil,е, nil)), а, бд(nil, с, nil)).

Представление множеств с помощью бинарных деревьев

Описание множеств в виде списков позволяет использовать для множеств целевое утверждение принадлежит, определенное ранее для списков.

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

?- принадлежит(3000, b).

Прологу придется проверить все 1024 числа, прежде чем заключить, что такого числа нет:

нет

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

(рис 8.2) Упорядоченное бинарное дерево

Обратите внимание, что упорядочение приводит не к единственному варианту представления множества с помощью дерева. Например, на рис. 8.3 изображено то же множество, что и на рис. 8.2

Будем называть линейным представление такого вида, как на рис. 8.3, и сбалансированным — такое, как на рис. 8.2

(рис 8.3) Линейное представление

Моделирование принадлежности множеству. Имея множество, описанное бинарным деревом, мы можем моделировать принадлежность множеству с помощью целевого утверждения принадлежит_дереву. При этом используется оператор @<, выражающий отношение "меньше, чем", и оператор @>, выражающий отношение "больше, чем".

/* Граничное условие: Х принадлежит 
/* дереву, если Х является корнем.
принадлежит_дереву(Х, бд(Лд, Х, Пд)),
/* Рекурсивные условия 
/* Х принадлежит дереву, если Х больше 
/* значении корня и находится в правом 
/* поддереве:
принадлежит_дереву(Х, бд(Лд, У, Пд)) :- X@Y,
припадлежит_дереву(Х, Пд).
/* Х принадлежит дереву, если Х меньше 
/* значения корня и находится в левом 
/* поддереве:
принадлежит_дереву(Х, бд(Лд ,У ,Пд)) :-X@Y, 
принадлежит_дереву(Х, Лд).

Если множество из первых 1024 чисел описать с помощью сбалансированного бинарного дерева Т, то при ответе на запрос

?- принадлежит_дереву(3000, Т).

Пролог сравнит число 3000 не более чем с 11 элементами множества. прежде чем ответит:

нет

Конечно, если Т имеет линейное представление, то потребуется сравнение 3000 с 1024 элементами множества.

Построение бинарного дерева. Задача создания упорядоченного бинарного дерева при добавлении элемента Х к другому упорядоченному бинарному дереву формулируется следующим образом.

Граничное условие:

Добавление Х к nil дает бд(nil, Х, nil).

Рекурсивные условия:

При добавлении Х к бд(Лд, К, Пд) нужно рассмотреть два случая, чтобы быть уверенным, что результирующее дерево будет упорядоченным.

  • Х меньше, чем К. В этом случае нужно добавить Х к Лд, чтобы получить левое поддерево. Правое поддерево равно Пд, а значение корня результирующего дерева равно К.
  • Х больше, чем К. В таком случае нужно добавить Х к Пд, чтобы получить правое поддерево. Левое поддерево равно Лд, а значение корня — К.
  • Такой формулировке задачи соответствует программа:

    /* Граничное условие:
    включ_бд(nil, Х, бд(nil, Х, nil)). 
    /* Рекурсивные условия:
    /*(1) 
    включ_бд(бд(Лд, К, Пд), Х, бд(Лднов, К, Пд)) :-
    Х@К,
    включ_бд(Лд,Х,Лднов). 
    /*(2) 
    включ_бд(бд(Лд, К, Пд), Х, бд(Лд, К, Пднов)) :-
    Х@К,
    включ_бд(Пд, Х, Пднов).
    На запрос
    ?- включ_бд(nil, d, Т1), включ_бд(Т1, а, Т2). 
    будут получены значения
    Т1=бд(nil, d, nil)
    Т2=бд(бд(nil, а, nil), d, nil)

    Процедуру включ_бд() можно использовать для построения упорядоченного дерева из списка:

    /* Граничное условие:
    список_в_дерево([], nil). 
    /* Рекурсивное условие:
    список_в_дерево([Н | Т], Бд) :-
    список_в_дерево(Т, Бд2),
    включ_бд(Н, Бд2, Бд).

    Заметим, что включ_бд не обеспечивает построения сбалансированного дерева. Однако существуют алгоритмы, гарантирующие такое построение.

    Механизм возврата и процедурная семантика

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

    Механизм возврата

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

    В качестве примера рассмотрим утверждения:

    меньше(X.Y) :- 
    X<Y, write(X), 
    write ('меньше, чем'),write(Y).
    меньше(Х.У) :- 
    Y<X, write(Y), 
    write ('меньше, чем'),write(X).

    Целевое утверждение

    ?- меньше (5, 2).

    сопоставляется с головой первого утверждения при Х=5 и У=2. Однако не удается согласовать первый член конъюнкции в теле утверждения X<Y. Значит, Пролог не может использовать первое утверждение для согласования целевого утверждения меньше(5, 2). Тогда Пролог переходит к следующему утверждению, голова которого сопоставима с целевым утверждением. В нашем случае это второе утверждение. При значениях переменных Х=5 и Y=2 тело утверждения согласуется. Целевое утверждение меньше(5,2) доказано, и Пролог выдает сообщение "2 меньше, чем 5". Запрос

    ?-меньше (2, 2).

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

    Такой процесс согласования целевого утверждения путем прямого продвижения по программе мы называем прямой трассировкой (forward tracking). Даже если целевое утверждение согласовано, с помощью прямой трассировки мы можем попытаться получить другие варианты его доказательства, т.е. вновь согласовать целевое утверждение.

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

    Пример: задача поиска пути в лабиринте

    В качестве примера использования механизма возврата напишем процедуру для поиска пути в лабиринте. Лабиринт представлен фактами вида:

    стена(I, J)               	для позиции в I-м ряду и J-й колонке, 
      где есть стена 
    отсутств_стена(I, J)      	для позиции в I-м ряду и J-й колонке, 
      где нет стены
    выход (I, J)              	для позиции в 1-м ряду и J-й колонке, 
      являющейся выходом

    Рассмотрим небольшой лабиринт:

    Последний ряд лабиринта описывается фактами:

    стена(4,1). 
    стена(4,3). 
    стена(4,4). 
    отсутств_стена(4,2).

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

    Граничное условие:

    Если исходная позиция является выходом, то путь найден.

    Рекурсивные условия:

    Ищем путь из исходной позиции в северном направлении. Если пути нет, идем на юг. Если пути нет, идем на запад. Если нельзя, идем на восток. Если соседняя позиция на севере (юге, западе, востоке) является стеной, то нет смысла искать путь из начальной позиции к выходу. Чтобы не ходить кругами, будем вести список позиций, в которых мы побывали.

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

    /* Терм a(I, J) представляет позицию в 
    /* I-м ряду и J-й колонке. 
    /* Нашли путь ? 
    путь(а(I, J),[а(I, J)], Были) :- выход(I, J).
    /* Пытаемся идти на север 
    путь(а(I, J),[а(I, J) | Р], Были) :-
    К is I-1,
    можем_идти(a (K, J), Были),
    путь(а(I, J) ,Р, [a(K, J) | Были]).
    /* Пытаемся идти на юг 
    путь(а(I, J),[а(I, J) | Р], Были) :-
    К is I+1,
    можем_идти(a (K, J), Были),
    путь(а(I, J) ,Р, [a(K, J) | Были]).
     /* Пытаемся идти на запад 
    путь(а (I, J), [a (I, J) | P], Были) :-
    L is J-1,
    можем_идти(а(I, L), Были),
    путь(а(I, L), Р, [а(I, L)| Были]).
    /* Пытаемся идти на восток 
    путь(а (I, J), [a (I, J) | P], Были) :-
    L is J+1,
    можем_идти(а(I, L), Были),
    путь(а(I, L), Р, [а(I, L)| Были]).
    /* в позицию a(I, J) можно попасть при 
    /* условии, что там нет стены и мы 
    /* не побывали в ней прежде 
    можем_идти(а(I, J)), Были) :-
    отсутств_стена(I, J),
    not (принадлежит (a (I, J), Были)).

    Чтобы понять, каким образом процедура ищет путь к выходу, рассмотрим процесс согласования запроса с описанием лабиринта, описанного выше:

    ?-путь(а(4,2), Р, [а(4.2)]).

    Выходом из лабиринта является позиция выход (3,1).

    Выбор первого утверждения не приводит к согласованию целевого утверждения, поскольку а (4,2) — не выход. Во втором утверждении делается попытка найти путь в северном направлении, т.е. согласовать целевое утверждение

    путь(а(3, 2), Р2, [а(3, 2), а(4, 2)]).

    Целевое утверждение не удается согласовать с первым утверждением

    путь(а(3, 2), Р2, [а(3, 2), а(4, 2)])

    так как а (3,2) не является выходом. Во втором утверждении предпринимается попытка найти путь, двигаясь на север, т.е. согласовать целевое утверждение

    путь(а(2,2), РЗ, [а(2, 2), а(3, 2), а(4, 2)]).

    Ни одно из утверждений не может согласовать

    путь(а(2, 2), РЗ, [а(2, 2), а(3, 2), а(4, 2)]).

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

    Неудача в согласовании

    путь(а(2, 2), РЗ, [а(2, 2), а(3, 2), а(4, 2)])

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

    путь(а(3, 2), Р2, [а(3, 2), а(4, 2)]).

    Решение пересматривается и выбирается третье утверждение.

    В третьем утверждении осуществляется попытка найти путь, двигаясь на юг, но она оказывается неудачной, поскольку мы уже побывали в позиции а (4, 2). Тогда, чтобы согласовать

    путь(а(3, 2), Р2, [а(3, 2), а(4, 2)]),

    выбирается четвертое утверждение. Мы успешно находим путь, двигаясь в западном направлении к позиции а(3,1), которая и является выходом. Рекурсия сворачивается, и в результате получается путь

    Р=[а(4, 2),а(3, 2), а(3,1)] 
    другие решения(да/нет)? да
    Других решений нет
    Альтернативный путь
    [a(4,2), a(3,2), a(2,2), a(3,2), a(3,1)]

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

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

    Иерархия типов

    Иерархия типов и подтипов является стандартной характеристикой семантических сетей. Иерархия может включать сущности: ТАКСА < СОБАКА < ПЛОТОЯДНОЕ < ЖИВОТНОЕ < ЖИВОЕ СУЩЕСТВО < ФИЗИЧЕСКИЙ ОБЪЕКТ < СУЩНОСТЬ. Они также могут включать в себя события: ЖЕРТВОВАТЬ < ДАВАТЬ < ДЕЙСТВИЕ < СОБЫТИЕ или состояния: ЭКСТАЗ < СЧАСТЬЕ < ЭМОЦИОНАЛЬНОЕ СОCТОЯНИЕ < СОСТОЯНИЕ. Иерархия Аристотеля включала в себя 10 основных категорий: субстанция, количество, качество, отношение, место, время, состояние, активность и пассивность. Некоторые учение дополнили его своими категориями.

    Символ < между более общим и более частным символом читается как: "Х-тип/подтип У".

    Термин " иерархия " обычно обозначает частичное упорядочение, где одни типы являются более общими, чем другие. Упорядочение является частичным, потому, что многие типы просто не подлежат сравнению между собой. Сравним HOUSE<DOG и DOG<HOUSE бессмысленны, если их сравнивать, однако слово DOGHOUSE является подтипом HOUSE, но не DOG. Рассмотрим некоторые виды графов:

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

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

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

    Наследование.

    Основным свойством иерархии является возможность наследования подтипами качеств гипертипов: все характеристики, которые присущи ЖИВОТНОМУ, также присущи МЛЕКОПИТАЮЩЕМУСЯ, РЫБЕ и ПТИЦЕ. В основе теории наследования лежит теория силлогизмов Аристотеля: Если А — характеристика В, а В — х-ка С, то А — хар-ка всех С.

    Преимущества иерархии и наследования:

  • Иерархия типов является отличной структурой для индексирования базы знаний и ее эффективной организации.
  • Следование по какой-либо ветви с помощью иерархии осуществляется гораздо быстрее.
  • Синтаксический анализ языка и его порождение.

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

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

    Синтаксический анализатор с использованием семантики. Синтаксический анализатор с использованием семантики оперирует также как и парсер, в основе которого лежит синтаксис. Однако он оперирует не с синтаксическими категориями типа группа подлежащего и группа сказуемого, а с концептами высокого уровня типа КОРАБЛЬ и ПЕРЕВОЗИТЬ.

    Концептуальный парсинг. Семантическая сеть предсказывает возможные ограничения, которые могут встретится в отношениях между словами, а также прогнозировать слова, которые позже могут встретиться в предложении. Например, глагол давать требует одушевленного агента и а также прогнозирует возможность реципиента и объекта, который будет дан. Шенк был одним из самых активных сторонников концептуального парсинга.

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

    Аргументы за и против различных техник парсинга часто основывался не на конкретные данные, а больше на уже устоявшемся мнении. И лишь один проект на практике сравнил несколько видов парсинга — это Язык Семантических Репрезентаций, проект разработанный в Университете Берлина. В течение нескольких лет они создали четыре разных вида парсеров для анализа немецкого языка и его записи на Язык Семантических Репрезентаций, который представляет собой сеть.

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

    Второй парсер был семантически ориентированные расширенные сети перехода. В нем было легче обобщить синтаксис, однако аппарат синтаксиса работал медленнее, чем у первого рассмотренного парсера.

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

    Парсер, который был создан относительно недавно, — это синтаксически ориентированный парсер, основанный на общей грамматике фразовой структуры. Он наиболее систематичен и обобщен и относительно быстр.

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

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

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

    Элементы нечеткой логики

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

    $$F \in \{0…1\}$$

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

    a I b = min{a,b}

    a Y b = max{a,b}

    $$\bar a = 1-a$$

    Как можно легко заметить, при использовании только классических состояний (ложь-0, истина-1) мы приходим к классическим законам логики.

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

    Вернуться к учебному плану