Bagaimana menangani data gambar MNIST di Tensorflow.js

Ada lelucon bahwa 80 persen ilmu data membersihkan data dan 20 persen mengeluhkan tentang pembersihan data ... pembersihan data adalah proporsi ilmu data yang jauh lebih tinggi daripada yang diperkirakan orang luar. Sebenarnya model pelatihan biasanya proporsi yang relatif kecil (kurang dari 10 persen) dari apa yang dilakukan oleh pelajar mesin atau ilmuwan data.

 - Anthony Goldbloom, CEO Kaggle

Memanipulasi data adalah langkah penting untuk masalah pembelajaran mesin. Artikel ini akan mengambil contoh MNIST untuk Tensorflow.js (0.11.1), dan berjalan melalui kode yang menangani pemuatan data baris demi baris.

Contoh MNIST

18 impor * sebagai tf dari '@ tensorflow / tfjs';
19
20 const IMAGE_SIZE = 784;
21 const NUM_CLASSES = 10;
22 const NUM_DATASET_ELEMENTS = 65000;
23
24 const NUM_TRAIN_ELEMENTS = 55000;
25 const NUM_TEST_ELEMENTS = NUM_DATASET_ELEMENTS - NUM_TRAIN_ELEMENTS;
26
27 const MNIST_IMAGES_SPRITE_PATH =
28 'https://storage.googleapis.com/learnjs-data/model-builder/mnist_images.png';
29 const MNIST_LABELS_PATH =
30 'https: //storage.googleapis.com/learnjs-data/model-builder/mnist_labels_uint8'; `

Pertama, kode ini mengimpor Tensorflow (pastikan Anda mengubah urutan kode Anda!), Dan menetapkan beberapa konstanta, termasuk:

  • IMAGE_SIZE - ukuran gambar (lebar dan tinggi 28x28 = 784)
  • NUM_CLASSES - jumlah kategori label (angka bisa 0-9, jadi ada 10 kelas)
  • NUM_DATASET_ELEMENTS - jumlah total gambar (65.000)
  • NUM_TRAIN_ELEMENTS - jumlah gambar pelatihan (55.000)
  • NUM_TEST_ELEMENTS - jumlah gambar uji (10.000, alias sisanya)
  • MNIST_IMAGES_SPRITE_PATH & MNIST_LABELS_PATH - jalur ke gambar dan label

Gambar digabungkan menjadi satu gambar besar yang terlihat seperti:

MNISTData

Selanjutnya, mulai pada baris 38, adalah MnistData, kelas yang memperlihatkan fungsi-fungsi berikut:

  • load - bertanggung jawab atas pemuatan gambar dan pelabelan data secara tidak sinkron
  • nextTrainBatch - muat batch pelatihan berikutnya
  • nextTestBatch - muat batch tes berikutnya
  • nextBatch - fungsi generik untuk mengembalikan batch berikutnya, tergantung pada apakah itu di set pelatihan atau set tes

Untuk keperluan memulai, artikel ini hanya akan membahas fungsi memuat.

beban

44 async load () {
45 // Buat permintaan untuk gambar sprite MNIST.
46 const img = Gambar baru ();
47 const canvas = document.createElement ('canvas');
48 const ctx = canvas.getContext ('2d');

async adalah fitur bahasa yang relatif baru di Javascript yang membutuhkan transpiler.

Objek Gambar adalah fungsi DOM asli yang mewakili gambar dalam memori. Ini memberikan panggilan balik ketika gambar dimuat bersama, dengan akses ke atribut gambar. kanvas adalah elemen DOM lain yang menyediakan akses mudah ke susunan piksel dan pemrosesan melalui konteks.

Karena keduanya adalah elemen DOM, jika Anda bekerja di Node.js (atau Pekerja Web), Anda tidak akan memiliki akses ke elemen-elemen ini. Untuk pendekatan alternatif, lihat di bawah.

imgRequest

49 const imgRequest = Janji baru ((tekad, tolak) => {
50 img.crossOrigin = '';
51 img.onload = () => {
52 img.width = img.naturalWidth;
53 img.height = img.naturalHeight;

Kode ini menginisialisasi janji baru yang akan diselesaikan setelah gambar berhasil dimuat. Contoh ini tidak secara eksplisit menangani status kesalahan.

crossOrigin adalah atribut img yang memungkinkan pemuatan gambar di seluruh domain, dan mengatasi masalah CORS (cross-origin resource sharing) saat berinteraksi dengan DOM. naturalWidth dan naturalHeight merujuk ke dimensi asli dari gambar yang dimuat, dan berfungsi untuk memastikan bahwa ukuran gambar sudah benar saat melakukan perhitungan.

55 const datasetBytesBuffer =
56 ArrayBuffer baru (NUM_DATASET_ELEMENTS * IMAGE_SIZE * 4);
57
58 const chunkSize = 5000;
59 canvas.width = img.width;
60 canvas.height = chunkSize;

Kode menginisialisasi buffer baru untuk memuat setiap piksel dari setiap gambar. Ini mengalikan jumlah total gambar dengan ukuran setiap gambar dengan jumlah saluran (4).

Saya percaya bahwa chunkSize digunakan untuk mencegah UI memuat terlalu banyak data ke memori sekaligus, meskipun saya tidak 100% yakin.

62 untuk (mis. I = 0; i 

Kode ini memotong setiap gambar dalam sprite dan menginisialisasi TypedArray baru untuk iterasi itu. Kemudian, gambar konteks mendapat potongan dari gambar yang diambil. Akhirnya, gambar yang diambil diubah menjadi data gambar menggunakan fungsi getImageData konteks, yang mengembalikan objek yang mewakili data piksel yang mendasarinya.

72 untuk (misalkan j = 0; j 

Kami mengulang-ulang piksel, dan membaginya dengan 255 (nilai maksimum piksel yang mungkin) untuk menjepit nilai antara 0 dan 1. Hanya saluran merah yang diperlukan, karena ini adalah gambar skala abu-abu.

78 this.datasetImages = new Float32Array (datasetBytesBuffer);
79
80 menyelesaikan ();
81};
82 img.src = MNIST_IMAGES_SPRITE_PATH;
83});

Baris ini mengambil buffer, menyusunnya kembali menjadi TypedArray baru yang menampung data piksel kami, dan kemudian menyelesaikan Janji. Baris terakhir (pengaturan src) sebenarnya mulai memuat gambar, yang memulai fungsinya.

Satu hal yang membingungkan saya pada awalnya adalah perilaku TypedArray sehubungan dengan buffer data yang mendasarinya. Anda mungkin memperhatikan bahwa datasetBytesView diatur dalam loop, tetapi tidak pernah dikembalikan.

Di bawah tenda, datasetBytesView mereferensikan buffer datasetBytesBuffer (yang diinisialisasi). Ketika kode memperbarui data piksel, itu secara tidak langsung mengedit nilai-nilai buffer itu sendiri, yang pada gilirannya disusun kembali menjadi Float32Array baru pada baris 78.

Mengambil data gambar di luar DOM

Jika Anda berada di DOM, Anda harus menggunakan DOM. Peramban (melalui kanvas) mencari tahu format gambar dan menerjemahkan data buffer ke dalam piksel. Tetapi jika Anda bekerja di luar DOM (katakanlah, di Node.js, atau Pekerja Web), Anda akan memerlukan pendekatan alternatif.

fetch menyediakan mekanisme, response.arrayBuffer, yang memberi Anda akses ke buffer file yang mendasarinya. Kita dapat menggunakan ini untuk membaca byte secara manual, menghindari DOM sepenuhnya. Berikut adalah pendekatan alternatif untuk menulis kode di atas (kode ini membutuhkan fetch, yang dapat di-polyfilled di Node dengan sesuatu seperti isomorphic-fetch):

const imgRequest = fetch (MNIST_IMAGES_SPRITE_PATH). lalu (resp => resp.arrayBuffer ()) lalu (buffer => {
  kembalikan Janji baru (tekad => {
    const reader = PNGReader baru (buffer);
    return reader.parse ((err, png) => {
      const pixel = Float32Array.from (png.pixels) .map (pixel => {
        return pixel / 255;
      });
      this.datasetImages = piksel;
      menyelesaikan();
    });
  });
});

Ini mengembalikan buffer array untuk gambar tertentu. Saat menulis ini, saya pertama-tama mencoba mengurai buffer yang masuk, yang tidak akan saya rekomendasikan. (Jika Anda tertarik untuk melakukan itu, inilah beberapa informasi tentang cara membaca buffer array untuk png.) Sebagai gantinya, saya memilih untuk menggunakan pngjs, yang menangani png parsing untuk Anda. Saat berurusan dengan format gambar lain, Anda harus mengetahui sendiri fungsi parsing.

Hanya menggaruk permukaan

Memahami manipulasi data adalah komponen penting dari pembelajaran mesin di JavaScript. Dengan memahami kasus dan persyaratan penggunaan kami, kami dapat menggunakan beberapa fungsi utama untuk memformat data kami dengan benar untuk kebutuhan kami.

Tim Tensorflow.js terus mengubah API data yang mendasarinya di Tensorflow.js. Ini dapat membantu mengakomodasi lebih banyak kebutuhan kita seiring dengan perkembangan API. Ini juga berarti bahwa ada baiknya mengikuti perkembangan ke API karena Tensorflow.js terus tumbuh dan ditingkatkan.

Awalnya diterbitkan di thekevinscott.com

Terima kasih khusus kepada Ari Zilnik.