1 file changed, 106 insertions(+), 6 deletions(-) final.py | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- modified final.py @@ -45,8 +45,12 @@ import nltk # In[92]: import nltk -huvel = nltk.corpus.CategorizedPlaintextCorpusReader("/home/lukes/edu/python/huvel",r".*\.txt", - encoding="utf8",cat_pattern=r".*-(.*),") # Čistě formální drobnost: za čárkou píšeme v Pythonu mezeru. huvel = nltk.corpus.CategorizedPlaintextCorpusReader("/home/lukes/edu/python/huvel", r".*\.txt", encoding="utf8", cat_pattern=r".*-(.*),") # Když je řádek před začátkem závorky hodně dlouhý, často se příkaz pro lepší čitelnost píše takto: huvel = nltk.corpus.CategorizedPlaintextCorpusReader( "/home/lukes/edu/python/huvel", r".*\.txt", encoding="utf8", cat_pattern=r".*-(.*),") # In[93]: @@ -121,14 +125,32 @@ fd # In[98]: # Import stačí v rámci souboru provést jen jednou, takže když máte `import nltk` a `import csv` v # jedné z předchozích buněk, které jste už vyhodnotila, není nutné je zde provádět znovu (taky to # není problém, Python pozná, že už jste import provedla a nebude ho opakovat). Na druhou stranu, # pokud buňky vyhodnocujete většinou spíš na přeskáčku, tak chápu, že chcete mít importy pro jistotu # v každé buňce, která je využívá (když pak soubor otevřete znovu, můžete rovnou vyhodnotit # kteroukoli buňku, nemusíte nejdřív hledat buňky s importy). import nltk import csv # Nebál bych se v zájmu čitelnosti občas přidat prázdný řádek (typicky se tak oddělují jednotlivé # funkce nebo importy) :) def load_fdist(path): fdist = nltk.FreqDist() with open(path, newline='') as csvfile: csv_in = csv.reader(csvfile, delimiter=';', quotechar='"') for row in csv_in: - fdist[row[1]]=int(row[2]) #pozor, musim ulozit cislo a ne retezec! # Je to sice jen zvyklost, ne nutnost, ale `=` v Pythonu píšeme bez mezer, pokud je ve # volání funkce (`foo(bar="baz")`) a s mezerami pokud ne (`foo = "baz"`). (Tyhle # formální drobnosti, které nemají vliv na výstup programu, znějí jako hloupost, ale váš # kód se pak bude mnoheme lépe číst ostatním programátorům, a vám se bude mnohem lépe # číst zase cizí kód, pokud tyhle zvyklosti budeme všichni víceméně dodržovat :) A vím, # že jsme na ně v semestru nekladli příliš velký důraz, neberte to tedy vůbec jako výtku # nebo tak něco.) fdist[row[1]] = int(row[2]) #pozor, musim ulozit cislo a ne retezec! return fdist @@ -269,10 +291,42 @@ fd.N() from scipy.stats import chi2_contingency def is_keyword_candidate(word, text_fd, ref_fd): #vratim vysledek statisticke vyznamnosti podle prikladu v zadani, zmenim jen hladinu z 0.05 na 0.001 # Pozor, místo `text_fd.N()` je potřeba použít `text_fd.N() - text_fd[word.lower()]` (a obdobně # pro `ref_fd`), v kontingenční tabulce potřebujeme počet výskytů cílového slova a počet výskytů # ostatních slov, ne celkovou délku textu. (Byť u někdy -- třeba u velkého korpusu a malé # frekvence cílového slova -- může být rozdíl zanedbatelný.) # # Další věc je, že testujete pouze, zda se poměr v textu statisticky významně *liší* od poměru v # korpusu, takže vrátíte `True` i v případě, že bude *nižší*. O slově, které je v textu významně # méně zastoupeno než v korpusu, bychom jako o kandidátovi na klíčové asi uvažovat neměli. # # Také bych operaci kvůli přehlednosti rozepsal do více kroků a pojmenoval si mezivýsledky (byť # jinak je naprosto v pořádku). Viz níže funkce `is_keyword_candidate_upraveno()` pro představu, # jak by všechny výše navržené úpravy mohly vypadat. p=chi2_contingency([[text_fd[word.lower()], text_fd.N()], [ref_fd[word.lower()], ref_fd.N()]], lambda_="log-likelihood")[1] # Velmi dobře! Někdy mají začátečníci tendenci tento krok rozepisovat pomocí `if`, přitom # porovnávací operátory (`<`, `>=` a spol.) rovnou vracejí pravdivostní hodnoty `True` a # `False` (což zde správně využíváte). return p<0.001 - def is_keyword_candidate_upravene(word, text_fd, ref_fd): word = word.lower() # je-li frekvence v textu menší než v ref. korpusu, můžeme # rovnou rozhodnout, že slovo nebude klíčové; musíme porovnávat # relativní frekvence, protože text a korpus budou skoro jistě # jinak dlouhé if text_fd.freq(word) < ref_fd.freq(word): return False # nyní dopočítáme hodnoty pro kontingenční tabulku # počet výskytů slova `word` v textu a ref. korpusu text_freq = text_fd[word] ref_freq = ref_fd[word] # počet slov jiných než `word` v textu a v ref. korpusu text_zbytek = text_fd.N() - text_freq ref_zbytek = ref_fd.N() - ref_freq p = chi2_contingency([[text_freq, text_zbytek], [ref_freq, ref_zbytek]], lambda_="log-likelihood")[1] return p < 0.001 # **OVĚŘENÍ:** Otestujte si, zda vaše funkce dobře funguje. V následující buňce jsou proporce v textu i v referenčním korpusu stejné, výsledek by tedy měl být `False`. @@ -296,8 +350,13 @@ is_keyword_candidate("kočka",text_fd_test,ref_fd_test ) text_fd_test = nltk.FreqDist() ref_fd_test = nltk.FreqDist() -text_fd_test["kočka"] = 90 -text_fd_test["zbytek"] = 1910 # Zkuste zde schválně upravit hodnoty tak, aby relativní frekvence v textu byla *nižší* než v # referenčním korpusu; uvidíte, že původní verze funkce `is_keyword_candidate()` vrátí `True` i v # tomto případě (bude-li rozdíl ve frekvencích dostatečně velký na to, aby byl statisticky # významný). text_fd_test["kočka"] = 10 text_fd_test["zbytek"] = 1990 ref_fd_test["kočka"] = 3000 ref_fd_test["zbytek"] = 197000 @@ -321,7 +380,11 @@ is_keyword_candidate("kočka", text_fd_test, ref_fd_test) #DIN je hodnota ziskana jako 100x podil rozdilu a souctu relativnich frekvenci v testovanem textu a korpusu #relativni frekvence je pocet vyskytu v textu / celkovy pocet slov def din(word, text_fd, ref_fd): # Jen bych přidal kvůli čitelnosti mezi operátory pár mezer, ale jinak skvěle! :) word2=word.lower() #ulozim si slovo word jako lower case # Případně mají ještě objekty typu `nltk.FreqDist` metodu `.freq()`, která relativní frekvenci # spočítá rovnou: # Rel_ft = text_fd.freq(word2) Rel_ft=text_fd[word2]/text_fd.N() #relativni cetnost slova v textu Rel_fc=ref_fd[word2]/ref_fd.N() #relativni cetnost slova v referencnim korpusu DIN=(Rel_ft-Rel_fc)/(Rel_ft+Rel_fc)*100 #spocitam DIN @@ -432,13 +495,38 @@ def keywords(text, ref_fd): for token in text: #projdu oznackovany text #fdist[token[0].lower()]+=1 # prictu jednicku ke kazdemu vyskytu slova a ulozim do distribuce fdist[token[0]]+=1 #pokud chci prevod na mala pismena, zakomentuji tento radek a odkomentuji ten nad nim a naopak # Výsledek je správně, ale obecně tento způsob inicializace frekvenční distribuce používáme spíš # v případě, že máme k dispozici předpočítaná data. Pokud máme k dispozici seznam tokenů, je # výhodnější (rychlejší, kratší, přehlednější...) předat tento seznam rovnou konstruktoru: # `fdist = nltk.FreqDist(text)`. #uWords={token[0].lower():token[2] for token in text} #ulozim si unikatni slova s jejich znackami uWords={token[0]:token[2] for token in text}#pokud chci prevod na mala pismena, zakomentuji tento radek a odkomentuji ten nad nim a naopak # To je hezký nápad jak se vypořádat s omezením klíčových slov na autosémantika! Jen pozor: # pokud se nějaký slovní tvar vyskytuje v textu s více značkami, tak ve slovníku `uWords` # zachováme jen tu poslední odpovídající značku (pokaždé když narazíme na nějaký slovní tvar, # tak pokud už ve slovníku je, jeho záznam přepíšeme tím novým výskytem). # Takže teoreticky vám může utéct potenciální klíčové slovo, které se v textu objevuje homonymně # jako autosémantikum i synsémantikum, pokud je poslední výskyt synsémantický. Na druhou stranu, # je otázka, jestli takové případy vůbec existují (záleží na použitém systému morfologického # značkování), a pokud bychom chtěli být opravdu důslední, museli bychom homonymii ošetřit i na # straně referenčních frekvenčních hodnot (a rozlišit tak např. frekvenci tvaru "stát" jako # slovesa vs. jako substantiva). # Jinak je vaše řešení obecně moc pěkné, stručné a přehledné. for word in fdist: #projdu distribuci if fdist[word]>2: #zajimaji me slova, ktera se vysytnou alespon 3x if re.match("[NAVD]", uWords[word]): #zajimaji me jen substantiva, adjektiva, adverbia, verba if is_keyword_candidate(word,fdist,ref_fd): #zjistim, jestli je frekvence slova statisticky vyznamne vyssi kw.append((word,din(word,fdist,ref_fd),fdist[word])) #pokud ano, pridam si do seznamu slovo, DIN a pocet vyskytu v textu # Funkce `sorted()` vytvoří nový seznam, do něhož vloží stejné položky, jen seřazené, takže po # tu dobu, co existují oba, zabírají společně dvakrát tolik místa v paměti. Pokud původní pořadí # už k ničemu nepotřebujete, bývá výhodnější použít metodu `.sort()` (takže `kw.sort(...)`), # která položky "zpřehází" úsporněji v rámci původního seznamu. return sorted(kw, key=lambda podle: podle[1], reverse=True) #seradim vysledny seznam podle DIN huvel = nltk.corpus.CategorizedPlaintextCorpusReader("/home/lukes/edu/python/huvel",r".*\.txt", @@ -496,6 +584,14 @@ sorted(seznam, key=itemgetter(0, 2)) # Pricemz nektera slova slova se v referencnim korpusu po takove uprave ani nevyskytuji a tudiz je DIN=100 (napr. KSC vs. ksc) # Musel by se upravit referencni korpus (sloucit slova a jejich vyskyty) # Ano, velmi dobře! Při analýze klíčových slov většinou právě začínáme od case sensitive analýzy # slovních tvarů, protože se stává, že specifický je konkrétní zápis (např. relativně běžné slovo # může být i jméno firmy, pokud je psané s velkým počátečním písmenem) nebo tvar (viz Husákovy # projevy a tvar "minulém", který se vyskytuje v kolokaci "v minulém roce"). Teprve pak případně # přistupujeme ke case insensitive analýze, případně analýze lemmat (kde by myslím např. ve vztahu k # těm Husákovým projevům lemma "minulý" jako nijak zvlášť klíčové nevyvstalo, jde tu skutečně o # klíčovost onoho specifického tvaru). V té chvíli je pak pochopitelně třeba příslušně upravit # referenční data. with open("/home/lukes/edu/python/huvel/1975-Husák,_Gustáv.txt") as file: txt = file.read() @@ -574,3 +670,7 @@ print_din(kw,90) # do parametru funkce is_keyword_candidate by slo doplnit: # hladina alfa # otazkou je, zda vyhodnocovat slova jako takova, nebo pracovat s lemmaty - vysledky by byly objektivnejsi # Pěkné nápady! Ohledně lemmat viz výše -- ale kdybyste psala celou vypilovanou knihovnu na práci s # klíčovými slovy, rozhodně by bylo šikovné mít možnost jednoduše (jedním parametrem) přepínat mezi # case sensitive analýzou, case insensitive a lemmaty :)