Logika RozmytaProgramownie A.I. Gier

Logika Rozmyta

Ludzie mają niesamowitą zdolność do komunikowania umiejętności w prosty i dokładny sposób, stosując niejasne reguły językowe. Na przykład kucharz telewizyjny może pouczyć Cię, jak zrobić idealny ser na toście:

1. Wytnij dwie kromki chleba średniej grubości.
2. Włącz ciepło na patelni na wysokim poziomie.
3. Grilluj plastry z jednej strony, aż będą złotobrązowe.
4. Odwróć plastry i dodaj obficie porcję sera.
5. Wymień i grilluj, aż wierzch sera będzie lekko brązowy.
6. Usuń, posyp niewielką ilością czarnego pieprzu i jedz.

Wszyscy bylibyśmy pewni, że postępując zgodnie z tymi instrukcjami, przygotujesz pyszną przekąskę. Ludzie robią takie rzeczy przez cały czas. Jest to dla nas przejrzysty i naturalny proces interpretowania takich instrukcji w sensowny i dokładny sposób. Czy przy projektowaniu sztucznej inteligencji do gier komputerowych nie byłoby wspaniale móc komunikować się z komputerem w podobny sposób - w celu szybkiego i prostego mapowania wiedzy eksperckiej z dziedziny ludzkiej na cyfrową? Gdyby komputery były w stanie zrozumieć niejasne terminy lingwistyczne, moglibyśmy usiąść z ekspertem w dziedzinie zainteresowań (częściej to nie Ty), zadawać pytania dotyczące umiejętności niezbędnych do odniesienia sukcesu w tej dziedzinie oraz z odpowiedzi szybko tworzą reguły językowe do interpretacji przez komputer - tak jak te pokazane przy robieniu tostów. Konwencjonalna logika jest nieodpowiednia do przetwarzania takich reguł. Na przykład wyobraź sobie, że programujesz grę w golfa i masz zadanie spędzenia dnia z Tigerem Woodsem w celu ustalenia podstawowych zasad gry w golfa. Pod koniec dnia twój notatnik jest pełen mądrości, takiej jak te:

Podczas kładzenia: jeśli piłka jest daleko od dołka, a zieleń delikatnie pochyla się w dół od lewej do prawej, następnie mocno uderz piłkę pod kątem lekko na lewo od flagi.
Podczas stawiania: Jeśli piłka jest bardzo blisko dołka, a zieleń między piłką a dołkiem jest pozioma, uderz piłkę delikatnie i bezpośrednio w dołek. Podczas jazdy z tee: jeśli wiatr ma dużą siłę i wieje od prawej do lewej, a dziura jest daleko, uderz piłkę mocno i pod kątem z prawej strony flagi.

Reguły te są bardzo opisowe i mają sens dla człowieka, ale trudno je przetłumaczyć na język, który komputer może zrozumieć. Słowa takie jak "daleko", "bardzo blisko" i "delikatnie" nie mają ostrych, dobrze zdefiniowanych granic, a kiedy próbujemy opisać je w kodzie, wynik często wygląda na niezdarny i sztuczny. Na przykład możemy zakodować opisowy termin "Odległość" jako zbiór przedziałów:

Zamknij = piłka znajduje się między 0 a 2 metry od dołka.
Średni = piłka znajduje się w odległości od 2 do 5 metrów od dołka.
Daleko = piłka jest dalej niż 5 metrów od dołka.
Ale co jeśli piłka jest 4,99 metra od dołka? Wykorzystując te interwały do przedstawienia odległości, komputer mocno umieści piłkę w szczelinie "Średniej", mimo że dodanie kilku centymetrów sprawi, że będzie daleko! Nietrudno zauważyć, że podczas manipulowania danymi przedstawionymi w taki sposób rozumowanie AI dotyczące domeny będzie zasadniczo błędne. Oczywiście możliwe jest zmniejszenie efektu tego problemu poprzez tworzenie coraz mniejszych przedziałów, ale podstawowy problem pozostaje, ponieważ składniki odległości są nadal reprezentowane przez odrębne przedziały. Porównaj to z ludzkimi powodami. Rozważając terminy lingwistyczne, takie jak "daleko" i "blisko" lub "delikatnie" i "stanowczo", człowiek jest w stanie ustalić niejasne granice i pozwolić na powiązanie wartości z terminem w pewnym stopniu. Gdy piłka znajduje się 4,99 metra od dołka, człowiek uzna, że jest ona częściowo związana z terminem "średni dystans", ale głównie z terminem "daleki dystans". W ten sposób ludzie postrzegają jakość odległości piłki stopniowo przesuwając się między terminami językowymi zamiast gwałtownie się zmieniając, co pozwala nam dokładnie rozumować zasady językowe, takie jak te podane podczas gry w golfa lub robienia tostów. Rozmyta logika, wynaleziona przez człowieka o imieniu Lotfi Zadeh w połowie lat sześćdziesiątych, umożliwia komputerowi rozumowanie terminów i zasad językowych w sposób podobny do ludzi. Pojęcia takie jak "daleko" lub "nieznacznie" nie są reprezentowane przez dyskretne przedziały, lecz przez zbiory rozmyte, co umożliwia przypisanie wartości do zbiorów w pewnym stopniu - proces zwany fuzyfikacją. Używając zakłopotanych wartości, komputery są w stanie interpretować reguły językowe i generować wyniki, które mogą pozostać rozmyte lub - częściej, zwłaszcza w grach wideo - mogą być rozmrożone, aby zapewnić wyraźną wartość. Jest to znane jako wnioskowanie oparte na regułach rozmytych i jest jednym z najpopularniejszych zastosowań logiki rozmytej.



Wkrótce zajmiemy się bardziej szczegółowo procesem rozmytym, ale zanim zaczniesz rozumieć zestawy rozmyte, pomaga zrozumieć matematykę zestawów wyraźnych, więc nasza podróż do rozmytych rozpocznie się tam.

UWAGA. Interpretacja reguł językowych jest tylko jednym z wielu zastosowań logiki rozmytej. Skoncentrowałem się na tej aplikacji, ponieważ jest to jedna z najbardziej przydatnych funkcji dla programistów AI. Logikę rozmytą z powodzeniem zastosowano w wielu innych obszarach, w tym w inżynierii sterowania, rozpoznawaniu wzorców, relacyjnych bazach danych i analizie danych. Prawdopodobnie masz w domu kilka półprzewodnikowych sterowników logiki rozmytej. Mogą regulować system centralnego ogrzewania lub stabilizować obraz w kamerze wideo.

Zestawy Crisp

Zestawy Crisp to matematyczne pojęcia nauczane w szkole. Mają one jasno określone granice: Obiekt (czasem nazywany elementem) albo całkowicie należy do zbioru, albo nie. Jest to przydatne w przypadku wielu problemów, ponieważ wiele obiektów można precyzyjnie sklasyfikować. W końcu pik to szpadel; to nie jest częściowo szpadel, a częściowo para nożyc ogrodowych. Domena wszystkich elementów, do których należy zbiór, nazywana jest wszechświatem dyskursu. Biały prostokąt na rycinie 10.2 przedstawia wszechświat dyskursu liczb całkowitych z zakresu od 1 do 15. Koła wewnątrz UOD oznaczają zbiór liczb całkowitych parzystych i zbiór liczb nieparzystych.



Za pomocą notacji matematycznej zestawy te można zapisać jako:

Nieparzyste = {1, 3, 5, 7, 9, 11, 13, 15}
Parzyste = {2, 4, 6, 8, 10, 12, 14}

Jak widać, stopień przynależności liczby do wyraźnego zbioru wynosi albo prawda, albo fałsz, 1 lub 0. Liczba 5 to 100 procent nieparzystych i 0 procent parzystych. W klasycznej teorii zbiorów wszystkie liczby całkowite są w ten sposób czarno-białe - należą one do jednego zbioru do stopnia 1, a drugiego do stopnia 0. Warto również podkreślić, że element może być zawarty w więcej niż jednym ostry zestaw. Na przykład liczba całkowita 3 jest członkiem zbioru liczb nieparzystych, zbioru liczb pierwszych i zbioru wszystkich liczb mniejszych niż 5. Ale we wszystkich tych zbiorach jego stopień członkostwa wynosi 1.

Ustaw operatory

Istnieje wiele operacji, które można wykonać na zestawach. Najczęstsze to zjednoczenie, przecięcie i uzupełnienie. Połączenie dwóch zbiorów jest zbiorem zawierającym wszystkie elementy z obu zbiorów. Operator związku jest zwykle zapisywany za pomocą symbolu ∪. Biorąc pod uwagę dwa zbiory A = {1, 2, 3, 4} i B = {3, 5, 7}, połączenie A i B można zapisać jako:

A ∪ B = 1,2,3,4,3,5,7 } (10.1)

Połączenie dwóch zbiorów jest równoważne ORingowaniu zbiorów razem - dany element znajduje się w jednym LUB drugim. Przecięcie dwóch zbiorów, napisanych za pomocą symbolu ∩, jest zbiorem zawierającym wszystkie elementy obecne w obu zestawach. Używając zestawów A i B z góry, ich przecięcie jest zapisane jako:

A ∩ B = {3} (10.2)

Przecięcie dwóch zbiorów jest równoważne ANDowaniu zbiorów razem. Używając naszych dwóch zestawów powyżej, jest tylko jeden element, który znajduje się w zestawie A ORAZ w zestawie B, tworząc przecięcie zbiorów A i B {3}. Dopełnieniem zbioru jest zbiór zawierający wszystkie elementy wszechświata dyskursu nieobecnego w zbiorze. Innymi słowy, jest odwrotnością zbioru. Powiedzmy, że wszechświat dyskursu A i B to A ∪ B, jak podano w równaniu (10.1), to dopełniacz A jest B, a dopełniacz B jest A. Operator dopełniacza jest zwykle zapisywany za pomocą symbolu ", chociaż czasami jest oznaczony przez pasek w górnej części nazwy zestawu. Obie opcje pokazano w równaniu



Operator dopełniania jest równoważny NOT

Zestawy rozmyte

Zestawy Crisp są przydatne, ale problematyczne w wielu sytuacjach. Na przykład zbadajmy wszechświat dyskursu wszystkich IQ i zdefiniujmy zestawy dla Głupiego, Średniego i Sprytnego w następujący sposób:

Głupi = {70, 71, 72,… 89}
Średnia = {90, 91, 92,… 109}
Sprytne = {110, 111, 112,… 129}

Graficzny sposób pokazania tych wyraźnych zestawów pokazano na rysunku. Zauważ, że stopień przynależności elementu do dowolnego zestawu może wynosić 1 lub 0.



Inteligencję ludzi można teraz podzielić na kategorie, przypisując ich do jednego z tych zestawów na podstawie ich wyniku IQ. Najwyraźniej jednak osoba o ilorazie inteligencji 109 jest znacznie powyżej średniej inteligencji i prawdopodobnie większość jego rówieśników uznałaby go za sprytnego. Z pewnością jest znacznie bardziej inteligentny niż osoba, która ma wynik 92, mimo że obie należą do tej samej kategorii. Śmieszne jest też porównywanie osoby z IQ 79 i osoby z IQ 80 i dochodzenie do wniosku, że jedno jest głupie, a drugie nie! To tutaj spadają ostre zestawy. Zestawy rozmyte pozwalają w pewnym stopniu przypisywać do nich elementy.

Definiowanie rozmytych granic za pomocą funkcji członkostwa

Zestaw rozmyte jest definiowany przez funkcję członkostwa. Funkcje te mogą mieć dowolny kształt, ale zazwyczaj są trójkątne lub trapezowe. Rysunek 10.4 pokazuje kilka przykładów funkcji członkostwa. Zwróć uwagę, w jaki sposób definiują stopniowe przejście z regionów całkowicie spoza zestawu do regionów całkowicie w zestawie, umożliwiając w ten sposób częściowe członkostwo w zestawie. To jest istota logiki rozmytej.



Rysunek poniższy pokazuje, w jaki sposób pojęcia językowe Dumb, Average i Clever można przedstawić jako zbiory rozmyte składające się z trójkątnych funkcji członkostwa. Linia przerywana pokazuje, w jaki sposób Brian, który ma iloraz inteligencji równy 115, jest członkiem dwóch zestawów. Jego stopień członkostwa w Clever wynosi 0,75, a średnio 0,25. Jest to zgodne z tym, w jaki sposób człowiek rozumowałby inteligencję Briana. Człowiek uzna go za przebiegłego, ponadprzeciętnego, co właśnie można wywnioskować z jego rozmytych ustalonych wartości członkostwa.



UWAGA Warto zauważyć, że terminy lingwistyczne powiązane z rozmytymi zbiorami mogą zmieniać swoje znaczenie, gdy są używane w różnych ramach odniesienia. Na przykład znaczenie zbiorów rozmytych Wysoki, Średni i Krótki będzie inne dla Europejczyków niż dla pigmejów Ameryki Południowej. Dlatego wszystkie zbiory rozmyte są zdefiniowane i używane w kontekście. Funkcję członkostwa można zapisać w notacji matematycznej w następujący sposób:

FName_of_set(x) (10.4)

Za pomocą tej notacji możemy napisać stopień członkostwa Briana, lub w skrócie DOM, w rozmytym zestawie Clever jako:

CleverBrian = FClever(115) = 0.75 (10.5)

Operatory zbiorów rozmytych

Możliwe są przecięcia, związki i uzupełnienia zbiorów rozmytych, podobnie jak w przypadku zestawów wyraźnych. Rozmyta operacja przecięcia jest matematycznie równoważna operatorowi AND. Wynikiem ANDing dwóch lub więcej zbiorów rozmytych razem jest inny zestaw rozmytych. Zamazany zestaw ludzi, którzy są przeciętni ORAZ sprytni, pokazano graficznie na tu.



Przykład graficzny dobrze ilustruje, w jaki sposób operator AND jest równoważny z przyjęciem minimalnej wartości DOM (stopnia członkostwa) dla każdego zestawu, którego członkiem jest wartość. Jest to zapisane matematycznie jako:

FAverage∩Clever(x) = min{FAverage(x), FClever(x)} (10.6)

Stopień członkostwa Briana w grupie osób przeciętnych ORAZ Sprytnych wynosi 0,25. Połączenie zbiorów rozmytych jest równoważne operatorowi OR. Zestaw złożony, który jest wynikiem ORing dwóch lub więcej zestawów razem, wykorzystuje maksimum DOM zestawów komponentów. Dla zbiorów Średnia i Sprytna jest to zapisane jako:

FAverage∪Clever(x) = max{FAverage(x), FClever(x)} (10.7)

Rysunek pokazuje grupę ludzi, którzy są przeciętni lub sprytni. Członkostwo Briana w tym zestawie wynosi 0,75.



Uzupełnienie wartości DOM o wartości m wynosi 1 - m. Rysunek 10.8 opisuje grupę ludzi, którzy NIE są Mądrzy. Widzieliśmy wcześniej, jak stopień członkostwa Briana w Clever wynosi 0,75, więc jego DOM dla NOT Clever powinien wynosić 1 - 0,75 = 0,25, i dokładnie to widzimy na rysunku.



NOT Clever można zapisać matematycznie jako:

FClever (x)′ = 1 - FClever (x) (10.8)

Żywopłoty

Żywopłoty to jednoargumentowe operatory, które można wykorzystać do modyfikacji znaczenia zbioru rozmytego. Dwa powszechnie stosowane żywopłoty są BARDZO i FAIRY. Dla rozmytego zestawu A, BARDZO modyfikuje go w następujący sposób:

FVERY(A) = FA(x))2 (10.9)

Innymi słowy, skutkuje kwadratem stopnia członkostwa. FAIRLY modyfikuje rozmyty zbiór, biorąc pierwiastek kwadratowy ze stopnia członkostwa, w ten sposób:

FVERY(A) =√ FA(x) (10.10)

Efekt tych żywopłotów najlepiej widać graficznie. Rycina 10.9 pokazuje, w jaki sposób BARDZO zawęża funkcję członkostwa i jak FAIRLY ją poszerza. Jest to intuicyjne, ponieważ kryteria członkostwa w zestawie zmodyfikowanym przez FAIRLY powinny być bardziej rozluźnione niż w przypadku samego zestawu. I odwrotnie, BARDZO - kryterium jest zaostrzone.



Rozmyte zmienne językowe

Rozmyta zmienna językowa (lub FLV) to kompozycja jednego lub więcej zbiorów rozmytych reprezentujących jakościowo pojęcie lub domenę. Biorąc pod uwagę nasz wcześniejszy przykład, zbiory Dumb, Average i Clever są członkami rozmytej zmiennej językowej IQ. Można to zapisać w zestawie notacji jako:

IQ = {Głupi, średni, sprytny}

Oto kilka innych przykładów rozmytych zmiennych językowych i ich składowych zbiorów rozmytych:

Prędkość = {Wolny, Średni, Szybki}
Wysokość = {karzeł, krótki, średni, wysoki, gigant}
Wierność = {przyjaciel, neutralny, wróg}
Kierowanie na cel = {skrajnie lewy, lewy, środkowy, prawy, skrajny prawy}

Rysunek pokazuje nagłówek celu FLV w formie graficznej. Zwróć uwagę, jak funkcje członkostwa w zestawach elementów mogą być zróżnicowane pod względem kształtu i asymetryczne, jeśli wymaga tego problem. Zbiór kształtów (funkcje członkostwa), które składają się na FLV, jest znany jako rozmyta rozmaitość lub rozmyta powierzchnia



UWAGA. Praktycy logiki rozmytej wydają się nie być w stanie uzgodnić spójnej terminologii opisującej elementy językowe, które składają się na system rozmyty (o ironio). Często wyrażenie "rozmyta zmienna językowa" (lub po prostu "zmienna językowa") znajduje zastosowanie do zbioru zbiorów rozmytych i do samych zestawów. Może to być mylące podczas czytania dostępnej literatury.

Reguły rozmyte

Tutaj wszystko zaczyna się łączyć. Zdaję sobie sprawę, że możesz być w tej chwili zdezorientowany, ale trzymaj się tam; oświecenie będzie wkrótce wasze! Reguły rozmyte składają się z poprzednika i konsekwencji w postaci:

JEŚLI poprzednia WTEDY konsekwencja

Poprzednik reprezentuje warunek, a konsekwencja opisuje konsekwencję, jeśli warunek ten jest spełniony. Ten typ reguły jest znany wszystkim programistom. Wszyscy napisaliśmy kod taki jak:

JEŻELI Wizard.Health () <= 0 NASTĘPNIE Wizard.isDead ()

Różnica w stosunku do reguł rozmytych polega na tym, że w przeciwieństwie do konwencjonalnych reguł, w których konsekwencja strzela albo nie, w systemach rozmytych konsekwencja może strzelać do pewnego stopnia. Oto kilka przykładów rozmytych reguł:
JEŚLI Target_isFarRight NASTĘPNIE Turn_QuicklyToRight
JEŚLI BARDZO (Enemy_BadlyInjured) NASTĘPNIE Behavior_Aggressive
JEŚLI Target_isFarAway I Allegiance_isEnemy THEN
Shields_OnLowPower
JEŻELI Ball_isCloseToHole ORAZ Green_isLevel NASTĘPNIE HitBall_Gently
AND HitBall_DirectlyAtHole
JEŻELI (Bend_HairpinLeft LUB Bend_HairpinRight) ORAZ Track_SlightlyWet
NASTĘPNIE Speed_VerySlow

Poprzednikiem może zatem być pojedynczy termin rozmyty lub zbiór, który jest wynikiem kombinacji kilku terminów rozmytych. Stopień przynależności poprzednika określa stopień, w jakim konsekwentnie strzela. System wnioskowania rozmytego składa się zazwyczaj z wielu takich reguł, których liczba jest proporcjonalna do liczby plików FLV wymaganych dla domeny problemowej i liczby zestawów członkostwa w tych plikach FLV. Za każdym razem, gdy zamazany system przechodzi przez zestaw reguł, łączy konsekwencje, które zostały wystrzelone, i defragmentuje wynik, aby dać wyraźną wartość. Więcej informacji na ten temat za chwilę, ale najpierw, zanim zagłębimy się głębiej, zaprojektujmy kilka FLV, których możemy użyć, aby rozwiązać rzeczywisty problem. Biorąc pod uwagę praktyczny przykład, w który można zatopić zęby, jestem pewien, że łatwiej będzie ci zobaczyć, jak wszystkie te rzeczy działają razem.

Projektowanie FLV do selekcji broni

Ponieważ zasady, którymi gracz decyduje się na zmianę broni, można łatwo opisać przy użyciu terminów językowych, dobór broni jest dobrym kandydatem do zastosowania logiki rozmytej. Zobaczmy, jak ten pomysł można zastosować do Ravena. Aby uprościć ten przykład, powiemy, że celowość wyboru konkretnej broni z ekwipunku zależy od dwóch czynników: odległości do celu i ilości amunicji. Każda klasa broni posiada instancję rozmytego modułu, a każdy moduł jest inicjalizowany za pomocą plików FLV reprezentujących terminy lingwistyczne Odległość do celu, Status amunicji (poprzednicy) i Celowość (konsekwencja), a także zasady dotyczące tej broni. Reguły określają, jak pożądana jest ta broń w danym scenariuszu, umożliwiając botowi wybranie broni o najwyższej wartości pożądanej jako bieżącej broni. Odległość FLV do celu i celowość są definiowane identycznie dla każdego rodzaju broni. Status amunicji i zestaw reguł są tworzone na zamówienie. Przykłady podane w tym rozdziale skupią się na projektowaniu FLV i zestawu reguł dla wyrzutni rakiet.

Projektowanie wymgalności FLV

Zaczniemy od zaprojektowania rozmytej zmiennej językowej wymaganej do oznaczenia następującego zbioru: Celowość. Istnieje kilka ważnych wskazówek, których należy przestrzegać przy projektowaniu FLV. Są to:

•  Dla każdej linii pionowej (reprezentującej wartość wejściową) przeciągniętej przez FLV, suma DOM w każdym z rozmytych zbiorów, z którymi się przecina, powinna wynosić około 1. Zapewnia to płynne przejście między wartościami przez rozmytą rozmaitość FLV (połączone kształt wszystkich zestawów członkowskich).
•  Dla każdej linii pionowej poprowadzonej przez FLV powinna przecinać się tylko z dwoma lub mniej rozmytymi zestawami.

FLV, który łamie pierwszą wytyczną, pokazano na rysunku A, a FLV, który łamie drugą wytyczną, pokazano na rysunku B.



Wymagalność FLV jest wymagana do reprezentowania dziedziny wszystkich wyników od 0 do 100. Dlatego zestawy elementów muszą odpowiednio obejmować ten zakres (przy jednoczesnym przestrzeganiu wytycznych). Wybrałem użycie trzech zestawów elementów: zestawu z lewym ramieniem, zestawu z trójkątem i zestawu z prawym ramieniem, reprezentujących terminy lingwistyczne Niepożądane, pożądane i bardzo pożądane, jak pokazano tu



Projektowanie odległości do docelowego FLV

Następnie rozważymy poprzednik: Odległość do celu. Po raz kolejny FLV składa się z trzech zestawów o nazwach Target_Close, Target_Medium i Target_Far. Te trzy warunki są całkowicie odpowiednie, aby umożliwić ekspertowi (czyli nam, ludziom) określenie zasad wyboru broni. Kiedy gram w grę, myślę, że termin "bliski" oznacza prawie obok mnie - w takim zakresie, w którym możesz rozważyć walkę wręcz. Dlatego ustawiłem pik rozmytego zestawu Target_Close w odległości 25 pikseli, co wydaje mi się właściwe, biorąc pod uwagę skalę typowej mapy Ravena (bot ma promień ograniczający około 10 pikseli). Wybrałem użycie 150 pikseli jako piku dla Target_Medium, ponieważ wydaje mi się to słuszne, i postanowiłem uczynić z Target_Far kształt ramienia, który osiąga wartość szczytową przy 300, a następnie płaskowyż do 400. Zauważ, że nie martwię się zbytnio o konkretne wartości; Po prostu używam wartości, które "wydają się" prawidłowe. Odległość do celu pokazano na rysunku

.

Projektowanie stanu amunicji FLV

Na koniec zajmiemy się statusem amunicji, który będzie wykorzystywał zestawy rozmyte Ammo_Low, Ammo_Okay i Ammo_Loads. Ponieważ terminy lingwistyczne są zdefiniowane w kontekście (ponieważ to, co możesz uznać za dobrą ilość amunicji, powiedzmy, granatnika, raczej nie będzie odpowiednią ilością dla karabinu maszynowego), ten FLV różni się w zależności od broni. Wyrzutnia rakiet jest w stanie wystrzelić dwie rakiety na sekundę, więc powiedziałbym, że dobra ilość amunicji to około 10 rakiet. Niosąc około 30 rakiet, uważam, że mam mnóstwo amunicji, a wszystko poniżej 10 jest niskie. Mając to na uwadze, zaprojektowałem Stan amunicji, jak pokazano na rysunku



Jak widać, projektowanie FLV jest głównie zdrowym rozsądkiem: po prostu zbadaj i przetłumacz swoją wiedzę, a nawet lepiej, wiedzę eksperta o domenie.

Projektowanie zestawu reguł do wyboru broni

Teraz, gdy mamy do czynienia z rozmytymi terminami, przejdźmy do zasad. Aby uwzględnić wszystkie możliwości, należy utworzyć regułę dla każdej możliwej kombinacji zbiorów poprzedników. Każdy z rodzajów amunicji FLV i dystansu do celu zawiera trzy zestawy elementów, więc aby uwzględnić każdą kombinację, należy zdefiniować dziewięć zasad. Po raz kolejny wcielę się w rolę eksperta. W mojej opinii "eksperta" wyrzutnia rakiet jest świetną bronią na średnim dystansie, ale używanie jej z bliska jest niebezpieczne, ponieważ grozi jej uszkodzenie przez wybuch. Ponadto, ponieważ rakiety poruszają się powoli, jest to zły wybór broni, gdy cel jest daleko, ponieważ rakiety można łatwo uniknąć. Mając na uwadze te fakty, oto dziewięć zasad, które stworzyłem w celu określenia celowość użycia wyrzutni rakiet:

Zasada 1. JEŚLI Target_Far ORAZ ŁADUNKI amunicji NIŻ Pożądane
Zasada 2. JEŚLI Target_DALNIE ORAZ Amunicja jest OK TO Niepożądane
Zasada 3. JEŚLI Target_Dalej I Amunicja_NISKIE TO Niepożądane
Zasada 4. JEŚLI Target_Medium ORAZ ŁADUNKI amunicji WTEDY Bardzo pożądane
Zasada 5. JEŚLI Target_Medium ORAZ amunicja jest OK, to bardzo pożądane
Zasada 6. JEŚLI Target_Medium I Ammo_LOW THEN Pożądane
Zasada 7. JEŚLI Target_Close AND Ammo_Loads THEN Niepożądane
Reguła 8. JEŚLI Target_Close AND Ammo_Okay THEN Niepożądane
Zasada 9. JEŚLI Target_Close AND Ammo_LOW THEN Niepożądane

Pamiętaj, że te zasady są tylko moją opinią i odzwierciedlają mój poziom doświadczenia w grze. Podczas projektowania reguł dla własnej gry skonsultuj się z najlepszym graczem w zespole programistów, ponieważ im bardziej ekspert, z którego wywodzisz reguły, tym lepsza będzie Twoja sztuczna inteligencja. Ma to sens w ten sam sposób, w jaki Michael Schumacher będzie w stanie opisać znacznie lepszy zestaw zasad prowadzenia samochodu wyścigowego Formuły 1 niż ty lub ja.

Wnioskowanie rozmyte

Nadszedł czas, aby przestudiować procedurę wnioskowania rozmytego. To tam przedstawimy systemowi niektóre wartości, aby zobaczyć, które reguły odpalają i do jakiego stopnia. Wnioskowanie rozmyte przebiega następująco:

1. Dla każdej reguły
1a. Dla każdego poprzednika obliczyć stopień członkostwa w danej wejściowej.
1b. Oblicz wywnioskowaną regułę na podstawie wartości określone w 1a.
2. Połącz wszystkie wywnioskowane wnioski w jeden wniosek (zbiór rozmytych).
3. Aby uzyskać wyraźne wartości, wnioski z 2 muszą być zszokowane.

Przeanalizujmy teraz te kroki, wykorzystując niektóre z zasad, które stworzyliśmy dla wyboru broni, i kilka wyraźnych wartości wejściowych. Załóżmy, że cel znajduje się w odległości 200 pikseli, a pozostała ilość amunicji to 8 rakiet. Jedna reguła narz

Zasada pierwsza

JEŚLI Target_Far AND Ammo_Loads THEN Pożądane
Stopień przynależności wartości 200 do zestawu Target_Far wynosi 0,33. Stopień przynależności do wartości 8 w zestawie Ammo_Loads wynosi 0. Operator AND powoduje uzyskanie minimum tych wartości, więc pożądany wniosek dla reguły 1 jest pożądany = 0. Innymi słowy, reguła nie jest uruchamiana. Rysunek 10.15 pokazuje tę regułę graficznie.



Zasada druga

IF Target_Far AND Ammo_Okay THEN Niepożądane
Dla drugiej reguły stopień przynależności wartości 200 do zestawu Target_Far wynosi 0,33. Stopień członkostwa o wartości 8 w zestawie Ammo_Okay wynosi 0,78. Wnioskowany wniosek dla reguły 2 jest zatem niepożądany = 0,33.



Zasada trzecia

IF Target_Far AND Ammo_Low THEN Niepożądane
Stosując te same wartości do trzeciej reguły, stopień przynależności wartości 200 do zestawu Target_Far wynosi 0,33. Stopień członkostwa o wartości 8 w zestawie Ammo_Low wynosi 0,2. Wnioskowany wniosek dla reguły 3 jest zatem niepożądany = 0,2.



Jestem pewien, że masz już sedno tego, więc aby zaoszczędzić wiele powtórzeń, uzyskane wyniki dla wszystkich reguł są podsumowane przez macierz pokazaną na rysunku. (Ten typ macierzy jest znany jako rozmyta macierz asocjacyjna lub w skrócie FAM).



Zauważ, że VeryDesiable wystrzelił raz do stopnia 0,67. Pożądany strzelił raz do stopnia 0,2, a Niepożądany strzelił dwa razy ze stopniem 0,2 i 0,33. Jednym ze sposobów myślenia o tych wartościach jest poziom zaufania. Biorąc pod uwagę dane wejściowe, reguły rozmyte wywnioskowały wynik Bardzo pożądany z pewnością 0,67, a wynik Pożądany z pewnością 0,2. Ale jakie wnioski można wyciągnąć z Niepożądanego, który strzelił dwa razy? Istnieje kilka sposobów postępowania z wieloma zwierzeniami. Dwie najpopularniejsze to suma ograniczona (suma i wartość 1) oraz wartość maksymalna (odpowiednik ORing razem ufności). Nie ma wielkiego znaczenia, którą metodę wybierzesz. Wolę OR wartości razem, co w tym przykładzie daje pewność niepożądanemu z 0,33. Podsumowując, w Tabeli wymieniono wnioski wynikające z zastosowania wartości odległości do celu = 200 i stanu amunicji = 8 do wszystkich reguł.



Te wyniki pokazano graficznie na rysunku. Zwróć uwagę, w jaki sposób funkcja członkostwa każdego następcy jest przycinana do poziomu pewności.



Następnym krokiem jest połączenie uzyskanych wyników w jeden rozmyty kolektor.



Teraz, gdy mamy złożony zestaw rozmytych reprezentujących wnioskowanie wszystkich reguł w bazie reguł, nadszedł czas, aby odwrócić proces i przekształcić ten zestaw danych wyjściowych w jedną wyraźną wartość. Osiąga się to poprzez proces zwany defuzzification.

Defuzzification

Defuzzification jest odwrotnością fuzzification: proces odwracania rozmycia ustawiony na wyraźną wartość. Można to zrobić na wiele sposobów, a następne kilka stron zostanie poświęconych na sprawdzenie najczęściej używanych.

Mean of Maximum (MOM) Średnia maksimum - MOM w skrócie - metoda defuzzification oblicza średnią z tych wartości wyjściowych, które mają najwyższy stopień ufności. Rysunek pokazuje, jak można zastosować tę technikę do określenia wyraźnej wartości z rozkładu wyjściowego dla obliczonej wcześniej celowości.



Jednym z problemów związanych z tą metodą jest to, że nie bierze pod uwagę tych zbiorów wyjściowych, które nie mają ufności równej najwyższej (jak te pokazane na rysunku kolorem szarym), które mogą przesunąć wynikową wyraźną wartość na jeden koniec domena. Dokładniejsze metody fuzyfikacji, takie jak te wymienione poniżej, rozwiązują ten problem.

Centroid

Metoda centroid jest najdokładniejsza, ale jest również najbardziej złożona do obliczenia. Działa poprzez określenie środka masy zbiorów wyjściowych. Jeśli wyobrażasz sobie każdy zestaw elementów zestawu wyjściowego wycięty z kartonu, a następnie sklejony ze sobą, tworząc kształt rozmytego kolektora, środek masy jest pozycją, w której wynikowy kształt byłby zrównoważony, gdyby został umieszczony na linijce.



Środek ciężkości rozmytego kolektora oblicza się, dzieląc kolektor na punkty próbkowania i obliczając sumę udziału DOM w każdym punkcie próbki w sumie, podzieloną przez sumę DOMs próbek. Wzór podano w (10.11).



gdzie s jest wartością w każdym punkcie próbki, a DOM (s) to stopień członkostwo w FLV o tej wartości. Im więcej punktów próbki zostanie wybranych do wykonania obliczeń, tym dokładniejszy będzie wynik, chociaż w praktyce zwykle wystarcza 10 do 20 próbek. Teraz zdaję sobie sprawę, że niektórzy z was mogą mieć kołatanie serca w tym momencie, więc prawdopodobnie najlepiej będzie, jeśli wyjaśnię na przykładzie. Rozmyślimy rozmytą rozmaitość wynikającą z uruchomienia zasad wyboru broni przy użyciu 10 przykładowych punktów pokazanych na rysunku



Dla każdego punktu próbki obliczany jest DOM w każdym zestawie elementów. Tabela podsumowuje wyniki. (Uwaga: Wartości podane dla próbek w 30 i 70 są nieprecyzyjne, ponieważ są po prostu oszacowane na powyższym rysunku, ale są wystarczająco dokładne dla tego pokazu.)



Teraz podłącz liczby do równania (10.11). Najpierw obliczmy licznik (część równania powyżej linii).

A teraz mianownik (część poniżej linii):

0,33 + 0,33 + 0,53 + 0,53 + 0,2 + 0,6 + 0,87 + 0,67 + 0,67 + 0,67 = 5,4 (10,13)

Dzielenie licznika przez mianownik daje wyraźną wartość:

Celowość = 334,8 / 5,4 = 62 (10,14)

Average of Maxima (MaxAv)

Maksymalna lub reprezentatywna wartość rozmytego zbioru to wartość, w której członkostwo w tym zestawie wynosi 1. W przypadku zbiorów trójkątnych jest to po prostu wartość w punkcie środkowym; dla zestawów zawierających płaskowyże - takich jak prawe ramię, lewe ramię i zestawy trapezowe - ta wartość jest średnią wartości na początku i na końcu płaskowyżu. Średnia z maksimów (w skrócie MaxAv) metoda defuzzyfikacji skaluje reprezentatywną wartość każdego następstwa według jego pewności i przyjmuje średnią, jak poniżej:



Reprezentatywne wartości zbiorów zawierających kolektor wyjściowy zestawiono w tabeli 10.3.

Podłączenie tych wartości do równania daje celowość jako wyraźną wartość:

Celowość = 12,5 x 0,33 x 50 x 0,2 x 87,5 x 0,67 / 0,33 + 0,2 + 0,67 = 72,75 / 1,2 = 60,625 (10,16)

Jak widać, metoda ta wygenerowała wartość bardzo zbliżoną do obliczonej przez dokładniejszą, ale bardziej kosztowną metodę obliczania centroidów (i byłby bliżej, gdybym nie oszacował niektórych wartości w obliczeniach centroidów), a zatem to ten, który radzę używać w swoich grach i aplikacjach. Cóż, to wszystko! Przeszliśmy z wyraźnych wartości (odległość do celu = 200, stan amunicji = 8) do rozmytych zestawów, wnioskowania i z powrotem do wyraźnej wartości reprezentującej celowość korzystania z wyrzutni rakiet (83, 62 lub 60.625 w zależności od metody defuzzyfikacji). Jeśli ten proces powtarza się dla każdego rodzaju broni, którą nosi bot, łatwo jest wybrać tę, która ma najwyższy wynik pożądania, jako broń, której bot powinien użyć w obecnej sytuacji.

Od teorii do zastosowania: kodowanie modułu logiki rozmytej

Nadszedł czas, aby dokładnie zobaczyć, jak zostały zaprojektowane klasy wymagane do implementacji logiki rozmytej i jak są one zintegrowane z Raven.

Klasa FuzzyModule

Klasa FuzzyModule jest sercem systemu rozmytego. Zawiera std :: mapa rozmytych zmiennych językowych i wektor std :: zawierający podstawę reguły. Ponadto ma metody dodawania plików FLV i reguł do modułu oraz przeprowadzania modułu przez proces fuzzification, wnioskowania i defuzzification.

class FuzzyModule
{
private:
typedef std::map VarMap;
public:
// klient musi przekazać jedną z tych wartości do metody defuzzify.
// Ten moduł obsługuje tylko metody MaxAv i centroid.
enum DefuzzifyType{max_av, centroid};
// przy obliczaniu środka masy rozmytego kolektora ta wartość jest używana
// aby określić, ile przekrojów należy próbkować
enum {NumSamplesToUseForCentroid = 15};
private:
// mapa wszystkich rozmytych zmiennych używanych przez ten moduł
VarMap m_Variables;
// wektor zawierający wszystkie reguły rozmyte
std::vector m_Rules;
// zeruje DOM z konsekwencji każdej reguły. Używany przez Defuzzify()
inline void SetConfidencesOfConsequentsToZero();
public:
˜FuzzyModule();
// tworzy nową "pustą" zmienną rozmytą i zwraca do niej odwołanie.
FuzzyVariable& CreateFLV(const std::string& VarName);
// dodaje regułę do modułu
void AddRule(FuzzyTerm& antecedent, FuzzyTerm& consequence);
// ta metoda wywołuje metodę Fuzzify o nazwie FLV
inline void Fuzzify(const std::string& NameOfFLV, double val);
// podana zmienna rozmyta i metoda defuzzyfikacji zwraca wartość
// wyraźna wartość
inline double DeFuzzify(const std::string& key, DefuzzifyType method);
};

Klient zazwyczaj tworzy wystąpienie tej klasy dla każdej AI, która wymaga unikalnego zestawu reguł rozmytych. Pliki FLV można następnie dodać do modułu za pomocą metody CreateFLV. Ta metoda zwraca odwołanie do nowo utworzonego pliku FLV. Oto przykład wykorzystania modułu do utworzenia rozmytych zmiennych językowych wymaganych w przykładzie wyboru broni

FuzzyModule fm;
FuzzyVariable & DistToTarget = fm.CreateFLV ("DistToTarget");
FuzzyVariable & Desirability = fm.CreateFLV ("Desirability");
FuzzyVariable & AmmoStatus = fm.CreateFLV ("AmmoStatus");

W tym momencie jednak każdy z tych plików FLV jest "pusty". Aby był użyteczny, plik FLV musi zostać zainicjowany przy użyciu niektórych zestawów elementów. Rzućmy okiem na to, jak różne typy zbiorów rozmytych są enkapsulowane.

Klasa podstawowa FuzzySet

Ponieważ konieczne jest manipulowanie zestawami rozmytymi przy użyciu wspólnego interfejsu, wszystkie typy zestawów rozmytych pochodzą z abstrakcyjnej klasy FuzzySet. Każda klasa zawiera członka danych do przechowywania stopnia członkostwa wartości, która ma być zszokowana. Konkretne FuzzySets posiadają dodatkowe dane opisujące kształt ich funkcji członkostwa.

class FuzzySet
{
protected:
//utrzyma to stopień członkostwa w tym zestawie danej wartości
double m_dDOM;
// to maksimum funkcji członkostwa w zestawie. Na przykład, jeśli
// zestaw jest trójkątny, to będzie punkt szczytowy trójkąta.
// Jeśli zestaw ma płaskowyż, wówczas ta wartość będzie punktem środkowym
//Płaskowyż. Ta wartość jest ustawiana w konstruktorze, aby uniknąć czasu wykonywania
// obliczenie wartości punktu środkowego.
double m_dRepresentativeValue;
public:
FuzzySet(double RepVal):m_dDOM(0.0), m_dRepresentativeValue(RepVal){}
// zwraca stopień członkostwa w tym zestawie podanej wartości. UWAGA:
// to nie ustawia m_dDOM na DOM wartości przekazanej jako parametr.
// Wynika to z faktu, że metoda defuzzyfikacji centroidów również korzysta z tej metody
// w celu określenia DOM wartości, których używa jako punktów próbnych.
virtual double CalculateDOM(double val)const = 0;
// jeśli ten rozmyty zestaw jest częścią wynikowego pliku FLV i jest uruchamiany przez regułę,
// następnie ta metoda ustawia DOM (w tym kontekście DOM reprezentuje
// poziom ufności) do maksymalnej wartości parametru lub zbioru
// istniejąca wartość m_dDOM
void ORwithDOM(double val);
// metody akcesora
double GetRepresentativeVal()const;
void ClearDOM(){m_dDOM = 0.0;}
double GetDOM()const{return m_dDOM;}
void SetDOM(double val);
};

Przyjrzyjmy się teraz kilku konkretnym rozmytym klasom zbiorów.

Klasa trójkątnego zestawu rozmytego

Trójkątny zbiór rozmytych jest definiowany przez trzy wartości: punkt szczytowy, lewe przesunięcie i prawe przesunięcie.



Deklaracja klasy enkapsulującej te dane jest następująca:

class FuzzySet_Triangle : public FuzzySet
{
private:
// wartości, które określają kształt tego pliku FLV
double m_dPeakPoint;
double m_dLeftOffset;
double m_dRightOffset;
public:
FuzzySet_Triangle(double mid,
double lft,
double rgt):FuzzySet(mid),
m_dPeakPoint(mid),
m_dLeftOffset(lft),
m_dRightOffset(rgt)
{}
// ta metoda oblicza stopień członkostwa dla określonej wartości
double CalculateDOM(double val)const;
};

Jak widać, jest to bardzo proste. Zwróć uwagę, w jaki sposób punkt środkowy trójkąta jest przekazywany do konstruktora klasy podstawowej jako wartość reprezentatywna dla tego kształtu. Interfejs FuzzySet definiuje tylko jedną metodę, która musi zostać zaimplementowana: CalculateDOM, metoda określająca stopień przynależności wartości do zestawu. Oto kod tej implementacji:

double FuzzySet_Triangle::CalculateDOM(double val)const
{
// test dla przypadku, gdy lewe lub prawe odsunięcie trójkąta ma wartość zero // (aby zapobiec podzieleniu przez zero błędów poniżej)
if ( (isEqual(m_dRightOffset, 0.0) && (isEqual(m_dPeakPoint, val))) ||
(isEqual(m_dLeftOffset, 0.0) && (isEqual(m_dPeakPoint, val))) )
{
return 1.0;
}
// znajdź DOM, jeśli pozostanie po środku
if ( (val <= m_dPeakPoint) && (val >= (m_dPeakPoint - m_dLeftOffset)) )
{
double grad = 1.0 / m_dLeftOffset;
return grad * (val - (m_dPeakPoint - m_dLeftOffset));
}
// znajdź DOM, jeśli jest na prawo od środka
else if ( (val > m_dPeakPoint) && (val < (m_dPeakPoint + m_dRightOffset)) )
{
double grad = 1.0 / -m_dRightOffset;
return grad * (val - m_dPeakPoint) + 1.0;
}
// poza zakresem tego FLV, zwróć zero
else
{
return 0.0;
}
}

Klasa zestawu rozmytego prawego ramienia Zestaw rozmytych prawych ramion jest również parametryzowany przez trzy wartości: punkt szczytowy, lewe przesunięcie i prawe przesunięcie.



Po raz kolejny definicja klasy jest prosta:

class FuzzySet_RightShoulder : public FuzzySet
{
private:
// wartości, które określają kształt tego pliku FLV
double m_dPeakPoint;
double m_dLeftOffset;
double m_dRightOffset;
public:
FuzzySet_RightShoulder(double peak,
double LeftOffset,
double RightOffset):
FuzzySet( ((peak + RightOffset) + peak) / 2),
m_dPeakPoint(peak),
m_dLeftOffset(LeftOffset),
m_dRightOffset(RightOffset)
{}
// ta metoda oblicza stopień członkostwa dla określonej wartości
double CalculateDOM(double val)const;
};
Tym razem reprezentatywną wartością jest punkt środkowy płaskowyżu łopatki. Metoda CalculateDOM jest również nieco inna

double FuzzySet_RightShoulder::CalculateDOM(double val)const
{
// sprawdź, czy przesunięcie może wynosić zero
if (isEqual(0, m_dLeftOffset) && isEqual(val,m_dMidPoint))
{
return 1.0;
}
//znajdź DOM, jeśli pozostanie po środkur
if ( (val <= m_dMidPoint) && (val > (m_dMidPoint - m_dLeftOffset)) )
{
double grad = 1.0 / m_dLeftOffset;
return grad * (val - (m_dMidPoint - m_dLeftOffset));
}
//znajdź DOM, jeśli jest na prawo od środka
else if (val > m_dMidPoint)
{
return 1.0;
}
// poza zakresem tego FLV, zwróć zero
else
{
return 0.0;
}
}

Znowu wszystko jest bardzo proste. Nie chcę marnować papieru, wymieniając kod dla innych zbiorów rozmytych; są równie jasne i łatwe do zrozumienia. Przejdźmy do rozmytej klasy zmiennych językowych.

Tworzenie rozmytej klasy zmiennej językowej

Klasa rozmytej zmiennej językowej FuzzyVariable zawiera std :: mapę wskaźników do instancji FuzzySets - zestawów, które składają się na jej różnorodność. Ponadto ma metody dodawania rozmytych zestawów oraz zamazywania i rozmrażania wartości. Za każdym razem, gdy zestaw elementów jest tworzony i dodawany do FLV, zakresy min / maks FLV są ponownie obliczane i przypisywane do wartości m_dMinRange i m_dMaxRange. Prowadzenie rejestru zakresu domeny FLV w ten sposób pozwala logice ustalić, czy wartość przedstawiona dla fuzyfikacji jest poza zakresem, i w razie potrzeby wyjść z twierdzeniem. Oto deklaracja klasy:

class FuzzyVariable
{
private:
typedef std::map MemberSets;
private:
// nie zezwalaj na kopie
FuzzyVariable(const FuzzyVariable&);
FuzzyVariable& operator=(const FuzzyVariable&);
private:
//mapa zbiorów rozmytych zawierających tę zmiennąe
MemberSets m_MemberSets;
// minimalna i maksymalna wartość zakresu tej zmiennej
double m_dMinRange;
double m_dMaxRange;
// ta metoda jest wywoływana z górną i dolną granicą zestawu za każdym razem, gdy
// dodano nowy zestaw w celu odpowiedniego dostosowania górnego i dolnego zakresu wartości
void AdjustRangeToFit(double min, double max);
// klient pobiera odwołanie do zmiennej rozmytej, gdy instancja jest
// stworzony za pomocą FuzzyModule :: CreateFLV (). Aby uniemożliwić klientowi usunięcie
// instancja FuzzyVariable destruktor jest prywatny i
// Klasa FuzzyModule zaprzyjaźniła się.
˜FuzzyVariable();
friend class FuzzyModule;
public:
FuzzyVariable():m_dMinRange(0.0),m_dMaxRange(0.0){}
// następujące metody tworzą instancje zestawów nazwanych w metodzie
// nazwa i dodaje je do mapy zestawu członków. Za każdym razem, gdy zestaw dowolnego typu jest
// dodano m_dMinRange i m_dMaxRange są odpowiednio dostosowane. Wszystkie zdjęcia // metody zwracają klasę proxy reprezentującą nowo utworzoną instancję. To
// zestaw proxy może być używany jako operand podczas tworzenia podstawy reguły.
FzSet AddLeftShoulderSet(std::string name,
double minBound,
double peak,
double maxBound);
FzSet AddRightShoulderSet(std::string name,
double minBound,
double peak,
double maxBound);
FzSet AddTriangularSet(std::string name,
double minBound,
double peak,
double maxBound);
FzSet AddSingletonSet(std::string name,
double minBound,
double peak,
double maxBound);
// fuzzify wartość, obliczając jej DOM w każdym podzbiorze tej zmiennej
void Fuzzify(double val);
// defuzzify zmiennej przy użyciu metody MaxAv
double DeFuzzifyMaxAv()const;
// defuzzify zmiennej przy użyciu metody centroid
double DeFuzzifyCentroid(int NumSamples)const;
};

Zauważ, że metody tworzenia i dodawania zestawów nie używają tych samych parametrów, które same wykorzystują same klasy zbiorów rozmytych. Na przykład, oprócz łańcucha reprezentującego nazwę, metoda AddLeftShoulderSet przyjmuje jako parametry minimalną granicę, punkt szczytowy i maksymalną granicę, podczas gdy klasa FuzzySet_Triangle wykorzystuje wartości określające punkt środkowy, lewe przesunięcie i prawe przesunięcie. Ma to na celu uczynienie metod bardziej instynktownymi dla klientów. Zazwyczaj podczas tworzenia plików FLV szkicujesz ich zestawy elementów na papierze (lub wyobrażasz je sobie w głowie), co znacznie ułatwia odczytywanie wartości od lewej do prawej zamiast obliczania wszystkich przesunięć. Oprzyjmy się na naszym przykładzie i dodajmy niektóre zestawy członków do DistToTarget

FuzzyModule fm;
FuzzyVariable& DistToTarget = fm.CreateFLV("DistToTarget");
FzSet Target_Close = DistToTarget.AddLeftShoulderSet("Target_Close", 0, 25, 150);
FzSet Target_Medium = DistToTarget.AddTriangularSet("Target_Medium", 25, 50, 300);
FzSet Target_Far = DistToTarget.AddRightShoulderSet("Target_Far", 150, 300, 500);

Te kilka wierszy kodu tworzy plik FLV pokazany na rysunku



Zauważ, w jaki sposób instancja FzSet jest zwracana przez każdą z metod dodawania zestawu. Jest to klasa proxy, która naśladuje funkcjonalność konkretnego FuzzySet. Konkretne same instancje są zawarte w FuzzyVariable :: m_MemberSets. Te proxy są używane jako operandy podczas konstruowania rozmytej bazy reguł.

Projektowanie klas do budowania reguł rozmytych

Jest to niewątpliwie najbardziej upiorna część kodowania rozmytego systemu. Jak się dowiedziałeś, każda rozmyta reguła ma postać:

JEŚLI poprzednia WTEDY konsekwencja

gdzie poprzednikiem i następcą mogą być pojedyncze zbiory rozmyte lub zbiory złożone, które są wynikiem operacji. Aby być elastycznym, moduł rozmyty musi być w stanie obsługiwać reguły przy użyciu nie tylko operatora AND, ale także operatorów OR i NOT oraz rozmytych żywopłotów, takich jak BARDZO i FAIRY. Innymi słowy, moduł powinien być w stanie poradzić sobie z następującymi regułami:

JEŻELI a1 i a2 NASTĘPNIE c1
JEŚLI BARDZO (a1) I (a2 LUB a3) NASTĘPNIE c1
JEŻELI [(a1 ORAZ a2) LUB (NIE (a3) ORAZ BARDZO (a4))] NASTĘPNIE [c1 AND c2]

W ramach ostatecznej reguły obserwuj, jak operator OR działa na podstawie wyniku (a1 ORAZ a2) i wyniku (NIE (a3) ORAZ BARDZO (a4)). Z kolei AND w drugim okresie działa na wynikach NOT (a3) i VERY (a4). Jeśli to nie jest wystarczająco skomplikowane, reguła ma dwa konsekwencje AND razem. Oczywiście ten przykład jest zdecydowanie przesadzony i jest bardzo mało prawdopodobne, że programiści zajmujący się sztuczną inteligencją gry będą wymagać takiej reguły (chociaż nie byłoby to niczym niezwykłym w wielu rozmytych systemach eksperckich), ale dobrze ilustruje to, co mam na myśli - każdy klasa operatora warta swojej soli musi być w stanie obsługiwać poszczególne operandy oraz ich kompozycje i operatory w identyczny sposób. To wyraźnie kolejny obszar, w którym na ratunek przychodzi złożony wzór. Powtórzmy: Ideą złożonego wzoru jest zaprojektowanie wspólnego interfejsu zarówno dla obiektów złożonych, jak i atomowych; kiedy zostanie złożony wniosek o kompozyt, przekaże go jednemu lub większej liczbie swoich dzieci (patrz rozdział 9, jeśli potrzebujesz bardziej szczegółowego wyjaśnienia złożonego wzoru). W regułach rozmytych operandy (zbiory rozmyte) są obiektami atomowymi, a operatory (AND, OR, BARDZO itd.) Są kompozytami. Dlatego wymagana jest klasa, która definiuje wspólny interfejs dla obu tych typów obiektów aby wprowadzić w życie. Ta klasa nazywa się FuzzyTerm i wygląda następująco:

class FuzzyTerm
{
public:
virtual ~FuzzyTerm(){}
// wszystkie warunki muszą implementować wirtualnego konstruktora
virtual FuzzyTerm* Clone()const = 0;
// pobiera stopień członkostwa tego terminu
virtual double GetDOM()const=0;
// wyjaśnia stopień członkostwa w tym terminie
virtual void ClearDOM()=0;
// metoda aktualizacji DOM konsekwenta po uruchomieniu reguły
virtual void ORwithDOM(double val)=0;
};

Ponieważ jakakolwiek operacja rozmyta na jednym lub kilku zestawach powoduje powstanie złożonego zbioru rozmytego, ten mały interfejs jest wystarczający do zdefiniowania obiektów do użycia w konstrukcji reguł rozmytych. Rysunek 10.27 pokazuje związek między klasą FuzzyTerm, rozmytą klasą operatorską AND FzAND (kompozyt), a obiektem proxy FzSet z zestawem rozmytym (atomowym).



Zauważ, jak obiekt FzAND może zawierać od dwóch do czterech FuzzyTerms, a gdy jedna z jego metod zostanie wywołana, iteruje się przez każdą z nich i przekazuje wywołanie odpowiedniej metodzie każdego dziecka lub używa interfejsu do obliczenia wyniku. Zauważ też, jak FzSet działa jako proxy dla obiektu FuzzySet. Klasa proxy służy do ukrywania prawdziwej klasy przed klientem; działa jako surogat prawdziwej klasy, aby kontrolować dostęp do niej. Klasy proxy przechowują odwołanie do klasy, dla której są surogatem, a gdy klient wywołuje metodę klasy proxy, przekazuje wywołanie do równoważnej metody odwołania. Za każdym razem, gdy FuzzySet jest dodawany do FuzzyVariable, klient otrzymuje do niego serwer proxy w postaci FzSet. Ten serwer proxy może zostać skopiowany i użyty wiele razy przy tworzeniu bazy reguł. Bez względu na to, ile razy jest kopiowany, zawsze zastępuje ten sam obiekt, co znacznie upraszcza projekt, ponieważ nie musimy się martwić śledzeniem kopii FuzzySets podczas tworzenia reguł. Korzystając z tego projektu dla wszystkich operatorów i operandów, można stworzyć bardzo przyjazny interfejs do tworzenia rozmytych reguł. Klienci używają składni w następujący sposób, aby dodać reguły:

fm.AddRule (FzAND (Target_Far, Ammo_Low), niepożądane);
Nawet złożony termin pokazany wcześniej jest łatwy do zbudowania:
fm.AddRule (FzOR (FzAND (a1, a2), FzAND (FzNOT (a3), FzVery (a4))), FzAND (c1, c2));
Aby to lepiej zrozumieć, zagłębimy się w treść metody AddRule. Oto implementacja:
void FuzzyModule :: AddRule (FuzzyTerm i poprzednik, FuzzyTerm i konsekwencja)
{
m_Rules.push_back (nowy FuzzyRule (poprzednik, konsekwencja));
}

Jak widać, cała ta metoda polega na utworzeniu lokalnej kopii klasy FuzzyRule. Reguła FuzzyRule zawiera instancję FuzzyTerm oznaczającą poprzednika i kolejną oznaczającą konsekwencję. Te instancje są kopiami FuzzyTerms używanych do konstruowania FuzzyRule. Jest to jeden z powodów, dla których każda podklasa FuzzyTerm musi implementować metodę klonowania wirtualnego konstruktora. Oto lista, dzięki czemu możesz dokładnie zobaczyć, co się dzieje.

class FuzzyRule
{
private:
// poprzednik (zwykle złożony z kilku rozmytych zbiorów i operatorów)
const FuzzyTerm* m_pAntecedent;
// konsekwencja (zwykle pojedynczy zestaw rozmytych, ale może być kilka AND razem)
FuzzyTerm* m_pConsequence;
// nie ma sensu zezwalać klientom na kopiowanie reguł
FuzzyRule(const FuzzyRule&);
FuzzyRule& operator=(const FuzzyRule&);
public:
FuzzyRule(FuzzyTerm& ant,
FuzzyTerm& con):m_pAntecedent(ant.Clone()),
m_pConsequence(con.Clone())
{}
˜FuzzyRule(){delete m_pAntecedent; delete m_pConsequence;}
void SetConfidenceOfConsequentToZero(){m_pConsequence->ClearDOM();} // ta metoda aktualizuje DOM (zaufanie) następującego terminu o
// DOM poprzedzającego terminu.
void Calculate()
{
m_pConsequence->ORwithDOM(m_pAntecedent->GetDOM());
}
};

Okay, myślę, że to wystarczający komentarz do projektu klas używanych do tworzenia i wykonywania rozmytych reguł. Jeśli chcesz zagłębić się w trzewia, radzę sprawdzić implementację klas FzAND, FzOR, FzVery i FzFairly, które można znaleźć w folderze common / fuzzy. Diagram UML na rysunku 10.28 pomoże ci również zrozumieć, w jaki sposób wszystkie obiekty używane przez moduł rozmytych są ze sobą powiązane. Kontynuując kod pokazany wcześniej w tej sekcji, oto jak można dodać zasadę dla wyrzutni rakiet do modułu rozmytego:

/ * najpierw zainicjuj rozmyty moduł za pomocą plików FLV * /
/ * teraz dodaj zestaw reguł * /
fm.AddRule(FzAND(Target_Close, Ammo_Loads), Undesirable);
fm.AddRule(FzAND(Target_Close, Ammo_Okay), Undesirable);
fm.AddRule(FzAND(Target_Close, Ammo_Low), Undesirable);
fm.AddRule(FzAND(Target_Medium, Ammo_Loads), VeryDesirable);
fm.AddRule(FzAND(Target_Medium, Ammo_Okay), VeryDesirable);
fm.AddRule(FzAND(Target_Medium, Ammo_Low), Desirable);
fm.AddRule(FzAND(Target_Far, Ammo_Loads), Desirable);
fm.AddRule(FzAND(Target_Far, Ammo_Okay), Desirable);
fm.AddRule(FzAND(Target_Far, Ammo_Low), Undesirable);

Po zainicjowaniu modułu FuzzyModule wprowadzanie wartości i obliczenie wyraźnych wniosków jest bezbolesne. Oto metoda, która właśnie to robi:

double CalculateDesirability(FuzzyModule& fm, double dist, double ammo)
{
// fuzzify wejścia
fm.Fuzzify("DistToTarget", dist);
fm.Fuzzify("AmmoStatus", ammo);
// ta metoda automatycznie przetwarza reguły i odblokowuje
// wywnioskowany wniosek
return fm.DeFuzzify("Desirability", FuzzyModule::max_av);
}

Po wywołaniu metody DeFuzzify reguły są przetwarzane i wywnioskowany wniosek rozbity na wyraźną wartość. Oto metoda twojego przejrzenia:

inline double
FuzzyModule::DeFuzzify(const std::string& NameOfFLV, DefuzzifyMethod method)
{
// najpierw upewnij się, że nazwa FLV istnieje w tym module
assert ( (m_Variables.find(NameOfFLV) != m_Variables.end()) &&
":key not found");
// wyczyść DOM ze wszystkich następstw
SetConfidencesOfConsequentsToZero();
// przetwarza zasady
std::vector::iterator curRule = m_Rules.begin();
for (curRule; curRule != m_Rules.end(); ++curRule)
{
(*curRule)->Calculate();
}
// teraz odtajmy wynikowy wniosek przy użyciu określonej metody
switch (method)
{
case centroid:
return m_Variables[NameOfFLV]->DeFuzzifyCentroid(NumSamples);
case max_av:
return m_Variables[NameOfFLV]->DeFuzzifyMaxAv();
}
return 0;
}

Jak Raven używa klas logiki rozmytej

Każda broń Kruka posiada instancję rozmytego modułu, który jest inicjowany FLV i regułami specyficznymi dla broni. Wszystkie bronie pochodzą z abstrakcyjnej klasy bazowej Raven_Weapon i implementują metodę GetDesirability, która aktualizuje moduł rozmytych i zwraca wyraźny wynik pożądania. Oto odpowiednie części Raven_Weapon:

class Raven_Weapon
{
protected:
FuzzyModule m_FuzzyModule;
/* DODATKOWE SZCZEGÓŁY POMINIĘTE */
public:
virtual double GetDesirability(double DistToTarget)=0;
/* DODATKOWE SZCZEGÓŁY POMINIĘTE */
};

Co kilka cykli aktualizacji (domyślnie dwa razy na sekundę) boty sprawdzają każdą z broni w ekwipunku, aby ustalić, która z nich jest najbardziej pożądana, biorąc pod uwagę odległość do celu bota i pozostałej amunicji, i wybiera tę o najwyższej celowości wynik. Kod implementujący tę logikę wymieniono poniżej.

void Raven_Bot::SelectWeapon()
{
// wystarczy uruchomić ten kod, tylko jeśli obecny jest cel
if (m_pTargSys->isTargetPresent())
{
// obliczyć odległość do celu
double DistToTarget = Vec2DDistance(Pos(), m_pTargSys->GetTarget()->Pos());
// dla każdej broni w ekwipunku oblicz jej celowość biorąc pod uwagę
//obecna sytuacja. Wybrano najbardziej pożądaną broń
double BestSoFar = MinDouble;
std::vector::const_iterator curWeap;
for (curWeap = m_Weapons.begin(); curWeap != m_Weapons.end(); ++curWeap)
{
// chwycić celowość tej broni (celowość opiera się na
// odległość do celu i pozostała amunicja)
double score = (*curWeap)->GetDesirability(DistToTarget);
// jeśli jest to jak dotąd najbardziej pożądane, wybierz go
if (score > BestSoFar)
{
BestSoFar = score;
// umieść broń w ręce bota.
m_pCurrentWeapon = *curWeap;
}
}
}
}

Metoda grzebienia

Jednym z głównych problemów z rozmytymi systemami wnioskowania jest to, że wraz ze wzrostem złożoności problemu liczba wymaganych reguł rośnie w zastraszającym tempie. Na przykład prosty moduł stworzony do rozwiązania problemu wyboru broni wymagał tylko dziewięciu reguł - po jednej dla każdej możliwej kombinacji poprzednich zestawów - ale jeśli dodamy jeszcze jeden FLV, ponownie składający się z trzech zestawów elementów, wówczas 27 reguł jest niezbędnych. Jest znacznie gorzej, jeśli liczba zestawów elementów w każdym pliku FLV musi zostać zwiększona, aby uzyskać większą precyzję. Na przykład 125 reguł jest wymaganych dla systemu z trzema plikami FLV, z których każdy zawiera pięć zestawów elementów. Dodaj kolejny plik FLV z pięciu zestawów członków i liczba gwałtownie wzrasta do 625 zasad! Ten efekt jest znany jako eksplozja kombinatoryczna i stanowi ogromny problem przy projektowaniu rozmytych systemów do krytycznych aplikacji, którymi są oczywiście gry komputerowe. Na szczęście dla nas mamy rycerza w lśniącej zbroi w postaci Williama Combsa, inżyniera z Boeingiem. W 1997 roku Combs zaproponował system, który umożliwia liniowy wzrost liczby reguł z liczbą zestawów elementów zamiast wykładniczo. Tabela 10.4 pokazuje liczbę reguł wymaganych przy użyciu metody tradycyjnej w porównaniu z metodą grzebieniową (zakładając, że każdy plik FLV zawiera pięć zestawów elementów).



Duża różnica, jestem pewien, że się zgodzisz! Teoria leżąca u podstaw metody Combs działa na zasadzie, że:

JEŚLI Target_Far AND Ammo_Loads THEN Pożądane
jest logicznie równoważne z:
JEŚLI Target_Far THEN Pożądany
LUB
JEŻELI Ammo_Load się THEB Pożądany

Korzystając z tej zasady, można zdefiniować podstawę reguły, która zawiera tylko jedną regułę na kolejny zestaw elementów. Na przykład dziewięć podanych wcześniej zasad dotyczących wyrzutni rakiet:

Zasada 1. JEŚLI Target_Far ORAZ ŁADUNKI amunicji NIŻ Pożądane
Zasada 2. JEŚLI Target_DALNIE ORAZ Amunicja jest OK TO Niepożądane
Zasada 3. JEŚLI Target_Dalej I Amunicja_NISKIE TO Niepożądane
Zasada 4. JEŚLI Target_Medium ORAZ ŁADUNKI amunicji WTEDY Bardzo pożądane
Zasada 5. JEŚLI Target_Medium ORAZ amunicja jest OK, to bardzo pożądane
Zasada 6. JEŚLI Target_Medium I Ammo_LOW THEN Pożądane
Zasada 7. JEŚLI Target_Close AND Ammo_Loads THEN Niepożądane
Reguła 8. JEŚLI Target_Close AND Ammo_Okay THEN Niepożądane
Zasada 9. JEŚLI Target_Close AND Ammo_LOW THEN Niepożądane

Można sprowadzić do sześciu zasad:
Zasada 1. JEŚLI Target_Close THEN Niepożądane
Zasada 2. JEŚLI Target_MEDIUM NASTĘPNIE Bardzo pożądane
Zasada 3. JEŚLI Target_DALNIE TO Niepożądane
Zasada 4. JEŚLI Amunicja_NISKIE TO Niepożądana
Zasada 5. JEŚLI Amunicja jest OK POTEM
Zasada 6. JEŚLI Amunicja ładuje się wtedy bardzo pożądane

To nie jest duża redukcja kursu, ale jak zauważyłeś w Tabeli 10.4, metoda Combs staje się coraz bardziej atrakcyjną alternatywą wraz ze wzrostem liczby zestawów elementów używanych przez zmienne lingwistyczne. Jedną z wad tej metody jest to, że zmiany w regule to podstawa wymagana do przyjęcia logiki nie jest intuicyjna. Combs podaje dobry przykład w swoim artykule "Metoda grzebienia do szybkiego wnioskowania": Kiedy dostałem moje pierwsze prawo jazdy, mój agent ubezpieczeniowy przypomniał mi, że skoro miałem szesnaście lat ORAZ singla ORAZ singla, moja składka ubezpieczeniowa byłaby wysoka. Później, po studiach, powiedział, że ponieważ miałem około dwudziestki i byłam mężczyzną ORAZ żonaty, moja składka ubezpieczeniowa byłaby umiarkowanie niska. To ostatnie stwierdzenie wydaje się mieć bardziej intuicyjny sens niż nasz alternatywny format: ponieważ miałem około dwudziestki, moja składka ubezpieczeniowa byłaby umiarkowanie niska, LUB ponieważ byłem mężczyzną, moja składka ubezpieczeniowa byłaby umiarkowanie wysoka, LUB odkąd byłem żonaty, moja składka ubezpieczeniowa byłaby niska. Żaden agent, który nie chce zamknąć sprzedaży, nie wypowie tak pozornie sprzecznego oświadczenia. Jednym z problemów związanych z [tą metodą] jest to, że transformacja z [(p i q) następnie r] do [(p następnie r) lub (q następnie r)] przesuwa nasze skupienie z jednej reguły na coś, co wydaje się być unią dwóch (lub więcej) zasad. Ponadto, ponieważ każda z tych alternatywnych reguł może zawierać różne konsekwencje, wydaje się, że są ze sobą sprzeczne: albo moja premia jest wysoka LUB moja premia jest niska. Jak to może być jedno i drugie? Dla wielu z was przeciwdziałanie tej metodzie może być przeszkodą, ale jeśli tak, to wytrwaj - zdecydowanie warto spróbować, jeśli pracujesz z dużymi zasadami.