1. บทนำ
ในโค้ดแล็บนี้ คุณจะได้สร้างเครือข่ายการจดจำเสียงและใช้เครือข่ายดังกล่าวเพื่อควบคุมแถบเลื่อนในเบราว์เซอร์ด้วยการทำเสียง คุณจะใช้ TensorFlow.js ซึ่งเป็นไลบรารีแมชชีนเลิร์นนิงที่มีประสิทธิภาพและยืดหยุ่นสำหรับ JavaScript
ก่อนอื่น คุณจะโหลดและเรียกใช้โมเดลที่ฝึกไว้ล่วงหน้าซึ่งจดจำคำสั่งเสียงได้ 20 คำสั่ง จากนั้นใช้ไมโครโฟนเพื่อสร้างและฝึกโครงข่ายประสาทเทียมแบบง่ายที่จะจดจำเสียงของคุณและทำให้แถบเลื่อนไปทางซ้ายหรือขวา
Codelab นี้จะไม่กล่าวถึงทฤษฎีเบื้องหลังโมเดลการจดจำเสียง หากคุณอยากทราบข้อมูลเพิ่มเติม โปรดดูบทแนะนำนี้
นอกจากนี้ เรายังได้สร้างพจนานุกรมคำศัพท์เกี่ยวกับแมชชีนเลิร์นนิงที่คุณจะเห็นใน Codelab นี้ด้วย
สิ่งที่คุณจะได้เรียนรู้
- วิธีโหลดโมเดลการจดจำคำสั่งเสียงที่ฝึกไว้ล่วงหน้า
- วิธีคาดคะเนแบบเรียลไทม์โดยใช้ไมโครโฟน
- วิธีฝึกและใช้โมเดลการจดจำเสียงที่กำหนดเองโดยใช้ไมโครโฟนของเบราว์เซอร์
มาเริ่มกันเลย
2. ข้อกำหนด
คุณต้องมีสิ่งต่อไปนี้จึงจะทำ Codelab นี้ให้เสร็จสมบูรณ์ได้
- Chrome หรือเบราว์เซอร์สมัยใหม่อื่นๆ เวอร์ชันล่าสุด
- โปรแกรมแก้ไขข้อความที่ทำงานในเครื่องของคุณหรือบนเว็บผ่านเครื่องมืออย่าง Codepen หรือ Glitch
- ความรู้เกี่ยวกับ HTML, CSS, JavaScript และเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome (หรือเครื่องมือสำหรับนักพัฒนาเว็บของเบราว์เซอร์ที่คุณต้องการ)
- ความเข้าใจเชิงแนวคิดระดับสูงเกี่ยวกับโครงข่ายประสาทเทียม หากต้องการดูข้อมูลเบื้องต้นหรือทบทวน โปรดดูวิดีโอนี้จาก 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 และคำสั่งเพิ่มเติมอีก 2-3 คำสั่ง เช่น "ซ้าย" "ขวา" "ใช่" "ไม่ใช่" เป็นต้น
พูดคำใดคำหนึ่ง ระบบจดจำคำพูดของคุณได้ถูกต้องไหม เล่นกับ probabilityThreshold ซึ่งควบคุมความถี่ที่โมเดลจะเริ่มทำงาน โดย 0.75 หมายความว่าโมเดลจะเริ่มทำงานเมื่อมีความมั่นใจมากกว่า 75% ว่าได้ยินคำที่กำหนด
ดูข้อมูลเพิ่มเติมเกี่ยวกับโมเดลคำสั่งเสียงและ API ของโมเดลได้ที่ README.md ใน GitHub
6. รวบรวมข้อมูล
เพื่อเพิ่มความสนุก เราจะใช้เสียงสั้นๆ แทนคำทั้งคำเพื่อควบคุมแถบเลื่อน
คุณจะฝึกโมเดลให้จดจำคำสั่ง 3 คำสั่ง ได้แก่ "ซ้าย" "ขวา" และ "เสียงรบกวน" ซึ่งจะทำให้แถบเลื่อนเลื่อนไปทางซ้ายหรือขวา การจดจำ "เสียงรบกวน" (ไม่ต้องดำเนินการใดๆ) เป็นสิ่งสำคัญในการตรวจจับคำพูด เนื่องจากเราต้องการให้แถบเลื่อนตอบสนองเฉพาะเมื่อเราออกเสียงที่ถูกต้องเท่านั้น ไม่ใช่เมื่อเราพูดและเคลื่อนไหวไปมา
- ก่อนอื่นเราต้องรวบรวมข้อมูล เพิ่ม 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>
- เพิ่มข้อความต่อไปนี้ไปยัง
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);
}
- วิธีนำ
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. ฝึกการใช้งานโมเดล
- เพิ่มปุ่ม "Train" ต่อจากปุ่ม "Noise" ในส่วนเนื้อหาของ index.html:
<br/><br/>
<button id="train" onclick="train()">Train</button>
- เพิ่มโค้ดต่อไปนี้ลงในโค้ดที่มีอยู่ใน 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;
}
- เรียกใช้
buildModel()เมื่อแอปโหลด
async function app() {
recognizer = speechCommands.create('BROWSER_FFT');
await recognizer.ensureModelLoaded();
// Add this line.
buildModel();
}
ตอนนี้หากรีเฟรชแอป คุณจะเห็นปุ่ม "ฝึก" ใหม่ คุณทดสอบการฝึกได้โดยรวบรวมข้อมูลอีกครั้งแล้วคลิก "ฝึก" หรือจะรอจนถึงขั้นตอนที่ 10 เพื่อทดสอบการฝึกพร้อมกับการคาดการณ์ก็ได้
การวิเคราะห์
ในระดับสูง เราจะทำ 2 สิ่ง ได้แก่ buildModel() กำหนดสถาปัตยกรรมของโมเดล และ train() ฝึกโมเดลโดยใช้ข้อมูลที่รวบรวม
สถาปัตยกรรมโมเดล
โมเดลมี 4 เลเยอร์ ได้แก่ เลเยอร์ Convolutional ที่ประมวลผลข้อมูลเสียง (แสดงเป็นสเปกโตรแกรม) เลเยอร์ Max Pool, เลเยอร์ Flatten และเลเยอร์ Dense ที่แมปกับการดำเนินการ 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 ซึ่งเป็น Optimizer ทั่วไปที่ใช้ในการเรียนรู้แบบลึก และ categoricalCrossEntropy สำหรับ Loss ซึ่งเป็น Loss Function มาตรฐานที่ใช้สำหรับการจัดประเภท กล่าวโดยย่อคือ จะวัดว่าความน่าจะเป็นที่คาดการณ์ (ความน่าจะเป็น 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. อัปเดตแถบเลื่อนแบบเรียลไทม์
ตอนนี้เราฝึกโมเดลได้แล้ว มาเพิ่มโค้ดเพื่อทำการคาดการณ์แบบเรียลไทม์และเลื่อนแถบเลื่อนกัน เพิ่มโค้ดนี้ต่อจากปุ่ม "Train" ใน 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) คือเทนเซอร์ที่มีรูปร่าง [1, numClasses] ซึ่งแสดงการกระจายความน่าจะเป็นตามจำนวนคลาส กล่าวอย่างง่ายๆ ก็คือ นี่เป็นเพียงชุดความเชื่อมั่นสำหรับแต่ละคลาสเอาต์พุตที่เป็นไปได้ ซึ่งรวมกันได้ 1 Tensor มีมิติภายนอกเป็น 1 เนื่องจากเป็นขนาดของกลุ่ม (ตัวอย่างเดียว)
หากต้องการแปลงการกระจายความน่าจะเป็นเป็นจำนวนเต็มเดียวที่แสดงถึงคลาสที่มีแนวโน้มมากที่สุด เราจะเรียกใช้ probs.argMax(1) ซึ่งจะแสดงดัชนีคลาสที่มีความน่าจะเป็นสูงสุด เราส่ง "1" เป็นพารามิเตอร์แกนเนื่องจากต้องการคำนวณ argMax ในมิติข้อมูลสุดท้าย numClasses
การอัปเดตแถบเลื่อน
moveSlider() จะลดค่าของแถบเลื่อนหากป้ายกำกับเป็น 0 ("ซ้าย") เพิ่มค่าหากป้ายกำกับเป็น 1 ("ขวา") และไม่สนใจหากป้ายกำกับเป็น 2 ("เสียงรบกวน")
การทิ้งเทนเซอร์
การเรียก tf.dispose() ในเอาต์พุตเทนเซอร์ด้วยตนเองเป็นสิ่งสำคัญในการล้างหน่วยความจำ GPU ทางเลือกแทน tf.dispose() ด้วยตนเองคือการเรียกฟังก์ชันใน tf.tidy() แต่จะใช้กับฟังก์ชันแบบอะซิงโครนัสไม่ได้
tf.dispose([input, probs, predLabel]);
10. ทดสอบแอปขั้นสุดท้าย
เปิด index.html ในเบราว์เซอร์และรวบรวมข้อมูลเช่นเดียวกับในส่วนก่อนหน้าด้วยปุ่ม 3 ปุ่มที่สอดคล้องกับคำสั่ง 3 คำสั่ง อย่าลืมกดค้างปุ่มแต่ละปุ่มเป็นเวลา 3-4 วินาทีขณะเก็บรวบรวมข้อมูล
เมื่อรวบรวมตัวอย่างแล้ว ให้กดปุ่ม "ฝึก" ซึ่งจะเริ่มฝึกโมเดล และคุณควรเห็นความแม่นยำของโมเดลสูงกว่า 90% หากโมเดลมีประสิทธิภาพไม่ดี ให้ลองรวบรวมข้อมูลเพิ่มเติม
เมื่อฝึกเสร็จแล้ว ให้กดปุ่ม "ฟัง" เพื่อให้ระบบคาดคะเนจากไมโครโฟนและควบคุมแถบเลื่อน
ดูบทแนะนำเพิ่มเติมได้ที่ http://js.tensorflow.org/