1. 소개
이 Codelab에서는 음성을 내어 브라우저에서 슬라이더를 제어하는 데 사용할 수 있는 음성 인식 네트워크를 빌드합니다. 강력하면서도 유연한 자바스크립트 머신러닝 라이브러리인 TensorFlow.js를 사용합니다.
먼저 20개의 음성 명령어를 인식할 수 있는 사전 학습된 모델을 로드하고 실행합니다. 그런 다음 마이크를 사용하여 음성을 인식하고 슬라이더를 왼쪽이나 오른쪽으로 이동시키는 간단한 신경망을 빌드하고 학습시킵니다.
이 Codelab에서는 음성 인식 모델의 배경 이론에 대해서는 다루지 않습니다. 이론에 대해 궁금하다면 이 가이드를 확인하세요.
이 Codelab에서 볼 수 있는 머신러닝 용어의 용어집도 마련해 두었습니다.
학습할 내용
- 사전 학습된 음성 명령어 인식 모델을 로드하는 방법
- 마이크를 사용하여 실시간 예측을 수행하는 방법
- 브라우저 마이크를 사용하여 커스텀 음성 인식 모델을 학습시키고 사용하는 방법
그럼 시작해 보겠습니다
2. 요구사항
이 Codelab을 완료하려면 다음이 필요합니다.
- 최신 버전의 Chrome 또는 다른 최신 브라우저
- 텍스트 편집기(머신에서 로컬로 실행되거나 Codepen 또는 Glitch와 같은 것을 통해 웹에서 실행)
- HTML, CSS, 자바스크립트, Chrome DevTools (또는 선호하는 브라우저의 개발 도구)에 대한 지식
- 신경망에 대한 대략적인 개념 이해. 소개 또는 복습이 필요하다면 3blue1brown의 동영상 또는 아시 크리슈난의 자바스크립트 딥 러닝 동영상을 확인하세요.
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. 데이터 수집
재미를 위해 전체 단어 대신 짧은 소리를 사용하여 슬라이더를 제어해 보겠습니다.
슬라이더를 왼쪽이나 오른쪽으로 이동시키는 'Left', 'Right', 'Noise'라는 세 가지 명령어를 인식하도록 모델을 학습시킵니다. 슬라이더가 일반적인 말이나 움직임이 아닌 올바른 소리를 낼 때만 반응하도록 하려면 음성 감지에서 'Noise'(작업 필요 없음)를 인식하는 것이 중요합니다.
- 먼저 데이터를 수집해야 합니다.
<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>
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);
}
app()에서predictWord()를 삭제합니다.
async function app() {
recognizer = speechCommands.create('BROWSER_FFT');
await recognizer.ensureModelLoaded();
// predictWord() no longer called.
}
상세 설명
이 코드는 처음에는 이해하기 어려울 수 있으므로 세부적으로 살펴보겠습니다.
모델이 인식하도록 하려는 세 가지 명령어에 해당하는 'Left', 'Right', 'Noise'라는 세 가지 버튼을 UI에 추가했습니다. 이러한 버튼을 누르면 새로 추가된 collect() 함수가 호출되어 모델의 학습 예시가 생성됩니다.
collect()는 label을 recognizer.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);
마지막으로 각 학습 예시에는 다음 두 필드가 있습니다.
label****: 'Left', 'Right', 'Noise'의 경우 각각 0, 1, 2입니다.vals****: 주파수 정보 (스펙트로그램)를 보유하는 696개의 숫자
모든 데이터는 examples 변수에 저장합니다.
examples.push({vals, label});
7. 데이터 수집 테스트
브라우저에서 index.html 을 열면 세 가지 명령어에 해당하는 세 가지 버튼이 표시됩니다. 로컬 파일에서 작업하는 경우 마이크에 액세스하려면 웹 서버를 시작하고 http://localhost:port/를 사용해야 합니다.
포트 8000에서 간단한 웹 서버를 시작하려면 다음을 실행합니다.
python -m SimpleHTTPServer
각 명령어의 예시를 수집하려면 각 버튼을 3~4초 동안 누르고 있는 상태에서 일관된 소리를 반복적으로 (또는 지속적으로) 냅니다. 각 라벨에 대해 약 150개의 예시를 수집해야 합니다. 예를 들어 'Left'의 경우 손가락을 튕기고 'Right'의 경우 휘파람을 불고 'Noise'의 경우 침묵과 말하기를 번갈아 할 수 있습니다.
예시를 더 많이 수집할수록 페이지에 표시된 카운터가 증가합니다. 콘솔에서 examples 변수에 console.log()를 호출하여 데이터를 검사할 수도 있습니다. 이 시점에서 목표는 데이터 수집 프로세스를 테스트하는 것입니다. 나중에 전체 앱을 테스트할 때 데이터를 다시 수집합니다.
8. 모델 학습
- index.html 의 본문에서 "Noise" 버튼 바로 뒤에 "Train" 버튼을 추가합니다.
<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();
}
이 시점에서 앱을 새로고침하면 새 "Train" 버튼이 표시됩니다. 데이터를 다시 수집하고 '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개의 숫자가 포함된 23ms의 오디오입니다 (232는 사람의 목소리를 포착하는 데 필요한 주파수 버킷의 양이므로 선택되었습니다). 이 Codelab에서는 전체 단어를 말하는 대신 소리를 내어 슬라이더를 제어하므로 3프레임 길이의 샘플 (약 70ms 샘플)을 사용합니다.
모델을 학습할 준비를 하기 위해 컴파일합니다.
const optimizer = tf.train.adam(0.01);
model.compile({
optimizer,
loss: 'categoricalCrossentropy',
metrics: ['accuracy']
});
딥 러닝에서 사용되는 일반적인 옵티마이저인 Adam 옵티마이저와 분류에 사용되는 표준 손실 함수인 손실에 categoricalCrossEntropy를 사용합니다. 간단히 말해 예측된 확률 (클래스당 확률 1개)이 실제 클래스에서 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. 실시간으로 슬라이더 업데이트
이제 모델을 학습시킬 수 있으므로 실시간으로 예측하고 슬라이더를 이동하는 코드를 추가해 보겠습니다. index.html 의 "Train" 버튼 바로 뒤에 다음을 추가합니다.
<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)을 호출합니다. 그러면 확률이 가장 높은 클래스 색인이 반환됩니다. 마지막 측정기준인 numClasses에 대해 argMax를 계산하려고 하므로 '1'을 축 매개변수로 전달합니다.
슬라이더 업데이트
moveSlider()는 라벨이 0('Left')이면 슬라이더의 값을 줄이고, 라벨이 1('Right')이면 값을 늘리고, 라벨이 2('Noise')이면 무시합니다.
텐서 삭제
GPU 메모리를 정리하려면 출력 텐서에서 tf.dispose()를 수동으로 호출하는 것이 중요합니다. 수동 tf.dispose()의 대안은 함수 호출을 tf.tidy()로 래핑하는 것이지만 비동기 함수와 함께 사용할 수는 없습니다.
tf.dispose([input, probs, predLabel]);
10. 최종 앱 테스트
브라우저에서 index.html 을 열고 이전 섹션에서와 같이 세 가지 명령어에 해당하는 세 가지 버튼으로 데이터를 수집합니다. 데이터를 수집하는 동안 각 버튼을 3~4초 동안 누르고 있어야 합니다.
예시를 수집한 후 "Train" 버튼을 누릅니다. 그러면 모델 학습이 시작되고 모델의 정확도가 90%를 넘어야 합니다. 모델 성능이 좋지 않으면 데이터를 더 많이 수집해 보세요.
학습이 완료되면 "Listen" 버튼을 눌러 마이크에서 예측하고 슬라이더를 제어합니다.
http://js.tensorflow.org/에서 더 많은 가이드를 확인하세요.