====== 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}}