MNISTの手書き数字画像データをJavaでベクトル化、画像化する

はじめに[MNISTの手書き数字画像データ](http://yann.lecun.com/exdb/mnist/)の仕様を説明した後、ベクトル化したり、画像として表示･保存する[Javaのコード](https://github.com/kinmojr/MNIST_Java)を説明します。

# MNISTの手書き数字画像データセットとは
ネットに公開されている0から9までの手書き数字画像データセットです。
個々の画像の大きさは28ピクセル四方で、色は255階調のグレースケールです。
機械学習の学習および評価によく利用されています。

# データ仕様
MNISTの手書き数字データの仕様について説明します。

## 訓練データセット
訓練(学習)に用いる0から9までの6万枚の画像データと正解ラベルデータがあります。

### 訓練用画像データ
[train-images-idx3-ubyte.gz](http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz)
訓練用画像データが独自のバイナリフォーマットで格納されています。

| オフセット | データ型 | 値 | 説明 |
|:-:|:-:|:-:|:-:|
| 0 | 32bit整数 | 2051 | マジックナンバー(MSB First) |
| 4 | 32bit整数 | 60000 | 画像数 |
| 8 | 32bit整数 | 28 | 画像の縦のピクセル数 |
| 12 | 32bit整数 | 28 | 画像の横のピクセル数 |
| 16 | 符号なしバイト | 0から255 | 1枚目の画像の1行1列目のピクセルのグレースケール値 |
| 17 | 符号なしバイト | 0から255 | 1枚目の画像の1行2列目のピクセルのグレースケール値 |
| ･･･  | 符号なしバイト | 0から255 | 6万枚目の画像の28行目28列目のピクセルのグレースケール値 |

### 訓練用正解ラベルデータ
[train-labels-idx1-ubyte.gz](http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz)
訓練データの正解ラベルが独自のバイナリフォーマットで格納されています。


| オフセット | データ型 | 値 | 説明 |
|:-:|:-:|:-:|:-:|
| 0 | 32bit整数 | 2049 | マジックナンバー(MSB First) |
| 4 | 32bit整数 | 60000 | 画像数 |
| 8 | 符号なしバイト | 0から9 | 1枚目の画像の正解ラベル |
| 9 | 符号なしバイト | 0から9 | 2枚目の画像の正解ラベル |
| ･･･  | 符号なしバイト | 0から9 | 6万枚目の画像の正解ラベル |

## テストデータセット
テスト(評価)に用いる0から9までの1万枚の画像データと正解ラベルデータがあります。

### テスト用画像データ
[t10k-images-idx3-ubyte.gz](http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz)
テスト用画像データが独自のバイナリフォーマットで格納されています。

| オフセット | データ型 | 値 | 説明 |
|:-:|:-:|:-:|:-:|
| 0 | 32bit整数 | 2051 | マジックナンバー(MSB First) |
| 4 | 32bit整数 | 10000 | 画像数 |
| 8 | 32bit整数 | 28 | 画像の縦のピクセル数 |
| 12 | 32bit整数 | 28 | 画像の横のピクセル数 |
| 16 | 符号なしバイト | 0から255 | 1枚目の画像の1行1列目のピクセルのグレースケール値 |
| 17 | 符号なしバイト | 0から255 | 1枚目の画像の1行2列目のピクセルのグレースケール値 |
| ･･･  | 符号なしバイト | 0から255 | 1万枚目の画像の28行目28列目のピクセルのグレースケール値 |

### テスト用正解ラベル
[t10k-labels-idx1-ubyte.gz](http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz)
テストデータの正解ラベルが独自のバイナリフォーマットで格納されています。

| オフセット | データ型 | 値 | 説明 |
|:-:|:-:|:-:|:-:|
| 0 | 32bit整数 | 2049 | マジックナンバー(MSB First) |
| 4 | 32bit整数 | 10000 | 画像数 |
| 8 | 符号なしバイト | 0から9 | 1枚目の画像の正解ラベル |
| 9 | 符号なしバイト | 0から9 | 2枚目の画像の正解ラベル |
| ･･･  | 符号なしバイト | 0から9 | 1万枚目の画像の正解ラベル |

# Javaによるベクトル化、画像化
[GitHub上のソースコード](https://github.com/kinmojr/MNIST_Java)
簡単な使い方は[README.md](https://github.com/kinmojr/MNIST_Java)をご覧ください。

## 画像データの読み込み
DataInputStreamを使ってデータを読み込みます。
readIntでマジックナンバー、画像数、画像の縦のピクセル数、画像の横のピクセル数を読み込みます。
次元数は28*28=784になります。
double型の2次元配列featuresにreadUnsignedByteに画像データを読み込みます。
1次元目が画像のインデックスで、2次元目が次元のインデックスです。
機械学習での利用を考慮して、値を255.0で割って正規化(normalize)しています。

```java:ImageDataSet.java
    private void loadFeatures() throws IOException {
        System.out.println("Loading feature data from " + fileName + " ...");
        DataInputStream is = new DataInputStream(new GZIPInputStream(new FileInputStream(Const.BASE_PATH + fileName)));
        is.readInt();
        numImages = is.readInt();
        numDimensions = is.readInt() * is.readInt();

        features = new double[numImages][numDimensions];
        for (int i = 0; i < numImages; i++) {
            for (int j = 0; j < numDimensions; j++) {
                features[i][j] = (double) is.readUnsignedByte() / 255.0;
            }
        }
    }
```

## ラベルデータの読み込み
DataInputStreamを使ってデータを読み込みます。
readIntでマジックナンバー、画像数を読み込みます。
int型の配列labelsにreadUnsignedByteで正解ラベルを読み込みます。

```java:LabelDataSet.java
    private void loadLabels() throws IOException {
        System.out.println("Loading label data from " + fileName + " ...");
        DataInputStream is = new DataInputStream(new GZIPInputStream(new FileInputStream(Const.BASE_PATH + fileName)));

        is.readInt();
        numLabels = is.readInt();

        labels = new int[numLabels];
        for (int i = 0; i < numLabels; i++) {
            labels[i] = is.readUnsignedByte();
        }
    }
```

## 画像の表示
### テキストで表示
引数で画像のインデックスを指定して、コンソールに画像の概形をテキストで表示します。

```java:ImageViewer.java
    public void showImageAsText(int index) {
        System.out.println("Label: " + labels[index]);
        for (int i = 0; i < 28; i++) {
            for (int j = 0; j < 28; j++) {
                double value = images[index][i * 28 + j];
                if (value > 0.0) {
                    System.out.print("**");
                } else {
                    System.out.print("  ");
                }
            }
            System.out.println();
        }
    }
```

### BufferedImageを作成
ベクトル化された画像データを復元して、BufferedImageを作成します。
値は正規化されているので、255.0を掛けて元のグレースケール値に戻します。

```java:ImageViewer.java
    private BufferedImage makeImage(int index) {
        BufferedImage image =
                new BufferedImage(28, 28, BufferedImage.TYPE_INT_RGB);

        for (int i = 0; i < 28; i++) {
            for (int j = 0; j < 28; j++) {
                int value = (int) (images[index][i * 28 + j] * 255.0);
                image.setRGB(j, i, 0xff000000 | value << 16 | value << 8 | value);
            }
        }

        return image;
    }
```

### ダイアログに表示
makeImageで読み込んだBufferedImageをダイアログに表示します。


```java:ImageViewer.java
    public void showImage(int index) {
        BufferedImage image = makeImage(index);
        Icon icon = new ImageIcon(image);
        JOptionPane.showMessageDialog(null, labels[index], "MnistImageViewer", JOptionPane.PLAIN_MESSAGE, icon);
    }
```

### 画像ファイルに保存
makeImageで読み込んだBufferedImageをgifファイルに保存します。

```java:ImageViewer.java
    public void saveImage(String dir, String prefix, int index) throws IOException {
        BufferedImage image = makeImage(index);
        File file = new File(dir + "/" + prefix + "_" + String.format("%05d", index) + "_" + labels[index] + ".gif");
        if (file.exists()) file.delete();
        ImageIO.write(image, "gif", file);
    }
```