JPS 1. Wykład 1
Transkrypt
JPS 1. Wykład 1
JPS
1. Wykład 1:
Formalności:
2 Kolokwia:
- 1 (50pkt) - dwie części: podstawy LISP(20pkt) i PROLOG(30pkt). Dwa kolejne dni
poniedziałek i wtorek. Może być to odstęp tygodniowy, ale sprawdzian jest traktowany
jako całość. Planowane na 21.04.
- 2 (50pkt) - około końca semestru, przedostatni tydzień (dyskusyjne). Tylko PROLOG,
będzie się wiązał w dużej mierze z projektem.
Projekt: zaliczenie
- nie jest punktowany. Ocena w grupach 2-3 osoby, trzeba przekonać prowadzącego, że
rozumiemy projekty. Pozytywny wynik rozmowy jest podstawą do drugiego sprawdzianu,
który oceni wiedzę punktowo.
- wszyscy studenci pracują nad dwoma takimi samymi zadaniami
- zadania dotyczą PROLOGu:
> Zadanie 1: Maszyna wnioskująca dla systemu eksperckiego o funkcjonalności
zbliżonej do realistycznej (prowadzący mówi: 30-40 wierszy kodu). Startujemy od zapisu
w pseudokodzie (struktura + komentarze: co, gdzie, jak). Trzeba uważać na pewne
"subtelności" w PROLOGU - analiza działania kodu, dokładne poznanie mechanizmów
języka.
> Zadanie 2: Przeanalizowanie gotowego programu, wprowadzenie do niego
odpowiednich modyfikacji. Będzie to planowanie akcji zapisane według bardziej
wyrafinowanego algorytmu, niż proste przeszukiwanie drzewa. Należy dokładnie
zrozumieć, w jaki program działa, potem wprowadzić modyfikacje i różne optymalizacje.
Sam planer (główna procedura) zajmuje około 10 wierszy. Modyfikacje po 3 wiersze, ale
trzeba wstawić w odpowiednie miejsca i wiedzieć, co się dzieje
- będą organizowane spotkania na wykładzie, które będą pewne aspekty projektów
dosyć dokładnie objaśniać (chodzić na wykłady...)
- projekt może być dyskutowany w dowolnych grupach, rozmowy 2-3 osoby.
Trzeba uzyskać pozytywną ocenę z: Kol1.LISP, Kol1.PROLOG, rozmowa oceniająca, Kol2.
Części pierwszego kolokwium osobno oceniane (ciekawe... :/).
Literatura:
- Bratko - Prolog Programming for Artificial Intelligence ed.3, Pearson Education 2001
Wszystko objaśnione na przykładach, bardzo dobra do nauki.
www.booksites.net/bratko - przykłady
- Clocksin, Mellish - Programming in PROLOG ed.5, Springer-Verlag 2003
Dużo objaśnień, mało przykładów. Wartość praktyczna i dydaktyczna słaba.
- Winston, Horn - LISP, Addison-Wesley 1984
Pozycja nadal na czasie... Po prostu w LISP niewiele się zmieniło
- Graham - ANSI Common LISP, Prentice Hall 1995
Charakter bardziej praktyczny, implementacyjny
- Luger - Artificial Intelligence ed.5, Pearson Education 2005
Podejście implementacyjne, LISP i PROLOG, podstawy sztucznej inteligencji.
[Przeszukać torrenty!]
Języki przetwarzania danych symbolicznych:
Języki przystosowane do przetwarzania danych symbolicznych. Korzysta z nich sztuczna
inteligencja. Dane symboliczne to struktury zbudowane e składników elementarnych
zwanych symbolami. Symbol określa znaczenie przypisane elementowi danych w trakcie
przetwarzania, odniesione do modelu świata.
Za pewną formę przetwarzania symbolicznego można uznać języki zapytania do baz
danych. M.in. PROLOG może zastąpić w pewnym stopniu SQL. Jednak głównym polem
jest sztuczna inteligencja.
Podstawowe obiekty i związki modelowanego świata są reprezentowane przez symbole
(semantyka). Składniowo, symbole są ciągami znaków. W trakcie przetwarzania są
traktowane jako niepodzielne elementy danych. Ciągi znaków pełnią rolę
identyfikatorów obiektów przetwarzanych jako reprezentujące pewne obiekty świata
rzeczywistego. Decydują o tożsamości.
Symbole to elementy danych używane do reprezentowania obiektów i związków występujących w dziedzinie rozpatrywanego problemu.
W kodzie źródłowym symbol ma postać ciągu znaków jednak w przetwarzaniu jest traktowany jako niepodzielna jednostka.
Przykładowa struktura danych symbolicznych:
Przedstawiona struktura reprezentuje regułę bazy wiedzy systemu eksperckiego
Definicja (zapisana w pseudokodzie)
STRUKTURA reguła
{
poprzednik: <lista list symboli>
następnik: <lista symboli>
}
Przykładowy egzemplarz struktury typu reguła
{
[
}
[zwierzę należy do gromady ssaki]
[zwierzę ma kopyta]
] // poprzednik
[ zwierzę należy do grupy kopytne] // następnik
Symbole to w sumie każdy wyraz z listy. Przetwarzanie polega na: dekomponowaniu,
przetwarzaniu, porównywaniu, porządkowaniu. Natomiast nigdy nie polega na
operacjach typu zliczanie znaków identyfikatora symboli itp.
Powszechnie znanymi językami używanymi w dziedzinie sztucznej inteligencji są LISP i
PROLOG.
LISP:
Język proceduralny (imperatywny). Zapis problemu i sposobu rozwiązania ma postać
algorytmu (sekwencja operacji do wykonania).
Język funkcyjny - każdy identyfikator języka jest funkcją. Przetwarzanie w języku
funkcyjnym przebiega na zasadzie : f(g(h(),h()), h()) itp. Przekazywanie danych odbywa
się przez wartość funkcji. W LISP nie istnieją parametry wynikowe. Istnieją zmienne
globalne (z reguły używamy tylko wtedy, gdy jest to niezbędne).
PROLOG:
Język deklaratywny. Podajemy warunki, jakie ma spełniać wynik. Mniejszy nacisk
kładziony na sekwencję operacji. Opisujemy problem w kategoriach specyfikacji
związków zachodzących pomiędzy obiektami z dziedziny problemu. Problem jest
zapisywany za pomocą logiki formalnej, z użyciem trochę innej specyfikacji:
ZACHODZI f(X,Y)
WTEDY GDY
ZACHODZI g(X,Z) I ZACHODZI h(Z,Y)
Pomimo charakteru deklaratywnego, daje się przetłumaczyć sprawnie na sekwencję
operacji (wykorzystywane w interpreterach). Ma aspekt proceduralny. Pisząc kod należy
mieć na uwadze to, jak on zostanie wykonany.
Zajęcia będą kładły nacisk na język PROLOG (gdyż jest pojęciowo nowy :) ), zwłaszcza
pod kątem formułowania problemów i zastosowań w sztucznej inteligencji.
2. Wyklad 2:
Przykład przetwarzania danych symbolicznych:
W zastosowaniach AI zmienia się model świata. Metody i algorytmy poszukiwania
rozwiązań oraz problemy z nimi związane pozostają te same. Także metody
konstruowania modeli nie zmieniają się. Dlatego będziemy pracowali na prostych
przykładach.
Układanie planu sekwencji akcji:
W tzw. świecie klocków. Przyjmijmy, że w naszym świecie są cztery pola, na których
możemy rozmieszczać klocki. Mamy dany pewien stan początkowy i docelowy. Klocki
mogą być ustawione jeden na drugim. W naszym świecie operujemy pojęciami, cechami
stanów:
- co na czym stoi
- pole / klocek wolny (nic na nim nie stoi) lub nie.
Przykładowo:
klocek2
klocek1
klocek5
---------|---------|---------|---------|
pole1 pole2 pole3 pole4
Tworzymy struktury danych:
Struktura danych definiująca związki przestrzenne:
class ZW_P
- typ związku (tutaj tylko LEZY_NA)
- element_nad
- element_pod
Przykładowy związek:
ZW_P
LEZY_NA
KLOCEK5
KLOCEK2
Struktura danych reprezentująca status:
class STATUS
- element
- status
Przykładowy status:
STATUS
KLOCEK1
WOLNY ; nic na nim nie leży
Struktura danych reprezentująca stan świata:
class STAN
- lista związków przestrzennych
- lista statusów
Przykładowy stan:
STAN
[<LEZY_NA KLOCEK1 POLE4>, ...]
[<KLOCEK1, WOLNY>, ...]
Struktura danych reprezentująca cel:
class CEL
- lista związków przestrzennych
- lista statusów
! Niektóre cechy statusów i związków mogą być dla celu nieistotne. Nie wszystkie
związki i stany nas interesują przy rozwiązywaniu danego problemu.
Przykładowy cel:
CEL
[<LEZY_NA KLOCEK1 KLOCEK2>]
[<POLE1 WOLNE>]
Struktura danych reprezentująca akcję:
class AKCJA
- rodzaj akcji
- podmiot
- miejsce źródłowe
- miejsce docelowe
Przykładowa akcja:
AKCJA
PRZENIEŚ
KLOCEK1
POLE1
POLE3
Algorytm planowania:
Jest to pewien algorytm przeszukiwania przestrzeni stanów. Reprezentujemy go przez
drzewo. Węzłami są stany, gałęzie to akcje prowadzące do kolejnych stanów.
s0-------------------------------------|
|
|a01
|a02
|
|
s11----s12
|
|
|a11
|a12
|
|
s21 s22
........................
|
|
s13
|a03
Korzystamy ze struktury danych - trójki opisu stanu: <stan aktualny, stan poprzedni,
akcja przenosząca>
Utrzymujemy listę stanów poprzednich (w postaci listy opisów stanów) oraz kolejkę
stanów aktualnie badanych (także lista opisów stanów).
Algorytm:
- zdejmujemy stan z kolejki
- tworzymy listę akcji, które można podejmować w tym stanie
- wyznacz dla każdej akcji nowy stan
- jeżeli stan odpowiada celowi, to zwróć plan akcji
- jeżeli stan różny od celu, twórz nowy opis stanu i dołącz go do kolejki stanów
Przykładowy zapis - konstruowanie akcji:
[<LEZY_NA KLOCEK2 KLOCEK 5>, <LEZY_NA KLOCEK1 POLE4>, ...]
[<POLE2 WOLNE>, <KLOCEK1 WOLNE>, ...]
- wyodrębnienie listy opisów statusu
- przeszukanie listy opisów statusu w celu znalezienia klocka o opisie WOLNE (pomysł:
dodatkowe pole typu KL / PL w opisie)
- znalezienie klocka / pola, na którym leży
- znalezienie na tej samej zasadzie wolnego pola
- skonstruowanie akcji ze związanych elementów
Dygresja: ze względu na charakter symboli jako niepodzielnych jednostek,
porównywanie symboli odbywa się technicznie przez porównywanie wskaźników.
Jakie kryteria musi spełniać język do przetwarzania danych symbolicznych /
AI:
- Potrzebujemy języka w którym prosto możemy manipulować danymi.
- Wymagana jest także odpowiednia optymalizacja wszystkich operacji, aby software
szybko działał.
- Wreszcie, czasami potrzeba odpowiedniego sprzętu (istnieją wyspecjalizowane
procesory LISPowe, przetwarzające w odpowiednio szybki sposób operacje,
umożliwiające prostą manipulację z punktu widzenia sprzętu).
- Wbudowane przeszukiwanie przestrzeni możliwych rozwiązań (w AI trzeba
przeszukiwać masywne przestrzenie - PROLOG ma to zaimplementowane, LISP nie)
Przykład
3. Wykład 3:
Sprawy formalne:
Zajęcia odbywają się w każdy poniedziałek i co drugi wtorek. Najbliższy harmonogram:
wt. 11.03
pon. 17.03
pon. 31.03
wt. 01.04
Czego oczekujemy od języka przetwarzania symbolicznego – revisited:
- na poziomie kodu źródłowego: odpowiednio wysoki poziom abstrakcji bez bawienia się
w szczegóły manipulacji
- na poziomie manipulacji odpowiednia efektywność czasowa działania na strukturach,
ze szczególnym uwzględnieniem implementacji w postaci zespołu wskaźników
poszczególnych symboli
- translator powinien mieć wbudowane rozwiązanie przeszukiwania rozwiązań
(odpowiednie pętle, techniki za pomocą odpowiednich instrukcji)
Przykład w języku LISP:
Definicja funkcji wydobywającej z listy listę dwuelementową złożoną z pierwszego i
ostatniego elementu:
(defun skrajne-elementy (lista) (cons (first lista) (last lista) ) )
; Można sobie przeformatować dla czytelności
(
defun skrajne-elementy(lista)
(
cons (first lista) (last lista)
)
)
Omówienie:
LISP składa się na podstawowym poziomie z atomów: liczby i symbole. Instrukcja defun
definiuje funkcję. Pierwszy element jest nazwą, drugi to lista argumentów, trzeci listą
instrukcji. Wszystkie te elementy są listami! W języku LISP nie istnieje coś takiego jak
instrukcja. Wszystko jest wywołaniem procedury. Nie ma zwykłych instrukcji postaci np.
instrukcji charakterystycznych dla języka C.
Wszelkie przekazywanie danych pomiędzy procedurami odbywa się przez wartości
zwracane przez funkcje. Stają się one argumantami dla innych funkcji. W ten sposób
możemy np. zrealizować instrukcję przypisania bez przypisywania pod zmienną.
Istnieje odpowiednia procedura (funkcja), nawet cała rodzina, która jest implementacją
operacji przypisania. Używamy jednak tylko wtedy, gdy to jest wygodne bądź
uzasadnione:
(setf x y) - przypisz liście
Wartość zwracana jest wartością przypisywaną.
Wywołanie procedury jest listą. Pierwszy element to nazwa procedury wywoływanej,
następne są interpretowane jako argumenty. Tak jak w przykładzie, first i last są po
prostu wywołaniami odpowiednich procedur na argumencie lista. Obydwie zwracają
listę.
cons jest procedurą wbudowaną tworzącą listę przyrostowo, z głowy i ogona. Pierwszy
element dołączany na początek, drugi argument jest listą - ogonem. Istnieje coś takiego,
jak lista pusta: () lub nil.
Wartością wywołania funkcji defun jest lista będąca definicją funkcji. Lista napotkana w
programie to tzw. forma, my będziemy używali określenia "wyrażenie listowe".
Dygresja programistyczna ponadjęzykowa :)
Lista jest to para <obiekt> <lista>.
class LISTA
- obiekt
- lista
Wywołanie funkcji skrajne-elementy: (skrajne-elementy lista1).
Zmienne a symbole w LISP:
Zmienne w LISP nie są deklarowane. W takim razie jak rozróżnić (na poziomie
translatora / interpretera) procedurę od zmiennej? Translator tego nie odróżnia.
Każdemu symbolowi można przypisać pewną wartość. Symbol, który w momencie
wykonania ma przypisaną wartość jest z definicji zmienną.
Przykładowo wywołanie: (f1 x y) , gdy nie jest określona wartość x, jest błędne. Każdy
element wywołania z wyjątkiem pierwszego jest poddawany ewaluacji. Interpreter
usiłuje wyznaczyć wartość tego elementu. Jeżeli trafia na wyrażenie listowe, to zachodzi
to rekurencyjnie. Jeżeli jest to symbol, to interpreter usiłuje wyznaczyć wartość. Brak
wyznaczonej wartości jest równoznaczny błędowi.
Wartości natychmiastowe uzyskiwane są przy pomocy wyspecjalizowanej funkcji quote.
Przykładowe wywołanie: (f1 (quote x) y). Skrótowa notacja: (f1 'x y). Czyli w sumie
chodzi o cytowanie wartości natychmiastowej bez dalszego poddawania ewaluacji.
Kontrola typów:
W LISP funkcjonuje RTTI. Np. jeżeli podamy zamiast listy atom, zwracany jest błąd, gdy
oczekiwano atomu. W przykładzie błąd zgłoszony zostanie dopiero na poziomie
podstawowych procedur, gdyż nie akceptują pojedynczych atomów.
Przykład w LISP nr 2:
Zwrócenie listy bez ostatniego elementu. Jest to implementacja własna procedury
wbudowanej but-last:
(
defun my-butlast (lista)
(
if (endp (rest lista))
()
(
const (first lista)
(my-butlast (rest lista)
)
)
)
Omówienie:
endp zwraca prawdę, gdy lista podana jest pusta.
LISP:
Powstał w latach 50' na MIT. Autor: John McCarthy. Powstał w wyniku zamierzenia, żeby
język programowania był uporządkowany (!). Chodziło o pewien rygorystyczny język, w
którym nie byłoby zbyt dużo dowolności. Powstał język programowania oparty na teorii
matematycznej funkcji rekurencyjnych Church'a. Czynnikiem rozwojowym dla LISP były
początki badań nad sztuczną inteligencją. LISP był pod ręką i doskonalono go pod kątem
sztucznej inteligencji. Wszystkie struktury zdecydowano się implementować w oparciu o
listy (LISt Processing). Zaczęły powstawać także procesory LISPowe, dostosowane do
przetwarzania struktur listowych w pamięci. Wszystko razem komponowało się w
sprawnie działającą całość. W efekcie, LISP jeszcze przed narodzinami PROLOGu stał się
językiem dominującym w dziedzinie AI.
Na poziomie podstawowym języka nie ma procedur sprawnego przeszukiwania
przestrzeni rozwiązań. Wypracowano natomiast odpowiednie biblioteki zawierające
procedury przeszukiwania wprzód, w tył i mieszane. Zastosowanie bibliotek umożliwia
sprawne kodowanie zagadnień sztucznej inteligencji. Dla porównania, mechanizmy tego
typu są wbudowane w PROLOGu na poziomie podstawowym.
Przykład w PROLOGu:
Przykładowy program to prezentowany w zeszłym tygodniu program planowania akcji w
świecie klocków.
plan (State, Goals, [], State) :- goals_achieved(Goals, State)
plan(InitState, Goals, Plan, FinalState) :- ...
......
PROLOG w porównaniu z LISP jest językiem deklaratywnym. Zapisujemy tylko to, co ma
zostać rozwiązane, a nie to, jak ma to być rozwiązane.
1) :- 2) == zachodzi 1) gdy 2), lub 1) wynika z 2). Jest to odwrotny zapis implikacji,
odpowiednie reguły wnioskowania zapisujemy i dajemy je maszynie wnioskującej z
wbudowanymi mechanizmami przeszukiwania, wnioskowania.
Przykład nr 2 w PROLOGu:
Przeszukiwanie grafu. Graf jest zdefiniowany przez szereg definicji łuków skierowanych:
arc(a,b).
arc(a,c).
arc(b,c).
arc(b,d).
arc(c,d).
Zdania prologowe są wyrażeniami rachunku predykatowego, pierwszego rzędu. Pierwszy
symbol to symbol predykatowy p(a,b). p ma wartość T tylko wtedy, gdy zachodzi dla a i
b.
Najprostsza konstrukcja w prologu jest to zdanie predykatowe (ważna kropka!). Bez
kropki jest to tylko formuła (WFF - Well Formed Formula). W taki sposób wpisane proste
zdania rachunku predykatów to FAKTY.
Co to znaczy, że istnieje ścieżka w grafie? Jest to pewien związek:
path (X, Y, [X,Y]):-arc(X,Y).
Pierwszy argument to początek ścieżki, drugi koniec, trzeci to sama ścieżka. Czyli jeżeli
istnieje łuk od X do Y, To istnieje ścieżka o początku X, końcu Y, ścieżce [X,Y].
path(X, Y, [X|L]):-arc(X,Z),path(Z,Y,L).
Ścieżka pomiędzy X,Y o przebiegu X|L (X łączony z pewną listą L - ogonem) istnieje
wtedy, gdy istnieje łuk pomiędzy X a Z i istnieje ścieżka od Z do Y o przebiegu L.
Zmienną w prologu jest każdy symbol zaczynający się od wielkiej litery lub od symbolu
specjalnego.
Maszyna wnioskująca w PROLOGu wnioskuje wstecz. Formułujemy twierdzenie i każemy
udowodnić je interpreterowi. Przykładowo, jeżeli chcemy stwierdzić, czy istnieje ścieżka
z a do d:
?_path(a, d, Path)
Domyślnym kwantyfikatorem przy formułowaniu twierdzeń jest kwantyfikator
szczególny "istnieje". Po sformułowaniu takiego zapytania, maszyna wnioskująca wstecz
znajdzie ścieżkę i zapise ją w zmiennej "Path".
Sformułowanie jest celem początkowym (goal). Interpreter sprawdza fakty w
poszukiwaniu natychmiastowego rozwiązania. Jeżeli nie, to sprawdza reguły (rules) w
poszukiwaniu konkluzji, którą da się uzgodnić z celem. Jeżeli znajdzie, to proces
wnioskowania standardowo przebiega według wzorca poszukiwania wstecz z nawrotami.
Domyślnym kwantyfikatorem reguł jest kwantyfikator ogólny "dla każdego".
Uzgodnienie zmiennych określamy tutaj jako związanie zmiennych. Implementacja
wiązania zmiennych jest realizowana przez zastosowanie rezolucji. [Można sobie
przypomnieć IW: Rezolucja w PROLOGU z wykładu...].
Interpreter może wykonywać nawroty do węzłów przeszukiwanego drzewa rozwiązań, w
którym zachodził jakikolwiek wybór. Przykładowe wnioskowanie z wiązaniem zmiennych
może przebiegać następująco:
path(a,d,Path)
arc(a,Z1)
path(Z1,d,L1)
Z1=b
Path=[a|L1]
path(b,d,L1)
L1=[b,d]
arc(b,d)
<cel pusty>
Konstruowane struktury, które przynajmniej częściowo składają się ze zmiennych, to
tzw. "struktury nieukonkretnione" lub "struktury nie do końca ukonkretnione". Tworzenie
odpowiedzi przebiega już na etapie wnioskowania wstecz, przez dopisywanie "w drugą
stronę". Nie potrzeba wracać, tak jak u Traczyka na wykładach w zadaniach. Może to jest
mniej intuicyjne dla człowieka, ale znacznie szybsze dla maszyny.
4. Wykład 4:
Przykład ze ścieżkami w grafie - krok po kroku:
Potencjalnie, w problemie jest więcej niż jedno rozwiązanie. Jak jest to realizowane?
Interpreter prologu przeszukuje przestrzeń celów poszukiwaniu takich, które są
osiągalne z celu początkowego. Cele osiągalne przedstawiają drzewo - mówimy o
drzewie celów.
<rys1>
Przykładowe drzewo celów:
<rys2>
Czyli możemy skorzystać albo z jednej albo z drugiej reguły na każdym etapie. Schodząc
wgłąb, tworzymy drzewo celów. Na gałęziach wpisujemy uzgodnienia zmiennych, na
końcu listy celów pośrednich (w węzłach).
Traktujemy fakt jako regułę o pustej prawej stronie. Zdanie o pustym poprzedniku,
poprzednik jest rozpatrywany jako "prawda". Drzewo celów jest przeglądane od lewej
wgłąb. Przy natrafieniu na brak rozwiązania w liściu, wraca do ostatniego "rozwidlenia"
(backtrack) i kontynuuje.
Prolog domyślnie przeszukuje tylko do pierwszego rozwiązania. Żądanie znalezienia
wszystkich może przebiegać w dwóch trybach:
1) ?_path(a,d,S)
S=[a,b,d] XeS ; // średnik mówi, żeby szukać wszystkich spełniających celów
2) qs(X,Y,S) :- path (X,Y,S), check(S) // sprawdzenie S wymusza kolejne nawroty
Sprawy formalne:
- LISP nie będzie niezbędny do zaliczenia, ale konsekwencja w postaci niższych ocen
może się zdarzyć. Dziwna skala ocen.
- rozmowy projektowe przewidziane 3,4,5.06 - dopuszczenie do 2 kolosa na podstawie
zrozumienia zadań.
Prolog - historia i zarys:
Współcześnie tłumaczone: PROgramming in LOGIC, było PROgrammation en LOGique
(tak, koleś który to wymyślił - Alain Colmerauer - oczywiście jest Francuzem). Koncepcja
poracowana okoo roku 1970.
Powstał specjalnie jako język do przetwarzania gramatyk, nie przypuszczano, że
rozwinie się na taką skalę. Do popularności przyczyniły się prace prowadzone w
Londynie przez Roberta Kowalskiego. Powstaje podręcznik "Logic for problem solving".
Początkowo Prolog był językiem interpretowanym. Stał się popularny nie tylko do
zastosowań akademickich po skonstruowaniu efektywnego kompilatora Prologu.
Konstrukcja opracowana około 1970 roku przez Grenoble i Marseilles (tzw. składnia
marsylska, obecnie historyczna).
Kompilator używający współczesnej składni na uniwersytecie w Edynburgu, Warren i
Pereira. Nareszcie znalazł zastosowanie jako język do szerokiego przetwarzania danych,
właściwie nawet język ogólnego przeznaczenia.
Do zastosowania w dziedzinie sztucznej inteligencji predestynowało go środowisko
powstania i specyficzne właściwości.
Duża część rynku sztucznej inteligencji należy do Prologu i LISP. Od jakiegoś czasu
wchodzi na ten rynek Java, głównie moduły do integracji z większymi systemami. M.in.
wsparcie dla prologu. Innymi znanymi dla sztucznej inteligencji są m.in. SmallTalk i kilka
innych.
W porównaniu z LISP Prolog jest bardziej rozwojowy, umożliwia łatwiejsze poszukiwanie
nowych rozwiązań, jest dobrym językiem do prototypowania sztucznej inteligencji.
Dodatkowo, zmusza do poprawnego formułowania problemów, jest bardziej poprawny
"politycznie" i logicznie :). Rozłożenie języków na świecie: USA podzielone
(LISP=EastSide, Prolog=WestSide ;]). W Europie dominuje Prolog.
[piejemy dalej na cześć prologu...]
Prolog jest nazywany "Tool for thinking" - konstrukcja języka wymusza na nas logiczne
myślenie o problemie i prowadzi do rozwiązania i poprawnego formułowania problemów.
LISP wprowadza pewną dyscyplinę do programowania proceduralnego przez to, że
większość wartości przekazywanych jest przez wywołania funkcji.
[rzeczywiście, bardzo to pozytywne zjawisko, gdzie zagnieżdżamy 15 stopni struktur
nawiasowych zamiast używać zmiennych, ale wykładowca ma rację...]
Przykład w Prolog deklaratywnego wyznaczania konkatenacji dwóch list:
<następnik implikacji>
{zachodzi związek KONKATENACJA między obiektami ListaA, ListaB, ListaC}
JESLI
<poprzednik implikacji>
{
{ListaA=[] ORAZ ListaB=ListaC}
LUB
{
ListaA=[Pierwszy|Reszta]
ORAZ ListaC=[Pierwszy|NowaReszta]
ORAZ
{zachodzi związek KONKATENACJA między obiektami Reszta, ListaB,
NowaReszta}
}
}
Prolog dopuszcza alternatywę w poprzedniku, ale bardziej "poprawnie" jest używać po
prostu koniunkcji.
Na slajdach z wykładu mamy zapis w Prologu. Najpierw "beznadziejny i mało
efektywny". Dalej mamy optymalizację tego zapisu i działania.
Co się dzieje dla postaci uporządkowanej i nieuporządkowanej?
?_conc([a,b], [c,d], L). - tutaj dla pierwszej postaci programu mamy uzgadnianie wielu
zmiennych. W postaci skompresowanej / zoptymalizowanej, mamy za to jedno
uzgodnienie. Nie ma sensu przyrównywanie do prawej strony w przesłance tak, jak w
wersji niezoptymalizowanej. Złe przyzwyczejenie z czasów programowania
proceduralnego. Możemy od razu wielkości przyrównywane po prawej stronie wstawić
jako argumenty tworzonej funkcji.
Rysunek przedstawia budowanie wyniku z postaci zoptymalizowanej: <rys3>
5. Wykład 5:
Środowisko interpretacyjne Prologu:
Programy napisane w Prologu są standardowo interpretowane. Można je kompilować.
Użytkownik ma dostęp do kompilatora i interpretera wywołań. Użytkownik podaje cel.
Procedura może być skompilowana lub nieskompilowana. Użytkownik nie widzi
wewnętrznej reprezentacji. <rys1>
Możemy ogólnie mówić w takim razie o interpretacji (z punktu widzenia użytkownika).
Wywołania są interpretowane, natomiast procedury mogą być skompilowane
(wywołanie) lub nie (interpretacja).
Środowisko zalecane przez Parewicza: SWI PROLOG (http://www.swi-prolog.org).
Opracowane na wydziale nauk społecznych, katedra psychologii. Do badań modeli
psychologicznych. Procedura conc jest inaczej zbudowana, należy ją przesłaniać.
Do przykładu przeszukiwania grafu:
Pojawiło się opisane całe drzewo poszukiwań w materiałach wykładowych. Założenia
dotyczące grafu: acykliczny, skierowany.
Do przykładu przeszukiwania listy:
Wszelkie przetwarzanie danych w Prologu występuje w ramach operacji uzgodnienia.
Polega na tworzeniu struktur, budowaniu ich przy pomocy zmian wskaźników. Efektem
jest skonstruowana struktura.
Struktury danych w języku Prolog:
Powiedzmy, że chcemy stworzyć strukturę, która przedstawia zamówienie w pewnej
firmie. Dysponujemy numerem zamówienia, datą, nazwami klientów. <rys2>
Złożona struktura jest tworzona przez zdefiniowanie funktora, który transformuje zbiór
iloczynu kartezjańskiego prostych danych w zbiór struktur. Przykładowym zamówieniem
jest ZAM(l217, 20080312, 'XYZ').
Przypuśćmy, że mamy teraz w zamówieniu zagnieżdżony funktor zwracający datę:
Zam(l217, data(2008,03,12), 'XYZ'). Chcemy zdobyć zamówienia z zeszłego miesiąca.
Jak to robimy?
zamow_miesiac(zam(Nr, data(D,M,R), Klient), M)
W zależności, co chcemy zrobić, możemy zastosować zdefiniowany selektor po numerze
miesiąca w różny sposób.
?_zamow_miesiac(zam(l217, data(17,03,2008), 'XYZ'), M) - otrzymanie miesiąca danego
zamówienia, jeżeli zamowienie jest zmienną związaną.
Jeżeli miesiąc jest zmienną związaną, zaś zamówienie nie jest, to dostajemy listę
zamówień z określonego miesiąca.
Dla funktorów występujących w tym samym miejscu w wywołaniu, kontrola typu w
czasie wywołania polega na porównaniu funktorów celu i reguły. Jeżeli funktory są
identyczne, następuje uzgodnienie. Inaczej otrzymujemy błąd. Procedura uzgadniania
jest rekurencyjna, możemy dowolnie zagnieżdżać uzgodnienie.
Jeżeli w celu występuje zmienna, w regule pewien funktor, to zmienna zostaje związana
z wynikiem działania funktora.
[ Po prostu uzgadnianie w Prologu - dokładnie tak, jak na wykładzie z IW ]
Pary obiektów w Prologu:
Domyślnym funktorem dla pary obiektów w Prolog jest "." Np. wyrażenie: .(a,b)
reprezentuje parę obiektów. Listę można również zdefiniować jako parę: .(a,[]) dla listy
jednoelementowej, dla większej liczby elementów, np. dla dwóch: .(a,.(b,[])) . Każda lista
jest widoczna jako para, której pierwszym elementem jest pewien obiekt, a drugim inna
lista.
Porównanie funktorów dla par odbywa się zgodnie ze schematem:
.(X1, R1)
.(a, .(b,[]))
Taka wewnętrzna reprezentacja powoduje, że lista musi być "rozbierana" od przodu.
Przykład kolejny - procedura sprawdzająca, czy dany obiekt jest elementem
listy:
MEMBER(X,L):- L=[X|R].
MEMBER(X,[X|R]).
MEMBER(X,[X|_])
Ostatnia konstrukcja tego pierwszego zdania zawiera symbol specjalny "_" oznaczający
cokolwiek, z punktu widzenia do pominięcia. "_" nie przenosi wartości, nie interesuje
nas.
MEMBER(X,L):- L=[Y|R], X\=Y, MEMBER(X,R). // czyli X nie jest pierwszym elementem,
ale należy do reszty listy. Po przepisaniu:
MEMBER(X,[Y|R]) :- X\=Y, MEMBER(X,R).
Ostatecznie nasza procedura member wygląda następująco:
MEMBER(X,[X|_]).
MEMBER(X,[Y|R]) :- X\=Y, MEMBER(X,R).
Na materiałach wykładowych jest rozrysowane drzewo przeszukiwania przestrzeni celu
dla przykładu. Własność nierówności jest interpretowana jako cel tak, jakby była
uzgodniona z faktem. Jest to związane z implementacją wewnętrzną (różne wskaźniki?).
Zastępowane jest przez cel pusty.
Co się dzieje, jeżeli cel nie może być uzgodniony:
W kontekście pytania użytkownika po prostu nie uzyskujemy nic w odpowiedzi,
odpowiedź negatywna, np. na pytanie czy jest MEMBER, odpowiedź NO.
W kontekście przetwarzania w programie, jeżeli wywołanie MEMBER :
P(L1, L2):- Q(L1, X), MEMBER(X,L2).
X jest uzgadniany najpierw przez Q. Jeżeli MEMBER zakończy się niepowodzeniem,
następuje nawrót do ostatniego punktu wyboru X, czyli sprawdzenie, czy Q może w inny
sposób uzgodnić X. Ogólnie, w przypadku braku możliwości ustalenia celu, następuje
powrót do najbliższego punktu przetwarzania, gdzie mogła zostać podjęta inna decyzja,
czyli inne uzgodnienie zmiennych.
Procedury deterministyczne w kontekście celu:
Procedura jest deterministyczna w kontekście rozpatrywanego celu, jeżeli dla tego celu
początkowego w każdym kroku istnieje możliwość wybrania co najwyżej jednego celu.
Procedury CONC i MEMBER są deterministyczne w kontekście celu w zastosowaniach do
tej pory rozpatrywanych.
Niedeterministyczne zastosowania CONC:
?_conc(LA, LB, [a,b,c])
Gdzie zmienne LA i LB nie są związane. Wtedy rozwiązaniem są wszystkie pary LA, LB
takie, że lista [a,b,c] jest ich konkatenacją. W takim użyciu, pytamy o wszystkie możliwe
podziały listy.
Prawie w każdym kroku (z wyjątkiem liścia związanego z drugim celem) może być
uzgodniony każdy z dwóch celów:
conc([], L2, L2).
conc([X|L1], L2, [X,RN]) :- conc(R1, L2, RN).
Na slajdach z wykładu jest zademonstrowane drzewo przeszukiwania przestrzeni celów.
Jeżeli cel początkowy jest określony przez użytkownika, wtedy otrzyma wartości
zmiennych i odpowiedź YES.
Jeżeli kontekstem jest przebieg programu, nastąpi nawrót do najbliższego punktu
przetwarzania, w którym możliwe jest podjęcie innej decyzji, inne uzgodnienie celu.
Przykładowe zastosowanie operacji CONC w kontekście programu – final_split:
final_split(List1, LA, LB, List2) :- conc(LA,LB,List1), check_list(LB, List2).
Rozdzielamy listę w taki sposób, że
6. Wykład 6 - nie byłem... (cholerne PKP...)
7. Wykład 7:
Deterministyczne i niedeterministyczne usuwanie elementu:
Wersja funkcji usuwającej deterministyczna - usuwa pierwsze wystąpienie i tylko
pierwsze wystąpienie usuwanego obiektu.
Wersja niedeterministyczna - obydwie reguły mogą zostać wykonane równorzędnie, brak
warunku różności.
Usunięcie czegoś z listy pustej daje listę pustą. Usunięcie w przypadku
deterministycznym nieistniejącego obiektu powoduje zwrócenie listy bez zmian.
W przypadku niedeterministycznym - zwrócenie listy bez zmian (?) ... Rozrysować sobie
drzewka...
Deterministyczne i niedeterministyczne usuwanie wszystkich elemntów:
Czy usunięcie warunku w przypadk usuwania wszystkiego zmieni coś? Co? Czy to będzie
poprawne?
Dodawanie elementu przez usuwanie:
Argument 2 jest nieukonkretniony. Czyli oczekujemy listy, która po usunięciu arg1
dałaby arg3. Dostajemy w ten sposób, przy nieukonkretnionym L, wszystkie listy, które
po usunięciu a dałyby [b,e,c]
?_delete1(a, L, [b,e,c]);
Modyfikacja member:
member1(X,[X|_]).
member1(X,[Y|Rest]) :- member1(X,Rest). - wprowadzony niedeterminizm, po nawrotach
analizuje dalej i wyszukuje
Wywołanie ?_member(X, [a,b,c]). zwróci nam po kolei wszystkie elementy listy...
search_list(List1, List2) :- member1(X,List1), member(X,List2). Co to robi... ? Sprawdza,
czy kolejne elementy List1 należą do List2.
znajdz_zma(Lista_zam, Klient) :- member1(Zamowienie, Lista_zam),
zam_klient(Zamowienie, Klient).
zam_klient (zamowienie(Numer, Data, Wartosc, Klient), Klient).
Generowanie szablonów:
/= - to nie jest różność, tylko "nieuzgadnialne z..." !!!
?_member1(a,L). // gdzie L jest nieukonkretnione
Wygeneruje szablony list, na których a jest na 1, 2, 3 itd. miejscu.
Sprawy formalne:
Kolokwium I z PROLOG 30 - w postaci opracowania domowego, 5.05 + 2 lub trochę
więcej dni = termin realizacji. Lepiej zrobić wcześniej.
Kolokwium I z LISP (opcja) 20 – 27.05
Kolokwium II z PROLOG 50 - przedostatni poniedziałek semestru
Bez LISP – 57-80=4
Z LISP – normalnie.
Deklaratywność:
Wystartować z postaci proceduralnej, biorąc pod uwagę możliwości interpretera prologu,
deklaratywnie sformułować zadanie. Zdefiniowaliśmy tak concat. A co, jeżeli
proceduralnie?
CONC(LA, LB, LC)
{
if LA=[] then LC = LB
else
{
X = <pierwszy_element LA>
R = <reszta LA>
conc(R, LB, LC1)
LC = dolacz(X,LC1)
}
}
Tłumaczenie na PROLOG:
Nagłówek i if:
uproszczenie:
conc(LA,LB,LC):- LA = [], LC = LB.
conc([], L, L).
Jeżeli chcemy mieć regułę deterministyczną, to reguły muszą się nawzajem wykluczać.
// Hint: niepusta lista nie uzgodni się z pustą...
Część else:
conc([X|R] ..... // odpowiednik X = pierwszy, R = reszta
conc([X|R], LB, [X|LC1]) :- conc(R, LB, LC1).
// [X|LC1] - dolaczenie X na poczatku LC1
Czyli wynik:
conc([], L, L).
conc([X|R], LB, [X|LC1]) :- conc(R, LB, LC1).
Procedura dająca prefiksy listy:
prefix(L1, L2) :- L1=[].
=>
prefix([], _).
nie odwol.
prefix(L1, L2) :- L1=[X|R1], L2=[X|R2], prefix(R1, R2).
prefix([X|R1], [X|R2]) :- prefix(R1, R2).
// _ - zmienna anonimowa,
=>
a proceduralnie byłoby:
prefix(P, L)
{
P=[]
OR // rozejscie, nie xor!
X=<pierwszy L>
R=<reszta L>
prefix(P1, R)
P=<dodaj X do P>
}
Operacja podlisty:
Deklaratywnie:
sublist(S, L) :- prefix(S,L).
sublist (S, [X|R]) :- sublist (S, R).
8. Wykład 8:
A jeszcze bardziej deklaratywnie? Tak:
- S ma być sufiksem dowolnego prefiksu. Wykorzystamy do tego conc:
sublist(S,L) :- conc(P,Sf,L) , conc(PP, S, P). // S jest podlistą L wtedy, gdy P jest prefixem
L a Sf jest sufiksem (zachodzi związek, że [P|Sf]==L. I : prefix P składa się z pewnej listy
PP, w przypadku granicznym pustej, oraz listy S. Czyli jak w definicji: podlista jest
sufiksem dowolnego prefiksu L.
Do generowania wszystkich podlist: S niezwiązane, L związane, np. [a,b,c].
A co będzie, jeżeli przestawimy kolejność sublist? Nic nie jest związane dla
pierwszego wyrażenia:
sublist(S,L) :- conc(PP, S, P), conc(P, Sf, L).
Wiemy, że:
conc([], L, L).
conc([X|R], L2, [X|RN]) :- conc(R,L2,RN).
Jak to się będzie rozwijało drzewo? Powstanie generator szablonów:
conc(L,M,N)
L<-[]
M<-N
L=[], M=_G32, N=_G32
L<-[X1|R1]
M<-LZ1
N<-[X1|RN1]
conc (R1, LZ1, RN1)
R1<-[]
LZ1<-RN1
L=_G15, M=_G38, N=[_G15|_G38]
Generuje nam szablony podlist, w końcu zostaną wypełnione. Tutaj kolejność nie ma do
końca znaczenia, może jedna metoda trochę dłuższa niż druga, za to przy rekurencji...
Związek przełożony:
przelozony(X,Y):- bezp_przelozony(X,Y).
przelozony(X,Y):- przelozony(Z,Y), przelozony(X,Z).
Jeżeli w drugiej regule zmienimy kolejność predykatów w przesłance, wejdziemy w
nieskończoną pętlę, gdyż interpreter PROLOGU szuka wgłąb. Przestawienie reguł też
spowoduje nieskończoną pętlę.
HEURYSTYKA: Zawsze, jeżeli mamy definicje rekurencyjne, to reguła nierekurencyjna
przed rekurencją. Zwracać uwagę na kolejność predykatów!
Procedura tworzenia podzbioru:
Powiedzmy, że mamy: [a,b,c]. Chcemy podzbiorów: [], [a], [a,b], [a,b,c], [], [b], [b,c] ...
// zmienne wielkimi literami
subset([], _).
subset(Subset, Set):- Set=[X|R], Subset=[X|R1], subset(R1, R).
uproszczenie:
subset([],_).
subset([X|R1], [X|R]):- subset(R1, R).
druga możliwość:
subset(Subset, Set) :- Set=[X|R], Subset = R1, subset(R1, R)
uproszczenie całości:
subset([], _).
subset([X|R1], [X|R]) :- subset(R1, R). // podzbiory z elementem X
subset(R1, [X|R]) :- subset(R1, R).
// podzbiory bez elementu X
Przecięcie dwóch zbiorów:
set_intersec(Set1, Set2, Int)
{
if Set1=[] then Int=[]
else
{
X=<pierwszy Set1>
R=<reszta Set1>
if member(X, Set2) then
{
set_intersec(R, Set2, Int1)
Int<dodaj X do Int1>
}
else
{
set_intersec(R, Set2, Int1)
Int=Int1
}
}
}
Zapisanie do PROLOGU:
set_intersec([], _, []).
// W Prologu nie można zwracać true ani false, ale procedura może się powieść, albo nie.
I to wykorzystujemy:
// member zakończyło się powodzeniem
set_intersec([X|R], Set2, [X|Int1]) :- member(X, Set2), set_intersec(R, Set2, Int1).
// member zakończyło się niepowodzeniem
set_intersec([X|R], Set2, Int1) :- not member(X, Set2), set_intersec(R,Set2,Int1). (?!)
W sumie negację można sobie samemu napisać, nie patrząc na procedury wbudowane.
Jeżeli wywołujemy set_intersec po prawej stronie jakiejś reguły, to wiadomo, że trzecia
reguła nigdy się nie wykona przy pierwszym podejściu. Przy nawrocie, interpreter
wybierze pozostałą możliwość i przy nawrocie wejdzie w trzecią możliwość.
Jeżeli wyrzucimy z prawej strony 3 reguły "not member...", otrzymamy niedeterminizm
niezamierzony, robi się błąd. Konieczny jest pewien mechanizm siłowy, który umożliwia
pisanie czegoś w stylu 'else'. Warunek już raz został spełniony i w drugą gałąź nie trzeba
wchodzić.
Taki mechanizm istnieje.
Wstawianie do listy i sortowanie:
insert(X, [Y|Rest], [Y|Rest1]) :- X>Y, insert(X, Rest, Rest1). // Przesuwanie X po liście do
coraz wyższych elementów, aż w końcu znalezienie odpowiedniego miejsca i...
insert(X, [Y|Rest], [X,Y|Rest]) :- X =< Y. // ... wstawienie
Żeby nie sprawdzać dla drugiej reguły warunku, który już raz został sprawdzony, przy
możliwości nawrotu w wywołaniu po prawej stronie, stosujemy ODCIĘCIE. Oznaczone
przez "!".
insert(X, [Y|Rest], [Y|Rest1]) :- X>Y, !, insert (X, Rest, Rest1).
insert(X, List, [X|List]).
Odcięcie:
Zinterpretowanie odcięcia powoduje wymazanie z pamięci interpretera wszystkich
punktów, które wystąpiły przed odcięciem w kontekście przetwarzania procedury.
P' :- Q, !, R.
P'' :- S, T.
P'
Q1,R1
x
P0 x
P''
Definicja operacji z negacją z odcięciem:
not_member(X, L) :- member(X,L). // jeżeli member zakończy się nie powodzeniem
not_member(_,_).
powodzeniem
// to not_member zakończy się
not_member(X, L) :- member(X, L), fail // jeżeli member zakończy się powodzeniem, to
predykat fail spowoduje niepowodzenie dla not_member
not_member(X, L) :- member(X, L), !, fail // Teraz zabezpieczamy, żeby nie wykonało się
przy nawrocie wejście w gałąź, która spowoduje zwrócenie prawdy i błąd.
Ostatecznie:
not_member(X, L) :- member(X, L), !, fail.
not_member(_, _).
Dla dowolnego predykatu P:
not(P) :- call(P), !, fail. // metaprogramowanie, wartość zmiennej staje się celem.
not(P).
W nowszej wersji można sobie napisać po prawej stronie samą zmienną i automatycznie
zostanie to obsłużone.
Przy takim zapisie, wywołanie not(member(...)). Da się te nawiasy jakoś opuścić - na
przyszłych zajęciach.
9. Wykład 9:
Punkt wyjścia do projektu – książka Bratki.
Bazy danych – ciąg dalszy:
Uzyskanie listy wszystkich wyników z bazy danych klientów (uzyskanie widoku
zamówień dla klienta):
suma_zam(Klient, Suma) :- findall(W, zamowienie(_,_,W,Klient), Lista_wartosci),
sumuj_liste(Lista_wartosci, Suma).
sumuj_liste([], 0).
sumuj_liste([X|R], S) :- sumuj_liste(R, S1), S is S1+X.
Zbiorczy termin dla procedur i faktów w PROLOGu to klauzula (clause).
Do dostępu do baz danych jest używany podzbiór PROLOGu bez struktur – DATALOG.
Subtelności dotyczące wprowadzania do bazy danych:
Przez klauzulę typu 'jest':
jest_klient(klient(...))
jest_zamówienie(zamowienie(...))
[WTF?! Dlaczego on tego nie może prosto wytłumaczyć...]
Procedura sortowania przez wstawianie:
insertsort([],[]).
insertsort([X|R], Sorted) :- insertsort(R, Sorted1), insert(X, Sorted1, Sorted).
insert(X, [Y|Rest], [Y|Rest1]):- X>Y, !, insert (X, Rest, Rest1).
insert(X,List[X|List]).
insert(X, [], [X]). // jak to zmienia działanie? Co by było, jakby tego nie było?
Takie sortowanie jest nieefektywne, gdyż złożoność jest w tym przypadku kwadratowa.
Jak zdefiniować sortowanie tak, żeby było efektywne? Trzeba zdefiniować dodatkowy
związek pomiędzy wynikiem pośrednim, a końcowym. Zmienną gromadzącą wyniki
pośrednie nazywamy akumulatorem.
insertsort1([X|R], Acc, Sorted) :- insert(X, Acc, Acc1), insertsort1(R, Acc1, Sorted).
insertsort1([], Acc, Acc). // przecież to już wynik końcowy, czyli Sorted jest po prostu
akumulatorem...
Wywołanie: insertsort1([1,5,7,3], [], Wynik). // pusty akumulator
Procedura rekurencyjna tutaj wykorzystuje rekursję w ogonie (tail recursion). Dzięki
temu silnik może to rozwinąć w iterację (wiemy, że nie będziemy korzystali z wyniku
pośredniego, więc możemy tak sobie rozwinąć i pominąć).
10. Wykład 10:
Materiał do kolokwium / projektu:
- uzgadnianie argumentów
- nawroty (zapamiętywanie punktów wyboru)
- sens logiczny
- konsekwencje:
* operacje na strukturach wbudowanych w interpreter
* obiekty częściowo / (nie) ukonkretnione
* konstrukcje generuj / testuj
- procedury niedeterministyczne
- generatory szablonów
- odpowiednik konstrukcji if-then-else (oraz uogólnienie w postaci możliwości wycofania
się z dowolnego poziomu)
Rozwiązania zadań można wysyłać mailem, będzie tydzień na wykonanie, ale pracy na 2
godziny ( ;) ). Kolokwium 30 pkt.
Projekt – Planer Sekwencji Akcji:
Planer wybiera sekwencję akcji tak, aby osiągnąć pewien stan wyjściowy z wejściowego
w świecie klocków, zwracając sekwencję akcji.
Jednym z podejść analizy przestrzeni rozwiązań jest MEANS-ENDS ANALYSIS (analiza
celów środków) – bardziej optymalna od zwykłego przeszukiwania. W metodzie tej
startujemy od celów (np. zbioru położeń klocków). Zastanówmy się, co jest potrzebne,
żeby któryś cel osiągnąć w jednym kroku (jednej akcji). Wybieramy arbitralnie któryś z
nich. Tworzymy pewien stan pośredni, w którym cel (np. G2) jest osiągnięty, a pozostałe
nie. Tworzymy stany pośrednie przed G2, które dzieli od G2 jeden krok. Bierzemy pod
uwagę tylko te stany, które są możliwe z punktu widzenia dostępnych ruchów w
przestrzeni. Zbiór stanów pośrednich staje się nowym zbiorem celów. Za każdym razem
sprawdzamy, czy osiągnięty stan pośredni nie zawiera celów częściowo zawartych w
stanie początkowym.
Utworzone drzewo ma własność następującą: przy przeszukiwaniu wgłąb w lewo, jeżeli
nie prowadzi nas do rozwiązania pewna ścieżka, to ta ścieżka staje się nieskończona.
Należy przeszukiwać wszerz, inaczej narażamy się na przepełnienie stosu i
nieotrzymanie odpowiedzi. Przeszukiwanie wszerz da także najkrótszą możliwość.
Program pierwotny implementuje taki algorytm przeszukiwania, który należy
odpowiednio zmodyfikować / poprawić. Jedna z modyfikacji dostępna w materiałach
internetowych do książki Bratki: www.booksites.net/bratko/ . Podany wybrany kod, m.in.
„A simple means-ends planer”. [Poczytać z Bratki...].
Omówienie kodu:
plan(State, Goals, [], State) :- goals_achieved(Goals, State). // przypadek graniczny,
stany docelowe=początkowe, koniec rekurencji.
plan(State, Goals, Plan, FinalState) :- ...
State – zbiór stanów początkowych, lista egzemplarzy funktorów reprezentujących stan.
Goals – podobnie, tylko zbiór stanów docelowych
W przestrzeni klocków mamy dwa funktory reprezentujące stan: on(b5, p7) - coś stoi na
czymś, clear(b3) – nic nie stoi np. na klocku.
Plan - lista akcji, związki przestrzenne o postaci przykładowo:
move(<co>,<skąd>,<dokąd>). Plan będzie listą takich akcji.
FinalState – lista stanów końcowych
Przechowujemy listę wszystkich celów (dlaczego, o tym za chwilę).
choose_goal – wybiera pewien cel
achieves – daje akcję, która prowadzi do celu
requires – daje warunek dla wykonania akcji prowadzącej do celu, Condition staje się
nową listą celów
plan – tworzy stan pośredni 1 na podstawie akcji
perform_action – przejście do stanu pośredniego 2 na podstawie akcji
plan – twórz plan dla następnego zbioru celów (stanu pośredniego 2), lista Goals zostaje
rozszerzona!
conc – dołącz
To jest przeszukiwanie wgłąb.
Modyfikacja 1: zaimplementowanie przeszukiwania wszerz:
Conc wywoływane jako pierwsza przesłanka. Powoduje to wygenerowanie szablonów,
ograniczenia dają przeszukiwanie wszerz.
Pierwsze zadanie badawcze: jak to działa, w jaki sposób i dlaczego działa to jako
przeszukiwanie wszerz (sens deklaratywny jest zachowany, ale jak to wygląda
proceduralnie?). Sprawdzić, gdzie następują nawroty (w której procedurze i na jakiej
zasadzie?). Nawrót następuje w przypadku nieuzgodnienia (niepowodzenia). Dokąd
nawracamy? Generalnie, co można powiedzieć o nawrotach.
W jaki sposób następuje sprawdzenie, że wszystkie cele znajdują się na liście?
Wywołanie member (niedeterministycznego! u nas to było member1!). Pobierane są
kolejne elementy listy celów i sprawdzane (w goals_achieved).
choose_goal – wybieramy cel, który nie został jeszcze osiągnięty
achieves - ma nam podać akcję, która osiąga zadany cel. Sprawdzamy, co akcja ma
dodać do stanu, aby go spełnić. Pomocnicze procedury adds i removes na związkach
przestrzennych.
requires – kolejna modyfikacja: requires(move(Block, From, To), [clear(Block), clear(To),
on(Block,From)]):- block(Block), object(To), Block\=From, From\=To, To\=Block. // i
właśnie to w materiałach jest modyfikacją. Usuwa informacje dotyczące „From”. Ma to
spore konsekwencje dla sprawdzenia, na razie po prostu przyjmujemy.
Rozpatrując procedury pomocnicze trzeba patrzeć, gdzie jest możliwość wyboru,
nawrotu itp.
Podpowiedź do problemu nawrotów: pomocna będzie umiejętność wyszukania w kodzie,
jak następują nawroty w kodzie (czerwone i zielone strzałki w materiałach
wykładowych).
11. Wykład 11:
Projektu ciąg dalszy ku uciesze gawiedzi...
Wstawianie checkpointu w Prologu:
pokaz_krok(rezultaty) :- write(co_sie_dzieje), write( ), read( ), ...
Można też wprowadzić znacznik pracy krokowej jako dodatkowy argument. Następuje
sprawdzenie wartości znacznika, praca krokowa / debug dla wartości true, dla fail –
normalne wykonanie.
Projekt:
1 Przeszukiwanie wszerz na planie bliższym i dalszym:
- kiedy następują i jak wyglądają nawroty
- jak działa przeszukiwanie wszerz – plan bliższy i dalszy
2 Ochrona osiągniętych celów:
- w pewnym miejscu w kodzie jest wstawione odwołanie do procedury, która zapobiega
zniszczeniu już znalezionego celu. Gdzie? Jak działa?
3 Powtarzanie celów i stanów – czy trzeba zabezpieczyć przed wpadaniem w cykle i jak?
- czy osiągnięcie stanu wcześniej w czasie planowania, w danym podejściu, danej
rekurencji, a potem ponownie, świadczy o możliwości zapętlenia?
- jak sobie radzić z takim problemem, jeżeli wystąpi (zapętlenie?) - trzeba skorzystać z
akumulatora, ze zmiennej akumulacyjnej.
4 Obiekt „From” z aktualnego stanu:
Coś z wiązaniem planerowi rąk i usuwaniem jednego z celów (konkretnie From) z
requires, żeby można było wymieniać obiekty i tworzyć nowe i szukać bardziej
optymalnych:
- zmiana requires (usunięcie From)
- bardzo istotne zmiany w perform_action
action_block i action_from – to są selektory, trochę inaczej zrealizowane niż do tej pory.
Action ukonkretnione, Block nieukonkretnione, From podobnie. Użycie arg do rozbierania
struktur, użycie functor do wyłuskania funktora:
arg(2, f(a,b), b) // sprawdzamy, czy b jest drugim argumentem wywołania funktora
functor(f(a,b),F,N) // F=f, N=2
Czyli po prostu nie zamrażamy From, tylko przez odpowiednie selektory ukonkretniamy
sobie From wcześniej, jednocześnie nie wiążąc planerowi rąk... Arg wykorzystywane w
postaci generatora odpowiedniego From. Pokazanie elastyczności Prologu.
OŚWIECENIE w kwestii even i odd1: być może even i odd1 działają inaczej, bo lista jest
reprezentowana w postaci pary [head|tail], lista jednoelementowa ma pusty ogon, czyli
jest to para (element,lista_pusta). Ale sama lista pusta musi być reprezentowana w jakiś
inny sposób, chyba nie może być reprezentowana przez parę.
5 Obiekt z To z aktualnego stanu:
Podobnie jak dla 4, nie ograniczamy planera. Pozbawiamy requires prawej strony i
spodziewamy się, że sam sobie wszystko znajdzie... Bajera... Zamiast rzucać
przypadkowe rozwiązania, przedstawiamy nieukonkretnione obiekty i gdzieś one się
prawidłowo ukonkretniają. Gdzie, jak, kiedy, jakie procedury? Prolog, operacje na
strukturach nieukonkretnionych, elastyczność, itp.
Jakoś trzeba jeszcze zawrzeć sprawdzenia, że Block\= To itp. Warto się przynajmniej
zastanowić, jak. Spróbować włączyć różności typu From\=To, dlaczego to jest bez sensu
itp.
12. Wykład 12: