วิธีจัดการกับข้อมูลภาพ MNIST ใน Tensorflow.js

มีเรื่องตลกที่ร้อยละ 80 ของวิทยาศาสตร์ข้อมูลกำลังล้างข้อมูลและร้อยละ 20 บ่นเกี่ยวกับการล้างข้อมูล ... การล้างข้อมูลเป็นสัดส่วนของวิทยาศาสตร์ข้อมูลที่สูงกว่าที่คนนอกคาดหวัง ที่จริงแล้วแบบจำลองการฝึกอบรมมักจะมีสัดส่วนที่ค่อนข้างเล็ก (น้อยกว่า 10 เปอร์เซ็นต์) ของสิ่งที่ผู้เรียนรู้เครื่องจักรหรือนักวิทยาศาสตร์ด้านข้อมูลทำ

 - Anthony Goldbloom ซีอีโอของ Kaggle

การจัดการข้อมูลเป็นขั้นตอนสำคัญสำหรับปัญหาการเรียนรู้ของเครื่อง บทความนี้จะใช้ตัวอย่าง MNIST สำหรับ Tensorflow.js (0.11.1) และดำเนินการตามรหัสที่จัดการกับการโหลดข้อมูลแบบทีละบรรทัด

ตัวอย่าง MNIST

18 import * as 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 (ตรวจสอบให้แน่ใจว่าคุณกำลังเปลี่ยนรหัสของคุณ!) และสร้างค่าคงที่บางอย่างรวมถึง:

  • 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 คลาสที่แสดงฟังก์ชันต่อไปนี้:

  • load - รับผิดชอบการโหลดข้อมูลรูปภาพและการติดฉลากแบบอะซิงโครนัส
  • nextTrainBatch - โหลดชุดฝึกอบรมถัดไป
  • nextTestBatch - โหลดชุดทดสอบถัดไป
  • nextBatch - ฟังก์ชั่นทั่วไปเพื่อส่งคืนชุดถัดไปขึ้นอยู่กับว่าอยู่ในชุดฝึกอบรมหรือชุดทดสอบ

สำหรับวัตถุประสงค์ในการเริ่มต้นบทความนี้จะผ่านฟังก์ชั่นโหลดเท่านั้น

ภาระ

โหลด async 44 () {
45 // ให้ทำการร้องขอสำหรับภาพสปีดของ MNIST
46 const img = รูปภาพใหม่ ();
47 const canvas = document.createElement ('canvas');
48 const ctx = canvas.getContext ('2d');

async เป็นคุณสมบัติภาษาที่ค่อนข้างใหม่ใน Javascript ซึ่งคุณจะต้องมี transpiler

วัตถุรูปเป็นฟังก์ชั่น DOM พื้นเมืองที่แสดงถึงภาพในหน่วยความจำ มันให้โทรกลับเมื่อมีการโหลดภาพพร้อมกับการเข้าถึงคุณลักษณะภาพ canvas เป็นองค์ประกอบ DOM อื่นที่ให้การเข้าถึงอาร์เรย์พิกเซลและการประมวลผลตามบริบทได้ง่าย

เนื่องจากทั้งสองอย่างนี้เป็นองค์ประกอบ DOM หากคุณทำงานใน Node.js (หรือผู้ทำงานเว็บ) คุณจะไม่สามารถเข้าถึงองค์ประกอบเหล่านี้ได้ สำหรับแนวทางอื่นดูด้านล่าง

imgRequest

49 const imgRequest = สัญญาใหม่ ((แก้ไข, ปฏิเสธ) => {
50 img.crossOrigin = '';
51 img.onload = () => {
52 img.width = img.naturalWidth;
53 img.height = img.naturalHeight;

รหัสเริ่มต้นสัญญาใหม่ที่จะได้รับการแก้ไขเมื่อโหลดภาพสำเร็จแล้ว ตัวอย่างนี้ไม่ได้จัดการสถานะข้อผิดพลาดอย่างชัดเจน

crossOrigin เป็นคุณลักษณะ img ที่อนุญาตให้โหลดภาพข้ามโดเมนและแก้ไขปัญหา CORS (การแบ่งปันทรัพยากรข้ามแหล่ง) เมื่อมีปฏิสัมพันธ์กับ DOM naturalWidth และ naturalHeight อ้างอิงถึงขนาดดั้งเดิมของรูปภาพที่โหลดและใช้เพื่อบังคับใช้ว่าขนาดของรูปภาพนั้นถูกต้องเมื่อทำการคำนวณ

ชุดข้อมูล 55 constBytesBuffer =
56 ArrayBuffer ใหม่ (NUM_DATASET_ELEMENTS * IMAGE_SIZE * 4);
57
58 const chizeSize = 5,000;
59 canvas.width = img.width;
60 canvas.height = อันธพาลขนาด;

รหัสเริ่มต้นบัฟเฟอร์ใหม่เพื่อให้มีทุกพิกเซลของทุกภาพ มันคูณจำนวนภาพทั้งหมดด้วยขนาดของแต่ละภาพด้วยจำนวนช่อง (4)

ฉันเชื่อว่า chunkSize ใช้เพื่อป้องกัน UI จากการโหลดข้อมูลมากเกินไปในหน่วยความจำในครั้งเดียว แต่ฉันไม่แน่ใจ 100%

62 สำหรับ (ให้ฉัน = 0; i 

รหัสนี้วนซ้ำทุกภาพใน sprite และเริ่มต้น TypedArray ใหม่สำหรับการทำซ้ำนั้น จากนั้นภาพบริบทจะได้รับการวาดภาพ ในที่สุดรูปภาพที่วาดนั้นจะเปลี่ยนเป็นข้อมูลภาพโดยใช้ฟังก์ชัน getImageData ของบริบทซึ่งส่งคืนวัตถุที่แสดงข้อมูลพิกเซลพื้นฐาน

72 สำหรับ (ให้ j = 0; j 

เราวนผ่านพิกเซลและหารด้วย 255 (ค่าสูงสุดที่เป็นไปได้ของพิกเซล) เพื่อยึดค่าระหว่าง 0 และ 1 เฉพาะช่องสีแดงเท่านั้นที่จำเป็นเนื่องจากเป็นรูปภาพระดับสีเทา

78 this.datasetImages = new Float32Array (datasetBytesBuffer)
79
80 การแก้ไข ();
81};
82 img.src = MNIST_IMAGES_SPRITE_PATH;
83});

บรรทัดนี้จะใช้บัฟเฟอร์บัฟเฟอร์ใหม่เข้าไปใน TypedArray ใหม่ที่เก็บข้อมูลพิกเซลของเราแล้วแก้ไขสัญญา บรรทัดสุดท้าย (การตั้งค่า src) จริง ๆ แล้วเริ่มการโหลดรูปภาพซึ่งจะเริ่มการทำงาน

สิ่งหนึ่งที่ทำให้ฉันสับสนในตอนแรกคือพฤติกรรมของ TypedArray ที่เกี่ยวข้องกับบัฟเฟอร์ข้อมูลพื้นฐาน คุณอาจสังเกตว่า datasetBytesView ถูกตั้งค่าภายในลูป แต่จะไม่ส่งคืน

ภายใต้ประทุนนั้น datasetBytesView กำลังอ้างถึงบัฟเฟอร์ datasetBytesBuffer (ซึ่งเป็นค่าเริ่มต้น) เมื่อโค้ดอัพเดทข้อมูลพิกเซลมันจะทำการแก้ไขค่าของบัฟเฟอร์โดยตรงโดยทางอ้อมซึ่งจะถูกเปลี่ยนเป็นโฟลท 32Array ใหม่ในบรรทัดที่ 78

กำลังดึงข้อมูลภาพนอก DOM

หากคุณอยู่ใน DOM คุณควรใช้ DOM เบราว์เซอร์ (ผ่านพื้นที่วาดภาพ) จะดูแลการกำหนดรูปแบบของรูปภาพและการแปลข้อมูลบัฟเฟอร์เป็นพิกเซล แต่ถ้าคุณทำงานนอก DOM (พูดใน Node.js หรือ Web Worker) คุณจะต้องมีวิธีการอื่น

fetch มีกลไกคือ response.arrayBuffer ซึ่งให้สิทธิ์การเข้าถึงบัฟเฟอร์พื้นฐานของไฟล์ เราสามารถใช้สิ่งนี้เพื่ออ่านไบต์ด้วยตนเองโดยหลีกเลี่ยง DOM ทั้งหมด ต่อไปนี้เป็นวิธีการอื่นในการเขียนโค้ดด้านบน (รหัสนี้ต้องใช้การดึงข้อมูลซึ่งสามารถ polyfilled ในโหนดที่มีบางอย่างคล้าย isomorphic-fetch):

const imgRequest = fetch (MNIST_IMAGES_SPRITE_PATH) .then (resp => resp.arrayBuffer ()) จากนั้น (buffer => {
  ส่งคืนสัญญาใหม่ (แก้ไข => {
    const reader = PNGReader ใหม่ (บัฟเฟอร์);
    return reader.parse ((err, png) => {
      const pixels = Float32Array.from (png.pixels) .map (pixel => {
        ผลตอบแทนพิกเซล / 255;
      });
      this.datasetImages = พิกเซล;
      แก้ไข ();
    });
  });
});

สิ่งนี้จะคืนค่าบัฟเฟอร์ของอาร์เรย์สำหรับรูปภาพเฉพาะ เมื่อเขียนสิ่งนี้ฉันพยายามแยกบัฟเฟอร์ที่เข้ามาด้วยตัวเองก่อนซึ่งฉันจะไม่แนะนำ (หากคุณสนใจที่จะทำเช่นนั้นต่อไปนี้เป็นข้อมูลเกี่ยวกับวิธีการอ่านอาร์เรย์บัฟเฟอร์สำหรับ png) แต่ฉันเลือกที่จะใช้ pngjs ซึ่งจัดการการแยก png ให้คุณแทน เมื่อต้องรับมือกับรูปแบบภาพอื่น ๆ คุณจะต้องเข้าใจถึงการแยกวิเคราะห์ฟังก์ชันด้วยตัวคุณเอง

เพียงแค่เกาที่พื้นผิว

การทำความเข้าใจกับการจัดการข้อมูลเป็นองค์ประกอบสำคัญของการเรียนรู้ของเครื่องใน JavaScript โดยการทำความเข้าใจกรณีการใช้งานและข้อกำหนดของเราทำให้เราสามารถใช้ฟังก์ชันที่สำคัญสองสามประการเพื่อจัดรูปแบบข้อมูลของเราอย่างถูกต้องตามความต้องการของเรา

ทีม Tensorflow.js กำลังเปลี่ยนแปลง API ข้อมูลพื้นฐานใน Tensorflow.js อย่างต่อเนื่อง สิ่งนี้สามารถช่วยรองรับความต้องการที่มากขึ้นของเราเมื่อ API วิวัฒนาการ นอกจากนี้ยังหมายความว่ามันคุ้มค่าที่จะติดตามการพัฒนา API อย่างต่อเนื่องเนื่องจาก Tensorflow.js ยังคงเติบโตและได้รับการปรับปรุง

เผยแพร่ครั้งแรกที่ thekevinscott.com

ขอขอบคุณเป็นพิเศษสำหรับ Ari Zilnik