Befasst man sich mit der Entwicklung von künstlichen neuronalen Netzen, stolpert man unweigerlich über den MNIST Datensatz. Dieser hat sich mittlerweile zu einem Quasi-Standard entwickelt, wenn unterschiedliche neuronale Netze hinsichtlich ihrer Erkennungsqualität miteinander verglichen werden sollen.
In diesem Tutorial gehe ich auf die Besonderheiten vom relativ sperrigen MNIST Format ein und stelle eine Klasse vor, die den MNIST mit VBA ausliest und diesen mit einer einfach zu nutzenden Schnittstelle zur Verfügung stellt.
Aufbau des MNIST-Datensatzes
An dieser Stelle etwas Theorie. MNIST ist eine Abkürzung, die für Modified National Institute of Standards and Technology steht und ist in diesem Kontext eine Sammlung von insgesamt 70.000 handgeschriebenen Ziffern (0 bis 9), aufgeteilt auf zwei Datensätze. Mit dem ersten Datensatz (60.000 Ziffern) trainiert man ein neuronales Netz und mit dem zweiten (10.000 Ziffern), der dem Netz unbekannt ist, testet man es anschießend.
Wie ist denn der MNIST Datensatz nun aufgebaut?
The data is stored in a very simple file format designed for storing vectors and multidimensional matrices
http://yann.lecun.com/exdb/mnist/
Diese Aussage habe ich von der Webseite des Anbieters zitiert. Was Datenstrukturen angeht, würde ich mich selbst als relativ erfahren bezeichnen. In meiner beruflichen Praxis habe ich viele Dateiformate und Strukturen gesehen und entsprechende Importmodule entweder selbst entwickelt oder betreut. Es waren auch einige Formate aus der Dinosaurierzeit der elektronischen Datenverarbeitung dabei. Aber selbst verglichen damit, würde ich den MNIST Datensatz nicht als einfach oder intuitiv beschreiben. Ohne die Doku wäre ich da aufgeschmissen. Allerdings werden im kaufmännischen Bereich, in dem ich beruflich unterwegs bin, fast ausschließlich textbasierte Datenformate eingesetzt, sodass ich bisher kaum Berührung mit binären Formaten hatte. Vielleicht erscheint es für mich deswegen so kryptisch.
Doch nun genug der Vorrede, wir springen einfach ins kalte Wasser rein und schauen wir uns dafür den Trainingsdatensatz, der 60.000 Bilder umfasst und aus zwei Dateien besteht.
Öffnet man einer dieser Datei mit dem normalen Editor wie Nodepad++, kriegt man nur böhmische Dörfer zu sehen, da die Daten, wie oben erwähnt, in binärer Form vorliegen. Damit wir mit diesen Daten was anfangen können, müssen wir größere Geschütze auffahren und die Daten in hexadezimaler Darstellung anschauen. Hierfür können wir einen HEX-Plugin für Nodepad++ installieren. Um die im weiteren Verlauf der Erklärung vorgestellten Beispiele besser nachvollziehen zu können, müssen wir die HEX- in DEC-Darstellung umwandeln. Dies erledigen wir natürlich nicht „zu Fuß“ – hierzu können wir uns beispielsweise dieses coolen Onlinerechners bedienen.
Bildpixeldaten (train-images.idx3-ubyte)
Diese Datei enthält sämtliche Darstellungsdaten der 60.000 Bilder. Am Anfang der Datei, in den ersten 16 Bytes um genau zu sein, befinden sich Daten, die den Datensatz in seiner Gesamtheit beschreiben. Damit wir uns einen besseren Überblick darüber verschaffen können, betrachten wir die ersten 16 Bytes sowie die kompletten darauffolgenden 784 Bytes des ersten Bildes im HEX-Editor. Zur einfacheren Orientierung habe ich hierzu in der ersten Zeile die Positionen der einzelnen Bytes angedruckt.
Metadaten
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
00 00 08 03 00 00 EA 60 00 00 00 1C 00 00 00 1C
Byteposition 1-2
Diese Bytes haben keine Bedeutung (zumindest interpretiere ich es so), da sie immer den Wert 0 haben.
Byteposition 3
Hier wird die Information gespeichert, in welchem Byteformat die Daten in der Datei vorliegen. In unserem Fall ist es 08, was unsigned byte bedeutet. Als weitere Ausprägung wäre 09 (signed byte) möglich. Da unsigned byte lediglich positive Werte darstellen kann, wird in diesem Fall das Vorzeichenbit frei, das dazu genutzt wird, den Wertebereich zu vergrößern. So ist es dadurch möglich, Werte zwischen 0 und 255 darzustellen, während der signed byte lediglich die Werte zwischen -127 und 127 darstellen kann.
Byteposition 4
In diesem Byte wird die Information über die Anzahl der Datendimensionen. 01 steht für Vektor, 02 für Matrix. In unserem Fall steht in diesem Byte 03. Aktuell kann ich noch nicht nachvollziehen, warum hier 03 und nicht 02 für Matrix ausgewiesen wird. Irgendwo habe ich einen Denkfehler…
Byteposition 5-8
Diese 4 Bytes sind gemeinsam zu lesen, sie stellen die Anzahl der in der Datei enthaltenen Bilder. Wandelt man die HEX-Zahlen 00 00 EA 60 in normale Dezimaldarstellung um, erhält man die Anzahl 60.000.
Byteposition 9-12
In diesen Bytes ist die Anzahl der Pixelzeilen pro Bild enthalten. 00 00 00 1C in dezimale Darstellung umgewandelt, erhalten wir 28.
Byteposition 13-16
Hier ist die Anzahl der Pixelspalten pro Bild hinterlegt. Vergleicht man diese Bytes mit den vorherigen 4 Bytes, stellt man fest, dass die identisch sind, denn die Bilder sind quadratisch und 28 x 28 Pixel groß.
Bilddaten
Die eigentlichen Bilddaten in Pixelform beginnen ab der Byteposition 17 und gehen bis ans Ende der Datei (bis zur Byteposition 47.040.016). Es sind dann insgesamt (47.040.016 – 16 = 47.040.000 Bytes), was dann geteilt durch die Anzahl der Bytes pro Bild (28 x 28 = 784) insgesamt 60.000 Bilder ergibt.
In dem dargestellten Block sind alle Pixel des ersten Bildes dargestellt, bestehend aus 784 Bytes. Jedes Byte repräsentiert den Grauwert des jeweiligen Pixels, wobei die Farbpalette zwischen 00 für weiß bis FF für schwarz reicht. Werden Pixel anhand der Bytes aus dem Screenshot in den jeweiligen Farben gezeichnet, ergibt sich ein Bild in Grautönen, das eine 5 repräsentiert.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 8B
FD BE 02 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 0B BE FD 46 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 23 F1 E1 A0 6C 01 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 51 F0 FD FD 77 19 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 2D BA FD FD 96 1B 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 10
5D FC FD BB 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 F9 FD F9
40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 2E 82 B7 FD FD CF 02 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
27 94 E5 FD FD FD FA B6 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 18 72 DD FD FD FD
FD C9 4E 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 17 42 D5 FD FD FD FD C6 51 02 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 12 AB
DB FD FD FD FD C3 50 09 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 37 AC E2 FD FD FD FD F4
85 0B 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 88 FD FD FD D4 87 84 10 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 33
9F FD 9F 32 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 30 EE FC FC FC ED
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 36 E3 FD FC EF E9 FC 39 06 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0A
3C E0 FC FD FC CA 54 FC FD 7A 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 A3 FC FC FC FD
FC FC 60 BD FD A7 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 33 EE FD FD BE 72 FD E4 2F 4F
FF A8 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 30 EE FC FC B3 0C 4B 79 15 00 00 FD F3 32 00
Labelbilddaten (train-labels.idx1-ubyte)
Wenn wir das vorherige Kapitel mit den Meta- und den Bilddaten aufmerksam durchgearbeitet haben, dann ist uns bestimmt aufgefallen, dass es in den Daten nirgends hinterlegt ist, dass es sich bei dem gezeigten Beispiel um eine 5 handelt.
Das ist tatsächlich so. Die Informationen darüber, welche Bilder sich aus den hinterlegten Bytes aus der ersten Datei ergeben, sind in der 2. Datei abgelegt. Um ehrlich zu sein, kann ich nicht nachvollziehen, welche Vorteile es bringt, die Bildinformationen und das Bildlabeling auf zwei Dateien aufzuteilen. Es ist ein wenig wie in der Schule beim Deutschunterricht, wo es um Textanalysen und darum, was uns der Autor mit seinen sprachlichen Stilmitteln sagen wollte, ging. Auch in der Informatik ist man des Öfteren in der gleichen Rolle und versucht, die Beweggründe des Entwicklers nachzuvollziehen.
Schauen wir uns aber nun den Aufbau der zweiten Datei an.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
00 00 08 01 00 00 EA 60 05 00 04 01 09 02 01 03
Metadaten
Die Metadaten dieser Datei erstrecken sich nicht über 16, sondern nur über 8 Bytes, da die anderen 8 Bytes mit den beiden Matrixdimensionen hier nicht benötigt werden.
Die ersten 8 Bytes sind nach dem selben Muster aufgebaut, weisen allerdings im Byte an der Position 4 die Ausprägung 01 auf, was für Vektordarstellung steht.
Labeldaten
Diese beginnen ab dem Position 9 und erstrecken sich über die komplette Datei bis zu Position 60.008. In jedem dieser Bytes ist das Label der jeweiligen Ziffer hinterlegt. Genau hier entdecken wir nämlich die Information, dass es sich beim ersten Bild um eine 5 handelt. Im nächsten Byte, an der Position 10, haben wir dann das Label des zweiten Bildes.
Übrigens sind die anderen beiden Dateien (t10k-images.idx3-ubyte und t10k-labels.idx1-ubyte) mit Testdaten sind genauso aufgebaut, wiesen aber in den Bild- und Labeldaten weniger Bytes auf (weil darin Daten zu nur 10.000 Bildern enthalten sind)
Nun würde ich sagen, wir haben uns technisch soweit aufgeschlaut, dass wir uns an ein spannenderes Thema heranwagen können, nämlich an die Umsetzung des MNIST Readers.
VBA MNIST Reader
Den hier vorgestellten Reader habe ich als eine Add-In Klasse in Form einer xlam-Datei umgesetzt. Diese bietet den Vorteil, dass sie auf jeden Rechner aufgespielt(indem man die einfach im AddIn-Microsoftverzeichnis ablegt) und als Verweis eingebunden kann.
Nun tauchen wir in die Klasse ein.
Klasse MNISTReader
Option Explicit
Private c_labelFilePath As String
Private c_imageFilePath As String
Private Const c_MNIST_NUMBER_PIXEL As Byte = 28
Private Const c_OFFSET_LABELDATA As Byte = 8
Private Const c_OFFSET_IMAGEDATA As Byte = 16
Public Sub setLabelFilePath(labelFilePath As String)
If Dir(labelFilePath) = "" Then
Err.Raise _
Number:=513, _
Source:="MNISTReader", _
Description:="Labelfile " & labelFilePath & " was not found"
End If
c_labelFilePath = labelFilePath
End Sub
Public Sub setImageFilePath(imageFilePath As String)
If Dir(imageFilePath) = "" Then
Err.Raise _
Number:=513, _
Source:="MNISTReader", _
Description:="Imagefile " & imageFilePath & " was not found"
End If
c_imageFilePath = imageFilePath
End Sub
Private Function getMNISTFileContent(filePath As String) As Byte()
Dim intFilePointer As Integer
Dim bytFileContent() As Byte
intFilePointer = FreeFile
Open filePath For Binary Access Read As #intFilePointer
ReDim bytFileContent(1 To LOF(intFilePointer))
Get #intFilePointer, , bytFileContent
Close #intFilePointer
getMNISTFileContent = bytFileContent
End Function
Private Function vectorToMatrix(indexLabel As Long, bytImageData() As Byte) As Byte()
Dim bytImageMatrix() As Byte
Dim bytMatrixRow As Byte
Dim bytMatrixColumn As Byte
Dim lngStartVektorIndex As Long
ReDim bytImageMatrix(1 To c_MNIST_NUMBER_PIXEL, 1 To c_MNIST_NUMBER_PIXEL) As Byte
lngStartVektorIndex = ((indexLabel - 1) * c_MNIST_NUMBER_PIXEL * c_MNIST_NUMBER_PIXEL) + c_OFFSET_IMAGEDATA
For bytMatrixRow = 1 To c_MNIST_NUMBER_PIXEL
For bytMatrixColumn = 1 To c_MNIST_NUMBER_PIXEL
lngStartVektorIndex = lngStartVektorIndex + 1
bytImageMatrix(bytMatrixRow, bytMatrixColumn) = bytImageData(lngStartVektorIndex)
Next bytMatrixColumn
Next bytMatrixRow
vectorToMatrix = bytImageMatrix
End Function
Public Function getContent() As MNISTImage()
Dim bytLabelContent() As Byte
Dim bytImageContent() As Byte
Dim typMNISTImages() As MNISTImage
Dim lngLabelNumber As Long
Dim lngImageNumber As Long
bytLabelContent = getMNISTFileContent(c_labelFilePath)
bytImageContent = getMNISTFileContent(c_imageFilePath)
lngLabelNumber = UBound(bytLabelContent) - c_OFFSET_LABELDATA
lngImageNumber = (UBound(bytImageContent) - c_OFFSET_IMAGEDATA) / c_MNIST_NUMBER_PIXEL / c_MNIST_NUMBER_PIXEL
If lngLabelNumber <> lngImageNumber Then
Err.Raise _
Number:=513, _
Source:="MNISTReader", _
Description:="Number of labels does not fit to the number of images"
End If
ReDim typMNISTImages(1 To lngLabelNumber) As MNISTImage
Dim lngMNISTElementIndex As Long
For lngMNISTElementIndex = 1 To lngLabelNumber
typMNISTImages(lngMNISTElementIndex).label = bytLabelContent(c_OFFSET_LABELDATA + lngMNISTElementIndex)
typMNISTImages(lngMNISTElementIndex).image = vectorToMatrix(lngMNISTElementIndex, bytImageContent)
Next lngMNISTElementIndex
getContent = typMNISTImages
End Function
Methoden setLabelFilePath und setImageFilePath
Diese beiden öffentlichen Methoden sind dafür da, dass bei der Objekterzeugung dieser Klasse die beiden Dateipfade gesetzt werden können. Beim Aufruf der beiden Methoden wird außerdem noch geprüft, ob es die übergebenen Dateien wirklich gibt, anderenfalls wird eine Exception geworfen.
Methode getMNISTFileContent
Diese private Methode liest vom übergebenen Dateipfad den Inhalte der Datei und legt diesen in ein ByteArray ab. Um das Array richtig zu dimensionieren, ermitteln wir mit Hilfe der Funktion LOF (Length Of File) die Anzahl der gelesenen Bytes.
Methode vectorToMatrix
Diese private Methode liest aus dem übergebenen Vektor, der sämtliche Pixel aller Bilder repräsentiert, die 784 Pixel des jeweiligen Bildes und überführt diese in eine Matrixdarstellung in der Größe 28 x 28.
Dabei wird anhand der Labelnummer, Bildgröße und der Metadatengröße automatisch die richtige Position im Vektor ermittelt, ab der sich die Pixel des jeweiligen Bildes befinden.
Methode getContent
Diese öffentliche Methode liest die beiden übergebenen Dateien aus, prüft, ob diese von der Anzahl der Dateninformationen übereinstimmend sind (wenn in der Label-Datei 10.000 Bilder beschrieben werden, dann muss die Bild-Datei genauso viele Daten zu den eigentlichen Bildern liefern).
Hier ist noch anzumerken, dass ich diese Überprüfung nicht anhand der ausgelesenen Metadaten, sondern einfach anhand der Anzahl der Arrayelemente ermittelt habe, die ich um die Anzahl der Metadaten-Bytes bereinigt habe.
Anschließend wird dann für jedes Bild sein Label und seine Pixeldaten ausgelesen, in Matrixform umformatiert und anschließend als Gesamtpacket an den Aufrufer zurück geliefert.
Modul Instanz
In diesem Modul befindet sich die Konstruktormethode, der wir beide Pfadparameter übergeben und die uns das erzeugte Objekt zurückgibt, auf welches wir nur noch die Methode getContent() abzusetzen haben, um alle ausgelesenen Bild- und Labeldaten zu erhalten.
Darüber Hinaus ist in diesem Modul auch die Definition eines neuen Typs hinterlegt, der eine MNIST Ziffer repräsentiert. Diese besteht aus dem Label und der Matrix mit den Grauwerten der Bildpixeln.
Option Explicit
Public Type MNISTImage
label As Byte
image() As Byte
End Type
Public Function getClassInstance(labelFilePath As String, imageFilePath As String) As MNISTReader
Dim objInstance As MNISTReader
Set objInstance = New MNISTReader
objInstance.setLabelFilePath (labelFilePath)
objInstance.setImageFilePath (imageFilePath)
Set getClassInstance = objInstance
End Function
Fazit
Ich denke, mit der hier vorgestellten Erläuterung ist der Aufbau des MNIST Datensatzes klarer geworden. Die vorgestellte Klasse ermöglicht ein kinderleichtes Auslesen der MNIST Daten sowie deren bequeme Weiterverarbeitung. Sollte jemand die Add-In-Klasse gut gebrachen können, stelle ich die gerne zur Verfügung.