代码示例 / 音频数据 / 说话人识别

说话人识别

作者: Fadi Badine
创建日期: 14/06/2020
最后修改: 19/07/2023
描述: 使用快速傅里叶变换(FFT)和1D卷积网络对说话人进行分类。

在Colab中查看 GitHub源代码


介绍

这个例子演示了如何创建一个模型,以从通过快速傅里叶变换(FFT)获得的语音录音的频域表示中分类说话人。

它展示了以下内容:

  • 如何使用 tf.data 加载、预处理并将音频流输入模型
  • 如何创建带有残差连接的1D卷积网络用于音频分类。

我们的流程:

  • 我们准备一个不同说话人的语音样本数据集,以说话人为标签。
  • 我们向这些样本中添加背景噪声以增强数据。
  • 我们对这些样本进行FFT。
  • 我们训练一个1D卷积网络,以预测给定嘈杂FFT语音样本的正确说话人。

注意:

  • 此示例应该在TensorFlow 2.3或更高版本,或 tf-nightly 上运行。
  • 数据集中噪声样本需要在使用本示例中的代码之前重新采样为16000 Hz的采样率。为此,您需要安装 ffmpg

设置

import os

os.environ["KERAS_BACKEND"] = "tensorflow"

import shutil
import numpy as np

import tensorflow as tf
import keras

from pathlib import Path
from IPython.display import display, Audio

# 从 https://www.kaggle.com/kongaevans/speaker-recognition-dataset/ 获取数据
# 并将其保存到 ./speaker-recognition-dataset.zip
# 然后解压到 ./16000_pcm_speeches
!kaggle datasets download -d kongaevans/speaker-recognition-dataset
!unzip -qq speaker-recognition-dataset.zip
DATASET_ROOT = "16000_pcm_speeches"

# 我们将音频样本和噪声样本放置的文件夹
AUDIO_SUBFOLDER = "audio"
NOISE_SUBFOLDER = "noise"

DATASET_AUDIO_PATH = os.path.join(DATASET_ROOT, AUDIO_SUBFOLDER)
DATASET_NOISE_PATH = os.path.join(DATASET_ROOT, NOISE_SUBFOLDER)

# 用于验证的样本百分比
VALID_SPLIT = 0.1

# 随机打乱数据集和噪声时使用的种子
SHUFFLE_SEED = 43

# 要使用的采样率。
# 这是所有音频样本中使用的采样率。
# 我们将所有噪声重新采样到这个采样率。
# 这也将是音频波样本的输出大小
# (因为所有样本都是1秒长)
SAMPLING_RATE = 16000

# 根据以下公式乘以噪声的因子:
#   noisy_sample = sample + noise * prop * scale
#      其中 prop = sample_amplitude / noise_amplitude
SCALE = 0.5

BATCH_SIZE = 128
EPOCHS = 1
警告:您的Kaggle API密钥对系统上的其他用户可读!要解决此问题,您可以运行 'chmod 600 /home/fchollet/.kaggle/kaggle.json'
正在下载 speaker-recognition-dataset.zip 到 /home/fchollet/keras-io/scripts/tmp_5022915
 90%|████████████████████████████████████▉    | 208M/231M [00:00<00:00, 217MB/s]
100%|█████████████████████████████████████████| 231M/231M [00:01<00:00, 227MB/s]

数据准备

数据集由7个文件夹组成,分为2组:

  • 语音样本,包含5个文件夹,代表5个不同的说话人。每个文件夹包含1500个音频文件,每个文件长度为1秒,采样率为16000 Hz。
  • 背景噪声样本,包含2个文件夹,总共6个文件。这些文件长度超过1秒(最初的采样率不是16000 Hz,但我们将其重新采样为16000 Hz)。 我们将使用这6个文件创建354个长1秒的噪声样本用于训练。

让我们将这2个类别整理到2个文件夹中:

  • 一个 audio 文件夹,其中将包含所有按说话人分组的语音样本文件夹
  • 一个 noise 文件夹,其中将包含所有噪声样本

在将音频和噪声类别整理到2个文件夹之前, 我们有以下目录结构:

main_directory/
...speaker_a/
...speaker_b/
...speaker_c/
...speaker_d/
...speaker_e/
...other/
..._background_noise_/

整理后,我们得到以下结构:

main_directory/
...audio/
......speaker_a/
......speaker_b/
......speaker_c/
......speaker_d/
......speaker_e/
...noise/
......other/
......_background_noise_/
for folder in os.listdir(DATASET_ROOT):
    if os.path.isdir(os.path.join(DATASET_ROOT, folder)):
        if folder in [AUDIO_SUBFOLDER, NOISE_SUBFOLDER]:
            # 如果文件夹是 `audio` 或 `noise`,则不执行任何操作
            continue
        elif folder in ["other", "_background_noise_"]:
            # 如果文件夹是包含噪声样本的文件夹之一,
            # 将其移动到 `noise` 文件夹
            shutil.move(
                os.path.join(DATASET_ROOT, folder),
                os.path.join(DATASET_NOISE_PATH, folder),
            )
        else:
            # 否则,它应该是一个说话者文件夹,然后将其移动到
            # `audio` 文件夹
            shutil.move(
                os.path.join(DATASET_ROOT, folder),
                os.path.join(DATASET_AUDIO_PATH, folder),
            )

噪声准备

在这一节:

  • 我们加载所有噪声样本(这些样本应该已经重新采样到16000)
  • 我们将这些噪声样本拆分为16000样本的块,每块对应1秒的持续时间
# 获取所有噪声文件的列表
noise_paths = []
for subdir in os.listdir(DATASET_NOISE_PATH):
    subdir_path = Path(DATASET_NOISE_PATH) / subdir
    if os.path.isdir(subdir_path):
        noise_paths += [
            os.path.join(subdir_path, filepath)
            for filepath in os.listdir(subdir_path)
            if filepath.endswith(".wav")
        ]
if not noise_paths:
    raise RuntimeError(f"在 {DATASET_NOISE_PATH} 找不到任何文件")
print(
    "找到 {} 个文件属于 {} 个目录".format(
        len(noise_paths), len(os.listdir(DATASET_NOISE_PATH))
    )
)
找到 6 个文件属于 2 个目录

将所有噪声样本重新采样到16000 Hz

command = (
    "for dir in `ls -1 " + DATASET_NOISE_PATH + "`; do "
    "for file in `ls -1 " + DATASET_NOISE_PATH + "/$dir/*.wav`; do "
    "sample_rate=`ffprobe -hide_banner -loglevel panic -show_streams "
    "$file | grep sample_rate | cut -f2 -d=`; "
    "if [ $sample_rate -ne 16000 ]; then "
    "ffmpeg -hide_banner -loglevel panic -y "
    "-i $file -ar 16000 temp.wav; "
    "mv temp.wav $file; "
    "fi; done; done"
)
os.system(command)


# 将噪声拆分为每个16,000步的块
def load_noise_sample(path):
    sample, sampling_rate = tf.audio.decode_wav(
        tf.io.read_file(path), desired_channels=1
    )
    if sampling_rate == SAMPLING_RATE:
        # 从噪声样本中可以生成的每个16000的切片数量
        slices = int(sample.shape[0] / SAMPLING_RATE)
        sample = tf.split(sample[: slices * SAMPLING_RATE], slices)
        return sample
    else:
        print("文件 {} 的采样率不正确。忽略它".format(path))
        return None


noises = []
for path in noise_paths:
    sample = load_noise_sample(path)
    if sample:
        noises.extend(sample)
noises = tf.stack(noises)

print(
    "{} 个噪声文件被拆分为 {} 个噪声样本,每个样本长 {} 秒".format(
        len(noise_paths), noises.shape[0], noises.shape[1] // SAMPLING_RATE
    )
)
6 个噪声文件被拆分为 354 个噪声样本,每个样本长 1 秒

数据集生成

def paths_and_labels_to_dataset(audio_paths, labels):
    """构造音频和标签的数据集。"""
    path_ds = tf.data.Dataset.from_tensor_slices(audio_paths)
    audio_ds = path_ds.map(
        lambda x: path_to_audio(x), num_parallel_calls=tf.data.AUTOTUNE
    )
    label_ds = tf.data.Dataset.from_tensor_slices(labels)
    return tf.data.Dataset.zip((audio_ds, label_ds))


def path_to_audio(path):
    """读取和解码音频文件。"""
    audio = tf.io.read_file(path)
    audio, _ = tf.audio.decode_wav(audio, 1, SAMPLING_RATE)
    return audio


def add_noise(audio, noises=None, scale=0.5):
    if noises is not None:
        # 创建一个与音频大小相同的随机张量,范围从
        # 0 到我们拥有的噪声流样本的数量。
        tf_rnd = tf.random.uniform(
            (tf.shape(audio)[0],), 0, noises.shape[0], dtype=tf.int32
        )
        noise = tf.gather(noises, tf_rnd, axis=0)

        # 获取音频和噪声之间的幅度比例
        prop = tf.math.reduce_max(audio, axis=1) / tf.math.reduce_max(noise, axis=1)
        prop = tf.repeat(tf.expand_dims(prop, axis=1), tf.shape(audio)[1], axis=1)

        # 将重新缩放的噪声添加到音频中
        audio = audio + noise * prop * scale

    return audio


def audio_to_fft(audio):
    # 因为 tf.signal.fft 在最内层维度上应用 FFT,
    # 我们需要压缩维度,然后在 FFT 后再次扩展它们
    audio = tf.squeeze(audio, axis=-1)
    fft = tf.signal.fft(
        tf.cast(tf.complex(real=audio, imag=tf.zeros_like(audio)), tf.complex64)
    )
    fft = tf.expand_dims(fft, axis=-1)

    # 返回 FFT 前半部分的绝对值
    # 代表正频率
    return tf.math.abs(fft[:, : (audio.shape[1] // 2), :])


# 获取音频文件路径列表以及它们对应的标签

class_names = os.listdir(DATASET_AUDIO_PATH)
print(
    "我们的类别名称: {}".format(
        class_names,
    )
)

audio_paths = []
labels = []
for label, name in enumerate(class_names):
    print(
        "正在处理说话者 {}".format(
            name,
        )
    )
    dir_path = Path(DATASET_AUDIO_PATH) / name
    speaker_sample_paths = [
        os.path.join(dir_path, filepath)
        for filepath in os.listdir(dir_path)
        if filepath.endswith(".wav")
    ]
    audio_paths += speaker_sample_paths
    labels += [label] * len(speaker_sample_paths)

print(
    "找到 {} 个文件属于 {} 个类别。".format(len(audio_paths), len(class_names))
)

# 打乱
rng = np.random.RandomState(SHUFFLE_SEED)
rng.shuffle(audio_paths)
rng = np.random.RandomState(SHUFFLE_SEED)
rng.shuffle(labels)

# 分割为训练集和验证集
num_val_samples = int(VALID_SPLIT * len(audio_paths))
print("使用 {} 个文件进行训练。".format(len(audio_paths) - num_val_samples))
train_audio_paths = audio_paths[:-num_val_samples]
train_labels = labels[:-num_val_samples]

print("使用 {} 个文件进行验证。".format(num_val_samples))
valid_audio_paths = audio_paths[-num_val_samples:]
valid_labels = labels[-num_val_samples:]

# 创建 2 个数据集,一个用于训练,另一个用于验证
train_ds = paths_and_labels_to_dataset(train_audio_paths, train_labels)
train_ds = train_ds.shuffle(buffer_size=BATCH_SIZE * 8, seed=SHUFFLE_SEED).batch(
    BATCH_SIZE
)

valid_ds = paths_and_labels_to_dataset(valid_audio_paths, valid_labels)
valid_ds = valid_ds.shuffle(buffer_size=32 * 8, seed=SHUFFLE_SEED).batch(32)


# 向训练集添加噪声
train_ds = train_ds.map(
    lambda x, y: (add_noise(x, noises, scale=SCALE), y),
    num_parallel_calls=tf.data.AUTOTUNE,
)

# 使用 `audio_to_fft` 将音频波形转换为频率域
train_ds = train_ds.map(
    lambda x, y: (audio_to_fft(x), y), num_parallel_calls=tf.data.AUTOTUNE
)
train_ds = train_ds.prefetch(tf.data.AUTOTUNE)

valid_ds = valid_ds.map(
    lambda x, y: (audio_to_fft(x), y), num_parallel_calls=tf.data.AUTOTUNE
)
valid_ds = valid_ds.prefetch(tf.data.AUTOTUNE)
我们的类名: ['Nelson_Mandela', 'Jens_Stoltenberg', 'Benjamin_Netanyau', 'Julia_Gillard', 'Magaret_Tarcher']
处理说话者 Nelson_Mandela
处理说话者 Jens_Stoltenberg
处理说话者 Benjamin_Netanyau
处理说话者 Julia_Gillard
处理说话者 Magaret_Tarcher
找到 7501 个文件,属于 5 个类别。
使用 6751 个文件进行训练。
使用 750 个文件进行验证。

模型定义

def residual_block(x, filters, conv_num=3, activation="relu"):
    # 短路
    s = keras.layers.Conv1D(filters, 1, padding="same")(x)
    for i in range(conv_num - 1):
        x = keras.layers.Conv1D(filters, 3, padding="same")(x)
        x = keras.layers.Activation(activation)(x)
    x = keras.layers.Conv1D(filters, 3, padding="same")(x)
    x = keras.layers.Add()([x, s])
    x = keras.layers.Activation(activation)(x)
    return keras.layers.MaxPool1D(pool_size=2, strides=2)(x)


def build_model(input_shape, num_classes):
    inputs = keras.layers.Input(shape=input_shape, name="input")

    x = residual_block(inputs, 16, 2)
    x = residual_block(x, 32, 2)
    x = residual_block(x, 64, 3)
    x = residual_block(x, 128, 3)
    x = residual_block(x, 128, 3)

    x = keras.layers.AveragePooling1D(pool_size=3, strides=3)(x)
    x = keras.layers.Flatten()(x)
    x = keras.layers.Dense(256, activation="relu")(x)
    x = keras.layers.Dense(128, activation="relu")(x)

    outputs = keras.layers.Dense(num_classes, activation="softmax", name="output")(x)

    return keras.models.Model(inputs=inputs, outputs=outputs)


model = build_model((SAMPLING_RATE // 2, 1), len(class_names))

model.summary()

# 使用 Adam 的默认学习率编译模型
model.compile(
    optimizer="Adam",
    loss="sparse_categorical_crossentropy",
    metrics=["accuracy"],
)

# 添加回调:
# 'EarlyStopping' 在模型不再提升时停止训练
# 'ModelCheckPoint' 始终保留具有最佳 val_accuracy 的模型
model_save_filename = "model.keras"

earlystopping_cb = keras.callbacks.EarlyStopping(patience=10, restore_best_weights=True)
mdlcheckpoint_cb = keras.callbacks.ModelCheckpoint(
    model_save_filename, monitor="val_accuracy", save_best_only=True
)
模型: "functional_1"
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓
┃ 层 (类型)         输出形状       参数数量  连接到         ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩
│ input (输入层)  │ (None, 8000, 1)   │       0 │ -                    │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ conv1d_1 (一维卷积)   │ (None, 8000, 16)  │      64 │ input[0][0]          │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ activation          │ (None, 8000, 16)  │       0 │ conv1d_1[0][0]       │
│ (激活)        │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ conv1d_2 (卷积层1D)   │ (, 8000, 16)  │     784 │ activation[0][0]     │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ conv1d (卷积层1D)     │ (, 8000, 16)  │      32 │ input[0][0]          │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ add (加法层)           │ (, 8000, 16)  │       0 │ conv1d_2[0][0],      │
│                     │                   │         │ conv1d[0][0]         │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ activation_1        │ (, 8000, 16)  │       0 │ add[0][0]            │
│ (激活层)        │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ max_pooling1d       │ (, 4000, 16)  │       0 │ activation_1[0][0]   │
│ (最大池化层1D)      │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ conv1d_4 (卷积层1D)   │ (, 4000, 32)  │   1,568 │ max_pooling1d[0][0]  │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ activation_2        │ (, 4000, 32)  │       0 │ conv1d_4[0][0]       │
│ (激活层)        │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ conv1d_5 (Conv1D)   │ (None, 4000, 32)  │   3,104 │ activation_2[0][0]   │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ conv1d_3 (Conv1D)   │ (None, 4000, 32)  │     544 │ max_pooling1d[0][0]  │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ add_1 (Add)         │ (None, 4000, 32)  │       0 │ conv1d_5[0][0],      │
│                     │                   │         │ conv1d_3[0][0]       │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ activation_3        │ (None, 4000, 32)  │       0 │ add_1[0][0]          │
│ (Activation)        │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ max_pooling1d_1     │ (None, 2000, 32)  │       0 │ activation_3[0][0]   │
│ (MaxPooling1D)      │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ conv1d_7 (Conv1D)   │ (None, 2000, 64)  │   6,208 │ max_pooling1d_1[0][ │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ activation_4        │ (None, 2000, 64)  │       0 │ conv1d_7[0][0]       │
│ (Activation)        │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ conv1d_8 (卷积1D)   │ (None, 2000, 64)  │  12,352 │ activation_4[0][0]   │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ activation_5        │ (None, 2000, 64)  │       0 │ conv1d_8[0][0]       │
│ (激活)        │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ conv1d_9 (卷积1D)   │ (None, 2000, 64)  │  12,352 │ activation_5[0][0]   │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ conv1d_6 (卷积1D)   │ (None, 2000, 64)  │   2,112 │ max_pooling1d_1[0][ │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ add_2 (加法)         │ (None, 2000, 64)  │       0 │ conv1d_9[0][0],      │
│                     │                   │         │ conv1d_6[0][0]       │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ activation_6        │ (None, 2000, 64)  │       0 │ add_2[0][0]          │
│ (激活)        │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ max_pooling1d_2     │ (None, 1000, 64)  │       0 │ activation_6[0][0]   │
│ (最大池化1D)      │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ conv1d_11 (Conv1D)  │ (None, 1000, 128) │  24,704 │ max_pooling1d_2[0][ │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ activation_7        │ (None, 1000, 128) │       0 │ conv1d_11[0][0]      │
│ (激活)        │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ conv1d_12 (Conv1D)  │ (None, 1000, 128) │  49,280 │ activation_7[0][0]   │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ activation_8        │ (None, 1000, 128) │       0 │ conv1d_12[0][0]      │
│ (激活)        │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ conv1d_13 (Conv1D)  │ (None, 1000, 128) │  49,280 │ activation_8[0][0]   │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ conv1d_10 (Conv1D)  │ (None, 1000, 128) │   8,320 │ max_pooling1d_2[0][ │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ add_3 (加法)         │ (None, 1000, 128) │       0 │ conv1d_13[0][0],     │
│                     │                   │         │ conv1d_10[0][0]      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ activation_9        │ (, 1000, 128) │       0 │ add_3[0][0]          │
│ (激活)        │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ max_pooling1d_3     │ (, 500, 128)  │       0 │ activation_9[0][0]   │
│ (最大池化1D)      │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ conv1d_15 (卷积1D)  │ (, 500, 128)  │  49,280 │ max_pooling1d_3[0][ │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ activation_10       │ (, 500, 128)  │       0 │ conv1d_15[0][0]      │
│ (激活)        │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ conv1d_16 (卷积1D)  │ (, 500, 128)  │  49,280 │ activation_10[0][0]  │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ activation_11       │ (, 500, 128)  │       0 │ conv1d_16[0][0]      │
│ (激活)        │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ conv1d_17 (卷积1D)  │ (, 500, 128)  │  49,280 │ activation_11[0][0]  │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ conv1d_14 (卷积层1D)  │ (, 500, 128)  │  16,512 │ max_pooling1d_3[0][ │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ add_4 (加法层)         │ (, 500, 128)  │       0 │ conv1d_17[0][0],     │
│                     │                   │         │ conv1d_14[0][0]      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ activation_12       │ (, 500, 128)  │       0 │ add_4[0][0]          │
│ (激活层)        │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ max_pooling1d_4     │ (, 250, 128)  │       0 │ activation_12[0][0]  │
│ (最大池化层1D)      │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ average_pooling1d   │ (, 83, 128)   │       0 │ max_pooling1d_4[0][ │
│ (平均池化层1D)  │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ flatten (展平)   │ (, 10624)     │       0 │ average_pooling1d[0… │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ dense (全连接层)       │ (, 256)       │ 2,720,… │ flatten[0][0]        │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ dense_1 (Dense)     │ (, 128)       │  32,896 │ dense[0][0]          │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ output (Dense)      │ (, 5)         │     645 │ dense_1[0][0]        │
└─────────────────────┴───────────────────┴─────────┴──────────────────────┘
 总参数: 3,088,597 (11.78 MB)
 可训练参数: 3,088,597 (11.78 MB)
 不可训练参数: 0 (0.00 B)

训练

history = model.fit(
    train_ds,
    epochs=EPOCHS,
    validation_data=valid_ds,
    callbacks=[earlystopping_cb, mdlcheckpoint_cb],
)
WARNING: 所有在 absl::InitializeLog() 被调用之前的日志消息都写入 STDERR
I0000 00:00:1699469571.349760  302130 device_compiler.h:186] 通过 XLA 编译了集群!  这一行在进程的生命周期内最多只记录一次。
W0000 00:00:1699469571.377393  302130 graph_launch.cc:671] 回退到逐操作模式,因为 memset 节点破坏了图更新

 52/53 ━━━━━━━━━━━━━━━━━━━━  0s 396ms/step - accuracy: 0.4496 - loss: 5.2439

W0000 00:00:1699469622.140353  302129 graph_launch.cc:671] 回退到逐操作模式,因为 memset 节点破坏了图更新

 53/53 ━━━━━━━━━━━━━━━━━━━━ 0s 974ms/step - accuracy: 0.4531 - loss: 5.1842

W0000 00:00:1699469625.456199  302130 graph_launch.cc:671] 回退到逐操作模式,因为 memset 节点破坏了图更新
W0000 00:00:1699469627.405341  302129 graph_launch.cc:671] 回退到逐操作模式,因为 memset 节点破坏了图更新

 53/53 ━━━━━━━━━━━━━━━━━━━━ 101s 1s/step - accuracy: 0.4564 - loss: 5.1267 - val_accuracy: 0.8720 - val_loss: 0.3273

评估

print(model.evaluate(valid_ds))
 24/24 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - accuracy: 0.8641 - loss: 0.3521
[0.32171541452407837, 0.871999979019165]

我们获得了约 98% 的验证准确率。


演示

让我们取一些样本并:

  • 预测说话者
  • 将预测与真实说话者进行比较
  • 听音频,看看尽管样本存在噪音,模型仍然相当准确
SAMPLES_TO_DISPLAY = 10

test_ds = paths_and_labels_to_dataset(valid_audio_paths, valid_labels)
test_ds = test_ds.shuffle(buffer_size=BATCH_SIZE * 8, seed=SHUFFLE_SEED).batch(
    BATCH_SIZE
)

test_ds = test_ds.map(
    lambda x, y: (add_noise(x, noises, scale=SCALE), y),
    num_parallel_calls=tf.data.AUTOTUNE,
)

for audios, labels in test_ds.take(1):
    # 获取信号 FFT
    ffts = audio_to_fft(audios)
    # 预测
    y_pred = model.predict(ffts)
    # 取随机样本
    rnd = np.random.randint(0, BATCH_SIZE, SAMPLES_TO_DISPLAY)
    audios = audios.numpy()[rnd, :, :]
    labels = labels.numpy()[rnd]
    y_pred = np.argmax(y_pred, axis=-1)[rnd]

    for index in range(SAMPLES_TO_DISPLAY):
        # 对于每个样本,打印真实标签和预测标签
        # 以及播放带噪音的声音
        print(
            "Speaker:\33{} {}\33[0m\tPredicted:\33{} {}\33[0m".format(
                "[92m" if labels[index] == y_pred[index] else "[91m",
                class_names[labels[index]],
                "[92m" if labels[index] == y_pred[index] else "[91m",
                class_names[y_pred[index]],
            )
        )
        display(Audio(audios[index, :, :].squeeze(), rate=SAMPLING_RATE))
 4/4 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step  
Speaker: Magaret_TarcherPredicted: Benjamin_Netanyau

W0000 00:00:1699469629.002282  302130 graph_launch.cc:671] 由于 memset 节点中断图形更新,退回到逐操作模式

发言者: Julia_Gillard预测: Julia_Gillard

发言者: 纳尔逊·曼德拉 预测: 纳尔逊·曼德拉

说话者: Magaret_Tarcher预测: Magaret_Tarcher

说话者: Julia_Gillard预测: Julia_Gillard

说话者: Julia_Gillard预测: Julia_Gillard

发言者: Jens_Stoltenberg预测: Jens_Stoltenberg

发言人: 本杰明·内塔尼亚胡 预测: 本杰明·内塔尼亚胡

发言人:纳尔逊·曼德拉 预测:纳尔逊·曼德拉

说话者: 纳尔逊·曼德拉预测: 纳尔逊·曼德拉