TensorFlow.js - 転移学習を使用した音声認識

1. はじめに

この Codelab では、音声認識ネットワークを作成し、このネットワークを使用して音を鳴らしてブラウザのスライダーを制御します。JavaScript 用の強力で柔軟な ML ライブラリである TensorFlow.js を使用します。

まず、20 個の音声コマンドを認識できる事前トレーニング済みモデルを読み込んで実行します。次に、マイクを使用して、ユーザーの音を認識してスライダーを左右に動かす簡単なニューラル ネットワークを構築してトレーニングします。

この Codelab では、音声認識モデルの背後にある理論については説明しません。詳しくは、こちらのチュートリアルをご覧ください。

また、この Codelab で使用する ML 用語の用語集も作成しました。

学習内容

  • 事前トレーニング済みの音声コマンド認識モデルを読み込む方法
  • マイクを使用してリアルタイム予測を行う方法
  • ブラウザのマイクを使用してカスタム音声認識モデルをトレーニングして使用する方法

では始めましょう。

2. 要件

この Codelab を完了するには、以下が必要です。

  1. Chrome の最新バージョンまたは他の最新のブラウザ。
  2. テキスト エディタ。ローカルのマシン上で実行するか、CodepenGlitch などを介してウェブ上で実行します。
  3. HTML、CSS、JavaScript、Chrome DevTools(または推奨ブラウザ開発ツール)に関する知識。
  4. ニューラル ネットワークのコンセプトの概要。概要の説明や復習が必要な場合は、3blue1brown によるこちらの動画または Ashi Krishnan による 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>

1 つ目の <script> タグは TensorFlow.js ライブラリをインポートし、2 つ目の <script> タグはトレーニング済みの Speech Commands モデルをインポートします。<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 までの数字と、「left」、「right」、「yes」、「no」などのいくつかの追加コマンドを認識するようにトレーニングされています。

これらの単語のいずれか 1 つをお話しください。正しく言い換えられていますか?モデルが起動される頻度を制御する probabilityThreshold を試してみましょう。0.75 は、モデルが特定の単語を聞き取っているという確証が 75% 以上に達したときに起動されることを意味します。

Speech Commands モデルとその API について詳しくは、GitHub の README.md をご覧ください。

6. データの収集

スライダーを動かして、文字全体ではなく短い音で楽しみましょう。

「左」、「右」という 3 つの異なるコマンドを認識するようにモデルをトレーニングします。「ノイズ」スライダーを左右に動かします。「ノイズ」を認識する(対応不要)は、音声検出において重要です。これは、一般的に話して動き回っているときではなく、適切な音が生成されたときにのみスライダーが反応するようにしたいからです。

  1. まず、データを収集する必要があります。シンプルな 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>
  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.
}

詳細

このコードは最初はわかりにくいため、細かく見てみましょう。

モデルに認識させる 3 つのコマンドに対応する「Left」、「Right」、「Noise」という 3 つのボタンを UI に追加しました。これらのボタンを押すと、新しく追加された collect() 関数が呼び出され、モデルのトレーニング サンプルが作成されます。

collect()labelrecognizer.listen() の出力に関連付けます。includeSpectrogram が true であるため、, recognizer.listen() は、1 秒間の音声の未加工のスペクトログラム(周波数データ)を 43 フレームに分割して取得し、各フレームは約 23 ms の音声となります。

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****: 「Left」、「Right」は 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 個のサンプルを収集する必要があります。たとえば、「左」には指を鳴らす、「右」には指を鳴らす、「ノイズ」には消音と話しかける、を切り替えることができます。

サンプル数を増やすと、ページに表示されるカウンタが増えます。コンソールの examples 変数に対して console.log() を呼び出して、必要に応じてデータを検査することもできます。この時点での目標は、データ収集プロセスをテストすることです。後でアプリ全体をテストする際に、データを再収集します。

8. モデルをトレーニングする

  1. Train」を追加します。「ノイズ」の直後のボタン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();
}

この時点でアプリを更新すると、新しい [Train] が表示されます。] ボタンを離します。トレーニングをテストするには、データを再収集して [トレーニング] をクリックします。ステップ 10 まで待ってから、予測とともにトレーニングをテストすることもできます。

詳細

大まかには、buildModel() でモデル アーキテクチャを定義し、train() で収集したデータを使用してモデルをトレーニングします。

モデル アーキテクチャ

このモデルには、オーディオ データを処理する畳み込みレイヤ(スペクトログラムとして表される)、最大プールレイヤ、フラットレイヤ、3 つのアクションにマッピングされる密レイヤの 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] です。各フレームは、異なる周波数に対応する 232 個の数字を含む 23 ms の音声です(232 が選択されたのは、人間の声をキャプチャするために必要な周波数バケットの量であるため)。この Codelab では、3 フレームの長さのサンプル(約 70 ミリ秒のサンプル)を使用します。これは、スライダーをコントロールするために言葉全体を話すのではなく、音を鳴らすためです。

モデルをコンパイルして、トレーニングの準備をします。

const optimizer = tf.train.adam(0.01);
 model.compile({
   optimizer,
   loss: 'categoricalCrossentropy',
   metrics: ['accuracy']
 });

ディープ ラーニングで使用される一般的なオプティマイザーである Adam オプティマイザーと、分類に使用される標準的な損失関数である categoricalCrossEntropy を使用します。要するに、予測された確率(クラスごとに 1 つの確率)が、実際のクラスでは 100% の確率、他のすべてのクラスでは 0% の確率がどの程度のものであるかを測定します。また、モニタリングする指標として accuracy を指定します。これにより、トレーニングの各エポックの後にモデルが正しく取得されるサンプルの割合がわかります。

トレーニング

16 のバッチサイズを使用してデータを 10 回(エポック)トレーニングし(一度に 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() はマイクを認識し、リアルタイムで予測を行います。このコードは、未加工のスペクトログラムを正規化し、最後の NUM_FRAMES フレームを除くすべてのフレームを削除する collect() メソッドとよく似ています。唯一の違いは、予測を取得するためにトレーニング済みモデルも呼び出していることです。

const probs = model.predict(input);
const predLabel = probs.argMax(1);
await moveSlider(predLabel);

model.predict(input) の出力は、クラス数に対する確率分布を表す [1, numClasses] 形状のテンソルです。簡単に言うと、これは可能な出力クラスごとの信頼度のセットで、合計が 1 になります。テンソルの外側の次元は 1 です。これがバッチのサイズ(1 つの例)だからです。

確率分布を最も可能性の高いクラスを表す単一の整数に変換するには、probs.argMax(1) を呼び出します。これにより、最も高い確率を持つクラス インデックスが返されます。「1」というを軸パラメータとして渡します。これは、最後のディメンション numClasses に対して argMax を計算するためです。

スライダーの更新

moveSlider() は、ラベルが 0(「左」)の場合はスライダーの値を小さくし、ラベルが 1(「右」)の場合はスライダーの値を増やし(「右」)、ラベルが 2(「ノイズ」)の場合は無視します。

テンソルの破棄

GPU メモリをクリーンアップするには、出力テンソルに対して tf.dispose() を手動で呼び出すことが重要です。手動の tf.dispose() の代わりに、関数呼び出しを tf.tidy() でラップすることもできますが、これは非同期関数では使用できません。

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

10. 完成したアプリをテストする

ブラウザで index.html を開き、3 つのコマンドに対応する 3 つのボタンを使用して、前のセクションと同様にデータを収集します。データを収集している間、各ボタンを 3 ~ 4 秒間長押ししてください。

サンプルを収集したら、[トレーニング] ボタンを押します。モデルのトレーニングが始まり、モデルの精度が 90% を超えていることが確認できます。優れたモデルのパフォーマンスが得られない場合は、より多くのデータを収集してみてください。

トレーニングが完了したら、[Listen] ボタンを押してマイクから予測を行い、スライダーを操作します。

他のチュートリアルについては、http://js.tensorflow.org/ をご覧ください。