Πώς να χειριστείτε τα δεδομένα εικόνας MNIST στο Tensorflow.js

Υπάρχει το αστείο ότι το 80 τοις εκατό της επιστήμης των δεδομένων καθαρίζει τα δεδομένα και το 20 τοις εκατό διαμαρτύρεται για τον καθαρισμό των δεδομένων ... ο καθαρισμός δεδομένων είναι ένα πολύ υψηλότερο ποσοστό της επιστήμης των δεδομένων από ό, τι θα περίμενε ένας ξένος. Στην πραγματικότητα, τα μοντέλα εκπαίδευσης είναι συνήθως ένα σχετικά μικρό ποσοστό (λιγότερο από 10 τοις εκατό) από αυτό που κάνει κάποιος μαθητής μηχανής ή επιστήμονας δεδομένων.

 - Anthony Goldbloom, Διευθύνων Σύμβουλος της Kaggle

Η επεξεργασία δεδομένων είναι ένα κρίσιμο βήμα για οποιοδήποτε πρόβλημα μάθησης μηχανών. Αυτό το άρθρο θα λάβει το παράδειγμα MNIST για το Tensorflow.js (0.11.1) και θα περπατήσει μέσα από τον κώδικα που χειρίζεται τη φόρτωση δεδομένων γραμμής με γραμμή.

Παράδειγμα MNIST

18 εισαγωγή * ως tf από '@ 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' · '

Πρώτον, ο κώδικας εισάγει Tensorflow (βεβαιωθείτε ότι είστε transpiling κώδικα σας!), Και καθορίζει ορισμένες σταθερές, όπως:

  • IMAGE_SIZE - το μέγεθος μιας εικόνας (πλάτος και ύψος 28x28 = 784)
  • NUM_CLASSES - αριθμός κατηγοριών ετικετών (ένας αριθμός μπορεί να είναι 0-9, οπότε υπάρχουν 10 κατηγορίες)
  • NUM_DATASET_ELEMENTS - αριθμός συνολικών εικόνων (65.000)
  • NUM_TRAIN_ELEMENTS - αριθμός εικόνων εκπαίδευσης (55.000)
  • NUM_TEST_ELEMENTS - αριθμός εικόνων δοκιμής (10.000, οπότε και το υπόλοιπο)
  • MNIST_IMAGES_SPRITE_PATH & MNIST_LABELS_PATH - διαδρομές στις εικόνες και τις ετικέτες

Οι εικόνες συνενώνονται σε μια τεράστια εικόνα που μοιάζει με:

MNISTData

Στη συνέχεια, ξεκινώντας από τη γραμμή 38, είναι MnistData, μια κατηγορία που εκθέτει τις ακόλουθες λειτουργίες:

  • φορτίο - υπεύθυνο για την ασύγχρονη φόρτωση των δεδομένων εικόνας και επισήμανσης
  • nextTrainBatch - φορτώστε την επόμενη παρτίδα εκπαίδευσης
  • nextTestBatch - φορτώστε την επόμενη παρτίδα δοκιμής
  • nextBatch - μια γενική συνάρτηση για την επιστροφή της επόμενης παρτίδας, ανάλογα με το αν βρίσκεται στο σετ εκπαίδευσης ή στη δοκιμαστική ομάδα

Για τους σκοπούς της έναρξης, αυτό το άρθρο θα περάσει μόνο από τη λειτουργία φόρτωσης.

φορτώνω

44 φόρτιση async () {
45 // Δημιουργήστε ένα αίτημα για την εικόνα MNIST sprit.
46 const img = νέα Εικόνα ();
47 const canvas = document.createElement ('καμβάς');
48 const ctx = canvas.getContext ('2d');

Το async είναι ένα σχετικά νέο χαρακτηριστικό γλώσσας στο Javascript για το οποίο θα χρειαστείτε ένα transpiler.

Το αντικείμενο Εικόνα είναι μια εγγενής λειτουργία DOM που αντιπροσωπεύει μια εικόνα στη μνήμη. Παρέχει επανάκλησεις για τη φόρτωση της εικόνας, με πρόσβαση στα χαρακτηριστικά της εικόνας. ο καμβάς είναι ένα άλλο στοιχείο DOM που παρέχει εύκολη πρόσβαση σε συστοιχίες pixel και επεξεργασία μέσω πλαισίου.

Δεδομένου ότι και οι δύο είναι στοιχεία DOM, αν εργάζεστε σε Node.js (ή Web Worker), δεν θα έχετε πρόσβαση σε αυτά τα στοιχεία. Για μια εναλλακτική προσέγγιση, βλ. Παρακάτω.

imgRequest

49 const imgRequest = new Promise ((επίλυση, απόρριψη) => {
50 img.crossOrigin = '';
51 img.onload = () => {
52 img.width = img.naturalWidth;
53 img.height = img.naturalHeight;

Ο κώδικας προετοιμάζει μια νέα υπόσχεση που θα επιλυθεί μόλις ολοκληρωθεί η φόρτωση της εικόνας με επιτυχία. Αυτό το παράδειγμα δεν χειρίζεται ρητά την κατάσταση σφάλματος.

Το crossOrigin είναι ένα χαρακτηριστικό γνώρισμα img που επιτρέπει τη φόρτωση εικόνων σε όλους τους τομείς και παίρνει γύρω από τα ζητήματα CORS (κοινή χρήση πόρων) όταν αλληλεπιδρά με το DOM. το naturalWidth και το NaturalHeight αναφέρονται στις αρχικές διαστάσεις της φορτωμένης εικόνας και χρησιμεύουν για την επιβολή του σωστού μεγέθους της εικόνας κατά την εκτέλεση των υπολογισμών.

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

Ο κώδικας προετοιμάζει ένα νέο buffer για να περιέχει κάθε εικονοστοιχείο κάθε εικόνας. Πολλαπλασιάζει τον συνολικό αριθμό των εικόνων κατά το μέγεθος κάθε εικόνας κατά τον αριθμό των καναλιών (4).

Πιστεύω ότι το chunkSize χρησιμοποιείται για να αποτρέψει το UI να φορτώσει πάρα πολύ δεδομένα στη μνήμη ταυτόχρονα, αν και δεν είμαι 100% σίγουρος.

62 για (ας i = 0; i 

Αυτός ο κώδικας περνάει μέσα από κάθε εικόνα του sprite και αρχικοποιεί ένα νέο TypedArray για αυτή την επανάληψη. Στη συνέχεια, η εικόνα του περιβάλλοντος παίρνει ένα κομμάτι της εικόνας που έχει σχεδιαστεί. Τέλος, αυτή η εικόνα που έχει τραβηχτεί μετατρέπεται σε δεδομένα εικόνας χρησιμοποιώντας τη συνάρτηση getImageData του πλαισίου, η οποία επιστρέφει ένα αντικείμενο που αντιπροσωπεύει τα υποκείμενα δεδομένα εικονοστοιχείων.

72 για (let j = 0, j 

Βγάζουμε τα pixel και διαιρούμε με 255 (τη μέγιστη δυνατή τιμή ενός εικονοστοιχείου) για να σταθεροποιήσουμε τις τιμές μεταξύ 0 και 1. Απαιτείται μόνο το κόκκινο κανάλι, αφού είναι μια εικόνα σε κλίμακα του γκρι.

78 this.datasetImages = νέο Float32Array (datasetBytesBuffer);
79
80 επίλυση ();
81}.
82 img.src = MNIST_IMAGES_SPRITE_PATH;
83}).

Αυτή η γραμμή παίρνει το buffer, μετασχηματίζει το σε ένα νέο TypedArray που κρατά τα δεδομένα των εικονοστοιχείων μας, και στη συνέχεια επιλύει την υπόσχεση. Η τελευταία γραμμή (ρύθμιση του src) αρχίζει να φορτώνει την εικόνα, η οποία ξεκινά τη λειτουργία.

Ένα πράγμα που με συγχέεται κατά την πρώτη ήταν η συμπεριφορά του TypedArray σε σχέση με το υποκείμενο buffer δεδομένων. Ίσως παρατηρήσετε ότι το datasetBytesView έχει οριστεί εντός του βρόχου, αλλά δεν επιστρέφεται ποτέ.

Κάτω από την κουκούλα, το datasetBytesView αναφέρεται στο buffer datasetBytesBuffer (με το οποίο αρχικοποιείται). Όταν ο κώδικας ενημερώνει τα δεδομένα εικονοστοιχείων, διορθώνει έμμεσα τις τιμές του ίδιου του buffer, ο οποίος με τη σειρά του αναδιαμορφώνεται σε ένα νέο Float32Array στη γραμμή 78.

Ανάκτηση δεδομένων εικόνας εκτός του DOM

Εάν βρίσκεστε στο DOM, θα πρέπει να χρησιμοποιήσετε το DOM. Το πρόγραμμα περιήγησης (μέσω του καμβά) φροντίζει να υπολογίζει τη μορφή των εικόνων και να μεταφράζει τα δεδομένα του buffer σε εικονοστοιχεία. Αλλά αν εργάζεστε εκτός του DOM (π.χ. στο Node.js ή σε έναν Web Worker), θα χρειαστείτε μια εναλλακτική προσέγγιση.

fetch παρέχει ένα μηχανισμό, response.arrayBuffer, που σας δίνει πρόσβαση σε υποκείμενο buffer του αρχείου. Μπορούμε να χρησιμοποιήσουμε αυτό για να διαβάσουμε τα bytes με το χέρι, αποφεύγοντας εξ ολοκλήρου το DOM. Ακολουθεί μια εναλλακτική προσέγγιση για την εγγραφή του παραπάνω κώδικα (αυτός ο κώδικας απαιτεί την ανάκτηση, η οποία μπορεί να συμπληρωθεί στον κόμβο με κάτι παρόμοιο με την ισομορφορική φόρτωση):

const = imetRequest = fetch (MNIST_IMAGES_SPRITE_PATH) .then (resp => resp.arrayBuffer ()) τότε (buffer => {
  επιστροφή νέας υπόσχεσης (επίλυση => {
    const reader = νέος PNGReader (buffer).
    επιστροφή reader.parse ((err, png) => {
      const pixels = Float32Array.from (png.pixels) .map (pixel => {
        pixel επιστροφής / 255;
      });
      this.datasetImages = pixels;
      αποφασίζω();
    });
  });
});

Αυτό επιστρέφει ένα ρυθμιστικό πίνακα για τη συγκεκριμένη εικόνα. Όταν έγραψα αυτό, προσπάθησα πρώτα να αναλύσω τον ίδιο τον εισερχόμενο ρυθμιστή, τον οποίο δεν θα πρότεινα. (Αν ενδιαφέρεστε να το κάνετε αυτό, εδώ είναι μερικές πληροφορίες για το πώς να διαβάσετε ένα ρυθμιστικό πίνακα για ένα png.) Αντ 'αυτού, επέλεξα να χρησιμοποιήσω το pngjs, το οποίο χειρίζεται την ανάλυση png για εσάς. Όταν ασχολείσαι με άλλες μορφές εικόνας, θα πρέπει να υπολογίσεις τις λειτουργίες σύνταξης μόνος σου.

Απλά ξύστε την επιφάνεια

Η κατανόηση του χειρισμού των δεδομένων είναι ένα κρίσιμο στοιχείο της μηχανικής μάθησης στο JavaScript. Με την κατανόηση των περιπτώσεων χρήσης και των απαιτήσεων μας, μπορούμε να χρησιμοποιήσουμε μερικές βασικές λειτουργίες για να μορφοποιήσουμε κομψά τα δεδομένα μας σωστά για τις ανάγκες μας.

Η ομάδα Tensorflow.js αλλάζει διαρκώς το υποκείμενο API δεδομένων στο Tensorflow.js. Αυτό μπορεί να βοηθήσει να ικανοποιήσει περισσότερες από τις ανάγκες μας καθώς το API εξελίσσεται. Αυτό σημαίνει επίσης ότι αξίζει να παραμείνετε ενήμεροι για τις εξελίξεις στο API, καθώς το Tensorflow.js συνεχίζει να αναπτύσσεται και να βελτιώνεται.

Αρχικά δημοσιεύθηκε στο thekevinscott.com

Ειδικές ευχαριστίες στον Ari Zilnik.