Fun with MNIST dataset

Recently Clounce was carrying out some experiments with MNIST dataset and he wanted to glimpse at how the digits in this dataset looks like. So he thought of displaying them using ASCII art in a console window.

What is the MNIST dataset?

MNIST (http://yann.lecun.com/exdb/mnist/index.html) is a subset of NIST (https://www.nist.gov/srd/nist-special-database-19), a database for handwritten digits. MNIST contains 70,000 grey-scaled images. Each image is centered in a 28×28 pixel grid and its primary use for people who want to experiment with Machine Learning classifiers.

Parsing the data in C#

Clounce used C# FileStream to read MNIST image files. The first 16 bytes represent the header information:

  • First 4-bytes store a magic number and shall read 0x00000803
  • Second 4-bytes tells us the number of images in the file. If we are reading the train-images.idx3-ubyte file, this value shall read 60,000.
  • Third and Four 4-bytes represent the image dimension, number of rows and number of columns respectively. In our case, this is 28 for both rows and columns.

Here is a sample code and its output of one way to read this data:

var inputFile = "train-images.idx3-ubyte";
using (var inputStream = File.OpenRead(inputFile))
{
	byte[] buffer = new byte[4];
	var bytesRead = inputStream.Read(buffer, 0, 4);
	var magicNumber = SwapEndianness(BitConverter.ToInt32(buffer, 0));
	Console.WriteLine($"Magic number: {magicNumber}");


	bytesRead = inputStream.Read(buffer, 0, 4);
	var numberOfImages = SwapEndianness(BitConverter.ToInt32(buffer, 0));
	Console.WriteLine($"Number of images: {numberOfImages}");

	bytesRead = inputStream.Read(buffer, 0, 4);
	var numberOfRows = SwapEndianness(BitConverter.ToInt32(buffer, 0));
	bytesRead = inputStream.Read(buffer, 0, 4);
	var numberOfColumns = SwapEndianness(BitConverter.ToInt32(buffer, 0));
	Console.WriteLine($"Image size: {numberOfRows}x{numberOfColumns}");
}

The output from the code above is:

Magic number: 2051
Number of images: 60000
Image size: 28×28

Note that Clounce ran his code on an Intel machine, and thus, he had to swap the endianness of the byte buffer being read. This was done by bit-shifting as in the code below:

private static int SwapEndianness(int value)
{
	var b1 = (value >> 0) & 0xff;
	var b2 = (value >> 8) & 0xff;
	var b3 = (value >> 16) & 0xff;
	var b4 = (value >> 24) & 0xff;

	return b1 << 24 | b2 << 16 | b3 << 8 | b4 << 0;
}

Grey-scale to ASCII

The last step is to read the data from MNSIT and display it on a console window. Each cell/pixel in MNIST is given a value from 0 (white) to 255 (black). Clounce used an ASCII art scale from http://paulbourke.net/dataformats/asciiart/:

// http://paulbourke.net/dataformats/asciiart/
// black -> white
private static readonly string greyScale = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\" ^`'. ";

The he implemented a method to convert a byte value to an index in the greyScale string.

private static char GetCharFromGreyScale(byte value)
{
   var valueComplement = 255 - value;
   return greyScale[Math.Max(0, Convert.ToInt32(Math.Round(greyScale.Length / 255.0 * valueComplement)) - 1)];
}

Note that Clounce had to complement the value being drawn. This is needed because a value of 0 in MNIST means white, whereas, the greyScale[0] is black. Alternately, Clounce could have reversed the greyScale string, but he decided to leave it as is to make it more fun 🙂

Next, Clounce added a loop to iterate through all images and display them in a console window. The loop is simple:

buffer = new byte[numberOfColumns];

for (int i = 0; i < numberOfImages; i++)
{
  // read images
  for (int j = 0; j < numberOfRows; j++)
  {
    bytesRead = inputStream.Read(buffer, 0, numberOfColumns);
    Console.WriteLine(Print(buffer));
  }
}

Below are a couple of images:

        <Xh$$t{X<
       {&$$qh$$$Q
     .~%$q_.`m$$f
     ($$m^  )$$M"
    u$$X^   o$$[
   x$@O`  '[B$n
  ^&$O   Ib$$Q`
  "$$:"jd%$$$]
  ^W$$$$$MM$W
   <&$ov!,q$z
         +$$/
         ?$$"
          $$"
         ?$$"
         /$$"
         i$$"
          *$~
          ($q!
          ^m$Q`
           ^X$!
       l!\$$$$$$0'
     !c#M$$$$$$$$J^
     Z$$$$$$$$$$$$~
     /$$Wuuuuq$$$$~
     .::,    ^M$$0'
             {B$$z
            1q$$$^
          1a@$$$k`
     ;xppp$$$$8|:
    i#$$$$$$$$O
    i#$$$$$$$$$]
     ;r>!!!!z$$]
            ^$$]
            {$$]
  .]`      (%$$]
 +m$:     )%$$B_
 h$$Xuuuuk$$$w}
 h$$$$$$$$$WU'
 >J$$$$$ohXi
  '\&$0\,
            l#M
           X&$O
         "d$$h
        lZ$$j_
        ~$$$1
       l#$$[
       n$Z:
      dB*I
     [$$v:
    ^c$$j    tcc
    ^q$$~  ft#$$$:
    ]$$#I i0$$%o$d;
   ^/$Mn lL$&L>_$o;
   ?$$x "Z$%X'!o%X
   j$$  d$$_ ~$$c
   v$0 iW$t l#$t
   v$0 iM$JM&b:`
   ~$$~\%$$$&]
   >%$$$$$$%X
    ]q$$$Jt_

Pretty cool, ay! The full source is available on github at https://github.com/jod75/mnistparser. Enjoy!