16) Sifferigenkänning

I den här lektionen tittar vi på en app för sifferigenkänning.

Jämfört med tidigare appar verkar sifferigenkänning ganska trivialt men vi kommer att stöta på närmast filosofiska frågeställningar som ”klassen av allting annat”.

Sifferigenkänning (digit recognition) har funnits länge – långt före djupinlärning (deep learning). En välkänd tillämpning är att avläsa postnumret (det handskrivna) på brev och paket.

Att använda djupinlärning för sifferigenkänning ger mycket bra resultat (bättre än för äldre tekniker) och är enklare att använda.

Appen Digit (se videon överst på sidan) demonstrerar sifferigenkänning. Rita en siffra (eller något annat), tryck på Predict-knappen och du får två svar: 1) vilken siffra och 2) är det verkligen en siffra?

En modell för klassificering (här med 10 klasser för siffrorna 0-9) kan bara tala om vilken siffra är mest lik symbolen du ritade.

Så modellen svarar alltid med en siffra 0-9 men kanske med en dålig sannolikhet. Jag kallar den här modellen modell1.

Den andra modellen (som jag kallar modell2) har två klasser: siffra och inte-siffra.

Varför två modeller?

För länge sedan som grön programmerare lärde jag mig att alltid kolla indata till ett program – helt enkelt för det ska funka och inte krascha.

Här måste jag alltså kolla att bara siffror ges in. Men det är inte så enkelt…

Varför inte bara gå på sannolikheten? För varje ”prediction” får vi ju en sannolikhet.

Kan vi inte helt enkelt underkänna ”siffran” om sannolikheten är mindre än 90%?

Nej, funkar inte. Modellen kan vara helt säker på siffran men ändå är det fel. Se Exampel2 här nedan.

Modell1 (som använder deep learning) är tränad med välkända MNIST dataset. MNIST innehåller 60,000 bilder på handskrivna siffror 0-9 i form av små gråskalebilder 28x28 pixlar. En pixel är minsta punkten i en bild.

Modell2 (som också använder deep learning) är mycket lik modell1 och använder den färdigtränade modell1 som startpunkt.

Modellen är ändrad att bara ha två klasser som indata och utdata – siffra eller inte.

Modellen tränas med nya symboler – blandat siffra och något annat till exempel bokstav.

Den här metoden att utgå från en färdigtränad modell, ändra modellen och träna om modellen med nya data kallas transfer learning och är en mycket vanlig teknik.

Datapreparering

I våra tidigare appar har vi sett i koden att vi ger en bild till modellen med ett anrop predict(img) eller liknande.

Vi ger bilden (img) som den är utan att ändra den. Det funkar normalt inte.

Att det funkade med våra tidigare appar beror på att apparnas predict-funktion innehöll kod som först preparerade bilden så att den passade modellen.

Den ändrade bilden stoppades sedan in i modellen. Någon har alltså varit snäll och skrivit den koden åt oss.

För att vi ska få bra resultat från en modell krävs ofta att indata till modellen (vår bild) har samma egenskaper som de bilder modellen tränades med.

För MNIST vet vi att det är 28x28 pixlar i en svart-vit bild (gråskala).

Funktionen predict() i appen Digit gör följande:

  1. Hämtar bilden från canvas (ytan där du ritar siffran)
  2. Gör om bilden till en gråskala. Bilden från canvas har RGB-format där varje pixel (minsta delen av en bild) har 3 värden 0-255 för rött, grönt och blått. Nya pixel-värdet för grått (ett värde i stället för tre) fås genom att summera värdena för rött, grönt och blått och sedan dividera med 3 – alltså ett genomsnittsvärde.
  3. Bilden från canvas är 200x200 pixlar. Bilden måste krympas till 28x28. Det görs med en speciell funktion.
  4. Sedan normaliseras pixel-värdena genom division med 255. Alla pixel-värden är nu från 0.0 till 1.0.
  5. Sedan görs ytterligare en typ av normalisering (se Exempel 1).
  6. Pixel-värdena stoppas in i modellen för att få en förutsägelse 0-9.

Data som tensorer

Hur ser data (bilden) egentligen ut när den ges till funktionen predict() i appen Digit?

Det går att ta reda på med hjälp av Javascript-konsolen. I Uppgift 4 får du se hur.

	const output = model.predict(x);
	x.shape => [1, 28, 28, 1]
	

x är bilden för symbolen som du ritade, x ges som argument till funktionen predict().

x är en ”4D data image tensor” (4 dimensioner).

Egentligen hade en 2D-tensor [28,28] varit tillräcklig för vår gråskalebild.

Men det finns konventioner hur bilden ska ges in till modellerna.

I vårt fall är konventionen NHWC där N=antal bilder, H=bildens höjd i pixlar, W=bildens bredd i pixlar och C=antal kanaler i bilden (3 för RGB-bild, 1 för gråskala)

I vårt fall är N=1 (vi har bara en bild). För träning är kanske N=128 (batch-storleken).

Våra bilder är 28x28 pixlar så H=W=28.

Vår bild är gråskala så C=1.

Tensorflow och Tensorflow.js (vårt ramverk) jobbar internt alltid med tensorer. Data som vi ger till modellerna måste vi alltid göra om till tensorer.

Vad är en tensor?

Du har nu hört termen tensor många gånger t.ex. som i vår plattform TensorFlow.js. Men vad är egentligen en tensor?

En enkel förklaring: En tensor kan ses som en behållare för data oftast data som numeriska tal.

En tensor har en dimension. I exemplet ovan såg vi en 4D tensor som innehöll bilden som gavs som indata till modellen.

1D tensor - kallas också vektor. Exempel: [ 1, 2, 3 ], shape = [3]

2D tensor - kallas också matris. Exempel: [ [ 1, 2, 3 ] , [ 4, 5, 6 ] ], shape = [2, 3] d.v.s. 2 rader och 3 kolumner

Sedan börjar det bli komplicerat.

Vad vi kan lära oss av Digit

Appen Digit är lite överarbetad men samtidigt kanske lärorik.

Använd gärna ångvisslan den är rolig: Actions->Toggle whistle

Exempel 1. Slå av normaliseringen: Actions->Toggle Normalization. Ger röd Predict-knapp. Rita en fyra i övre högra hörnet. Tryck på Predict. Appen är helt säker på att det är en femma (du kan få annat resultat).

Slå på normaliseringen igen: Actions->Toggle Normalization. Ger grön Predict-knapp. Tryck på Predict. Appen är nu helt säker på att det är en fyra.

Vad beror det på? Bilderna som var input till träningen av vår modell var alla centrerade inom 28x28 pixlar och dessutom av ungefär samma storlek (fyller upp 28x28). Därför bör vi göra samma sak för att få ett bra resultat.


Exempel 2.

Rita ett H och ha normalisering på (grön Predict-knapp). Modell 1 säger 4 (helt säker) och modell 2 säger inte siffra (röd stapel, helt säker).

Ändra penna till suddgummi: Actions->Toggle pen/eraser. Sudda bort en bit av h:et – vänstra nedre delen. Tryck på Predict. Samma resultat!

Sudda igen så att hela vänstra benet är borta. Nu är modellerna överens!

Sätt tillbaka till penna: Actions->Toggle pen/eraser


Exempel 3. Rita ett hus. Tryck på Predict-knappen: Modell 1 säger siffran 0 (ganska säker). Modell 2 säger inte siffra pga flera symboler i bilden (gul stapel).

Om du ritar ungefär samma hus men utan fönster kan du få huset godkänt som siffra även av modell 2 – det finns brister i modell2.



Förbättra modell2

Appen är förberedd för att förbättra modell2. Varje gång som jag tycker att modell2 har fel om en ritad symbol kan jag spara symbolen och den rätta etiketten (siffra eller inte).

De sparade symbolerna och etiketterna kan sedan användas för att träna modell2 så att den blir bättre.

Träningen ligger utanför appen så du kan inte göra träningen.

Om appen hade många användare kunde de få skicka in sina symboler och etiketter till en central server.

Där kunde modell2 tränas om kanske varje dag.

Men egentligen ganska hopplöst

Jag tycker att det här är ett svårt problem.

Klassen ’inte siffra’ (till modell2) omfattar alla symboler (eller annat till exempel ett hus) som kan ritas på 28x28 pixlar och som inte är en siffra.

Det är knepigt att ta fram lämpliga bilder för träning.

Dessutom är skillnaden mellan siffra och ’inte siffra’ hårfin.

Med ’flood-fill’ kollas att bara en symbol har ritats.

Flood-fill har samma funktion som färgburken i ett ritprogram.

Men det är bara dumt att göra så här. Det är bättre att låta nätverket sköta sådana saker.

En modell i stället för två?

Det hade varit möjligt att använda bara en modell i stället för två.

Då hade modell1 fått ytterligare en klass ’inte siffra’ med motsvarande data.

Det vill säga klasserna 0-9 (för siffror) och 10 för ’inte-siffra’.

Modell1 är tränad med ungefär 40,000 bilder på siffror så klassen ’inte siffra’ bör innehålla ungefär 40,000/10=4,000 bilder för ett bra resultat.

Använda tröskelvärden?

När bilden av en siffra (indata) stoppas in i modell1 får vi som utdata en sannolikhet för siffrorna 0-9.

Siffran med den högsta sannolikheten väljs som rätt svar.

Men att använda ett tröskelvärde för sannolikheten för att underkänna siffran verkade inte funka här.

Men i andra sammanhang går det bra men det måste förstås kollas om det är ok.

Ett exempel: I appen för röststyrning användes ett tröskelvärde för att underkänna ett ord. Måste vara större än 75% för godkännande.

Modell1: Siffra 0-9

Så här ser Modell1 ut. Det är en ganska enkel modell med inte så många lager.

Det finns ett antal lagertyper som vi inte har talat om och jag förklarar dom inte. Det blir för komplicerat.

Några kommentarer:

  • För input (indata) känner vi igen [28, 28, 1] som är en bild 28x28 pixlar i gråskala (1 kanal)
  • Conv2D är ett 2-dimensionellt CNN (lämpligt för bild)
  • Dense är samma som FC (Fully Connected) som vi såg i Lektion 2.
  • Output (utdata) har 10 klasser, sifrorna 0-9.
  • Antalet parametrar (samma som vikter) är 594,922
____________________________________________
Layer (type)                 Output shape  
============================================
conv2d_Conv2D1_input (InputL [null,28,28,1]
____________________________________________
conv2d_Conv2D1 (Conv2D)      [null,26,26,32]
____________________________________________
conv2d_Conv2D2 (Conv2D)      [null,24,24,32]
____________________________________________
max_pooling2d_MaxPooling2D1  [null,12,12,32]
___________________________________________
conv2d_Conv2D3 (Conv2D)      [null,10,10,64]
____________________________________________
conv2d_Conv2D4 (Conv2D)      [null,8,8,64] 
____________________________________________
max_pooling2d_MaxPooling2D2  [null,4,4,64] 
____________________________________________
flatten_Flatten1 (Flatten)   [null,1024]   
____________________________________________
dropout_Dropout1 (Dropout)   [null,1024]   
____________________________________________
dense_Dense1 (Dense)         [null,512]    
____________________________________________
dropout_Dropout2 (Dropout)   [null,512]   
____________________________________________
dense_Dense2 (Dense)         [null,10]     
============================================
Total params: 594922
Trainable params: 594922
Non-trainable params: 0

Summering

I den här lektionen kollade vi en applikation för sifferigenkänning – inte så enkelt.

Klassen ’allt annat’ eller ’inte siffra’ är knepig.

Indata till en modell måste normalt prepareras så att formatet på indata liknar de data som modellen tränades med.

Dessutom måste indata vara en tensor av en viss form.

Äntligen vet vi vad en tensor är - en behållare med en viss dimension. Innehåller oftast numeriska värden.

Tröskelvärden verkade inte funka för sifferigenkänning men används för andra tillämpningar.

I nästa lektion behandlas kreativ AI – populärt och spektakulärt.

<< Bakåt Framåt >>