Obligatorisk oppgave om Titanic (frist 28. mars)

Note

Oppgaveteksten er nå fullstendig. Dersom det er noe som er uklart, er det lurt å spørre/sende en mail. Enkelte ting som dukker opp i obligen har ikke vært gjennomgått i undervisningen. Da finnes det forhåpentligvis noe informasjon i statistikkboka, blant medstudenter eller blant underviserne i HON2200!

Etter å ha fullført denne obligen skal studenten kunne - Lese inn og behandle kategoriske data i pandas - Implementere logistisk klassifikasjon - Benytte og beskrive beslutningstrær - Benytte og beskrive random forests

Denne obligen skal fungere som en øvelse på maskinlæring anvendt på kategoriske data. Dere skal gjennom forbereding av data med pandas og scikit-learn, og så tilpasning av dataene med logistisk klassifikasjon og beslutningstrær:

  • Logistisk klassifikasjon for å få erfaring med å gjøre maskinlæring fra grunnen av,
  • Beslutningstrær for å få erfaring med et sentralt verktøy i maskinlæring som brukes i f.eks Random forests.
  • Random forests fordi det fungerer veldig bra i praksis.

Dataene dere skal se på inneholder informasjon om passasjerene på Titanic, inkludert om de overlevde katestrofen eller ikke. Målet er å lage algoritmer som kan predikere om en gitt person på titanic overlevde eller ikke, og å se hvilke personlige egenskaper som var “avgjørende”.

Datasettet dere skal bruke heter train.csv og kan lastes ned her

Gjennom hele obligen har vi gjort det slik at vi først gir oppgavene, og under gir vi en del hint og forklaringer på hvordan de kan løses. Det betyr at om du står fast, kan det være lurt å lese litt lenger nedover på siden. I blant forklarer vi ting i selve oppgaven som ikke har blitt gjennomgått tidligere i undervisningen.

I tillegg til å gjøre de statistiske analysene vi ber om krever vi også at besvarelsen er godt kommentert. Det holder altså ikke bare å levere et dokument som inneholder riktig kode, tall og figurer, du må også beskrive og diskutere resultatene.

Merk at Oppgave 2 har to versjoner

  1. Oppgave-2—Bruk-av-scikitlearn for de som går på humanioraprogrammet
  2. Oppgave-2—Fra-grunnen for de som går på realfag

da sistnevnte krever forkunnskaper tilsvarende MAT1110.

Lesing og behandling av kategoriske data i pandas

Oppgave 1:

a) Les datasettet (“train.csv”) med pandas. Se på dataene! Finn ut hva kolonnene betyr.

b) Håndter de manglende dataene for alder.

c) Gjør de kategoriske dataene du vil benytte brukbare ved å gjøre en one hot encoding.

d) Samle kolonnene du vil bruke til å predikere overlevelse i en dataframe/array X. Samle overlevelses-kolonnen i en annen dataframe/array Y. Under er det vist hvordan dere kan hente ut kolonner til en egene variable. - X = df[["Alder", "Kjønn"]] - Y = df["Lønn"]

e) Del dataenne videre inn i trening og test data. Se dokumentasjonen for scikit-learn sin train_test_split: https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html.

Få oversikt over dataene

Man bruker som regel pd.read_csv() når man leser data med pandas. Da får man et objekt av typen dataframe. For å få denne printet pent i en notebook kan man enten avslutte en celle med dataframe objektet, eller bruke display() funksjonen (ikke print(), det blir stygt).

Manglende data er et stort problem i dataanalyse, og har mange løsninger med egne ulemper og fordeler. I dette datasettet er det mye manglende data for alder og kabin. Du kan få en rask oversikt over manglende data med kodesnutten

display(df.isnull().sum())

Manglende data (her NaN verdier) gjør at maskinlæringsalgoritmene feiler. Den enkleste løsningen er å fjerne alle rader med manglende data. Dette kan man f.eks gjør med koden

df = df.dropna(subset=["Age"])

Men, når vi allerede jobber med så lite data er det en stor kostnad å bare kaste bort så mye. En annen løsning er å fylle inn manglende verdier med gjennomsnittlig alder for kombinasjonen av kjønnet og klassen personen har. Om denne dataen vi fyller inn gjør mer godt enn skade vil variere fra dataset til dataset. Dere velger selv hvordan dere vil håndtere manglende data.

df["Age"] = df.groupby(["Pclass", "Sex"],group_keys=False)["Age"].apply(lambda x: x.fillna(x.median()))

Kategorisk data og train-test split

Kategorisk data er data som ikke skal bli tolket som en numerisk verdi. Maskinlæringsalgoritmer skjønner seg kun på numeriske verdier, så dette fører til et problem. Eksempler på kategorisk data kan være nasjonalitet og telefonnummer. Når en maskinlæringsalgoritme ser telefonnummerene 12345678, 23456789 og 34567891 vil den prøve å finne en sammenheng basert på størrelsen til tallene, selv om det ikke gir mening. Og når den ser ordet “Norge”, får du en feilmelding.

Løsningen er å gi hver enkelt mulige verdi av de kategoriske dataene sin egen kolonne. Kolonnen vil da inneholde nuller og enere som forteller om man hører hjemme i den kategorien eller ikke. En kolonne Norge kan da ha en null hvis personen ikke kommer fra Norge, eller et ett-tall hvis personen kommer fra Norge. Dette kalles one hot encoding. Dette gjør man i pandas med metoden .get_dummies(). Hvis kolonnene Nasjonalitet og Telefonnummer skal bli gjort om til en one-hot kolonner gjøres det slik:

categorical_cols = ["Nasjonalitet", "Telefonnummer"]
df = pd.get_dummies(df, columns=categorical_cols, prefix=categorical_cols, prefix_sep='_')

Merk at kategoriske data som navn og telefonnummer ikke er særlig nyttige, spesielt når du ender opp med en kolonne for hver person.

Når manglende og kategorisk data er tatt hånd om, er neste steg å samle de dataene man skal bruke til å gjøre prediksjoner, og de dataene som man prøver å predikere. Man tar ut kolonner av en dataframe ved å bruke hakeparanteser og navnet på kolonnen, eller en liste med navnene på kolonnene man vil ta ut.

X = df[["Nasjonalitet_Norge", "Nasjonalitet_Sverige", "Alder"]]
Y = df["Lønn"]

Til slutt må man dele opp dataene i trenings-, validerings- og testdata. Algoritmene blir trent på treningsdata, og så ser vi hvor godt de funker ved å teste de på test data. Merk at i denne obligen holder det at dere deler opp i treningsdata og valideringsdata. Vi har låst ned testdataene i kjelleren på forhånd.

Logistisk Klassifisering

Denne oppgaven kommer i to versjoner: Én for dem som går på realgasprogrammet og en for dem som går på humanioraprogrammet. Det kommer av at realfagsversjonen krever forkunnskaper tilsvarende MAT1110.

Oppgave 2 - Bruk av scikitlearn (SV/HF):

a) Plott sigmoidfunksjonen (den logistiske funksjonen). Hva gjør denne funksjonen godt egnet til å “svare på” ja/nei spørsmål?

b) Bruk scikit-learn til å trene en logistisk klassifikasjonsmodell.

c) Test logistisk klassifikasjon modellen. Dere kan bruke clf.score() til å se hvor stor andel riktige modellen hadde.

d) Bruk funksjonen confusion_matrix fra sklearn.metrics til å finne ut i hvilken grad modellen produserer sanne positive, sanne negative, falske positive og falske negative prediksjoner.

Oppgave 2 - Fra grunnen (MN-programmet og eventuelt interesserte):

a) Plott sigmoid-funksjonen. Hva gjør denne funksjonen godt egnet til å “svare” på ja/nei spørsmål?

b) Implementer logistisk klassifisering og tren modellen. - Sett opp vekter (array med like mange tall som input) og bias (ett tall) - Regn ut output. Multipliser trenings-input (X) med vektene, legg til bias, og mat gjennom sigmoid funksjonen. (Deler av kode under) - Regn ut gradienter. (Kode under) - Oppdater vekter. (Formler under)

Viktig i alle program med vektorer og vektorprodukter: Få oversikt over dimensjonene til alle variablene i programmet ditt. Er noe et tall, vektor eller matrise(har flere rader og kolonner)? print() alt du er usikker på, og print() .shape egenskapen for å se størrelsen på ting. Gjør dette underveis mens du skriver programmet, men unngå alt unødvendig i det fullførte programmet.

c) Test logistisk klassifikasjon modellen på test-dataen ved å ta testing-input (X) gjennom input-prosedyren over og sammenlign med test output (Y).

Her kommer det en del formeler og teori, men det viktigste er å forstå de overordnede ideene, som skal være overkommelig. Programmet dere skal skrive vil ha en slik struktur:

Lag vekter og bias

for i in range(iterasjoner):
    Regn ut output (ved hjelp av vekter og bias)
    
    Regn ut gradienter (ved hjelp av output)
    
    Oppdater vekter og bias (ved hjelp av gradienter)

En logistisk modell, som alle andre modeller, tar inn en input og gir en output. Vi kaller input til modellen x, det representerer alle egenskapene til en passasjer. De ulike egenskapene som x inneholder kaller vi x1, x2, osv. Om passasjeren overlever eller er ikke, som er det vi prøver å predikere, kaller vi y. Det modellen predikerer kaller vi y^ (“y hatt”). Først ser vi kun på en og en passasjer, senere regner vi med mange passasjerer, for å finne gjennomsnitt og slikt.

Output til modellen

Den logistiske modellen inneholder vekter W og en bias b, som bestemmer hvordan input x blir om til output y^. Det skal være like mange vekter som egenskaper, og bias skal være et tall. Disse begynner vanligvis med tilfeldige verdier, og kan i Python bli laget med np.random.randn() og np.random.rand().

De ulike vektene ganges komponentvis med de ulike egenskapene, og blir så lagt sammen med bias:

z=b+w1x1+w2x2++wmxm=Wx+b

Dette kan vi gjøre som et vektorprodukt i Python. Merk at vi kan gange vektene med alle egenskapene til ALLE passasjerene på en gang! Dette gjør alt mye mer kompakt: (Vi bruker @ for matrise og vektorprodukt i Python)

z = X_train @ W + b

Alt dette (z) blir så matet gjennom en sigmoid funksjon, som er definert slik:

y^=σ(z)=11+ez

Hvorfor får jeg overflow feil av og til? Det finnes to likegyldige definisjoner av sigmoid funksjonen:

σ(z)=11+ez=ez1+ez

Begge to gir akkurat samme svar, men datamaskiner behandler de to ulikt for veldig store positive og negative tall (fordi utregningen vil inkludere tall som er for store til å behandles av datamaskinen). Den første definisjonen fører til utregningen av eSTORTTALL for store negative verdier for z, som fører til overflow(alt for store tall for datamaskinen). Den andre definisjonen fører til utregningen av eSTORTTALL for store positive verdier for z, som fører til overflow. En måte å programmere en stabil sigmoid funksjon som benytter riktig definisjon til riktig tid er gitt under. np.where() er en slags effektiv if for utregninger. Å bruke en vanlig if til å bruke riktig sigmoid definisjon vil gjøre at man ikke kan bruke funksjonen til vektoriserte utregninger (som å bruke sigmoid funksjonen på en hel vektor på en gang).

def sigmoid(z):     #hvis?           True                False 
    return np.where(z >= 0, 1 / (1 + np.exp(-z)), np.exp(z) / (1 + np.exp(z)))

Merk at når man gjør disse operasjonene på X_train som inneholder egenskapene til alle passasjerene, blir y^ en vektor som inneholder prediksjonene for alle passasjerene, og ikke bare for én.

Siden hele modellen kun består av ett sett vekter, en bias og en sigmoid, kan en logistisk modell bli sett på som et nevralt nettverk med kun én node. Hvis modellen tar fem inputs kan man tegne den slik:

Input 1

Output

Input 2

Input 3

Input 4

Input 5

Cost funksjonen og gradient

Modellen gjør sannsynligvis en veldig dårlig jobb i å predikere noe som helst, siden vektene og bias kun er tilfeldige tall så langt. Vi samler vektene og bias i en variabel θ for å gjøre ligningene mer kompakt, θ er da bare parametrene til modellen vår. Før vi kan få modellen vår til å gjøre en bedre jobb, må vi bestemme hva en bedre jobb betyr. Vi bestemmer at modellen vår er bedre jo mindre cross entropy den har. Denne formelen for cross entropy er litt tung å forstå, bare vit at hvis modellen vår predikerer mye riktig får den lavere cross entropy, ellers får den høyere cross entropy. Indeksen i i uttrykket forteller hvilken passasjer vi ser på, tegnet sier at vi summerer verdiene for alle passasjerer. Denne formelen skal du ikke bruke i programmet ditt.

C(θ)=1ni=1n[yilog(y^i)+(1yi)log(1y^i)]

Hva er motivasjonen bak cost funksjonen?

Modellen vår vil predikere enten 0 eller 1 (i praksis vil den under trening gi et tall mellom 0 og 1 som er sannsynligheten for at den vil predikere 1). Sannsynligheten for at den predikerer 1 gitt en input xi og parametre θ er p(yi=1|xi,θ). Sannsynligheten for at den predikerer 0 gitt en input xi og parametre θ er 1p(yi=1|xi,θ). For hver xi er da sannsynligheten for at den predikerer riktig det du ser i ligningen under, hvis du ignorerer tegnet. tegnet gjør at vi ganger sammen disse sannsynlighetene for alle xi i datasettet, så vi ender opp med sannsynligheten for at modellen predikerer alle datapunkter yi riktig.

P(D|θ)=i=1n[p(yi=1|xi,θ)]yi[1p(yi=1|xi,θ)]1yi

Vi ønsker å maksimere sannsynligheten for at modellen predikerer alle datapunktene riktig, så vi prøver å finne θ slik at verdien av forrige ligning blir størst mulig. Vi kan trikse litt med ligningen for å gjøre dette enklere. Vi tar noe som heter log-likelihood av ligningen, og setter på en minus. Ved en del ekstra triksing ender vi opp med cost funksjonen, som vi nå ønsker å minimere, på grunn av minustegnet.

Vi vil velge parametre θ slik at C(θ) blir så liten som mulig. Dette gjør vi ved å finne den deriverte av C(θ) og bruke den deriverte til å bevege parametrene θ nærmere de som tilsvarer bunnen av C(θ), dette kalles gradient descent. Tenk at du står i en dal og vil finne bunnen, da beveger du deg helst nedover i den retningen det er brattest. De deriverte av C(θ) med hensyn på W og b viser seg å bli:

C(θ)wk=1ni=1nxi,k(yiy^i)

C(θ)b=1ni=1nyiy^i

I python kan man bruke likhetene i uttrykkene til å spare litt beregninger, vi lager et mellomledd delta som er gjennomsnittlig forskjell mellom y og y^:

delta = -(Y_train - y_hat) / n
dCdW = X_train.T @ delta
dCdb = np.sum(delta, axis = 0)

De deriverte peker i retningen hvor C(θ) øker raskest, så vi vil endre parametrene med minus den deriverte. Den deriverte er ofte for stor, som betyr at når vi endrer på parametrene får vi bare søppel, se figur under. For å faktisk nærme oss bunnen av C(θ) må vi dermed velge riktig læringsrate γ, som vi bruker til å skalere(“krympe”) gradienten. Å finne en god læringsrate er enda et stort spørsmål med mange ulike løsninger i maskinlæring. 0.001 med 10000 iterasjoner funker ok her. Dette er hvordan man oppdaterer vektene og bias ved hjelp av de deriverte og læringsraten:

W=WγC(θ)W b=bγC(θ)b

Testing av modellen

Når du skal bruke modellen til å gjøre faktiske prediksjoner må du finne z og y^ for test dataen, men rund av tallene slik at y^ bare får nuller og enere. np.round() kan runde av hele arrays. Så må du finne hvor stor andel av prediksjonene som var riktige. scikit learn sin accuracy_score() anbefales. Se hvilken accuracy en modell som antar at alle dør får for å sammenligne. Du kan forvente en accuracy på rundt 75% for modellen din.

Beslutningstrær

Oppgave 3:

a) Lag et tre med maksdybde 3 som tilpasser treningsdataene.

b) Test treet på testdataene.

c) Plot treet og beskriv med ord hva alle forgreningene sier, og hvilke personer treet sier vil overleve.

d) Se hvordan maksdybden til treet påvirker hvor godt treet klassifiserer testdataene.

Beslutningstrær fungerer på en helt annen måte enn logistiske modeller eller nevrale nettverk. Istedenfor å justere mange vekter litt og litt med gradienter, finner beslutningstrær ja/nei spørsmål som best skiller dataene inn i kategoriene man er ute etter. I Titanic eksempelet finner treet de spørsmålene som best skiller de som overlever fra de som dør. Et slikt spørsmål kan f.eks være om en passasjer er en kvinne, eller om de er yngre enn 50 år. For hvert slikt spørsmål får treet to nye grener, en for ja og en for nei. På hver av disse grenene kan man igjen stille nye spørsmål og få enda flere grener som bedre grupperer dataene.

Når man trener et beslutningstre på data kan man alltid la treet stille så mange spørsmål det bare vil, slik at det kan gruppere alle dataene riktig. Da kan man ende opp med et tre hvor omtrent hvert eneste datapunkt har sin egen forgrening, og treet ikke har noen “overordnede mønstre” det grupperer dataene etter. Treet vil da ha et overtilpasnings(“overfitting”) problem som vil føre til at det presterer dårlig på å klassifisere data det aldri har sett før. Dette kan unngås ved å begrense antall forgreninger treet kan ha, eller maks-dybden til treet.

Dere skal bruke DecisionTreeClassifier modellen til scikit-learn. Følg eksempelet i dokumentasjonen(linken til venstre), og lag et tre med maks dybde 3. Bruk scikit learn sin accuracy_score() for å teste modellen. For å plotte treet anbefaler vi

plt.figure(figsize=(16,10))
tree.plot_tree(model, max_depth=3, feature_names=X_train.columns, class_names=["Dead", "Alive"], filled=True)
plt.show()

Random forest

Oppgave 4:

a) Lag en random forest-modell som tilpasser treningsdataene.

b) Test modellen på testdataene.

c) Undersøk hvordan antallet weak learners i modellen påvirker nøyaktigheten.

d) For din beste modell: Plot “feature importance” og “partial dependance”. Hva sier plottene om hva som var avgjørende for om noen overlevde på Titanic?

e) Kan man ut ifra plottene vite hva Random forest-modellen vil predikere for en spesifikk person? Hvor godt kan man forstå hvordan en slik modell fungerer? Hva med et beslutningstre, eller en logistisk klassifiseringsmodell?