====== Spam filter - krok 6: Vlastní filtr ====== Implementujte vlastní, co nejlepší filtr. [[.unit_testing|Testy]] ke kroku 6: * samostatně {{test6_filter.zip|}} nebo * společně se všemi předchozími testy {{test6_all.zip|}}. ===== Příprava ===== Rozmyslete si, jaké vnitřní mechanismy byste chtěli ve svém filtru použít, podle čeho by se váš filtr měl rozhodovat mezi korektním emailem a spamem a jak by se dal takový filtr učit z dat. Projděte si také sekci s několika tipy, které by se vám při konstrukci filtru mohly hodit. ===== Programovací tipy ===== Na tomto místě bychom vám rádi poskytli pár užitečných tipů ke konstrukci vlastních filtrů. Zdaleka ne vše, co je v této sekci uvedeno, budete nutně potřebovat. Např. nepředpokládáme, že budete využívat modul ''email'' nebo regulární výrazy (i když samozřejmě můžete). ++++ Tokenizace | Rozhodnete-li se vytvořit filtr, který potřebuje v textu emailu identifikovat jednotlivá slova či jiné důležité fráze, budete nejspíš provádět tzv. tokenizaci textu ([[wp>Tokenization]]). Prostudujte si [[http://docs.python.org/release/3.1.3/library/stdtypes.html#string-methods|operace s řetězci]], které lze dělat. Zvlášť bychom vás chtěli upozornit na následující: * [[http://docs.python.org/release/3.1.3/library/stdtypes.html#str.lower|str.lower()]] a [[http://docs.python.org/release/3.1.3/library/stdtypes.html#str.lower|str.upper()]], pokud např. nebudete chtít rozlišovat mezi 'RESULT', 'Result' a 'result', * [[http://docs.python.org/release/3.1.3/library/stdtypes.html#str.lstrip|str.lstrip()]], [[http://docs.python.org/release/3.1.3/library/stdtypes.html#str.rstrip|str.rstrip()]], [[http://docs.python.org/release/3.1.3/library/stdtypes.html#str.strip|str.strip()]], pokud budete chtít z nějakého řetězce třeba odstranit úvodní, koncové, nebo obojí mezery, * [[http://docs.python.org/release/3.1.3/library/stdtypes.html#str.startswith|str.startswith()]], [[http://docs.python.org/release/3.1.3/library/stdtypes.html#str.endswith|str.endswith()]], pokud budete chtít zjistit, zda nějaký řetězec začíná či končí jiným řetězcem, * ''short_str in long_str'', pokud budete chtít zjistit, zda se jeden řetězec vyskytuje ve druhém, * [[http://docs.python.org/release/3.1.3/library/stdtypes.html#str.find|str.find()]], pokud budete chtít zjistit, kde přesně se jeden řetězec vyskytuje ve druhém, * [[http://docs.python.org/release/3.1.3/library/stdtypes.html#str.replace|str.replace()]], budete-li chtít např. nějaké podřetězce ignorovat a nahradit je např. mezerami nebo jiným řetězcem, * [[http://docs.python.org/release/3.1.3/library/stdtypes.html#str.translate|str.translate()]], budete-li chtít hromadně zaměnit jednotlivé znaky za jiné, a konečně * [[http://docs.python.org/release/3.1.3/library/stdtypes.html#str.split|str.split()]], budete-li chtít dlouhý řetězec rozsekat na pole kratších řetězců podle specifikovaného znaku nebo podle bílých znaků. Uvedený výčet berte pouze jako upozornění na metody, které by vám mohly být užitečné. Jednoduchý tokenizer může vypadat např. takto: shortphrase = """ Lorem ipsum dolor sit amet consectetuer Aliquam orci wisi pretium Praesent. Lorem pellentesque ac ipsum Aenean Sed pretium Pellentesque adipiscing enim at. Nisl tempus tempor interdum dictumst wisi consequat id at eu platea. Volutpat adipiscing Curabitur pede Aenean interdum Nulla congue nibh pharetra Maecenas. Convallis aliquam leo leo nisl mi cursus nisl enim Nam leo. Duis nunc nulla leo augue nunc pretium sit Aenean Nam. """ # Replace dots with spaces shortphrase = shortphrase.translate(str.maketrans('.',' ')) # Transform the letters to lowercase and cut the string into tokens. tokens = shortphrase.lower().split() print(tokens) ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetuer', 'aliquam', 'orci', 'wisi', 'pretium', 'praesent', 'lorem', 'pellentesque', 'ac', 'ipsum', 'aenean', 'sed', 'pretium', 'pellentesque', 'adipiscing', 'enim', 'at', 'nisl', 'tempus', 'tempor', 'interdum', 'dictumst', 'wisi', 'consequat', 'id', 'at', 'eu', 'platea', 'volutpat', 'adipiscing', 'curabitur', 'pede', 'aenean', 'interdum', 'nulla', 'congue', 'nibh', 'pharetra', 'maecenas', 'convallis', 'aliquam', 'leo', 'leo', 'nisl', 'mi', 'cursus', 'nisl', 'enim', 'nam', 'leo', 'duis', 'nunc', 'nulla', 'leo', 'augue', 'nunc', 'pretium', 'sit', 'aenean', 'nam'] >>> ++++ ++++ Čítače | V minulých cvičeních jsme si ukazovali, jak udělat čítač výskytů písmen nebo slov z obyčejného slovníku. Protože se toto využití slovníku vyskytuje poměrně často, obsahuje Python třídu ''[[http://docs.python.org/release/3.1.3/library/collections.html?highlight=collections.counter#collections.Counter|collections.Counter]]'', která je odvozena od slovníku (má tedy všechny jeho metody), ale zároveň má upravené chování: * ''counter['neexistující klíč']'' vrací 0, nikoli ''KeyError''. (Nemusíme tedy používat metodu ''get()''.) Oproti obyčejnému slovníku má také pár užitečných metod navíc: * [[http://docs.python.org/release/3.1.3/library/collections.html?highlight=collections.counter#collections.Counter.update|Counter.update()]] * [[http://docs.python.org/release/3.1.3/library/collections.html?highlight=collections.counter#collections.Counter.most_common|Counter.most_common()]] * přetížené operace sčítání a odčítání - je možné dva objekty třídy ''Counter'' sčítat či odečítat. Budeme-li pokračovat v předchozím příkladu, následující řádky counter = Counter(tokens) print(counter) print() print(counter.most_common(10)) vypíší Counter({'leo': 4, 'pretium': 3, 'aenean': 3, 'nisl': 3, 'at': 2, 'enim': 2, 'nunc': 2, 'interdum': 2, 'nam': 2, 'pellentesque': 2, 'lorem': 2, 'aliquam': 2, 'ipsum': 2, 'wisi': 2, 'sit': 2, 'adipiscing': 2, 'nulla': 2, 'pharetra': 1, 'ac': 1, 'pede': 1, 'duis': 1, 'sed': 1, 'eu': 1, 'id': 1, 'praesent': 1, 'volutpat': 1, 'platea': 1, 'convallis': 1, 'congue': 1, 'dolor': 1, 'tempus': 1, 'nibh': 1, 'consequat': 1, 'cursus': 1, 'curabitur': 1, 'maecenas': 1, 'amet': 1, 'dictumst': 1, 'mi': 1, 'augue': 1, 'tempor': 1, 'orci': 1, 'consectetuer': 1}) [('leo', 4), ('pretium', 3), ('aenean', 3), ('nisl', 3), ('at', 2), ('enim', 2), ('nunc', 2), ('interdum', 2), ('nam', 2), ('pellentesque', 2)] ++++ ++++ Procházení uspořádaných prvků slovníku (čítače) | Často chceme procházet prvky slovníku (čítače) uspořádané podle hodnoty četnosti. Python používá k řazení funkci ''[[http://docs.python.org/release/3.1.3/library/functions.html?highlight=sort#sorted|sorted()]]''. Zkusme, co udělají následující příkazy: counter2 = Counter(counter.most_common(5)) sorted(counter2) Výsledek: [('aenean', 3), ('at', 2), ('leo', 4), ('nisl', 3), ('pretium', 3)] Je to seznam dvojic (token, četnost tokenu) seřazený abecedně podle tokenu. My ale chceme seznam seřadit podle četnosti tokenu. Musíme funkci ''sorted()'' dát najevo, aby používala druhý prvek z dvojice jako primární klíč k řazení. To lze udělat např. pomocí tzv. lambda funkce. (V tuto chvíli to můžete prostě brát jako recept, kterak slovník řadit nikoli podle klíče, ale podle hodnoty). Navíc bychom jí měli sdělit, že chceme, aby řadila sestupně: sorted(counter2, key=lambda pair: (pair[1],pair[0]), reverse=True) Dostaneme kýžený výsledek: [('leo', 4), ('pretium', 3), ('nisl', 3), ('aenean', 3), ('at', 2)] ++++ ++++ Tokenizace pro pokročilé aneb regulární výrazy | Pro pokročilejší nebo zvídavější studenty uveďme, že Python umožňuje pracovat s [[http://docs.python.org/release/3.1.3/library/re.html?highlight=re#module-re|regulárními výrazy]]. Ty umožňují velmi flexibilně popsat vzor v řetězci a následně najít všechny části řetězce, které danému vzoru odpovídají, příp. řetězec podle nich rozdělit, atd. Použití takového regulárního výrazu k jednoduché tokenizaci může vypadat např. takto: import re pattern = r"""(?im)[a-zA-Z\$]+[\w\d\$\-']*""" compiledre = re.compile(pattern) tokens = compiledre.findall(message) counter = Counter(tokens) ++++ ++++ Struktura mailu | Nechcete-li s emaily pracovat jen jako s nestrukturovaným řetězcem a raději byste jej nějakým způsobem rozebrali na části a ty pak zpracovávali zvlášť, odkazujeme vás na [[http://docs.python.org/release/3.1.3/library/email.html?highlight=email#module-email|modul ''email'']] a jeho funkce: * ''[[http://docs.python.org/release/3.1.3/library/email.parser.html#email.message_from_string|email.message_from_string()]]'', ''[[http://docs.python.org/release/3.1.3/library/email.parser.html#email.message_from_file|email.message_from_file()]]'' umožní vytvořit objekt třídy Message z řetězce či přímo ze souboru. * metoda ''[[http://docs.python.org/release/3.1.3/library/email.message.html#email.message.Message.walk|Message.walk()]]'' pak umožní systematicky email procházet po jednotlivých částech. ++++ ===== Třída ''MyFilter'' ===== Úkol: * Vytvořte vlastní funkční spam filtr. K čemu nám to bude: * Budete jej odevzdávat a budete za něj hodnoceni. Následující specifikace pro třídu ''MyFilter'' jsou povinné a musíte jim vyhovět! ==== Specifikace ==== V modulu ''filter.py'' vytvořte třídu ''MyFilter'', která bude schopna * převzít trénovací korpus při zavolání metody ''train(train_corpus_dir)'', * naučit filtr na trénovacích emailech (pokud se tak rozhodnete) a * ohodnotit všechny emaily testovacího korpusu při zavolání metody ''test(test_corpus_dir)'', tj. vytvoří v adresáři testovacího korpusu soubor ''!prediction.txt''. Metoda by měla být schopná pracovat i tehdy, když předtím nedojde k natrénování filtru metodou ''train()''. * Filtr má za úkol detekovat spamy, očekává se tedy, že **SPAMy bude považovat za pozitivní** příklady a **HAMy za negativní**, detaily viz [[krok3|krok 3]]. * Vnitřní mechanismy vašeho spam filtru jsou zcela na vás! * **Časový limit**: Učení filtru a ohodnocení testovacího korpusu by celkem nemělo zabrat více než 5 minut; filtru by mělo stačit i výrazně méně času. ==== Použití třídy MyFilter ==== Typické použití třídy ''MyFilter'' bude následující: from filter import MyFilter filter = MyFilter() filter.train('/path/to/training/corpus') # Tento adresář bude obsahovat soubor !truth.txt filter.test('/path/to/testing/corpus') # V tomto adresáři metoda vytvoří soubor !prediction.txt Protože by ale metoda ''test()'' měla být schopná pracovat i bez předchozího volání metody ''train()'', je přípustné i toto použití (a bude také testováno): from filter import MyFilter filter = MyFilter() filter.test('/path/to/testing/corpus') # V tomto adresáři metoda vytvoří soubor !prediction.txt Při hodnocení kvality predikcí filtru samozřejmě budeme vždy volat metodu ''train()'' před metodou ''test()''. ==== Trénování ==== * Pokud se váš spam filtr nebude umět učit, můžete trénovací corpus ignorovat. Pokud jste se rozhodli držet našich doporučení, měli byste ve třídě ''MyFilter'' implementovat metodu ''train()'', která ovšem může být prázdná. * V trénovacím korpusu zcela jistě bude soubor ''!truth.txt'' s informací o skutečné třídě emailů v daném korpusu. ==== Testování ==== * Váš filtr **musí** v adresáři testovacího korpusu **vytvořit soubor ''!prediction.txt''**, v němž pro každý soubor s emailem v testovacím korpusu uvede řádek ''nazevsouboru OK'', jedná-li se (podle mínění filtru) o korektní email, nebo ''nazevsouboru SPAM'', jedná-li se (podle mínění filtru) o spam. * //Váš filtr nesmí při testování v žádném případě využívat soubor ''!truth.txt'' v adresářích testovacích korpusů. Testy budou probíhat tak, že tam žádný takový soubor nebude!// ===== Možné varianty filtrů ===== - Natvrdo "zadrátovaný" filtr, který se neumí učit. Takový filtr bude mít patrně prázdnou metodu ''train()''. V metodě ''test()'' pak bude nějakým způsobem využívat postupy zvolené autorem, podle nichž pro každý testovací email určí, jedná-li se o spam nebo ham. - Filtr využívající předem připravené externí informace (např. předem naučený filtr). Filtr bude mít asi také prázdnou metodu ''train()''. V metodě ''test()'' se pak bude rozhodovat na základě externích informací uložených v souboru (souborech) uploadovaných spolu se zdrojovým kódem filtru. Důvodem k takovéto "architektuře" může být např. to, že proces učení filtru je časově náročný - filtr byl studentem naučen offline a předem. (Pokud chce student v tomto případě body za to, že se filtr umí učit, musí na cvičení svého cvičícího přesvědčit, že předem připravené informace skutečně pocházejí z procesu učení.) Jiným důvodem použití této varianty může být využití nějakého externího slovníku, apod. - Učicí se filtr. Filtr nemá prázdnou metodu ''train()'' a ta je [[courses:b4b33rph:cviceni:spam:specifikace#casove_pozadavky|dostatečně rychlá]], že zvládne extrahovat potřebné znalosti z informací uložených v řádově stovkách emailů (velikost trénovací sady).