INF 2820 V2018: Innleveringsoppgave 1 - løsningsforslag Oppgave 1: Endelige tilstandsmaskiner (20 poeng) Denne oppgaven kan gjøres i JFLAP. Du anbefales likevel å løse den med papir og penn først for å få eksamenstrening. Så kan du bruke JFLAP til å kontrollere løsningen din. a. Lag en ikke-deterministisk endelig tilstandsmaskin (NFA) som beskriver språket L1=L(a*b(a+c)* + ac(b+a)), der alfabetet er A={a, b, c}. (Symbolet + er her disjunksjon). b. Lag en deterministisk maskin (DFA) som beskriver det samme språket. 1
c. Lag en tilstandsmaskin som beskriver komplementspråket til L1. d. Hvilke av følgende uttrykk er i L1? i. abc ii. acb iii. bac iv. bbc v. aaaa vi. aaab vii. aaba viii. abaa ix. abab x. baaa abc acb bac aaab aaba abaa baaa 2
Opgave 2: NFA, algoritme for gjenkjenning, klokkeslettuttrykk (25 poeng) Lag en NFA for norske klokkeslettutrykk. Du kan bruke 12 timers klokke og "gammel tellemåte", eks. 'ti over halv fire'. Du kan også ta med uttrykksmåten 'femten førti', men det er frivillig. Eksempler: Skal være med tre halv fem ti på sju fem over halv åtte kvart over ni kvart på ti Skal ikke være med halv kvart på halv ni fem over kvart på ti ti på halv halv over to halv på tolv Det skal være minst ett uttrykk for alle minutter gjennom dagen. NFA-en skal skrives på samme format som template_abrs.nfa og testes med nfa.py på eksemplene over. Skriv nettverket ditt til en fil etter modell av template_abrs.nfa og test netteverket på eksemplene. Innlevering: Filen med NFA og kjøringseksempel med eksempeluttrykkene både de som skal være med og de som ikke skal være med. NFA START: 0 FINAL: 5 EDGES: 0 # 1 0 'kvart' 3 0 MINUTE 4 1 'halv' 2 1 HOUR 5 2 HOUR 5 3 PRE 2 4 PRE 1 ABRS: HOUR: 'ett', 'to', 'tre', 'fire', 'fem', 'seks', 'syv', 'åtte', 'ni', 'ti', 'elleve', 'tolv' MINUTE: 'ett', 'to', 'tre', 'fire', 'fem', 'seks', 'syv', 'åtte', 'ni', 'ti', 'elleve', 'tolv', 'tretten', 'fjorten' PRE: 'på', 'over' 3
Kjøringseksempel from nfa_smart import * correct = ("tre", "halv fem", "ti på syv", "fem over halv åtte", "kvart over ni", "kvart på ti", "fem på ti", "ti over tolv", "fem på halv syv") incorrect = ("halv", "kvart på halv ni", "fem over kvart på ti", "ti på halv", "halv over to", "halv på tolv", "halv på kvart ni", "ti over fem over ett") nfa = NFAFromFile('klokken.nfa') for lst, status in ((correct, True), (incorrect, False)): for item in lst: if nrec(item.split(), nfa)!= status: if status: print("should have been recognized: '{}'".format(item)) else: print("should not have been recognized: '{}'".format(item)) Oppgave 3: Innlesning av tekst (20 poeng) Vi skal nå bevege oss fra de mer teoretiske eksemplene til språkteknologiske anvendelser. I dette emnet skal vi arbeide mye med tekster. Teksten "Python og NLTK i INF2820, V2018" er konvertert til ren tekst og lagret på IFIs maskiner her /projects/nlp/inf2820/python_inf2820_v2017.txt. a) Les den inn i en interaktiv Python-sesjon som en streng. Kall den "pyt_raw". Oppskriften finner du i seksjon 3.1 i NLTK-boka, underavsnitt "Reading local files". Hvis du ser noe rusk i teksten, så rens den. with open("/projects/nlp/inf2820/python_inf2820_v2018.txt") as infile: pyt_raw = infile.read() In [277]: pyt_clean = pyt_raw[1:] Fjerner '\ufeff' i begynnelsen av strengen. Hvis vi også ønsker å fjerne "dots" (som kommer fra punktmarkert liste) kan vi f.eks. bruke. In [280]: pyt_clean_2 = re.sub(chr(8226), '', pyt_clean) chr(8226) er "dot". b) Når vi videre skal arbeide med en tekst, kan det være en fordel å dele den opp i en liste av ord, der hvert ord er en streng. Den enkleste måten å gjøre dette på er ved å bruke split i python. >>> pyt_words1 = pyt_raw.split() NLTK gir oss også et annet alternativ: >>> pyt_words2 = nltk.word_tokenize(pyt_raw) Hva blir forskjellen på de to? Ser du fordeler ved å bruke word_tokenize? (Obs! NLTKs word_tokenize er optimalisert for engelsk og kan gi noen rare resultat for norsk.) 4
Den klare forskjellen er at nltk.word_tokenize splitter av tegn fra slutten av ord som egne tokens, mens split() lar de sitte på ordet, f.eks. 'språkteknologi, '.' vs 'språkteknologi.' I tillegg vil split() beholde en web-adresse som 'https:'//docs.python.org/3/' mens nltk.word_tokenize vil splitte den til 3 tokens: 'https', ':', '//docs.python.org/3/' c) Et ord som "jeg" er det samme om det står først i en setning og skrives "Jeg". Tilsvarende for andre ord. For en del anvendelser er det derfor en fordel å gjøre om teksten til bare små bokstaver før vi går videre. Gjør om alle ordene i pyt_words2 til små bokstaver, og kall resultatet pyt_low. d) Plukk ut alle ordforekomstene i pyt_low som inneholder en av de norske bokstavene æ, ø, å. Hvor mange slike ordforekomster er det? e) Vi er nå interessert i hvor mange forskjellige ord det er i teksten som inneholder en av de norske bokstavene. Plukk ut de unike forekomstene. Hvor mange er det? In [296]: pyt_low = [w.lower() for w in pyt_words2] In [297]: æøå = [w for w in pyt_low if re.search('[æøå]', w)] In [298]: len(æøå) Out[298]: 66 In [299]: æøå_types = set(æøå) In [300]: len(æøå_types) Out[300]: 26 f) Vi skal nå skrive de unike forekomstene til en fil. Når vi skal skrive noe til en fil, kan vi åpne fila ved >>> f=open(<filnavn>, 'w') Vi kan skrive til fila ved å bruke >>> f=write(<det vi vil skrive>) Og til slutt lukke fila ved >>> f.close() For mer om dette se https://docs.python.org/3/tutorial/inputoutput.html#reading-and-writing-files Skriv de unike forekomstene av ord som inneholder æ, ø eller å til en fil kalt norske.txt, ett ord på hver linje. Det kan være nyttig å vite at '\n' betyr ny linje i Python-strenger, prøv >>> print('jeg\ner\n\nglad') with open("/uio/kant/ifi-ansatt-u06/jtl/norske.txt", 'w') as outfile: for w in æøå_types: outfile.write(w+'\n') Innlevering: Svar på spørsmålene i (b), (d) og (e), forklar med kode hvordan du løste oppgaven. Levér også fila norske.txt. 5
Oppgave 4: Regulære uttrykk og bearbeiding av tekst (20 poeng) I teksten vi har lest inn er det en del uttrykk som ikke er vanlige ord, inkludert stinavn på Unix-format, som "/projects/nlp/nltk_data" tall For en del prosessering kan det være bedre å skifte ut slike uttrykk, som ofte bare forekommer en gang, med en betegnelse som sier oss at her står det et stinavn, eller her står det et tall. a) Skriv et regulært uttrykk i Python for stinavn. In [332]: path1 = "(/[^/\s]+)+/?" Denne anerkjenner alt som begynner med /, inneholder en streng uten whitespace før neste / og kan ende på en / eller ikke. Litt uklart hva oppgaven mener med stinavn. Et stinavn trenger ikke starte med "/", men hvis vi tar med stinavn som ikke starter med "/", er det vanskelig å ikke få med for mye, dvs. annen bruk av "/" i en tekst. Et stinavn kan jo også starte med "." eller med "..". Vi kan jo gå ut i fra at stinavn som ellers ville startet uten en "/ "starter med "./ "Et litt mer generelt uttrykk er da In [333]: path2 = ".?.?(/[^/\s]+)+/?" En annen ting er at et stinavn faktisk kan inneholde white space som blanke, f.eks. "min fil". (Ganske vanlig i Windows). Men når vi skal referere til slike stinavn må vi bruke escape. Det er derfor rimelig at vi også i en tekst med slike navn bruker escape. Da kan teksten se f.eks. slik ut "/home/min\ fil" og et regulært uttrykk som matcher dette In [339]: path3 = "(.?.?(/[^/\s] \\ )+)+/?" b) Skriv en prosedyre som bruker Pythons regulære uttrykk (seksjon 3.4 og delvis 3.5 i NLTK-boka) som tar en tokenisert tekst og skifter alle tokens som er stinavn med "<path>" og alle tokens som er tall med "<num>". Med en tokenisert tekst, mener vi her en liste av strenger som f.eks. pyt_words2. Bruk resultatet ditt på pyt_words2. def replace_tokens(tokens): path_regex =... num_regex = "^\d+(\.\d+)?$" replaced = [] for token in tokens: if re.search(path_regex, token): replaced.append("<path>") elif re.search(num_regex, token): replaced.append("<num>") else: replaced.append(token) return replaced 6
c) Vi vil i stedet skifte ut stinavn og tall på en tekst som ikke er tokenisert, dvs. en tekst som er en streng. Skriv en prosedyre som leser en streng og som skifter ut alle substrenger som er stinavn med "path" og alle substrenger som er tall med "<num>". Hint: Det enkleste her er å bruke sub-metoden i Pythons re-modul, se: https://docs.python.org/3/howto/regex.html#search-and-replace def replace_string(text): path_regex =... num_regex = "\d+(\.\d+)?" text = re.sub(path_regex, "<path>", text) text = re.sub(num_regex, "<num>", text) return text Oppgave 5: Stemming (15 poeng) a) Se på Seksjon 3.6 Normalizing Text i NLTK-boka. Last inn LancasterStemmer og PorterStemmer. Hvordan vil de to takle eksempelet med "split/splitting"? Hvordan vil de to stemme "goldfishes"? Innlevering: Svar på spørsmålet og utskrift som viser resultater fra kjøring. Using <LancasterStemmer>: split - split splitting - splitting goldfishes - goldf Using <PorterStemmer>: split - split splitting split goldfishes goldfish Porter-stemmeren gjør det vi ønsker, mens Lancaster gir ikke samme stamme for split og splitting. Lancaster gir også uforventede resultater for goldfishes, mens Porter igjen gjør det vi forventer. b) Porter-stemmer er langt på vei blitt en standard stemmer for engelsk som mange tyr til. For referanseformål er den frosset og blir ikke utviklet videre. Men det er mulig med forbedringer og dette prøver bl.a. Snowball-stemmeren på. Den kan lastes i NLTK ved >>> snowball = nltk.snowballstemmer('english') Vi skal sammenlikne Porter-stemmer og Snowball-stemmer. Bruk teksten 'grail.txt' som kan lastes som en liste av ord med kommandoen >>> grail = nltk.corpus.webtext.words('grail.txt') Finn deretter 6 forskjellige ordformer hvor Snowball-stemmeren og Porter-stemmeren gir forskjellig resultat. Vi regner ikke forskjeller mellom stor og liten bokstav som forskjeller i denne sammenheng.. (Hint: Finn alle ordforekomster hvor de to gir forskjellig resultat, og bruk set( ) til å plukke forskjellige ordformer fra disse.) 7
Innlevering: 6 forskjellige ord som blir stemmet forskjellig av de to og resultatet de to stemmerne gir for disse ordene. Ordform Porter Snowball vicious viciou vicious slightly slightli slight commune commun commune his hi his jesus jesu jesus herring her herring - SLUTT - 8