Teil 2: Praktischer Einstieg in Maschinelles Lernen

Nun zum interessanteren Teil bei diesem Projekt – die Umsetzung. Um später nur noch auf unsere Funktionen zugreifen zu können, werden wir uns zunächst die Funktionalität des knn und der euklidischen Distanz anschauen.

Bevor wir damit beginnen können, fehlt uns noch die Spezifikation unseres Daten-Modells. Das habe ich indirekt in Teil 1 bereits verwendet, ohne darauf einzugehen. Deshalb hier noch kurze einen Ausschnitt dazu.

var exampleData = [
{label: 1, data: [..5625..]},
{label: 2, data: [..5625..]},
....
];

Wir speichern unsere gelabelten Daten in einem Array. Und jeweils als ein Objekt mit den Eigenschaften „label“ und „data“. Wobei die Daten ein Array aus Farbwerten unseres Bildes enthält.

euklidische Distanz

Jeder kann sich natürlich zunächst selbst den Kopf zerbrechen und schauen, wie er die jeweilige Formel in eine von ihm gewählte Sprache übersetzt. Ich gebe euch hier, da es sich um ein Beispiel handelt die Funktionen vor. Verändert könnt ihr sie ja auch nachträglich.

function calcDistance(testElement, inputData) {
distances = [];

testElement.data.forEach((value, index) => {
distances.push(Math.pow(value - inputData[index], 2));
});

distanceSum = distances.filter(distance => distance !== NaN).reduce(function(a, b) { return a + b; }, 0);

return Math.sqrt(distanceSum);
}

knn-Algorithmus

function getLabelByMinDistances(distances, k=3) {
// sort distances ascending
distances.sort((d1, d2) => {
if (d1.distance > d2.distance) {
return 1;
} else if (d1.distance < d2.distance) {
return -1;
}
return 0;
});

var counter = [];

// get k top elements and fill counter for label
distances.splice(1, k).forEach(distance => {
if (counter.find(el => el.label === distance.label)) {
counter.find(el => el.label === distance.label).count += 1;
} else {
counter.push({label: distance.label, count: 1});
}
});

// sort counter descending
counter.sort((c1, c2) => {
if (c1.count > c2.count) {
return -1;
} else if (c1.count < c2.count) {
return 1;
}
return 0;
});

return counter[0].label;
}

Unter Umständen lässt sich sicherlich auch die ein oder andere Codezeile noch vereinfachen oder eleganter schreiben. Rein für das Verständnis sollte das aber für uns genügen.

Canvas Data auslesen

Anhand der Funktionen, die der Kontext eines Canvas-Elements bereitstellt, ist das auch wirklich sehr einfach. Es gibt die Funktion „getImageData“. Dieser kann man eine Fläche von Pixeln mitgeben, und man bekommt ein Uint8ClampedArray. Dies beinhaltet im Grunde für jeden Pixel 4 Werte. Diese 4 Werte stellen den RGBA-Farbcode dar. Sprich Rot, Grün, Blau und Alpha. Da wir im Beispiel keine Farben verwenden und uns eigentlich nur die Sättigung von „schwarz“ interessiert, verwerfen wir aus diesem Array alle RGB-Werte und nehmen für jeden Pixel nur den Alpha-Wert auf.

function readData() {
var imgData = [];
var arr = Array.from(ctx.getImageData(0, 0, 75, 75).data);

for (var i=0; i < arr.length; i+=4) {
imgData.push(arr.slice(i, i+4)[3]);
}

return imgData;
}

Fun with Buttons

Nun wird es Zeit unsere Button mit der jeweiligen Funktion zu versehen. Zunächst der Button, der fürs Labeln zuständig ist.

script.js

function addTestData() {
var canvasContent = readData();
var label = document.getElementById('label').value;
testData.push({label: label, data: canvasContent});
erase();
}

Als zweites müssen wir noch eine ungelabelte Eingabe mit unseren Beispielen vergleichen und eine Vorhersage treffen können.

script.js

function predict() {
var canvasContent = readData();

var distances = [];
testData.forEach(testObject => {
distances.push({label: testObject.label, distance: calcDistance(testObject, canvasContent)});
});

var label = getLabelByMinDistances(distances);
document.getElementById('output').value = label;
}

Export der Testdaten

Glückwunsch. Du hast die Basis für das eigentlich Testen geschaffen. Unser Konstrukt kann nun gelabelte Testdaten empfangen und anhand dieser Testdaten für einen bisher unbekannten Input Vorhersagen treffen. Da man nicht jedes mal die Daten neu labeln will, hier noch ein kurzes Beispiel für das Loggen der bisherigen Testdaten und wie man diese wieder verwenden kann.

index.html

<button onclick="exp()">Exportieren</button>

script.js

function exp() {
console.log(JSON.stringify(testData));
}

Diesen Output könnt ihr ganz leicht aus der Konsole heraus kopieren und z.B. in eurem script.js auf die testData anstelle des leeren Arrays setzen oder in der Init Funktion aus einem anderen JavaScript-File lesen.

bekannte Probleme

  • wir vergleichen Pixelweise, d.h. je nachdem wie man die Zahl schreibt, kann die Anzahl der matchenden Pixel auf ein anderes Label hinweisen. Alternativ könnte man auch Formen im Bild erkennen und diese Vergleichen.
  • Backpropagation fehlt

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.