paint-brush
ML-Aufgaben auf Swift ohne Python, neuronale Netze und Bibliothekenby@pichukov
552
552

ML-Aufgaben auf Swift ohne Python, neuronale Netze und Bibliotheken

Aleksei Pichukov15m2024/01/04
Read on Terminal Reader

Dieser Artikel ist keine Anleitung, wie man etwas in Swift schreibt; Vielmehr handelt es sich eher um einen Denkanstoß über die aktuelle Denkweise vieler Entwickler, die Python als Brücke zur ultimativen Lösung für ML-Bibliotheken betrachten, die jedes Problem oder jede Aufgabe löst, auf die sie stoßen, unabhängig von der verwendeten Sprache. Ich würde wetten, dass die meisten Entwickler ihre Zeit lieber in die Suche nach Möglichkeiten investieren, Python-Bibliotheken in ihre Sprache/Umgebung zu integrieren, anstatt alternative Lösungen ohne sie in Betracht zu ziehen. Das ist zwar nicht grundsätzlich schlecht – die Wiederverwendung war in den letzten Jahrzehnten ein wesentlicher Treiber für den Fortschritt in der IT –, aber ich habe das Gefühl, dass viele Entwickler nicht einmal mehr über alternative Lösungen nachdenken. Diese Denkweise wird mit dem aktuellen Stand und den Fortschritten bei großen Sprachmodellen noch stärker verankert. Wir werden eine klassische ML-Aufgabe nehmen und sie mithilfe der Swift-Sprache und ohne Bibliotheken lösen.
featured image - ML-Aufgaben auf Swift ohne Python, neuronale Netze und Bibliotheken
Aleksei Pichukov HackerNoon profile picture
0-item
1-item

Neuronale Netze stehen heute an der Spitze des maschinellen Lernens (ML), und Python ist zweifellos die Programmiersprache der Wahl für jede ML-Aufgabe, unabhängig davon, ob man sie mit neuronalen Netzen lösen möchte oder nicht. Es steht eine große Auswahl an Python-Bibliotheken zur Verfügung, die das gesamte Spektrum an ML-Aufgaben abdecken, wie z. B. NumPy, Pandas, Keras, TensorFlow, PyTorch und so weiter. Diese Bibliotheken basieren normalerweise auf C- oder C++-Implementierungen von ML-Algorithmen und -Ansätzen unter der Haube, da Python für sie zu langsam ist. Allerdings ist Python nicht die einzige Programmiersprache, die es gibt, und es ist auch nicht die, die ich in meiner täglichen Arbeit verwende.


Dieser Artikel ist keine Anleitung, wie man etwas in Swift schreibt; Vielmehr handelt es sich eher um einen Denkanstoß über die aktuelle Denkweise vieler Entwickler, die Python als Brücke zur ultimativen Lösung für ML-Bibliotheken betrachten, die jedes Problem oder jede Aufgabe löst, auf die sie stoßen, unabhängig von der verwendeten Sprache. Ich würde wetten, dass die meisten Entwickler ihre Zeit lieber in die Suche nach Möglichkeiten investieren, Python-Bibliotheken in ihre Sprache/Umgebung zu integrieren, anstatt alternative Lösungen ohne sie in Betracht zu ziehen. Das ist zwar nicht grundsätzlich schlecht – die Wiederverwendung war in den letzten Jahrzehnten ein wesentlicher Treiber für den Fortschritt in der IT –, aber ich habe das Gefühl, dass viele Entwickler nicht einmal mehr über alternative Lösungen nachdenken. Diese Denkweise wird mit dem aktuellen Stand und den Fortschritten bei großen Sprachmodellen noch stärker verankert.


Es fehlt das Gleichgewicht; Wir beeilen uns, LLMs zu bitten, unsere Probleme zu lösen, etwas Python-Code zu besorgen, ihn zu kopieren und unsere Produktivität mit potenziell erheblichem Overhead durch unnötige Abhängigkeiten zu genießen.


Lassen Sie uns einen alternativen Ansatz zur Lösung der vorliegenden Aufgabe erkunden, indem wir nur Swift, Mathematik und keine anderen Tools verwenden.


Wenn Leute anfangen, neuronale Netze zu erlernen, gibt es zwei klassische Hello World-Beispiele, die Sie in den meisten Tutorials und Einführungsmaterialien dafür finden können. Die erste Möglichkeit ist die Erkennung handschriftlicher Ziffern. Die zweite ist eine Datenklassifizierung. Ich werde mich in diesem Artikel auf die zweite Lösung konzentrieren, aber die Lösung, die ich durchgehen werde, funktioniert auch für die erste.


Das sehr gute visuelle Beispiel dafür findet sich im TensorFlow Playground, wo man mit verschiedenen neuronalen Netzwerkstrukturen herumspielen und visuell beobachten kann, wie gut das resultierende Modell die Aufgabe löst.


TensorFlow Playground-Beispiel


Sie fragen sich vielleicht, welche praktische Bedeutung diese Punkte auf einem Bild mit unterschiedlichen Farben haben? Die Sache ist, dass es sich um eine visuelle Darstellung einiger Datensätze handelt. Sie können viele verschiedene Arten von Daten auf genau die gleiche oder ähnliche Weise darstellen, beispielsweise soziale Gruppen von Menschen, die bestimmte Produkte oder Musikpräferenzen kaufen. Da ich mich hauptsächlich auf die mobile iOS-Entwicklung konzentriere, werde ich auch ein Beispiel einer realen Aufgabe geben, die ich gelöst habe und die auf ähnliche Weise visuell dargestellt werden kann: das Auffinden elektrischer Leitungen in Wänden mithilfe eines Gyroskops und eines Magnetometers auf einem Mobiltelefon. In diesem speziellen Beispiel haben wir einen Parametersatz, der sich auf den gefundenen Draht bezieht, und einen weiteren Parametersatz für nichts innerhalb der Wand.


Werfen wir einen Blick auf die Daten, die wir verwenden werden.

ML-Test


Wir haben hier zwei Arten von Daten: rote Punkte und blaue Punkte. Wie ich oben beschrieben habe, kann es sich um eine visuelle Darstellung jeglicher Art von klassifizierten Daten handeln. Nehmen wir zum Beispiel den roten Bereich als den Bereich, in dem wir ein Signal vom Magnetometer und Gyroskop erhalten, wenn wir ein elektrisches Kabel in der Wand haben, und den blauen Bereich, falls wir kein Signal haben.


Wir können sehen, dass diese Punkte irgendwie gruppiert sind und eine Art rote und blaue Formen bilden. Diese Punkte wurden erzeugt, indem zufällige Punkte aus dem folgenden Bild entnommen wurden:


Punkte gruppiert


Wir werden dieses Bild als Zufallsmodell für unseren Zugprozess verwenden, indem wir Zufallspunkte zum Trainieren des Modells und andere Zufallspunkte zum Testen unseres trainierten Modells nehmen.


Das Originalbild ist 300 x 300 Pixel groß und enthält 90.000 Punkte (Punkte). Für Trainingszwecke werden wir nur 0,2 % dieser Punkte verwenden, was weniger als 100 Punkte ist. Um die Leistung des Modells besser zu verstehen, wählen wir zufällig 3000 Punkte aus und zeichnen auf dem Bild Kreise um sie herum. Diese visuelle Darstellung wird uns einen umfassenderen Überblick über die Ergebnisse verschaffen. Wir können auch den Prozentsatz der Genauigkeit messen, um die Effizienz des Modells zu überprüfen.


Wie machen wir ein Modell? Wenn wir uns diese beiden Bilder zusammen ansehen und versuchen, unsere Aufgabe zu vereinfachen, werden wir feststellen, dass die Aufgabe tatsächlich darin besteht, das Origin-Bild aus den uns vorliegenden Daten (Stapel roter und blauer Punkte) neu zu erstellen. Und je näher das Bild, das wir von unserem Modell erhalten, dem Original kommt, desto genauer wird unser Modell funktionieren. Wir können unsere Testdaten auch als eine Art extrem komprimierte Version unseres Originalbilds betrachten und das Ziel haben, sie wieder zu dekomprimieren.


Was wir tun werden, ist, unsere Punkte in mathematische Funktionen umzuwandeln, die im Code als Arrays oder Vektoren dargestellt werden (ich werde hier im Text den Vektorbegriff verwenden, nur weil er zwischen einer Funktion aus der Welt der Mathematik und einem Array aus der Softwareentwicklung liegt). Anschließend werden wir diese Vektoren verwenden, um jeden Testpunkt herauszufordern und herauszufinden, zu welchem Vektor er eher gehört.


Um unsere Daten zu transformieren, werde ich eine diskrete Kosinustransformation (DCT) versuchen. Ich werde nicht auf mathematische Erklärungen darüber eingehen, was es ist und wie es funktioniert, da Sie diese Informationen bei Bedarf leicht finden können. Ich kann jedoch in einfachen Worten erklären, wie es uns helfen kann und warum es nützlich ist. Die DCT wird in vielen Bereichen eingesetzt, unter anderem bei der Bildkomprimierung (z. B. im JPEG-Format). Es wandelt die Daten in ein kompakteres Format um, indem nur die wichtigen Teile des Bildes beibehalten und unwichtige Details entfernt werden. Wenn wir die DCT auf unser 300x300-Bild anwenden, das nur rote Punkte enthält, erhalten wir eine 300x300-Wertematrix, die in ein Array (oder einen Vektor) umgewandelt werden kann, indem jede Zeile separat genommen wird.


Lassen Sie uns endlich etwas Code dafür schreiben. Zuerst müssen wir ein einfaches Objekt erstellen, das unseren Punkt (Punkt) darstellt.


 enum Category { case red case blue case none } struct Point: Hashable { let x: Int let y: Int let category: Category }


Möglicherweise stellen Sie fest, dass wir eine zusätzliche Kategorie namens none haben. Am Ende werden wir tatsächlich drei Vektoren erstellen: einen für red Punkte, einen zweiten für blue Punkte und den dritten für alles andere, was durch none dargestellt wird. Wir könnten zwar nur zwei davon haben, aber ein trainierter Vektor für Nicht-Rot und Nicht-Blau würde die Sache etwas einfacher machen.


Wir haben „Point“ entsprechend dem Hashable Protokoll festgelegt, um einen Set zu verwenden, um zu vermeiden, dass Punkte mit denselben Koordinaten in unserem Testvektor vorhanden sind.


 func randomPoints(from points: [Point], percentage: Double) -> [Point] { let count = Int(Double(points.count) * percentage) var result = Set<Point>() while result.count < count { let index = Int.random(in: 0 ..< points.count) result.insert(points[index]) } return Array<Point>(result) }


Jetzt können wir es verwenden, um 0.2% zufällige Punkte aus unserem Originalbild für rote, blaue und keine Punkte zu ziehen.


 redTrainPoints = randomPoints(from: redPoints, percentage: 0.002) blueTrainPoints = randomPoints(from: bluePoints, percentage: 0.002) noneTrainPoints = randomPoints(from: nonePoints, percentage: 0.002)


Wir sind bereit, diese Trainingsdaten mithilfe von DCT zu transformieren. Hier ist eine Implementierung davon:


 final class CosTransform { private var sqrtWidthFactorForZero: Double = 0 private var sqrtWidthFactorForNotZero: Double = 0 private var sqrtHeightFactorForZero: Double = 0 private var sqrtHeightFactorForNotZero: Double = 0 private let cosLimit: Int init(cosLimit: Int) { self.cosLimit = cosLimit } func discreteCosTransform(for points: [Point], width: Int, height: Int) -> [[Double]] { if sqrtWidthFactorForZero == 0 { prepareSupportData(width: width, height: height) } var result = Array(repeating: Array(repeating: Double(0), count: width), count: height) for y in 0..<height { for x in 0..<width { let cos = cosSum( points: points, width: width, height: height, x: x, y: y ) result[y][x] = cFactorHeight(index: y) * cFactorWidth(index: x) * cos } } return result } func shortArray(matrix: [[Double]]) -> [Double] { let height = matrix.count guard let width = matrix.first?.count else { return [] } var array: [Double] = [] for y in 0..<height { for x in 0..<width { if y + x <= cosLimit { array.append(matrix[y][x]) } } } return array } private func prepareSupportData(width: Int, height: Int) { sqrtWidthFactorForZero = Double(sqrt(1 / CGFloat(width))) sqrtWidthFactorForNotZero = Double(sqrt(2 / CGFloat(width))) sqrtHeightFactorForZero = Double(sqrt(1 / CGFloat(height))) sqrtHeightFactorForNotZero = Double(sqrt(2 / CGFloat(height))) } private func cFactorWidth(index: Int) -> Double { return index == 0 ? sqrtWidthFactorForZero : sqrtWidthFactorForNotZero } private func cFactorHeight(index: Int) -> Double { return index == 0 ? sqrtHeightFactorForZero : sqrtHeightFactorForNotZero } private func cosSum( points: [Point], width: Int, height: Int, x: Int, y: Int ) -> Double { var result: Double = 0 for point in points { result += cosItem(point.x, x, height) * cosItem(point.y, y, width) } return result } private func cosItem( _ firstParam: Int, _ secondParam: Int, _ lenght: Int ) -> Double { return cos((Double(2 * firstParam + 1) * Double(secondParam) * Double.pi) / Double(2 * lenght)) } }


Lassen Sie uns eine Instanz des CosTransform Objekts erstellen und testen.


 let math = CosTransform(cosLimit: Int.max) ... redCosArray = cosFunction(points: redTrainPoints) blueCosArray = cosFunction(points: blueTrainPoints) noneCosArray = cosFunction(points: noneTrainPoints)


Wir verwenden hier einige einfache Hilfsfunktionen:


 func cosFunction(points: [Point]) -> [Double] { return math.shortArray( matrix: math.discreteCosTransform( for: points, width: 300, height: 300 ) ) }


Es gibt einen cosLimit Parameter in CosTransform , der in der shortArray-Funktion verwendet wird. Ich werde den Zweck später erläutern. Lassen Sie uns ihn zunächst ignorieren und das Ergebnis von 3000 zufälligen Punkten aus dem Originalbild mit unseren erstellten Vektoren redCosArray , blueCosArray und noneCosArray vergleichen. Damit es funktioniert, müssen wir einen weiteren DCT-Vektor aus einem einzelnen Punkt erstellen, der aus dem Originalbild stammt. Dies machen wir genau auf die gleiche Weise und mit den gleichen Funktionen, die wir bereits für unsere Cos-Vektoren Red , Blue und None gemacht haben. Aber wie können wir herausfinden, zu welchem dieser neue Vektor gehört? Dafür gibt es einen sehr einfachen mathematischen Ansatz: Dot Product . Da wir die Aufgabe haben, zwei Vektoren zu vergleichen und das ähnlichste Paar zu finden, gibt uns Dot Product genau dies. Wenn Sie eine Skalarproduktoperation auf zwei identische Vektoren anwenden, erhalten Sie einen positiven Wert, der größer ist als jedes andere Skalarproduktergebnis, das auf denselben Vektor und jeden anderen Vektor mit unterschiedlichen Werten angewendet wird. Und wenn Sie ein Skalarprodukt auf die orthogonalen Vektoren anwenden (Vektoren, die untereinander nichts gemeinsam haben), erhalten Sie als Ergebnis eine 0. Unter Berücksichtigung dessen können wir einen einfachen Algorithmus entwickeln:


  1. Gehen Sie alle unsere 3000 Zufallspunkte einzeln durch.
  2. Erstellen Sie mit DCT (Diskrete Kosinustransformation) einen Vektor aus einer 300x300-Matrix mit nur einem einzigen Punkt.
  3. Wenden Sie ein Skalarprodukt für diesen Vektor mit redCosArray , dann mit blueCosArray und dann mit noneCosArray an.
  4. Das beste Ergebnis aus dem vorherigen Schritt weist uns auf die richtige Antwort hin: Red , Blue , None .


Die einzige hier fehlende Funktionalität ist ein Punktprodukt. Schreiben wir eine einfache Funktion dafür:


 func dotProduct(_ first: [Double], _ second: [Double]) -> Double { guard first.count == second.count else { return 0 } var result: Double = 0 for i in 0..<first.count { result += first[i] * second[i] } return result }


Und hier ist eine Implementierung des Algorithmus:


 var count = 0 while count < 3000 { let index = Int.random(in: 0 ..< allPoints.count) let point = allPoints[index] count += 1 let testArray = math.shortArray( matrix: math.discreteCosTransform( for: [point], width: 300, height: 300 ) ) let redResult = dotProduct(redCosArray, testArray) let blueResult = dotProduct(blueCosArray, testArray) let noneResult = dotProduct(noneCosArray, testArray) var maxValue = redResult var result: Category = .red if blueResult > maxValue { maxValue = blueResult result = .blue } if noneResult > maxValue { maxValue = noneResult result = .none } fillPoints.append(Point(x: point.x, y: point.y, category: result)) }


Jetzt müssen wir nur noch ein Bild aus fillPoints zeichnen. Werfen wir einen Blick auf die Zugpunkte, die wir verwendet haben, die DCT-Vektoren, die wir aus unseren Zugdaten erstellt haben, und das Endergebnis, das wir erhalten haben:

Ergebnis

Nun, es sieht nach zufälligem Rauschen aus. Aber werfen wir einen Blick auf die visuelle Darstellung von Vektoren. Dort sind einige Spitzen zu sehen. Das sind genau die Informationen, auf die wir uns konzentrieren müssen, um den größten Teil des Rauschens aus unserem DCT-Ergebnis zu entfernen. Wenn wir uns die einfache visuelle Darstellung der DCT-Matrix ansehen, werden wir feststellen, dass die nützlichsten Informationen (diejenigen, die die einzigartigen Merkmale des Bildes beschreiben) in der oberen linken Ecke konzentriert sind:


Konzentration


Gehen wir nun einen Schritt zurück und überprüfen wir die shortArray Funktion noch einmal. Wir verwenden hier einen cosLimit Parameter genau deshalb, weil wir die obere linke Ecke der DCT-Matrix nehmen und nur die aktivsten Parameter verwenden, die unseren Vektor einzigartig machen.


 func shortArray(matrix: [[Double]]) -> [Double] { let height = matrix.count guard let width = matrix.first?.count else { return [] } var array: [Double] = [] for y in 0..<height { for x in 0..<width { if y + x <= cosLimit { array.append(matrix[y][x]) } } } return array }


Lassen Sie uns unser math Objekt mit verschiedenen cosLimit erstellen:


 let math = CosTransform(cosLimit: 30)


Anstatt nun alle 90.000 Werte zu verwenden, verwenden wir nur 30 x 30 / 2 = 450 davon aus der oberen linken Ecke der DCT-Matrix. Werfen wir einen Blick auf das Ergebnis, das wir erhalten haben:

Ergebnis

Wie Sie sehen, ist es schon besser. Wir können auch beobachten, dass sich die meisten Spitzen, die Vektoren einzigartig machen, immer noch im vorderen Teil befinden (wie im Bild mit Grün markiert). Versuchen wir, CosTransform(cosLimit: 6) zu verwenden, was bedeutet, dass wir nur 6 x 6 / 2 = 18 verwenden werden. 6 x 6 / 2 = 18 Werte von 90.000 und überprüfen Sie das Ergebnis:

Erfolg

Es ist jetzt viel besser, kommt dem Originalbild sehr nahe. Allerdings gibt es nur ein kleines Problem: Diese Implementierung ist langsam. Man muss kein Experte für die Komplexität von Algorithmen sein, um zu erkennen, dass DCT eine zeitaufwändige Operation ist, aber selbst das Skalarprodukt, das eine lineare Zeitkomplexität aufweist, ist nicht schnell genug, wenn mit großen Vektoren mithilfe von Swift-Arrays gearbeitet wird. Die gute Nachricht ist, dass wir die Implementierung viel schneller und einfacher durchführen können, indem wir vDSP aus Apples Accelerate Framework verwenden, das wir bereits als Standardbibliothek haben. Sie können hier etwas über vDSP lesen, aber in einfachen Worten handelt es sich um eine Reihe von Methoden zur sehr schnellen Ausführung digitaler Signalverarbeitungsaufgaben. Es verfügt über viele Low-Level-Optimierungen, die perfekt mit großen Datenmengen funktionieren. Lassen Sie uns unser Punktprodukt und DCT mit vDSP implementieren:


 infix operator • public func •(left: [Double], right: [Double]) -> Double { return vDSP.dot(left, right) } prefix operator ->> public prefix func ->>(value: [Double]) -> [Double] { let setup = vDSP.DCT(count: value.count, transformType: .II) return setup!.transform(value.compactMap { Float($0) }).compactMap { Double($0) } }


Um es weniger mühsam zu machen, habe ich einige Operatoren verwendet, um die Lesbarkeit zu verbessern. Jetzt können Sie diese Funktionen auf folgende Weise nutzen:


 let cosRedArray = ->> redValues let redResult = redCosArray • testArray


Bei der neuen DCT-Implementierung gibt es ein Problem bezüglich unserer aktuellen Matrixgröße. Mit unserem 300 x 300-Bild würde es nicht funktionieren, da es für die Arbeit mit bestimmten Größen optimiert ist, die Potenzen von 2 sind. Daher müssen wir einige Anstrengungen unternehmen, um das Bild zu skalieren, bevor wir es der neuen Methode übergeben.

Zusammenfassung

Vielen Dank an alle, die es bisher geschafft haben, diesen Text zu lesen, oder die faul genug waren, durchzublättern, ohne sie zu lesen. Der Zweck dieses Artikels bestand darin, zu zeigen, dass viele Aufgaben, die mit einigen nativen Instrumenten nicht gelöst werden könnten, mit minimalem Aufwand gelöst werden können. Es macht Spaß, nach alternativen Lösungen zu suchen und sich nicht auf die Integration der Python-Bibliothek als einzige Option zur Lösung solcher Aufgaben zu beschränken.