4 elementy rozmowy z komputerem
Aby rozmowa z komputerem przypominała rozmowę z człowiekiem, komputer musi opanować cztery elementy dialogu:
- rozpoznawanie mowy,
- rozumienie języka,
- generowanie wypowiedzi,
- syntezę mowy.
Rozpoznawanie mowy
Rozpoznawanie mowy to proces, w ramach którego komputer słucha wypowiedzi człowieka, generując jej pisemny odpowiednik. Proces ten można zaobserwować na przykład podczas niektórych programów informacyjnych, kiedy to z nieznacznym opóźnieniem na ekranie telewizora pojawia się tekst wypowiedziany przez dziennikarza.
W ostatnich latach systemy tego typu poczyniły ogromne postępy w skutecznym rozpoznawaniu sygnału mowy praktycznie dowolnego języka naturalnego na świecie. Jeszcze nie tak dawno poprawne rozpoznanie przez system nawigacji samochodowej wypowiedzianego przez człowieka adresu graniczyło z cudem; dziś natomiast dziwimy się, gdy w rozpoznaniu tego typu komunikatu pojawi się najmniejszy błąd.
Synteza mowy
Nie dawniej niż kilka lat temu głos wygenerowany komputerowo uderzał swym jednostajnym rytmem i nienaturalną intonacją. Dziś odróżnienie głosu syntetycznego od ludzkiego to już zadanie dla osób o wybitnie wyczulonym słuchu.
Generowanie wypowiedzi
Generowanie wypowiedzi to proces tworzenia wypowiedzi języka naturalnego na podstawie wiedzy zakodowanej w komputerze. Na przykład na podstawie informacji typu: “<stacja odjazdu: Warszawa; typ pociągu: pospieszny, stacja docelowa: Poznań, opóźnienie: 30>” system informatyczny generuje wypowiedź: “Pociąg pośpieszny z Warszawy do Poznania jest opóźniony o trzydzieści minut”.
Generowanie wypowiedzi może odbywać się na podstawie opracowanego przez człowieka zestawu reguł lub wzorców i jest to zadanie znacznie łatwiejsze niż rozumienie języka, o którym mowa w kolejnym punkcie.
Rozumienie języka
Powiemy, że system informatyczny rozumie język naturalny, jeśli w reakcji na wypowiedź człowieka podejmuje on działanie zgodne z oczekiwaniami mówcy. Na przykład w odpowiedzi na zadane pytanie system udziela relewantnej informacji, w reakcji na polecenie realizuje czynności w nim zawarte, a w odpowiedzi na wyrażoną opinię kontynuuje dialog w sposób charakterystyczny dla człowieka.
Rozumienie wypowiedzi języka naturalnego jest najtrudniejszym i wciąż dalekim od rozwiązania elementem prowadzenia dialogu przez komputer. Wynika to z faktu, że nieskończony zbiór możliwych wypowiedzi ludzkich bardzo trudno jest ogarnąć skończonym zestawem reguł czy zasad.
Jeżeli jednak umówimy się, że system informatyczny ma działać wyłącznie w pewnej uproszczonej rzeczywistości, w której wypowiedzi użytkownika ograniczone są pod względem słownictwa i struktury, wówczas możliwe – a wręcz niespecjalnie skomplikowane – staje się napisanie programu komputerowego, który rozumie język naturalny.
W tym wpisie na blogu opiszę krok po kroku, jak taki system można stworzyć. Niezbędny na te potrzeby przykład zostanie opracowany z użyciem języka programowania Python, jednakże wydaje się, że nawet bez znajomości tego języka – czy wręcz bez znajomości jakiegokolwiek języka programowania – czytelnik nauczy się skutecznie budować podobnego typu systemy.
Wirtualny sprzedawca
Stworzony w ramach naszego przykładu system będzie wirtualnym sprzedawcą, realizującym misję zawierającą się w słowach: “Kupię, by potem sprzedać”. Będzie to system nieskomplikowany, reagujący na niedługie i proste składniowo zdania. Z myślą o poszerzeniu potencjalnego rynku zbytu nasz sprzedawca komunikował się będzie w języku angielskim, a jego rozmówca będzie wydawał mu polecenia za pomocą komend typu:
Buy 5 large blue metal boxes.
Sell 3 tiny red plastic rings.
Nasz sprzedawca będzie robił wszystko, aby za każdym razem dobić targu. Każde polecenie zakupu zaakceptuje, a następnie zarejestruje w swoim spisie towarów informacje na jego temat. Polecenie sprzedaży będzie realizować tylko wówczas, gdy uprzednio zakupił odpowiednią ilość towaru.
Uznamy, że nasz system rozumie, co się do niego mówi, jeśli jego spis towarów będzie na bieżąco zgodny z poczynionymi zakupami oraz zrealizowanymi transakcjami sprzedaży.
Jak pracuje wirtualny sprzedawca?
Nasz program ma działać w sposób następujący:
Użytkownik wydaje komendę językiem naturalnym, np.:
Buy 5 large blue metal boxes.
- System identyfikuje towar, o którym mowa, czyli w tym przypadku:
large blue metal boxes - Każdy towar, którym handluje sprzedawca, ma swój indeks w spisie towarów. System wyznacza indeks wskazanego towaru w określony sposób (jest to element rozumienia wypowiedzi), przy czym w naszym systemie będzie nim czterocyfrowa liczba – np. 1215.
- System analizuje pierwsze słowo komendy, by sprawdzić, czy chodzi o kupno, czy też o sprzedaż (oczywiście z jego punktu widzenia).
3.1. Jeśli chodzi o kupno, akceptuje wydane polecenie bezwarunkowo, a w swoim spisie towarów zwiększa posiadaną liczbę sztuk towaru o danym indeksie.
3.2. Jeśli chodzi o sprzedaż, najpierw sprawdza w spisie towarów, czy posiada na stanie wymaganą liczbę sztuk towaru o danym indeksie. Jeśli nie posiada, odmawia wykonania polecenia; jeśli zaś posiada, dokonuje transakcji sprzedaży, po czym zapisuje ubytek w spisie towarów.
Piszemy program
Słownik
Typy tokenów czyli znaczenia wyrazów
Musimy zawrzeć w słowniku wszystkie wyrazy, które system ma zrozumieć. Będziemy je nazywać tokenami. Przedtem jednak musimy określić dopuszczalne znaczenia wyrazów, zwane typami tokenów. Odpowiedni kod źródłowy w języku Python ma następującą postać:
tokens = ( 'OPERATE', 'NUMBER', 'SIZE', 'COLOR', 'MATERIAL' 'KIND', )
Zdefiniowaliśmy sześć typów tokenów, odpowiadających różnym znaczeniom tokenów. Za chwilę określimy na przykład, że w słowniku dopuszczalny jest token “Buy” należący do typu OPERATE.
Zadanie dla czytelnika
Zgadnij, do których z powyższych typów przydzielone zostaną kolejne tokeny zdania:
“Buy 5 large blue metal boxes”.
Odpowiedź już za chwilę...
Zawartość słownika
W kolejnym kroku dla każdego typu określamy, jakie tokeny do niego należą i co system ma zrobić, gdy określony token pojawi się w komendzie użytkownika:
Operacje
def t_OPERATE(t): r'Buy | Sell' return t
Powyższy kod przypisuje do typu OPERATE dwa wyrazy: Buy lub Sell (drugi wiersz) oraz nakazuje systemowi zapamiętać podaną operację, gdy taki token pojawi się w komendzie użytkownika (trzeci wiersz).
Liczby
def t_NUMBER(t): r'\d+' t.value = int(t.value) return t
Powyższy kod przypisuje do typu NUMBER dowolny napis składający się z samych cyfr (drugi wiersz), przekształca taki napis składający się z cyfr na odpowiadającą mu wartość liczbową (trzeci wiersz) i nakazuje systemowi zapamiętać tę wartość, gdy taki token pojawi się w komendzie użytkownika (czwarty wiersz).
Atrybuty
def t_MATERIAL(t): r'metal | plastic' if t.value =='metal': t.value = 1 elif t.value == 'plastic': t.value = 2 return t
Powyższy kod przypisuje do typu MATERIAL napisy metal lub plastic, a następnie (i tu nowość!) nakazuje systemowi zapamiętać albo 1 (gdy materiałem jest metal), albo 2 (gdy materiałem jest plastic).
Analogicznie do typu MATERIAL utworzyć możemy pozostałe typy tokenów:
def t_COLOR(t): r'(black | white | red | green | blue)' if t.value =='black': t.value = 1 elif t.value == 'white': t.value = 2 elif t.value == 'red': t.value = 3 elif t.value == 'green': t.value = 4 elif t.value == 'blue': t.value = 5 return t def t_SIZE(t): r'tiny | small | big | large' if t.value =='tiny': t.value = 1 elif t.value =='small': t.value = 2 elif t.value =='big': t.value = 3 elif t.value =='large': t.value = 4 return t
Rodzaje towarów
def t_KIND(t): r'box(es)? | ring(s)?' if t.value[0] =='b': t.value = 1 else: t.value = 2 return t
W definicji typu KIND zadbaliśmy, aby w słowniku zawarte były wyrazy box, boxes, ring oraz rings. Gdy pojawia się token związany z pudełkami, zapamiętywana jest wartość 1, a gdy token związany z krążkami – wartość 2.
Indeks towarów
W jaki sposób komputerowy sprzedawca wyznacza indeks towaru, o którym mowa w komendzie? Pomaga mu w tym podręczna tabela, która zawiera wartości określone dla poszczególnych tokenów.
Wartość | KIND | SIZE | MATERIAL | COLOR |
---|---|---|---|---|
1 | box(es) | tiny | metal | black |
2 | ring(s) | small | plastic | white |
3 | big | red | ||
4 | large | green | ||
5 | blue |
Ilekroć system musi wyznaczyć indeks kupowanego lub sprzedawanego artykułu, odwołuje się on do powyższej tabeli – przy czym istotna jest kolejność jej kolumn.
Aby na przykład wyznaczyć indeks towaru:
large blue metal boxes
system musi najpierw ułożyć podane cechy towaru w takiej kolejności, na jaką wskazują kolumny przedstawionej powyżej tabeli, a mianowicie: KIND, SIZE, MATERIAL, COLOR, czyli:
boxes large metal blue
a następnie przypisać po kolei poszczególnym tokenom cyfry w zgodzie z treścią powyższej tabeli:
1415
(boxes → 1; large → 4 metal → 1 blue → 5)
Indeksem towaru large blue metal boxes jest zatem 1415, przy czym zauważmy, że nawet gdyby kolejność atrybutów towaru podana została w innej kolejności, indeks artykułu pozostałby niezmieniony.
Reguły gramatyki
Wirtualny sprzedawca zrozumie tylko takie komendy, które będą zgodne z regułami gramatyki podanymi przez autora programu. Każda reguła mówi o tym, w jaki sposób mniejsze części zdania składać można w większe całości. Na przykład reguła mówiąca o tym, że w skład komendy wchodzić muszą po kolei: token oznaczający operację, token oznaczający liczbę oraz składowa określająca artykuł mogłaby mieć następującą postać:
command: OPERATE NUMBER article
W odpowiedniej regule gramatyki można również zawrzeć instrukcje, które ma wykonać program, gdy rozpozna, że analizowany tekst jest zgodny z tą właśnie regułą.
Reguły dotyczące atrybutów
Zacznijmy od reguł najprostszych, dotyczących atrybutów towarów:
def p_attribute_color(p): 'attribute : COLOR' p[0] = p[1]
Reguła o nazwie p_attribute_color (nazwa reguły musi zaczynać się od zapisu “p_”, po którym autor programu może przyjąć dowolny zapis) mówi, że część zdania o nazwie attribute może się składać wyłącznie z tokenu typu COLOR (drugi wiersz). Trzeci wiersz mówi, że w takiej sytuacji wartość składowej attribute (oznaczanej jako p[0]) ma być tożsama z wartością tokenu COLOR (p[1] oznacza pierwszą – w tym wypadku jedyną – składową).
Tak więc atrybut black, jeśli pojawi się w komendzie, będzie miał wartość 1, atrybut white – wartość 2, itd.
def p_attribute_material(p): 'attribute : MATERIAL' p[0] = 10 * p[1]
Reguła o nazwie p_attribute_material mówi, że część zdania o nazwie attribute może się alternatywnie składać wyłącznie z tokenu typu MATERIAL (drugi wiersz). Trzeci wiersz mówi, że w takiej sytuacji wartość składowej attribute ma być tożsama z wartością tokenu MATERIAL pomnożonego przez 10.
Tak więc atrybut metal ma wartość 10, atrybut plastic ma wartość 20.
Analogicznie:
def p_attribute_size(p): 'attribute : SIZE' p[0] = 100 * p[1]
Reguły dotyczące artykułów
Opis artykułu może składać się z samego rodzaju artykułu:
def p_article_kind(p): 'article : KIND' p[0] = 1000 * p[1]
“Wartość” pudełka wynosi 1000, a “wartość” krążka – 2000.
Opis artykułu może być poprzedzony dowolnego typu atrybutem:M.
def p_article_attribute(p): 'article : attribute article' p[0] = p[1] + p[2]
Dodanie atrybutu do opisu artykułu zwiększa jego wartość. Na przykład zapis “boxes” ma wartość 1000, zapis “metal boxes” – 1010, “blue metal boxes” – 1015, a “large blue metal boxes” – 1415.
Reguła dotycząca komendy
Najważniejsza reguła dotyczy całej komendy:
# Main parser rule (command) def p_command(p): 'command : OPERATE NUMBER article' index = p[3] #Buy article if p[1] == 'Buy': tab[index] += p[2] print('OK. I am buying ' + str(p[2])+ ' new articles indexed as ' + str(index) +'.') print('No of articles in shop: '+ str(tab[index])) #Sell article elif p[1] == 'Sell': if p[2] > tab[index]: print('I do not have as many articles as you want.') else: tab[index] -= p[2] print('OK. I am selling ' + str(p[2])+ ' articles indexed as ' + str(index) + '.') print('No of articles in shop: ' + str(tab[index]))
Analizowana komenda musi składać się kolejno z: operacji (token OPERATE), liczby artykułów (token NUMBER) oraz opisu artykułu (article) – pierwszy wiersz. Jako indeks towaru, o którym mowa w komendzie, wyznaczana jest wartość artykułu (p[3]). W przypadku kupna zwiększa się odpowiednio informację w spisie towarów o wartość tokenu NUMBER oraz podaje odpowiedni komunikat. W przypadku sprzedaży dodatkowo sprawdza się, czy w spisie dostępna jest odpowiednia liczba artykułów.
Przykładowa sesja rozmowy z wirtualnym sprzedawcą
What can I do for you?
Buy 10 large blue metal boxes
OK. I am buying 10 new articles indexed as 1415.
No of articles in shop: 10
What can I do for you?
Buy 5 tiny white plastic rings
OK. I am buying 5 new articles indexed as 2122.
No of articles in shop: 5
What can I do for you?
Sell 5 large blue metal boxes
OK. I am selling 5 articles indexed as 1415.
No of articles in shop: 5
What can I do for you?
Sell 3 tiny white plastic rings
OK. I am selling 3 articles indexed as 2122.
No of articles in shop: 2
What can I do for you?
Sell 3 tiny white plastic rings
I do not have as many articles as you want.
What can I do for you?
Bye
>>>
Post Scriptum
Poniżej znajduje się pełny kod programu – z niezbędnymi “dekoracjami”.
# Import lexer and parser from ply module import ply.lex as lex import ply.yacc as yacc # List of token types. tokens = ( 'NUMBER', 'OPERATE', 'SIZE', 'KIND', 'COLOR', 'MATERIAL' ) # Token types may be defined as regular expressions, e.g. r'Buy | Sell' def t_OPERATE(t): r'Buy | Sell' return t def t_MATERIAL(t): r'metal | plastic' if t.value =='metal': t.value = 1 elif t.value == 'plastic': t.value = 2 return t def t_COLOR(t): r'(black | white | red | green | blue)' if t.value =='black': t.value = 1 elif t.value == 'white': t.value = 2 elif t.value == 'red': t.value = 3 elif t.value == 'green': t.value = 4 elif t.value == 'blue': t.value = 5 return t def t_SIZE(t): r'tiny | small | big | large' if t.value =='tiny': t.value = 1 elif t.value =='small': t.value = 2 elif t.value =='big': t.value = 3 elif t.value =='large': t.value = 4 return t def t_KIND(t): r'box(es)? | ring(s)?' if t.value[0] =='b': t.value = 1 else: t.value = 2 return t def t_NUMBER(t): r'\d+' t.value = int(t.value) return t # Lexer error handling rule (Handle words out of vocabulary) def t_error(t): print("Illegal character '%s'" % t.value[0]) t.lexer.skip(1) # Ignore white spaces t_ignore = ' \t' # Main parser rule (command) def p_command(p): 'command : OPERATE NUMBER article' index = p[3] #Buy article if p[1] == 'Buy': tab[index] += p[2] print('OK. I am buying ' + str(p[2])+ ' new articles indexed as ' + str(index) +'.') print('No of articles in shop: '+ str(tab[index])) #Sell article elif p[1] == 'Sell': if p[2] > tab[index]: print('I do not have as many articles as you want.') else: tab[index] -= p[2] print('OK. I am selling ' + str(p[2])+ ' articles indexed as ' + str(index) + '.') print('No of articles in shop: ' + str(tab[index])) # Parser rule (attribute) def p_attribute_color(p): 'attribute : COLOR' p[0] = p[1] # Parser rule (attribute) def p_attribute_material(p): 'attribute : MATERIAL' p[0] = 10 * p[1] # Parser rule (attribute) def p_attribute_size(p): 'attribute : SIZE' p[0] = 100 * p[1] # Parser rule (article - stop) def p_article_kind(p): 'article : KIND' p[0] = 1000 * p[1] # Parser rule (article - recursion) def p_article_attribute(p): 'article : attribute article' p[0] = p[1] + p[2] # Syntax error handling rule def p_error(p): print("Syntax error in input!") ####################################### #Main program #Initialize table of articles (zero articles at the beginning) tab = [] for index in range(3000): tab.append(0) #Build the lexer lexer = lex.lex() #Tokenize (short version) # for tok in lexer: # print(tok) #Build the parser parser = yacc.yacc() #Main loop while True: s = input('What can I do for you? \n') if s == 'Bye': break parser.parse(s)