Программирование на языке Пролог для искусственного интеллекта



              

Эффективность



    Эффективность

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

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

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

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

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

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

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

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

    Повышение эффективности решения задачи о восьми ферзях
1.    Повышение эффективности решения задачи о восьми ферзях

В качестве простого примера повышения эффективности давайте вернемся к задаче о восьми ферзях (см. рис. 4.7). В этой программе Y-координаты ферзей перебираются последовательно - для каждого ферзя пробуются числа от 1 до 8. Этот процесс был запрограммирован в виде цели

        принадлежит( Y, [1, 2, 3, 4, 5, 6, 7, 8] )

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

        принадлежит( Y, [1, 5, 2, 6, 3, 7, 4, 8] )

Это маленькое изменение уменьшит время, необходимое для нахождения первого решения, в 3-4 раза.

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

    Повышение эффективности программы раскраски карты
2.    Повышение эффективности программы раскраски
карты

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

Пусть карта задана отношением соседства

        соседи( Страна, Соседи)

где Соседи - список стран, граничащих со страной Страна. При помощи этого отношения карта Европы с 20-ю странами будет представлена (в алфавитном порядке) так:

        соседи( австрия, [венгрия, запгермания, италия,
                                        лихтенштейн, чехословакия,
                                        швейцария, югославия]),

        соседи( албания, [греция, югославия]).

        соседи( андорра, [испания, франция]).

        . . .

Решение представим в виде списка пар вида

        Страна / Цвет

которые устанавливают цвет для каждой страны на данной карте. Для каждой карты названия стран всегда известны заранее, так что задача состоит в нахождении цветов. Таким образом, для Европы задача сводится к отысканию подходящей конкретизации переменных C1, C2, СЗ и т.д. в списке

        [австрия/C1, албания/С2, андорра/С3, . . .]

Теперь определим предикат

        цвета( СписЦветСтран)

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

        цвета( [ ]).

        цвета( [Страна/Цвет | Остальные] ) :-
        цвета( Остальные),
            принадлежит( Цвет, [желтый, синий, красный, зеленый]),
            not( принадлежит( Страна1/Цвет, Остальные),
                                                        сосед( Страна, Страна1) ).

        сосед( Страна, Страна1) :-
                соседи( Страна, Соседи),
                принадлежит( Страна1, Соседи).

Здесь принадлежит( X, L) - как всегда, отношение принадлежности к списку. Для простых карт с небольшим числом стран такая программа будет работать. Что же касается Европы, то здесь результат проблематичен. Если считать, что мы располагаем встроенным предикатом setof, то можно попытаться раскрасить карту Европы следующим образом. Определим сначала вспомогательное отношение:

        страна( С) :- соседи( С, _ ).

Тогда вопрос для раскраски карты Европы можно сформулировать так:

        ?-  sеtоf( Стр/Цвет, страна( Стр), СписЦветСтран),
             цвета( СписЦветСтран).

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

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

Ясно поэтому, что эффективность зависит от порядка раскраски стран. Интуиция подсказывает простую стратегию раскраски, которая должна быть лучше, чем случайная: начать со страны, имеющей иного соседей, затем перейти к ее соседям, затем - к соседям соседей и т.д. В случае Европы хорошим кандидатом для начальной страны является Западная Германия (как имеющая наибольшее количество соседей - 9). Понятно, что при построении шаблона списка элементов вида страна/цвет Западную Германию следует поместить в конец этого списка, а остальные страны - добавлять со стороны его начала. Таким образом, алгоритм раскраски, который начинает работу с конца списка, в начале займется Западной Германией и продолжит работу, переходя от соседа к соседу.

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

Можно было бы построить такой правильно упорядоченный список стран вручную, но в этом нет необходимости. Эту работу выполнит процедура создспис. Она начинает построение с некоторой указанной страны (в нашем случае - с Западной Германии) и собирает затем остальные страны в список под названием Закрытый. Каждая страна сначала попадает в другой список, названный Открытый, а потом переносится в Закрытый. Всякий раз, когда страна переносится из Открытый в Закрытый, ее соседи добавляются в Открытый.

        создспис( Спис) :-
                собрать( [запгермания], [ ], Спис ).
                собрать( [ ], Закрытый, Закрытый).

                                    % Кандидатов в Закрытый больше нет

        собрать( [X | Открытый], Закрытый, Спис) :-
                    принадлежит( Х | Закрытый),  !,

                                        % Х уже собран ?

        собрaть( Открытый, Закрытый, Спис).
                            % Отказаться от Х

        собрать( [X | Открытый], Закрытый, Спис) :-
                соседи( X, Соседи),

                                % Найти соседей Х
                конк( Соседи, Открытый, Открытый1),
                                % Поместить их в Открытый
                собрать( Открытый1, [X | Закрытый], Спис).
                                % Собрать остальные

Отношение конк - как всегда - отношение конкатенации списков.

    Повышение эффективности конкатенации списков за счет совершенствования структуры данных
3.    Повышение эффективности конкатенации списков за счет совершенствования структуры данных

До сих пор в наших программах конкатенация была определена так:

        конк( [ ], L, L).

        конк( [X | L1], L2, [X | L3] ) :-
                конк( L1, L2, L3 ).

Эта процедура неэффективна, если первый список - длинный. Следующий пример объясняет, почему это так:

        ?-  конк( [а, b, с], [d, e], L).

Этот вопрос порождает следующую последовательность целей:

        конк( [а, b, с], [d, e], L)

            конк( [b, с], [d, e], L')                 где L = [a | L']

                конк( [с], [d, e], L")                    где L' = [b | L"]

                    конк( [ ], [d, e], L'")                   где L" = [c | L''']

                        true             (истина)                   где L'" = [d, е]

Ясно, что программа фактически сканирует весь первый список, пока не обнаружит его конец.

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

        [а, b, с]

можно представить следующими двумя списками:

        L1 = [a, b, c, d, e]
        L2 = [d, e]

Подобная пара списков, записанная для краткости как L1-L2, представляет собой "разность" между L1 и L2. Это представление работает только при том условии, что L2 - "конечный участок" списка L1. Заметим, что один и тот же список может быть представлен несколькими "разностными парами". Поэтому список [а, b, с] можно представить как

        [а, b, с]-[ ]
или
        [a, b, c, d, e]-[d, e]
или
        [a, b, c, d, e | T]-[d, e | T]
или
        [а, b, с | Т]-Т

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

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

        конкат( A1-Z1, Z1-Z2, A1-Z2).

Давайте используем конкат для конкатенации двух списков: списка [а, b, с], представленного парой [а, b, с | Т1]-Т1, и списка [d, e], представленного парой [d, e |  Т2]-Т2 :

        ?-  конкат( [а, b, с | Т1]-T1, [d, е | Т2]-Т2, L ).

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

        T1 = [d, e | Т2]
        L = [a, b, c, d, e | T2]-T2









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