TensorFlow.js - การจดจำเสียงโดยใช้การเรียนรู้การโอน

1. บทนำ

ใน Codelab นี้ คุณจะสร้างเครือข่ายการจดจำเสียงและใช้เครือข่ายดังกล่าวเพื่อควบคุมแถบเลื่อนในเบราว์เซอร์ด้วยการส่งเสียง คุณจะได้ใช้ TensorFlow.js ซึ่งเป็นไลบรารีแมชชีนเลิร์นนิงที่มีประสิทธิภาพและยืดหยุ่นสำหรับ JavaScript

ก่อนอื่นให้โหลดและเรียกใช้โมเดลที่ฝึกล่วงหน้าซึ่งจดจำคำสั่งเสียง 20 คำสั่ง จากนั้นใช้ไมโครโฟนเพื่อสร้างและฝึกโครงข่ายระบบประสาทเทียมที่จะจดจำเสียงของคุณและทำให้แถบเลื่อนเลื่อนไปทางซ้ายหรือขวา

Codelab นี้จะไม่อธิบายทฤษฎีเบื้องหลังโมเดลการจดจำเสียง หากคุณสงสัยในเรื่องนี้ โปรดดูบทแนะนำนี้

เรายังได้สร้างอภิธานศัพท์ของคำศัพท์เกี่ยวกับแมชชีนเลิร์นนิงที่คุณพบใน Codelab นี้ด้วย

สิ่งที่คุณจะได้เรียนรู้

  • วิธีโหลดโมเดลการจดจำคำสั่งเสียงที่ฝึกไว้แล้ว
  • วิธีคาดคะเนแบบเรียลไทม์โดยใช้ไมโครโฟน
  • วิธีฝึกและใช้โมเดลการจดจำเสียงที่กำหนดเองโดยใช้ไมโครโฟนของเบราว์เซอร์

มาเริ่มกันเลย

2. ข้อกำหนด

เพื่อให้ Codelab นี้เสร็จสมบูรณ์ คุณจะต้องมีสิ่งต่อไปนี้

  1. Chrome เวอร์ชันล่าสุด หรือเบราว์เซอร์ที่ทันสมัยอื่นๆ
  2. เครื่องมือแก้ไขข้อความ ซึ่งจะทำงานในเครื่องหรือบนเว็บผ่านบางอย่าง เช่น Codepen หรือ Glitch
  3. ความรู้เกี่ยวกับ HTML, CSS, JavaScript และเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome (หรือเครื่องมือสำหรับนักพัฒนาเว็บในเบราว์เซอร์ที่คุณต้องการ)
  4. ความเข้าใจในแนวคิดระดับสูงเกี่ยวกับโครงข่ายระบบประสาทเทียม หากต้องการข้อมูลเบื้องต้นหรือทบทวนความรู้ โปรดดูวิดีโอนี้โดย 3blue1brown หรือวิดีโอเกี่ยวกับการเรียนรู้เชิงลึกใน JavaScript โดย Ashi Krishnan

3. โหลด TensorFlow.js และโมเดลเสียง

เปิด index.html ในเครื่องมือแก้ไขและเพิ่มเนื้อหานี้

<html>
  <head>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/speech-commands"></script>
  </head>
  <body>
    <div id="console"></div>
    <script src="index.js"></script>
  </body>
</html>

แท็ก <script> แรกจะนำเข้าไลบรารี TensorFlow.js และ<script>รายการที่ 2 จะนำเข้าโมเดลคำสั่งเสียงพูดที่ฝึกล่วงหน้ามา ระบบจะใช้แท็ก <div id="console"> เพื่อแสดงเอาต์พุตของโมเดล

4. คาดการณ์แบบเรียลไทม์

จากนั้นให้เปิด/สร้างไฟล์ index.js ในตัวแก้ไขโค้ด และใส่โค้ดต่อไปนี้

let recognizer;

function predictWord() {
 // Array of words that the recognizer is trained to recognize.
 const words = recognizer.wordLabels();
 recognizer.listen(({scores}) => {
   // Turn scores into a list of (score,word) pairs.
   scores = Array.from(scores).map((s, i) => ({score: s, word: words[i]}));
   // Find the most probable word.
   scores.sort((s1, s2) => s2.score - s1.score);
   document.querySelector('#console').textContent = scores[0].word;
 }, {probabilityThreshold: 0.75});
}

async function app() {
 recognizer = speechCommands.create('BROWSER_FFT');
 await recognizer.ensureModelLoaded();
 predictWord();
}

app();

5. ทดสอบการคาดการณ์

ตรวจสอบว่าอุปกรณ์ของคุณมีไมโครโฟน โปรดอย่าลืมว่าวิธีการนี้สามารถใช้บนโทรศัพท์มือถือได้ด้วย หากต้องการเรียกใช้หน้าเว็บ ให้เปิด index.html ในเบราว์เซอร์ หากทำงานจากไฟล์ในเครื่อง หากต้องการเข้าถึงไมโครโฟน คุณจะต้องเริ่มเว็บเซิร์ฟเวอร์และใช้ http://localhost:port/

หากต้องการเริ่มต้นเว็บเซิร์ฟเวอร์แบบง่ายบนพอร์ต 8000:

python -m SimpleHTTPServer

การดาวน์โหลดโมเดลอาจใช้เวลาสักครู่ โปรดอดใจรอ ทันทีที่โมเดลโหลด คุณควรจะเห็นคำๆ หนึ่งที่ด้านบนของหน้า โมเดลได้รับการฝึกให้จดจำตัวเลข 0 ถึง 9 และคำสั่งเพิ่มเติมบางอย่าง เช่น "ซ้าย" "ขวา" "ใช่" "no" ฯลฯ

พูดคำใดคำหนึ่ง คำนี้เข้าใจถูกต้องไหม เล่นกับ probabilityThreshold ซึ่งควบคุมความถี่ที่โมเดลจะเริ่มทำงาน โดยค่า 0.75 หมายความว่าโมเดลจะเริ่มทำงานเมื่อมีความมั่นใจมากกว่า 75% ว่าได้ยินคำที่ระบุ

ดูข้อมูลเพิ่มเติมเกี่ยวกับโมเดล Speech Commands และ API ได้ที่ README.md ใน GitHub

6. รวบรวมข้อมูล

ลองใช้เสียงสั้นๆ แทนคำทั้งหมดเพื่อควบคุมแถบเลื่อนเพื่อให้สนุกยิ่งขึ้น

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

  1. ก่อนอื่นเราต้องรวบรวมข้อมูล เพิ่ม UI แบบง่ายไปยังแอปโดยการเพิ่มข้อมูลนี้ลงในแท็ก <body> ก่อน <div id="console"> ดังนี้
<button id="left" onmousedown="collect(0)" onmouseup="collect(null)">Left</button>
<button id="right" onmousedown="collect(1)" onmouseup="collect(null)">Right</button>
<button id="noise" onmousedown="collect(2)" onmouseup="collect(null)">Noise</button>
  1. เพิ่มใน index.js:
// One frame is ~23ms of audio.
const NUM_FRAMES = 3;
let examples = [];

function collect(label) {
 if (recognizer.isListening()) {
   return recognizer.stopListening();
 }
 if (label == null) {
   return;
 }
 recognizer.listen(async ({spectrogram: {frameSize, data}}) => {
   let vals = normalize(data.subarray(-frameSize * NUM_FRAMES));
   examples.push({vals, label});
   document.querySelector('#console').textContent =
       `${examples.length} examples collected`;
 }, {
   overlapFactor: 0.999,
   includeSpectrogram: true,
   invokeCallbackOnNoiseAndUnknown: true
 });
}

function normalize(x) {
 const mean = -100;
 const std = 10;
 return x.map(x => (x - mean) / std);
}
  1. นำ predictWord() ออกจาก app():
async function app() {
 recognizer = speechCommands.create('BROWSER_FFT');
 await recognizer.ensureModelLoaded();
 // predictWord() no longer called.
}

ดูรายละเอียด

โค้ดนี้อาจคลุมเครือในตอนแรก ดังนั้นมาดูรายละเอียดกัน

เราได้เพิ่มปุ่ม 3 ปุ่มใน UI ของเรา ซึ่งมีป้าย "ซ้าย" "ขวา" และ "เสียงรบกวน" ซึ่งตรงกับคำสั่ง 3 คำสั่งที่เราต้องการให้โมเดลของเราจดจำ การกดปุ่มเหล่านี้จะเรียกใช้ฟังก์ชัน collect() ที่เพิ่มเข้ามาใหม่ ซึ่งจะสร้างตัวอย่างการฝึกสำหรับโมเดลของเรา

collect() เชื่อมโยง label กับเอาต์พุต recognizer.listen() เนื่องจาก includeSpectrogram เป็นจริง, recognizer.listen() จะให้สเปกโตรแกรมดิบ (ข้อมูลความถี่) สำหรับเสียง 1 วินาทีโดยแบ่งออกเป็น 43 เฟรม ดังนั้นแต่ละเฟรมจึงมีเสียงประมาณ 23 มิลลิวินาที ดังนี้

recognizer.listen(async ({spectrogram: {frameSize, data}}) => {
...
}, {includeSpectrogram: true});

เนื่องจากเราต้องการใช้เสียงสั้นๆ แทนการใช้คำเพื่อควบคุมแถบเลื่อน เราจึงพิจารณาเฉพาะ 3 เฟรมสุดท้าย (ประมาณ 70 มิลลิวินาที) เท่านั้น)

let vals = normalize(data.subarray(-frameSize * NUM_FRAMES));

และเพื่อหลีกเลี่ยงปัญหาเกี่ยวกับตัวเลข เราปรับข้อมูลให้เป็นมาตรฐานโดยให้มีค่าเฉลี่ยเป็น 0 และส่วนเบี่ยงเบนมาตรฐานเท่ากับ 1 ในกรณีนี้ ค่าสเปกโตรแกรมมักเป็นจำนวนลบจำนวนมากที่ประมาณ -100 และส่วนเบี่ยงเบนของ 10

const mean = -100;
const std = 10;
return x.map(x => (x - mean) / std);

สุดท้าย ตัวอย่างการฝึกอบรมแต่ละรายการจะมี 2 ฟิลด์ ดังนี้

  • label****: 0, 1 และ 2 สำหรับ "ซ้าย", "ขวา" และ "เสียงรบกวน" ตามลำดับ
  • vals****: ตัวเลข 696 ตัวที่มีข้อมูลความถี่ (สเปกโตรแกรม)

และเราจะจัดเก็บข้อมูลทั้งหมดในตัวแปร examples ดังนี้

examples.push({vals, label});

7. ทดสอบการเก็บรวบรวมข้อมูล

เปิด index.html ในเบราว์เซอร์ และคุณควรเห็นปุ่ม 3 ปุ่มที่สอดคล้องกับคำสั่ง 3 รายการ หากคุณทำงานจากไฟล์ในเครื่อง หากต้องการเข้าถึงไมโครโฟน คุณจะต้องเริ่มต้นเว็บเซิร์ฟเวอร์และใช้ http://localhost:port/

หากต้องการเริ่มต้นเว็บเซิร์ฟเวอร์แบบง่ายบนพอร์ต 8000:

python -m SimpleHTTPServer

หากต้องการรวบรวมตัวอย่างสำหรับแต่ละคำสั่ง ให้ทำเสียงที่สม่ำเสมอซ้ำๆ (หรือต่อเนื่อง) ขณะกดค้างแต่ละปุ่มเป็นเวลา 3-4 วินาที คุณควรรวบรวมประมาณ 150 ตัวอย่างสำหรับป้ายกำกับแต่ละป้าย ตัวอย่างเช่น เราสามารถดีดนิ้วไปที่ "ซ้าย", ผิวปากสำหรับ "ขวา" และสลับระหว่างความเงียบและพูดสำหรับ "เสียงรบกวน"

ขณะที่คุณรวบรวมตัวอย่างเพิ่มเติม ตัวนับที่แสดงในหน้าควรเลื่อนขึ้น นอกจากนี้ คุณยังตรวจสอบข้อมูลได้โดยเรียกใช้console.log() ในตัวแปร examples ในคอนโซล ในจุดนี้ เป้าหมายคือการทดสอบกระบวนการเก็บรวบรวมข้อมูล คุณจะรวบรวมข้อมูลอีกครั้งได้เมื่อทดสอบทั้งแอป

8. ฝึกการใช้งานโมเดล

  1. เพิ่ม "Train" หลังปุ่ม "เสียงรบกวน" ในส่วนเนื้อความใน index.html:
<br/><br/>
<button id="train" onclick="train()">Train</button>
  1. เพิ่มโค้ดต่อไปนี้ลงในโค้ดที่มีอยู่ใน index.js
const INPUT_SHAPE = [NUM_FRAMES, 232, 1];
let model;

async function train() {
 toggleButtons(false);
 const ys = tf.oneHot(examples.map(e => e.label), 3);
 const xsShape = [examples.length, ...INPUT_SHAPE];
 const xs = tf.tensor(flatten(examples.map(e => e.vals)), xsShape);

 await model.fit(xs, ys, {
   batchSize: 16,
   epochs: 10,
   callbacks: {
     onEpochEnd: (epoch, logs) => {
       document.querySelector('#console').textContent =
           `Accuracy: ${(logs.acc * 100).toFixed(1)}% Epoch: ${epoch + 1}`;
     }
   }
 });
 tf.dispose([xs, ys]);
 toggleButtons(true);
}

function buildModel() {
 model = tf.sequential();
 model.add(tf.layers.depthwiseConv2d({
   depthMultiplier: 8,
   kernelSize: [NUM_FRAMES, 3],
   activation: 'relu',
   inputShape: INPUT_SHAPE
 }));
 model.add(tf.layers.maxPooling2d({poolSize: [1, 2], strides: [2, 2]}));
 model.add(tf.layers.flatten());
 model.add(tf.layers.dense({units: 3, activation: 'softmax'}));
 const optimizer = tf.train.adam(0.01);
 model.compile({
   optimizer,
   loss: 'categoricalCrossentropy',
   metrics: ['accuracy']
 });
}

function toggleButtons(enable) {
 document.querySelectorAll('button').forEach(b => b.disabled = !enable);
}

function flatten(tensors) {
 const size = tensors[0].length;
 const result = new Float32Array(tensors.length * size);
 tensors.forEach((arr, i) => result.set(arr, i * size));
 return result;
}
  1. โทรหา buildModel() เมื่อแอปโหลด
async function app() {
 recognizer = speechCommands.create('BROWSER_FFT');
 await recognizer.ensureModelLoaded();
 // Add this line.
 buildModel();
}

เมื่อถึงจุดนี้ หากรีเฟรชแอป คุณจะเห็นข้อความ "Train" ใหม่ คุณทดสอบการฝึกได้โดยรวบรวมข้อมูลอีกครั้งแล้วคลิก "ฝึก" หรือจะรอจนถึงขั้นตอนที่ 10 เพื่อทดสอบการฝึกพร้อมกับการคาดการณ์ก็ได้

การแจกแจง

เราจะทำ 2 อย่างในระดับสูง ได้แก่ buildModel() จะกำหนดสถาปัตยกรรมโมเดล และ train() จะฝึกโมเดลโดยใช้ข้อมูลที่รวบรวมมา

สถาปัตยกรรมโมเดล

โมเดลนี้มี 4 เลเยอร์ คือ เลเยอร์คอนโวลูชัน (Convolutional) ที่ประมวลผลข้อมูลเสียง (แสดงเป็นสเปกโตรแกรม) เลเยอร์พูลสูงสุด เลเยอร์ราบเรียบ และเลเยอร์หนาแน่นที่แมปกับการทำงาน 3 อย่างต่อไปนี้

model = tf.sequential();
 model.add(tf.layers.depthwiseConv2d({
   depthMultiplier: 8,
   kernelSize: [NUM_FRAMES, 3],
   activation: 'relu',
   inputShape: INPUT_SHAPE
 }));
 model.add(tf.layers.maxPooling2d({poolSize: [1, 2], strides: [2, 2]}));
 model.add(tf.layers.flatten());
 model.add(tf.layers.dense({units: 3, activation: 'softmax'}));

รูปร่างอินพุตของโมเดลคือ [NUM_FRAMES, 232, 1] โดยที่แต่ละเฟรมคือเฟรมของเสียง 23 มิลลิวินาที มีตัวเลข 232 ตัวซึ่งตรงกับความถี่ที่ต่างกัน (มีการเลือก 232 เนื่องจากเป็นจำนวนที่เก็บข้อมูลความถี่ที่จำเป็นในการจับเสียงพูดของมนุษย์) ใน Codelab นี้ เราใช้ตัวอย่างที่มีความยาว 3 เฟรม (ตัวอย่างประมาณ 70 มิลลิวินาที) เนื่องจากเราจะสร้างเสียงแทนที่จะพูดทั้งคำเพื่อควบคุมแถบเลื่อน

เรารวบรวมโมเดลของเราเพื่อเตรียมพร้อมสำหรับการฝึก:

const optimizer = tf.train.adam(0.01);
 model.compile({
   optimizer,
   loss: 'categoricalCrossentropy',
   metrics: ['accuracy']
 });

เราใช้ Adam optimizer ซึ่งเป็นเครื่องมือเพิ่มประสิทธิภาพทั่วไปที่ใช้ในการเรียนรู้เชิงลึก และใช้ categoricalCrossEntropy สำหรับการสูญเสีย ซึ่งเป็นฟังก์ชันการสูญเสียการติดตั้งแบบมาตรฐานสำหรับการแยกประเภท กล่าวโดยสรุปคือ เมตริกนี้จะวัดว่าความน่าจะเป็นที่คาดการณ์ไว้ (ความน่าจะเป็น 1 รายการต่อคลาส) มาจากความน่าจะเป็น 100% ในกลุ่มที่แท้จริงเท่าใด และความน่าจะเป็น 0% สำหรับคลาสอื่นๆ ทั้งหมด นอกจากนี้ เรายังมี accuracy เป็นเมตริกในการตรวจสอบ ซึ่งจะเป็นการระบุเปอร์เซ็นต์ของตัวอย่างที่โมเดลถูกต้องหลังจากแต่ละ Epoch ของการฝึก

ฝึกอบรม

การฝึกจะทำงานกับข้อมูล 10 ครั้ง (Epoch) บนข้อมูลโดยใช้ขนาดกลุ่ม 16 ชุด (ประมวลผลตัวอย่าง 16 ตัวอย่างพร้อมกัน) และแสดงความแม่นยำปัจจุบันใน UI ดังนี้

await model.fit(xs, ys, {
   batchSize: 16,
   epochs: 10,
   callbacks: {
     onEpochEnd: (epoch, logs) => {
       document.querySelector('#console').textContent =
           `Accuracy: ${(logs.acc * 100).toFixed(1)}% Epoch: ${epoch + 1}`;
     }
   }
 });

9. อัปเดตแถบเลื่อนแบบเรียลไทม์

ตอนนี้เราสามารถฝึกโมเดลได้แล้ว เรามาเพิ่มโค้ดเพื่อคาดการณ์แบบเรียลไทม์และเลื่อนแถบเลื่อนกัน เพิ่มข้อมูลนี้ต่อท้าย "รถไฟ" ใน index.html:

<br/><br/>
<button id="listen" onclick="listen()">Listen</button>
<input type="range" id="output" min="0" max="10" step="0.1">

และข้อมูลต่อไปนี้ใน index.js

async function moveSlider(labelTensor) {
 const label = (await labelTensor.data())[0];
 document.getElementById('console').textContent = label;
 if (label == 2) {
   return;
 }
 let delta = 0.1;
 const prevValue = +document.getElementById('output').value;
 document.getElementById('output').value =
     prevValue + (label === 0 ? -delta : delta);
}

function listen() {
 if (recognizer.isListening()) {
   recognizer.stopListening();
   toggleButtons(true);
   document.getElementById('listen').textContent = 'Listen';
   return;
 }
 toggleButtons(false);
 document.getElementById('listen').textContent = 'Stop';
 document.getElementById('listen').disabled = false;

 recognizer.listen(async ({spectrogram: {frameSize, data}}) => {
   const vals = normalize(data.subarray(-frameSize * NUM_FRAMES));
   const input = tf.tensor(vals, [1, ...INPUT_SHAPE]);
   const probs = model.predict(input);
   const predLabel = probs.argMax(1);
   await moveSlider(predLabel);
   tf.dispose([input, probs, predLabel]);
 }, {
   overlapFactor: 0.999,
   includeSpectrogram: true,
   invokeCallbackOnNoiseAndUnknown: true
 });
}

การแจกแจง

การคาดการณ์แบบเรียลไทม์

listen() จะฟังไมโครโฟนและคาดการณ์แบบเรียลไทม์ โค้ดนี้คล้ายกับเมธอด collect() อย่างมาก ซึ่งจะปรับสเปกโตรแกรมดิบให้เป็นมาตรฐานและทิ้งเฟรมทั้งหมดยกเว้น NUM_FRAMES เฟรมสุดท้าย ความแตกต่างเพียงอย่างเดียวคือเรายังเรียกใช้โมเดลที่ผ่านการฝึกเพื่อรับการคาดการณ์ด้วย ดังนี้

const probs = model.predict(input);
const predLabel = probs.argMax(1);
await moveSlider(predLabel);

เอาต์พุตของ model.predict(input) คือ Tensor ของรูปร่าง [1, numClasses] ที่แทนการแจกแจงความน่าจะเป็นตามจำนวนคลาส พูดง่ายๆ ก็คือนี่เป็นชุดความเชื่อมั่นสำหรับคลาสเอาต์พุตที่เป็นไปได้แต่ละคลาสซึ่งรวมกันเป็น 1 Tensor มีมิติข้อมูลด้านนอกของ 1 เนื่องจากเป็นขนาดของแบทช์ (ตัวอย่างเดียว)

ในการแปลงการแจกแจงความน่าจะเป็นเป็นจำนวนเต็มเดียวที่แสดงถึงคลาสที่เป็นไปได้มากที่สุด เราเรียก probs.argMax(1) ซึ่งจะแสดงดัชนีคลาสที่มีความน่าจะเป็นสูงสุด เราสอบผ่าน "1" เป็นพารามิเตอร์ของแกนเพราะเราต้องการคำนวณ argMax ในมิติข้อมูลสุดท้าย numClasses

การอัปเดตแถบเลื่อน

moveSlider() ลดค่าของแถบเลื่อนหากป้ายกำกับเป็น 0 ("ซ้าย") เพิ่มค่าหากป้ายกำกับเป็น 1 ("ขวา") และเพิกเฉยหากป้ายกำกับเป็น 2 ("เสียงรบกวน")

การกำจัด Tensor

เราจำเป็นต้องเรียกใช้ tf.dispose() บนเอาต์พุต Tensor เพื่อล้างหน่วยความจำ GPU อีกทางเลือกหนึ่งนอกเหนือจากการใช้ tf.dispose() ด้วยตนเองคือการรวมการเรียกใช้ฟังก์ชันใน tf.tidy() แต่ใช้ไม่ได้กับฟังก์ชันอะซิงโครนัส

   tf.dispose([input, probs, predLabel]);

10. ทดสอบแอปสุดท้าย

เปิด index.html ในเบราว์เซอร์และรวบรวมข้อมูลเช่นเดียวกับที่คุณทำในส่วนก่อนหน้าด้วยปุ่ม 3 ปุ่มที่สอดคล้องกับคำสั่ง 3 รายการ อย่าลืมกดค้างไว้ที่แต่ละปุ่มเป็นเวลา 3-4 วินาทีขณะรวบรวมข้อมูล

เมื่อรวบรวมตัวอย่างได้แล้ว ให้กดปุ่ม "ฝึก" การดำเนินการนี้จะเริ่มการฝึกโมเดล และคุณควรเห็นว่าความแม่นยำของโมเดลสูงกว่า 90% หากคุณไม่มีประสิทธิภาพโมเดลที่ดี ให้ลองรวบรวมข้อมูลเพิ่มเติม

เมื่อฝึกเสร็จเรียบร้อยแล้ว ให้กดปุ่ม "ฟัง" เพื่อคาดคะเนจากไมโครโฟนและควบคุมแถบเลื่อน!

ดูบทแนะนำเพิ่มเติมได้ที่ http://js.tensorflow.org/