TensorFlow.js — распознавание звука с использованием трансферного обучения

1. Введение

В этой лаборатории кода вы создадите сеть распознавания звука и будете использовать ее для управления ползунком в браузере с помощью звуков. Вы будете использовать TensorFlow.js, мощную и гибкую библиотеку машинного обучения для Javascript.

Сначала вы загрузите и запустите предварительно обученную модель , которая сможет распознавать 20 речевых команд. Затем с помощью микрофона вы создадите и обучите простую нейронную сеть, которая распознает ваши звуки и заставляет ползунок двигаться влево или вправо.

В этой лаборатории не будет рассматриваться теория, лежащая в основе моделей распознавания звука. Если вам это интересно, посмотрите этот урок .

Мы также создали глоссарий терминов машинного обучения, который вы найдете в этой лаборатории.

Что вы узнаете

  • Как загрузить предварительно обученную модель распознавания речевых команд
  • Как делать прогнозы в реальном времени с помощью микрофона
  • Как обучить и использовать собственную модель распознавания звука с помощью микрофона браузера

Итак, давайте начнем.

2. Требования

Для выполнения этой лаборатории кода вам понадобится:

  1. Последняя версия Chrome или другой современный браузер.
  2. Текстовый редактор, работающий либо локально на вашем компьютере, либо в Интернете через что-то вроде Codepen или Glitch .
  3. Знание HTML, CSS, JavaScript и инструментов разработчика Chrome (или инструментов разработчика предпочитаемого вами браузера).
  4. Концептуальное понимание нейронных сетей высокого уровня. Если вам нужно введение или повышение квалификации, рассмотрите возможность просмотра этого видео от 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 Threshold, которая контролирует частоту срабатывания модели — 0,75 означает, что модель сработает, когда она более чем на 75% уверена, что слышит данное слово.

Чтобы узнать больше о модели речевых команд и ее API, см. README.md на Github.

6. Соберите данные

Чтобы было интереснее, давайте вместо целых слов использовать короткие звуки для управления ползунком!

Вы собираетесь научить модель распознавать три разные команды: «Влево», «Вправо» и «Шум», которые заставят ползунок двигаться влево или вправо. Распознавание «Шума» (никаких действий не требуется) имеет решающее значение при обнаружении речи, поскольку мы хотим, чтобы ползунок реагировал только тогда, когда мы издаем правильный звук, а не когда мы вообще говорим и двигаемся.

  1. Сначала нам нужно собрать данные. Добавьте в приложение простой пользовательский интерфейс, добавив его в тег <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.
}

Разбивая это

Поначалу этот код может показаться сложным, поэтому давайте разберем его.

Мы добавили в наш пользовательский интерфейс три кнопки с названиями «Влево», «Вправо» и «Шум», соответствующие трем командам, которые мы хотим, чтобы наша модель распознавала. Нажатие этих кнопок вызывает нашу недавно добавленную функцию 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 кнопки, соответствующие трем командам. Если вы работаете с локальным файлом, для доступа к микрофону вам придется запустить веб-сервер и использовать http://localhost:port/ .

Чтобы запустить простой веб-сервер на порту 8000:

python -m SimpleHTTPServer

Чтобы собрать примеры для каждой команды, несколько раз (или непрерывно) издавайте одинаковый звук, нажимая и удерживая каждую кнопку в течение 3–4 секунд. Вам необходимо собрать около 150 примеров для каждого ярлыка. Например, мы можем щелкать пальцами, говоря «Влево», свистеть, чтобы сказать «Право», и чередовать молчание и разговор, чтобы сказать «Шум».

По мере того, как вы собираете больше примеров, счетчик, отображаемый на странице, должен увеличиваться. Не стесняйтесь также проверять данные, вызывая console.log() для переменной examples в консоли. На этом этапе цель состоит в том, чтобы протестировать процесс сбора данных. Позже вы повторно соберете данные при тестировании всего приложения.

8. Обучите модель

  1. Добавьте кнопку « Поезд » сразу после кнопки « Шум » в тексте 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();
}

На этом этапе, если вы обновите приложение, вы увидите новую кнопку « Поезд ». Вы можете протестировать обучение, повторно собрав данные и нажав «Обучить», или дождаться шага 10, чтобы протестировать обучение вместе с прогнозом.

Разбивая это

На высоком уровне мы делаем две вещи: buildModel() определяет архитектуру модели, а train() обучает модель, используя собранные данные.

Модельная архитектура

Модель имеет 4 слоя: сверточный слой, который обрабатывает аудиоданные (представленные в виде спектрограммы), уровень максимального пула, сглаживающий слой и плотный слой, который соответствует трем действиям:

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']
 });

Мы используем оптимизатор Адама , распространенный оптимизатор, используемый в глубоком обучении, и 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. Обновляйте слайдер в режиме реального времени.

Теперь, когда мы можем обучить нашу модель, давайте добавим код для прогнозирования в режиме реального времени и переместим ползунок. Добавьте это сразу после кнопки « 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. Тензор имеет внешнее измерение, равное 1, потому что это размер пакета (единственный пример).

Чтобы преобразовать распределение вероятностей в одно целое число, представляющее наиболее вероятный класс, мы вызываем probs.argMax(1) , который возвращает индекс класса с наибольшей вероятностью. Мы передаем «1» в качестве параметра оси, потому что хотим вычислить argMax по последнему измерению, numClasses .

Обновление слайдера

moveSlider() уменьшает значение ползунка, если метка равна 0 («Влево»), увеличивает его, если метка равна 1 («Вправо»), и игнорирует, если метка равна 2 («Шум»).

Удаление тензоров

Чтобы очистить память графического процессора, нам важно вручную вызвать tf.dispose() для выходных тензоров. Альтернативой ручному tf.dispose() является перенос вызовов функций в tf.tidy() , но это нельзя использовать с асинхронными функциями.

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

10. Протестируйте финальное приложение

Откройте index.html в своем браузере и соберите данные, как вы это делали в предыдущем разделе, с помощью трех кнопок, соответствующих трем командам. Не забудьте нажать и удерживать каждую кнопку в течение 3–4 секунд во время сбора данных.

Собрав примеры, нажмите кнопку «Обучить» . Это начнет обучение модели, и вы увидите, что точность модели превысит 90%. Если вам не удалось добиться хорошей производительности модели, попробуйте собрать больше данных.

После завершения тренировки нажмите кнопку «Слушать», чтобы делать прогнозы с микрофона и управлять ползунком!

Дополнительные руководства см. на http://js.tensorflow.org/.