MNIST-Ziffer

MNIST mit VBA einlesen und umformen

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 es darum geht, die Erkennungsqualität von neuronalen Netzen zu bewerten, bzw. die Performance unterschiedlicher Netzen miteinander vergleichen zu können.

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). Diese sind auf 2 Datensätze (60.000 Ziffern zum Training und zu 10.000 Ziffern zum Testen von neuronalen Netzen) aufgeteilt. Mit dem ersten Datensatz trainiert man ein neuronales Netz und mit dem zweiten, das 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 Einlesemodule 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 Datenformaten 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 – schauen wir uns dafür den Trainigsdatensatz, der 60.000 Bilder umfasst und aus zwei Dateien besteht.

MNIST-Datensatz als Text
MNIST-Datensatz als Text

Ö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 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
MNIST-Ziffer in grafischer Darstellung
MNIST-Ziffer in grafischer Darstellung

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.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.