Naivni Bayesov klasifikator

Zanima nas, ali lahko iz opisa filma napovemo njegove žanre. Gre za klasifikacijski problem, saj želimo filme klasificirati v žanre, naša naloga pa je napisati ustrezen program, ki mu pravimo klasifikator.

Predpriprava

# naložimo paket
import pandas as pd

# naložimo razpredelnico, s katero bomo delali
filmi = pd.read_csv('../02-zajem-podatkov/predavanja/obdelani-podatki/filmi.csv', index_col='id')
osebe = pd.read_csv('../02-zajem-podatkov/predavanja/obdelani-podatki/osebe.csv', index_col='id')
vloge = pd.read_csv('../02-zajem-podatkov/predavanja/obdelani-podatki/vloge.csv')
zanri = pd.read_csv('../02-zajem-podatkov/predavanja/obdelani-podatki/zanri.csv')

Korenjenje besed

Da zadevo naredimo bolj obvladljivo, bomo opis predstavili le z množico korenov besed, ki se v opisu pojavljajo.

def koren_besede(beseda):
    beseda = ''.join(znak for znak in beseda if znak.isalpha())
    if not beseda:
        return '$'
    konec = len(beseda) - 1
    if beseda[konec] in 'ds':
        konec -= 1
    while konec >= 0 and beseda[konec] in 'aeiou':
        konec -= 1
    return beseda[:konec + 1]

def koreni_besed(niz):
    return pd.Series(sorted({
        koren_besede(beseda) for beseda in niz.replace('-', ' ').lower().split() if beseda
    }))
koreni_besed("In 1938, after his father Professor Henry Jones, Sr. goes missing while pursuing the Holy Grail, Indiana Jones finds himself up against Adolf Hitler's Nazis again to stop them obtaining its powers.")
0             $
1         adolf
2         after
3         again
4       against
5        father
6          find
7             g
8         grail
9             h
10        henry
11      himself
12       hitler
13         holy
14           in
15       indian
16           it
17          jon
18      missing
19          naz
20    obtaining
21        power
22    professor
23     pursuing
24           sr
25         stop
26            t
27           th
28         them
29           up
30         whil
dtype: object

Bayesov izrek

Zanimala nas bo torej verjetnost, da ima film žanr \(Ž_i\) ob pogoju, da njegov opis vsebuje korene \(K_1, \ldots, K_m\), torej

\[P(Ž_i | K_1 \cap \cdots \cap K_n)\]

Pri tem se bomo poslužili Bayesovega izreka

\[P(A | B) = \frac{P(A \cap B)}{P(B)} = \frac{P(B | A) \cdot P(A)}{P(B)}\]

zaradi česar našemu klasifikatorju pravimo Bayesov klasifikator. Velja

\[P(Ž_i | K_1 \cap \cdots \cap K_n) = \frac{P(K_1 \cap \cdots \cap K_n | Ž_i) \cdot P(Ž_i)}{P(K_1 \cap \cdots \cap K_n)}\]

Nadalje si nalogo poenostavimo s predpostavko, da so pojavitve besed med seboj neodvisne. To sicer ni res, na primer ob besedi treasure se bolj pogosto pojavlja beseda hidden kot na primer boring, zato pravimo, da je klasifikator naiven. Ob tej predpostavki velja:

\[P(K_1 \cap \cdots \cap K_n | Ž_i) = P(K_1 | Ž_i) \cdot \cdots \cdot P(K_n | Ž_i)\]

oziroma

\[P(Ž_i | K_1 \cap \cdots \cap K_n) = \frac{P(K_1 | Ž_i) \cdot \cdots \cdot P(K_n | Ž_i) \cdot P(Ž_i)}{P(K_1 \cap \cdots \cap K_n)}\]

Filmu, katerega opis vsebuje korene \(K_1, \dots, K_n\) bomo priredili tiste žanre \(Ž_i\), pri katerih je dana verjetnost največja. Ker imenovalec ni odvisen od žanra, moramo torej za vsak \(Ž_i\) izračunati le števec:

\[P(K_1 | Ž_i) \cdot \cdots \cdot P(K_n | Ž_i) \cdot P(Ž_i)\]

Vse te podatke znamo izračunati, zato se lahko lotimo dela.

Verjetnost posameznega žanra \(P(Ž)\) izračunamo brez večjih težav:

verjetnosti_zanrov = zanri.groupby('zanr').size() / len(filmi)
verjetnosti_zanrov.sort_values()
zanr
Film-Noir    0.0057
Western      0.0119
Musical      0.0137
Sport        0.0201
War          0.0237
Music        0.0279
History      0.0345
Animation    0.0447
Family       0.0524
Biography    0.0633
Sci-Fi       0.0707
Fantasy      0.0751
Mystery      0.0964
Horror       0.1237
Adventure    0.1609
Thriller     0.1677
Romance      0.1814
Crime        0.1984
Action       0.2401
Comedy       0.3741
Drama        0.5662
dtype: float64

Verjetnosti \(P(K|Ž)\) bomo shranili v razpredelnico, v kateri bodo vrstice ustrezale korenom \(K\), stolpci pa žanrom \(Ž\). Najprej moramo poiskati vse filme, ki imajo žanr \(Ž\), njihov opis pa vsebuje koren \(K\). Vzemimo vse opise filmov:

filmi.opis
id
4972        The Stoneman family finds its friendship with ...
6864        The story of a poor young woman, separated by ...
9968        A frail waif, abused by her brutal boxer fathe...
10323       Hypnotist Dr. Caligari uses a somnambulist, Ce...
12349       The Tramp cares for an abandoned child, but ev...
                                  ...                        
11390036    Disheartened since her ex-husband's affair, Gr...
11905962    The lone survivor of an enigmatic spaceship in...
12393526    A man returns home after years to find his bro...
12567088    The film follows a small town cop who is summo...
12749596    Six friends hire a medium to hold a seance via...
Name: opis, Length: 10000, dtype: object

To vrsto nizov pretvorimo v vrsto množic besed. Uporabimo metodo apply, ki dano funkcijo uporabi na vsakem vnosu.

filmi.opis.apply(
    koreni_besed
)
0 1 2 3 4 5 6 7 8 9 ... 45 46 47 48 49 50 51 52 53 54
id
4972 affect an arm assassination birth both by cameron civil development ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
6864 an baby by from her history husban interwoven intoleranc ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
9968 abus befriend boxer brutal by chines consequenc district father ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
10323 caligar cesar commit dr hypnotist murder somnambulist t us ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
12349 abandon an but car chil event for in jeopardy put ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
11390036 affair but by dishearten erod ex feel grac her ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
11905962 alon an back body creatur danger enigmatic h hasnt ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
12393526 abandon after an ancestral brid brother by chil death ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
12567088 an by complicat conflict cop death family film follow ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
12749596 bargain but during far for friend g get hir ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

10000 rows × 55 columns

Po nekaj iskanja po internetu in masiranja pridemo do iskane razpredelnice:

koreni_filmov = filmi.opis.apply(
    koreni_besed
).stack(
).reset_index(
    level='id'
).rename(columns={
    'id': 'film',
    0: 'koren',
})
koreni_filmov
film koren
0 4972 affect
1 4972 an
2 4972 arm
3 4972 assassination
4 4972 birth
... ... ...
19 12749596 they
20 12749596 thing
21 12749596 v
22 12749596 wrong
23 12749596 zoom

225294 rows × 2 columns

Razpredelnico združimo z razpredelnico žanrov, da dobimo razpredelnico korenov žanrov.

koreni_zanrov = pd.merge(
    koreni_filmov,
    zanri
)[['koren', 'zanr']]
koreni_zanrov
koren zanr
0 affect Drama
1 affect History
2 affect War
3 an Drama
4 an History
... ... ...
576013 v Mystery
576014 wrong Horror
576015 wrong Mystery
576016 zoom Horror
576017 zoom Mystery

576018 rows × 2 columns

S pomočjo funkcije crosstab preštejemo, kolikokrat se vsaka kombinacija pojavi.

pojavitve_korenov_po_zanrih = pd.crosstab(koreni_zanrov.koren, koreni_zanrov.zanr)
pojavitve_korenov_po_zanrih
zanr Action Adventure Animation Biography Comedy Crime Drama Family Fantasy Film-Noir ... Horror Music Musical Mystery Romance Sci-Fi Sport Thriller War Western
koren
2167 1415 389 497 3311 1817 5073 454 673 54 ... 1150 243 122 902 1605 655 170 1574 213 99
$ 233 148 33 147 359 184 685 44 51 2 ... 110 29 8 78 171 86 30 148 48 18
aang 1 1 0 0 0 0 0 1 0 0 ... 0 0 0 0 0 0 0 0 0 0
aaron 1 1 0 0 2 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
aart 0 0 0 0 0 0 1 0 0 0 ... 0 0 0 0 1 0 0 0 0 0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
0 0 1 0 1 0 0 1 0 0 ... 0 0 0 0 0 0 0 0 0 0
ángel 0 0 0 0 0 0 0 0 0 0 ... 1 0 0 1 0 0 0 1 0 0
çanakkal 0 0 0 0 1 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
édith 0 0 0 1 0 0 1 0 0 0 ... 0 1 0 0 0 0 0 0 0 0
émilien 1 0 0 0 1 1 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0

15474 rows × 21 columns

Iskane verjetnosti sedaj dobimo tako, da vsak stolpec delimo s številom filmov danega žanra. Da ne bomo dobili ničelne verjetnosti pri korenih, ki se v našem vzorcu ne pojavijo, verjetnost malenkost povečamo.

verjetnosti_korenov_po_zanrih = pojavitve_korenov_po_zanrih / zanri.groupby('zanr').size() + 0.001

Poglejmo, kaj so najpogostejši koreni pri nekaj žanrih:

verjetnosti_korenov_po_zanrih.Crime.sort_values(ascending=False).head(20)
koren
          0.916827
th        0.664306
an        0.635577
t         0.607855
of        0.528722
h         0.461685
in        0.419851
on        0.237895
with      0.230335
wh        0.188500
for       0.181444
by        0.162290
when      0.141625
after     0.128520
from      0.127512
their     0.120960
murder    0.119952
her       0.110375
that      0.100294
him       0.097774
Name: Crime, dtype: float64
verjetnosti_korenov_po_zanrih.Romance.sort_values(ascending=False).head(20)
koren
         0.885785
an       0.636612
th       0.623933
t        0.567703
of       0.462411
in       0.450283
h        0.426028
with     0.322389
her      0.273326
on       0.218200
lov      0.208828
for      0.207174
young    0.163073
woman    0.159214
wh       0.155355
their    0.154252
when     0.144881
lif      0.132202
man      0.131650
sh       0.125587
Name: Romance, dtype: float64
verjetnosti_korenov_po_zanrih['Sci-Fi'].sort_values(ascending=False).head(20)
koren
         0.927450
th       0.692655
an       0.651636
t        0.645979
of       0.524338
in       0.435229
h        0.360264
on       0.248525
with     0.211750
for      0.174975
that     0.170731
from     0.167902
by       0.155173
their    0.150929
when     0.135371
after    0.131127
earth    0.124055
$        0.122641
int      0.112740
man      0.109911
Name: Sci-Fi, dtype: float64

Žanre sedaj določimo tako, da za vsak žanr pomnožimo verjetnost žanra in pogojne verjetnosti vseh korenov, ki nastopajo v opisu filma.

def doloci_zanre(opis):
    faktorji_zanrov = verjetnosti_zanrov * verjetnosti_korenov_po_zanrih[
        verjetnosti_korenov_po_zanrih.index.isin(
            koreni_besed(opis)
        )
    ].prod()
    faktorji_zanrov /= max(faktorji_zanrov)
    return faktorji_zanrov.sort_values(ascending=False).head(5)
doloci_zanre('Alien space ship appears above Slovenia.')
zanr
Sci-Fi       1.000000
Adventure    0.586644
Animation    0.243516
Action       0.222337
Horror       0.197353
dtype: float64
doloci_zanre('A story about a young mathematician, who discovers her artistic side')
zanr
Drama        1.000000
Biography    0.756991
Romance      0.297698
Mystery      0.086731
Comedy       0.081492
dtype: float64