音频样本分类的深度学习探索
1. 线性SVM的困境
线性支持向量机(SVM)在处理当前特征时失效了,原因在于特征似乎并非线性可分。虽然没有尝试径向基函数(RBF,高斯核)SVM,但读者可以自行尝试。若进行尝试,需注意有两个超参数需要调整:C和γ。
2. 使用传统神经网络
此前尚未尝试传统神经网络。可以像之前那样使用
sklearn
的
MLPClassifier
类,但现在是展示如何在Keras中实现传统网络的好时机,代码如下:
import keras
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras import backend as K
import numpy as np
batch_size = 32
num_classes = 10
epochs = 16
nsamp = (882,1)
x_train = np.load("esc10_raw_train_audio.npy")
y_train = np.load("esc10_raw_train_labels.npy")
x_test = np.load("esc10_raw_test_audio.npy")
y_test = np.load("esc10_raw_test_labels.npy")
x_train = (x_train.astype('float32') + 32768) / 65536
x_test = (x_test.astype('float32') + 32768) / 65536
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
model = Sequential()
model.add(Dense(1024, activation='relu', input_shape=nsamp))
model.add(Dropout(0.5))
model.add(Dense(512, activation='relu'))
model.add(Dropout(0.5))
model.add(Flatten())
model.add(Dense(num_classes, activation='softmax'))
model.compile(loss=keras.losses.categorical_crossentropy,
optimizer=keras.optimizers.Adam(),
metrics=['accuracy'])
model.fit(x_train, y_train,
batch_size=batch_size,
epochs=epochs,
verbose=0,
validation_data=(x_test, y_test))
score = model.evaluate(x_test, y_test, verbose=0)
print('Test accuracy:', score[1])
加载必要的模块后,加载数据并像处理经典模型那样进行缩放。接着构建模型架构,仅需要全连接层(Dense)和丢弃层(Dropout),在最终的softmax输出之前添加了一个扁平化层(Flatten)以消除额外维度。遗憾的是,该模型并未带来改善,准确率仅为27.6%。
3. 使用一维卷积神经网络
经典模型和传统神经网络都不太理想,接下来尝试将一维卷积神经网络(1D CNN)应用于该数据集,看看效果是否更好。除了输入数据的结构不同外,一维CNN与二维CNN的唯一区别是将
Conv2D
和
MaxPooling2D
替换为
Conv1D
和
MaxPooling1D
。第一个尝试的模型代码如下:
import keras
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv1D, MaxPooling1D
import numpy as np
batch_size = 32
num_classes = 10
epochs = 16
nsamp = (882,1)
x_train = np.load("esc10_raw_train_audio.npy")
y_train = np.load("esc10_raw_train_labels.npy")
x_test = np.load("esc10_raw_test_audio.npy")
y_test = np.load("esc10_raw_test_labels.npy")
x_train = (x_train.astype('float32') + 32768) / 65536
x_test = (x_test.astype('float32') + 32768) / 65536
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
model = Sequential()
model.add(Conv1D(32, kernel_size=3, activation='relu',
input_shape=nsamp))
model.add(MaxPooling1D(pool_size=3))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(512, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes, activation='softmax'))
model.compile(loss=keras.losses.categorical_crossentropy,
optimizer=keras.optimizers.Adam(),
metrics=['accuracy'])
history = model.fit(x_train, y_train,
batch_size=batch_size,
epochs=epochs,
verbose=1,
validation_data=(x_test[:160], y_test[:160]))
score = model.evaluate(x_test[160:], y_test[160:], verbose=0)
print('Test accuracy:', score[1])
该模型同样加载并预处理数据集。这种架构称为浅层架构,有一个包含32个滤波器、核大小为3的卷积层,随后是一个池化核大小为3的最大池化层,接着是丢弃层和扁平化层,再之后是一个有512个节点并带有丢弃层的全连接层,最后以softmax层结束。
训练16个周期,批量大小为32,保留训练历史以检查损失和验证性能随周期的变化。1600个测试样本中,10%用于训练验证,90%用于整体准确率评估。还会尝试将
Conv1D
的核大小从3变化到33,以找到最适合训练数据的核大小。
另外定义了四种架构,分别为中等架构(medium)、深度架构0(deep0)、深度架构1(deep1)和深度架构2(deep2),代码如下:
# medium
model = Sequential()
model.add(Conv1D(32, kernel_size=3, activation='relu',
input_shape=nsamp))
model.add(Conv1D(64, kernel_size=3, activation='relu'))
model.add(Conv1D(64, kernel_size=3, activation='relu'))
model.add(MaxPooling1D(pool_size=3))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(512, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes, activation='softmax'))
# deep0
model = Sequential()
model.add(Conv1D(32, kernel_size=3, activation='relu',
input_shape=nsamp))
model.add(Conv1D(64, kernel_size=3, activation='relu'))
model.add(Conv1D(64, kernel_size=3, activation='relu'))
model.add(MaxPooling1D(pool_size=3))
model.add(Dropout(0.25))
model.add(Conv1D(64, kernel_size=3, activation='relu'))
model.add(Conv1D(64, kernel_size=3, activation='relu'))
model.add(MaxPooling1D(pool_size=3))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(512, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes, activation='softmax'))
# deep1
model = Sequential()
model.add(Conv1D(32, kernel_size=3, activation='relu',
input_shape=nsamp))
model.add(Conv1D(64, kernel_size=3, activation='relu'))
model.add(Conv1D(64, kernel_size=3, activation='relu'))
model.add(MaxPooling1D(pool_size=3))
model.add(Dropout(0.25))
model.add(Conv1D(64, kernel_size=3, activation='relu'))
model.add(Conv1D(64, kernel_size=3, activation='relu'))
model.add(MaxPooling1D(pool_size=3))
model.add(Dropout(0.25))
model.add(Conv1D(64, kernel_size=3, activation='relu'))
model.add(Conv1D(64, kernel_size=3, activation='relu'))
model.add(MaxPooling1D(pool_size=3))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(512, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes, activation='softmax'))
# deep2
model = Sequential()
model.add(Conv1D(32, kernel_size=3, activation='relu',
input_shape=nsamp))
model.add(Conv1D(64, kernel_size=3, activation='relu'))
model.add(Conv1D(64, kernel_size=3, activation='relu'))
model.add(MaxPooling1D(pool_size=3))
model.add(Dropout(0.25))
model.add(Conv1D(64, kernel_size=3, activation='relu'))
model.add(Conv1D(64, kernel_size=3, activation='relu'))
model.add(MaxPooling1D(pool_size=3))
model.add(Dropout(0.25))
model.add(Conv1D(64, kernel_size=3, activation='relu'))
model.add(Conv1D(64, kernel_size=3, activation='relu'))
model.add(MaxPooling1D(pool_size=3))
model.add(Dropout(0.25))
model.add(Conv1D(64, kernel_size=3, activation='relu'))
model.add(Conv1D(64, kernel_size=3, activation='relu'))
model.add(MaxPooling1D(pool_size=3))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(512, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes, activation='softmax'))
多次训练不同模型,每次改变第一个
Conv1D
的核大小,得到的结果如下表所示:
| 核大小 | 浅层架构 | 中等架构 | 深度架构0 | 深度架构1 | 深度架构2 |
| ---- | ---- | ---- | ---- | ---- | ---- |
| 3 | 44.51 | 41.39 | 48.75 | 54.03 | 9.93 |
| 5 | 43.47 | 41.74 | 44.72 | 53.96 | 48.47 |
| 7 | 38.47 | 40.97 | 46.18 | 52.64 | 49.31 |
| 9 | 41.46 | 43.06 | 46.88 | 48.96 | 9.72 |
| 11 | 39.65 | 40.21 | 45.21 | 52.99 | 10.07 |
| 13 | 42.71 | 41.67 | 46.53 | 50.56 | 52.57 |
| 15 | 40.00 | 42.78 | 46.53 | 50.14 | 47.08 |
| 33 | 27.57 | 42.22 | 41.39 | 48.75 | 9.86 |
从表中可以看出,随着模型深度增加,准确率总体呈上升趋势,但到深度架构2时情况开始变差,部分模型无法收敛,准确率接近随机猜测。深度架构1在所有核大小下表现最佳,核大小为3时,在五种架构中的三种表现最佳。这意味着一维CNN的最佳组合是初始核大小为3和深度架构1。
仅对深度架构1训练16个周期,若增加训练周期是否会有改善呢?将深度架构1训练60个周期并绘制训练和验证损失及误差随周期的变化图,发现验证集的损失在大约18个周期后开始爆炸式增长,训练损失持续下降,这是明显的过拟合现象。过拟合的可能原因是训练集规模有限,即使进行了数据增强,也只有6400个样本。验证误差在初始下降后基本保持不变,结论是使用一维向量处理该数据集,准确率很难超过54%。
4. 频谱图的应用
回到增强后的音频文件集,此前构建数据集时,只保留了两秒的音频且每隔100个样本取一个,最佳准确率略高于50%。
如果处理输入音频文件中一小段(如200毫秒)的声音样本,可以使用样本向量计算傅里叶变换。傅里叶变换能告诉我们构成信号的频率,简单信号(如陶笛声音)的傅里叶变换在特定频率上有几个峰值,复杂信号(如语音或音乐)则有许多不同频率的峰值。
傅里叶变换是复数值的,取其绝对值可得到代表特定频率能量的实数,即信号的功率谱。简单音调可能只在少数频率上有能量,而钹声或白噪声的能量则更均匀地分布在所有频率上。
功率谱仅代表一小段时间的频谱,而音频样本长达五秒,因此使用频谱图。频谱图是由代表各个频谱的列组成的图像,x轴表示时间,y轴表示频率,像素颜色与该时间该频率的能量成正比。
要为增强后的音频文件创建频谱图,需要一个新工具
sox
,它是一个命令行工具,在Ubuntu Linux系统中可能已安装,若未安装可使用以下命令安装:
sudo apt-get install sox
使用Python脚本调用
sox
生成所需的频谱图图像,处理训练图像的代码如下:
import os
import numpy as np
from PIL import Image
rows = 100
cols = 160
flist = [i[:-1] for i in open("augmented_train_filelist.txt")]
N = len(flist)
img = np.zeros((N,rows,cols,3), dtype="uint8")
lbl = np.zeros(N, dtype="uint8")
p = []
for i,f in enumerate(flist):
src, c = f.split()
os.system("sox %s -n spectrogram" % src)
im = np.array(Image.open("spectrogram.png").convert("RGB"))
im = im[42:542,58:858,:]
im = Image.fromarray(im).resize((cols,rows))
img[i,:,:,:] = np.array(im)
lbl[i] = int(c)
p.append(os.path.abspath(src))
os.system("rm -rf spectrogram.png")
p = np.array(p)
idx = np.argsort(np.random.random(N))
img = img[idx]
lbl = lbl[idx]
p = p[idx]
np.save("esc10_spect_train_images.npy", img)
np.save("esc10_spect_train_labels.npy", lbl)
np.save("esc10_spect_train_paths.npy", p)
首先定义频谱图的大小为100×160像素,加载训练文件列表,创建NumPy数组来存储频谱图图像和标签,列表
p
用于存储每个频谱图的源文件路径。然后遍历文件列表,调用
sox
将声音文件转换为频谱图图像,加载输出的频谱图并去除边框信息,调整大小后存储,同时记录标签和文件路径。生成所有频谱图后,删除临时文件,随机打乱图像顺序,最后将图像、标签和路径名保存到磁盘。对测试集重复此过程。
从可视化的频谱图来看,不同类别的频谱图通常可以区分,这为使用二维CNN进行分类提供了希望。
5. 频谱图分类
要处理频谱图数据集,需要使用二维卷积神经网络(2D CNN)。一个可能的起点是将浅层一维CNN架构转换为二维,即将
Conv1D
替换为
Conv2D
,
MaxPooling1D
替换为
MaxPooling2D
。但这样得到的模型有3070万个参数,数量过多。因此,选择一个更深但参数更少的架构,并探索不同的第一个卷积层核大小的影响,代码如下:
import keras
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
import numpy as np
batch_size = 16
num_classes = 10
epochs = 16
img_rows, img_cols = 100, 160
input_shape = (img_rows, img_cols, 3)
x_train = np.load("esc10_spect_train_images.npy")
y_train = np.load("esc10_spect_train_labels.npy")
x_test = np.load("esc10_spect_test_images.npy")
y_test = np.load("esc10_spect_test_labels.npy")
x_train = x_train.astype('float32') / 255
x_test = x_test.astype('float32') / 255
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
model = Sequential()
model.add(Conv2D(32, kernel_size=(3,3), activation='relu',
input_shape=input_shape))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes, activation='softmax'))
model.compile(loss=keras.losses.categorical_crossentropy,
optimizer=keras.optimizers.Adam(),
metrics=['accuracy'])
history = model.fit(x_train, y_train,
batch_size=batch_size, epochs=epochs,
verbose=0, validation_data=(x_test, y_test))
score = model.evaluate(x_test, y_test, verbose=0)
print('Test accuracy:', score[1])
model.save("esc10_cnn_deep_3x3_model.h5")
这里使用的小批量大小为16,训练16个周期,采用Adam优化器。模型架构包含两个卷积层、一个带丢弃层的最大池化层、另一个卷积层和第二个带丢弃层的最大池化层,在softmax输出之前有一个包含128个节点的全连接层。
测试第一个卷积层的两种核大小:3×3和7×7。3×3配置的代码如上所示,将
(3,3)
替换为
(7,7)
即可改变核大小。之前的一维卷积运行都只对模型进行一次训练评估,由于随机初始化,即使其他条件不变,每次训练的结果也会略有不同。对于二维CNN,每个模型训练六次,并将整体准确率表示为均值 ± 均值标准误差,结果如下表所示:
| 核大小 | 得分 |
| ---- | ---- |
| 3×3 | 78.78 ± 0.60% |
| 7×7 | 78.44 ± 0.72% |
这表明使用3×3或7×7的初始卷积层核大小没有显著差异,因此后续选择3×3的核大小。
绘制一次二维CNN在频谱图上训练的训练和验证损失(顶部)及误差(底部)随周期的变化图,和一维CNN情况类似,几个周期后验证误差开始增加。
二维CNN的表现明显优于一维CNN,准确率达到79%,而一维CNN只有54%。不过,这个准确率对于很多应用来说可能还不够,但对某些应用可能是可以接受的。需要注意的是,数据和硬件存在一些限制,因为采用仅使用CPU的方法,限制了训练模型的时间。如果使用GPU,性能可能会提高25倍,但如果要在嵌入式系统上运行模型,可能无法使用GPU,就需要选择更小的模型。
下面是整个音频样本分类的流程mermaid图:
graph LR
A[数据加载] --> B[线性SVM尝试]
B --> C{是否成功}
C -- 否 --> D[传统神经网络尝试]
D --> E[一维CNN尝试]
E --> F[生成频谱图]
F --> G[二维CNN尝试]
C -- 是 --> H[结束]
D --> I{是否满意结果}
I -- 否 --> E
I -- 是 --> H
E --> J{是否满意结果}
J -- 否 --> F
J -- 是 --> H
G --> K{是否满意结果}
K -- 是 --> H
K -- 否 --> L[调整模型或数据]
L --> G
综上所述,在音频样本分类任务中,从线性SVM到传统神经网络,再到一维CNN和二维CNN,不断尝试不同的模型架构和数据处理方法,逐步提高了分类准确率。虽然目前的准确率还有提升空间,但通过合理选择模型和利用频谱图等数据处理技巧,已经取得了一定的成果。未来可以进一步探索更多的模型架构和数据增强方法,以提高分类性能。同时,考虑硬件资源的限制,选择合适的模型进行部署。
超级会员免费看
2665

被折叠的 条评论
为什么被折叠?



