Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Буч Г. - Объектно-ориентированный анализ и проектирование с примерами приложений на C++ - 2001

.pdf
Скачиваний:
1500
Добавлен:
13.08.2013
Размер:
5.88 Mб
Скачать

Аналогично, подклассы класса WordKnowledgeSource определяются

так:

class wordStructureKnowledgeSource : public WordKnowledgeSource ...

class SmallWordKnowledgeSource : public WordKnowledgeSource

...

class pattemMatchingKnowledgeSource : public WordKnowledgeSource ...

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

Любой элемент

 

?

Не элемент

~

 

Несколько элементов

*

Начало группы

 

{

Конец группы

 

}

Используя такие обозначения, мы можем передать объекту этого класса шаблон ?Е~{А Е I O U}, чтобы он искал в своем словаре слово из трех букв, начинающееся с некоторой буквы, после которой идет Е, а затем - любая буква кроме гласной.

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

class PatternMatchingKnowledgeSource : public WordKnowledgeSource { public:

...

protected:

static BoundedCollection<Word*> words; REPattemMatching pattemMatcher;

};

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

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

Определим теперь подклассы класса stringKnowledgeSource следующим образом:

class CoinmonPrefixKnowledgeSource : public StringKnowledgesource ...

class ConunonSuffixKnowledgeSource : public StringKnowledgesource ...

class DoubleLetterKnowledgeSource : public StringKnowledgesource ...

class LegalStringKnowledgeSource : public StringKnowledgesource ...

Наконец, определим подклассы класса LetterKnowledgeSource:

class DirectSubstitutionKnowledgeSource : public

LetterKnowledgeSource ...

class VowelKnowledgeSource : public

LetterKnowledgeSource ...

class ConsonantKnowledgeSource : public

LetterKnowledgeSource ...

class LetterPrequencyKnowledgeSource : public LetterKnowledgeSource ...

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

Reset

Перезапуск источника знаний.

evaluate

Определение состояния информационной доски.

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

Высказать предположение о подстановке.

Найти противоречие в ранее предложенных подстановках и откатить их.

Высказать утверждение о подстановке.

Сообщить контроллеру о своем желании записать на доску что-то интересное.

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

class InferenceEngine ( public: InferenceEngine<DynamicSet<Rules*>};

...

};

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

evaluate Выполнить правило механизма вывода.

Теперь о том, как сотрудничают источники знаний: каждый специализированный источник определяет свои собственные правила и возлагает ответственность за их выполнение на класс InferenceEngine. Точнее, операция KnowledgeSource::evaluate вызывает метод InferenceEngine::evaluate, что приводит к выполнению одной из четырех упомянутых выше операций. На рис. 11-4 показан сценарий такого взаимодействия:

Рис. 11-4. Взаимодействия с источником знаний

Что такое правило? Для иллюстрации приведем (в формате Lisp) правило, касающееся знаний об общеупотребительных суффиксах:

((* I ? ?) (* I N G) (* I E S)

(* I E D))

Это правило означает, что заданному шаблону *I?? (условие - antecedent) могут соответствовать суффиксы ING, IES и IED (заключение - consequent). В C++ можно определить следующий класс для представления правил:

class Rule{ public:

int bind(String<char>& antecedent, String<char>& consequent);

int remove(Strlng<char>& antecedent);

int remove(String<char>t antecedent, String<char>& conseiruent);

int hasConflict(const String<char>& antecedent) const;

protected:

String<char> antecedent; List<String<char> > consequents;

};

Смысл приведенных операций полностью понятен из их наименований. Мы здесь повторно использовали некоторые классы из главы

9.

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

Выразим все сказанное следующим образом:

class KnowledgeSource : public InferenceEngine, public Dependent (

public:

KnowledgeSource(Blackboard*, Controller*); void reset() ;

void evaluate {) ; protected:

Blackboard* blackboard; Controller* controller;

UnboundedOrderedCollection<Assumption*> pastAssumptions;

};

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

Экземпляры класса Blackboard служат для хранения объектов информационной доски. По схожим соображениям, необходим также класс KnowledgeSources, охватывающий все источники знаний, относящиеся к решаемой задаче:

class KnowledgeSources : public DynamicCollection<KnowledgeSource*>...

Одно из свойств этого класса состоит в том, что при создании его экземпляра создаются также 13 специализированных источников знаний. Для объектов этого класса определяются три операции:

restart

Перезапустить источник знаний.

StartKnowledgeSource

Задать начальные условия для

источника знаний.

 

connect

Связать источник знаний с доской или

контроллером.

 

Рис. 11-5. Диаграмма классов источников знаний

На рис. 11-5 показана структура созданных в процессе проектирования классов источников знаний.

Проектирование контроллера

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

evaluate.

Каким образом контроллер определяет, какой из источников знаний следует активизировать? Можно предложить несколько разумных правил:

Утверждение более приоритетно чем предположение.

Если кто-то говорит, что решил всю фразу, надо дать ему возможность высказаться.

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

Контроллер действует в качестве агента, ответственного за взаимодействие источников знаний.

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

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

После изолированного анализа класса мы предлагаем ввести для класса controller следующие операции:

reset

Перезапуск контроллера.

addHint

Добавить высказывание от источника

знаний.

 

removeHint

Удалить высказывание от источника

знаний.

 

processNextHint

Разрешить выполнение следующего по

приоритету высказывания.

isSolved

Селектор. Истина, если задача решена.

UnableToProceed

Селектор. Истина, если источники знаний

застряли.

 

connect

Устанавливает связь с источником

знаний.

 

Все эти решения можно описать следующим образом: class Controller {

public:

void reset{);

void connect(Knowledgesource&); void addHint{KnowledgeSourceu); void removeHint(KnowledgeSourceu) ; void processNextHint () ;

int isSolved() const;

int unableToProceed() const;

};

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

Рассмотрим диаграмму состояний и переходов на рис. 11 -6. Из нее видно, что контроллер может находиться в одном из пяти основных состояний: инициализация (Initializing), выбор (Selecting), вычисление (Evaluating), тупик (Stuck) и решение (Solved). Наибольший интерес для нас представляет поведение контроллера при переходе от выбора к вычислению. В состоянии selecting контроллер переходит от создания стратегии (CreatingStrategy) к вычислению высказывания (ProcessingHint) и, в конце концов, выбирает источник знаний

(SelectingKS).

Рис. 11-6. Контроллер как конечный автомат

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

Конечной точкой работы нашего механизма является solved (задача решена) или stuck (тупиковая ситуация).

11.3. Эволюция

Интеграция

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

Интеграция объектов верхнего уровня. На рис. 11-7 показана диаграмма объектов нашей системы на самом верхнем уровне, которая полностью соответствует структуре информационной доски, приведенной на рис. 11-1. Физическое содержание объектов доски в коллекции theBlackboard и источников знаний в коллекции theKnowledgeSources

показано в соответствии с описанием вложенности классов.

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

Рис. 11-7.Диаграмма объектов криптоанализа

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

Для этого класса следует определить две основные операции:

reset

Перезапустить информационную доску

decipher

Начать дешифровку криптограммы

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

Метод decipher принимает строку - криптограмму. Теперь функции высокого уровня нашего приложения становятся предельно простыми, как это обычно и происходит в объектно-ориентированных системах:

char* solveProblem (char* ciphertext) { Cryptographer theCryptographer;

return theCryptographer.decipher(ciphertext);

}

Метод decipher оказывается несколько сложнее. В первую очередь с помощью операции assertProblem задание помещается на доску. После этого активизируются источники знаний. И, наконец, начинается циклический процесс обращения источников знаний к контроллеру с новыми и новыми предположениями и утверждениями до тех пор, пока не будет найдено решение задачи либо процесс не зайдет в тупик. Для иллюстрации можно воспользоваться диаграммами взаимодействия или диаграммами объектов, но код на C++ выглядит тоже не слишком сложно: theBlackboard.assertProblem();

cheKnowledgeSources.reset() ; while (StheController.isSolvea || !theController.unableToProceed() )

theController.proceaaNextHint(); it (theBlackboard.isSolved())

return theBlackboard.retrieveSolution() ;

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

Посмотрим на две операции, определенные в классе decipher, а именно assertProblem и retrieveSolution. Первая из них интересна тем, что создает структуру доски. Опишем наш алгоритм следующим образом:

убрать из строки все начальные и концевые пробелы if получилась пустая строка return создать объект-предложение занести предложение на доску создать объект-слово (самое крайнее слева) занести слово на доску добавить слово к предложению for каждый символ строки слева направо if символ есть пробел

сделать текущее слово предыдущим создать объект-слово занести слово на доску

добавить слово к предложению else создать объект "буква шифра" занести букву на доску добавить букву к слову

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

Операция retrieveSolution очень проста: она возвращает строку,записанную в данный момент на доске. Вызывая эту операцию до того как функция isSolved вернула значение True, можно получать частичные решения.

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

На рис. 11-8 показан сценарий выдвижения предположений. Источник знаний сообщает об имеющихся предположениях информационной доске, которая применяет их к алфавиту и оповещает остальные источники.

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

Рис. 11-8. Выдвижение предположений

Можно действовать тоньше. Пусть мы предположили, что однобуквеиное слово соответствует I (нужна гласная). Далее, сделано предположение, что некоторое двухбуквенное сочетание - это NN (нужны согласные). Если первое предположение окажется ошибочным, то второе вполне может быть сохранено. При таком подходе класс Assumption нужно дополнить еще одним методом, регистрирующим связь предположений между собой (взаимозависимость). Реализацию этого поведения можно отложить на более поздний срок, поскольку оно мало влияет на архитектуру.

Добавление источников знаний

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

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

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

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

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

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

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

11.4. Сопровождение

Расширение функциональных возможностей

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

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

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

Посмотрим, какие классы нам понадобятся. Прежде всего, введем класс Action, регистрирующий действия источников знаний и контроллера: class Action ( public:

Action(KnowledgeSource* who, BlackboardObject* what, char* why);

Action<Controller* who, KnowledgeSource* what, char*

why);

};

Экземпляр данного класса создается, например, при активизации контроллером какого-либо источника знаний. При этом в аргумент who (кто) заносится указатель на контроллер, в аргумент what (что) - активный источник знаний, а в аргумент why (почему) - какое-либо пояснение (например, приоритет предположения).

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

методы, которые выдвигают предположения;

методы, которые откатывают предположения;

методы, которые активизируют источники знаний;

методы, которые выполняют правила;

методы, которые регистрируют высказывания от источников знаний. Все эти события сконцентрированы в двух местах: в конечном

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

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

Изменение технических требований

Если принятые проектные решения были реализованы правильно, то новые технические требования к системе могут быть удовлетворены при минимальных изменениях проекта. Допустим, что предъявлены три новые требования к данной системе:

возможность дешифровки с иностранных языков;

возможность дешифровки перестановочного и простого подстановочного шифра, использующего (одну) подстановку и перестановку;

способность к самообучению.

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

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

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

Соседние файлы в предмете Программирование на C++