1. Wprowadzenie
W tym ćwiczeniu w Codelabs utworzysz sieć rozpoznawania dźwięku i użyjesz jej do sterowania suwakiem w przeglądarce za pomocą dźwięków. Będziesz używać TensorFlow.js, potężnej i elastycznej biblioteki uczenia maszynowego w JavaScripcie.
Najpierw wczytasz i uruchomisz wstępnie wytrenowany model, który rozpoznaje 20 poleceń głosowych. Następnie za pomocą mikrofonu zbudujesz i wytrenujesz prostą sieć neuronową, która rozpoznaje Twoje dźwięki i przesuwa suwak w lewo lub w prawo.
W tych ćwiczeniach z programowania nie omówimy teorii modeli rozpoznawania dźwięku. Jeśli chcesz dowiedzieć się więcej, obejrzyj ten samouczek.
Przygotowaliśmy też słowniczek terminów związanych z uczeniem maszynowym, które pojawiają się w tym ćwiczeniu.
Czego się nauczysz
- Jak wczytać wytrenowany model rozpoznawania poleceń głosowych
- Jak tworzyć prognozy w czasie rzeczywistym za pomocą mikrofonu
- Jak trenować i używać niestandardowego modelu rozpoznawania dźwięku za pomocą mikrofonu w przeglądarce
Zaczynamy.
2. Wymagania
Aby ukończyć to ćwiczenie, potrzebujesz:
- Najnowsza wersja Chrome lub innej nowoczesnej przeglądarki.
- edytor tekstu uruchomiony lokalnie na komputerze lub w internecie, np. Codepen lub Glitch;
- Znajomość HTML-a, CSS-a, JavaScriptu i Narzędzi deweloperskich w Chrome (lub narzędzi deweloperskich w innej przeglądarce).
- ogólne pojęcie sieci neuronowych; Jeśli potrzebujesz wprowadzenia lub przypomnienia, obejrzyj ten film 3blue1brown lub ten film Ashiego Krishnana o uczeniu głębokim w JavaScript.
3. Wczytywanie TensorFlow.js i modelu audio
Otwórz plik index.html w edytorze i dodaj te treści:
<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>
Pierwszy tag <script> importuje bibliotekę TensorFlow.js, a drugi tag <script> importuje wstępnie wytrenowany model poleceń głosowych. Tag <div id="console"> będzie używany do wyświetlania danych wyjściowych modelu.
4. Przewidywanie w czasie rzeczywistym
Następnie otwórz lub utwórz plik index.js w edytorze kodu i dodaj ten kod:
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. Testowanie prognozy
Sprawdź, czy urządzenie ma mikrofon. Warto pamiętać, że ta funkcja działa też na telefonach komórkowych. Aby uruchomić stronę internetową, otwórz plik index.html w przeglądarce. Jeśli pracujesz z plikiem lokalnym, aby uzyskać dostęp do mikrofonu, musisz uruchomić serwer WWW i użyć http://localhost:port/.
Aby uruchomić prosty serwer WWW na porcie 8000:
python -m SimpleHTTPServer
Pobranie modelu może trochę potrwać, więc prosimy o cierpliwość. Gdy model się wczyta, u góry strony zobaczysz słowo. Model został wytrenowany tak, aby rozpoznawać cyfry od 0 do 9 oraz kilka dodatkowych poleceń, takich jak „w lewo”, „w prawo”, „tak”, „nie” itp.
Wypowiedz jedno z tych słów. Czy dobrze rozpoznaje wypowiadane słowa? Eksperymentuj z wartością probabilityThreshold, która określa, jak często model będzie reagować – wartość 0,75 oznacza, że model zareaguje, gdy będzie miał ponad 75% pewności, że usłyszał dane słowo.
Więcej informacji o modelu poleceń głosowych i jego interfejsie API znajdziesz w pliku README.md na GitHubie.
6. Zbieranie danych
Aby było ciekawiej, użyjemy krótkich dźwięków zamiast całych słów do sterowania suwakiem.
Wytrenujesz model rozpoznawania 3 różnych poleceń: „Lewo”, „Prawo” i „Szum”, które będą przesuwać suwak w lewo lub w prawo. Rozpoznawanie „szumu” (nie wymaga działania) jest kluczowe w wykrywaniu mowy, ponieważ chcemy, aby suwak reagował tylko wtedy, gdy wydajemy odpowiedni dźwięk, a nie wtedy, gdy mówimy i poruszamy się.
- Najpierw musimy zebrać dane. Dodaj do aplikacji prosty interfejs, umieszczając ten kod w tagu
<body>przed tagiem<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>
- Dodaj to do
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);
}
- Usuń urządzenie
predictWord()z kontaapp():
async function app() {
recognizer = speechCommands.create('BROWSER_FFT');
await recognizer.ensureModelLoaded();
// predictWord() no longer called.
}
Tanecznym krokiem
Ten kod może początkowo wydawać się skomplikowany, więc go rozbijmy.
Dodaliśmy do interfejsu 3 przyciski o nazwach „Lewo”, „Prawo” i „Szum”, które odpowiadają 3 poleceniom, jakie chcemy, aby model rozpoznawał. Naciśnięcie tych przycisków wywołuje dodaną przez nas funkcję collect(), która tworzy przykłady szkoleniowe dla naszego modelu.
collect() przypisuje label do danych wyjściowych recognizer.listen(). Ponieważ includeSpectrogram ma wartość prawda,, recognizer.listen() zwraca surowy spektrogram (dane o częstotliwości) dla 1 sekundy dźwięku podzielonej na 43 klatki, więc każda klatka to około 23 ms dźwięku:
recognizer.listen(async ({spectrogram: {frameSize, data}}) => {
...
}, {includeSpectrogram: true});
Ponieważ do sterowania suwakiem chcemy używać krótkich dźwięków zamiast słów, bierzemy pod uwagę tylko ostatnie 3 klatki (ok. 70 ms):
let vals = normalize(data.subarray(-frameSize * NUM_FRAMES));
Aby uniknąć problemów numerycznych, normalizujemy dane tak, aby miały średnią wartość 0 i odchylenie standardowe 1. W tym przypadku wartości spektrogramu są zwykle dużymi liczbami ujemnymi w okolicach –100, a odchylenie wynosi 10:
const mean = -100;
const std = 10;
return x.map(x => (x - mean) / std);
Każdy przykład treningowy będzie miał 2 pola:
label****: 0, 1 i 2 odpowiednio dla opcji „Lewa”, „Prawa” i „Szum”.vals****: 696 liczb zawierających informacje o częstotliwości (spektrogram)
i przechowujemy wszystkie dane w zmiennej examples:
examples.push({vals, label});
7. Testowanie zbierania danych
Otwórz plik index.html w przeglądarce. Powinny się w niej wyświetlić 3 przyciski odpowiadające 3 poleceniom. Jeśli pracujesz z plikiem lokalnym, aby uzyskać dostęp do mikrofonu, musisz uruchomić serwer internetowy i użyć http://localhost:port/.
Aby uruchomić prosty serwer WWW na porcie 8000:
python -m SimpleHTTPServer
Aby zebrać przykłady dla każdego polecenia, powtarzaj (lub utrzymuj) ten sam dźwięk, naciskając i przytrzymując każdy przycisk przez 3–4 sekundy. W przypadku każdej etykiety należy zebrać około 150 przykładów. Możemy na przykład pstrykać palcami, aby wybrać „Lewy”, gwizdać, aby wybrać „Prawy”, a na zmianę mówić i milczeć, aby wybrać „Szum”.
Wraz z kolejnymi przykładami licznik na stronie powinien rosnąć. Możesz też sprawdzić dane, wywołując funkcję console.log() na zmiennej examples w konsoli. Na tym etapie celem jest przetestowanie procesu zbierania danych. Później ponownie zbierzesz dane podczas testowania całej aplikacji.
8. Trenuj model
- Dodaj przycisk „Train” bezpośrednio po przycisku „Noise” w treści pliku index.html:
<br/><br/>
<button id="train" onclick="train()">Train</button>
- Dodaj ten kod do istniejącego kodu w pliku 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;
}
- Wywołaj funkcję
buildModel()po wczytaniu aplikacji:
async function app() {
recognizer = speechCommands.create('BROWSER_FFT');
await recognizer.ensureModelLoaded();
// Add this line.
buildModel();
}
Jeśli teraz odświeżysz aplikację, zobaczysz nowy przycisk „Trenuj”. Aby przetestować trenowanie, ponownie zbierz dane i kliknij „Trenuj” lub poczekaj do kroku 10, aby przetestować trenowanie wraz z prognozowaniem.
Szczegółowe informacje
Ogólnie rzecz biorąc, wykonujemy 2 czynności: buildModel() określamy architekturę modelu i train() trenujemy model przy użyciu zebranych danych.
Architektura modelu
Model ma 4 warstwy: warstwę konwolucyjną, która przetwarza dane audio (reprezentowane jako spektrogram), warstwę maksymalnego pulowania, warstwę spłaszczania i warstwę gęstą, która mapuje 3 działania:
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'}));
Kształt danych wejściowych modelu to [NUM_FRAMES, 232, 1], gdzie każda ramka to 23 ms dźwięku zawierającego 232 liczby odpowiadające różnym częstotliwościom (232 zostało wybrane, ponieważ jest to liczba przedziałów częstotliwości potrzebnych do uchwycenia ludzkiego głosu). W tym laboratorium kodowania używamy próbek o długości 3 klatek (ok. 70 ms), ponieważ do sterowania suwakiem wydajemy dźwięki, a nie wypowiadamy całych słów.
Kompilujemy model, aby przygotować go do trenowania:
const optimizer = tf.train.adam(0.01);
model.compile({
optimizer,
loss: 'categoricalCrossentropy',
metrics: ['accuracy']
});
Używamy optymalizatora Adam, powszechnie stosowanego w deep learningu, oraz funkcji categoricalCrossEntropy jako funkcji utraty, która jest standardową funkcją utraty używaną do klasyfikacji. Mówiąc w skrócie, mierzy, jak bardzo prawdopodobieństwa prognozowane (jedno prawdopodobieństwo na klasę) odbiegają od 100% prawdopodobieństwa w przypadku prawdziwej klasy i 0% prawdopodobieństwa w przypadku wszystkich pozostałych klas. Podajemy też accuracy jako wskaźnik do monitorowania, który pokazuje odsetek przykładów, które model prawidłowo sklasyfikował po każdej epoce trenowania.
Szkolenie
Trenowanie odbywa się 10 razy (epok) na danych przy wielkości wsadu wynoszącej 16 (przetwarzanie 16 przykładów naraz) i wyświetla bieżącą dokładność w interfejsie:
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. Aktualizowanie suwaka w czasie rzeczywistym
Model jest już gotowy do trenowania, więc dodajmy kod, który umożliwi prognozowanie w czasie rzeczywistym i przesuwanie suwaka. Dodaj ten kod bezpośrednio po przycisku „Train” w pliku index.html:
<br/><br/>
<button id="listen" onclick="listen()">Listen</button>
<input type="range" id="output" min="0" max="10" step="0.1">
A ten kod w pliku 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
});
}
Szczegółowe informacje
Prognozowanie w czasie rzeczywistym
listen() słucha mikrofonu i generuje prognozy w czasie rzeczywistym. Kod jest bardzo podobny do metody collect(), która normalizuje surowy spektrogram i usuwa wszystkie klatki z wyjątkiem ostatnich NUM_FRAMES. Jedyna różnica polega na tym, że wywołujemy też wytrenowany model, aby uzyskać prognozę:
const probs = model.predict(input);
const predLabel = probs.argMax(1);
await moveSlider(predLabel);
Wartością wyjściową funkcji model.predict(input)jest tensor o kształcie [1, numClasses] reprezentujący rozkład prawdopodobieństwa liczby klas. Mówiąc prościej, jest to po prostu zestaw wartości ufności dla każdej z możliwych klas wyjściowych, których suma wynosi 1. Tensor ma wymiar zewnętrzny równy 1, ponieważ jest to rozmiar partii (pojedynczy przykład).
Aby przekształcić rozkład prawdopodobieństwa w pojedynczą liczbę całkowitą reprezentującą najbardziej prawdopodobną klasę, wywołujemy funkcję probs.argMax(1), która zwraca indeks klasy o najwyższym prawdopodobieństwie. Jako parametr osi przekazujemy wartość „1”, ponieważ chcemy obliczyć argMax w ostatnim wymiarze, numClasses.
Aktualizowanie suwaka
moveSlider() zmniejsza wartość suwaka, jeśli etykieta ma wartość 0 („Lewy”), zwiększa ją, jeśli etykieta ma wartość 1 („Prawy”), i ignoruje, jeśli etykieta ma wartość 2 („Szum”).
Usuwanie tensorów
Aby zwolnić miejsce w pamięci GPU, musimy ręcznie wywołać funkcję tf.dispose() na tensorach wyjściowych. Alternatywą dla ręcznego używania funkcji tf.dispose() jest umieszczanie wywołań funkcji w funkcji tf.tidy(), ale nie można jej używać z funkcjami asynchronicznymi.
tf.dispose([input, probs, predLabel]);
10. Testowanie gotowej aplikacji
Otwórz plik index.html w przeglądarce i zbierz dane w taki sam sposób jak w poprzedniej sekcji, używając 3 przycisków odpowiadających 3 poleceniom. Pamiętaj, aby podczas zbierania danych naciskać i przytrzymywać każdy przycisk przez 3–4 sekundy.
Po zebraniu przykładów kliknij przycisk „Trenuj”. Rozpocznie to trenowanie modelu i powinna się pojawić dokładność modelu powyżej 90%. Jeśli nie uzyskasz dobrej skuteczności modelu, spróbuj zebrać więcej danych.
Po zakończeniu trenowania kliknij przycisk „Posłuchaj”, aby uzyskać prognozy z mikrofonu i sterować suwakiem.
Więcej samouczków znajdziesz na stronie http://js.tensorflow.org/.