1. 简介
在此 Codelab 中,您将构建一个音频识别网络,并使用该网络通过发出声音来控制浏览器中的滑块。您将使用 TensorFlow.js,这是一个功能强大且灵活的 JavaScript 机器学习库。
首先,您将加载并运行一个预训练模型,该模型可以识别 20 个语音指令。然后使用麦克风构建和训练一个简单的神经网络,该网络可识别您的声音,并让滑块向左或向右移动。
此 Codelab 不涵盖音频识别模型背后的理论。如果您想了解相关信息,请查看此教程。
我们还创建了一个术语表,其中包含您在此 Codelab 中找到的机器学习术语。
学习内容
- 如何加载预训练的语音指令识别模型
- 如何使用麦克风进行实时预测
- 如何使用浏览器麦克风训练和使用自定义音频识别模型
下面我们开始步入正题
2. 要求
要完成此 Codelab,您需要:
- 最新版本的 Chrome 或其他现代浏览器。
- 文本编辑器(可以在机器本地运行,也可以通过 Codepen 或 Glitch 等工具在网络上运行)。
- 了解 HTML、CSS、JavaScript 和 Chrome 开发者工具(或您的首选浏览器开发者工具)。
- 大致了解神经网络的概念。如果您需要了解简介或回顾相关知识,请观看这个由 3blue1brown 制作的视频或这个由 Ashi Krishnan 制作的有关使用 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% 时会触发。
如需详细了解 Speech Commands 模型及其 API,请参阅 GitHub 上的 README.md。
6. 收集数据
为了有趣,我们可以使用短音(而不是整个字词)来控制滑块!
您将训练模型来识别 3 个不同的命令:“左”“右”和“噪音”这会使滑块向左或向右移动识别“噪音”(无需操作)在语音检测中至关重要,因为我们希望滑块仅在我们发出正确的声音时做出反应,而不是在我们通常说话和四处移动时做出反应。
- 首先,我们需要收集数据。将以下代码添加到
<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>
- 将以下内容添加到
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”的按钮,分别对应于我们希望模型识别的三个命令。按下这些按钮会调用我们新添加的 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 个命令相对应的 3 个按钮。如果您通过本地文件访问麦克风,则必须启动网络服务器并使用 http://localhost:port/
。
要在端口 8000 上启动一个简单的网络服务器,请执行以下操作:
python -m SimpleHTTPServer
如需收集每条指令的示例,请在按住每个按钮 3-4 秒的同时(或连续)发出一致的声音。您应为每个标签收集大约 150 个样本。例如,我们可以用手指打响“左”手势,用口哨吹出“右耳”,还可以在静音和讲话之间切换“噪音”。
随着您收集更多样本,页面上显示的计数器应该也会变大。您还可以随时通过在控制台中的 examples
变量上调用 console.log() 来检查数据。此阶段的目标是测试数据收集流程。之后,您会在测试整个应用时重新收集数据。
8. 训练模型
- 添加“火车”“Noise”按钮之后index.html: 中正文的按钮
<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”按钮。您可以通过重新收集数据并点击“训练”来测试训练,也可以等到第 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]
,其中每帧是 23 毫秒的音频,包含 232 个对应于不同频率的数字(选择了 232 个,因为这是捕获人类语音所需的频率范围量)。在此 Codelab 中,我们将使用时长为 3 帧的样本(样本时长约为 70 毫秒),因为我们通过发出声音来控制滑块,而不是读出整个字词。
我们编译模型,使其准备好进行训练:
const optimizer = tf.train.adam(0.01);
model.compile({
optimizer,
loss: 'categoricalCrossentropy',
metrics: ['accuracy']
});
我们使用 Adam 优化器(深度学习中常用的优化器)和 categoricalCrossEntropy
(用于分类的标准损失函数)处理损失函数。简而言之,它会测量预测的概率(每个类别一个概率)与真实类别中的概率为 100%,所有其他类别中的概率为 0% 的差距。我们还提供 accuracy
作为监控的指标,这可提供每个训练周期后模型正确获取样本的百分比。
训练
使用批量大小为 16 的数据对数据进行 10 次(周期)训练(一次处理 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”作为轴参数,因为我们想要计算最后一个维度 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/。