1. 소개
이 Codelab에서는 오디오 인식 네트워크를 빌드하고 이를 사용하여 소리를 내 브라우저에서 슬라이더를 제어합니다. 자바스크립트용 강력하고 유연한 머신러닝 라이브러리인 TensorFlow.js를 사용합니다.
먼저 20개의 음성 명령어를 인식할 수 있는 선행 학습된 모델을 로드하고 실행합니다. 그런 다음 마이크를 사용하여 내 소리를 인식하고 슬라이더를 왼쪽이나 오른쪽으로 움직이는 간단한 신경망을 구축하고 학습시킵니다.
이 Codelab에서는 오디오 인식 모델의 배경 이론에 대해서는 다루지 않습니다. 관련 내용은 이 튜토리얼을 참조하세요.
이 Codelab에서 사용되는 머신러닝 용어의 용어집도 마련되어 있습니다.
학습할 내용
- 사전 학습된 음성 명령 인식 모델을 로드하는 방법
- 마이크를 사용하여 실시간 예측을 수행하는 방법
- 브라우저 마이크를 사용하여 맞춤 오디오 인식 모델을 학습시키고 사용하는 방법
그럼 시작해 보겠습니다
2. 요구사항
이 Codelab을 완료하려면 다음이 필요합니다.
- 최신 버전의 Chrome 또는 다른 최신 브라우저
- 머신에서 로컬로 실행되거나 Codepen 또는 Glitch 등을 통해 웹에서 실행되는 텍스트 편집기
- HTML, CSS, JavaScript, 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의 숫자와 '왼쪽', '오른쪽', '예', '아니요'와 같은 몇 가지 추가 명령어를 인식하도록 학습되었습니다.
이 단어 중 하나를 말합니다. 단어를 올바르게 인식하나요? 모델이 얼마나 자주 실행되는지 제어하는 probabilityThreshold를 사용해 보세요. 0.75는 모델이 특정 단어를 들었다고 75% 이상 확신할 때 실행된다는 의미입니다.
음성 명령어 모델과 API에 대해 자세히 알아보려면 GitHub의 README.md를 참고하세요.
6. 데이터 수집
재미를 위해 전체 단어 대신 짧은 소리를 사용하여 슬라이더를 제어해 보겠습니다.
슬라이더를 왼쪽이나 오른쪽으로 이동시키는 '왼쪽', '오른쪽', '소음'의 세 가지 명령어를 인식하도록 모델을 학습시킬 것입니다. '소음'을 인식하는 것은(조치가 필요하지 않음) 음성 감지에서 매우 중요합니다. 슬라이더는 올바른 소리를 낼 때만 반응하고 일반적인 대화나 이동 중에는 반응하지 않아야 하기 때문입니다.
- 먼저 데이터를 수집해야 합니다.
<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.
}
상세 설명
처음에는 이 코드가 부담스러울 수 있으므로 자세히 살펴보겠습니다.
모델이 인식하도록 하려는 세 가지 명령어에 해당하는 '왼쪽', '오른쪽', '소음'이라는 라벨이 지정된 버튼 세 개를 UI에 추가했습니다. 이 버튼을 누르면 새로 추가된 collect() 함수가 호출되어 모델의 학습 예시가 생성됩니다.
collect()은 label를 recognizer.listen()의 출력과 연결합니다. includeSpectrogram이 참이므로, 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****: '왼쪽', '오른쪽', '소음'의 경우 각각 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. 모델 학습
- 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();
}
이 시점에서 앱을 새로고침하면 새로운 '학습' 버튼이 표시됩니다. 데이터를 다시 수집하고 '학습'을 클릭하여 학습을 테스트하거나 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]이며, 여기서 각 프레임은 다양한 주파수에 해당하는 232개의 숫자가 포함된 23ms의 오디오입니다 (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. 슬라이더를 실시간으로 업데이트
이제 모델을 학습시킬 수 있으므로 실시간으로 예측하고 슬라이더를 이동하는 코드를 추가해 보겠습니다. 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 ('왼쪽')인 경우 슬라이더 값을 감소시키고, 라벨이 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/에서 더 많은 튜토리얼을 확인하세요.