====== Spam II ====== * Dotazy a odpovědi * Testík * Diskuse DÚ: confusion matrix, funkce kvality * Interaktivní programování: ''BinaryConfusionMatrix'' metodou TDD * Diskuse: jak dokončit krok 3 spam filtru? Co se bude odevzdávat? * Hádanka * Diskuse DÚ: jednoduché filtry ===== Dotazy a odpovědi ===== ===== Programovací testík ===== [[courses:b4b33rph:internal:kratke_testiky:start|Zadání]] na interních stránkách. ===== Diskuse ===== * Jak hodnotit kvalitu filtru? Co jsou to TP, FP, TN, FN? > {{page>..:..:internal:cviceni:spam:krok3#matice_zamen&editbtn&noheader}} * Definice funkce kvality > {{page>..:..:internal:cviceni:spam:krok3#funkce_kvality&editbtn&noheader}} ===== TDD: BinaryConfusionMatrix ===== Příprava: * Projděte si specifikace třídy ''BinaryConfusionMatrix''. * Jaké testy (scénáře použití) dokážete pro danou třídu vymyslet? > {{page>..:..:internal:cviceni:spam:krok3#info&editbtn&noheader}} ==== Krok 0 ==== > {{page>..:..:internal:cviceni:spam:krok3#teachers_0&editbtn&noheader}} ++++ 0. Vytvoření kostry | TDD nás nabádá, abychom vždy psali testy před produkčním kódem. Budeme používat modul ''unittest'' a testy pro třídu ''BinaryConfusionMatrix'' (dále jen BCF) budeme psát jako metody třídy ''BinaryConfusionMatrixTest''. Založme tedy soubor ''test_confmat.py'' a vložme do něj následující kostru: import unittest class BinaryConfusionMatrixTest(unittest.TestCase): pass if __name__=='__main__': unittest.main() Poslední část (''if __name__...'') je nutná tehdy, když chcete testy spouštět v shellu nebo v debuggeru. Pokud využijete nástroj Testing z PyCharm, tato část nutná není. Spusťte test. Měli byste vidět, že proběhlo 0 testů. Doplňme teď do testu import třídy BCF z modulu ''confmat'', kterou chceme testovat: from confmat import BinaryConfusionMatrix Když modul s testem spustíme (viz předchozí cvičení), dostaneme ''ImportError''. Jak by ne, modul ''confmat'' neexistuje. Založme tedy soubor ''confmat.py''. Po opětovném spuštění modulu s testem opět dostaneme ''ImportError'', protože se snažíme importovat třídu BCM, která v modulu zatím neexistuje. Založme tedy v modulu ''confmat'' prázdnou třídu ''BinaryConfusionMatrix''. class BinaryConfusionMatrix: pass Test nyní nekončí chybou, bezva, máme připravenou kostru pro naše testy. V modulu ''test_confmat'' ovšem zatím žádné skutečné testy nemáme. Zkusme je tedy postupně vytvořit. ++++ ==== Krok 1 ==== ++++ 1. Po vytvoření třídy jsou čítače vynulovány | Doplňte do testové třídy následující test. Aby třída byla obecná, musíme jí při vytváření předat kódy, jimiž budeme označovat spam a korektní emaily. Následně se pokusíme získat matici ve formě slovníku a tento slovník porovnáme s očekávanými hodnotami, které by měly být hned po vytvoření objektu samozřejmě nulové. def test_countersAreZero_afterCreation(self): # Prepare fixture cm = BinaryConfusionMatrix(pos_tag=SPAM_TAG, neg_tag=HAM_TAG) # Exercise the SUT cmdict = cm.as_dict() # Assert self.assertDictEqual(cmdict, {'tp': 0, 'tn': 0, 'fp': 0, 'fn': 0}) Spusťte test. Test skončí s chybou ''NameError'', protože nemáme definovány konstanty ''SPAM_TAG'' a ''HAM_TAG''. Jelikož by tyto konstanty měly mít platnost co nejmenší, aby neovlivňovaly žádné další testy, definujme tyto konstanty přímo v modulu ''test_confmat''. Jejich hodnoty si pro účely testování můžete zvolit prakticky libovolně. SPAM_TAG = 'SPAM' HAM_TAG = 'OK' Spusťte test. Dostanete výjimku ''TypeError'', protože třída BCM nemá definovanou metodu ''__init__'' se dvěma argumenty. Doplňme ji tedy (do třídy BCM). Zapamatujme si kódy pro spam a ham a inicializujme čítače TP, TN, FP, FN. def __init__(self, pos_tag, neg_tag): self.tp = 0 self.tn = 0 self.fp = 0 self.fn = 0 self.pos_tag = pos_tag self.neg_tag = neg_tag Spusťte test. Třída teď jde instanciovat. Dostáváme ale ''AttributeError'', protože třída BCM nemá metodu ''as_dict()''. Doplňme ji do třídy BCM: def as_dict(self): """Return the conf. mat. as a dictionary.""" return {'tp': self.tp, 'tn': self.tn, 'fp': self.fp, 'fn': self.fn} Spusťte test. Měl by proběhnout bez chyby. V čem je hodnota tohoto testu? Zaručí nám, že pokud se v budoucnu budeme 'hrabat' v metodě ''__init__()'' nebo ''as_dict()'' a zaneseme tam nechtěně nějakou chybu, pak ji test s velkou pravděpodobností odhalí. ++++ ==== Krok 2 ==== > {{page>..:..:internal:cviceni:spam:krok3#teachers_2&editbtn&noheader}} ++++ 2. Metoda update() správně upraví čítač TP | Vytvořme test: def test_updatesTPcorrectly(self): # Prepare fixture cm = BinaryConfusionMatrix(pos_tag=SPAM_TAG, neg_tag=HAM_TAG) # Exercise the SUT cm.update(SPAM_TAG, SPAM_TAG) # Assert self.assertDictEqual(cm.as_dict(), {'tp': 1, 'tn': 0, 'fp': 0, 'fn': 0}) Spusťte test. Dostáváme ''AttributeError'', protože BCM nemá metodu ''update()''. Vytvořme v BCM prázdnou metodu ''update()'' se dvěma parametry ''truth'' a ''prediction''. Spusťte test. Metoda nyní lze zavolat, ale nefunguje tak, jak by měla (''AssertionError''). Ve výsledku testu i vidíme, kde se vrácený slovník liší od očekávaného. Doplňme tedy její tělo: očekáváme, že metoda zvýší čítač TP. Vyplňme tedy kód metody: def update(self, truth, prediction): """Compare the truth with the prediction and increment the related counter.""" self.tp += 1 Spusťte test. Měl by projít bez chyby. ++++ ==== Krok 3 ==== ++++ 3. Ruční refactoring opakujícího se kódu | Prohlédněte si vaše 2 testovací metody. Nemají něco společného? Mají, zdá se, že ve všech testech budeme potřebovat čerstvou instanci BCM. Extrahujme tedy její vytvoření do metody ''setUp()''. Abychom vytvořenou instanci BCM mohli používat i v jiných metodách, musíme ji přiřadit do členské proměnné testové třídy. Z testových metod pak odstraníme vytváření BCM a budeme rovnou používat tu instanci v členské proměnné (použití ''self''). Kód testové třídy by po změnách měl vypadat takto: class BinaryConfusionMatrixTest(unittest.TestCase): def setUp(self): # Prepare fixture self.cm = BinaryConfusionMatrix(pos_tag=SPAM_TAG, neg_tag=HAM_TAG) def test_countersAreZero_afterCreation(self): # Exercise the SUT cmdict = self.cm.as_dict() # Assert self.assertDictEqual(cmdict, {'tp': 0, 'tn': 0, 'fp': 0, 'fn': 0}) def test_updatesTPcorrectly(self): # Exercise the SUT self.cm.update(SPAM_TAG, SPAM_TAG) # Assert self.assertDictEqual(self.cm.as_dict(), {'tp': 1, 'tn': 0, 'fp': 0, 'fn': 0}) Spusťte testy. Měly by proběhnout v pořádku. Ale možná jste při opravách někde něco přehlédli, testy by vám pak na onom místě měly selhat. ++++ ==== Krok 4 ==== ++++ 4. Metoda update() správně upraví čítač TN | Vytvořme test: def test_updatesTNcorrectly(self): # Exercise the SUT self.cm.update(HAM_TAG, HAM_TAG) # Assert self.assertDictEqual(self.cm.as_dict(), {'tp': 0, 'tn': 1, 'fp': 0, 'fn': 0}) Spusťte test. Metoda selhává (''AssertionError''), protože místo čítače TN inkrementovala čítač TP. Upravme tedy její tělo co nejjednodušším způsobem tak, aby procházel test: def update(self, truth, prediction): """Compare the truth with the prediction and increment the related counter.""" if prediction == self.pos_tag: self.tp += 1 elif prediction == self.neg_tag: self.tn += 1 Spusťte test. Měl by projít bez chyby. ++++ ==== Krok 5 ==== ++++ 5. Metoda update() správně upraví čítač FP | Vytvořme test: def test_updatesFPcorrectly(self): # Exercise the SUT self.cm.update(HAM_TAG, SPAM_TAG) # Assert self.assertDictEqual(self.cm.as_dict(), {'tp': 0, 'tn': 0, 'fp': 1, 'fn': 0}) Spusťte test. Metoda selhává (''AssertionError''). Upravme tedy její tělo co nejjednodušším způsobem tak, aby procházel test: def update(self, truth, prediction): """Compare the truth with the prediction and increment the related counter.""" if prediction == self.pos_tag: if truth == prediction: self.tp += 1 else: self.fp += 1 elif prediction == self.neg_tag: self.tn += 1 Spusťte test. Měl by projít bez chyby. ++++ ==== Krok 6 ==== ++++ 6. Metoda update() správně upraví čítače FN | Vytvořme test: def test_updatesFNcorrectly(self): # Exercise the SUT self.cm.update(SPAM_TAG, HAM_TAG) # Assert self.assertDictEqual(self.cm.as_dict(), {'tp': 0, 'tn': 0, 'fp': 0, 'fn': 1}) Spusťte test. Metoda selhává (''AssertionError''). Upravme tedy její tělo co nejjednodušším způsobem tak, aby procházel test: def update(self, truth, prediction): """Compare the truth with the prediction and increment the related counter.""" if prediction == self.pos_tag: if truth == prediction: self.tp += 1 else: self.fp += 1 elif prediction == self.neg_tag: if truth == prediction: self.tn += 1 else: self.fn += 1 Spusťte test. Měl by projít bez chyby. ++++ ==== Krok 7 ==== > {{page>..:..:internal:cviceni:spam:krok3#teachers_7&editbtn&noheader}} ++++ 7. Co má metoda update dělat při chybném vstupu? | Metoda ''update(truth, prediction)'' přebírá 2 argumenty, jejichž hodnoty by vždy měly být rovny pozitivnímu štítku nebo negativnímu štítku, které jsme zadali při vytváření instannce BCM. Co má metoda dělat, pokud některý z těchto argumentů tuto podmínku nesplňuje? Metoda by v takovém případě nejspíš měla vyhodit výjimku ''ValueError''. Jak se dá toto otestovat? Pokud v tuto chvíli nevíte nic o výjimkách, zkuste si o nich [[http://openbookproject.net/thinkcs/python/english3e/exceptions.html|něco přečíst]], diskutujte se cvičícím, ptejte se na fóru. Nevadí, pokud tomuto tématu nerozumíte, ale bude dobře, když o něm budete alespoň vědět. def test_update_raisesValueError_forWrongTruthValue(self): # Assert and exercise the SUT with self.assertRaises(ValueError): self.cm.update('a bad value', SPAM_TAG) def test_update_raisesValueError_forWrongPredictionValue(self): # Assert and exercise the SUT with self.assertRaises(ValueError): self.cm.update(SPAM_TAG, 'a bad value') Spusťte test. Měl by selhat. Upravme metodu ''update()'' třeba takto: def update(self, truth, prediction): """Compare the truth with the prediction and increment the related counter.""" if truth not in (self.pos_tag, self.neg_tag): raise ValueError('The "truth" parameter can be either %s or %s.' \ % (self.pos_tag, self.neg_tag)) if prediction not in (self.pos_tag, self.neg_tag): raise ValueError('The "prediction" parameter can be either %s or %s.' \ % (self.pos_tag, self.neg_tag)) if prediction == self.pos_tag: Spusťte test. Měl by proběhnout bez chyb. Hmm, ale opakuje se tam kód na kontrolu hodnot argumentů. Proveďte refactoring. ++++ ==== Krok 8 ==== > {{page>..:..:internal:cviceni:spam:krok3#teachers_8&editbtn&noheader}} ++++ 8. Refactoring s pomocí IDE | Metoda ''update()'' teď má na začátku dva téměř stejné bloky kódu: jeden kontroluje hodnotu parametru ''truth'', druhý hodnotu parametru ''prediction''. Udělejme pro kontrolu hodnoty zvláštní metodu, kterou jen pak 2x zavoláme. Označte následující blok kódu: if truth not in (self.pos_tag, self.neg_tag): raise ValueError('The "truth" parameter can be either %s or %s.' \ % (self.pos_tag, self.neg_tag)) Z menu **Refactor** nebo z nabídky **Refactor** v kontextovém menu vyberte položku **Extract** a následně **Method**. V dialogu **Extract method** zvolte jako jméno metody třeba ''check_value_of'' a klikněte na tlačítko **OK**. Ve vašem kódu by měly nastat 2 změny: * Automaticky byla vytvořena metoda ''check_value_of()'' s následujícím tělem: def check_value_of(self, truth): if truth not in (self.pos_tag, self.neg_tag): raise ValueError('The "truth" parameter can be either %s or %s.' \ % (self.pos_tag, self.neg_tag)) * Na místě, kde se tento kód vykonával, je teď jen volání nové metody. Spusťte testy, abyste si ověřili, že IDE nezměnilo funkčnost kódu. Nyní bychom ale měli trochu upravit tělo nové metody a také jejím voláním nahradit druhý opakující se blok kódu. To už musíme udělat ručně. Upravme tedy ''check_value_of()'' třeba takto: def check_value_of(self, value): """Raise ValueError if var does not contain either positive or negative tag.""" if value not in (self.pos_tag, self.neg_tag): raise ValueError('The arguments may be either %s or %s.' \ % (self.pos_tag, self.neg_tag)) A začátek metody ''update()'' upravte takto: def update(self, truth, prediction): """Compare the truth with the prediction and increment the related counter.""" self.check_value_of(truth) self.check_value_of(prediction) if prediction == self.pos_tag: ... Spusťte testy pro kontrolu, zda jsme do kódu nezanesli nějakou chybu. ++++ ==== Krok 9 ==== ++++ 9. Metoda compute_from_dicts() správně updatuje čítače | Vytvořme test: def test_computeFromDicts_allCasesOnce(self): # Prepare fixture truth = {1: SPAM_TAG, 2: SPAM_TAG, 3: HAM_TAG, 4: HAM_TAG} prediction = {1: SPAM_TAG, 2: HAM_TAG, 3: SPAM_TAG, 4: HAM_TAG} # Excercise the SUT self.cm.compute_from_dicts(truth, prediction) # Assert self.assertDictEqual(self.cm.as_dict(), {'tp': 1, 'tn': 1, 'fp': 1, 'fn': 1}) Spusťme test. Selhává (''AttributeError''), protože třída BCM nemá metodu ''compute_from_dicts()''. Vytvořme ji. def compute_from_dicts(self, truth_dict, pred_dict): """Update the matrix using the corresponding values from both dictionaries. It is assumed that both dicts have the same keys. """ for key in truth_dict: self.update(truth_dict[key], pred_dict[key]) Spusťe test. Měl by proběhnout v pořádku. ++++ ==== Krok 10 ==== ++++ 10. Diskuse: další testy? | * Jak bychom dál měli otestovat metodu compute_from_dicts()? * Co když slovníky nebudou mít shodné klíče? V naší aplikaci by se to stát nemělo, bude-li dobře napsaná. Ale co když uděláme někde chybu a tato situace nastane? * Měli bychom to v tichosti přejít a prostě napočítat matici záměn jen z hodnot se stejnými klíči? Nebo vyhodit výjimku? ++++ ===== Hádanka ===== /* [[https://forms.gle/kFqxMq54p7hjByPu6|101]] | [[https://forms.gle/3nLTaoxHoVa3zQVk7|102]] | [[https://forms.gle/QaZvFUtncp1Ne1CH7|103]] | [[https://forms.gle/vzsGD3QyLgcqMk1g7|104]] | [[https://forms.gle/oMqTMgeh4ao4uiwL8|105]] | [[https://forms.gle/uyin2XUR2RuBFCna6|106]] | [[https://forms.gle/MuffuZrZW4u4wWx8A|107]] | [[https://forms.gle/4kfZisGQAbhkvdvHA|108]] | [[https://forms.gle/yARdGc2Gxtt5qAudA|109]] */ > {{page>courses:b4b33rph:internal:puzzles#cviceni_9}} ===== Programovací trik ===== ++++ Volání funkce s argumenty připravenými v seznamu nebo slovníku | Jak volat metodu či funkci, pro jejíž argumenty máme hodnoty připravené v seznamu nebo slovníku? Mějme následující funkci: def print_args(a, b): print(a, b) Předpokládejme, že bychom pro tuto funkci měli hodnoty parametrů ''a'' a ''b'' připraveny v poli nebo slovníku: >>> arg_list = ['Seznam', 'hodnot'] >>> arg_dict = {'a': 'Slovnik', 'b': 'hodnot'} Kdybychom chtěli funkci spustit s těmito hodnotami, museli bychom seznam i slovník ručně rozebrat na prvky a ty pak předat funkci, např. takto: >>> print_args(arg_list[0], arg_list[1]) Seznam hodnot >>> print_args(arg_dict['a'], arg_dict['b']) Slovnik hodnot Takový způsob volání funkce je ale nepohodlný, obzvlášť tehdy, bude-li mít seznam či slovník mnohem více prvků. Python proto nabízí **trik**: >>> print_args(*arg_list) Seznam hodnot >>> print_args(**arg_dict) Slovnik hodnot Tento trik můžete s výhodou použít, až budete hodnoty prvků matice záměn získané metodou ''as_dict()'' přeávat do funkce ''quality_score()''. ++++ ===== Diskuse: jednoduché filtry ===== Pokud jste tak ještě neučinili, přečtěte si specifikace [[..:spam:krok4|kroku 4]] úlohy Spam filtr: * Jaké metody filtr musí mít? * Musejí být implementovány všechny? * Jak tedy bude implementace jednoduchých filtrů přibližně vypadat? ===== Kontrola DÚ z minulého cvičení ===== * Zkontrolovat, zda funkce ''read_classification_from_file()'' a třída ''Corpus'' procházejí unit testem. * Pokud ne, probrat výstup z unit testu a nasměrovat na správnou cestu! ===== Programovací úloha ===== * Dokončete [[..:spam:krok3|krok 3]] úlohy spam filtr, tedy funkci pro výpočet kvality filtru. Od neděle za týden budete modul ''quality'' společně s dalšími potřebnými moduly odevzdávat do upload systému! * Můžete začít pracovat na [[..:spam:krok4|kroku 4]] úlohy spam filtr, tj. implementujte 3 jednoduché filtry. ====== Domácí úkol ====== ===== Programování ===== V příštích dnech je termín **odevzdání 1. části úlohy Spam filtr!** Postupujte podle [[..:spam:specifikace#sp-eval|specifikací]]. Termín najdete v odevzdávacím systému. * Vyzkoušejte si/proveďte/dokončete programování třídy BinaryConfusionMatrix podle výše uvedeného návodu. Snažte se přitom pochopit nejen co děláte, ale i proč to děláte. * Pokud jste nestihli na cvičení, dokončete [[..:spam:krok3|krok 3]] úlohy spam filtr, tedy funkci pro výpočet kvality filtru. * Dokončete [[..:spam:krok4|krok 4]] úlohy spam filtr, implementujte 3 jednoduché filtry: * Zkuste je aplikovat na nějaký korpus. * Použijte funkci z kroku 3 na odhad kvality filtru. * Jaké hodnoty kvality jste dostali pro naše 3 jednoduché filtry??? ===== Příprava ===== Přečtete si něco o dědičnosti: > {{section>..:spam:krok4#priprava:dedicnost&noheader&editbtn}} Příprava na krok 5 úlohy spam filtr: > {{section>..:spam:krok5#priprava&noheader&editbtn}}