25.md 16.4 KB
Newer Older
W
wizardforcel 已提交
1
# 使用`torchaudio`的语音命令识别
W
wizardforcel 已提交
2

W
wizardforcel 已提交
3
> 原文:<https://pytorch.org/tutorials/intermediate/speech_command_recognition_with_torchaudio.html>
W
wizardforcel 已提交
4 5 6 7 8

本教程将向您展示如何正确设置音频数据集的格式,然后在数据集上训练/测试音频分类器网络。

Colab 提供了 GPU 选项。 在菜单选项卡中,选择“运行系统”,然后选择“更改运行系统类型”。 在随后的弹出窗口中,您可以选择 GPU。 更改之后,运行时应自动重新启动(这意味着来自已执行单元的信息会消失)。

W
wizardforcel 已提交
9
首先,让我们导入常见的 Torch 包,例如[`torchaudio`](https://github.com/pytorch/audio),可以按照网站上的说明进行安装。
W
wizardforcel 已提交
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34

```py
# Uncomment the following line to run in Google Colab

# CPU:
# !pip install torch==1.7.0+cpu torchvision==0.8.1+cpu torchaudio==0.7.0 -f https://download.pytorch.org/whl/torch_stable.html

# GPU:
# !pip install torch==1.7.0+cu101 torchvision==0.8.1+cu101 torchaudio==0.7.0 -f https://download.pytorch.org/whl/torch_stable.html

# For interactive demo at the end:
# !pip install pydub

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torchaudio

import matplotlib.pyplot as plt
import IPython.display as ipd
from tqdm.notebook import tqdm

```

W
wizardforcel 已提交
35
让我们检查一下 CUDA GPU 是否可用,然后选择我们的设备。 在 GPU 上运行网络将大大减少训练/测试时间。
W
wizardforcel 已提交
36 37 38 39 40 41 42

```py
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)

```

W
wizardforcel 已提交
43
## 导入数据集
W
wizardforcel 已提交
44

W
wizardforcel 已提交
45
我们使用`torchaudio`下载并表示数据集。 在这里,我们使用 [SpeechCommands](https://arxiv.org/abs/1804.03209),它是由不同人员说出的 35 个命令的数据集。 数据集`SPEECHCOMMANDS`是数据集的`torch.utils.data.Dataset`版本。 在此数据集中,所有音频文件的长度约为 1 秒(因此约为 16000 个时间帧)。
W
wizardforcel 已提交
46

W
wizardforcel 已提交
47
实际的加载和格式化步骤是在访问数据点时发生的,`torchaudio`负责将音频文件转换为张量。 如果想直接加载音频文件,可以使用`torchaudio.load()`。 它返回一个包含新创建的张量的元组以及音频文件的采样频率(`SpeechCommands`为 16kHz)。
W
wizardforcel 已提交
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80

回到数据集,这里我们创建一个子类,将其分为标准训练,验证和测试子集。

```py
from torchaudio.datasets import SPEECHCOMMANDS
import os

class SubsetSC(SPEECHCOMMANDS):
    def __init__(self, subset: str = None):
        super().__init__("./", download=True)

        def load_list(filename):
            filepath = os.path.join(self._path, filename)
            with open(filepath) as fileobj:
                return [os.path.join(self._path, line.strip()) for line in fileobj]

        if subset == "validation":
            self._walker = load_list("validation_list.txt")
        elif subset == "testing":
            self._walker = load_list("testing_list.txt")
        elif subset == "training":
            excludes = load_list("validation_list.txt") + load_list("testing_list.txt")
            excludes = set(excludes)
            self._walker = [w for w in self._walker if w not in excludes]

# Create training and testing split of the data. We do not use validation in this tutorial.
train_set = SubsetSC("training")
test_set = SubsetSC("testing")

waveform, sample_rate, label, speaker_id, utterance_number = train_set[0]

```

W
wizardforcel 已提交
81
`SPEECHCOMMANDS`数据集中的数据点是一个由波形(音频信号),采样率,发声(标签),讲话者的 ID,发声数组成的元组。
W
wizardforcel 已提交
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98

```py
print("Shape of waveform: {}".format(waveform.size()))
print("Sample rate of waveform: {}".format(sample_rate))

plt.plot(waveform.t().numpy());

```

让我们找到数据集中可用的标签列表。

```py
labels = sorted(list(set(datapoint[2] for datapoint in train_set)))
labels

```

W
wizardforcel 已提交
99
35 个音频标签是用户说的命令。 前几个文件是人们所说的`marvin`
W
wizardforcel 已提交
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117

```py
waveform_first, *_ = train_set[0]
ipd.Audio(waveform_first.numpy(), rate=sample_rate)

waveform_second, *_ = train_set[1]
ipd.Audio(waveform_second.numpy(), rate=sample_rate)

```

最后一个文件是有人说“视觉”。

```py
waveform_last, *_ = train_set[-1]
ipd.Audio(waveform_last.numpy(), rate=sample_rate)

```

W
wizardforcel 已提交
118
## 格式化数据
W
wizardforcel 已提交
119 120 121

这是将转换应用于数据的好地方。 对于波形,我们对音频进行下采样以进行更快的处理,而不会损失太多的分类能力。

W
wizardforcel 已提交
122
我们无需在此应用其他转换。 对于某些数据集,通常必须通过沿通道维度取平均值或仅保留其中一个通道来减少通道数量(例如,从立体声到单声道)。 由于`SpeechCommands`使用单个通道进行音频,因此此处不需要。
W
wizardforcel 已提交
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152

```py
new_sample_rate = 8000
transform = torchaudio.transforms.Resample(orig_freq=sample_rate, new_freq=new_sample_rate)
transformed = transform(waveform)

ipd.Audio(transformed.numpy(), rate=new_sample_rate)

```

我们使用标签列表中的每个索引对每个单词进行编码。

```py
def label_to_index(word):
    # Return the position of the word in labels
    return torch.tensor(labels.index(word))

def index_to_label(index):
    # Return the word corresponding to the index in labels
    # This is the inverse of label_to_index
    return labels[index]

word_start = "yes"
index = label_to_index(word_start)
word_recovered = index_to_label(index)

print(word_start, "-->", index, "-->", word_recovered)

```

W
wizardforcel 已提交
153
为了将由录音和语音构成的数据点列表转换为该模型的两个成批张量,我们实现了整理函数,PyTorch `DataLoader`使用了该函数,允许我们分批迭代数据集。 有关使用整理函数的更多信息,请参见[文档](https://pytorch.org/docs/stable/data.html#working-with-collate-fn)
W
wizardforcel 已提交
154

W
wizardforcel 已提交
155
在整理函数中,我们还应用了重采样和文本编码。
W
wizardforcel 已提交
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210

```py
def pad_sequence(batch):
    # Make all tensor in a batch the same length by padding with zeros
    batch = [item.t() for item in batch]
    batch = torch.nn.utils.rnn.pad_sequence(batch, batch_first=True, padding_value=0.)
    return batch.permute(0, 2, 1)

def collate_fn(batch):

    # A data tuple has the form:
    # waveform, sample_rate, label, speaker_id, utterance_number

    tensors, targets = [], []

    # Gather in lists, and encode labels as indices
    for waveform, _, label, *_ in batch:
        tensors += [waveform]
        targets += [label_to_index(label)]

    # Group the list of tensors into a batched tensor
    tensors = pad_sequence(tensors)
    targets = torch.stack(targets)

    return tensors, targets

batch_size = 256

if device == "cuda":
    num_workers = 1
    pin_memory = True
else:
    num_workers = 0
    pin_memory = False

train_loader = torch.utils.data.DataLoader(
    train_set,
    batch_size=batch_size,
    shuffle=True,
    collate_fn=collate_fn,
    num_workers=num_workers,
    pin_memory=pin_memory,
)
test_loader = torch.utils.data.DataLoader(
    test_set,
    batch_size=batch_size,
    shuffle=False,
    drop_last=False,
    collate_fn=collate_fn,
    num_workers=num_workers,
    pin_memory=pin_memory,
)

```

W
wizardforcel 已提交
211
## 定义网络
W
wizardforcel 已提交
212

W
wizardforcel 已提交
213
在本教程中,我们将使用卷积神经网络来处理原始音频数据。 通常,更高级的转换将应用于音频数据,但是 CNN 可以用于准确处理原始数据。 具体架构是根据[本文](https://arxiv.org/pdf/1610.00087.pdf)中描述的 M5 网络架构建模的。 模型处理原始音频数据的一个重要方面是其第一层过滤器的接收范围。 我们模型的第一个滤波器长度为 80,因此在处理以 8kHz 采样的音频时,接收场约为 10ms(而在 4kHz 时约为 20ms)。 此大小类似于语音处理应用,该应用通常使用 20ms 到 40ms 的接收域。
W
wizardforcel 已提交
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270

```py
class M5(nn.Module):
    def __init__(self, n_input=1, n_output=35, stride=16, n_channel=32):
        super().__init__()
        self.conv1 = nn.Conv1d(n_input, n_channel, kernel_size=80, stride=stride)
        self.bn1 = nn.BatchNorm1d(n_channel)
        self.pool1 = nn.MaxPool1d(4)
        self.conv2 = nn.Conv1d(n_channel, n_channel, kernel_size=3)
        self.bn2 = nn.BatchNorm1d(n_channel)
        self.pool2 = nn.MaxPool1d(4)
        self.conv3 = nn.Conv1d(n_channel, 2 * n_channel, kernel_size=3)
        self.bn3 = nn.BatchNorm1d(2 * n_channel)
        self.pool3 = nn.MaxPool1d(4)
        self.conv4 = nn.Conv1d(2 * n_channel, 2 * n_channel, kernel_size=3)
        self.bn4 = nn.BatchNorm1d(2 * n_channel)
        self.pool4 = nn.MaxPool1d(4)
        self.fc1 = nn.Linear(2 * n_channel, n_output)

    def forward(self, x):
        x = self.conv1(x)
        x = F.relu(self.bn1(x))
        x = self.pool1(x)
        x = self.conv2(x)
        x = F.relu(self.bn2(x))
        x = self.pool2(x)
        x = self.conv3(x)
        x = F.relu(self.bn3(x))
        x = self.pool3(x)
        x = self.conv4(x)
        x = F.relu(self.bn4(x))
        x = self.pool4(x)
        x = F.avg_pool1d(x, x.shape[-1])
        x = x.permute(0, 2, 1)
        x = self.fc1(x)
        return F.log_softmax(x, dim=2)

model = M5(n_input=transformed.shape[0], n_output=len(labels))
model.to(device)
print(model)

def count_parameters(model):
    return sum(p.numel() for p in model.parameters() if p.requires_grad)

n = count_parameters(model)
print("Number of parameters: %s" % n)

```

我们将使用与本文相同的优化技术,将权重衰减设置为 0.0001 的 Adam 优化器。 首先,我们将以 0.01 的学习率进行训练,但是在 20 个周期后的训练过程中,我们将使用`scheduler`将其降低到 0.001。

```py
optimizer = optim.Adam(model.parameters(), lr=0.01, weight_decay=0.0001)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.1)  # reduce the learning after 20 epochs by a factor of 10

```

W
wizardforcel 已提交
271
## 训练和测试网络
W
wizardforcel 已提交
272

W
wizardforcel 已提交
273
现在,我们定义一个训练函数,它将训练数据输入模型中,并执行反向传播和优化步骤。 对于训练,我们将使用的损失是负对数可能性。 然后,在每个时期之后将对网络进行测试,以查看训练期间准确率如何变化。
W
wizardforcel 已提交
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304

```py
def train(model, epoch, log_interval):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):

        data = data.to(device)
        target = target.to(device)

        # apply transform and model on whole batch directly on device
        data = transform(data)
        output = model(data)

        # negative log-likelihood for a tensor of size (batch x 1 x n_output)
        loss = F.nll_loss(output.squeeze(), target)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        # print training stats
        if batch_idx % log_interval == 0:
            print(f"Train Epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)} ({100\. * batch_idx / len(train_loader):.0f}%)]\tLoss: {loss.item():.6f}")

        # update progress bar
        pbar.update(pbar_update)
        # record loss
        losses.append(loss.item())

```

W
wizardforcel 已提交
305
现在我们有了训练函数,我们需要制作一个用于测试网络准确率的函数。 我们将模型设置为`eval()`模式,然后对测试数据集进行推断。 调用`eval()`将网络中所有模块中的训练变量设置为`false`。 某些层(例如批量归一化层和丢弃层)在训练期间的行为会有所不同,因此此步骤对于获取正确的结果至关重要。
W
wizardforcel 已提交
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337

```py
def number_of_correct(pred, target):
    # count number of correct predictions
    return pred.squeeze().eq(target).sum().item()

def get_likely_index(tensor):
    # find most likely label index for each element in the batch
    return tensor.argmax(dim=-1)

def test(model, epoch):
    model.eval()
    correct = 0
    for data, target in test_loader:

        data = data.to(device)
        target = target.to(device)

        # apply transform and model on whole batch directly on device
        data = transform(data)
        output = model(data)

        pred = get_likely_index(output)
        correct += number_of_correct(pred, target)

        # update progress bar
        pbar.update(pbar_update)

    print(f"\nTest Epoch: {epoch}\tAccuracy: {correct}/{len(test_loader.dataset)} ({100\. * correct / len(test_loader.dataset):.0f}%)\n")

```

W
wizardforcel 已提交
338
最后,我们可以训练和测试网络。 我们将训练网络十个时期,然后降低学习率,再训练十个时期。 在每个时期之后将对网络进行测试,以查看训练过程中准确率如何变化。
W
wizardforcel 已提交
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360

```py
log_interval = 20
n_epoch = 2

pbar_update = 1 / (len(train_loader) + len(test_loader))
losses = []

# The transform needs to live on the same device as the model and the data.
transform = transform.to(device)
with tqdm(total=n_epoch) as pbar:
    for epoch in range(1, n_epoch + 1):
        train(model, epoch, log_interval)
        test(model, epoch)
        scheduler.step()

# Let's plot the training loss versus the number of iteration.
# plt.plot(losses);
# plt.title("training loss");

```

W
wizardforcel 已提交
361
2 个周期后,测试集的网络准确度应超过 65%,而 21 个周期后,网络应达到 85%。 让我们看一下训练集中的最后几句话,看看模型是如何做到的。
W
wizardforcel 已提交
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445

```py
def predict(tensor):
    # Use the model to predict the label of the waveform
    tensor = tensor.to(device)
    tensor = transform(tensor)
    tensor = model(tensor.unsqueeze(0))
    tensor = get_likely_index(tensor)
    tensor = index_to_label(tensor.squeeze())
    return tensor

waveform, sample_rate, utterance, *_ = train_set[-1]
ipd.Audio(waveform.numpy(), rate=sample_rate)

print(f"Expected: {utterance}. Predicted: {predict(waveform)}.")

```

如果有一个示例,我们来寻找一个分类错误的示例。

```py
for i, (waveform, sample_rate, utterance, *_) in enumerate(test_set):
    output = predict(waveform)
    if output != utterance:
        ipd.Audio(waveform.numpy(), rate=sample_rate)
        print(f"Data point #{i}. Expected: {utterance}. Predicted: {output}.")
        break
else:
    print("All examples in this dataset were correctly classified!")
    print("In this case, let's just look at the last data point")
    ipd.Audio(waveform.numpy(), rate=sample_rate)
    print(f"Data point #{i}. Expected: {utterance}. Predicted: {output}.")

```

随意尝试使用其中一个标签的自己的录音! 例如,使用 Colab,在执行下面的单元格时说“ Go”。 这将录制一秒钟的音频并尝试对其进行分类。

```py
from google.colab import output as colab_output
from base64 import b64decode
from io import BytesIO
from pydub import AudioSegment

RECORD = """
const sleep  = time => new Promise(resolve => setTimeout(resolve, time))
const b2text = blob => new Promise(resolve => {
  const reader = new FileReader()
  reader.onloadend = e => resolve(e.srcElement.result)
  reader.readAsDataURL(blob)
})
var record = time => new Promise(async resolve => {
  stream = await navigator.mediaDevices.getUserMedia({ audio: true })
  recorder = new MediaRecorder(stream)
  chunks = []
  recorder.ondataavailable = e => chunks.push(e.data)
  recorder.start()
  await sleep(time)
  recorder.onstop = async ()=>{
    blob = new Blob(chunks)
    text = await b2text(blob)
    resolve(text)
  }
  recorder.stop()
})
"""

def record(seconds=1):
    display(ipd.Javascript(RECORD))
    print(f"Recording started for {seconds} seconds.")
    s = colab_output.eval_js("record(%d)" % (seconds * 1000))
    print("Recording ended.")
    b = b64decode(s.split(",")[1])

    fileformat = "wav"
    filename = f"_audio.{fileformat}"
    AudioSegment.from_file(BytesIO(b)).export(filename, format=fileformat)
    return torchaudio.load(filename)

waveform, sample_rate = record()
print(f"Predicted: {predict(waveform)}.")
ipd.Audio(waveform.numpy(), rate=sample_rate)

```

W
wizardforcel 已提交
446
## 总结
W
wizardforcel 已提交
447

W
wizardforcel 已提交
448
在本教程中,我们使用了`torchaudio`来加载数据集并对信号进行重新采样。 然后,我们定义了经过训练的神经网络,以识别给定命令。 还有其他数据预处理方法,例如找到梅尔频率倒谱系数(MFCC),可以减小数据集的大小。 此变换也可以在`torchaudio`中作为`torchaudio.transforms.MFCC`使用。
W
wizardforcel 已提交
449

W
wizardforcel 已提交
450
**脚本的总运行时间**:(0 分钟 0.000 秒)
W
wizardforcel 已提交
451

W
wizardforcel 已提交
452
[下载 Python 源码:`speech_command_recognition_with_torchaudio.py`](../_downloads/4cbc77c0f631ff7a80a046f57b97a075/speech_command_recognition_with_torchaudio.py)
W
wizardforcel 已提交
453

W
wizardforcel 已提交
454
[下载 Jupyter 笔记本:`speech_command_recognition_with_torchaudio.ipynb`](../_downloads/d87597d0062580c9ec699193e951e3f4/speech_command_recognition_with_torchaudio.ipynb)
W
wizardforcel 已提交
455

W
wizardforcel 已提交
456
[由 Sphinx 画廊](https://sphinx-gallery.readthedocs.io)生成的画廊