Steigerung der Leistung maschinellen Lernens mit Rust (Teil 2) | von Vince Vella | Juni 2023

0
26


Experimentieren mit Convolutional Neural Networks (CNNs) von Grund auf in Rust.

Foto von Alina Grubnyak An Unsplash

In meinem vorherigen Artikel (Teil 1) Ich habe mein Experiment zur Entwicklung eines Frameworks für maschinelles Lernen in Rust von Grund auf begonnen. Das Hauptziel meines Experiments bestand darin, die Geschwindigkeitsverbesserungen beim Modelltraining zu messen, die durch die Verwendung von Rust in Verbindung mit PyTorch gegenüber einem Python-Äquivalent erreicht werden können. Die Ergebnisse waren für Feedforward Networks sehr ermutigend. In diesem Artikel baue ich darauf weiter auf, wobei das Hauptziel darin besteht, Convolutional Neural Networks (CNNs) definieren und trainieren zu können. Wie im vorherigen Artikel verwende ich weiterhin die Rust-Kiste Tch-rs als Wrapper für die PyTorch C++-Bibliothek LibTorch, hauptsächlich für den Zugriff auf die linearen Algebra- und Autograd-Funktionen der Tensoren, der Relaxation wird von Grund auf neu entwickelt. Der Code für Teil 1 und 2 ist jetzt auf Github verfügbar (Verknüpfung).

Das Endergebnis dieses Artikels ermöglicht es, Convolutional Neural Networks (CNNs) in Rust wie folgt zu definieren:

Itemizing 1 – Definieren meines CNN-Modells.

struct MyModel {
l1: Conv2d,
l2: Conv2d,
l3: Linear,
l4: Linear,
}

impl MyModel {
fn new (mem: &mut Reminiscence) -> MyModel {
let l1 = Conv2d::new(mem, 5, 1, 10, 1);
let l2 = Conv2d::new(mem, 5, 10, 20, 1);
let l3 = Linear::new(mem, 320, 64);
let l4 = Linear::new(mem, 64, 10);
Self {
l1: l1,
l2: l2,
l3: l3,
l4: l4,
}
}
}

impl Compute for MyModel {
fn ahead (&self, mem: &Reminiscence, enter: &Tensor) -> Tensor {
let mut o = self.l1.ahead(mem, &enter);
o = o.max_pool2d_default(2);
o = self.l2.ahead(mem, &o);
o = o.max_pool2d_default(2);
o = o.flat_view();
o = self.l3.ahead(mem, &o);
o = o.relu();
o = self.l4.ahead(mem, &o);
o
}
}

… und dann wie folgt instanziieren und trainieren:

Itemizing 2 – CNN-Modell trainieren.

fn predominant() {
let (mut x, y) = load_mnist();
x = x / 250.0;
x = x.view([-1, 1, 28, 28]);

let mut m = Reminiscence::new();
let mymodel = MyModel::new(&mut m);
practice(&mut m, &x, &y, &mymodel, 20, 512, cross_entropy, 0.0001);
set free = mymodel.ahead(&m, &x);
println!("Accuracy: {}", accuracy(&y, &out));
}

Durch den Versuch, die Modelldefinition so ähnlich wie möglich mit einem Python-Äquivalent zu halten, sollte Itemizing 1 oben für Python-PyTorch-Benutzer recht intuitiv sein. In der MyModel-Struktur können wir nun Conv2D-Ebenen hinzufügen und diese dann in der zugehörigen Funktion neu initiieren. In der Compute-Trait-Implementierung ist die Vorwärtsfunktion definiert und leitet die Eingabe durch alle Ebenen, einschließlich der Zwischenfunktion MaxPooling. In der Hauptfunktion (Itemizing 2) trainieren wir ähnlich wie in unserem vorherigen Artikel unser Modell und wenden es auf den Mnist-Datensatz an.

In den nächsten Abschnitten werde ich beschreiben, was unter der Haube steckt, um CNNs auf diese Weise definieren und trainieren zu können. Es wird davon ausgegangen, dass der Leser meinem ersten Artikel gefolgt ist (Teil 1), daher werde ich mich in diesem Artikel nur auf die neuen Ergänzungen zu meinem Framework konzentrieren.

Das einzigartige Merkmal von Faltungsnetzwerken besteht darin, dass wir in einigen Schichten (mindestens einer) eine Faltung anstelle einer allgemeinen Matrixmultiplikation anwenden. Der Zweck der Faltungsoperation besteht darin, dass sie mithilfe eines Kernels bestimmte interessierende Merkmale aus einem Eingabebild extrahiert. Ein Kernel ist eine Matrix, die über Unterabschnitte des Bildes (Eingabe) verschoben und multipliziert wird, sodass die Ausgabe eine Transformation der Eingabe in einer bestimmten gewünschten Weise ist (siehe Diagramm unten).

Durchführen einer Faltung mithilfe eines Kernels (Quelle: Kernel (Bildverarbeitung) – Wikipedia)

Im zweidimensionalen Fall verwenden wir ein zweidimensionales Bild ICH Als Eingabe verwenden wir typischerweise auch einen zweidimensionalen Kernel Okay, Dies führt zu den folgenden Faltungsberechnungen:

Gleichung 1 – Faltung für den zweidimensionalen Fall (Goodfellow et. al., 2016)

Wie aus Gleichung 1 abgeleitet werden kann, ist ein naiver Algorithmus zur Anwendung einer Faltung aus rechnerischer Sicht aufgrund der erheblichen Menge an Schleifen und Matrixmultiplikationen recht kostspielig. Erschwerend kommt hinzu, dass diese Berechnung für jede Faltungsschicht im Netzwerk und für jedes Trainingsbeispiel/jeden Stapel mehrmals wiederholt werden muss. Bevor ich meine Bibliothek aus Teil 1 auf die Verarbeitung von CNNs ausweitete, bestand der erste Schritt daher darin, eine effiziente Methode zur Berechnung von Faltungen zu untersuchen.

Die Suche nach effizienten Methoden zur Berechnung von Faltungen ist ein sehr intestine erforschtes Downside (siehe Verknüpfung). Nachdem ich verschiedene Optionen untersucht hatte, darunter einige reine Rust-Versionen, die jedoch Zwischendatentransformationen von PyTorch-Tensoren erforderten, entschied ich mich für die Verwendung der LibTorch C++-Faltungsfunktion. Um mit dieser Funktion zu experimentieren, wollte ich ein kleines Spielzeugprogramm erstellen, das ein Farbbild aufnimmt, es in Graustufen umwandelt und dann einige bekannte Kernel zur Kantenerkennung anwendet.

Ich habe zuerst den Microsoft Bing-Chat gebeten, ein Bild für mich zu erstellen. Nachdem ich mit dem Bild zufrieden conflict, wollte ich die Faltungsfunktion zunächst mit einem Gaußschen Kernel und anschließend mit einem Laplace-Kernel anwenden.

Gaußscher Kernel
Laplace-Kernel

Die Kernel wurden mit der LibTorch C++-Methode conv2d angewendet, die über Tch-rs wie folgt bereitgestellt wird:

Itemizing 3 – LibTorch-conv2d-Methode als verfügbar gemachtes Tch-rs.

pub fn conv2d<T: Borrow<Tensor>>(
&self,
weight: &Tensor,
bias: Possibility<T>,
stride: impl IntList,
padding: impl IntList,
dilation: impl IntList,
teams: i64
) -> Tensor

Mein letztes Spielzeugprogramm ist unten dargestellt:

Itemizing 4 – Ein Bild aufnehmen und Faltungsoperationen zur Kantenerkennung anwenden.

use tch::{Tensor, imaginative and prescient::picture, Form, Machine};

fn rgb_to_grayscale(tensor: &Tensor) -> Tensor {
let red_channel = tensor.get(0);
let green_channel = tensor.get(1);
let blue_channel = tensor.get(2);

// Calculate the grayscale tensor utilizing the luminance formulation
let grayscale = (red_channel * 0.2989) + (green_channel * 0.5870) + (blue_channel * 0.1140);
grayscale.unsqueeze(0)
}

fn predominant() {
let mut img = picture::load("mypic.jpg").anticipate("Did not open picture");
img = rgb_to_grayscale(&img).reshape(&[1,1,1024,1024]);
let bias: Tensor = Tensor::full(&[1], 0.0, (Form::Float, Machine::Cpu));

// Outline and apply Gaussian Kernel
let mut k1 = [-1.0, 0.0, 1.0, -2.0, 0.0, 2.0, -1.0, 0.0, 1.0];
for component in k1.iter_mut() {
*component /= 16.0;
}
let kernel1 = Tensor::from_slice(&k1)
.reshape(&[1,1,3,3])
.to_kind(Form::Float);
img = img.conv2d(&kernel1, Some(&bias), &[1], &[0], &[1], 1);

// Outline and apply Laplacian Kernel
let k2 = [0.0, 1.0, 0.0, 1.0, -4.0, 1.0, 0.0, 1.0, 0.0];
let kernel2 = Tensor::from_slice(&k2)
.reshape(&[1,1,3,3])
.to_kind(Form::Float);
img = img.conv2d(&kernel2, Some(&bias), &[1], &[0], &[1], 1);

picture::save(&img, "filtered.jpg");

}

Das Ergebnis der Operation ist folgendes:

Originalbild (hyperlinks) und resultierendes Bild nach Anwendung der Gaußschen und Laplace-Kernel-Filter auf das Graustufenbild (rechts).

In diesem Spielzeugprogramm haben wir unsere ausgewählten Kernel für die Faltung angewendet und das Originalbild in Kanten (unsere gewünschten Merkmale) umgewandelt. Im nächsten Abschnitt beschreibe ich, wie diese Idee in ein CNN integriert wird, wobei der Hauptunterschied darin besteht, dass der Wert der Kernelmatrix vom Netzwerk während des Trainings ausgewählt wird – d. h. das Netzwerk selbst entscheidet, welche Options es aus dem Bild auswählt, indem es es optimiert Kernel.

Eine typische CNN-Struktur umfasst eine Reihe von Faltungsschichten, auf die jeweils eine Unterabtastungsschicht (Pooling) folgt, die dann typischerweise in vollständig verbundene Schichten eingespeist wird. Pooling trägt wesentlich zur Parameterreduzierung bei, da es die Eingabedaten heruntersampelt. Das folgende Diagramm zeigt eines der frühesten CNNs namens LeNet-5.

Die LeNet-5-Netzwerkarchitektur (Lecun et. al., 1998)

In Teil 1 Wir haben bereits ein einfaches Framework definiert, das eine vollständig verbundene Ebene umfasst. Ebenso müssen wir jetzt eine Definition einer Faltungsschicht in unser Framework einfügen, damit wir sie bei der Definition neuer Netzwerkarchitekturen zur Verfügung haben (wie in Itemizing 1). Das andere, was wir im Hinterkopf behalten müssen, ist, dass wir in unserem Spielzeugprogramm (Itemizing 4) sowohl die Kernel-Matrix als auch den Bias als fest festlegen, jetzt müssen wir sie jedoch als Netzwerkparameter definieren, die von unserem Trainingsalgorithmus trainiert werden. Daher müssen wir ihre Farbverläufe im Auge behalten und entsprechend aktualisieren.

Die neue Faltungsschicht Conv2d ist wie folgt definiert:

Itemizing 5 – Neue Conv2d-Ebene.

pub struct Conv2d {
params: HashMap<String, usize>,
}

impl Conv2d {
pub fn new (mem: &mut Reminiscence, kernel_size: i64, in_channel: i64, out_channel: i64, stride: i64) -> Self {
let mut p = HashMap::new();
p.insert("kernel".to_string(), mem.new_push(&[out_channel, in_channel, kernel_size, kernel_size], true));
p.insert("bias".to_string(), mem.push(Tensor::full(&[out_channel], 0.0, (Form::Float, Machine::Cpu)).requires_grad_(true)));
p.insert("stride".to_string(), mem.push(Tensor::from(stride as i64)));
Self {
params: p,
}
}
}

impl Compute for Conv2d {
fn ahead (&self, mem: &Reminiscence, enter: &Tensor) -> Tensor {
let kernel = mem.get(self.params.get(&"kernel".to_string()).unwrap());
let stride: i64 = mem.get(self.params.get(&"stride".to_string()).unwrap()).int64_value(&[]);
let bias = mem.get(self.params.get(&"bias".to_string()).unwrap());
enter.conv2d(&kernel, Some(bias), &[stride], 0, &[1], 1)
}
}

Wenn Sie sich an meinen Ansatz von erinnern Teil 1, die Struktur enthält ein Feld namens params. Das Feld params ist eine Sammlung vom Typ HashMap, wobei der Schlüssel vom Typ String ist, der einen Parameternamen speichert, und der Wert vom Typ ist usize, das den Speicherort des spezifischen Parameters (der ein PyTorch-Tensor ist) in unserem Speicher enthält, der wiederum als unser Speicher für alle unsere Modellparameter fungiert. Im Fall der Faltungsschicht fügen wir in unserer zugehörigen Funktion new zwei Parameter in unsere HashMap ein „Kernel“ Und „Voreingenommenheit“ die mit dem Flag „required_gradient“ gesetzt sind WAHR. Ich injiziere auch einen Parameter „Schreiten“dies ist jedoch nicht als trainierbarer Parameter festgelegt.

Anschließend implementieren wir das Compute-Merkmal für unsere Faltungsschicht. Dies erfordert die Definition der Funktion vorwärts, die während des Vorwärtsdurchlaufs des Trainingsprozesses aufgerufen wird. In dieser Funktion erhalten wir zunächst mithilfe der get-Methode einen Verweis auf die Kernel-, Bias- und Stride-Tensoren aus unserem Tensorspeicher und rufen dann unsere Conv2d-Funktion auf (wie wir es in unserem Spielzeugprogramm getan haben, allerdings sagt uns das Netzwerk in diesem Fall was). Kernel, der verwendet werden soll). Padding wurde auf einen Nullwert fest codiert. Wenn Sie möchten, kann dies jedoch auch problemlos als Parameter ähnlich wie Stride hinzugefügt werden.

Und das ist es! Das ist die einzige Ergänzung, die in unserem kleinen Framework erforderlich ist Teil 1 um CNNs wie in Itemizing 1–2 definieren und trainieren zu können.

In meinem vorherigen Artikel habe ich zwei Trainingsalgorithmen programmiert, Stochastic Gradient Descent und Stochastic Gradient Descent with Momentum. Allerdings ist Adam heute wahrscheinlich einer der beliebtesten Trainingsalgorithmen – warum additionally nicht, programmieren wir ihn auch in Rust!

Der Adam-Algorithmus wurde erstmals 2015 veröffentlicht (Verknüpfung) und kombiniert im Grunde die Idee der Trainingsalgorithmen Momentum und RMSprop. Der Algorithmus aus dem Originalpapier lautet wie folgt:

Adam-Algorithmus (Kingma et. al., 2015)

In Teil 1 Wir haben unseren Tensorspeicher implementiert, der auch die äquivalente Gradientenschrittfunktion von PyTorch unterstützt (Methoden apply_grads_sgd und apply_grads_sgd_momentum). Daher wird der Reminiscence-Strukturimplementierung eine neue Methode hinzugefügt, die eine Gradientenaktualisierung mithilfe von Adam durchführt:

Itemizing 6 – Unsere Implementierung von Adam.

fn apply_grads_adam(&mut self, learning_rate: f32) {
let mut g = Tensor::new();
const BETA:f32 = 0.9;

let mut velocity = Tensor::zeros(&[self.size as i64], (Form::Float, Machine::Cpu)).cut up(1, 0);
let mut mother = Tensor::zeros(&[self.size as i64], (Form::Float, Machine::Cpu)).cut up(1, 0);
let mut vel_corr = Tensor::zeros(&[self.size as i64], (Form::Float, Machine::Cpu)).cut up(1, 0);
let mut mom_corr = Tensor::zeros(&[self.size as i64], (Form::Float, Machine::Cpu)).cut up(1, 0);
let mut counter = 0;

self.values
.iter_mut()
.for_each(|t| {
if t.requires_grad() {
g = t.grad();
mother[counter] = BETA * &mother[counter] + (1.0 - BETA) * &g;
velocity[counter] = BETA * &velocity[counter] + (1.0 - BETA) * (&g.pow(&Tensor::from(2)));
mom_corr[counter] = &mother[counter] / (Tensor::from(1.0 - BETA).pow(&Tensor::from(2)));
vel_corr[counter] = &velocity[counter] / (Tensor::from(1.0 - BETA).pow(&Tensor::from(2)));

t.set_data(&(t.knowledge() - learning_rate * (&mom_corr[counter] / (&velocity[counter].sqrt() + 0.0000001))));
t.zero_grad();
}
counter += 1;
});

}

Ähnlich wie mein Ansatz in Teil 1Um den obigen Code mit einem Python-PyTorch-Äquivalent zu vergleichen, habe ich versucht, so genau wie möglich zu sein, um einen fairen Vergleich zu erhalten, und dabei hauptsächlich sichergestellt, dass ich dieselben Hyperparameter, Trainingsparameter und Trainingsalgorithmen des neuronalen Netzwerks anwende. Für meine Exams habe ich auch denselben Mnist-Datensatz verwendet. Ich habe die Exams auf demselben Laptop computer durchgeführt, einem Floor Professional 8, i7, mit 16 GB RAM, additionally ohne GPU.

Nach mehrmaliger Durchführung der Exams führte das Rust-Coaching im Durchschnitt zu einer Geschwindigkeitsverbesserung von 60 % gegenüber dem Python-Äquivalent. Obwohl es sich um eine deutliche Verbesserung handelte, conflict diese geringer als die, die im Fall von FFNs erreicht wurde (meine Erkenntnisse aus Teil 1). Ich habe diese geringere Verbesserung auf die Tatsache zurückgeführt, dass die Faltung die teuerste Berechnung in CNNs ist, und wie oben erläutert habe ich mich für die Verwendung der conv2d-Funktion von LibTorch C++ entschieden, die letztendlich dieselbe Funktion ist, die vom Python-Äquivalent aufgerufen wird. Dennoch darf die Reduzierung der Zeit für das Modelltraining um mehr als die Hälfte nicht außer Acht gelassen werden – dies würde in der Regel immer noch eine Zeitersparnis von Stunden, wenn nicht Tagen bedeuten!

Ich hoffe, Ihnen hat mein Artikel gefallen!

Ian Goodfellow, Yoshua Bengio und Aaron Courville, Tiefes LernenMIT Press, 2016. http://www.deeplearningbook.org

Pavel Karas und David Svoboda, Algorithmen zur effizienten Berechnung der Faltung, in Design und Architekturen für die digitale Signalverarbeitung, New York, NY, USA: IntechOpen, Januar 2013.

Lecun, Y., Bottou, L., Bengio, Y. & Haffner, P., Gradientenbasiertes Lernen zur Dokumentenerkennung, Proceedings of the IEEE 86, 2278–2324, 1998.

Diederik P. Kingma und Jimmy Ba, Adam: Eine Methode zur stochastischen Optimierungin Proceedings of the third Worldwide Convention on Studying Representations (ICLR), 2015.



Source link

HINTERLASSEN SIE EINE ANTWORT

Please enter your comment!
Please enter your name here