Search
BinaryConfusionMatrix
Zadání na interních stránkách.
Příprava:
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:
unittest
BinaryConfusionMatrixTest
test_confmat.py
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í.
if __name__…
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:
confmat
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.
ImportError
confmat.py
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.
test_confmat
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ě.
NameError
SPAM_TAG
HAM_TAG
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.
TypeError
__init__
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:
AttributeError
as_dict()
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í.
__init__()
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:
update()
truth
prediction
AssertionError
def update(self, truth, prediction): """Compare the truth with the prediction and increment the related counter.""" self.tp += 1
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:
setUp()
self
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})
4. Metoda update() správně upraví čítač TN
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
5. Metoda update() správně upraví čítač FP
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
6. Metoda update() správně upraví čítače FN
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})
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
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?
update(truth, prediction)
Metoda by v takovém případě nejspíš měla vyhodit výjimku ValueError. Jak se dá toto otestovat?
ValueError
Pokud v tuto chvíli nevíte nic o výjimkách, zkuste si o nich 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.
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.
check_value_of
Ve vašem kódu by měly nastat 2 změny:
check_value_of()
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))
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: ...
9. Metoda compute_from_dicts() správně updatuje čítače
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.
compute_from_dicts()
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.
10. Diskuse: další testy?
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:
a
b
>>> 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().
quality_score()
Pokud jste tak ještě neučinili, přečtěte si specifikace kroku 4 úlohy Spam filtr:
read_classification_from_file()
Corpus
quality
V příštích dnech je termín odevzdání 1. části úlohy Spam filtr! Postupujte podle specifikací. Termín najdete v odevzdávacím systému.
Přečtete si něco o dědičnosti:
Prostudujte si, jak v OOP funguje a jak se v Pythonu realizuje dědičnost. Informace najdete např. v oficiálním Python tutoriálu, nebo v [Wentworth2012], kapitola 23 (navazuje na kapitolu 22). 2018/07/17 13:25
Prostudujte si, jak v OOP funguje a jak se v Pythonu realizuje dědičnost. Informace najdete např.
Příprava na krok 5 úlohy spam filtr:
Rozmyslete si, co by měla třída vašeho trénovacího korpusu (TrainingCorpus) umět, aby vám usnadnila učení filtru. T.j. co musí umět navíc vzhledem k třídě Corpus. 2018/07/17 13:25
TrainingCorpus