Kapitel 4. Die "Hello World" von TinyML: Aufbau und Training eines Modells
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
In Kapitel 3 haben wir die grundlegenden Konzepte des maschinellen Lernens und den allgemeinen Arbeitsablauf von maschinellen Lernprojekten kennengelernt. In diesem und im nächsten Kapitel werden wir unser Wissen in die Praxis umsetzen. Wir werden ein Modell von Grund auf erstellen und trainieren und es dann in ein einfaches Mikrocontroller-Programm integrieren.
Dabei wirst du dir die Hände schmutzig machen mit einigen leistungsstarken Entwickler-Tools, die täglich von führenden Experten für maschinelles Lernen eingesetzt werden. Außerdem lernst du, wie du ein maschinelles Lernmodell in ein C++-Programm integrierst und es auf einem Mikrocontroller einsetzt, um den Stromfluss in einem Schaltkreis zu steuern. Das könnte deine erste Erfahrung mit der Kombination von Hardware und maschinellem Lernen sein, und es wird dir Spaß machen!
Du kannst den Code, den wir in diesen Kapiteln schreiben, auf deinem Mac-, Linux- oder Windows-Rechner testen, aber für das volle Erlebnis brauchst du eines der Embedded-Geräte, die in "Welche Hardware brauchst du?" erwähnt werden :
Um unser Modell für maschinelles Lernen zu erstellen, verwenden wir Python, TensorFlow und Googles Colaboratory, ein cloudbasiertes interaktives Notizbuch zum Experimentieren mit Python-Code. Dies sind einige der wichtigsten Tools für Ingenieure, die sich mit maschinellem Lernen beschäftigen, und sie sind alle kostenlos zu benutzen.
Hinweis
Du fragst dich, wie der Titel dieses Kapitels lautet? In der Programmierung ist es Tradition, dass neue Technologien mit Beispielcode eingeführt werden, der zeigt, wie man etwas sehr Einfaches macht. Oft besteht die einfache Aufgabe darin, ein Programm dazu zu bringen, die Worte "Hallo, Welt" auszugeben . Es gibt keine eindeutige Entsprechung in ML, aber wir verwenden den Begriff "Hallo Welt", um ein einfaches, leicht zu lesendes Beispiel für eine durchgängige TinyML-Anwendung zu beschreiben.
Im Laufe dieses Kapitels werden wir Folgendes tun:
-
Besorge dir einen einfachen Datensatz.
-
Trainiere ein Deep Learning Modell.
-
Bewerte die Leistung des Modells.
-
Konvertiere das Modell für die Ausführung auf dem Gerät.
-
Schreibe Code, um Inferenzen auf dem Gerät durchzuführen.
-
Baue den Code in eine Binärdatei um.
-
Übertrage die Binärdatei auf einen Mikrocontroller.
Der gesamte Code, den wir verwenden werden, ist im GitHub-Repository von TensorFlow verfügbar.
Wir empfehlen, dass du jeden Teil dieses Kapitels durchgehst und dann versuchst, den Code auszuführen. Auf dem Weg dorthin gibt es Anweisungen, wie das geht. Aber bevor wir anfangen, lass uns besprechen, was wir genau bauen werden.
Was wir bauen
In Kapitel 3 haben wir besprochen, wie Deep Learning-Netzwerke lernen, Muster in ihren Trainingsdaten zu modellieren, damit sie Vorhersagen treffen können. Wir werden nun ein Netzwerk trainieren, um einige sehr einfache Daten zu modellieren. Du hast wahrscheinlich schon von der Sinusfunktion gehört. Sie wird in der Trigonometrie verwendet, um die Eigenschaften von rechtwinkligen Dreiecken zu beschreiben. Die Daten, mit denen wir trainieren werden, sind eine Sinuskurve, also das Diagramm, das sich ergibt, wenn man das Ergebnis der Sinusfunktion über der Zeit aufträgt (siehe Abbildung 4-1).
Unser Ziel ist es, ein Modell zu trainieren, das aus einem Wert, x
, den Sinus, y
, vorhersagen kann. Wenn du in einer realen Anwendung den Sinus von x
benötigst, könntest du ihn einfach direkt berechnen. Indem wir jedoch ein Modell trainieren, das das Ergebnis annähert, können wir die Grundlagen des maschinellen Lernens demonstrieren.
Der zweite Teil unseres Projekts wird darin bestehen, dieses Modell auf einem Hardware-Gerät laufen zu lassen. Optisch ist die Sinuskurve eine angenehme Kurve, die gleichmäßig von -1 bis 1 und zurück verläuft. Das macht sie perfekt für die Steuerung einer visuell ansprechenden Lichtshow! Wir werden das Ausgangssignal unseres Modells verwenden, um entweder blinkende LEDs oder eine grafische Animation zeitlich zu steuern, je nachdem, was das Gerät kann.
Im Internet kannst du eine GIF-Animation dieses Codes sehen, in der die LEDs eines SparkFun Edge blinken. Abbildung 4-2 ist ein Standbild aus dieser Animation, in der einige LEDs des Geräts aufleuchten. Das ist vielleicht keine besonders nützliche Anwendung des maschinellen Lernens, aber im Sinne eines "Hallo Welt"-Beispiels ist es einfach, macht Spaß und hilft dabei, die grundlegenden Prinzipien zu demonstrieren, die du kennen musst.
Nachdem wir unseren Grundcode zum Laufen gebracht haben, werden wir ihn auf drei verschiedenen Geräten einsetzen: dem SparkFun Edge, einem Arduino Nano 33 BLE Sense und einem ST Microelectronics STM32F746G Discovery Kit.
Hinweis
Da TensorFlow ein aktiv entwickeltes Open-Source-Projekt ist, das ständig weiterentwickelt wird, wirst du vielleicht leichte Unterschiede zwischen dem hier abgedruckten Code und dem online gehosteten Code feststellen. Keine Sorge - auch wenn sich ein paar Codezeilen ändern, bleiben die Grundprinzipien dieselben.
Unsere Machine Learning Toolchain
Für das maschinelle Lernen in diesem Projekt verwenden wir dieselben Tools, die auch in der Praxis für maschinelles Lernen eingesetzt werden. In diesem Abschnitt stellen wir sie dir vor.
Python und Jupyter Notebooks
Python ist die bevorzugte Programmiersprache von Wissenschaftlern und Ingenieuren, die sich mit maschinellem Lernen beschäftigen. Sie ist leicht zu erlernen, eignet sich für viele verschiedene Anwendungen und verfügt über eine Vielzahl von Bibliotheken für nützliche Aufgaben rund um Daten und Mathematik. Die überwiegende Mehrheit der Deep-Learning-Forschung wird mit Python durchgeführt, und die Forscher/innen veröffentlichen oft den Python-Quellcode für die von ihnen erstellten Modelle.
Python ist besonders gut in Kombination mit Jupyter Notebooks. Dabei handelt es sich um ein spezielles Dokumentformat, das es dir ermöglicht, Texte, Grafiken und Code zu mischen, die auf Knopfdruck ausgeführt werden können. Jupyter Notebooks werden häufig verwendet, um Code und Probleme des maschinellen Lernens zu beschreiben, zu erklären und zu erforschen.
Wir werden unser Modell in einem Jupyter-Notizbuch erstellen, mit dem wir unsere Daten während der Entwicklung wunderbar visualisieren können. Dazu gehört auch die Anzeige von Diagrammen, die die Genauigkeit und Konvergenz unseres Modells zeigen.
Wenn du etwas Programmiererfahrung hast, ist Python leicht zu lesen und zu lernen. Du solltest in der Lage sein, diesem Tutorial ohne Probleme zu folgen.
Google Kolaboratorium
Um unser Notizbuch auszuführen, verwenden wir ein Tool namens Colaboratory, kurz Colab. Colab wird von Google entwickelt und bietet eine Online-Umgebung für die Ausführung von Jupyter-Notizbüchern. Es wird kostenlos zur Verfügung gestellt, um Forschung und Entwicklung im Bereich maschinelles Lernen zu fördern.
Früher musstest du ein Notebook auf deinem eigenen Computer erstellen. Dazu mussten viele Abhängigkeiten, wie z. B. Python-Bibliotheken, installiert werden, was Kopfschmerzen bereiten kann. Außerdem war es schwierig, das erstellte Notebook mit anderen Personen zu teilen, da diese möglicherweise unterschiedliche Versionen der Abhängigkeiten hatten, sodass das Notebook nicht wie erwartet lief. Außerdem kann maschinelles Lernen sehr rechenintensiv sein, sodass das Trainieren von Modellen auf deinem Entwicklungscomputer langsam sein kann.
Mit Colab kannst du Notebooks auf der leistungsstarken Hardware von Google ausführen, und das zum Nulltarif. Du kannst deine Notebooks von jedem Webbrowser aus bearbeiten und ansehen und sie mit anderen Personen teilen, die garantiert die gleichen Ergebnisse erhalten, wenn sie sie ausführen. Du kannst Colab sogar so konfigurieren, dass dein Code auf speziell beschleunigter Hardware ausgeführt wird, die das Training schneller durchführen kann als ein normaler Computer.
TensorFlow und Keras
TensorFlow ist eine Reihe von Werkzeugen für das Erstellen, Trainieren, Auswerten und Einsetzen von Machine-Learning-Modellen. Ursprünglich bei Google entwickelt, ist TensorFlow heute ein Open-Source-Projekt, das von Tausenden von Mitwirkenden auf der ganzen Welt entwickelt und gepflegt wird. Es ist das beliebteste und am weitesten verbreitete Framework für maschinelles Lernen. Die meisten Entwickler interagieren mit TensorFlow über die Python-Bibliothek.
TensorFlow macht viele verschiedene Dinge. In diesem Kapitel werden wir Keras verwenden, die High-Level-API von TensorFlow, mit der sich Deep-Learning-Netzwerke einfach erstellen und trainieren lassen. Außerdem werden wir TensorFlow Lite verwenden, eine Reihe von Tools, mit denen wir TensorFlow-Modelle auf mobilen und eingebetteten Geräten einsetzen können, um unser Modell auf dem Gerät auszuführen.
In Kapitel 13 wird TensorFlow noch viel ausführlicher behandelt. Für den Moment solltest du einfach wissen, dass es ein extrem leistungsfähiges und branchenübliches Tool ist, das dir auf deinem Weg vom Anfänger zum Deep Learning-Experten weiterhelfen wird.
Unser Modell bauen
Jetzt gehen wir durch den Prozess des Aufbaus, Trainings und der Umwandlung unseres Modells. Der gesamte Code ist in diesem Kapitel enthalten, aber du kannst ihn auch in Colab nachverfolgen und ausführen.
Lade zunächst das Notizbuch. Nachdem die Seite geladen wurde, klicke oben auf die Schaltfläche "In Google Colab ausführen", wie in Abbildung 4-3 gezeigt. Dadurch wird das Notizbuch von GitHub nach Colab kopiert, so dass du es ausführen und bearbeiten kannst.
Standardmäßig enthält das Notizbuch neben dem Code auch ein Beispiel für die Ausgabe, die du erwarten solltest, wenn der Code ausgeführt wird. Da wir den Code in diesem Kapitel durchgehen werden, sollten wir diese Ausgabe löschen, damit das Notizbuch in einem tadellosen Zustand ist. Dazu klickst du im Menü von Colab auf Bearbeiten und wählst dann "Alle Ausgaben löschen", wie in Abbildung 4-4 gezeigt.
Gute Arbeit. Unser Notizbuch ist jetzt einsatzbereit!
Tipp
Wenn du dich bereits mit maschinellem Lernen, TensorFlow und Keras auskennst, kannst du den Teil überspringen, in dem wir unser Modell für die Verwendung mit TensorFlow Lite konvertieren. Springe im Buch zu "Konvertieren des Modells für TensorFlow Lite". In Colab scrollst du nach unten zur Überschrift "Zu TensorFlow Lite konvertieren".
Abhängigkeiten importieren
Unsere erste Aufgabe besteht darin, die benötigten Abhängigkeiten zu importieren. In Jupyter-Notizbüchern werden Code und Text in Zellen angeordnet. Es gibt Codezellen, die ausführbaren Python-Code enthalten, und Textzellen, die formatierten Text enthalten.
Unsere erste Codezelle befindet sich unter "Abhängigkeiten importieren". Sie richtet alle Bibliotheken ein, die wir zum Trainieren und Konvertieren unseres Modells brauchen. Hier ist der Code:
# TensorFlow is an open source machine learning library
!
pip
install
tensorflow
==
2.0
import
tensorflow
as
tf
# NumPy is a math library
import
numpy
as
np
# Matplotlib is a graphing library
import
matplotlib.pyplot
as
plt
# math is Python's math library
import
math
In Python lädt die Anweisung import
eine Bibliothek, damit sie in unserem Code verwendet werden kann. Aus dem Code und den Kommentaren kannst du ersehen, dass diese Zelle Folgendes tut:
-
Installiert die TensorFlow 2.0 Bibliothek mit
pip
, einem Paketmanager für Python -
Importiert TensorFlow, NumPy, Matplotlib und die
math
Bibliothek von Python
Wenn wir eine Bibliothek importieren, können wir ihr einen Alias geben, damit wir später leicht auf sie verweisen können. Im vorangegangenen Code verwenden wir zum Beispiel import numpy as np
, um NumPy zu importieren und geben ihr den Alias np
. Wenn wir sie in unserem Code verwenden, können wir auf sie als np
verweisen.
Der Code in den Codezellen kann ausgeführt werden, indem du auf die Schaltfläche klickst, die oben links erscheint, wenn die Zelle ausgewählt ist. Im Abschnitt "Abhängigkeiten importieren" klickst du irgendwo in die erste Codezelle, damit sie ausgewählt wird. Abbildung 4-5 zeigt, wie eine ausgewählte Zelle aussieht.
Um den Code auszuführen, klicke auf die Schaltfläche, die oben links erscheint. Während der Code ausgeführt wird, wird die Schaltfläche mit einem Kreis animiert, wie in Abbildung 4-6 dargestellt.
Die Abhängigkeiten werden nun installiert und du wirst einige Ausgaben sehen, die erscheinen. Schließlich solltest du die folgende Zeile sehen, was bedeutet, dass die Bibliothek erfolgreich installiert wurde:
Successfully installed tensorboard-2.0.0 tensorflow-2.0.0 tensorflow-estimator-2.0.0
Nachdem eine Zelle in Colab ausgeführt wurde, siehst du, dass in der oberen linken Ecke eine 1
angezeigt wird, wenn sie nicht mehr ausgewählt ist, wie in Abbildung 4-7 dargestellt. Diese Zahl ist ein Zähler, der jedes Mal erhöht wird, wenn die Zelle ausgeführt wird.
So kannst du herausfinden, welche Zellen wie oft ausgeführt wurden.
Daten generieren
Deep Learning-Netzwerke lernen, Muster in den zugrunde liegenden Daten zu modellieren. Wie bereits erwähnt, werden wir ein Netzwerk darauf trainieren, Daten zu modellieren, die durch eine Sinusfunktion erzeugt werden. Das Ergebnis ist ein Modell, das einen Wert, x
, und seinen Sinus, y
, vorhersagen kann.
Bevor wir weitermachen, brauchen wir einige Daten. In einer realen Situation könnten wir Daten von Sensoren und Produktionsprotokollen sammeln. In diesem Beispiel verwenden wir jedoch einen einfachen Code, um einen Datensatz zu erstellen.
Die nächste Zelle ist der Ort, an dem dies geschehen wird. Unser Plan ist es, 1.000 Werte zu erzeugen, die zufällige Punkte entlang einer Sinuswelle darstellen. Schauen wir uns Abbildung 4-8 an, um uns daran zu erinnern, wie eine Sinuswelle aussieht.
Jeder volle Zyklus einer Welle wird als ihre Periode bezeichnet. Aus dem Diagramm geht hervor, dass ein voller Zyklus etwa alle sechs Einheiten auf der x
-Achse vollendet wird. Die Periode einer Sinuswelle ist also 2 × π, also 2π.
Damit wir eine ganze Sinuswelle an Daten zum Trainieren haben, erzeugt unser Code zufällige x
Werte von 0 bis 2π. Dann berechnet er den Sinus für jeden dieser Werte.
Hier ist der vollständige Code für diese Zelle, die NumPy (np
, das wir zuvor importiert haben) verwendet, um Zufallszahlen zu erzeugen und ihren Sinus zu berechnen:
# We'll generate this many sample datapoints
SAMPLES
=
1000
# Set a "seed" value, so we get the same random numbers each time we run this
# notebook. Any number can be used here.
SEED
=
1337
np
.
random
.
seed
(
SEED
)
tf
.
random
.
set_seed
(
SEED
)
# Generate a uniformly distributed set of random numbers in the range from
# 0 to 2π, which covers a complete sine wave oscillation
x_values
=
np
.
random
.
uniform
(
low
=
0
,
high
=
2
*
math
.
pi
,
size
=
SAMPLES
)
# Shuffle the values to guarantee they're not in order
np
.
random
.
shuffle
(
x_values
)
# Calculate the corresponding sine values
y_values
=
np
.
sin
(
x_values
)
# Plot our data. The 'b.' argument tells the library to print blue dots.
plt
.
plot
(
x_values
,
y_values
,
'b.'
)
plt
.
show
()
Zusätzlich zu dem, was wir bereits besprochen haben, gibt es noch ein paar Dinge, die in diesem Code hervorzuheben sind. Zuerst siehst du, dass wir np.random.uniform()
verwenden, um unsere x
Werte zu erzeugen. Diese Methode gibt ein Array mit Zufallszahlen in dem angegebenen Bereich zurück. NumPy enthält viele nützliche Methoden, die mit ganzen Arrays von Werten arbeiten, was beim Umgang mit Daten sehr praktisch ist.
Zweitens mischen wir die Daten, nachdem wir sie erzeugt haben. Das ist wichtig, weil der Trainingsprozess beim Deep Learning davon abhängt, dass die Daten in einer wirklich zufälligen Reihenfolge eingespeist werden. Wären die Daten geordnet, wäre das resultierende Modell weniger genau.
Als Nächstes verwenden wir die Methode sin()
von NumPy, um unsere Sinuswerte zu berechnen. NumPy kann dies für alle unsere x
Werte auf einmal tun und ein Array zurückgeben. NumPy ist großartig!
Zum Schluss siehst du einen mysteriösen Code, der plt
aufruft, unseren Alias für Matplotlib:
# Plot our data. The 'b.' argument tells the library to print blue dots.
plt
.
plot
(
x_values
,
y_values
,
'b.'
)
plt
.
show
()
Was macht dieser Code? Er zeichnet ein Diagramm unserer Daten auf. Eine der besten Eigenschaften von Jupyter-Notebooks ist die Möglichkeit, Grafiken anzuzeigen, die von dem Code, den du ausführst, ausgegeben werden. Matplotlib ist ein hervorragendes Werkzeug, um aus Daten Grafiken zu erstellen. Da die Visualisierung von Daten ein wichtiger Teil des Arbeitsablaufs beim maschinellen Lernen ist, wird dies unglaublich hilfreich sein, wenn wir unser Modell trainieren.
Um die Daten zu generieren und sie als Diagramm darzustellen, führe den Code in der Zelle aus. Nachdem die Zelle ausgeführt wurde, solltest du ein schönes Diagramm darunter sehen, wie in Abbildung 4-9 dargestellt.
Das sind unsere Daten! Es handelt sich um eine Auswahl zufälliger Punkte entlang einer schönen, glatten Sinuskurve. Wir könnten sie zum Trainieren unseres Modells verwenden. Aber das wäre zu einfach. Das Spannende an Deep-Learning-Netzwerken ist ihre Fähigkeit, Muster aus dem Rauschen herauszufiltern. Dadurch können sie selbst dann Vorhersagen treffen, wenn sie auf unordentlichen Daten aus der realen Welt trainiert werden. Um das zu verdeutlichen, fügen wir unseren Datenpunkten etwas Rauschen hinzu und zeichnen ein weiteres Diagramm:
# Add a small random number to each y value
y_values
+=
0.1
*
np
.
random
.
randn
(
*
y_values
.
shape
)
# Plot our data
plt
.
plot
(
x_values
,
y_values
,
'b.'
)
plt
.
show
()
Führe diese Zelle aus und sieh dir die Ergebnisse an, wie in Abbildung 4-10 gezeigt.
Viel besser! Unsere Punkte sind jetzt zufällig verteilt, so dass sie eine Verteilung um eine Sinuskurve statt einer glatten, perfekten Kurve darstellen. Das spiegelt die reale Situation viel besser wider, in der die Daten in der Regel ziemlich chaotisch sind.
Aufteilung der Daten
Aus dem vorherigen Kapitel erinnerst du dich vielleicht daran, dass ein Datensatz oft in drei Teile aufgeteilt wird: Training, Validierung und Test. Um die Genauigkeit des von uns trainierten Modells zu bewerten, müssen wir seine Vorhersagen mit echten Daten vergleichen und prüfen, wie gut sie übereinstimmen.
Diese Bewertung findet während des Trainings (hier wird sie als Validierung bezeichnet) und nach dem Training (als Test) statt. In jedem Fall ist es wichtig, dass wir neue Daten verwenden, die nicht bereits für das Training des Modells verwendet wurden.
Um sicherzustellen, dass wir Daten für die Auswertung haben, legen wir einige beiseite, bevor wir mit dem Training beginnen. Wir reservieren 20% unserer Daten für die Validierung und weitere 20% für das Testen. Die restlichen 60 % verwenden wir zum Trainieren des Modells. Das ist eine typische Aufteilung für das Training von Modellen.
Der folgende Code teilt unsere Daten auf und stellt dann jedes Set in einer anderen Farbe dar:
# We'll use 60% of our data for training and 20% for testing. The remaining 20%
# will be used for validation. Calculate the indices of each section.
TRAIN_SPLIT
=
int
(
0.6
*
SAMPLES
)
TEST_SPLIT
=
int
(
0.2
*
SAMPLES
+
TRAIN_SPLIT
)
# Use np.split to chop our data into three parts.
# The second argument to np.split is an array of indices where the data will be
# split. We provide two indices, so the data will be divided into three chunks.
x_train
,
x_validate
,
x_test
=
np
.
split
(
x_values
,
[
TRAIN_SPLIT
,
TEST_SPLIT
])
y_train
,
y_validate
,
y_test
=
np
.
split
(
y_values
,
[
TRAIN_SPLIT
,
TEST_SPLIT
])
# Double check that our splits add up correctly
assert
(
x_train
.
size
+
x_validate
.
size
+
x_test
.
size
)
==
SAMPLES
# Plot the data in each partition in different colors:
plt
.
plot
(
x_train
,
y_train
,
'b.'
,
label
=
"Train"
)
plt
.
plot
(
x_validate
,
y_validate
,
'y.'
,
label
=
"Validate"
)
plt
.
plot
(
x_test
,
y_test
,
'r.'
,
label
=
"Test"
)
plt
.
legend
()
plt
.
show
()
Um unsere Daten aufzuteilen, verwenden wir eine weitere praktische NumPy-Methode: split()
. Diese Methode nimmt ein Array mit Daten und ein Array mit Indizes und zerlegt die Daten dann in Teile an den angegebenen Indizes .
Führe diese Zelle aus, um die Ergebnisse unserer Aufteilung zu sehen. Jede Art von Daten wird durch eine andere Farbe (oder einen anderen Farbton, wenn du die gedruckte Version dieses Buches liest) dargestellt, wie in Abbildung 4-11 gezeigt.
Ein Basismodell definieren
Jetzt, wo wir unsere Daten haben, ist es an der Zeit, das Modell zu erstellen, das wir darauf trainieren wollen.
Wir werden ein Modell erstellen, das einen Eingabewert (in diesem Fall x
) verwendet, um einen numerischen Ausgabewert (den Sinus von x
) vorherzusagen. Diese Art von Problem nennt man eine Regression. Wir können Regressionsmodelle für alle Arten von Aufgaben verwenden, die eine numerische Ausgabe erfordern. Ein Regressionsmodell könnte zum Beispiel versuchen, die Laufgeschwindigkeit einer Person in Meilen pro Stunde anhand der Daten eines Beschleunigungsmessers vorherzusagen.
Um unser Modell zu erstellen, werden wir ein einfaches neuronales Netzwerk entwerfen. Es verwendet Schichten von Neuronen, um zu versuchen, die den Trainingsdaten zugrunde liegenden Muster zu lernen, damit es Vorhersagen treffen kann.
Der Code dafür ist eigentlich ganz einfach. Er verwendet Keras, die High-Level-API von TensorFlow zur Erstellung von Deep Learning-Netzwerken:
# We'll use Keras to create a simple model architecture
from
tf.keras
import
layers
model_1
=
tf
.
keras
.
Sequential
()
# First layer takes a scalar input and feeds it through 16 "neurons." The
# neurons decide whether to activate based on the 'relu' activation function.
model_1
.
add
(
layers
.
Dense
(
16
,
activation
=
'relu'
,
input_shape
=
(
1
,)))
# Final layer is a single neuron, since we want to output a single value
model_1
.
add
(
layers
.
Dense
(
1
))
# Compile the model using a standard optimizer and loss function for regression
model_1
.
compile
(
optimizer
=
'rmsprop'
,
loss
=
'mse'
,
metrics
=
[
'mae'
])
# Print a summary of the model's architecture
model_1
.
summary
()
Zunächst erstellen wir mit Keras ein Sequential
Modell, d.h. ein Modell, bei dem jede Schicht von Neuronen über die nächste gestapelt wird, wie wir in Abbildung 3-1 gesehen haben. Dann definieren wir zwei Schichten. Hier wird die erste Schicht definiert:
model_1
.
add
(
layers
.
Dense
(
16
,
activation
=
'relu'
,
input_shape
=
(
1
,)))
Die erste Schicht hat eine einzige Eingabe - unseren x
Wert - und 16 Neuronen. Es handelt sich um eine Dense
Schicht (auch bekannt als voll verknüpfte Schicht), d.h. die Eingabe wird während der Inferenz, wenn wir Vorhersagen machen, in jedes einzelne Neuron eingespeist. Jedes Neuron wird dann bis zu einem bestimmten Grad aktiviert. Der Aktivierungsgrad jedes Neurons basiert auf seinen Gewichtungs- und Verzerrungswerten, die während des Trainings gelernt wurden, sowie auf seiner Aktivierungsfunktion. Die Aktivierung des Neurons wird als Zahl ausgegeben.
Die Aktivierung wird mit einer einfachen Formel berechnet, die in Python gezeigt wird. Wir werden das nie selbst programmieren müssen, da es von Keras und TensorFlow erledigt wird, aber es wird hilfreich sein, wenn wir uns weiter mit Deep Learning beschäftigen:
activation
=
activation_function
((
input
*
weight
)
+
bias
)
Um die Aktivierung des Neurons zu berechnen, wird seine Eingabe mit dem Gewicht multipliziert und die Vorspannung zu dem Ergebnis addiert. Der berechnete Wert wird in die Aktivierungsfunktion eingegeben. Die daraus resultierende Zahl ist die Aktivierung des Neurons.
Die Aktivierungsfunktion ist eine mathematische Funktion, mit der die Ausgabe des Neurons geformt wird. In unserem Netzwerk verwenden wir eine Aktivierungsfunktion namens rectified linear unit, kurz ReLU. Diese wird in Keras durch das Argument activation=relu
angegeben.
ReLU ist eine einfache Funktion, die hier in Python gezeigt wird:
def
relu
(
input
):
return
max
(
0.0
,
input
)
ReLU gibt den jeweils größeren Wert zurück: den Eingabewert oder Null. Wenn der Eingabewert negativ ist, gibt ReLU Null zurück. Liegt der Eingabewert über Null, gibt ReLU ihn unverändert zurück.
Abbildung 4-12 zeigt die Ausgabe von ReLU für eine Reihe von Eingabewerten.
Ohne eine Aktivierungsfunktion wäre die Ausgabe des Neurons immer eine lineare Funktion der Eingabe. Das würde bedeuten, dass das Netzwerk nur lineare Beziehungen modellieren könnte, bei denen das Verhältnis zwischen x
und y
über den gesamten Wertebereich hinweg gleich bleibt. Das würde verhindern, dass ein Netzwerk unsere Sinuswelle modellieren kann, denn eine Sinuswelle ist nicht linear.
Da ReLU nichtlinear ist, können mehrere Schichten von Neuronen ihre Kräfte bündeln und komplexe nichtlineare Beziehungen modellieren, bei denen der Wert von y
nicht bei jeder Erhöhung von x
um denselben Betrag steigt.
Hinweis
Es gibt noch andere Aktivierungsfunktionen, aber ReLU ist die am häufigsten verwendete. Im Wikipedia-Artikel über Aktivierungsfunktionen findest du einige der anderen Optionen. Jede Aktivierungsfunktion bringt unterschiedliche Kompromisse mit sich, und Ingenieure für maschinelles Lernen experimentieren, um herauszufinden, welche Optionen für eine bestimmte Architektur am besten geeignet sind.
Die Aktivierungszahlen aus unserer ersten Schicht werden als Eingaben in unsere zweite Schicht eingespeist, die in der folgenden Zeile definiert wird:
model_1
.
add
(
layers
.
Dense
(
1
))
Da es sich bei dieser Schicht um ein einzelnes Neuron handelt, erhält es 16 Eingaben, eine für jedes Neuron der vorherigen Schicht. Seine Aufgabe ist es, alle Aktivierungen der vorherigen Schicht zu einem einzigen Ausgangswert zu kombinieren. Da es sich um unsere Ausgabeschicht handelt, geben wir keine Aktivierungsfunktion an - wir wollen nur das Ergebnis.
Da dieses Neuron mehrere Eingänge hat, hat es für jeden einen entsprechenden Gewichtswert. Die Ausgabe des Neurons wird mit der folgenden Formel berechnet, die in Python dargestellt ist:
# Here, `inputs` and `weights` are both NumPy arrays with 16 elements each
output
=
sum
((
inputs
*
weights
))
+
bias
Den Ausgangswert erhält man, indem man jeden Eingang mit dem entsprechenden Gewicht multipliziert, die Ergebnisse addiert und dann den Bias des Neurons hinzufügt.
Die Gewichte und Verzerrungen des Netzes werden beim Training gelernt. Der Schritt compile()
im Code, der weiter oben in diesem Kapitel gezeigt wurde, konfiguriert einige wichtige Argumente, die im Trainingsprozess verwendet werden, und bereitet das Modell für das Training vor:
model_1
.
compile
(
optimizer
=
'rmsprop'
,
loss
=
'mse'
,
metrics
=
[
'mae'
])
Das Argument optimizer
gibt den Algorithmus an, mit dem das Netz während des Trainings seine Eingaben modelliert. Es gibt mehrere Möglichkeiten, und um den besten Algorithmus zu finden, muss man oft experimentieren. Du kannst die Optionen in der Keras-Dokumentation nachlesen.
Das Argument loss
gibt die Methode an, mit der während des Trainings berechnet wird, wie weit die Vorhersagen des Netzes von der Realität entfernt sind. Diese Methode wird als Verlustfunktion bezeichnet. Hier verwenden wir mse
, den mittleren quadratischen Fehler. Diese Verlustfunktion wird bei Regressionsproblemen verwendet, bei denen wir versuchen, eine Zahl vorherzusagen. Es gibt verschiedene Verlustfunktionen in Keras. Einige der Optionen sind in den Keras-Dokumenten aufgeführt.
Mit dem Argument metrics
können wir einige zusätzliche Funktionen angeben, die zur Beurteilung der Leistung unseres Modells verwendet werden. Wir geben mae
an, den mittleren absoluten Fehler, eine hilfreiche Funktion zur Messung der Leistung eines Regressionsmodells. Diese Kennzahl wird während des Trainings gemessen, und die Ergebnisse stehen uns nach dem Training zur Verfügung.
Nachdem wir unser Modell kompiliert haben, können wir die folgende Zeile verwenden, um einige zusammenfassende Informationen über seine Architektur auszugeben:
# Print a summary of the model's architecture
model_1
.
summary
()
Führe die Zelle in Colab aus, um das Modell zu definieren. Du bekommst die folgende Ausgabe gedruckt:
Model: "sequential" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense (Dense) (None, 16) 32 _________________________________________________________________ dense_1 (Dense) (None, 1) 17 ================================================================= Total params: 49 Trainable params: 49 Non-trainable params: 0 _________________________________________________________________
Diese Tabelle zeigt die Schichten des Netzes, ihre Ausgangsformen und die Anzahl ihrer Parameter. Die Größe eines Netzes - wie viel Speicherplatz es benötigt - hängt vor allem von der Anzahl der Parameter ab, also der Gesamtzahl der Gewichte und Verzerrungen. Dies kann eine nützliche Kennzahl sein, wenn es um die Größe und Komplexität eines Modells geht.
Bei einfachen Modellen wie dem unseren kann die Anzahl der Gewichte bestimmt werden, indem man die Anzahl der Verbindungen zwischen den Neuronen im Modell berechnet, vorausgesetzt, jede Verbindung hat ein Gewicht.
Das Netzwerk, das wir gerade entworfen haben, besteht aus zwei Schichten. Unsere erste Schicht hat 16 Verbindungen - eine zwischen dem Eingang und jedem Neuron. Unsere zweite Schicht hat ein einzelnes Neuron, das ebenfalls 16 Verbindungen hat - eine zu jedem Neuron der ersten Schicht. Damit beträgt die Gesamtzahl der Verbindungen 32.
Da jedes Neuron eine Vorspannung hat, hat das Netz 17 Vorspannungen, d.h. es hat insgesamt 32 + 17 = 49 Parameter.
Wir sind jetzt durch den Code gegangen, der unser Modell definiert. Als Nächstes beginnen wir mit dem Trainingsprozess.
Unser Modell trainieren
Nachdem wir unser Modell definiert haben, ist es an der Zeit, es zu trainieren und dann seine Leistung zu bewerten, um zu sehen, wie gut es funktioniert. Wenn wir die Ergebnisse sehen, können wir entscheiden, ob es gut genug ist oder ob wir unser Design ändern und es erneut trainieren sollten.
Um ein Modell in Keras zu trainieren, rufen wir einfach die Methode fit()
auf und übergeben alle unsere Daten und einige andere wichtige Argumente. Der Code in der nächsten Zelle zeigt, wie das geht:
history_1
=
model_1
.
fit
(
x_train
,
y_train
,
epochs
=
1000
,
batch_size
=
16
,
validation_data
=
(
x_validate
,
y_validate
))
Führe den Code in der Zelle aus, um mit dem Training zu beginnen. Du wirst sehen, dass einige Logs erscheinen:
Train on 600 samples, validate on 200 samples Epoch 1/1000 600/600 [==============================] - 1s 1ms/sample - loss: 0.7887 - mae: 0.7848 - val_loss: 0.5824 - val_mae: 0.6867 Epoch 2/1000 600/600 [==============================] - 0s 155us/sample - loss: 0.4883 - mae: 0.6194 - val_loss: 0.4742 - val_mae: 0.6056
Unser Modell wird jetzt trainiert. Das wird eine Weile dauern, also lass uns in der Zwischenzeit die Details unseres Aufrufs an fit()
durchgehen:
history_1
=
model_1
.
fit
(
x_train
,
y_train
,
epochs
=
1000
,
batch_size
=
16
,
validation_data
=
(
x_validate
,
y_validate
))
Als Erstes wirst du feststellen, dass wir den Rückgabewert unseres fit()
-Aufrufs einer Variablen namens history_1
zuweisen. Diese Variable enthält eine Menge Informationen über unseren Trainingslauf und wir werden sie später verwenden, um zu untersuchen, wie es gelaufen ist.
Als Nächstes werfen wir einen Blick auf die Argumente der Funktion fit()
:
x_train
,y_train
-
Die ersten beiden Argumente von
fit()
sind die Wertex
undy
unserer Trainingsdaten. Erinnere dich daran, dass wir einen Teil unserer Daten für die Validierung und die Tests beiseite legen, sodass nur die Trainingsdaten zum Trainieren des Netzwerks verwendet werden. epochs
-
Das nächste Argument gibt an, wie oft unser gesamtes Trainingsset während des Trainings durch das Netzwerk laufen soll. Je mehr Epochen, desto mehr Training findet statt. Man könnte meinen, dass das Netz umso besser wird, je öfter es trainiert wird. Manche Netze fangen jedoch nach einer bestimmten Anzahl von Epochen an, sich zu sehr an die Trainingsdaten anzupassen, so dass wir die Anzahl der Trainingsdurchläufe begrenzen sollten.
Auch wenn es keine Überanpassung gibt, wird sich ein Netzwerk nach einer gewissen Zeit des Trainings nicht mehr verbessern. Da das Training Zeit und Rechenressourcen kostet, ist es am besten, nicht zu trainieren, wenn das Netz nicht besser wird!
Wir beginnen mit 1.000 Trainingsepochen. Wenn das Training abgeschlossen ist, können wir in unseren Metriken nachsehen, ob das die richtige Zahl ist.
batch_size
-
Das Argument
batch_size
gibt an, wie viele Trainingsdaten in das Netz eingespeist werden sollen, bevor die Genauigkeit gemessen und die Gewichte und Verzerrungen aktualisiert werden. Wenn wir wollten, könnten wirbatch_size
von1
angeben, was bedeutet, dass wir die Inferenz mit einem einzigen Datenpunkt durchführen, den Verlust der Vorhersage des Netzwerks messen, die Gewichte und Vorspannungen aktualisieren, um die Vorhersage beim nächsten Mal genauer zu machen, und diesen Zyklus dann für den Rest der Daten fortsetzen.Da wir 600 Datenpunkte haben, würde jede Epoche zu 600 Aktualisierungen des Netzwerks führen. Das ist eine Menge Rechenarbeit, sodass unser Training eine Ewigkeit dauern würde! Eine Alternative wäre es, mehrere Datenpunkte auszuwählen und zu inferenzieren, den Gesamtverlust zu messen und das Netz entsprechend zu aktualisieren.
Wenn wir
batch_size
auf600
setzen, würde jeder Stapel alle unsere Trainingsdaten enthalten. Wir müssten jetzt nur noch eine Aktualisierung des Netzwerks pro Epoche vornehmen - das geht viel schneller. Das Problem ist, dass dies zu weniger genauen Modellen führt. Untersuchungen haben gezeigt, dass Modelle, die mit großen Stapelgrößen trainiert werden, weniger gut auf neue Daten verallgemeinert werden können - sie neigen eher zu einer Überanpassung.Der Kompromiss besteht darin, eine Losgröße zu verwenden, die irgendwo in der Mitte liegt. In unserem Trainingscode verwenden wir eine Stapelgröße von 16. Das bedeutet, dass wir 16 Datenpunkte nach dem Zufallsprinzip auswählen, die Inferenz mit ihnen durchführen, den Gesamtverlust berechnen und das Netz einmal pro Stapel aktualisieren. Wenn wir 600 Trainingsdaten haben, wird das Netz etwa 38 Mal pro Epoche aktualisiert, was viel besser ist als 600.
Bei der Wahl der Stapelgröße gehen wir einen Kompromiss zwischen Trainingseffizienz und Modellgenauigkeit ein. Die ideale Stapelgröße variiert von Modell zu Modell. Es ist eine gute Idee, mit einer Stapelgröße von 16 oder 32 zu beginnen und zu testen, was am besten funktioniert.
validation_data
-
Hier legen wir unseren Validierungsdatensatz fest. Die Daten aus diesem Datensatz werden während des gesamten Trainingsprozesses durch das Netzwerk geleitet, und die Vorhersagen des Netzwerks werden mit den erwarteten Werten verglichen. Die Ergebnisse der Validierung werden in den Protokollen und als Teil des
history_1
Objekts angezeigt.
Ausbildungsmetriken
Hoffentlich ist das Training jetzt beendet. Wenn nicht, warte ein paar Augenblicke, bis es beendet ist.
Wir werden nun verschiedene Metriken überprüfen, um zu sehen, wie gut unser Netzwerk gelernt hat. Als erstes schauen wir uns die Logs an, die während des Trainings geschrieben wurden. Daraus geht hervor, wie sich das Netzwerk während des Trainings von seinem zufälligen Ausgangszustand aus verbessert hat.
Hier sind die Logs für unsere erste und letzte Epoche:
Epoch 1/1000 600/600 [==============================] - 1s 1ms/sample - loss: 0.7887 - mae: 0.7848 - val_loss: 0.5824 - val_mae: 0.6867
Epoch 1000/1000 600/600 [==============================] - 0s 124us/sample - loss: 0.1524 - mae: 0.3039 - val_loss: 0.1737 - val_mae: 0.3249
Die loss
, mae
, val_loss
und val_mae
sagen uns verschiedene Dinge:
loss
-
Dies ist die Ausgabe unserer Verlustfunktion. Wir verwenden den mittleren quadratischen Fehler, der als positive Zahl ausgedrückt wird. Generell gilt: Je kleiner der Verlustwert, desto besser. Das ist also ein guter Anhaltspunkt für die Bewertung unseres Netzwerks.
Vergleicht man die erste und die letzte Epoche, so hat sich das Netzwerk während des Trainings deutlich verbessert: von einem Verlust von ~0,7 auf einen kleineren Wert von ~0,15. Schauen wir uns die anderen Zahlen an, um zu sehen, ob diese Verbesserung ausreichend ist!
mae
-
Dies ist der mittlere absolute Fehler unserer Trainingsdaten. Er zeigt die durchschnittliche Differenz zwischen den Vorhersagen des Netzwerks und den erwarteten
y
Werten aus den Trainingsdaten.Wir können davon ausgehen, dass unser anfänglicher Fehler ziemlich beängstigend sein wird, da er auf einem ungeübten Netzwerk basiert. Das ist auch der Fall: Die Vorhersagen des Netzwerks liegen im Durchschnitt um ~0,78 daneben, was eine große Zahl ist, wenn der Bereich der akzeptablen Werte nur zwischen -1 und 1 liegt!
Aber auch nach dem Training liegt unser mittlerer absoluter Fehler bei ~0,30. Das bedeutet, dass unsere Vorhersagen im Durchschnitt um ~0,30 daneben liegen, was immer noch ziemlich schlimm ist.
val_loss
-
Dies ist die Ausgabe unserer Verlustfunktion für unsere Validierungsdaten. In unserer letzten Epoche ist der Trainingsverlust (~0,15) etwas geringer als der Validierungsverlust (~0,17). Das ist ein Hinweis darauf, dass unser Netzwerk möglicherweise überangepasst ist, weil es bei Daten, die es noch nie gesehen hat, schlechter abschneidet.
val_mae
-
Dies ist der mittlere absolute Fehler für unsere Validierungsdaten. Mit einem Wert von ~0,32 ist er schlechter als der mittlere absolute Fehler unseres Trainingssets, was ein weiteres Zeichen dafür ist, dass das Netzwerk möglicherweise überangepasst ist.
Die Geschichte grafisch darstellen
Bis jetzt ist klar, dass unser Modell keine guten Vorhersagen macht. Unsere Aufgabe ist es nun, herauszufinden, warum. Dazu nutzen wir die Daten, die wir in unserem history_1
Objekt gesammelt haben.
Die nächste Zelle extrahiert die Trainings- und Validierungsverlustdaten aus dem Verlaufsobjekt und stellt sie in einem Diagramm dar:
loss
=
history_1
.
history
[
'loss'
]
val_loss
=
history_1
.
history
[
'val_loss'
]
epochs
=
range
(
1
,
len
(
loss
)
+
1
)
plt
.
plot
(
epochs
,
loss
,
'g.'
,
label
=
'Training loss'
)
plt
.
plot
(
epochs
,
val_loss
,
'b'
,
label
=
'Validation loss'
)
plt
.
title
(
'Training and validation loss'
)
plt
.
xlabel
(
'Epochs'
)
plt
.
ylabel
(
'Loss'
)
plt
.
legend
()
plt
.
show
()
Das Objekt history_1
enthält ein Attribut namens history_1.history
, das ein Wörterbuch ist, das die Metrikwerte während des Trainings und der Validierung aufzeichnet. Wir verwenden es, um die Daten zu sammeln, die wir aufzeichnen wollen. Für die x-Achse verwenden wir die Epochenzahl, die wir anhand der Anzahl der Verlustdatenpunkte ermitteln. Führe die Zelle aus und du siehst das Diagramm in Abbildung 4-13.
Wie du siehst, nimmt die Höhe der Verluste in den ersten 50 Epochen schnell ab, bevor sie abflacht. Das bedeutet, dass das Modell besser wird und genauere Vorhersagen macht.
Unser Ziel ist es, das Training abzubrechen, wenn sich das Modell entweder nicht mehr verbessert oder der Trainingsverlust kleiner ist als der Validierungsverlust. Das würde bedeuten, dass das Modell gelernt hat, die Trainingsdaten so gut vorherzusagen, dass es nicht mehr auf neue Daten verallgemeinern kann.
Der Verlust fällt in den ersten paar Epochen sprunghaft ab, was den Rest des Diagramms ziemlich schwer lesbar macht. Lass uns die ersten 100 Epochen überspringen, indem wir die nächste Zelle ausführen:
# Exclude the first few epochs so the graph is easier to read
SKIP
=
100
plt
.
plot
(
epochs
[
SKIP
:],
loss
[
SKIP
:],
'g.'
,
label
=
'Training loss'
)
plt
.
plot
(
epochs
[
SKIP
:],
val_loss
[
SKIP
:],
'b.'
,
label
=
'Validation loss'
)
plt
.
title
(
'Training and validation loss'
)
plt
.
xlabel
(
'Epochs'
)
plt
.
ylabel
(
'Loss'
)
plt
.
legend
()
plt
.
show
()
Abbildung 4-14 zeigt das Diagramm, das von dieser Zelle erstellt wurde.
Jetzt, wo wir reingezoomt haben, kannst du sehen, dass der Verlust bis etwa 600 Epochen weiter abnimmt und dann weitgehend stabil ist. Das bedeutet, dass es wahrscheinlich nicht nötig ist, unser Netz so lange zu trainieren.
Du kannst aber auch sehen, dass der niedrigste Verlustwert immer noch bei 0,15 liegt. Das scheint relativ hoch zu sein. Außerdem sind die Verlustwerte bei der Validierung durchweg noch höher.
Um mehr Einblick in die Leistung unseres Modells zu bekommen, können wir weitere Daten aufzeichnen. Dieses Mal wollen wir den mittleren absoluten Fehler aufzeichnen. Führe die nächste Zelle aus, um dies zu tun:
# Draw a graph of mean absolute error, which is another way of
# measuring the amount of error in the prediction.
mae
=
history_1
.
history
[
'mae'
]
val_mae
=
history_1
.
history
[
'val_mae'
]
plt
.
plot
(
epochs
[
SKIP
:],
mae
[
SKIP
:],
'g.'
,
label
=
'Training MAE'
)
plt
.
plot
(
epochs
[
SKIP
:],
val_mae
[
SKIP
:],
'b.'
,
label
=
'Validation MAE'
)
plt
.
title
(
'Training and validation mean absolute error'
)
plt
.
xlabel
(
'Epochs'
)
plt
.
ylabel
(
'MAE'
)
plt
.
legend
()
plt
.
show
()
Abbildung 4-15 zeigt das resultierende Diagramm.
Diese Grafik des mittleren absoluten Fehlers gibt uns weitere Hinweise. Wir können sehen, dass die Trainingsdaten im Durchschnitt einen geringeren Fehler aufweisen als die Validierungsdaten. Das bedeutet, dass das Netzwerk möglicherweise eine Überanpassung vorgenommen oder die Trainingsdaten so starr gelernt hat, dass es keine effektiven Vorhersagen für neue Daten machen kann.
Außerdem sind die mittleren absoluten Fehlerwerte recht hoch, etwa 0,31, was bedeutet, dass einige der Vorhersagen des Modells um mindestens 0,31 falsch sind. Da unsere erwarteten Werte nur zwischen -1 und +1 liegen, bedeutet ein Fehler von 0,31, dass wir sehr weit davon entfernt sind, die Sinuswelle genau zu modellieren.
Um einen besseren Einblick in die Vorgänge zu bekommen, können wir die Vorhersagen unseres Netzwerks für die Trainingsdaten mit den erwarteten Werten vergleichen.
Dies geschieht in der folgenden Zelle:
# Use the model to make predictions from our validation data
predictions
=
model_1
.
predict
(
x_train
)
# Plot the predictions along with the test data
plt
.
clf
()
plt
.
title
(
'Training data predicted vs actual values'
)
plt
.
plot
(
x_test
,
y_test
,
'b.'
,
label
=
'Actual'
)
plt
.
plot
(
x_train
,
predictions
,
'r.'
,
label
=
'Predicted'
)
plt
.
legend
()
plt
.
show
()
Wenn du model_1.predict(x_train)
aufrufst, werden alle x
Werte aus den Trainingsdaten ausgewertet. Die Methode gibt ein Array mit Vorhersagen zurück. Diese stellen wir im Diagramm zusammen mit den tatsächlichen y
Werten aus unserem Trainingssatz dar. Führe die Zelle aus, um das Diagramm in Abbildung 4-16 zu sehen.
Oh je! Die Grafik macht deutlich, dass unser Netzwerk gelernt hat, die Sinusfunktion nur sehr begrenzt zu approximieren. Die Vorhersagen sind sehr linear und passen nur sehr grob zu den Daten.
Die Starrheit dieser Anpassung deutet darauf hin, dass das Modell nicht genug Kapazität hat, um die volle Komplexität der Sinuswellenfunktion zu lernen, und sie daher nur auf eine allzu einfache Weise annähern kann. Wenn wir unser Modell vergrößern, sollten wir seine Leistung verbessern können.
Unser Modell verbessern
Mit dem Wissen, dass unser ursprüngliches Modell zu klein war, um die Komplexität unserer Daten zu lernen, können wir versuchen, es zu verbessern. Das ist ein normaler Teil des Arbeitsablaufs beim maschinellen Lernen: Wir entwickeln ein Modell, bewerten seine Leistung und nehmen Änderungen vor, in der Hoffnung, dass es sich verbessert.
Eine einfache Möglichkeit, das Netzwerk zu vergrößern, ist das Hinzufügen einer weiteren Schicht von Neuronen. Jede Neuronenschicht stellt eine Umwandlung der Eingabe dar, die sie hoffentlich näher an die erwartete Ausgabe bringt. Je mehr Schichten von Neuronen ein Netzwerk hat, desto komplexer können diese Umwandlungen sein.
Führe die folgende Zelle aus, um unser Modell auf dieselbe Weise wie zuvor neu zu definieren, aber mit einer zusätzlichen Schicht von 16 Neuronen in der Mitte:
model_2
=
tf
.
keras
.
Sequential
()
# First layer takes a scalar input and feeds it through 16 "neurons." The
# neurons decide whether to activate based on the 'relu' activation function.
model_2
.
add
(
layers
.
Dense
(
16
,
activation
=
'relu'
,
input_shape
=
(
1
,)))
# The new second layer may help the network learn more complex representations
model_2
.
add
(
layers
.
Dense
(
16
,
activation
=
'relu'
))
# Final layer is a single neuron, since we want to output a single value
model_2
.
add
(
layers
.
Dense
(
1
))
# Compile the model using a standard optimizer and loss function for regression
model_2
.
compile
(
optimizer
=
'rmsprop'
,
loss
=
'mse'
,
metrics
=
[
'mae'
])
# Show a summary of the model
model_2
.
summary
()
Wie du siehst, ist der Code im Grunde derselbe wie bei unserem ersten Modell, aber mit einer zusätzlichen Dense
Schicht. Lassen wir die Zelle laufen, um die summary()
Ergebnisse zu sehen:
Model: "sequential_1" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense_2 (Dense) (None, 16) 32 _________________________________________________________________ dense_3 (Dense) (None, 16) 272 _________________________________________________________________ dense_4 (Dense) (None, 1) 17 ================================================================= Total params: 321 Trainable params: 321 Non-trainable params: 0 _________________________________________________________________
Mit zwei Schichten von 16 Neuronen ist unser neues Modell viel größer. Es hat (1 * 16) + (16 * 16) + (16 * 1) = 288 Gewichte, plus 16 + 16 + 1 = 33 Verzerrungen, also insgesamt 288 + 33 = 321 Parameter. Unser ursprüngliches Modell hatte nur 49 Parameter, also ist das Modell um 555% größer geworden. Wir hoffen, dass diese zusätzliche Kapazität dazu beiträgt, die Komplexität unserer Daten darzustellen.
Die folgende Zelle wird unser neues Modell trainieren. Da sich unser erstes Modell nicht mehr so schnell verbessert hat, trainieren wir dieses Mal weniger Epochen - nur 600. Starte diese Zelle, um mit dem Training zu beginnen:
history_2
=
model_2
.
fit
(
x_train
,
y_train
,
epochs
=
600
,
batch_size
=
16
,
validation_data
=
(
x_validate
,
y_validate
))
Wenn das Training abgeschlossen ist, können wir einen Blick auf das abschließende Protokoll werfen, um ein schnelles Gefühl dafür zu bekommen, ob sich die Dinge verbessert haben:
Epoch 600/600 600/600 [==============================] - 0s 150us/sample - loss: 0.0115 - mae: 0.0859 - val_loss: 0.0104 - val_mae: 0.0806
Wow! Du kannst sehen, dass wir bereits eine enorme Verbesserung erreicht haben - der Validierungsverlust ist von 0,17 auf 0,01 gesunken und der mittlere absolute Fehler der Validierung ist von 0,32 auf 0,08 gefallen. Das sieht sehr vielversprechend aus.
Um zu sehen, wie die Dinge laufen, lass uns die nächste Zelle ausführen. Sie ist so eingestellt, dass sie dieselben Graphen erstellt, die wir beim letzten Mal verwendet haben. Zuerst zeichnen wir ein Diagramm des Verlusts:
# Draw a graph of the loss, which is the distance between
# the predicted and actual values during training and validation.
loss
=
history_2
.
history
[
'loss'
]
val_loss
=
history_2
.
history
[
'val_loss'
]
epochs
=
range
(
1
,
len
(
loss
)
+
1
)
plt
.
plot
(
epochs
,
loss
,
'g.'
,
label
=
'Training loss'
)
plt
.
plot
(
epochs
,
val_loss
,
'b'
,
label
=
'Validation loss'
)
plt
.
title
(
'Training and validation loss'
)
plt
.
xlabel
(
'Epochs'
)
plt
.
ylabel
(
'Loss'
)
plt
.
legend
()
plt
.
show
()
Abbildung 4-17 zeigt das Ergebnis.
Als Nächstes zeichnen wir dasselbe Verlustdiagramm, lassen aber die ersten 100 Epochen weg, damit wir die Details besser sehen können:
# Exclude the first few epochs so the graph is easier to read
SKIP
=
100
plt
.
clf
()
plt
.
plot
(
epochs
[
SKIP
:],
loss
[
SKIP
:],
'g.'
,
label
=
'Training loss'
)
plt
.
plot
(
epochs
[
SKIP
:],
val_loss
[
SKIP
:],
'b.'
,
label
=
'Validation loss'
)
plt
.
title
(
'Training and validation loss'
)
plt
.
xlabel
(
'Epochs'
)
plt
.
ylabel
(
'Loss'
)
plt
.
legend
()
plt
.
show
()
Abbildung 4-18 zeigt die Ausgabe.
Schließlich stellen wir den mittleren absoluten Fehler für dieselbe Anzahl von Epochen dar:
plt
.
clf
()
# Draw a graph of mean absolute error, which is another way of
# measuring the amount of error in the prediction.
mae
=
history_2
.
history
[
'mae'
]
val_mae
=
history_2
.
history
[
'val_mae'
]
plt
.
plot
(
epochs
[
SKIP
:],
mae
[
SKIP
:],
'g.'
,
label
=
'Training MAE'
)
plt
.
plot
(
epochs
[
SKIP
:],
val_mae
[
SKIP
:],
'b.'
,
label
=
'Validation MAE'
)
plt
.
title
(
'Training and validation mean absolute error'
)
plt
.
xlabel
(
'Epochs'
)
plt
.
ylabel
(
'MAE'
)
plt
.
legend
()
plt
.
show
()
In Abbildung 4-19 ist das Diagramm dargestellt.
Tolle Ergebnisse! Aus diesen Diagrammen können wir zwei spannende Dinge erkennen:
-
Die Metriken sind im Großen und Ganzen besser für die Validierung als für das Training, was bedeutet, dass das Netzwerk nicht überangepasst ist.
-
Der Gesamtverlust und der mittlere absolute Fehler sind viel besser als in unserem vorherigen Netzwerk.
Du fragst dich vielleicht, warum die Metriken für die Validierung besser sind als die für das Training und nicht einfach identisch. Der Grund dafür ist, dass die Validierungsmetriken am Ende jeder Epoche berechnet werden, während die Trainingsmetriken noch während der Trainingsepoche berechnet werden. Das bedeutet, dass die Validierung mit einem Modell erfolgt, das schon etwas länger trainiert wurde.
Anhand unserer Validierungsdaten scheint unser Modell gut zu funktionieren. Um uns dessen sicher zu sein, müssen wir jedoch noch einen letzten Test durchführen.
Testen
Zuvor haben wir 20 % unserer Daten für die Tests beiseite gelegt. Wie wir bereits besprochen haben, ist es sehr wichtig, Validierungs- und Testdaten voneinander zu trennen. Da wir unser Netzwerk auf der Grundlage seiner Validierungsleistung feinabstimmen, besteht die Gefahr, dass wir das Modell versehentlich so abstimmen, dass es sich zu stark an die Validierungsdaten anpasst und nicht in der Lage ist, es auf neue Daten zu verallgemeinern. Indem wir einige neue Daten aufbewahren und sie für einen letzten Test unseres Modells verwenden, können wir sicherstellen, dass dies nicht passiert.
Nachdem wir unsere Testdaten verwendet haben, müssen wir dem Drang widerstehen, unser Modell weiter zu optimieren. Wenn wir Änderungen mit dem Ziel vornehmen, die Testleistung zu verbessern, könnte das dazu führen, dass das Modell unsere Testdaten übererfüllt. Wenn wir das tun, können wir es nicht wissen, weil wir dann keine neuen Daten mehr haben, mit denen wir testen können.
Das heißt, wenn unser Modell bei unseren Testdaten schlecht abschneidet, ist es an der Zeit, wieder an das Reißbrett zu gehen. Wir müssen dann aufhören, das aktuelle Modell zu optimieren, und eine ganz neue Architektur entwickeln.
Deshalb wird in der folgenden Zelle unser Modell anhand unserer Testdaten bewertet:
# Calculate and print the loss on our test dataset
loss
=
model_2
.
evaluate
(
x_test
,
y_test
)
# Make predictions based on our test dataset
predictions
=
model_2
.
predict
(
x_test
)
# Graph the predictions against the actual values
plt
.
clf
()
plt
.
title
(
'Comparison of predictions and actual values'
)
plt
.
plot
(
x_test
,
y_test
,
'b.'
,
label
=
'Actual'
)
plt
.
plot
(
x_test
,
predictions
,
'r.'
,
label
=
'Predicted'
)
plt
.
legend
()
plt
.
show
()
Zunächst rufen wir die Methode evaluate()
des Modells mit den Testdaten auf. Dadurch werden der Verlust und der mittlere absolute Fehler berechnet und ausgedruckt, was uns darüber informiert, wie weit die Vorhersagen des Modells von den tatsächlichen Werten abweichen. Als Nächstes erstellen wir eine Reihe von Vorhersagen und stellen sie in einem Diagramm neben den tatsächlichen Werten dar.
Jetzt können wir die Zelle ausführen, um zu erfahren, wie unser Modell funktioniert! Sehen wir uns zunächst die Ergebnisse von evaluate()
an:
200/200 [==============================] - 0s 71us/sample - loss: 0.0103 - mae: 0.0718
Dies zeigt, dass 200 Datenpunkte ausgewertet wurden, also unsere gesamte Testmenge. Das Modell brauchte 71 Mikrosekunden für jede Vorhersage. Die Verlustmetrik betrug 0,0103, was hervorragend ist und sehr nahe an unserem Validierungsverlust von 0,0104 liegt. Unser mittlerer absoluter Fehler, 0,0718, ist ebenfalls sehr gering und liegt ziemlich nah an seinem Äquivalent in der Validierung, 0,0806.
Das bedeutet, dass unser Modell gut funktioniert und nicht überanpasst! Hätte das Modell unsere Validierungsdaten überangepasst, wäre zu erwarten gewesen, dass die Metriken auf unserer Testmenge deutlich schlechter wären als die der Validierung.
Die Grafik unserer Vorhersagen im Vergleich zu den tatsächlichen Werten in Abbildung 4-20 macht deutlich, wie gut unser Modell funktioniert.
Du kannst sehen, dass die Punkte, die die vorhergesagten Werte darstellen, größtenteils eine glatte Kurve entlang der Mitte der Verteilung der tatsächlichen Werte bilden. Unser Netzwerk hat gelernt, sich einer Sinuskurve anzunähern, obwohl der Datensatz verrauscht war!
Wenn du genau hinsiehst, wirst du jedoch feststellen, dass es einige Unvollkommenheiten gibt. Die Spitze und das Tal unserer vorhergesagten Sinuswelle sind nicht vollkommen glatt, wie es bei einer echten Sinuswelle der Fall wäre. Die Schwankungen in unseren Trainingsdaten, die zufällig verteilt sind, wurden von unserem Modell gelernt. Dies ist ein leichter Fall von Überanpassung: Anstatt die glatte Sinusfunktion zu lernen, hat unser Modell gelernt, die genaue Form unserer Daten zu replizieren.
Für unsere Zwecke ist diese Überanpassung kein großes Problem. Unser Ziel ist es, dass dieses Modell eine LED sanft ein- und ausblendet, und dafür muss es nicht perfekt angepasst sein. Wenn wir den Grad der Überanpassung für problematisch hielten, könnten wir versuchen, ihn durch Regularisierungstechniken oder durch die Beschaffung von mehr Trainingsdaten zu beheben.
Jetzt, wo wir mit unserem Modell zufrieden sind, machen wir es bereit für den Einsatz auf dem Gerät!
Konvertieren des Modells für TensorFlow Lite
Zu Beginn dieses Kapitels haben wir uns kurz mit TensorFlow Lite befasst, einer Sammlung von Werkzeugen, mit denen TensorFlow-Modelle auf "Kanten-Geräten" ausgeführt werden können - das heißt, von Mobiltelefonen bis hin zu Mikrocontroller-Boards.
Kapitel 13 befasst sich ausführlich mit TensorFlow Lite für Mikrocontroller. Für den Moment können wir uns vorstellen, dass es zwei Hauptkomponenten hat:
- TensorFlow Lite Konverter
-
Sie konvertiert TensorFlow-Modelle in ein spezielles, platzsparendes Format für die Verwendung auf Geräten mit begrenztem Speicherplatz und kann Optimierungen anwenden, die die Modellgröße weiter reduzieren und die Ausführung auf kleinen Geräten beschleunigen.
- TensorFlow Lite Interpreter
-
Dabei wird ein entsprechend konvertiertes TensorFlow Lite Modell mit den effizientesten Operationen für ein bestimmtes Gerät ausgeführt.
Bevor wir unser Modell mit TensorFlow Lite verwenden, müssen wir es konvertieren. Dazu verwenden wir die Python-API des TensorFlow Lite Converters. Sie nimmt unser Keras-Modell und schreibt es in Form eines FlatBuffers auf die Festplatte, ein spezielles Dateiformat, das platzsparend ist. Da wir auf Geräten mit begrenztem Speicherplatz arbeiten, ist das sehr nützlich! Wir werden uns FlatBuffers in Kapitel 12 genauer ansehen.
Neben der Erstellung eines FlatBuffers kann der TensorFlow Lite Converter auch Optimierungen auf das Modell anwenden. Diese Optimierungen verringern in der Regel die Größe des Modells, die Zeit, die es zur Ausführung braucht, oder beides. Das kann auf Kosten einer geringeren Genauigkeit gehen, aber die Verringerung ist oft so gering, dass sie sich lohnt. Mehr über Optimierungen erfährst du in Kapitel 13.
Eine der nützlichsten Optimierungen ist die Quantisierung. Standardmäßig werden die Gewichte und Verzerrungen in einem Modell als 32-Bit-Gleitkommazahlen gespeichert, damit beim Training hochpräzise Berechnungen durchgeführt werden können. Durch Quantisierung kannst du die Genauigkeit dieser Zahlen reduzieren, so dass sie in 8-Bit-Ganzzahlen passen - eine Vervierfachung der Größe. Noch besser: Da es für eine CPU einfacher ist, mit ganzen Zahlen zu rechnen als mit Fließkommazahlen, läuft ein quantisiertes Modell schneller.
Das Tolle an der Quantisierung ist, dass sie oft nur zu minimalen Genauigkeitsverlusten führt. Das bedeutet, dass sie sich beim Einsatz auf Geräten mit geringem Speicherplatz fast immer lohnt.
In der folgenden Zelle verwenden wir den Konverter, um zwei neue Versionen unseres Modells zu erstellen und zu speichern. Die erste wird in das TensorFlow Lite FlatBuffer Format konvertiert, aber ohne jegliche Optimierungen. Die zweite wird quantisiert.
Führe die Zelle aus, um das Modell in diese beiden Varianten umzuwandeln:
# Convert the model to the TensorFlow Lite format without quantization
converter
=
tf
.
lite
.
TFLiteConverter
.
from_keras_model
(
model_2
)
tflite_model
=
converter
.
convert
()
# Save the model to disk
open
(
"sine_model.tflite"
,
"wb"
)
.
write
(
tflite_model
)
# Convert the model to the TensorFlow Lite format with quantization
converter
=
tf
.
lite
.
TFLiteConverter
.
from_keras_model
(
model_2
)
# Indicate that we want to perform the default optimizations,
# which include quantization
converter
.
optimizations
=
[
tf
.
lite
.
Optimize
.
DEFAULT
]
# Define a generator function that provides our test data's x values
# as a representative dataset, and tell the converter to use it
def
representative_dataset_generator
():
for
value
in
x_test
:
# Each scalar value must be inside of a 2D array that is wrapped in a list
yield
[
np
.
array
(
value
,
dtype
=
np
.
float32
,
ndmin
=
2
)]
converter
.
representative_dataset
=
representative_dataset_generator
# Convert the model
tflite_model
=
converter
.
convert
()
# Save the model to disk
open
(
"sine_model_quantized.tflite"
,
"wb"
)
.
write
(
tflite_model
)
Um ein quantisiertes Modell zu erstellen, das so effizient wie möglich läuft, müssen wir einen repräsentativen Datensatzbereitstellen - eineReihe von Zahlen, die den gesamten Bereich der Eingabewerte des Datensatzes repräsentieren, auf dem das Modell trainiert wurde.
In der vorangegangenen Zelle können wir die Werte unseres Testdatensatzes x
als repräsentativen Datensatz verwenden. Wir definieren eine Funktion, representative_dataset_generator()
, die mit dem Operator yield
die Werte einzeln zurückgibt.
Um zu beweisen, dass diese Modelle auch nach der Konvertierung und Quantisierung noch genau sind, verwenden wir beide Modelle, um Vorhersagen zu treffen und diese mit unseren Testergebnissen zu vergleichen. Da es sich um TensorFlow Lite Modelle handelt, müssen wir dafür den TensorFlow Lite Interpreter verwenden.
Da er in erster Linie auf Effizienz ausgelegt ist, ist der TensorFlow Lite Interpreter etwas komplizierter zu bedienen als die Keras API. Um mit unserem Keras-Modell Vorhersagen zu machen, könnten wir einfach die Methode predict()
aufrufen und ein Array von Eingaben übergeben. Mit TensorFlow Lite müssen wir Folgendes tun:
-
Instanziere ein
Interpreter
Objekt. -
Rufe einige Methoden auf, die Speicher für das Modell zuweisen.
-
Schreibe den Input in den Input-Tensor.
-
Rufe das Modell auf.
-
Lies die Ausgabe aus dem Ausgabentensor.
Das hört sich nach viel an, aber mach dir erst einmal keine Gedanken darüber; wir werden es in Kapitel 5 im Detail durchgehen. Führe zunächst die folgende Zelle aus, um Vorhersagen mit beiden Modellen zu treffen und sie in einem Diagramm zusammen mit den Ergebnissen unseres ursprünglichen, nicht umgewandelten Modells darzustellen:
# Instantiate an interpreter for each model
sine_model
=
tf
.
lite
.
Interpreter
(
'sine_model.tflite'
)
sine_model_quantized
=
tf
.
lite
.
Interpreter
(
'sine_model_quantized.tflite'
)
# Allocate memory for each model
sine_model
.
allocate_tensors
()
sine_model_quantized
.
allocate_tensors
()
# Get indexes of the input and output tensors
sine_model_input_index
=
sine_model
.
get_input_details
()[
0
][
"index"
]
sine_model_output_index
=
sine_model
.
get_output_details
()[
0
][
"index"
]
sine_model_quantized_input_index
=
sine_model_quantized
.
get_input_details
()[
0
][
"index"
]
sine_model_quantized_output_index
=
\sine_model_quantized
.
get_output_details
()[
0
][
"index"
]
# Create arrays to store the results
sine_model_predictions
=
[]
sine_model_quantized_predictions
=
[]
# Run each model's interpreter for each value and store the results in arrays
for
x_value
in
x_test
:
# Create a 2D tensor wrapping the current x value
x_value_tensor
=
tf
.
convert_to_tensor
([[
x_value
]],
dtype
=
np
.
float32
)
# Write the value to the input tensor
sine_model
.
set_tensor
(
sine_model_input_index
,
x_value_tensor
)
# Run inference
sine_model
.
invoke
()
# Read the prediction from the output tensor
sine_model_predictions
.
append
(
sine_model
.
get_tensor
(
sine_model_output_index
)[
0
])
# Do the same for the quantized model
sine_model_quantized
.
set_tensor
\(
sine_model_quantized_input_index
,
x_value_tensor
)
sine_model_quantized
.
invoke
()
sine_model_quantized_predictions
.
append
(
sine_model_quantized
.
get_tensor
(
sine_model_quantized_output_index
)[
0
])
# See how they line up with the data
plt
.
clf
()
plt
.
title
(
'Comparison of various models against actual values'
)
plt
.
plot
(
x_test
,
y_test
,
'bo'
,
label
=
'Actual'
)
plt
.
plot
(
x_test
,
predictions
,
'ro'
,
label
=
'Original predictions'
)
plt
.
plot
(
x_test
,
sine_model_predictions
,
'bx'
,
label
=
'Lite predictions'
)
plt
.
plot
(
x_test
,
sine_model_quantized_predictions
,
'gx'
,
\label
=
'Lite quantized predictions'
)
plt
.
legend
()
plt
.
show
()
Wenn du diese Zelle ausführst, erhältst du das Diagramm in Abbildung 4-21.
Aus dem Diagramm geht hervor, dass die Vorhersagen für das ursprüngliche Modell, das umgewandelte Modell und das quantisierte Modell so nahe beieinander liegen, dass sie nicht voneinander zu unterscheiden sind. Es sieht gut aus!
Da die Quantisierung die Modelle kleiner macht, vergleichen wir die beiden umgewandelten Modelle, um den Größenunterschied zu sehen. Führe die folgende Zelle aus, um ihre Größen zu berechnen und zu vergleichen:
import
os
basic_model_size
=
os
.
path
.
getsize
(
"sine_model.tflite"
)
(
"Basic model is
%d
bytes"
%
basic_model_size
)
quantized_model_size
=
os
.
path
.
getsize
(
"sine_model_quantized.tflite"
)
(
"Quantized model is
%d
bytes"
%
quantized_model_size
)
difference
=
basic_model_size
-
quantized_model_size
(
"Difference is
%d
bytes"
%
difference
)
Du solltest die folgende Ausgabe sehen:
Basic model is 2736 bytes Quantized model is 2512 bytes Difference is 224 bytes
Unser quantisiertes Modell ist 224 Bytes kleiner als die ursprüngliche Version, was großartig ist - aber es ist nur eine geringfügige Verringerung der Größe. Mit etwa 2,4 KB ist dieses Modell bereits so klein, dass die Gewichte und Voreinstellungen nur einen Bruchteil der Gesamtgröße ausmachen. Zusätzlich zu den Gewichten enthält das Modell die gesamte Logik, die die Architektur unseres Deep Learning-Netzwerks ausmacht, den sogenannten Berechnungsgraphen. Bei wirklich winzigen Modellen kann dies mehr Platz beanspruchen als die Gewichte des Modells, so dass die Quantisierung kaum Auswirkungen hat.
Komplexere Modelle haben viel mehr Gewichte, was bedeutet, dass die Platzersparnis durch Quantisierung viel höher ist. Bei den anspruchsvollsten Modellen kann man davon ausgehen, dass sie sich dem Vierfachen nähert.
Unabhängig von der genauen Größe benötigt unser quantisiertes Modell weniger Zeit für die Ausführung als die Originalversion, was auf einem winzigen Mikrocontroller wichtig ist.
Konvertierung in eine C-Datei
Der letzte Schritt, um unser Modell für die Verwendung mit TensorFlow Lite für Mikrocontroller vorzubereiten, besteht darin, es in eine C-Quelldatei zu konvertieren, die in unsere Anwendung eingebunden werden kann.
Bislang haben wir in diesem Kapitel die Python-API von TensorFlow Lite verwendet. Das bedeutet, dass wir den Interpreter
Konstruktor verwenden konnten, um unsere Modelldateien von der Festplatte zu laden.
Die meisten Mikrocontroller haben jedoch kein Dateisystem, und selbst wenn sie eines hätten, wäre der zusätzliche Code, der zum Laden eines Modells von der Festplatte erforderlich ist, angesichts des begrenzten Platzes eine Verschwendung. Als elegante Lösung stellen wir das Modell stattdessen in einer C-Quelldatei bereit, die in unser Binärprogramm eingebunden und direkt in den Speicher geladen werden kann.
In der Datei ist das Modell als Array von Bytes definiert. Zum Glück gibt es ein praktisches Unix-Tool namens xxd
, das eine gegebene Datei in das gewünschte Format umwandeln kann.
Die folgende Zelle führt xxd
auf unserem quantisierten Modell aus, schreibt die Ausgabe in eine Datei namens sine_model_quantized.cc und gibt sie auf dem Bildschirm aus:
# Install xxd if it is not available
!apt-get -qq install xxd# Save the file as a C source file
!xxd -i sine_model_quantized.tflite > sine_model_quantized.cc# Print the source file
!cat sine_model_quantized.cc
Die Ausgabe ist sehr lang, daher werden wir sie hier nicht vollständig wiedergeben, aber hier ist ein Ausschnitt, der nur den Anfang und das Ende enthält:
unsigned
char
sine_model_quantized_tflite
[]
=
{
0x1c
,
0x00
,
0x00
,
0x00
,
0x54
,
0x46
,
0x4c
,
0x33
,
0x00
,
0x00
,
0x12
,
0x00
,
0x1c
,
0x00
,
0x04
,
0x00
,
0x08
,
0x00
,
0x0c
,
0x00
,
0x10
,
0x00
,
0x14
,
0x00
,
// ...
0x00
,
0x00
,
0x08
,
0x00
,
0x0a
,
0x00
,
0x00
,
0x00
,
0x00
,
0x00
,
0x00
,
0x09
,
0x04
,
0x00
,
0x00
,
0x00
};
unsigned
int
sine_model_quantized_tflite_len
=
2512
;
Um dieses Modell in einem Projekt zu verwenden, kannst du entweder den Quelltext kopieren und einfügen oder die Datei aus dem Notizbuch herunterladen.
Einpacken
Und damit sind wir mit der Erstellung unseres Modells fertig. Wir haben ein TensorFlow Deep Learning Netzwerk trainiert, ausgewertet und umgewandelt, das eine Zahl zwischen 0 und 2π annehmen und eine ausreichende Annäherung an den Sinus ausgeben kann.
Dies war unser erster Versuch, mit Keras ein kleines Modell zu trainieren. In zukünftigen Projekten werden wir Modelle trainieren, die zwar immer noch winzig sind, aber viel anspruchsvoller.
Lass uns jetzt mit Kapitel 5 weitermachen, wo wir Code schreiben werden, um unser Modell auf Mikrocontrollern laufen zu lassen.
Get TinyML now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.