1. Giới thiệu
Trong lớp học lập trình này, bạn sẽ xây dựng một mạng nhận dạng âm thanh và dùng mạng đó để điều khiển một thanh trượt trong trình duyệt bằng cách tạo ra âm thanh. Bạn sẽ sử dụng TensorFlow.js, một thư viện học máy mạnh mẽ và linh hoạt cho JavaScript.
Trước tiên, bạn sẽ tải và chạy một mô hình được huấn luyện tiền kỳ có thể nhận dạng 20 lệnh thoại. Sau đó, bằng micrô, bạn sẽ tạo và huấn luyện một mạng nơ-ron đơn giản có thể nhận dạng âm thanh của bạn và làm cho thanh trượt di chuyển sang trái hoặc phải.
Lớp học lập trình này không đề cập đến lý thuyết đằng sau các mô hình nhận dạng âm thanh. Nếu bạn muốn tìm hiểu về vấn đề này, hãy xem hướng dẫn này.
Chúng tôi cũng đã tạo một bảng chú giải về các thuật ngữ học máy mà bạn sẽ thấy trong lớp học lập trình này.
Kiến thức bạn sẽ học được
- Cách tải mô hình nhận dạng lệnh thoại được huấn luyện trước
- Cách đưa ra dự đoán theo thời gian thực bằng micrô
- Cách huấn luyện và sử dụng mô hình nhận dạng âm thanh tuỳ chỉnh bằng micrô của trình duyệt
Vì vậy, hãy bắt đầu.
2. Yêu cầu
Để hoàn tất lớp học lập trình này, bạn cần:
- Phiên bản mới nhất của Chrome hoặc một trình duyệt hiện đại khác.
- Một trình chỉnh sửa văn bản, chạy cục bộ trên máy hoặc trên web thông qua một công cụ như Codepen hoặc Glitch.
- Có kiến thức về HTML, CSS, JavaScript và Chrome DevTools (hoặc công cụ cho nhà phát triển của trình duyệt mà bạn muốn dùng).
- Hiểu biết khái niệm cấp cao về Mạng nơ-ron. Nếu bạn cần tìm hiểu hoặc ôn lại, hãy cân nhắc xem video này của 3blue1brown hoặc video này về Học sâu bằng JavaScript của Ashi Krishnan.
3. Tải TensorFlow.js và mô hình Âm thanh
Mở index.html trong trình chỉnh sửa rồi thêm nội dung sau:
<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>
Thẻ <script> đầu tiên nhập thư viện TensorFlow.js và thẻ <script> thứ hai nhập mô hình Lệnh thoại được huấn luyện trước. Thẻ <div id="console"> sẽ được dùng để hiển thị đầu ra của mô hình.
4. Dự đoán theo thời gian thực
Tiếp theo, hãy mở/tạo tệp index.js trong một trình soạn thảo mã và thêm đoạn mã sau:
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. Kiểm thử tính năng dự đoán
Đảm bảo thiết bị của bạn có micrô. Xin lưu ý rằng tính năng này cũng sẽ hoạt động trên điện thoại di động! Để chạy trang web, hãy mở index.html trong một trình duyệt. Nếu đang làm việc trên một tệp cục bộ, bạn sẽ phải khởi động một máy chủ web và sử dụng http://localhost:port/ để truy cập vào micrô.
Cách khởi động một máy chủ web đơn giản trên cổng 8000:
python -m SimpleHTTPServer
Quá trình tải mô hình xuống có thể mất chút thời gian, vì vậy, vui lòng kiên nhẫn chờ đợi. Ngay khi mô hình tải xong, bạn sẽ thấy một từ ở đầu trang. Mô hình này được huấn luyện để nhận dạng các số từ 0 đến 9 và một số lệnh bổ sung như "trái", "phải", "có", "không", v.v.
Nói một trong những từ đó. Tính năng này có nhận dạng chính xác lời nói của bạn không? Chơi với probabilityThreshold để kiểm soát tần suất kích hoạt mô hình – 0,75 có nghĩa là mô hình sẽ kích hoạt khi có độ tin cậy trên 75% rằng mô hình nghe thấy một từ nhất định.
Để tìm hiểu thêm về mô hình Lệnh thoại và API của mô hình này, hãy xem README.md trên GitHub.
6. Thu thập dữ liệu
Để cho vui, hãy dùng âm thanh ngắn thay vì toàn bộ từ để điều khiển thanh trượt!
Bạn sẽ huấn luyện một mô hình để nhận dạng 3 lệnh khác nhau: "Trái", "Phải" và "Nhiễu". Các lệnh này sẽ làm cho thanh trượt di chuyển sang trái hoặc sang phải. Việc nhận dạng "Tiếng ồn" (không cần làm gì cả) là rất quan trọng trong tính năng phát hiện lời nói vì chúng ta chỉ muốn thanh trượt phản ứng khi chúng ta tạo ra âm thanh phù hợp, chứ không phải khi chúng ta nói chuyện và di chuyển xung quanh.
- Trước tiên, chúng ta cần thu thập dữ liệu. Thêm một giao diện người dùng đơn giản vào ứng dụng bằng cách thêm nội dung này vào bên trong thẻ
<body>trước<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>
- Thêm ảnh này vào
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);
}
- Xoá
predictWord()khỏiapp():
async function app() {
recognizer = speechCommands.create('BROWSER_FFT');
await recognizer.ensureModelLoaded();
// predictWord() no longer called.
}
Vũ điệu cuồng nhiệt
Lúc đầu, mã này có thể khiến bạn cảm thấy khó hiểu, vì vậy hãy xem chi tiết.
Chúng tôi đã thêm 3 nút vào giao diện người dùng có nhãn "Trái", "Phải" và "Tiếng ồn", tương ứng với 3 lệnh mà chúng tôi muốn mô hình nhận dạng. Khi nhấn các nút này, hàm collect() mới thêm của chúng ta sẽ được gọi. Hàm này tạo ra các ví dụ huấn luyện cho mô hình.
collect() liên kết một label với đầu ra của recognizer.listen(). Vì includeSpectrogram là true, recognizer.listen() sẽ cho ra phổ đồ thô (dữ liệu tần số) cho 1 giây âm thanh, chia thành 43 khung hình, nên mỗi khung hình là ~23 mili giây âm thanh:
recognizer.listen(async ({spectrogram: {frameSize, data}}) => {
...
}, {includeSpectrogram: true});
Vì muốn sử dụng âm thanh ngắn thay vì từ ngữ để điều khiển thanh trượt, chúng ta chỉ xem xét 3 khung hình cuối cùng (~70 mili giây):
let vals = normalize(data.subarray(-frameSize * NUM_FRAMES));
Để tránh các vấn đề về số, chúng tôi chuẩn hoá dữ liệu để có giá trị trung bình là 0 và độ lệch chuẩn là 1. Trong trường hợp này, các giá trị của biểu đồ tần số thường là các số âm lớn khoảng -100 và độ lệch là 10:
const mean = -100;
const std = 10;
return x.map(x => (x - mean) / std);
Cuối cùng, mỗi ví dụ huấn luyện sẽ có 2 trường:
label****: 0, 1 và 2 lần lượt cho "Trái", "Phải" và "Tiếng ồn".vals****: 696 số chứa thông tin tần số (phổ đồ)
và chúng ta lưu trữ tất cả dữ liệu trong biến examples:
examples.push({vals, label});
7. Thu thập dữ liệu thử nghiệm
Mở index.html trong trình duyệt, bạn sẽ thấy 3 nút tương ứng với 3 lệnh. Nếu đang làm việc trên một tệp cục bộ, bạn sẽ phải khởi động một máy chủ web và sử dụng http://localhost:port/ để truy cập vào micrô.
Cách khởi động một máy chủ web đơn giản trên cổng 8000:
python -m SimpleHTTPServer
Để thu thập ví dụ cho từng lệnh, hãy tạo một âm thanh nhất quán nhiều lần (hoặc liên tục) trong khi nhấn và giữ từng nút trong 3 đến 4 giây. Bạn nên thu thập khoảng 150 ví dụ cho mỗi nhãn. Ví dụ: chúng ta có thể búng ngón tay cho "Trái", huýt sáo cho "Phải" và thay đổi giữa im lặng và nói cho "Tiếng ồn".
Khi bạn thu thập thêm ví dụ, bộ đếm xuất hiện trên trang sẽ tăng lên. Bạn cũng có thể kiểm tra dữ liệu bằng cách gọi console.log() trên biến examples trong bảng điều khiển. Tại thời điểm này, mục tiêu là kiểm thử quy trình thu thập dữ liệu. Sau đó, bạn sẽ thu thập lại dữ liệu khi kiểm thử toàn bộ ứng dụng.
8. Huấn luyện mô hình
- Thêm nút "Train" (Huấn luyện) ngay sau nút "Noise" (Tiếng ồn) trong phần nội dung của index.html:
<br/><br/>
<button id="train" onclick="train()">Train</button>
- Thêm nội dung sau vào mã hiện có trong 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;
}
- Gọi
buildModel()khi ứng dụng tải:
async function app() {
recognizer = speechCommands.create('BROWSER_FFT');
await recognizer.ensureModelLoaded();
// Add this line.
buildModel();
}
Tại thời điểm này, nếu làm mới ứng dụng, bạn sẽ thấy nút "Train" (Huấn luyện) mới. Bạn có thể kiểm thử quá trình huấn luyện bằng cách thu thập lại dữ liệu và nhấp vào "Huấn luyện", hoặc bạn có thể đợi đến bước 10 để kiểm thử quá trình huấn luyện cùng với hoạt động dự đoán.
Phân tích
Ở cấp độ cao, chúng ta đang làm hai việc: buildModel() xác định cấu trúc mô hình và train() huấn luyện mô hình bằng dữ liệu đã thu thập.
Cấu trúc mô hình
Mô hình này có 4 lớp: một lớp tích chập xử lý dữ liệu âm thanh (được biểu thị dưới dạng quang phổ), một lớp gộp tối đa, một lớp làm phẳng và một lớp dày đặc liên kết với 3 hành động:
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'}));
Hình dạng đầu vào của mô hình là [NUM_FRAMES, 232, 1], trong đó mỗi khung hình là 23 mili giây âm thanh chứa 232 số tương ứng với các tần số khác nhau (232 được chọn vì đây là số lượng nhóm tần số cần thiết để ghi lại giọng nói của con người). Trong lớp học lập trình này, chúng ta đang sử dụng các mẫu có độ dài 3 khung hình (mẫu ~70 mili giây) vì chúng ta đang tạo âm thanh thay vì nói toàn bộ từ để điều khiển thanh trượt.
Chúng ta biên dịch mô hình để chuẩn bị cho quá trình huấn luyện:
const optimizer = tf.train.adam(0.01);
model.compile({
optimizer,
loss: 'categoricalCrossentropy',
metrics: ['accuracy']
});
Chúng tôi sử dụng trình tối ưu hoá Adam, một trình tối ưu hoá phổ biến được dùng trong học sâu và categoricalCrossEntropy cho tổn thất, hàm tổn thất tiêu chuẩn được dùng để phân loại. Nói tóm lại, hàm này đo lường mức độ khác biệt giữa các xác suất dự đoán (một xác suất cho mỗi lớp) với xác suất 100% trong lớp thực và xác suất 0% cho tất cả các lớp khác. Chúng tôi cũng cung cấp accuracy làm chỉ số để theo dõi, chỉ số này sẽ cho chúng ta biết tỷ lệ phần trăm ví dụ mà mô hình nhận được chính xác sau mỗi giai đoạn huấn luyện.
Đào tạo
Quá trình huấn luyện diễn ra 10 lần (số lượng giai đoạn) trên dữ liệu bằng cách sử dụng kích thước lô là 16 (xử lý 16 ví dụ cùng một lúc) và cho biết độ chính xác hiện tại trong giao diện người dùng:
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. Cập nhật thanh trượt theo thời gian thực
Giờ đây, khi có thể huấn luyện mô hình, hãy thêm mã để đưa ra dự đoán theo thời gian thực và di chuyển thanh trượt. Thêm đoạn mã này ngay sau nút "Train" (Huấn luyện) trong index.html:
<br/><br/>
<button id="listen" onclick="listen()">Listen</button>
<input type="range" id="output" min="0" max="10" step="0.1">
Và nội dung sau trong 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
});
}
Phân tích
Dự đoán theo thời gian thực
listen() lắng nghe micrô và đưa ra dự đoán theo thời gian thực. Mã này rất giống với phương thức collect(), giúp chuẩn hoá phổ đồ thô và loại bỏ tất cả trừ NUM_FRAMES khung hình cuối cùng. Điểm khác biệt duy nhất là chúng ta cũng gọi mô hình đã huấn luyện để nhận được một dự đoán:
const probs = model.predict(input);
const predLabel = probs.argMax(1);
await moveSlider(predLabel);
Đầu ra của model.predict(input) là một Tensor có hình dạng [1, numClasses] biểu thị một phân phối xác suất trên số lượng lớp. Nói một cách đơn giản hơn, đây chỉ là một tập hợp các độ tin cậy cho từng lớp đầu ra có thể có, tổng bằng 1. Tensor có một chiều bên ngoài là 1 vì đó là kích thước của lô (một ví dụ duy nhất).
Để chuyển đổi phân phối xác suất thành một số nguyên duy nhất đại diện cho lớp có khả năng cao nhất, chúng ta gọi probs.argMax(1) để trả về chỉ mục lớp có xác suất cao nhất. Chúng ta truyền "1" làm tham số trục vì muốn tính argMax trên phương diện cuối cùng, numClasses.
Cập nhật thanh trượt
moveSlider() sẽ giảm giá trị của thanh trượt nếu nhãn là 0 ("Trái") , tăng giá trị nếu nhãn là 1 ("Phải") và bỏ qua nếu nhãn là 2 ("Nhiễu").
Xử lý các tensor
Để dọn dẹp bộ nhớ GPU, điều quan trọng là chúng ta phải gọi tf.dispose() theo cách thủ công trên các Tensor đầu ra. Giải pháp thay thế cho tf.dispose() thủ công là bao bọc các lệnh gọi hàm trong một tf.tidy(), nhưng bạn không thể sử dụng giải pháp này với các hàm không đồng bộ.
tf.dispose([input, probs, predLabel]);
10. Kiểm thử ứng dụng cuối cùng
Mở index.html trong trình duyệt và thu thập dữ liệu như bạn đã làm trong phần trước với 3 nút tương ứng với 3 lệnh. Hãy nhớ nhấn và giữ từng nút trong 3 đến 4 giây trong khi thu thập dữ liệu.
Sau khi bạn thu thập các ví dụ, hãy nhấn vào nút "Huấn luyện". Thao tác này sẽ bắt đầu huấn luyện mô hình và bạn sẽ thấy độ chính xác của mô hình tăng lên trên 90%. Nếu bạn không đạt được hiệu suất mô hình tốt, hãy thử thu thập thêm dữ liệu.
Sau khi quá trình huấn luyện hoàn tất, hãy nhấn vào nút "Nghe" để đưa ra dự đoán từ micrô và điều khiển thanh trượt!
Xem thêm hướng dẫn tại http://js.tensorflow.org/.