Exemplu de creare a sistemului expert
Update: Deoarece tema cu un "sistem expert care identifica cea mai potrivita tema de sistem expert in functie de personalitatea studentului" era un pic neclara, am optat pentru "sistem expert care identifica cea mai potrivita rasa de pisici pentru o familie care doreste sa achizitioneze o pisica".
Consideram ca vrem sa creem un sistem expert care identifica cea mai potrivita rasa de pisici pentru o familie care doreste sa achizitioneze o pisica.
-
Etapa de documentare. Stabilirea solutiilor posibile.
In cazul acestui sistem expert, cand caut solutiile incerc sa gasesc solutii care au suficiente caracteristici comune (de exemplu, au aceeasi culoare caracteristica, acelasi tip de blana, necesita acelasi grad de ingrijire) dar sunt si suficient de diferite (pot gasi macar 2-3 atribute prin care o solutie sa se diferentieze de ale solutii, ca sa imi dau seama de ramificari).
Astfel, pentru acest sistem expert am stabilit solutiile din lista de mai jos, cu niste idei de atribute in paranteza (care imi vor fi utile in etapa a doua). Atributele din dreptul fiecarei solutii nu sunt neaparat atributele finale(sunt doar schematice) si nici nu reprezinta totalitatea atributelor pe care le voi stabili pentru reguli. Aceste atribute au fost puse doar ca sa gasesc usor acele solutii care au si atribute comune, dar si suficiente caracteristici diferite. De asemenea vor fi notite legate de cazul clar in care recomandam cate o solutie.
- Rasa europeana (comuna). (frecventa: intalnita_des, colorit:divers, ingrijire_speciala: nu, talie:medie, lungime_blana:scurta etc. poate fi recomandata pentru buget mic)
- Rasa Sphinx. (daca cineva sufera de o alergie la parul de pisica -> pot recomanda pisica fara blana, ingrijire_speciala: da, tale medie, greutate mare, aspect controversat etc.)
- Rasa Persana (lungime_blana: lunga, ingrijire_speciala: da (38-40% sufera de PKD mostenita genetic), talie medie, picioare scurte, aspect pufos, rasa nobila (castigatoare frecvente la concursurile de frumuete felina)
- Rasa RagaMuffin (lungime_blana: medie, talie:mare, aspect pufos, loiala, ingrijire_speciala: nu (nu au probleme genetice), foarte frumoase, fire calma)
- Rasa siameza (traieste intre 15-20 ani, culoare: alba cu pete in extremitati, talie:mica-medie, picioare lungi, fire: temperamentala)
- Rasa Japanese Bobtail (lungime_blana: scurta, par scurt, culoare variata, talie medie, picioare lungi, fire:afectuoasa, buna pentru familii, naparlire: medie-mica)
- Rasa Maine Coon (talie: mare, tara de origine: SUA, fire: afectuoasa, ingrijire_speciala: da (boli de inima frecvente), lungime_blana: medie-lunga, intretinere dificila )
- si alte rase de pisici care nu au mai fost trecute aici pentru brevitate
Concluzii pentru urmatoarea etapa: anumite atribute posibile enuntate mai sus ar trebui deduse, cum ar fi tematica (multi studenti nu sunt decisi daca vor o anume tematica sau nu). Sau eventual in functie de raspuns sa le dau o serie de tematici de ales sau alta, ca sa nu le dau prea multe variante de raspuns pentru o intrebare (Ar fi obositor).
Exemplu de descriere pentru o solutie + imagine
Descrieriile trebuie sa fie scurte si la obiect, pentru a nu pierde prea mult timp cu ele, de exemplu:
Rasa Europeana. Este pisica pe care o intalnim cel mai des. Lipsita de boli genetice specifie rasei, si facil de procurat (gasiti usor pui pe strada, pe site-urile de adoptii). Chiar faceti o fapta foarte buna luand acasa o astfel de pisica, care altfel ar ajunge sa-si duca traiul pe strazi.
-
Etapa de gasire a atributelor. Crearea arborelui de dependente.
In cadrul acestei etape trebuie realizate urmatoarele:
- denumirea atributului scop
- stabilirea atributelor (numele lor si valorile lor). Se vor enumera intr-o lista toate atributele. In dreptul fiecarui atribut se va scrie daca e dedus sau obtinut de la utilizator. De asemenea se vor enumera valorile posibile (valorile nu_stiu si nu_conteaza cerute in enuntul sistemului expert nu se enumera aici). E posibil ca la scrierea regulilor sa va dati seama ca aveti nevoie de atribute noi sau vreti sa le inlocuiti pe cele gasite deja - acest lucru este ok, insa daca analiza voastra a fost bine facuta, multimea atributelor se va schimba poate in proportie de maxim 5-10%
- Pentru atributele deduse se vor mentiona atributele de care acestea depind. Acest lucru se poate face si cu ajutorul unor arbori.
- Se vor realiza pentru 5-7 solutii arborii de dependente:
- un arbore pentru o solutie care depinde doar de atribute cu valori date de utilizator (deci nu deduse), rezultand astfel un arbore de inaltime=2: exemplu
- un arbore pentru o solutie care depinde si de atribute care sunt deduse din alta regula (insa in aceasta regula atributele sa fie toate cu valori obtinute de la utilizator), deci vom avea un arbore de inaltime=3: exemplu
- un arbore pentru o solutie care depinde si de atribute care sunt deduse din alta regula (iar acestea depind la randul lor de atribute deduse). Deci arbore cu inaltime≥3: exemplu
Pentru a realiza acest lucru, trebuie sa ne gandim la descrierea solutiilor identificate.
Atribut scop: tema_sistem_expert
Atribute deduse:
- tema_principala: biologie, comert, psihologie, sport, muzica, distractie
Depinde de: interesat_de_natura, empatic, sociabil, grad_dificultate_documentare, mod_documentare - grad_dificultate_documentare: redus, mediu, mare
Depinde de: timp_documentare, mod_documentare, nr_membri_echipa -
tema_secundara: literatura, biologie, sport
Depinde de: sportiv, sta_in_casa, acces_biblioteca, acces_lab_biologie -
empatic: da, nu
Depinde de: resimte_emotii, interesat_de_altii, usor_impresionabil -
profil: real, uman
Depinde de: tip_profil_liceu, bun_la_matematica, gandire_analitica, citit_beletristica, scris_eseuri, autor_preferat - mod_documentare: citit, explorat, experimentare
Depinde de: tip_personalitate, timp_documentare, acces_biblioteca, fire_curioasa, fire_indrazneata, rabdare - etc.
Atribute a caror valoare e obtinuta din raspunsurile utilizatorilor
- nr_membri_echipa: 1, 2, 3
- timp_documentare:redus, mediu, mare
- interesat_de_natura: da, nu (boolean)
- resimte_emotii: da, nu (boolean)
- interesat_de_altii: da, nu (boolean)
- usor_impresionabil: da, nu (boolean)
- tip_profil_liceu: real, uman, artistic
- pasiune_principala: bucatarie, lectura, plimbare
- memorie_predominanta: vizuala, auditiva, kinestezica
- acces_biblioteca: da, nu
- fire_curioasa: da, nu
- fire_indrazneata: da, nu
- rabdare: da, nu
- tip_personalitate:intorvertit, extrovertit, ambivert
- etc.
Exemple de arbori:
Arbore cu o solutie bazata doar pe atribute cu valori obtinute de la utilizator
Unul dintre atributele pe care se bazeaza solutia e la randul lui dedus din valorile altor atribute
Un arbore mai complex cu mai multe atribute deduse. Faptul ca memorie_predominanta apare de 2 ori nu e gresit. Utilizatorul va primi doar o data intrebarea si odata memorat raspunsul va fi folosit si in cadrul altor reguli de evaluat. Atentie sa nu aveti in acelasi arbore valori diferite pentru acelasi atribut (un astfel de arbore e invalid); de exemplu undeva pe o ramura avem memorie auditiva, dar pe alta memoria principala e cea viziuala - solutia respectiva nu ar putea fi obtinuta pe aceasta cale. De asemenea observati ca pentru atributul mod_documentare cu valoarea experimentare avem alt subarbore decat in imaginea de mai sus. Acest lucru este corect, pur si simplu sunt doua reguli avand ca rezultat mod_documentare=experimentare
-
Etapa 3 (scrierea regulilor + parsarea)
Exemplele din aceasta etapa sunt pentru un sistem expert care indica locul de concediu potrivit pentru un utilizator (nu sunt pentru sistemul expert indicat mai sus).
Vom lua ca exemplu urmatorul sablon pentru scop, intrebari si reguli:
-
regulile vor avea formatul (ceea ce e scris cu gri e doar comentariu, nu face parte din format):
rl#id(unde id este numarul regulii)
#lista de premise (conditiile)
[?] atr <-- valoare (pentru atribute cu valori multiple)
[?] {atr} (pentru atribute booleene, valoare true)
[?] {fals atr} (pentru atribute booleene, valoare false)
#implica
atr := valoare cu fc=nr.(concluzia)Concluzii booleene:
atr cu fc=nr(true)
atr fals cu fc=nr(false)
-
intrebarile vor avea formatul:
#intrebare [atribut | 'continut intrebare']
#optiuni := { val1 / val2 / val3 / ... }
- scopul se va defini:
# scop := atr.
Pentru a urma sablonul si a scrie datele conform lui, trebuie inlocuite anumite elemente din fiecare tip de informatie. Atentie, conform scheletului de program oferit, toate informatiile (scop, reguli, intrebari) trebuie sa se termine cu punct. Puneti punctul chiar si daca nu apare neaparat precizat in sablon (acolo poate uneori sa fie omis pentru ca se considera implicit faptul ca trebuie sa existe).
Pentru reguli:
- id este numarul regulii.
- atr este numele atributului. Daca este un atribut boolean va avea forme separate pentru cazurile adevarat si fals. Pentru atributele cu valori multiple ( de exemplu atributul anotimp) trebuie precizata si valoarea. Atentie, conform implementarii de sistem expert care va este oferita, nu puteti pune mai multe valori pentru acelasi atribut (de exemplu pentru atributul culoare nu puteti pune si rosu si verde. Eventual faceti o valoare noua 'rosu si verde' sau spargeti atributul in mai multe atribute booleene: are_rosu, are_verde.)
- cele precizate mai sus sunt valabile si pentru atributul care apare in concluzie.
- in plus, in concluzie trebuie inlocuit si nr cu factorul de certitudine al regulii.
rl#5
#lista de premise
[?] anotimp <-- vara
[?] {doreste_drumetii}
[?] {fals la_mare}
#implica
loc_concediu := sinaia cu fc=88.Daca anotimpul este vara si utilizatorul doreste drumetii si nu vrea sa mearga la mare, atunci locul de concediu indicat este Sinaia (cu factorul de certitudine 88).
Atentie, nu e obligatoriu ca o regula sa cuprinda mereu si premise boolene si premise cu valori multiple - a fost doar un exemplu care sa treaca prin cele 3 cazuri.
O regula cu concluzie booleana adevarata:
rl#7
#lista de premise
[?] tensiune <-- normala
[?] {fals fumeaza}
[?] {fals probleme_inima}
[?] {fals probleme_locomotorii}
#implica
sanatos cu fc=95.Regula de mai sus spune ca daca utilizatorul are tensiune normala, nu fumeaza, nu are probleme cu inima si nici probleme locomotorii atunci este sanatos cu factor certitudine 95 (avand in vedere ca in programul initial maximul e 100, 95 e un scor bun; cu alte cuvinte e foarte probabil sa fie sanatos, dar nu vom pune 100 deoarece nu stim sigur ca nu are afectiuni relevante pe care poate nu le-am cuprins in reguli).
O regula cu concluzie booleana falsa:
rl#23
#lista de premise
[?] tensiune <-- ridicata
[?] {fumeaza}
[?] {oboseste_repede}
[?] culoare_ten <-- palid
#implica
sanatos fals cu fc=84.Regula de mai sus spune ca daca utilizatorul are tensiune ridicata, fumeaza, oboseste repede si are ten palid atunci nu este sanatos cu factor certitudine 84 (un scor mare, dar nu foarte mare, deoarece simptomele pe care le are pot fi si ale unui om relativ sanatos dar, de exemplu, obosit).
Sa scriem si doua intrebari conform sablonului. Una cu variante booleene de raspuns si una cu valori specifice. Se vor inlocui in sablon cuvintele atribut, 'continut intrebare' si optiunile posibile val1, val2 etc. Cele 3 puncte nu fac parte din sablon ci doar arata ca pot urma oricate variante de raspuns.
#intrebare [fumeaza | 'Obisnuiti sa fumati?']Vom scrie de asemenea si scopul, inlocuind cuvantul atr:
#optiuni := { da / nu }.
#intrebare [cantitate_bagaje | 'Cate bagaje estimati ca veti lua cu dumneavoastra in calatorie?']
#optiuni := { deloc / putine / numar_mediu / multe / foarte_multe }.
# scop := loc_concediu.Pentru parsare vom modifica predicatul trad. Avem initial 3 cazuri pentru trad:
- trad(scop(X)) - care parseaza scopul
- trad(interogabil(Atr,M,P)) - care parseaza intrebarile
- trad(regula(N,premise(Daca),concluzie(Atunci,F))) - care parseaza regulile.
Pentru a intelege usor cum se realizeaza parsarea, cititi despre regulile DCG.
Predicatul trad primeste lista de cuvinte a enuntului curent citit.
Cuvintele (tokenii) identificati de parser pot fi:
- caractere nealfanumerice. Nu toate caracterele nealfanumerice sunt preluate din fisier (unele sunt sarite, precum spatiul, si nu apar in lista de cuvinte citite din enuntul curent). Caracterele nealfanumerice pe care dorim sa le procesam sunt cele trecute in predicatul
caracter_cuvant
. Niciun caracter nealfanumeric care nu e trecut acolo nu va ajunge token in lista finala de cuvinte. Atentie, parserul dat ca exemplu considera caracterele nealfanumerice drept tokeni separati chiar si daca apar alaturati, astfel +++ va fi vazut sub forma a trei tokeni +. Exceptie fac caracterele linioara '-' si underscore '_'. - atomi (incep cu litere si pot contine si litere mari si mici, cifre, si caracterele _ si - ). Prin urmare loc_concediu si baba-cloanta sunt vazute drept un singur token si nu 3 tokeni
- atomi intre apostrofuri (veti folosi apostrof cand doriti sa aveti spatiu intr-un identificator, de exemplu 'Trei iezi cucuieti', sau daca aveti unitati de masura: '23m' (altfel ar fi vazut drept doi tokeni separati: 23 si m).
- numere (atentie, doar numere naturale - daca aveti nevoie si de alt tip de numere (negative sau cu zecimale), trebuie sa modificati voi in program).
Primul lucru pe care il facem e sa adaugam caracterele nealfanumerice care apar in plus in sablonul nostru, fata de cele precizate deja in lista lui caracter_cuvant.
Predicatul initial apare asa:
caracter_cuvant(C):-member(C,[44,59,58,63,33,46,41,40]).
cu un comentariu dedesubt care specifica pentru ce caractere sunt codurile ASCII din lista. E recomandat cand treceti codurile ASCII pentru caracterele voastre, sa completati si in comentariu, in ordine, caracterele corespunzatoare, ca sa nu va incurcati.
% am specificat codurile ASCII pentru , ; : ? ! . ) (Iata codul cu caracterele completate pentru sablonul nostru:
caracter_cuvant(C):-member(C,[44,59,58,63,33,46,41,40,35,91,93,60,123,125,61,124,47]).
% am specificat codurile ASCII pentru , ; : ? ! . ) ( # [ ] < { } = | /Acum vom trece la modificarea sablonului de parsare din program. Vom explica intai felul in care e parsat scopul. In fisierul de reguli dat ca exemplu, scopul este scris ca:
scopul este loc_concediu.
unde loc_concediu este numele atributului scop. Acesta trebuie sa ajunga memorat in baza de cunostinte, pentru ca sistemul expert sa stie ce trebuie sa deduca (valoarea carui atribut sa o calculeze). In momentul de fata avem doua reguli ale lui trad care tratateaza scopul si ajung la formarea faptului ce va fi asertat in baza de cunostinte, adicascop(X)
.trad(scop(X)) --> [scopul,este,X].
sitrad(scop(X)) --> [scopul,X].
asta inseamna ca putem scrie scopul in doua moduri:scopul este loc_concediu.
sauscopul loc_concediu.
Observati cum partea fixa a sablonului apare cuvant cu cuvant in lista, iar partea variabila (putem pune ce nume dorim pentru atributul scop) este ilustratata chiar printr-o variabila ce va contine in final numele atributului scop.Pentru sablonul nostru, trebuie sa identificam tokenii din care este format enuntul corespunzator scopului.
# scop := atr.Tokenii sunt #, scop, :, =, atr. Dar atr stim ca este variabil fiind numele atributului scop, deci rescrierea lui trad ar fi:
trad(scop(X)) --> ['#',scop,':','=',X].
Observati cum caracterele nealfanumerice au fost puse intre apostrofuri pentru a evita erori de sintaxa.Continuam cu parsarea regulilor. Partile de program care se ocupa de asta sunt:
trad(regula(N,premise(Daca),concluzie(Atunci,F))) --> identificator(N),daca(Daca),atunci(Atunci,F).
Si sa vedem si o regula scrisa conform sablonului din carte:
/*...in cod mai erau niste linii, omise aici...*/
identificator(N) --> [regula,N].
daca(Daca) --> [daca],lista_premise(Daca).
lista_premise([Daca]) --> propoz(Daca),[atunci].
lista_premise([Prima|Celalalte]) --> propoz(Prima),[si],lista_premise(Celalalte).
lista_premise([Prima|Celalalte]) --> propoz(Prima),[','],lista_premise(Celalalte).
atunci(Atunci,FC) --> propoz(Atunci),[fc],[FC].
atunci(Atunci,100) --> propoz(Atunci).
regula 100
daca not in_romania si
la_mare si
tip_oferta este excursie si
anotimp este iarna
atunci loc_concediu este rio_de_janeiro fc 80.Conform liniei:
trad(regula(N,premise(Daca),concluzie(Atunci,F))) --> identificator(N),daca(Daca),atunci(Atunci,F).
regula este impartita in 3 bucati: identificator, daca, si atunci. Vom marca pe rand fiecare bucata din regula, de care se ocupa fiecare predicat. Am marcat ce decupeaza identificator cu roz, ce decupeaza predicatul daca cu galben, si ce decupeaza predicatul atunci cu portocaliu:
regula 100
Sa rescriem predicatele identificator, daca si atunci pentru sablonul nostru:
daca not in_romania si
la_mare si
tip_oferta este excursie si
anotimp este iarna
atunci loc_concediu este rio_de_janeiro fc 80.rl#idAm marcat deja cu acelasi culori care sunt zonele procesate de fiecare predicat.
#lista de premise
[?] atr <-- valoare
[?] {atr}
[?] {fals atr}
#implica
atr := valoare cu fc=nr.Predicatul identificator(care obtine numarul regulii in N) s-ar rescrie:
identificator(N) --> [rl,'#',N].
si se vede astfel cum in N intra numarul din fisier.Urmeaza predicatul daca. Acesta initial avea forma:
daca(Daca) --> [daca],lista_premise(Daca).
Primul cuvant marcat era "daca" deoarece cu el incepea lista de premise (parsata apoi de predicatul lista_premise). La noi lista de premise incepe cu: "#lista de premise" deci vom scrie:
daca(Daca) --> ['#',lista,de,premise],lista_premise(Daca).
Urmeaza parsarea premiselor, care in codul initial e facuta astfel:
lista_premise([Daca]) --> propoz(Daca),[atunci].
Observam ca este un predicat recursiv. Motivul este ca nu stim cate premise vor fi intr-o regula (avem un numar variabil) si atunci parsam premise pana se termina. Pasul de oprire (primul rand din codul de sus) este cel care parseaza ultima premisa. Premisele sunt parsate cu predicatul propoz. Conform sablonului, dupa ultima premisa este scris cuvantul atunci (care ar marca inceputul zonei de concluzie), de asta si apare cuvantul "atunci" dupa predicatul propoz:
lista_premise([Prima|Celalalte]) --> propoz(Prima),[si],lista_premise(Celalalte).
lista_premise([Prima|Celalalte]) --> propoz(Prima),[','],lista_premise(Celalalte).
lista_premise([Daca]) --> propoz(Daca),[atunci].
La noi premisele se termina cu "#implica", deci pasul de oprire ar lua forma:
lista_premise([Daca]) --> ['[','?',']'], propoz_premisa(Daca),['#',implica].
In zona recursiva se precizeaza ca intai se parseaza o premisa (propoz) si apoi separatorul de premise: cuvantul "si" sau simbolul virgula, continuand apoi cu parsarea restului de premise (apelul recursiv):
lista_premise([Prima|Celalalte]) --> propoz(Prima),[si],lista_premise(Celalalte).
In cazul nostru, separatorul e la inceputul premisei, ca un fel de bullet pentru o lista, astfel ca in loc sa apara dupa propoz, va fi inainte. In plus, nu avem doua variante de scriere a premiselor, deci vom avea un singur rand de program:
lista_premise([Prima|Celalalte]) --> propoz(Prima),[','],lista_premise(Celalalte).
lista_premise([Prima|Celalalte]) --> ['[','?',']'],propoz_premisa(Prima),lista_premise(Celalalte).
In plus, am schimbat si numele lui propoz in propoz_premisa. Motivul este ca in exemplul initial, concluzia si premisele aveau acelasi format deoarece ambele presupuneau o pereche de forma atribut-valoare. Insa la noi, conform enuntului, premisele si concluzia au forme diferite (acest fapt nu apare in mod necesar in orice cerinta - deci nu trebuie modificat in cazul oricarui proiect).Ne vom uita acum la predicatul propoz initial:
propoz(not av(Atr,da)) --> [not,Atr].
Cazurile sunt in ordine pentru:
propoz(av(Atr,Val)) --> [Atr,este,Val].
propoz(av(Atr,da)) --> [Atr].
- atribut boolean fals
- atribut neboolean (cu valoare data: variabila Val)
- atribut boolean adevarat
Ne uitam acum la formatul premiselor noastre (am pus doar fragmentul corespunzator din sablon):
[?] atr <-- valoare
[?] {atr}
[?] {fals atr}
Partea cu "[?]" nu mai e nevoie sa o tratam pentru ca deja am trecut-o pe post de separator intre premise in predicatul lista_premise. Asadar scriem si noi cele 3 cazuri conform sablonului. Tinem cont ca Atr este variabila corespunzatoare numelui atributului si Val este valoarea lui. Perechile atribut-valoare sunt memorate in structuri de forma av(Atr,Val). Pentru atrbutele boolene structura este
av(Atr,da)
pentru valoare true si respectivnot av(Atr,da)
pentru valoare false. Nu este corectav(Atr,nu)
- e un truc in program, strict de elegeanta a codului, pentru a scrie doar clauzele pentru valoarea da iar cand av-ul e precedat de not, doar este negat factorul de certitudine (adica e inmultit cu -1).propoz_premisa(not av(Atr,da)) --> ['{', fals,Atr,'}'].
propoz_premisa(av(Atr,Val)) --> [Atr,'<','--',Val].
propoz_premisa(av(Atr,da)) --> ['{',Atr,'}'].
Atentie, in cazul unor cerinte e posibil sa fie nevoie sa schimbati ordinea celor trei cazuri si sa puneti si un cut (e cazul cand formatul atributelor booleene e asemenator cu cel al celor nebooleene)
A mai ramas doar concluzia, care initial avea forma:
atunci(Atunci,FC) --> propoz(Atunci),[fc],[FC].
atunci(Atunci,100) --> propoz(Atunci).
Prima clauza este pentru situatia cand este dat factorul de certitudine, iar a doua clauza pentru situatia cand factorul de certitudine lipseste, situatie in care este trecut automat 100 (valoare maxima).
Vom urmari in sablon formatul pentru concluzie:
atr := valoare cu fc=nr.
Am marcat cu o culoare zona de atribut-valoare a concluziei. Acesta este "propoz-ul" concluziei. Urmeaza zona de oferire a factorului de certitudine din care noi trebuie sa obtinem nr si sa il punem in variabila FC.atunci(Atunci,FC) --> propoz_concluzie(Atunci),[cu, fc, '=', FC].
iar pentru propoz_concluzie, conform enuntului:
atunci(Atunci,100) --> propoz_concluzie(Atunci).
atr cu fc=nr(true)vom avea codul:
atr fals cu fc=nr(false)
propoz_concluzie(not av(Atr,da)) --> [Atr,fals].
propoz_concluzie(av(Atr,Val)) --> [Atr,':','=',Val].
propoz_concluzie(av(Atr,da)) --> [Atr].
Pentru intrebari situatia e mai usoara. Partea de program care se ocupa de ele, este:
trad(interogabil(Atr,M,P)) --> [intreaba,Atr],lista_optiuni(M),afiseaza(Atr,P).
O intrebare astfel e descompusa si ea in trei parti:
lista_optiuni(M) --> [optiuni,'('],lista_de_optiuni(M).
lista_de_optiuni([Element]) --> [Element,')'].
lista_de_optiuni([Element|T]) --> [Element],lista_de_optiuni(T).
afiseaza(_,P) --> [afiseaza,P].
afiseaza(P,P) --> [].
- [intreaba,Atr] - partea care decupeaza numele atributului caruia ii corespunde intrebarea
- lista_optiuni(M) - partea care obtine lista de optiuni (valori posibile pentru atribut). Observam ca acest predicat apeleaza predicatul lista_de_optiuni, care e un predicat recursiv ce trece prin optiuni (este recursiv deoarece nu stim cate optiuni avem de parsat).
- afiseaza - partea care obtine enuntul intrebarii. A doua clauza a predicatului pune in variabila P chiar numele predicatului in cazul in care intrebarea nu exista. Observati asta din modul de apelare: afiseaza(Atr,P). Primeste numele atributului ca prim parametru. Iar in ultima linie, prin afiseaza(P,P) pune in variabila P corespunzatoare propozitiei, chiar numele atributului, daca enuntul nu e dat (lista de parsat e o lista vida []).
intreaba anotimp
Pentru sablonul nostru, cele 3 zone ar fi:
optiuni (vara iarna)
afiseaza 'In ce anotimp preferati sa va petreceti concediul?'.#intrebare [fumeaza | 'Obisnuiti sa fumati']
#optiuni := { da / nu }
Observam ca ordinea partilor de intrebare s-a schimbat (avem zona cu numele atributului, apoi zona cu intrebarea (predicatul afiseaza) si apoi zona cu optiunile), deci noi o sa avem:
trad(interogabil(Atr,M,P)) --> ['#',intrebare,'[',Atr],afiseaza(Atr,P),lista_optiuni(M).
Pentru predicatul afiseaza, codul este:
afiseaza(_,P) --> ['|',P,']'].
Am pus in cazul al doilea doar paranteza dreapta, in ideea ca tot se inchide in sablon, doar ca lipseste propozitia in sine, de exemplu:
afiseaza(P,P) --> [']'].
#intrebare [fumeaza ]Pentru predicatul de parsare a optiunilor, observam ca de data asta avem separator intre optiuni, deci:
#optiuni := { da / nu }
lista_optiuni(M) --> ['#',optiuni,':','=','{'],lista_de_optiuni(M).
lista_de_optiuni([Element]) --> [Element,'}'].
lista_de_optiuni([Element|T]) --> [Element, '/'],lista_de_optiuni(T).
Pentru a testa parsarea, ne vom face un fisier strict de test, deci enunturile din ele nu trebuie neaparat sa fie relevante (putem inventa pe loc atribute si valori care nu au legatura cu tema). Vom scrie in fisier:
- scopul
- trei reguli: una cu concluzie booleana adevarata, una concluzie booleana falsa, si una cu concluzie pentru un atribut neboolean. De asemenea in reguli vom avea mai multe premise dintre care vom avea minim: o premisa booleana adevarata, o premisa booleana falsa, si o premisa pentru un atribut neboolean.
- Doua intrebari: una pentru un atribut boolean si una pentru un atribut neboolean
Pentru verificare si debugging, pentru a economisi timp, nu o sa mai trecem prin meniul principal al sistemului expert si in loc de predicatul principal pornire o sa apelam predicatul incarca(cale_fisier_reguli). Nu uitati sa setati working directory inainte de apel. Apoi ca sa verificam daca scopul, regulile si intrebarile au fost parsate corect si memorate in baza de cunostinte, dam:
- listing(scop)
- listing(regula)
- listing(interogabil)
yes
| ?- listing(scop).
scop(loc_concediu).
yes
| ?- listing(regula).
yes
| ?- listing(interogabil).
yes
In acest exemplu scopul este parsat bine, dar regulile si intrebarile, nu.Astfel trebuie sa facem debug. Ne reamintim comanda s (skip) din cadrul trace. O vom folosi pentru call si redo pe predicatul citeste_propozitie, ca sa nu stam sa treaca prin toate citirile de caracter cu caracter, asa cum este marcat cu verde in exemplul de mai jos. Am dat skip si pentru proceseaza([#,scop,:,=,loc_concediu]) deoarece scopul deja era corect in exemplu. De asemenea, daca nu este afisata toata linia de parsat (din lista) e nevoie sa crestem bufferul de afisare, si dam o comanda de forma <dimensiune_buffer( in exemplu, marcat cu galben e <100). Nu toate failurile sunt rele. Apelul 5 de exemplu verifica daca regula poate fi interpretata ca scop, si raspunsul foarte corect este nu. Trebuie sa mergem pana la un fail care nu ar trebui sa sa intample. Acesta este failul de la call-ul 8, unde ne asteptam sa parseze premisa si totusi nu s-a intamplat. Comparand cele doua liste din stanga si dreapta egalului vedem ca in stanga avem cuvantul '--' iar in dreapta asteptam '-','-' (adica 2 tokeni). Asta inseamna ca trebuie corectat in program ca sa fie conform sablonului, deci pus '--'. | ?- trace.
% The debugger will first creep -- showing everything (trace)
yes
% trace
| ?- incarca('reguli_exemplu_cerinte_individuale.txt').
1 1 Call: incarca('reguli_exemplu_cerinte_individuale.txt') ?
2 2 Call: retractall(user:interogat(_799)) ?
2 2 Exit: retractall(user:interogat(_799)) ?
3 2 Call: retractall(user:fapt(_809,_810,_811)) ?
3 2 Exit: retractall(user:fapt(_809,_810,_811)) ?
4 2 Call: retractall(user:scop(_821)) ?
4 2 Exit: retractall(user:scop(_821)) ?
5 2 Call: retractall(user:interogabil(_831,_832,_833)) ?
5 2 Exit: retractall(user:interogabil(_831,_832,_833)) ?
6 2 Call: retractall(user:regula(_843,_844,_845)) ?
6 2 Exit: retractall(user:regula(_843,_844,_845)) ?
7 2 Call: see('reguli_exemplu_cerinte_individuale.txt') ?
7 2 Exit: see('reguli_exemplu_cerinte_individuale.txt') ?
8 2 Call: incarca_reguli ?
9 3 Call: repeat ?
? 9 3 Exit: repeat ?
10 3 Call: citeste_propozitie(_8461) ? s
? 10 3 Exit: citeste_propozitie([#,scop,:,=,loc_concediu]) ?
11 3 Call: proceseaza([#,scop,:,=,loc_concediu]) ? s
11 3 Exit: proceseaza([#,scop,:,=,loc_concediu]) ?
12 3 Call: [#,scop,:,=,loc_concediu]==[end_of_file] ?
12 3 Fail: [#,scop,:,=,loc_concediu]==[end_of_file] ?
10 3 Redo: citeste_propozitie([#,scop,:,=,loc_concediu]) ? s
10 3 Fail: citeste_propozitie(_8461) ?
9 3 Redo: repeat ?
? 9 3 Exit: repeat ?
13 3 Call: citeste_propozitie(_8461) ? s
? 13 3 Exit: citeste_propozitie([rl,#,5.0,#,lista,de,premise,'[',?|...]) ? <100
? 13 3 Exit: citeste_propozitie([rl,#,5.0,#,lista,de,premise,'[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0]) ?
14 3 Call: proceseaza([rl,#,5.0,#,lista,de,premise,'[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0]) ?
15 4 Call: trad(_45704,[rl,#,5.0,#,lista,de,premise,'[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0],[]) ?
16 5 Call: [rl,#,5.0,#,lista,de,premise,'[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0]=[#,scop,:,=,_46196] ?
16 5 Fail: [rl,#,5.0,#,lista,de,premise,'[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0]=[#,scop,:,=,_46196] ?
17 5 Call: [rl,#,5.0,#,lista,de,premise,'[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0]=[intreaba,_46196|_46208] ?
17 5 Fail: [rl,#,5.0,#,lista,de,premise,'[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0]=[intreaba,_46196|_46208] ?
18 5 Call: identificator(_46196,[rl,#,5.0,#,lista,de,premise,'[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0],_46210) ?
19 6 Call: [rl,#,5.0,#,lista,de,premise,'[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0]=[rl,#,_46196|_46210] ?
19 6 Exit: [rl,#,5.0,#,lista,de,premise,'[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0]=[rl,#,5.0,#,lista,de,premise,'[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0] ?
18 5 Exit: identificator(5.0,[rl,#,5.0,#,lista,de,premise,'[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0],[#,lista,de,premise,'[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0]) ?
20 5 Call: daca(_46200,[#,lista,de,premise,'[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0],_46217) ?
21 6 Call: [#,lista,de,premise,'[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0]=[#,lista,de,premise|_48683] ?
21 6 Exit: [#,lista,de,premise,'[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0]=[#,lista,de,premise,'[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0] ?
22 6 Call: lista_premise(_45203,['[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0],_45220) ?
23 7 Call: propoz_premisa(_49131,['[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0],_49139) ?
24 8 Call: ['[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0]=['{',fals,_49638,'}'|_49139] ?
24 8 Fail: ['[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0]=['{',fals,_49638,'}'|_49139] ?
25 8 Call: ['[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0]=[_49636,<,-,-,_49637|_49139] ?
25 8 Fail: ['[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0]=[_49636,<,-,-,_49637|_49139] ?
26 8 Call: ['[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0]=['{',_49636,'}'|_49139] ?
26 8 Fail: ['[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0]=['{',_49636,'}'|_49139] ?
23 7 Fail: propoz_premisa(_49131,['[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0],_49139) ?
27 7 Call: ['[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0]=['[',?,']'|_49144] ?
27 7 Exit: ['[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0]=['[',?,']',tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0] ?
28 7 Call: propoz_premisa(_49131,[tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0],_49151) ?
29 8 Call: [tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0]=['{',fals,_50651,'}'|_49151] ?
29 8 Fail: [tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0]=['{',fals,_50651,'}'|_49151] ?
30 8 Call: [tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0]=[_50649,<,-,-,_50650|_49151] ?
30 8 Fail: [tensiune,<,--,ridicata,'[',?,']','{',fumeaza,'}','[',?,']','{',oboseste_repede,'}','[',?,']',culoare_ten,<,--,palid,#,implica,sanatos,fals,cu,fc,=,84.0]=[_50649,<,-,-,_50650|_49151] ?
Pentru a nu trece cu trace prin informatiile deja corecte, putem pune enunturile cu probleme la inceputul fisierului. In cazul nostru, sa mutam regulile la inceput si scopul, la final, ca sa vedem dupa ce corectam daca nu am scris totusi ceva care a afectat si parsarile bune.
Observatie. Uneori ramane fisierul de reguli deschis, si primiti o eroare fie cand vreti sa il modificati in Notepad++, fie cand vreti sa rulati iar predicatul care incarca regulile. Pentru a scapa de problema, folositi close_all_streams: | ?- incarca('reguli_exemplu_cerinte_individuale.txt').
! Existence error in get_code/2
! attempt to read past end of stream
! goal: get_code('$stream'(75525744),_118)
| ?- close_all_streams.
yes
| ?- incarca('reguli_exemplu_cerinte_individuale.txt').
yes
| ?- Dupa ce faceti toate corecturile, ar trebui in urma celor trei listinguri sa va vedeti toate informatiile din fisier memorate in cele 3 predicate dinamice: | ?- listing(scop).
scop(loc_concediu).
yes
| ?- listing(regula).
regula(7.0, premise([av(tensiune,normala),not av(fumeaza,da),not av(probleme_inima,da),not av(probleme_locomotorii,da)]), concluzie(av(sanatos,da),95.0)).
regula(5.0, premise([av(tensiune,ridicata),av(fumeaza,da),av(oboseste_repede,da),av(culoare_ten,palid)]), concluzie(not av(sanatos,da),84.0)).
regula(23.0, premise([av(tensiune,ridicata),av(fumeaza,da),av(oboseste_repede,da),av(culoare_ten,palid)]), concluzie(not av(sanatos,da),84.0)).
yes
| ?- listing(interogabil).
interogabil(fumeaza, [da,nu], '\'Obisnuiti sa fumati?\'').
interogabil(cantitate_bagaje, [deloc,putine,numar_mediu,multe,foarte_multe], '\'Cate bagaje estimati ca veti lua cu dumneavoastra in calatorie?\'').
yes
| ?-Dupa ce terminati de facut parsarea, ca sa finalizati verificarea, alegeti un arbore de inaltime minim 2 din etapa 2 si scrieti scopul, regulile si intrebarile corespunzatoare lui. Dupa ce ati verificat ca au fost parsate corect, intrati in program cu predicatul principal, pornire si folositi meniul, asa cum vi s-a aratat la laborator, intai incarcand regulile si apoi consultand sistemul. Dati raspunsuri astfel incat sa se potriveasca solutiei si vedeti la final daca va da solutia din arbore. Daca nu, reverificati datele.
Abia dupa ce primiti acea unica solutie definita este momentul sa scrieti complet fisierul de reguli. Pentru asta veti lua pe rand fiecare atribut dat de utilizator si ii veti scrie intrebarea si variantele posibile. Apoi veti lua pe rand fiecare atribut dedus si pentru fiecare valoare posibila a lui veti scrie cateva reguli (poate fi una sau mai multe, dar recomandat e sa fie mai multe pentru fiecare valoare, folosind atribute diferite in premise).
La final, luati cateva valori posibile pentru atributul scop si incercati sa dati raspunsuri astfel incat sa picati pe acea solutie. Apoi verificati ca aveti, conform cerintelor generale:
- un set de raspunsuri fara solutii
- un set de raspunsuri cu mai multe solutii (sa zicem minim 3)
- un set de raspunsuri cu o singura solutie
Pentru fiecare solutie obtinuta incercati sa afisati si demonstratia, si sa vedeti nivelul de inferenta si daca modul de deducere e asa cum va asteptati.
-
regulile vor avea formatul (ceea ce e scris cu gri e doar comentariu, nu face parte din format):
-
Realizarea cerintelor generale de programare
Ne vom ocupa intai de cerinta afisarii raspunsurilor booleene (da si nu) - implicit ele nu sunt afisate.
Cautam in program predicatul interogheaza care se ocupa cu afisarea intrebarilor. Gasim doua clauze, una pentru intrebari pentru atribute booleene si cealalta pentru atribute generale. Cum ne dam seama care-i cea pentru atribute booleene? Observam parametrul al treilea (cuprinzand lista de optiuni posibile).interogheaza(Atr,Mesaj,[da,nu],Istorie) :- ....
In cadrul acestei, reguli, dupa afisarea textului intrebarii, adaugati un write, care sa afiseze si cele doua raspunsuri. Pentru estetica, raspunsurile ar trebui sa fie pe o linie noua.
Mai departe vom rezolva doua cerinte dintre cele generale in acelasi predicat: afisarea unui mesaj in cazul in care sistemul nu are solutii, respectiv afisarea solutiilor in ordinea crescatoare a factorului de certitudine.
Cautam predicatul care are implementata logica sistemului expert si porneste consultarea: scopuri_princ.
scopuri_princ :-
Mai precis, in loc sa afisam solutia (cu afiseaza_scop) dupa fiecare determinare de solutie, vom afisa, dupa ce au fost determinate toate solutiile. Astfel, afisarea se va muta in a doua clauza a lui scopuri_princ. Apelul lui fail din prima clauza il ajuta sa revina in predicatul determina. In momentul in care se determina o solutie, aceasta e memorata in predicatul dinamic fapt care are formatul:
scop(Atr),
determina(Atr),
afiseaza_scop(Atr), %pe acesta il eliminam
fail.
scopuri_princ. %completam codul astfel incat afisarea sa fie aici%fapt(av(atribut,valoare),factor_certitudine,istoric).
Astfel, pentru fiecare solutie, va fi un fapt memorat. Cand stim ca nu avem solutii? Cand nu exista predicate fapt corespunzatoare atributului scop.In plus, vrem sa ordonam solutiile dupa un criteriu (factorul de certitudine) ceea ce ne duce cu gandul la crearea unei liste ordonate. Avem predicatele de creare a unei liste pe baza faptelor din baza de cunostinte: setof, bagof, findall. Care dintre ele credeti ca este cel necesar? Evident setof care realizeaza o lista de elemente ordonate crescator, fara duplicate.
Reamintim ca daca vrem sa ordonam elementele unei liste dupa un anumit criteriu trebuie sa cream o lista de structuri de forma: struct(Criteriu, Element), deci inceputul setof-ului va fi:
setof(struct(Criteriu, Element), .....)
Care este criteriul? Conform enuntului, factorul de certitudine. Ce anume vrem sa ordonam? Evident solutiile care de fapt sunt memorate in structura av din fapt. Rezulta struct(FC, av(Atr,Val)). Urmeaza sa stabilim conditia lui setof. Evident aceasta va trebui sa contina predicatul fapt, dar, pentru ca si atributele secundare sunt memorate tot cu ajutorul lui fapt, trebuie intai sa precizam ca atributul trebuie sa fie atributul scop (scop(Atr)). La final, punem rezultatul intr-o lista L.Ne reamintim. Ce se intampla cand setof nu are ce pune in lista? (adica nu exista solutii). Esueaza. Deci putem scrie o expresie conditionala (cu operatorul -> ), unde conditia e setof-ul. Daca se termina cu succes avem o lista de solutii (ordonata dupa FC) pe care o afisam. Daca nu, pe partea de else scriem un mesaj care atentioneaza utilizatorul ca nu sunt solutii.
Sa ne ocupam de predicatul care afiseaza lista de solutii. O sa il numim afiseaza_lista_sol(L). Lista L e formata din structuri de forma struct(FC, av(Atr,Val)), fiindca asa am construit-o. Astfel cand parcurgem lista, scriem elementul H (din[H|T]) in acest format.Observam ca avem un predicat scrie_scop(av(Atr,Val),FC) care afiseaza formatat o solutie impreuna cu factorul ei de certitudine - si il folosim.
In acest moment, daca sistemul are solutii, multiple ar trebui sa le afiseze ordonate crescator dupa FC, iar daca nu are solutii, sa afiseze un mesaj corespunzator.
-
Parsarea si memorarea detaliilor despre solutii, impreuna cu meniul secundar
Pentru a citi si memora descrierile, procedam ca si in cazul regulilor si intrebarilor.
De exemplu, sa presupunem ca avem urmatorul format:
pentru varianta >>> nume_sol avem detaliile:
~~ imagine_solutie >>> { cale_relativa_imagine }
~~ descriere_solutie >>> { ... }
~~ proprietati_solutie >>> { prop1 [ val1 ] ## prop2 [ val2 ] ## ... }
||||||||
Unde, cuvantul nume_sol va fi inlocuit cu numele solutiei. In locul punctelor "..." avem descrierea. In locul identificatorului cale_relativa_imagine avem calea imaginii. Atentie! Descrierea si calea imaginii se pun intotdeauna intre apostrofuri pentru a fi vazut ca un singur token. Proprietatile sunt separate prin ##.
Observatie: Cand scrieti descrierile, nu e neaparat nevoie sa pastrati numarul de caractere din separator. Important e sa fie exact acelasi caracter dat in enunt, si sa fie mai mult de 3, iar din punct de vedere estetic chiar sa dea senzatia ca separa doua enunturi.
Consideram ca avem de memorat urmatoarele detalii pentru solutii:
- numele solutiei (acelasi cu cel din fisierul de reguli, folosit ca identificator unic, ca sa stim ale cui sunt detaliile)
- descrierea - un singur atom cu tot textul descrierii in care se explica ce reprezinta solutia respectiva
- calea imaginii corespunzatoare solutiei (tot un singur atom)
- o lista de proprietati (nu neaparat atribute din reguli) impreuna cu valorile lor
Pentru sablonul de mai sus, o descriere posibila a unei solutii este:
pentru varianta >>> rasa_persana avem detaliile:
~~ imagine_solutie >>> { 'imagini/pisica_persana.jpg' }
~~ descriere_solutie >>> { 'O rasa relativ veche. Castigatoare la multe concursuri de frumusete. Lasa mult par pe covoare.' }
~~ proprietati_solutie >>> { culoare [ culori_multiple ] ## timp_viata [ '10-18 ani' ] ## talie [ medie ] ## alura [ eleganta ] }
||||||||.
Astfel, pentru a memora datele pentru o solutie, ne trebuie un predicat de forma:
descriere(nume_sol, cale_relativa_imagine, descriere, lista_proprietati).Pentru omogenitate, putem memora proprietatile tot in structuri av (atribut-valoare) ca si atributele din cunostintele sistemuli expert. Astfel, lista de proprietati va fi de fapt o lista de elemente de forma av(proprietate, valoare).
De exemplu, descrierea de mai sus s-ar transforma in:
descriere(rasa_persana, 'imagini/pisica_persana.jpg', 'O rasa relativ veche. Castigatoare la multe concursuri de frumusete. Lasa mult par pe covoare.', [av(culoare,culori_multiple), av(timp_viata, '10-18 ani'), av(talie, medie), av(alura, eleganta)]).Pentru a parsa si memora descrierile, ne putem folosi de aceeasi strategie de la fisierul de reguli.
In predicatul incarca(F) avem zona de cod:
incarca(F):-...see(F),incarca_reguli,seen...
Acolo se deschide fisierul de reguli, cu see(Cale) dat de utilizator, se incarca regulile (predicatul incarca_reguli citeste fiecare propozitie - enunt pana la punct - si o parseaza cu ajutorul lui trad) si apoi se inchide fisierul cu seen. Putem scrie parsarea detaliilor chiar in continuarea predicatului, in acelasi stil. Putem sa nu mai cerem utilizatorului calea catre fisierul de descrieri, ci sa hardcodam in program: see('cale fisier descrieri'), apoi apelam acelasi incarca_reguli, si inchidem fisierul.Pentru a parsa descrierile, le tratam in predicatul trad ca si pe celelalte tipuri de enunturi:
trad(descriere(Solutie, Cale, Descriere, Lista_prop)):- ....
Pentru lista de proprietati scrieti un predicat recursiv asemanator cu lista_premise. Si veti avea un propoz_proprietate care parseaza o pereche proprietate-valoare si o pune intr-o structura av.Pentru a verifica daca descrierile sunt parsate corect, puteti, fara a folosi predicatul principal pornire (pentru rapiditate) sa scrieti in consola: |?- see('cale fisier descrieri'),incarca_reguli,seen. evident inlocuind 'cale fisier descrieri' cu calea reala, si apoi, ca si la reguli: |?- listing(descriere). Verificati apoi ca aveti toate descrierile in formatul corect. Debug-ul pe parsarea descrierilor se face similar cu cel de la reguli.
Dupa ce ati verificat ca aveti descrierile, sa realizam predicatul care afiseaza o solutie in formatul cerut (de exemplu doar cu descriere, sau doar cu proprietati, sau cu ambele).
Avand in vedere ca avem solutiile intr-o lista de solutii, si afisarea se face acum parcurgand acea lista, ar trebui sa ne facem un predicat nou de parcurgere si afisare a solutiilor din lista cu tot cu detalii.
Practic se ia predicatul creat anterior, si se inlocuieste scrie_scop cu scrie_scop_detalii.
Continutul predicatului scrie_scop_detalii va fi copiat din scrie_scop, in care, in plus a,pelam predicatul descriere (pentru valoarea solutiei curente) si afisam continutul parametrilor preluati din descriere.
Pentru a intelege cum sa faceti meniul secundar, observati predicatul pornire care afiseaza meniul principal. Va trebui sa va faceti un predicat nou, de exemplu numit meniu_secundar care va avea un comportament similar.
Unde apelam acum predicatul? Urmeaza implemenatrea meniului secundar, care va apela predicatul de afisare a detaliilor.
De exemplu, daca vi se cere sa afisati meniul secundar dupa afisarea solutiilor, il apelam chiar dupa afis_lista_sol.
Sa intelegem mai bine cum este creat un meniu in prolog urmarind exemplul de meniu cu reintrare, de mai jos:
meniu:- repeat, %(re)intrare in meniu,
write('\nMeniu:\n optiune1 optiune2 iesire'),nl, %afisarea optiunilor +cea de iesire
read(Opt_utiliz), %citim inputul utilizatorului
executie(Opt_utiliz), %executam codul corespunzator optiunii
Opt_utiliz == iesire, %verificam daca optiunea e cea de iesire
%daca testul de mai sus e fals, se intoarce prin backtracking la repeat
%daca e adevarat trece de cut si iese din zona repetitiva
!, %locul unde se termina zona repetitiva
write('Va multumim ca ati folosit acest meniu!'). %ce se intampla dupa iesirea din meniu
%mai jos avem tratarea optiunilor:
%executie(+Optiune)
%practic se scrie codul de executat pentru fiecare caz de optiune
executie(optiune1):- write('Ati ales optiunea 1'),
nl, !. %se pune cut pentru a nu trece mai departe si la restul de optiuni
executie(optiune2):- write('Ati ales optiunea 2'), nl,
write('Optiunea1 e nemultumita de alegerea dumneavoastra'),
nl, !. %se pune cut pentru a nu trece mai departe si la restul de optiuni
executie(iesire):- write('Cod optional pentru iesirea din meniu'), nl,%write-ul poate sa lipseasca
!. %cut-ul e insa obligatoriu fiindca avem un ultim caz de optiune
%la aceasta optiune ajunge doar daca nu s-a oprit mai sus (fapt asigurat de pozitia clauzelor si de existenta cut-ului la finalul fiecareia)
%oricare ar fi parametrul, stim ca nu e una din optiunile valide
executie(_):-write('Ai gresit optiunea, baiatul meu!'),nl.
Un exemplu de folosire a meniului:
| ?- meniu.
Meniu:
optiune1 optiune2 iesire
|: optiune1.
Ati ales optiunea 1
Meniu:
optiune1 optiune2 iesire
|: chestie.
Ai gresit optiunea, baiatul meu!
Meniu:
optiune1 optiune2 iesire
|: optiune2.
Ati ales optiunea 2
Optiunea1 e nemultumita de alegerea dumneavoastra
Meniu:
optiune1 optiune2 iesire
|: iesire.
Cod optional pentru iesirea din meniu
Va multumim ca ati folosit acest meniu!
yes
| ?-Predicatul de mai sus nu avea parametri, nefiind nevoie de date suplimentare. Adaugarea parametrilor in predcatul de afisare a meniului este, insa, foarte simpla. sa luam ca exemplu un predicat care primeste ca date de intrare doua numere si ofera un set de operatii posibile pentru ele
operatie(Nr1,Nr2):-repeat, number(Nr1), number(Nr2),
write('\nCe doresti sa faci cu cele doua numere?\n'),
write('adunare / scadere / inmultire / nimic\n'),
read(Op),
aplica(Nr1,Nr2,Op),
Op==nimic,
!.
%parametrii meniului trebuie transmisi mai departe
aplica(Nr1,Nr2,Op):- Op=='adunare', Rez is Nr1+Nr2, write(Rez), nl,!;
Op=='scadere', Rez is Nr1-Nr2, write(Rez), nl,!;
Op=='inmultire', Rez is Nr1+Nr2, write(Rez), nl,!;
Op==nimic, !;
write('Operatie incorecta!'),nl.
Un exemplu de folosire este: | ?- operatie(20,30).
Ce doresti sa faci cu cele doua numere?
adunare / scadere / inmultire / nimic
|: adunare.
50
Ce doresti sa faci cu cele doua numere?
adunare / scadere / inmultire / nimic
|: scadere.
-10
Ce doresti sa faci cu cele doua numere?
adunare / scadere / inmultire / nimic
|: inmultire.
50
Ce doresti sa faci cu cele doua numere?
adunare / scadere / inmultire / nimic
|: altceva.
Operatie incorecta!
Ce doresti sa faci cu cele doua numere?
adunare / scadere / inmultire / nimic
|: nimic.
yes
| ?-