+41 78 646 23 04 info@informatikburaeu.ch

Ich habe das tolle Buch Neuronale Netze selbst programmieren von Tariq Rashid gekauft. Original in Englisch heisst das Buch Make Your Own Neural Network. Der Beispielcode im Buch ist in Python geschrieben. Es gibt eine Implementation von Python unter macOS. Aber die Standardsprache unter macOS ist Swift. Deshalb habe ich eine Version des Programms in Swift geschrieben. Ich setze voraus, dass Du das Buch gelesen hast – entweder in deutsch oder englisch. Das Programm ist die finale Version mit mehreren Epochen (fünf). Ich erkläre in diesem Artikel meine Version in Swift. Deshalb erstellte ich ein neues Projekt in Xcode 10.1 und wählte die Applikation Command Line Tool. Der ganze Code steht in der Datei main.swift. Du findest ihn unter https://github.com/michabuehlmann/Neural-Network. Da ist eine Datei namens Neural Network 2.zip welche Du herunterladen kannst. Sie enthält das ganze Projekt. Du brauchst auch die MNIST Datensätze. Dazu musst du die beiden Dateien mnist_test.csv und mnist_train.csv ins download-Verzeichnis kopieren (Wichtig!). Dann kannst Du das Programm starten. Es sollte wie das Original in Python funktionieren. Mit dem Programm erhielt ich eine Performance von 0.963.

Die Klasse Matrix

Ich erstellte eine Klasse namens Matrix. Diese umfasst Funktionen, welche Python mit der Bibliothek numpy bietet. Sie umfasst die Handhabung von mathematischen Matrizen. Die Daten der Matrix werden in der Variable data gespeichert. Nebenbei werden Variablen rows und columns für die Zeilen und Spalten definiert:

class Matrix: CustomStringConvertible {
    internal var data: [[Double]]
    
    var rows: Int
    var columns: Int

Ex existieren einige Init-Funktionen für die Matrixklasse:

    init(_ data:[[Double]]) {
        self.data = data
        self.rows = data.count
        self.columns = data.first?.count ?? 0
    }
    
    init(_ data:[[Double]], rows:Int, columns:Int) {
        self.data = data
        self.rows = rows
        self.columns = columns
    }
    
    init(rows:Int, columns:Int) {
        self.data = [[Double]](repeating: [Double](repeating: 0, count: columns), count: rows)
        self.rows = rows
        self.columns = columns
    }

Diese Methoden können wie folgt verwendet werden:

    let wih = Matrix([[0.9, 0.3, 0.4], [0.2, 0.8, 0.2], [0.1, 0.5, 0.6]])
    let who = Matrix([[0.3, 0.7, 0.5], [0.6, 0.5, 0.2], [0.8, 0.1, 0.9]])
    
    let x = Matrix([[10.0, 9.0, 8.0], [3.0, 2.0, 1.0]], rows: 2, columns: 3)
    let y = Matrix([[1, 2, 3], [4, 5, 6]], rows: 2, columns: 3)
    
    let z = Matrix([[1, 2], [3, 4], [5, 6]], rows: 3, columns: 2)

    let xx = Matrix([[1,2], [3,4]], rows: 2, columns: 2)
    let yy = Matrix([[5,6], [7,8]], rows: 2, columns: 2)

Die nächste Methode ermöglicht den Gebrauch von einzelnen Elementen der Matrix mit der Subscript-Syntax:

    subscript(row: Int, column: Int) -> Double {
        get {
            return data[row][column]
        }
        set {
            data[row][column] = newValue
        }
    }

Diese Methode erlaubt es uns ein Element der Matrix wih mit Index row und col anzusprechen.

wih[row,col]

Die folgenden Methoden geben die Dimension der Matrix, die Anzahl an Zeilen und Spalten, sowie aller Elemente zurück.

    var dimensions: (rows: Int, columns: Int) {
        get {
            return (data.count, data.first?.count ?? 0)
        }
    }
    
    var rowCount: Int {
        get {
            return data.count
        }
    }
    
    var columnCount: Int {
        get {
            return data.first?.count ?? 0
        }
    }
    
    var count: Int {
        get {
            return rows * columns
        }
    }

Die nächste Methode gibt eine Matrix aus. Sie druckt die einzelnen Elemente und die Anzahl Zeilen und Spalten.

var description: String {
        var dsc = ""
        for row in 0..<rows {
            for col in 0..<columns {
                let d = data[row][col]
                dsc += String(d) + " "
            }
            dsc += "\n"
        }
        dsc += "rows: \(rows) columns: \(columns)\n"
        return dsc
    }

Diese Methode wird folgendermassen benutzt.

print(z)

Die Ausgabe ergibt folgendes:

1.0 2.0

3.0 4.0

5.0 6.0

rows: 3 columns: 2

Ich schrieb auch eine Funktion, um die Elemente einer Matrix grafisch darzustellen. Diese braucht aber noch zusätzliche Programmierung. Deshalb zeige ich nur den Kern der Funktion ohne den Zusatz.

func zeichne() {
        let context = NSGraphicsContext.current?.cgContext;
        //let shape = "square"
        context!.setLineWidth(1.0)
        
        for col in 0..<self.columns {
            for row in 0..<self.rows {
                let color = CGFloat(1.0/(7.0-self[row,col]));
                //print(color)
                context!.setStrokeColor(red: color, green: color, blue: color, alpha: 1)
                context!.setFillColor(red: color, green: color, blue: color, alpha: 1)
                
                let rectangle = CGRect(x: col*10, y: row*10, width: 10, height: 10)
                context!.addRect(rectangle)
                context!.drawPath(using: .fillStroke)
            }
        }
    }

Die folgende Funktion gibt den Index des grössten Wertes eines Vektors zurück.

    func maximum() -> Int {
        var data = 0.0
        var index = 0
        for row in 0.. data {
                data = self[row,0]
                index = row
            }
        }
        return index
    }

Nun kommen wir zu einer Menge an Funktionen um mit Matrizen zu rechnen. Diese wurden als Operatoren programmiert. So können sie wie in der Mathematik gebraucht werden. Als erstes kommt der Plus-Operator mit dem zwei Matrizen addiert werden können.

static func +(left: Matrix, right: Matrix) -> Matrix {
        assert(left.dimensions == right.dimensions, "Cannot add matrices of different dimensions")
        let m = Matrix(left.data, rows: left.rows, columns: left.columns)
        for row in 0..<left.rows {
            for col in 0..<left.columns {
                m[row,col] += right[row,col]
            }
        }
        return m
    }

Dieser Operator wird wie folgt gebraucht:

let a = x+y
print(a)

Die Ausgabe ist:

11.0 11.0 11.0

7.0 7.0 7.0

rows: 2 columns: 3

Es folgt der += Operator, welcher die rechte Matrix zur linken addiert:

static func +=(left: Matrix, right: Matrix) {
        assert(left.dimensions == right.dimensions, "Cannot add matrices of different dimensions")
        for row in 0..<left.rows {
            for col in 0..<left.columns {
                left[row,col] += right[row,col]
            }
        }
    }

Nun der entsprechende Minus-Operator. Er subtrahiert die rechte von der linken Matrix.

static func -(left: Matrix, right: Matrix) -> Matrix {
        assert(left.dimensions == right.dimensions, "Cannot add matrices of different dimensions")
        let m = Matrix(left.data, rows: left.rows, columns: left.columns)
        for row in 0..<left.rows {
            for col in 0..<left.columns {
                m[row,col] -= right[row,col]
            }
        }
        return m
    }

 

Als nächstes folgt eine Funktion welche die Matrix right von einem Double-Wert left subtrahiert.

static func -(left: Double, right: Matrix) -> Matrix {
        let m = Matrix(rows: right.rows, columns: right.columns)
        for row in 0..<right.rows {
            for col in 0..<right.columns {
                m[row,col] = left - right[row,col]
            }
        }
        return m
    }

Der folgende Operator multipliziert die Elemente der linken Matrix mit den Elementen der rechten Matrix. Dies ist nicht das innere Produkt, welches später folgt.

static func *(left: Matrix, right: Matrix) -> Matrix {
        assert(left.dimensions == right.dimensions, "Cannot add matrices of different dimensions")
        let m = Matrix(left.data, rows: left.rows, columns: left.columns)
        for row in 0..<left.rows {
            for col in 0..<left.columns {
                m[row,col] *= right[row,col]
            }
        }
        return m
    }

Ähnlich der obigen Funktion mit dem Minus-Operator multipliziert diese Funktion (Operator) einen Double-Wert left mit den einzelnen Elementen der Matrix right.

static func *(left: Double, right: Matrix) -> Matrix {
        let m = Matrix(right.data, rows: right.rows, columns: right.columns)
        for row in 0..<right.rows {
            for col in 0..<right.columns {
                m[row,col] *= left
            }
        }
        return m
    }

Nun folgt der Vergleichsoperator ==. Er gibt true zurück, falls die beiden Matrizen gleich sind. Ansonsten wird false zurückgegeben.

static func ==(left: Matrix, right: Matrix) -> Bool {
        if left.rows != right.rows {
            return false
        }
        if left.columns != right.columns {
            return false
        }
        for i in 0..<left.rows {
            for j in 0..<left.columns {
                if left[i,j] != right[i,j] {
                    return false
                }
            }
        }
        return true
    }

Kennst du die Transponierte einer Matrix? Der Operator ^ gibt die Transponierte der Matrix m zurück.

static postfix func ^(m: Matrix) -> Matrix {
        let t = Matrix(rows:m.columns, columns:m.rows)
        for row in 0..<m.rows {
            for col in 0..<m.columns {
                t[col,row] = m[row,col]
            }
        }
        return t
    }

Der Operator wird folgendermassen benutzt:

let b = x^
print(b)

Die Ausgabe:

10.0 3.0

9.0 2.0

8.0 1.0

rows: 3 columns: 2

Um die Klasse Matrix abzuschliessen folgt nun die Matrixmultiplikation, auch inneres Produkt genannt. Der Operator wird ** genannt.

static func **(left: Matrix, right: Matrix) -> Matrix {
        assert(left.columns == right.rows, "Two matricies can only be matrix mulitiplied if one has dimensions mxn & the other has dimensions nxp where m, n, p are in R")
        let C = Matrix(rows: left.rows, columns: right.columns)
        for i in 0..<left.rows {
            for j in 0..<right.columns {
                for k in 0..<right.rows {
                    C[i, j] += left[i, k] * right[k, j]
                }
            }
        }
        return C
    }

Du gebrauchst den Operator ** wie folgt:

let c = z**y
print(c)

Die Ausgabe ist:

9.0 12.0 15.0

19.0 26.0 33.0

29.0 40.0 51.0

rows: 3 columns: 3

 

Die Klasse NeuralNetwork

Hier kommt die Klasse NeuralNetwork mit folgenden Eigenschaften:

class NeuralNetwork {
    internal var inodes: Int
    internal var hnodes: Int
    internal var onodes: Int
    internal var lr: Double
    
    internal var wih: Matrix
    internal var who: Matrix
    
    internal var r: UInt64 = 0

Wie immer hat die Klasse eine Init-Funktion:

init(inputnodes: Int, hidddennodes: Int, outputnodes: Int, learningrate: Double) {
        self.inodes = inputnodes
        self.hnodes = hidddennodes
        self.onodes = outputnodes
        self.lr = learningrate
        
        self.wih = Matrix(rows: self.hnodes, columns: self.inodes)
        self.who = Matrix(rows: self.onodes, columns: self.hnodes)
        //self.wih = Matrix([[0.9, 0.3, 0.4], [0.2, 0.8, 0.2], [0.1, 0.5, 0.6]])
        //self.who = Matrix([[0.3, 0.7, 0.5], [0.6, 0.5, 0.2], [0.8, 0.1, 0.9]])
        //print(wih)
        //print(who)
        
        for row in 0..<wih.rows {
            for col in 0..<wih.columns {
                arc4random_buf(&self.r, 8)
                wih[row,col] = (Double(self.r) / Double(UInt64.max)) - 0.5
            }
        }
        for row in 0..<who.rows {
            for col in 0..<who.columns {
                arc4random_buf(&self.r, 8)
                who[row,col] = (Double(self.r) / Double(UInt64.max)) - 0.5
            }
        }
    }

 

Da ist eine Definition der beiden Matrizen wih und who. Den Elementen werden jeweils Zufallswerte zugewiesen, wie im Buch besprochen. Weil wir die Sigmoid-Funktion immer wieder brauchen, definieren wir sie hier.

func sigmoid(_ x: Double) -> Double {
        return 1.0 / (1.0 + exp(-x))
    }

 

Die nächste Routine ist die Aktivierungsfunktion welche die Werte der Matrix bzw. Vektors list an die Sigmoid-Funktion übergibt.

func activation_function(_ list: Matrix) -> Matrix {
        let result = Matrix(rows: list.rows, columns: list.columns)
        for i in 0..<list.rows {
            result[i,0] = sigmoid(list[i,0])
        }
        return result
    }

Nun folgt die Methode query.

func query(_ input_list: Matrix) -> Matrix {
    let inputs = input_list^
    let hidden_inputs = wih**inputs
    let hidden_outputs = activation_function(hidden_inputs)
     
    let final_inputs = who**hidden_outputs
    let final_outputs = activation_function(final_inputs)

    return final_outputs
}

inputs ist die Transponierte des Übergabewerts input_list an die Methode. Dann multiplizieren wird die Matrix wih mit dem Vektor Inputs. Das Resultat heisst hidden_inputs. Diese werden mit der Aktivierungsfunktion aktiviert. Das Gleiche wird mit der Multiplikation der Matrix who mit den hidden_outputs und der Aktivierung der final_inputs gemacht. Als Ergebnis werden die final_outputs von der Methode query zurückgegeben.

Danach folgt die Methode train. Sie ist detailliert im Buch beschrieben. Hier folgt deshalb nur eine kurze Zusammenfassung. Wir transponieren die beiden Eingabewerte input_list und target_list. Die hidden_inputs sind die Matrixmultiplikation von wih und den inputs. Die hidden_outputs sind die aktivierten hidden_inputs. Danach folgt die Berechnung der final_inputs und final_outputs. Diese werden wie die hidden_inputs und hidden_outputs berechnet. Danach folgt die Aktualisierung der Gewichte. Hier eine Zusammenfassung dieses Prozesses:

Du kannst den Prozess in Swift im folgenden Code sehen. Er verwendet die Operatoren, welche wir in der Klasse Matrix definiert haben.

func train(input_list: Matrix, target_list: Matrix) {
    let inputs = input_list^
    let targets = target_list^
        
    let hidden_inputs = wih**inputs
    let hidden_outputs = activation_function(hidden_inputs)
        
    let final_inputs = who**hidden_outputs
    let final_outputs = activation_function(final_inputs)
        
    let output_errors = targets - final_outputs
    let hidden_errors = (who^)**output_errors
        
    self.who += self.lr * ((output_errors * final_outputs * (1.0 - final_outputs)) ** (hidden_outputs^))
    self.wih += self.lr * ((hidden_errors * hidden_outputs * (1.0 - hidden_outputs)) ** (inputs^))
}

Der Hauptteil des Programms

Nun programmieren wir die Hauptfunktion. Als erstes deklarieren wir einige Konstanten (in Swift gebrauchen wir let).

let input_nodes = 784
let hidden_nodes = 100
let output_nodes = 10

let learning_rate = 0.2

Dann deklarieren wir eine Instanz der Klasse NeuralNetwork, genannt n.

let n = NeuralNetwork(inputnodes: input_nodes, hidddennodes: hidden_nodes, outputnodes: output_nodes, learningrate: learning_rate)

Als nächstes laden wir die MNIST Trainingsdaten als CSV-Datei mit 60’000 Datensätzen. Diese muss im Download-Verzeichnis liegen!

let training_data_file = "mnist_train"
let DocumentDirURL1 = try! FileManager.default.url(for: .downloadsDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let fileURL1 = DocumentDirURL1.appendingPathComponent(training_data_file).appendingPathExtension("csv")

Dann laden wir die Daten in eine Liste.

var training_data_list = ""
do {
    training_data_list = try String(contentsOf: fileURL1)
} catch let error as NSError {
    print(error)
}

Dann werden die Daten in Zeilen aufgeteilt, separiert mit dem «n» Code.

var lines = training_data_list.split() { $0 == "\n" }.map { $0 }

Danach deklarieren wir zwei Matrizen und die Anzahl Epochen (hier 5).

var inputs = Matrix(rows: 1, columns: input_nodes)
var targets = Matrix(rows: 1, columns: output_nodes)
let epochs = 5

Wir durchlaufen alle Datensätze der Trainingsdaten. Die Sätze werden durch «,» Kommas getrennt.

for j in 0..<epochs {
    for line in 0..<lines.count {
        var all_values = lines[line].split() { $0 == "," }.map { Int($0)! }
        if (line % 100 == 0) {
            print(j, " - ", line)
        }
        for i in 0..<input_nodes {
            inputs[0,i] = (Double(all_values[i+1]) / 255.0 * 0.99) + 0.01
        }
        //print(inputs)
        for i in 0..<output_nodes {
            targets[0,i] = 0.01
        }
        targets[0,all_values[0]] = 0.99
        //print(targets)
        n.train(input_list: inputs, target_list: targets)
    }
}

 

Nach jeweils 100 Datensätzen geben wir ein » – » und die Zeilennummer aus.

if (line % 100 == 0) {
            print(j, " - ", line)
        }

Skaliere und verschiebe die Inputs:

for i in 0..<input_nodes {
            inputs[0,i] = (Double(all_values[i+1]) / 255.0 * 0.99) + 0.01
        }

Erstelle die Zielausgabewerte (target output values, alle 0.01 ausser dem entsprechendem Element, welches den Wert 0.99 erhält).

for i in 0..<output_nodes {
    targets[0,i] = 0.01
}
targets[0,all_values[0]] = 0.99

Zum Schluss trainieren wir das Neuronale Netzwerk mit der entsprechenden Methode der Instanz n der Klasse NeuralNetwork.

n.train(input_list: inputs, target_list: targets)

Hier nochmals die ganze Trainingschleife:

for j in 0..<epochs {
    for line in 0..<lines.count {
        var all_values = lines[line].split() { $0 == "," }.map { Int($0)! }
        if (line % 100 == 0) {
            print(j, " - ", line)
        }
        for i in 0..<input_nodes {
            inputs[0,i] = (Double(all_values[i+1]) / 255.0 * 0.99) + 0.01
        }
        //print(inputs)
        for i in 0..<output_nodes {
            targets[0,i] = 0.01
        }
        targets[0,all_values[0]] = 0.99
        //print(targets)
        n.train(input_list: inputs, target_list: targets)
    }
}

Zum Testen der Daten laden wir die MNIST Testdaten CSV-Datei mit 10’000 Datensets. Nochmals: die Datei muss sich im Downloadverzeichnis befinden!

let test_data_file = "mnist_test"
let DocumentDirURL2 = try! FileManager.default.url(for: .downloadsDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let fileURL2 = DocumentDirURL2.appendingPathComponent(test_data_file).appendingPathExtension("csv")

Dann laden wir die Daten in eine Liste.

var test_data_list = ""
do {
    test_data_list = try String(contentsOf: fileURL2)
} catch let error as NSError {
    print(error)
}

Nun teilen wir die Daten in einzelne Zeilen auf, separiert mit dem «n» Code.

lines = test_data_list.split() { $0 == "\n" }.map { $0 }

Die Variable scorecard wird definiert, um die Netzwerk-Performance zu messen, anfangs noch leer.

var scorecard = [Int]()

Wir gehen alle Datensätze des Trainings-Datensets durch.

for line in 0..<lines.count {

Nach jeweils 100 Zeilen geben wir die Zeilennummer aus.

if (line % 100 == 0) {
        print(line)
    }

Teile die Datensätze, welche durch ein «,» separiert sind.

var all_values = lines[line].split() { $0 == "," }.map { Int($0)! }

Die korrekte Antwort liegt im ersten Element.

var correct_label = Int(all_values[0])

Skaliere und verschiebe die Inputs:

for i in 0..<input_nodes {
    inputs[0,i] = (Double(all_values[i+1]) / 255.0 * 0.99) + 0.01
}

Befrage das Netzwerk (mit der Methode query).

var outputs = Matrix(rows: output_nodes, columns: 1)
outputs = n.query(inputs)

Der Index mit dem höchsten Wert ist die Kennung.

let label = outputs.maximum()

Hänge den richtigen oder falschen Wert an die Liste an.

if (label == correct_label) {

Die Antwort des Netzwerks liefert die richtige Antwort, also hänge eine Eins an die Scorecard an.

    scorecard.append(1)
} else {

Die Antwort des Netzwerks liefert die falsche Antwort, also hänge eine Null an die Scorecard an.

scorecard.append(0)

Hier nochmals die ganze Testschleife:

for line in 0..<lines.count {
    if (line % 100 == 0) {
        print(line)
    }
    var all_values = lines[line].split() { $0 == "," }.map { Int($0)! }
    var correct_label = Int(all_values[0])
    //print("\(correct_label) correct_label")
    
    for i in 0..<input_nodes {
        inputs[0,i] = (Double(all_values[i+1]) / 255.0 * 0.99) + 0.01
    }
    var outputs = Matrix(rows: output_nodes, columns: 1)
    outputs = n.query(inputs)
    //print(outputs)
    let label = outputs.maximum()
    
    //print("\(label) network's answer")
    
    if (label == correct_label) {
        scorecard.append(1)
    } else {
        scorecard.append(0)
    }
}

Berechne den Leistungs-Wert, den Anteil der richtigen Antworten.

let sum = scorecard.reduce(0, +)
let performance = Double(sum) / Double(scorecard.count)
print("performance = \(performance)" )

Ich erhielt folgenden Wert:

performance = 0.963

Program ended with exit code: 0

Das war’s! Viel Spass mit dem Programm.