TensorFlow.js - 전이 학습을 사용한 오디오 인식

1. 소개

이 Codelab에서는 오디오 인식 네트워크를 빌드하고 이를 사용하여 소리를 내며 브라우저의 슬라이더를 제어합니다. 강력하고 유연한 JavaScript용 머신러닝 라이브러리인 TensorFlow.js를 사용하게 됩니다.

먼저 20개의 음성 명령어를 인식할 수 있는 선행 학습된 모델을 로드하고 실행합니다. 그런 다음 마이크를 사용하여 소리를 인식하고 슬라이더를 왼쪽이나 오른쪽으로 이동시키는 간단한 신경망을 만들고 학습시킵니다.

이 Codelab에서는 오디오 인식 모델의 이론을 다루지 않습니다. 자세한 내용은 이 튜토리얼을 참조하세요.

이 Codelab에서 찾을 수 있는 머신러닝 용어 용어집도 만들었습니다.

학습할 내용

  • 선행 학습된 음성 명령 인식 모델을 로드하는 방법
  • 마이크를 사용하여 실시간으로 예측하는 방법
  • 브라우저 마이크를 사용하여 커스텀 오디오 인식 모델을 학습시키고 사용하는 방법

그럼 시작해 보겠습니다

2. 요구사항

이 Codelab을 완료하려면 다음이 필요합니다.

  1. 최신 버전의 Chrome 또는 다른 최신 브라우저
  2. 컴퓨터에서 로컬로 또는 Codepen 또는 Glitch 등을 통해 웹에서 실행되는 텍스트 편집기
  3. HTML, CSS, JavaScript, Chrome DevTools (또는 선호하는 브라우저의 개발 도구)에 관한 지식
  4. 신경망에 관한 대략적인 개념 이해 소개나 복습이 필요한 경우 3blue1brown의 동영상 또는 아시 크리슈난의 JavaScript 딥 러닝에 관한 동영상을 시청해 보세요.

3. TensorFlow.js 및 오디오 모델 로드

편집기에서 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와 'left', 'right', 'yes', 'no' 등의 몇 가지 추가 명령어를 인식하도록 학습되었습니다.

말씀하신 단어 중 하나를 말씀해 주세요. 단어가 올바르게 입력되었나요? 모델의 실행 빈도를 제어하는 probabilityThreshold를 사용해 봅니다. 0.75는 주어진 단어를 인식한다고 75% 이상 확신할 때 모델이 실행됨을 의미합니다.

음성 명령어 모델 및 API에 관한 자세한 내용은 GitHub의 README.md를 참고하세요.

6. 데이터 수집

재미를 위해 전체 단어 대신 짧은 소리로 슬라이더를 제어해 보겠습니다.

'왼쪽', '오른쪽'이라는 3가지 명령어를 인식하도록 모델을 학습시킬 것입니다. '노이즈' 그러면 슬라이더가 왼쪽이나 오른쪽으로 이동합니다. '노이즈' 인식 (조치 필요 없음)는 음성 감지에서 매우 중요한데, 우리가 말하고 움직일 때가 아니라 적절한 소리가 나는 경우에만 슬라이더가 반응하도록 하기 때문입니다.

  1. 먼저 데이터를 수집해야 합니다. <div id="console"> 앞에 <body> 태그 내에 다음을 추가하여 앱에 간단한 UI를 추가합니다.
<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. app()에서 predictWord()을 삭제합니다.
async function app() {
 recognizer = speechCommands.create('BROWSER_FFT');
 await recognizer.ensureModelLoaded();
 // predictWord() no longer called.
}

상세 설명

이 코드는 처음에는 어려울 수 있으므로 자세히 살펴보겠습니다.

모델이 인식하도록 하려는 세 가지 명령에 대응하는 '왼쪽', '오른쪽', '노이즈'라는 라벨이 지정된 버튼 세 개를 UI에 추가했습니다. 이 버튼을 누르면 새로 추가된 collect() 함수가 호출되어 모델의 학습 예시가 생성됩니다.

collect()labelrecognizer.listen()의 출력과 연결합니다. includeSpectrogram가 true이므로, recognizer.listen()는 1초의 오디오에 대한 원시 스펙트로그램 (주파수 데이터)을 43프레임으로 나눠 제공하므로 각 프레임은 최대 23ms의 오디오입니다.

recognizer.listen(async ({spectrogram: {frameSize, data}}) => {
...
}, {includeSpectrogram: true});

단어 대신 짧은 사운드를 사용하여 슬라이더를 제어하려고 하므로 마지막 3프레임 (~70ms)만 고려합니다.

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 '노이즈' 각각 1개의 값으로 사용합니다.
  • 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()는 수집된 데이터를 사용하여 모델을 학습시킵니다.

모델 아키텍처

이 모델에는 오디오 데이터 (스펙트로그램으로 표시됨)를 처리하는 컨볼루셔널 레이어, 최대 풀 레이어, 평탄화 레이어, 세 가지 동작에 매핑되는 밀집 레이어, 이렇게 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개의 숫자가 포함된 23ms의 오디오입니다. 232는 사람의 음성을 캡처하는 데 필요한 주파수 버킷의 양이므로 232가 선택되었습니다. 이 Codelab에서는 전체 단어를 말하는 대신 소리를 만들어 슬라이더를 제어하기 때문에 길이가 3프레임 (약 70ms 샘플)인 샘플을 사용합니다.

모델을 컴파일하여 학습에 사용할 수 있도록 합니다.

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

딥 러닝에 사용되는 일반적인 옵티마이저인 Adam 옵티마이저와 분류에 사용되는 표준 손실 함수인 손실 categoricalCrossEntropy를 사용합니다. 간단히 말해 예측 확률 (클래스당 하나의 확률)이 실제 클래스의 확률이 100% 이고 다른 모든 클래스의 확률이 0% 인 정도를 측정합니다. 또한 모니터링할 측정항목으로 accuracy를 제공합니다. 이를 통해 각 학습 세대 후 모델이 올바른 예시의 비율을 확인할 수 있습니다.

교육

학습은 16개의 배치 크기 (한 번에 16개의 예시 처리)를 사용하여 데이터에 대해 10회 (에포크) 동안 진행하며 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입니다. 이것이 배치의 크기이기 때문입니다 (단일 예).

확률 분포를 가장 가능성이 높은 클래스를 나타내는 단일 정수로 변환하기 위해 확률이 가장 높은 클래스 색인을 반환하는 probs.argMax(1)를 호출합니다. 여기서는 '1' 축 매개변수로 사용합니다.argMaxnumClasses

슬라이더 업데이트

moveSlider()은 라벨이 0('왼쪽')인 경우 슬라이더의 값을 줄이고, 라벨이 1('오른쪽')이면 슬라이더 값을 늘리고, 라벨이 2('노이즈')인 경우 무시합니다.

텐서 처리

GPU 메모리를 정리하려면 출력 텐서에서 tf.dispose()를 수동으로 호출해야 합니다. 수동 tf.dispose()의 대안은 함수 호출을 tf.tidy()에 래핑하는 것이지만 비동기 함수와 함께 사용할 수 없습니다.

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

10. 최종 앱 테스트

브라우저에서 index.html을 열고 3개의 명령어에 해당하는 버튼 3개로 이전 섹션에서와 마찬가지로 데이터를 수집합니다. 데이터를 수집하는 동안 각 버튼을 3~4초 동안 길게 누르세요.

예시를 수집했다면 '학습' 버튼을 누릅니다. 그러면 모델 학습이 시작되고 모델의 정확성이 90%를 넘은 것을 확인할 수 있습니다. 좋은 모델 성능을 얻지 못하면 더 많은 데이터를 수집해 보세요.

학습이 끝나면 '듣기' 버튼을 눌러 마이크에서 예측을 실행하고 슬라이더를 제어하세요.

http://js.tensorflow.org/에서 더 많은 튜토리얼을 확인하세요.