转换 CNN(音频识别)的 MFCC 频谱图的输入
Transform the input of the MFCCs Spectogram for a CNN (Audio Recognition)
我有一个音频数据集,我已经将这些音频转换成如下所示的介绍 MFCC 绘图:
现在我想为我的神经网络提供数据
import tensorflow as tf
import tensorflow.keras as tfk
import tensorflow.keras.layers as tfkl
cnn_model = tfk.Sequential(name='CNN_model')
cnn_model.add(tfkl.Conv1D(filters= 225, kernel_size= 11, padding='same', activation='relu', input_shape=(4500,9000, 3)))
cnn_model.add(tfkl.BatchNormalization())
cnn_model.add(tfkl.Bidirectional(tfkl.GRU(200, activation='relu', return_sequences=True, implementation=0)))
cnn_model.add(tfkl.Dropout(0.2))
cnn_model.add(tfkl.BatchNormalization())
cnn_model.add(tfkl.TimeDistributed(tfkl.Dense(20)))
cnn_model.add(tfkl.Dropout(0.2))
cnn_model.add(tfkl.Softmax())
cnn_model.compile(loss='mae', optimizer='Adam', metrics=['mae'])
cnn_model.summary()
我使用了Conv1D,因为它是这种神经网络中使用的层。但我不知道如何将数据从 图像转换为 CNN 的输入。自己尝试了几次改造,就是不行。
正如您在下图中看到的,我需要提供第一层 Conv1D
但我不能,因为我的图像的形状是 (4500, 9000, 3)
。所以基本上,我想做的就是将此图像转换为 Conv1D
的输入 ,方法与下图相同。
此图像代表传递给 NN 的 1 个音频。
显然,当我将具有此形状的图像传递到 Conv1D
层时,我有一个 ValueError
ValueError: Input 0 of layer conv1d_4 is incompatible with the layer: expected ndim=3, found ndim=4. Full shape received: [None, 4500, 9000, 3]
我将我的图像转为灰度,但不是方法,我丢失了一个有价值的信息。
我认为你可以将图像转换为灰度,但你可能会丢失大量有价值的数据。
最好的方法是重塑 MFCC 频谱图。 img.reshape(4500, 3 * 9000)
例子
# Sample data
>>> a
array([[[1, 1, 1],
[2, 2, 2]],
[[3, 3, 3],
[4, 4, 4]]])
>>> a.shape
(2, 2, 3)
# Reshaping data
>>> a.reshape(2, -1)
array([[1, 1, 1, 2, 2, 2],
[3, 3, 3, 4, 4, 4]])
# Or
>>> a.reshape(2, 6)
array([[1, 1, 1, 2, 2, 2],
[3, 3, 3, 4, 4, 4]])
如果你说 X 是时间,那么考虑形状 (examples, time_steps, frequency_bins, img_channels)
,你可以尝试一些东西。
选项 1
最明显的是在@skillsmuggler 的回答中提到的。不是时间的一切都是特征,所以:
#if in the model:
x_train = original_x_train
cnn_model.add(tfkl.Reshape((4500, 27000), input_shape=(4500,9000,3))) #first layer
#if directly in the data:
x_train = original_x_train.reshape((-1, 4500, 27000))
cnn_model.add(tfkl.Conv1D(filters= 225, kernel_size= 11, padding='same',
activation='relu', input_shape=(4500,9000, 3))) #original first layer
选项 2
但还有更多的可能性。我不知道 MFCC 是什么,但我怀疑它是由以下材料制成的:
- x = 时间步长
- y = 频点
- 颜色=强度
如果是这样,首先要做的是获取强度的原始值而不是 3 通道像素值。对于网络来说,获得连续值的想法比 3 个通道以更复杂的方式变化来表示同一事物要容易得多(这些颜色仅适用于我们的人眼,但它们在数学上要复杂得多)
如果您可以访问原始值而不是颜色,那么您可以像 选项 2 输入 (examples, time_steps, frequency_bins)
,就是这样,没有图像颜色通道。更少的输入,更好地表示信息。本例中的值为 "intensity".
print(x_train.shape) #-> (examples, 4500, 9000)
那么你的模型就不需要改变了。
选项 3
现在,如果你说你用上述方法丢失了信息,那么你可以尝试许多其他奇特的东西,我能想到的第一个是首先对频率维度进行卷积,以某种方式合并或折叠它,然后然后开始处理时间维度。
类似于这个两部分模型。
第 1 部分: 卷积和折叠频率。
input_channels = 1 or 3 #preferrably 1, following option 2,
#but it's possible to use the 3 channel images too (less optimal)
cnn_model = tfk.Sequential(name='CNN_model')
cnn_model.add(tfkl.TimeDistributed(
Conv1D(filters, size, activation=...),
input_shape=(4500,9000,input_channels)))
#shapes will be all in the type (examples, 4500, decreasing_freq_size, increasing_channels)
#make a time distributed conv model in the VGG style until you collapse the last channel dimension
cnn_model.add(tfkl.TimeDistributed(Conv1D(...)))
...
cnn_model.add(tfkl.MaxPooling1D())
cnn_model.add(tfkl.TimeDistributed(Conv1D(...)))
...
cnn_model.add(tfkl.MaxPooling1D())
cnn_model.add(tfkl.TimeDistributed(Conv1D(...)))
...
#when the 9000 has been reduced a lot
import tf.keras.backend as K
cnn_model.add(tfkl.Lambda(lambda x: K.mean(x, axis=2)))
#the line above is equivalent to the following, but seems more efficient
#cnn_model.add(tfkl.TimeDistributed(GlobalAveragePooling1D()))
#new shape style: (examples, 4500, increased_channels)
#no need for a huge number of channels, maybe around 100?
cnn_model.add(tfkl.Dense(units=around_100)) #Dense is equal to TimeDistributed(Dense)
现在您已经将 (examples, 4500, 9000, ch_1_or_3)
的形状转换为 (examples, 4500, features_around_100)
的形状,您可以转到第二部分,即您的原始模型。
第 2 部分:继续您的原始模型。
cnn_model.add(tfkl.Conv1D(filters= 225, kernel_size= 11, padding='same', activation='relu'))
cnn_model.add(tfkl.BatchNormalization())
cnn_model.add(tfkl.Bidirectional(
tfkl.GRU(200, activation='relu', return_sequences=True, implementation=0)))
cnn_model.add(tfkl.Dropout(0.2))
cnn_model.add(tfkl.BatchNormalization())
cnn_model.add(tfkl.TimeDistributed(tfkl.Dense(20)))
cnn_model.add(tfkl.Dropout(0.2))
cnn_model.add(tfkl.Softmax())
cnn_model.compile(loss='mae', optimizer='Adam', metrics=['mae'])
选项 4
可以与选项 3 一起使用。
由于频率维度可能在垂直方向线性增加,并且由于卷积看不到它们作为一个整体进行卷积的维度,因此您可以添加一个具有归一化频率值(不是强度,而是实际频率)的通道,看看它是否增加了好处信息。
因此,作为示例,考虑形状为 (examples, 4500, 9000, channels_1_or_3)
的选项 2。选择一项:
在输入数据中:
freq_channel = (numpy.arange(9000) / 9000) - 0.5 #shape (9000,)
freq_channel = numpy.stack([freq_channel] * 4500, axis=0) #shape (4500,9000)
freq_channel = numpy.stach([freq_channel] * examples, axis=0) #shape (examples, 4500, 9000)
freq_channel = freq_channel.reshape((-1, 4500, 9000, 1))
new_x_train = numpy.concatenate([original_x_train, freq_channel], axis=-1))
模型中:
import tf.keras.backend as K
def add_freq_channel(x):
shape = K.shape(x) #(examples, 4500, 9000, channels)
shape = K.concatenate([shape[:-1], K.constant([1])]) #(examples, 4500, 9000, 1)
freq_channel = (K.arange(9000) / 9000) - 0.5 #shape (9000,)
freq_channel = K.reshape(freq_channel, (1, 1, 9000, 1))
freq_channel = freq_channel * K.ones(shape)
return K.concatenate([x, freq_channel], axis=-1)
cnn_model.add(tfkl.Lambda(add_freq_channel, input_shape=(4500,9000,channels)))
你也许可以(不确定它是否会带来改进),将这个想法也扩展到时间维度。按照上面的相同过程添加一个额外的通道,但关注 X 轴,大小为 4500。在这种情况下,您可以将它与任何其他选项一起使用。
对您的模型的一般建议
我不确定 GRU
是如何工作的,但由于它是经常性的,所以在这一层坚持使用 activation = 'tanh'
可能是一个更好的主意。我在某个地方读过,但不记得在哪里,'tanh' 激活至少对于 LSTM 层更好。可能是因为循环计算可能导致爆炸。 (当然你可以测试一下,得出更好的结论)
tfkl.TimeDistributed(tfkl.Dense(20))
在 Keras 中等于 tfkl.Dense(20)
。您可以避免在此处添加 TimeDistributed
开销。
我觉得您没有将此视为典型的语音识别问题。因为我在你的方法中发现了几个奇怪的选择。
我注意到的问题
MFCC 运算的输出形状。
如果你看librosa.feature.mfcc,就是这样说的,
Returns: M:np.ndarray [shape=(n_mfcc, t)]
如您所见,这里没有频道。有输入维度 (n_mfcc
) 和时间维度 (t
)。因此,你应该可以直接使用Conv1D
而不需要任何预处理。
SoftMax 之前的 Dropout
这就是你的算法尾部的样子,
cnn_model.add(tfkl.TimeDistributed(tfkl.Dense(20)))
cnn_model.add(tfkl.Dropout(0.2))
cnn_model.add(tfkl.Softmax())
就我个人而言,我没有使用过最后一层使用dropout的人。所以我会摆脱它。因为 dropout 会随机切换神经元。但是您希望所有输出节点随时打开。
损失函数
通常,CTC用于优化语音识别模型。我(个人)还没有看到任何人使用 mae
作为语音模型的损失。因为,您的输入数据和标签数据通常具有未对齐的时间维度。这意味着,并不总是有一个标签对应于预测的每个时间步长。这就是 CTC 损失的亮点。这可能就是您想要用于此模型的内容(除非您 100% 确定每个预测都有一个标签并且它们完全对齐)。
话虽如此,损失取决于您要解决的问题。但我将包含一个示例,说明如何使用此损失来解决此问题。
一个工作示例
数据集
为了展示一个工作示例,我将使用 this 语音数据集。我选择这个是因为,由于问题的简单性,我可以很快得到一个好的结果。
- 输入:音频
- 输出:一个标签0-9
MFCC 变换
然后你可以对音频文件进行MFCC,你会得到如下热图。所以正如我之前所说,这将是一个二维矩阵 (n_mfcc, timesteps)
大小的数组。随着批次维度的增加,(batch size, n_mfcc, timesteps)
.
以下是您如何可视化以上内容。这里,y 是通过 librosa.core.load()
函数加载的音频。
y = audios[aid][1][0]
sr = audios[aid][1][1]
mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=20)
print(mfcc.shape)
plt.figure(figsize=(6, 4))
librosa.display.specshow(mfcc, x_axis='time')
plt.colorbar()
plt.title('MFCC')
plt.tight_layout()
正在创建 training/testing 数据
接下来您可以创建训练和测试数据。这是我创建的。
- train_data -
(sample size, timesteps, n_mfcc)
大小数组
- train_labels = A
(sample size, timesteps, num_classes)
大小数组
- train_inp_lengths -
(sample size,
)` 大小数组(用于 CTC 损失)
train_seq_lengths - A (sample size,
)` 大小数组(用于 CTC 损失)
test_data - 一个(sample size, timesteps, n_mfcc)
大小的数组
- test_labels = A
(sample size, timesteps, num_classes+1)
大小数组
- test_inp_lengths -
(sample size,
)` 大小数组(用于 CTC 损失)
- test_seq_lengths -
(sample size,
)` 大小数组(用于 CTC 损失)
我正在使用以下映射将字符转换为数字
alphabet = 'abcdefghijklmnopqrstuvwxyz '
a_map = {} # map letter to number
rev_a_map = {} # map number to letter
for i, a in enumerate(alphabet):
a_map[a] = i
rev_a_map[i] = a
label_map = {0:'zero', 1:'one', 2:'two', 3:'three', 4:'four', 5:'five', 6:'six', 7: 'seven', 8: 'eight', 9:'nine'}
有几点需要注意。
- 注意
mfcc
操作returns(n_mfcc, time)
。您必须进行轴置换才能使其成为 (time, n_mfcc)
格式。这样卷积就发生在时间维度上。
- 我还必须确保标签具有与输入完全相同的时间步数(这对于 ctc_loss 不是必需的)。但这是 keras 模型定义强制执行的要求。这是通过在每个字符序列的末尾添加空格来完成的。
定义模型
我已经从顺序 API 更改为功能 API,因为我需要包含几个输入层才能使这项工作适用于 ctc_loss
。此外,我去掉了最后一个 dropout 层。
def ctc_loss(inp_lengths, seq_lengths):
def loss(y_true, y_pred):
l = tf.reduce_mean(K.ctc_batch_cost(tf.argmax(y_true, axis=-1), y_pred, inp_lengths, seq_lengths))
return l
return loss
K.clear_session()
inp = tfk.Input(shape=(10,50))
inp_len = tfk.Input(shape=(1))
seq_len = tfk.Input(shape=(1))
out = tfkl.Conv1D(filters= 128, kernel_size= 5, padding='same', activation='relu')(inp)
out = tfkl.BatchNormalization()(out)
out = tfkl.Bidirectional(tfkl.GRU(128, return_sequences=True, implementation=0))(out)
out = tfkl.Dropout(0.2)(out)
out = tfkl.BatchNormalization()(out)
out = tfkl.TimeDistributed(tfkl.Dense(27, activation='softmax'))(out)
cnn_model = tfk.models.Model(inputs=[inp, inp_len, seq_len], outputs=out)
cnn_model.compile(loss=ctc_loss(inp_lengths=inp_len , seq_lengths=seq_len), optimizer='Adam', metrics=['mae'])
训练模型
然后你只需打电话,
cnn_model.fit([train_data, train_inp_lengths, train_seq_lengths], train_labels, batch_size=64, epochs=20)
这给了,
Train on 900 samples
Epoch 1/20
900/900 [==============================] - 3s 3ms/sample - loss: 11.4955 - mean_absolute_error: 0.0442
Epoch 2/20
900/900 [==============================] - 2s 2ms/sample - loss: 4.1317 - mean_absolute_error: 0.0340
...
Epoch 19/20
900/900 [==============================] - 2s 2ms/sample - loss: 0.1162 - mean_absolute_error: 0.0275
Epoch 20/20
900/900 [==============================] - 2s 2ms/sample - loss: 0.1012 - mean_absolute_error: 0.0277
使用模型进行预测
y = cnn_model.predict([test_data, test_inp_lengths, test_seq_lengths])
n_ids = 5
for pred, true in zip(y[:n_ids,:,:], test_labels[:n_ids,:,:]):
pred_ids = np.argmax(pred,axis=-1)
true_ids = np.argmax(true, axis=-1)
print('pred > ',[rev_a_map[tid] for tid in pred_ids])
print('true > ',[rev_a_map[tid] for tid in true_ids])
这给出了,
pred > ['e', ' ', 'i', 'i', 'i', 'g', 'h', ' ', ' ', 't']
true > ['e', 'i', 'g', 'h', 't', ' ', ' ', ' ', ' ', ' ']
pred > ['o', ' ', ' ', 'n', 'e', ' ', ' ', ' ', ' ', ' ']
true > ['o', 'n', 'e', ' ', ' ', ' ', ' ', ' ', ' ', ' ']
pred > ['s', 'e', ' ', ' ', ' ', ' ', ' ', ' ', 'v', 'e']
true > ['s', 'e', 'v', 'e', 'n', ' ', ' ', ' ', ' ', ' ']
pred > ['z', 'e', ' ', ' ', ' ', ' ', ' ', 'r', 'o', ' ']
true > ['z', 'e', 'r', 'o', ' ', ' ', ' ', ' ', ' ', ' ']
pred > ['n', ' ', ' ', 'i', 'i', 'n', 'e', ' ', ' ', ' ']
true > ['n', 'i', 'n', 'e', ' ', ' ', ' ', ' ', ' ', ' ']
要去除中间的重复字母和空格,请使用 ctc_decode
函数,如下所示。
y = cnn_model.predict([test_data, test_inp_lengths, test_seq_lengths])
sess = K.get_session()
pred = sess.run(tf.keras.backend.ctc_decode(y, test_inp_lengths[:,0]))
rev_a_map[-1] = '-'
for pred, true in zip(pred[0][0][:n_ids,:], test_labels[:n_ids,:,:]):
print(pred.shape)
true_ids = np.argmax(true, axis=-1)
print('pred > ',[rev_a_map[tid] for tid in pred])
print('true > ',[rev_a_map[tid] for tid in true_ids])
这给了,
pred > ['e', 'i', 'g', 'h', 't']
true > ['e', 'i', 'g', 'h', 't', ' ', ' ', ' ', ' ', ' ']
pred > ['o', 'n', 'e', '-', '-']
true > ['o', 'n', 'e', ' ', ' ', ' ', ' ', ' ', ' ', ' ']
pred > ['s', 'e', 'i', 'v', 'n']
true > ['s', 'e', 'v', 'e', 'n', ' ', ' ', ' ', ' ', ' ']
pred > ['z', 'e', 'r', 'o', '-']
true > ['z', 'e', 'r', 'o', ' ', ' ', ' ', ' ', ' ', ' ']
pred > ['n', 'i', 'n', 'e', '-']
true > ['n', 'i', 'n', 'e', ' ', ' ', ' ', ' ', ' ', ' ']
- 请注意,我添加了一个新标签
-1
。这是 ctc_decode
函数为表示任何空白而添加的内容。
我有一个音频数据集,我已经将这些音频转换成如下所示的介绍 MFCC 绘图:
现在我想为我的神经网络提供数据
import tensorflow as tf
import tensorflow.keras as tfk
import tensorflow.keras.layers as tfkl
cnn_model = tfk.Sequential(name='CNN_model')
cnn_model.add(tfkl.Conv1D(filters= 225, kernel_size= 11, padding='same', activation='relu', input_shape=(4500,9000, 3)))
cnn_model.add(tfkl.BatchNormalization())
cnn_model.add(tfkl.Bidirectional(tfkl.GRU(200, activation='relu', return_sequences=True, implementation=0)))
cnn_model.add(tfkl.Dropout(0.2))
cnn_model.add(tfkl.BatchNormalization())
cnn_model.add(tfkl.TimeDistributed(tfkl.Dense(20)))
cnn_model.add(tfkl.Dropout(0.2))
cnn_model.add(tfkl.Softmax())
cnn_model.compile(loss='mae', optimizer='Adam', metrics=['mae'])
cnn_model.summary()
我使用了Conv1D,因为它是这种神经网络中使用的层。但我不知道如何将数据从 图像转换为 CNN 的输入。自己尝试了几次改造,就是不行。
正如您在下图中看到的,我需要提供第一层 Conv1D
但我不能,因为我的图像的形状是 (4500, 9000, 3)
。所以基本上,我想做的就是将此图像转换为 Conv1D
的输入 ,方法与下图相同。
此图像代表传递给 NN 的 1 个音频。
显然,当我将具有此形状的图像传递到 Conv1D
层时,我有一个 ValueError
ValueError: Input 0 of layer conv1d_4 is incompatible with the layer: expected ndim=3, found ndim=4. Full shape received: [None, 4500, 9000, 3]
我将我的图像转为灰度,但不是方法,我丢失了一个有价值的信息。
我认为你可以将图像转换为灰度,但你可能会丢失大量有价值的数据。
最好的方法是重塑 MFCC 频谱图。 img.reshape(4500, 3 * 9000)
例子
# Sample data
>>> a
array([[[1, 1, 1],
[2, 2, 2]],
[[3, 3, 3],
[4, 4, 4]]])
>>> a.shape
(2, 2, 3)
# Reshaping data
>>> a.reshape(2, -1)
array([[1, 1, 1, 2, 2, 2],
[3, 3, 3, 4, 4, 4]])
# Or
>>> a.reshape(2, 6)
array([[1, 1, 1, 2, 2, 2],
[3, 3, 3, 4, 4, 4]])
如果你说 X 是时间,那么考虑形状 (examples, time_steps, frequency_bins, img_channels)
,你可以尝试一些东西。
选项 1
最明显的是在@skillsmuggler 的回答中提到的。不是时间的一切都是特征,所以:
#if in the model:
x_train = original_x_train
cnn_model.add(tfkl.Reshape((4500, 27000), input_shape=(4500,9000,3))) #first layer
#if directly in the data:
x_train = original_x_train.reshape((-1, 4500, 27000))
cnn_model.add(tfkl.Conv1D(filters= 225, kernel_size= 11, padding='same',
activation='relu', input_shape=(4500,9000, 3))) #original first layer
选项 2
但还有更多的可能性。我不知道 MFCC 是什么,但我怀疑它是由以下材料制成的:
- x = 时间步长
- y = 频点
- 颜色=强度
如果是这样,首先要做的是获取强度的原始值而不是 3 通道像素值。对于网络来说,获得连续值的想法比 3 个通道以更复杂的方式变化来表示同一事物要容易得多(这些颜色仅适用于我们的人眼,但它们在数学上要复杂得多)
如果您可以访问原始值而不是颜色,那么您可以像 选项 2 输入 (examples, time_steps, frequency_bins)
,就是这样,没有图像颜色通道。更少的输入,更好地表示信息。本例中的值为 "intensity".
print(x_train.shape) #-> (examples, 4500, 9000)
那么你的模型就不需要改变了。
选项 3
现在,如果你说你用上述方法丢失了信息,那么你可以尝试许多其他奇特的东西,我能想到的第一个是首先对频率维度进行卷积,以某种方式合并或折叠它,然后然后开始处理时间维度。
类似于这个两部分模型。
第 1 部分: 卷积和折叠频率。
input_channels = 1 or 3 #preferrably 1, following option 2,
#but it's possible to use the 3 channel images too (less optimal)
cnn_model = tfk.Sequential(name='CNN_model')
cnn_model.add(tfkl.TimeDistributed(
Conv1D(filters, size, activation=...),
input_shape=(4500,9000,input_channels)))
#shapes will be all in the type (examples, 4500, decreasing_freq_size, increasing_channels)
#make a time distributed conv model in the VGG style until you collapse the last channel dimension
cnn_model.add(tfkl.TimeDistributed(Conv1D(...)))
...
cnn_model.add(tfkl.MaxPooling1D())
cnn_model.add(tfkl.TimeDistributed(Conv1D(...)))
...
cnn_model.add(tfkl.MaxPooling1D())
cnn_model.add(tfkl.TimeDistributed(Conv1D(...)))
...
#when the 9000 has been reduced a lot
import tf.keras.backend as K
cnn_model.add(tfkl.Lambda(lambda x: K.mean(x, axis=2)))
#the line above is equivalent to the following, but seems more efficient
#cnn_model.add(tfkl.TimeDistributed(GlobalAveragePooling1D()))
#new shape style: (examples, 4500, increased_channels)
#no need for a huge number of channels, maybe around 100?
cnn_model.add(tfkl.Dense(units=around_100)) #Dense is equal to TimeDistributed(Dense)
现在您已经将 (examples, 4500, 9000, ch_1_or_3)
的形状转换为 (examples, 4500, features_around_100)
的形状,您可以转到第二部分,即您的原始模型。
第 2 部分:继续您的原始模型。
cnn_model.add(tfkl.Conv1D(filters= 225, kernel_size= 11, padding='same', activation='relu'))
cnn_model.add(tfkl.BatchNormalization())
cnn_model.add(tfkl.Bidirectional(
tfkl.GRU(200, activation='relu', return_sequences=True, implementation=0)))
cnn_model.add(tfkl.Dropout(0.2))
cnn_model.add(tfkl.BatchNormalization())
cnn_model.add(tfkl.TimeDistributed(tfkl.Dense(20)))
cnn_model.add(tfkl.Dropout(0.2))
cnn_model.add(tfkl.Softmax())
cnn_model.compile(loss='mae', optimizer='Adam', metrics=['mae'])
选项 4
可以与选项 3 一起使用。 由于频率维度可能在垂直方向线性增加,并且由于卷积看不到它们作为一个整体进行卷积的维度,因此您可以添加一个具有归一化频率值(不是强度,而是实际频率)的通道,看看它是否增加了好处信息。
因此,作为示例,考虑形状为 (examples, 4500, 9000, channels_1_or_3)
的选项 2。选择一项:
在输入数据中:
freq_channel = (numpy.arange(9000) / 9000) - 0.5 #shape (9000,)
freq_channel = numpy.stack([freq_channel] * 4500, axis=0) #shape (4500,9000)
freq_channel = numpy.stach([freq_channel] * examples, axis=0) #shape (examples, 4500, 9000)
freq_channel = freq_channel.reshape((-1, 4500, 9000, 1))
new_x_train = numpy.concatenate([original_x_train, freq_channel], axis=-1))
模型中:
import tf.keras.backend as K
def add_freq_channel(x):
shape = K.shape(x) #(examples, 4500, 9000, channels)
shape = K.concatenate([shape[:-1], K.constant([1])]) #(examples, 4500, 9000, 1)
freq_channel = (K.arange(9000) / 9000) - 0.5 #shape (9000,)
freq_channel = K.reshape(freq_channel, (1, 1, 9000, 1))
freq_channel = freq_channel * K.ones(shape)
return K.concatenate([x, freq_channel], axis=-1)
cnn_model.add(tfkl.Lambda(add_freq_channel, input_shape=(4500,9000,channels)))
你也许可以(不确定它是否会带来改进),将这个想法也扩展到时间维度。按照上面的相同过程添加一个额外的通道,但关注 X 轴,大小为 4500。在这种情况下,您可以将它与任何其他选项一起使用。
对您的模型的一般建议
我不确定
GRU
是如何工作的,但由于它是经常性的,所以在这一层坚持使用activation = 'tanh'
可能是一个更好的主意。我在某个地方读过,但不记得在哪里,'tanh' 激活至少对于 LSTM 层更好。可能是因为循环计算可能导致爆炸。 (当然你可以测试一下,得出更好的结论)tfkl.TimeDistributed(tfkl.Dense(20))
在 Keras 中等于tfkl.Dense(20)
。您可以避免在此处添加TimeDistributed
开销。
我觉得您没有将此视为典型的语音识别问题。因为我在你的方法中发现了几个奇怪的选择。
我注意到的问题
MFCC 运算的输出形状。
如果你看librosa.feature.mfcc,就是这样说的,
Returns: M:np.ndarray [shape=(n_mfcc, t)]
如您所见,这里没有频道。有输入维度 (n_mfcc
) 和时间维度 (t
)。因此,你应该可以直接使用Conv1D
而不需要任何预处理。
SoftMax 之前的 Dropout
这就是你的算法尾部的样子,
cnn_model.add(tfkl.TimeDistributed(tfkl.Dense(20)))
cnn_model.add(tfkl.Dropout(0.2))
cnn_model.add(tfkl.Softmax())
就我个人而言,我没有使用过最后一层使用dropout的人。所以我会摆脱它。因为 dropout 会随机切换神经元。但是您希望所有输出节点随时打开。
损失函数
通常,CTC用于优化语音识别模型。我(个人)还没有看到任何人使用 mae
作为语音模型的损失。因为,您的输入数据和标签数据通常具有未对齐的时间维度。这意味着,并不总是有一个标签对应于预测的每个时间步长。这就是 CTC 损失的亮点。这可能就是您想要用于此模型的内容(除非您 100% 确定每个预测都有一个标签并且它们完全对齐)。
话虽如此,损失取决于您要解决的问题。但我将包含一个示例,说明如何使用此损失来解决此问题。
一个工作示例
数据集
为了展示一个工作示例,我将使用 this 语音数据集。我选择这个是因为,由于问题的简单性,我可以很快得到一个好的结果。
- 输入:音频
- 输出:一个标签0-9
MFCC 变换
然后你可以对音频文件进行MFCC,你会得到如下热图。所以正如我之前所说,这将是一个二维矩阵 (n_mfcc, timesteps)
大小的数组。随着批次维度的增加,(batch size, n_mfcc, timesteps)
.
以下是您如何可视化以上内容。这里,y 是通过 librosa.core.load()
函数加载的音频。
y = audios[aid][1][0]
sr = audios[aid][1][1]
mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=20)
print(mfcc.shape)
plt.figure(figsize=(6, 4))
librosa.display.specshow(mfcc, x_axis='time')
plt.colorbar()
plt.title('MFCC')
plt.tight_layout()
正在创建 training/testing 数据
接下来您可以创建训练和测试数据。这是我创建的。
- train_data -
(sample size, timesteps, n_mfcc)
大小数组 - train_labels = A
(sample size, timesteps, num_classes)
大小数组 - train_inp_lengths -
(sample size,
)` 大小数组(用于 CTC 损失) train_seq_lengths - A
(sample size,
)` 大小数组(用于 CTC 损失)test_data - 一个
(sample size, timesteps, n_mfcc)
大小的数组- test_labels = A
(sample size, timesteps, num_classes+1)
大小数组 - test_inp_lengths -
(sample size,
)` 大小数组(用于 CTC 损失) - test_seq_lengths -
(sample size,
)` 大小数组(用于 CTC 损失)
我正在使用以下映射将字符转换为数字
alphabet = 'abcdefghijklmnopqrstuvwxyz '
a_map = {} # map letter to number
rev_a_map = {} # map number to letter
for i, a in enumerate(alphabet):
a_map[a] = i
rev_a_map[i] = a
label_map = {0:'zero', 1:'one', 2:'two', 3:'three', 4:'four', 5:'five', 6:'six', 7: 'seven', 8: 'eight', 9:'nine'}
有几点需要注意。
- 注意
mfcc
操作returns(n_mfcc, time)
。您必须进行轴置换才能使其成为(time, n_mfcc)
格式。这样卷积就发生在时间维度上。 - 我还必须确保标签具有与输入完全相同的时间步数(这对于 ctc_loss 不是必需的)。但这是 keras 模型定义强制执行的要求。这是通过在每个字符序列的末尾添加空格来完成的。
定义模型
我已经从顺序 API 更改为功能 API,因为我需要包含几个输入层才能使这项工作适用于 ctc_loss
。此外,我去掉了最后一个 dropout 层。
def ctc_loss(inp_lengths, seq_lengths):
def loss(y_true, y_pred):
l = tf.reduce_mean(K.ctc_batch_cost(tf.argmax(y_true, axis=-1), y_pred, inp_lengths, seq_lengths))
return l
return loss
K.clear_session()
inp = tfk.Input(shape=(10,50))
inp_len = tfk.Input(shape=(1))
seq_len = tfk.Input(shape=(1))
out = tfkl.Conv1D(filters= 128, kernel_size= 5, padding='same', activation='relu')(inp)
out = tfkl.BatchNormalization()(out)
out = tfkl.Bidirectional(tfkl.GRU(128, return_sequences=True, implementation=0))(out)
out = tfkl.Dropout(0.2)(out)
out = tfkl.BatchNormalization()(out)
out = tfkl.TimeDistributed(tfkl.Dense(27, activation='softmax'))(out)
cnn_model = tfk.models.Model(inputs=[inp, inp_len, seq_len], outputs=out)
cnn_model.compile(loss=ctc_loss(inp_lengths=inp_len , seq_lengths=seq_len), optimizer='Adam', metrics=['mae'])
训练模型
然后你只需打电话,
cnn_model.fit([train_data, train_inp_lengths, train_seq_lengths], train_labels, batch_size=64, epochs=20)
这给了,
Train on 900 samples
Epoch 1/20
900/900 [==============================] - 3s 3ms/sample - loss: 11.4955 - mean_absolute_error: 0.0442
Epoch 2/20
900/900 [==============================] - 2s 2ms/sample - loss: 4.1317 - mean_absolute_error: 0.0340
...
Epoch 19/20
900/900 [==============================] - 2s 2ms/sample - loss: 0.1162 - mean_absolute_error: 0.0275
Epoch 20/20
900/900 [==============================] - 2s 2ms/sample - loss: 0.1012 - mean_absolute_error: 0.0277
使用模型进行预测
y = cnn_model.predict([test_data, test_inp_lengths, test_seq_lengths])
n_ids = 5
for pred, true in zip(y[:n_ids,:,:], test_labels[:n_ids,:,:]):
pred_ids = np.argmax(pred,axis=-1)
true_ids = np.argmax(true, axis=-1)
print('pred > ',[rev_a_map[tid] for tid in pred_ids])
print('true > ',[rev_a_map[tid] for tid in true_ids])
这给出了,
pred > ['e', ' ', 'i', 'i', 'i', 'g', 'h', ' ', ' ', 't']
true > ['e', 'i', 'g', 'h', 't', ' ', ' ', ' ', ' ', ' ']
pred > ['o', ' ', ' ', 'n', 'e', ' ', ' ', ' ', ' ', ' ']
true > ['o', 'n', 'e', ' ', ' ', ' ', ' ', ' ', ' ', ' ']
pred > ['s', 'e', ' ', ' ', ' ', ' ', ' ', ' ', 'v', 'e']
true > ['s', 'e', 'v', 'e', 'n', ' ', ' ', ' ', ' ', ' ']
pred > ['z', 'e', ' ', ' ', ' ', ' ', ' ', 'r', 'o', ' ']
true > ['z', 'e', 'r', 'o', ' ', ' ', ' ', ' ', ' ', ' ']
pred > ['n', ' ', ' ', 'i', 'i', 'n', 'e', ' ', ' ', ' ']
true > ['n', 'i', 'n', 'e', ' ', ' ', ' ', ' ', ' ', ' ']
要去除中间的重复字母和空格,请使用 ctc_decode
函数,如下所示。
y = cnn_model.predict([test_data, test_inp_lengths, test_seq_lengths])
sess = K.get_session()
pred = sess.run(tf.keras.backend.ctc_decode(y, test_inp_lengths[:,0]))
rev_a_map[-1] = '-'
for pred, true in zip(pred[0][0][:n_ids,:], test_labels[:n_ids,:,:]):
print(pred.shape)
true_ids = np.argmax(true, axis=-1)
print('pred > ',[rev_a_map[tid] for tid in pred])
print('true > ',[rev_a_map[tid] for tid in true_ids])
这给了,
pred > ['e', 'i', 'g', 'h', 't']
true > ['e', 'i', 'g', 'h', 't', ' ', ' ', ' ', ' ', ' ']
pred > ['o', 'n', 'e', '-', '-']
true > ['o', 'n', 'e', ' ', ' ', ' ', ' ', ' ', ' ', ' ']
pred > ['s', 'e', 'i', 'v', 'n']
true > ['s', 'e', 'v', 'e', 'n', ' ', ' ', ' ', ' ', ' ']
pred > ['z', 'e', 'r', 'o', '-']
true > ['z', 'e', 'r', 'o', ' ', ' ', ' ', ' ', ' ', ' ']
pred > ['n', 'i', 'n', 'e', '-']
true > ['n', 'i', 'n', 'e', ' ', ' ', ' ', ' ', ' ', ' ']
- 请注意,我添加了一个新标签
-1
。这是ctc_decode
函数为表示任何空白而添加的内容。