1. Введение
В этом практическом занятии вы создадите нейронную сеть для распознавания звука и будете использовать её для управления ползунком в браузере с помощью звуков. Вы будете использовать TensorFlow.js — мощную и гибкую библиотеку машинного обучения для JavaScript.
Сначала вы загрузите и запустите предварительно обученную модель , способную распознавать 20 речевых команд. Затем, используя микрофон, вы создадите и обучите простую нейронную сеть, которая будет распознавать ваши звуки и заставлять ползунок двигаться влево или вправо.
В этом практическом занятии теория, лежащая в основе моделей распознавания звука, рассматриваться не будет. Если вас это интересует, посмотрите этот учебный материал .
Мы также создали глоссарий терминов машинного обучения, который вы найдете в этом практическом задании.
Что вы узнаете
- Как загрузить предварительно обученную модель распознавания речевых команд
- Как делать прогнозы в реальном времени с помощью микрофона
- Как обучить и использовать собственную модель распознавания звука с помощью микрофона браузера
Итак, начнём.
2. Требования
Для выполнения этого практического задания вам потребуется:
- Последняя версия Chrome или другой современный браузер.
- Текстовый редактор, работающий локально на вашем компьютере или в интернете, например, через Codepen или Glitch .
- Знание HTML, CSS, JavaScript и инструментов разработчика Chrome (или инструментов разработчика вашего предпочитаемого браузера).
- Общее концептуальное понимание нейронных сетей. Если вам нужно вводное ознакомление или повторение материала, посмотрите это видео от 3blue1brown или это видео о глубоком обучении на JavaScript от Аши Кришнан .
3. Загрузите TensorFlow.js и модель Audio.
Откройте 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> импортирует предварительно обученную модель речевых команд . Тег <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, а также несколько дополнительных команд, таких как «влево», «вправо», «да», «нет» и т. д.
Произнесите одно из этих слов. Правильно ли модель его поняла? Поэкспериментируйте с параметром probabilityThreshold , который контролирует частоту срабатывания модели – значение 0,75 означает, что модель сработает, когда она уверена более чем на 75%, что услышала данное слово.
Чтобы узнать больше о модели речевых команд и ее API, см. файл README.md на Github.
6. Сбор данных
Чтобы было интереснее, давайте вместо целых слов будем использовать короткие звуки для управления ползунком!
Вам предстоит обучить модель распознавать 3 различных команды: «Влево», «Вправо» и «Шум», которые будут перемещать ползунок влево или вправо. Распознавание команды «Шум» (не требующей никаких действий) имеет решающее значение для распознавания речи, поскольку нам нужно, чтобы ползунок реагировал только тогда, когда мы произносим нужный звук, а не когда мы говорим и двигаемся.
- Сначала нам нужно собрать данные. Добавьте в приложение простой пользовательский интерфейс, добавив следующий код внутри тега
<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.
}
Разберем это по пунктам
Этот код может показаться сложным на первый взгляд, поэтому давайте разберем его по частям.
Мы добавили в наш пользовательский интерфейс три кнопки с надписями «Влево», «Вправо» и «Шум», соответствующие трем командам, которые мы хотим, чтобы наша модель распознавала. Нажатие этих кнопок вызывает нашу недавно добавленную функцию collect() , которая создает обучающие примеры для нашей модели.
collect() связывает label с результатом вызова recognizer.listen() . Поскольку includeSpectrogram имеет значение true , 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. Обучить модель
- Добавьте кнопку « Поезд » сразу после кнопки « Шум » в теле документа 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, чтобы протестировать обучение вместе с прогнозированием.
Разберем это по пунктам
В общих чертах мы делаем две вещи: buildModel() определяет архитектуру модели, а train() обучает модель, используя собранные данные.
Модель архитектуры
Модель имеет 4 слоя: сверточный слой, обрабатывающий аудиоданные (представленные в виде спектрограммы), слой максимального пулинга, слой сглаживания и полносвязный слой, который соответствует 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 было выбрано потому, что это количество частотных интервалов, необходимых для захвата человеческого голоса). В этом практическом задании мы используем сэмплы длиной в 3 кадра (~70 мс), поскольку мы издаем звуки, а не произносим целые слова, чтобы управлять ползунком.
Мы компилируем нашу модель, чтобы подготовить её к обучению:
const optimizer = tf.train.adam(0.01);
model.compile({
optimizer,
loss: 'categoricalCrossentropy',
metrics: ['accuracy']
});
Мы используем оптимизатор Adam , распространенный оптимизатор, применяемый в глубоком обучении, и функцию потерь categoricalCrossEntropy , стандартную функцию потерь, используемую для классификации. Вкратце, она измеряет, насколько предсказанные вероятности (одна вероятность на класс) далеки от 100% вероятности в истинном классе и 0% вероятности для всех остальных классов. Мы также предоставляем accuracy в качестве метрики для мониторинга, которая покажет нам процент примеров, которые модель правильно обрабатывает после каждой эпохи обучения.
Обучение
Обучение выполняется 10 раз (эпох) на данных с использованием пакета размером 16 (обработка 16 примеров за раз), и текущая точность отображается в пользовательском интерфейсе:
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) является тензор формы [1, numClasses] , представляющий распределение вероятностей по количеству классов. Проще говоря, это просто набор значений достоверности для каждого из возможных выходных классов, сумма которых равна 1. Внешний размер тензора равен 1, поскольку это размер пакета (одного примера).
Чтобы преобразовать распределение вероятностей в одно целое число, представляющее наиболее вероятный класс, мы вызываем probs.argMax(1) , который возвращает индекс класса с наибольшей вероятностью. Мы передаем "1" в качестве параметра axis, потому что хотим вычислить argMax по последнему измерению, numClasses .
Обновление ползунка
moveSlider() уменьшает значение ползунка, если метка равна 0 ("Левая"), увеличивает его, если метка равна 1 ("Правая"), и игнорирует значение, если метка равна 2 ("Шум").
Размещение тензоров
Для очистки памяти графического процессора важно вручную вызывать tf.dispose() для выходных тензоров. Альтернативой ручному tf.dispose() является обертывание вызовов функций в tf.tidy() , но это нельзя использовать с асинхронными функциями.
tf.dispose([input, probs, predLabel]);
10. Протестируйте готовое приложение.
Откройте файл index.html в браузере и соберите данные, как и в предыдущем разделе, используя 3 кнопки, соответствующие 3 командам. Не забудьте нажимать и удерживать каждую кнопку в течение 3-4 секунд во время сбора данных.
После сбора примеров нажмите кнопку «Обучить» . Начнётся обучение модели, и вы должны увидеть, что точность модели превысит 90%. Если вы не добились хороших результатов, попробуйте собрать больше данных.
После завершения обучения нажмите кнопку «Слушать» , чтобы получать прогнозы с микрофона и управлять ползунком!
Больше обучающих материалов вы найдете по адресу http://js.tensorflow.org/.