Perceptron mit und ohne Flux
Dinge einzuordnen ist eine immer wiederkehrende Aufgabenstellung. Finde in einer Liste von Produkten ein Produkt, das den Kunden auch interessieren könnte? Ist diese Pflanze Bärlauch oder ein Maiglöckchen? Ist dieser Pilz eßbar oder nicht? Bei diesen Beispielen und dem folgenden Beispiel geht es um eine Entscheidung mit zwei möglichen Antworten, d.h. die Elemente einer Menge sind zwei verschiedenen Klassen, U und V, zuzuordnen.
Ein linearer Klassifizierer
Der einfachste Versuch, eine Entscheidung zu fällen ist: Finde eine Linie (im zweidimensionalen Fall) oder eine (Hyper-) Ebene, welche den Raum in zwei Bereiche einteilt. In dem einen Bereich finden sich Elemete der Klasse U, im anderen Elemente der Klasse V. Falls das möglich ist, nennt man die Klassen linear separierbar.1
Wir versuchen das Problem iterativ, also schrittweise, zu lösen: Am Anfang nehmen wir eine zufällige Linie, die durch ihre Steigung und ihren y-Achsenabschnitt bestimmt ist. Dann prüfen wir, wie gut die Linie die beiden Klassen trennt und passen und an, wenn die Linie ein Element falsch zuordnet. Im allgemeinen Fall – mit mehr als einem Attribut – lautet die lineare Gleichung:
oder mit einem zusätzlich eingeführten , das immer den Wert 1 hat,
Wendet man das lineare Modell auf einen Merkmalsvektor an, ergibt sich die Zuordnung durch das Vorzeichen des Ergebnisses.
Die Anpassungsvorschrift lautet: Bei richtiger Zuordnung werden die Parameter nicht verändert. Bei falscher Zuordnung werden die Parameter entsprechend der Werte der und der Richtung der Abweichung angepasst. Ist das Ergebnis positiv falsch, werden die Parameter vermindert, ist das Ergebnis negativ falsch, werden die Parameter erhöht. Die folgende Zuordnung setzt diese Anpassungsvorschrift um:
ist die Lernrate. Sie bestimmt die Empfindlichkeit des Modells. Für die Werte der Vorhersage und der tatsächliche Klasse erhält man.
0 | 0 | 0 | keine Anpassung |
0 | 1 | -1 | wird vermindert |
1 | 0 | 1 | wird erhöht |
1 | 1 | 0 | keine Anpassug |
Eine ausführlichere Beschreibung dieser Perceptron Lernregel findet sich bei Kubat1.
Anwendung auf den Iris-Datensatz
Hier soll das Verfahren auf den
Iris-Datensatz von R. A. Fisher angewendet werden.
Er wird aus einer SQLite Datei in einen DataFrame
geladen:
julia> using SQLite: DB, Query
julia> using DataFrames
julia> irisfile = ("../../data/Iris/iris.sqlite3")
"../../data/Iris/iris.sqlite3"
julia> idb = DB(irisfile)
SQLite.DB("../../data/Iris/iris.sqlite3")
julia> datiris = Query(idb, "select * from 'iris'", nullable = false) |> DataFrame
150×5 DataFrame. Omitted printing of 1 columns
│ Row │ sepal_length │ sepal_width │ petal_length │ petal_width │
│ │ Float64 │ Float64 │ Float64 │ Float64 │
├─────┼──────────────┼─────────────┼──────────────┼─────────────┤
│ 1 │ 5.1 │ 3.5 │ 1.4 │ 0.2 │
│ 2 │ 4.9 │ 3.0 │ 1.4 │ 0.2 │
│ 3 │ 4.7 │ 3.2 │ 1.3 │ 0.2 │
│ 4 │ 4.6 │ 3.1 │ 1.5 │ 0.2 │
│ 5 │ 5.0 │ 3.6 │ 1.4 │ 0.2 │
⋮
│ 145 │ 6.7 │ 3.3 │ 5.7 │ 2.5 │
│ 146 │ 6.7 │ 3.0 │ 5.2 │ 2.3 │
│ 147 │ 6.3 │ 2.5 │ 5.0 │ 1.9 │
│ 148 │ 6.5 │ 3.0 │ 5.2 │ 2.0 │
│ 149 │ 6.2 │ 3.4 │ 5.4 │ 2.3 │
│ 150 │ 5.9 │ 3.0 │ 5.1 │ 1.8 │
Um bei einem zweiwertigen Beispiel zu bleiben, sollen in den Daten diejenigen Merkmalssätze gefunden werden, die “Iris-setosa” zuzuordnen sind.
julia> vclass = datiris.class .== "Iris-setosa"
150-element BitArray{1}:
1
1
1
1
1
1
1
⋮
0
0
0
0
0
0
0
Das Model wird durch einen Parameter-Vektor W
repräsentiert. Das Modell wird
ausgewertet, durch Multiplikation mit dem Merkmalsvektor und der Prüfung, ob das
Resultat positiv oder negativ ist. Um die Modelle
vergleichen zu können, geben wir eine Initialisierung vor.
struct Model
W
end
function (m::Model)(x)
div(sign((m.W * x)[1]) + 1, 2)
end
Winit = [-0.251586 -0.0537112 0.128108 0.0789731 -0.238168]
model = Model(copy(Winit))
Die Anpassung ergibt sich aus der oben angegebenen Vorschrift:
function update!(model, x, y, eta)
d = y - model(x)
for (i, cx) in enumerate(x)
model.W[i] += eta * d * cx
end
end
Wieviele falsche Vorhersagen macht das Modell am Anfang?
julia> sum([model(x) != y for (x, y) in zip(eachcol(Mx), vclass)])
39
Im Trainingsprozess wird der Parametervektor wiederholt angepasst:
function tloop!(model, data, eta, n)
hfalse(t) = model(t[1]) != t[2]
cfalse = Int[]
for epoch in 1:n
# train
for d in data
update!(model, d..., eta)
end
push!(cfalse, sum(hfalse.(itdat)))
end
cfalse
end
Mit einer Lernrate von 0.005 sind 5 Trainingsschritte nötig, um die Elemente des Datensatzes alle richtig zuzuordnen.
julia> itdat = zip(eachcol(Mx), vclass);
julia> eta = 0.005;
julia> tloop!(model, itdat, eta, 12)
12-element Array{Int64,1}:
49
20
48
12
0
0
0
0
0
0
0
0
Perceptron mit Flux, „The Julia Machine Learning Library“
Das Paket Flux umfasst Typen und Methoden für maschinelles Lernen und erlaubt es, Modelle unterschiedlicher Komplexität und unterschiedlichen Typs in wenigen Zeilen Code zu formulieren und zu trainieren. Eine der Kernfunktionen besteht darin, Gradienten von Julia-Funktionen zu bilden. Für das oben beschriebene sehr einfache lineare Modell erzielt man kaum einen Gewinn, aber die Prinzipien lassen sich sehr schön nachvollziehen.
Das Modell wird über den Konstruktor Dense
erzeugt, dem lediglich die
Dimensionen übergeben werden, sowie eine Aktivierungsfunktion, die in unserem
Fall das Vorzeichen des Ergebnisses in einen 0/1-Wert für die Klasse umwandelt.
using Flux
fclass(y) = div(sign(y) + 1, 2)
fxmodel = Dense(4, 1, fclass)
hyp(x) = fxmodel(x)[1].data
function fxinit!(model, Winit)
Flux.Tracker.update!(model.W, -model.W)
Flux.Tracker.update!(model.b, -model.b)
Flux.Tracker.update!(model.W, permutedims(Winit[2:5]))
Flux.Tracker.update!(model.b, Winit[1:1])
model
end
fxinit!(fxmodel, Winit)
Man beachte, dass in dem Modell der Offset b
als separater Wert abgelegt
ist. Entsprechend haben die Merkmalsvektoren nur eine Länge von 4.
Die Modelle mit den Parametern von Winit
liefern die gleichen Ergebnisse.
julia> fxMx = [datiris[i, j] for j in 1:4, i in 1:size(datiris, 1)]
4×150 Array{Float64,2}:
5.1 4.9 4.7 4.6 5.0 5.4 4.6 5.0 … 6.8 6.7 6.7 6.3 6.5 6.2 5.9
3.5 3.0 3.2 3.1 3.6 3.9 3.4 3.4 3.2 3.3 3.0 2.5 3.0 3.4 3.0
1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 5.9 5.7 5.2 5.0 5.2 5.4 5.1
0.2 0.2 0.2 0.2 0.2 0.4 0.3 0.2 2.3 2.5 2.3 1.9 2.0 2.3 1.8
julia> model(Mx[:, 34]), fxmodel(fxMx[:, 34])
(1.0, Float32[1.0] (tracked))
julia> model(Mx[:, 35]), fxmodel(fxMx[:, 35])
(0.0, Float32[0.0] (tracked))
julia> sum([hyp(x) != y for (x, y) in zip(eachcol(fxMx), vclass)])
39
Für die Modellanpassung wird eine Verlustfunktion angegeben, …
function loss(model, x, y)
d = y - model(x)[1]
l = d * model.b + d * (model.W * x)
# Flux liefert immmer einen Array zurück
-l[1]
end
… deren Gradient die Korrekturwerte in jedem Anpassungsschritt liefert.
julia> pm = Flux.params(fxmodel)
Params([Float32[-0.0537112 0.128108 0.0789731 -0.238168] (tracked), Float32[-0.251586] (tracked)])
julia> g34 = Flux.Tracker.gradient(() -> loss(fxmodel, fxMx[:, 34], vclass[34]), pm)
Grads(...)
julia> g34[fxmodel.W]
Tracked 1×4 Array{Float32,2}:
0.0 0.0 0.0 0.0
julia> g34[fxmodel.b]
Tracked 1-element Array{Float32,1}:
0.0f0
julia> g35 = Flux.Tracker.gradient(() -> loss(fxmodel, fxMx[:, 35], vclass[35]), pm)
Grads(...)
julia> g35[fxmodel.W]
Tracked 1×4 Array{Float32,2}:
-4.9 -3.1 -1.5 -0.1
julia> g35[fxmodel.b]
Tracked 1-element Array{Float32,1}:
-1.0f0
julia> fxMx[:, 35]
4-element Array{Float64,1}:
4.9
3.1
1.5
0.1
Der Gradient in den Parametern entspricht den Werten des Merkmalsvektors.
Der Trainingsprozess lautet nun …
function fxtloop!(model, data, eta, n)
pm = Flux.params(model)
opt = Descent(eta)
hfalse(t) = hyp(t[1]) != t[2]
cfalse = Int[]
for epoch in 1:n
for d in data
gs = Flux.Tracker.gradient(pm) do
loss(model, d...)
end
Flux.Tracker.update!(opt, pm, gs)
end
push!(cfalse, sum(hfalse.(data)))
end
cfalse
end
… und liefert ebenfalls nach fünf Schritten die gewünschte Zuordnung.
julia> fxitdat = zip(eachcol(fxMx), vclass);
julia> eta = 0.005;
julia> fxtloop!(fxmodel, fxitdat, eta, 12)
12-element Array{Int64,1}:
49
20
48
12
0
0
0
0
0
0
0
0
Quadratische Abweichung als Verlustfunktion
Mit der Abbildung der Modellauswertung auf das Intervall [0, 1] und einer (einfacheren) quadratischen Verlustfunktion erhält man:
fxmodel = Dense(4, 1, σ)
hyp(x) = div(fxmodel(x)[1].data, 0.5)
function loss(model, x, y)
abs2(model(x)[1] - y)
end
Wieder ordnet das Modell zunächst 39 Elemente falsch zu:
julia> fxinit!(fxmodel, Winit)
Dense(4, 1, σ)
julia> sum([hyp(x) != y for (x, y) in zip(eachcol(fxMx), vclass)])
39
In sieben Schritten, die aber jeweils sehr viel schneller berechnet werden, führt das Training zum Ziel:
julia> fxtloop!(fxmodel, fxitdat, eta, 12)
12-element Array{Int64,1}:
50
50
50
42
6
1
0
0
0
0
0
0
julia> VERSION
v"1.2.0"