CourseWare Wiki
Switch Term
Winter 2023 / 2024
Winter 2022 / 2023
Winter 2021 / 2022
Winter 2020 / 2021
Winter 2019 / 2020
Winter 2018 / 2019
Older
Search
Log In
b191
courses
b4b33rph
cviceni
program_po_tydnech
tyden_09
Warning
This page is located in archive. Go to the latest version of this
course pages
.
Differences
This shows you the differences between two versions of the page.
View differences:
Side by Side
Inline
Go
Link to this comparison view
Go
Go
courses:b4b33rph:cviceni:program_po_tydnech:tyden_09 [2018/12/04 16:25]
courses:b4b33rph:cviceni:program_po_tydnech:tyden_09 [2018/12/04 16:25]
(current)
Line 1:
Line 1:
+
====== 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 =====
+
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:
+
+
<code python>
+
import unittest
+
+
class BinaryConfusionMatrixTest(unittest.TestCase):
+
pass
+
+
if __name__=='__main__':
+
unittest.main()
+
</code>
+
+
Poslední část (''if <nowiki>__name__</nowiki>...'') 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:
+
+
<code python>
+
from confmat import BinaryConfusionMatrix
+
</code>
+
+
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''.
+
+
<code python>
+
class BinaryConfusionMatrix:
+
pass
+
</code>
+
+
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é.
+
<code python>
+
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})
+
</code>
+
+
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ě.
+
+
<code python>
+
SPAM_TAG = 'SPAM'
+
HAM_TAG = 'OK'
+
</code>
+
+
Spusťte test. Dostanete výjimku ''TypeError'', protože třída BCM nemá definovanou metodu ''<nowiki>__init__</nowiki>'' 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.
+
+
<code python>
+
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
+
</code>
+
+
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:
+
<code python>
+
def as_dict(self):
+
"""Return the conf. mat. as a dictionary."""
+
return {'tp': self.tp, 'tn': self.tn, 'fp': self.fp, 'fn': self.fn}
+
</code>
+
+
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ě ''<nowiki>__init__</nowiki>()'' 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:
+
<code python>
+
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})
+
</code>
+
+
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:
+
<code python>
+
def update(self, truth, prediction):
+
"""Compare the truth with the prediction and increment the related counter."""
+
self.tp += 1
+
</code>
+
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:
+
<code python>
+
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})
+
</code>
+
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:
+
<code python>
+
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})
+
</code>
+
+
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:
+
<code python>
+
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
+
</code>
+
Spusťte test. Měl by projít bez chyby.
+
++++
+
+
==== Krok 5 ====
+
++++ 5. Metoda update() správně upraví čítač FP |
+
Vytvořme test:
+
<code python>
+
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})
+
</code>
+
+
Spusťte test. Metoda selhává (''AssertionError''). Upravme tedy její tělo co nejjednodušším způsobem tak, aby procházel test:
+
<code python>
+
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
+
</code>
+
Spusťte test. Měl by projít bez chyby.
+
++++
+
+
==== Krok 6 ====
+
++++ 6. Metoda update() správně upraví čítače FN |
+
Vytvořme test:
+
<code python>
+
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})
+
</code>
+
+
Spusťte test. Metoda selhává (''AssertionError''). Upravme tedy její tělo co nejjednodušším způsobem tak, aby procházel test:
+
<code python>
+
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
+
</code>
+
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.
+
+
<code python>
+
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')
+
</code>
+
+
Spusťte test. Měl by selhat. Upravme metodu ''update()'' třeba takto:
+
+
<code python>
+
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:
+
</code>
+
+
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:
+
<code python>
+
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))
+
</code>
+
+
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:<code python> 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))</code>
+
* 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:
+
<code python>
+
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))
+
</code>
+
+
A začátek metody ''update()'' upravte takto:
+
<code python>
+
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:
+
...
+
</code>
+
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:
+
<code python>
+
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})
+
</code>
+
+
Spusťme test. Selhává (''AttributeError''), protože třída BCM nemá metodu ''compute_from_dicts()''. Vytvořme ji.
+
+
<code python>
+
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])
+
</code>
+
+
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 =====
+
> {{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:
+
<code python>
+
def print_args(a, b):
+
print(a, b)
+
</code>
+
+
Předpokládejme, že bychom pro tuto funkci měli hodnoty parametrů ''a'' a ''b'' připraveny v poli nebo slovníku:
+
+
<code python>
+
>>> arg_list = ['Seznam', 'hodnot']
+
>>> arg_dict = {'a': 'Slovnik', 'b': 'hodnot'}
+
</code>
+
+
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:
+
+
<code python>
+
>>> print_args(arg_list[0], arg_list[1])
+
Seznam hodnot
+
>>> print_args(arg_dict['a'], arg_dict['b'])
+
Slovnik hodnot
+
</code>
+
+
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**:
+
+
<code python>
+
>>> print_args(*arg_list)
+
Seznam hodnot
+
>>> print_args(**arg_dict)
+
Slovnik hodnot
+
</code>
+
+
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í =====
+
* 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. Tuto funkci budete za týden odevzdávat do upload systému a bude se hodnotit!!!
+
* 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}}
+
courses/b4b33rph/cviceni/program_po_tydnech/tyden_09.txt
· Last modified: 2018/12/04 16:25 by
xposik