本文正在参加《终于,掘金有人讲傅里叶变换了》。看完需要半个小时。
我上面说的,谷歌公司早就知道了。因此,他们在TensorFlow
框架中,早就内置了获取音频频谱的函数。它采用的是短时傅里叶变换stft
。
waveform = tf.squeeze(audio, axis=-1)
spectrogram = tf.signal.stft(waveform, frame_length=255, frame_step=128)
我们上面通过tf.audio.decode_wav
解析了音频文件,它返回的数据格式是[[-0.00238037][-0.0038147 ]]
这种形式。
你可能好奇,它为什么不是[-0.00238037, -0.0038147 ]
这种形式,非要外面再套一层。回忆一下,我们的紫色像素的例子,一个像素点表示为[[198, 102, 145]]
,这表示RGB三个色值通道描述一个彩色像素。其实,这里也一样,是兼容了多声道的情况。
但是,我们只要一个通道就好。所以需要通过tf.squeeze(audio, axis=-1)
对数据进行降一个维度,把[[-0.00238037][-0.0038147 ]]
变为[-0.00238037, -0.0038147 ]
。这,才是一个纯粹的波形。嗯,这样才能交给傅里叶先生进行分析。
tf.signal.stft
里面的参数,是指取小样的规则。就是从总波形里面,每隔多久取多少小样本进行分析。分析之后,我们也是可以像绘制波形一样,把分析的频谱结果绘制出来的。
看不懂上面的图没有关系,这很正常,非常正常,极其正常。因为,我即便用了一万多字,50多张图,专门做了详细的解释。但是依然,有20%左右的读者还是不明白。
不过,此时,你需要明白,一段声音的特性是可以通过科学的方法抽取出来的。这,就够了。
把特性抽取出来之后,我们就交给人工智能框架去训练了。
2.3 音频数据的预处理
上面,我们已经成功地获取到一段音频的重要灵魂:频谱。
下面,就该交给神经网络模型去训练了。
在正式交给模型之前,其实还有一些预处理工作要做。比如,给它切一切毛边,叠一叠,整理成同一个形状。
正如计算机只能识别0和1,很多框架也是只能接收固定的结构化数据。
举个简单的例子,你在训练古诗的时候,有五言的和七言的。比如:“床前明月光”和“一顿不吃饿得慌”两句。那么,最终都需要处理成一样的长短。要么前面加0,要么后边加0,要么把长的裁短。总之,必须一样长度才行。
床前明月光〇〇
一顿不吃饿得慌
蜀道难〇〇〇〇
那么,我们的音频数据如何处理呢?我们的音波数据经过短时傅里叶变换之后,格式是这样的:
<tf.Tensor: shape=(86, 129, 1), dtype=float32, numpy=
array([[[4.62073803e-01],
...,
[2.92062759e-05]],
[[3.96062881e-01],
[2.01166332e-01],
[2.09505502e-02],
...,
[1.43915415e-04]]], dtype=float32)>
这是因为我们11146长度的音频,经过tf.signal.stft
的frame_step=128
分割之后,可以分成86份。所以我们看到shape=(86, 129, 1)
。那么,如果音频的长度变化,那么这个结构也会变。这样不好。
因此,我们首先要把音频的长度规范一下。因为采样率是16000
,也就是1秒钟记录16000
次音频数据。那么,我们不妨就拿1秒音频,也就是16000
个长度,为一个标准单位。过长的,我们就裁剪掉后面的。过短的,我们就在后面补上0。
我说的这一系列操作,反映到代码上,就是下面这样:
waveform = tf.squeeze(audio, axis=-1)
input_len = 16000
waveform = waveform[:input_len]
zero_padding = tf.zeros([16000] - tf.shape(waveform),dtype=tf.float32)
waveform = tf.cast(waveform, dtype=tf.float32)
equal_length = tf.concat([waveform, zero_padding], 0)
spectrogram = tf.signal.stft(equal_length, frame_length=255, frame_step=128)
spectrogram = tf.abs(spectrogram)
spectrogram = spectrogram[..., tf.newaxis]
这时候,再来看看我们的频谱数据结构:
<tf.Tensor: shape=(124, 129, 1), dtype=float32, numpy=
array([[[4.62073803e-01],
...,
[2.92062759e-05]],
...
[0.00000000e+00],
...,
[0.00000000e+00]]], dtype=float32)>
现在,不管你输入任何长短的音频,最终它的频谱都是shape=(124, 129, 1)
。从图上我们也可以看出,不足的就算后面补0,也得凑成个16000长度。
下面,真的要开始构建神经网络了。
2.4 构建模型和训练
依照老张的要求……我现在不想提他,因为我的手指被绿萝电的还有点发麻。
依照要求……他要四种命令,分别是:前进、停止、左转、右转。那么,我就搞了四种音频,分别放在对应的文件夹下面。
从文件夹读取数据、将输入输出结对、按照比例分出数据集和验证集,以及把datasets
划分为batch
……这些操作,在TensorFlow
中已经很成熟了。而且,随着版本的更新,越来越成熟。体现在代码上,就是字数越来越少。此处我就不说了,我会把完整代码上传到github
,供诸君参考。
下面,我重点说一下,本例子中,实现语音分类,它的神经网络的结构,以及模型训练的配置。
import tensorflow as tf
from tensorflow.keras import layers
from tensorflow.keras import models
model = models.Sequential([
layers.Input(shape= (124, 129, 1)),
layers.Resizing(32, 32),
layers.Normalization(),
layers.Conv2D(32, 3, activation='relu'),
layers.Conv2D(64, 3, activation='relu'),
layers.MaxPooling2D(),
layers.Dropout(0.25),
layers.Flatten(),
layers.Dense(128, activation='relu'),
layers.Dropout(0.5),
layers.Dense(4),
])
model.compile(
optimizer=tf.keras.optimizers.Adam(),
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=['accuracy']
)
其实,我感觉人工智能应用层面的开发,预处理和后处理比较难。中间的模型基本上都是有固定招式的。
第1层layers.Input(shape= (124, 129, 1))
叫输入层,是训练样本的数据结构。就是我们上一节凑成16000
之后,求频谱得出的(124, 129)
这个结构。
最后一层layers.Dense(4)
,是输出层。我们搞了“走”,“停”,“左”,“右”4个文件夹分类,最终结果是4类,所以是4
。
头尾基本固定后,这个序列Sequential
就意味着:吃音频文件,然后排出它是4个分类中的哪一种。
那么中间我们就可以自己操作了。Normalization
是归一化。Conv2D
是做卷积。MaxPooling2D
是做池化。Dropout(0.25)
是随机砍掉一定比例(此处是25%)的神经网络,以保证其健壮性。快结束时,通过Flatten()
将多维数据拉平为一维数据。后面给个激活函数,收缩神经元个数,准备降落。最后,对接到Dense(4)
。
这就实现了,将前面16000
个音频采样点,经过一系列转化后,最终输出为某个分类。
最后,进行训练和保存模型。
model = create_model()
cp_callback = tf.keras.callbacks.ModelCheckpoint(filepath='model/model.ckpt',
save_weights_only=True,
save_best_only=True)
history = model.fit(
train_ds,
validation_data=val_ds,
epochs=50,
callbacks=[cp_callback]
)
filepath='model/model.ckpt'
表示训练完成后,存储的路径。save_weights_only=True
只存储权重数据。save_best_only=True
意思是只存储最好的训练的结果。调用训练很简单,调用model.fit
,传入训练集、验证集、训练轮数、以及训练回调就可以啦。
2.5 加载模型并预测
上一节中,我们指定了模型的保存路径,调用model.fit
后会将结果保存在对应的路径下。这就是我们最终要的产物:
我们可以加载这些文件,这样就让我们的程序具备了多年功力。可以对外来音频文件做预测。
model = create_model()
if os.path.exists('model/model.ckpt.index'):
model.load_weights('model/model.ckpt')
labels = ['go', 'left', 'right', 'stop']
# 音频文件转码
audio = get_audio_data('mysound.wav')
audios = np.array()
predictions = model(audios)
index = np.argmax(predictions[0])
print(labels[index])
上面代码中,先加载了历史模型。然后,将我录制的一个mysound.wav
文件进行预处理,方式就是前面说的凑成16000
,然后通过短时傅里叶解析成(124, 129)
结构的频谱数据。这也是我们训练时的模样。
最后,把它输入到模型。出于惯性,它会顺势输出这是'go'
分类的语音指令。尽管这个模型,从来没有见过我这段动听的嗓音。但是它也能识别出来,我发出了一个包含'go'
声音特性的声音。
以上,就是利用TensorFlow
框架,实现声音分类的全过程。
音频分类项目开源地址:github.com/hlwgy/sound
再次提醒大家:要求TensorFlow 2.6(2021年10之后版) + python 3.9。因为,里面用了很多新特性。旧版本是跑不通的,具体体现在TensorFlow各种找不到层。
三、我们的合作
我带着成果去找老张。老张沉默了一会儿,不说话。
我说,老张啊,你就说吧。你不说话,我心里没底,不知道会发生啥。
老张说,兄弟啊,其实语音小车这个项目,没啥创意。我昨天才知道,我们车间老王,三年前,自己一个人,就做出来过了。说完,老张又沉默了。
我安慰他说,没关系的。这个不行,你就再换一个呗。
老张猛然抬起头,眼睛中闪着光,他说:兄弟,宇宙飞船相关的软件,你搞得定吗?!火星车也行。
我不紧不忙地关闭服务,并把电脑收进包里。
我穿上鞋,然后拿上包。打开门,回头跟老张说了一句:兄弟,三个月内,我们先不联系了吧。
我是ITF男孩,在掘金是TF男孩,带你从IT视角看世界。