From abd112274d7793cf2dfaec9378c113a34b91b22c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=9C=E9=9B=A8=E9=A3=98=E9=9B=B6?= Date: Tue, 26 Apr 2022 15:34:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=A8=A1=E5=9E=8B=E5=92=8C?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=A2=84=E5=A4=84=E7=90=86=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + README.md | 212 +++++++++++++++++++++++--------------------- create_data.py | 4 +- eval.py | 66 ++++++++++++++ images/image1.png | Bin 0 -> 64003 bytes infer.py | 54 +++++------ infer_record.py | 60 +++++++------ reader.py | 41 --------- resnet.py | 160 --------------------------------- train.py | 82 ++++++++++------- utility.py | 17 ---- utils/__init__.py | 0 utils/ecapa_tdnn.py | 119 +++++++++++++++++++++++++ utils/reader.py | 89 +++++++++++++++++++ utils/utility.py | 59 ++++++++++++ 15 files changed, 557 insertions(+), 407 deletions(-) create mode 100644 eval.py create mode 100644 images/image1.png delete mode 100644 reader.py delete mode 100644 resnet.py delete mode 100644 utility.py create mode 100644 utils/__init__.py create mode 100644 utils/ecapa_tdnn.py create mode 100644 utils/reader.py create mode 100644 utils/utility.py diff --git a/.gitignore b/.gitignore index 8a51557..b97b70e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ __pycache__/ .idea/ dataset/ models/ +log/ infer_audio.wav \ No newline at end of file diff --git a/README.md b/README.md index 5296297..e63054c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ 主要介绍libsora,PyAudio,pydub的安装,其他的依赖包根据需要自行安装。 - Python 3.7 -- Pytorch 1.8.1 +- Pytorch 1.10.0 ## 安装libsora @@ -15,10 +15,10 @@ ```shell pip install pytest-runner -pip install librosa +pip install librosa==0.9.1 ``` -如果pip命令安装不成功,那就使用源码安装,下载源码:[https://github.com/librosa/librosa/releases/](https://github.com/librosa/librosa/releases/), windows的可以下载zip压缩包,方便解压。 +**注意:** 如果pip命令安装不成功,那就使用源码安装,下载源码:[https://github.com/librosa/librosa/releases/](https://github.com/librosa/librosa/releases/), windows的可以下载zip压缩包,方便解压。 ```shell pip install pytest-runner @@ -58,11 +58,12 @@ pip install pydub # 训练分类模型 -把音频转换成训练数据最重要的是使用了librosa,使用librosa可以很方便得到音频的梅尔频谱(Mel Spectrogram),使用的API为 `librosa.feature.melspectrogram()`,输出的是numpy值,可以直接用tensorflow训练和预测。关于梅尔频谱具体信息读者可以自行了解,跟梅尔频谱同样很重要的梅尔倒谱(MFCCs)更多用于语音识别中,对应的API为 `librosa.feature.mfcc()`。同样以下的代码,就可以获取到音频的梅尔频谱。 +把音频转换成训练数据最重要的是使用了librosa,使用librosa可以很方便得到音频的梅尔频谱(Mel Spectrogram),使用的API为 `librosa.feature.melspectrogram()`,输出的是numpy值。关于梅尔频谱具体信息读者可以自行了解,跟梅尔频谱同样很重要的梅尔倒谱(MFCCs)更多用于语音识别中,对应的API为 `librosa.feature.mfcc()`。同样以下的代码,就可以获取到音频的梅尔频谱。 ```python wav, sr = librosa.load(data_path, sr=16000) -spec_mag = librosa.feature.melspectrogram(y=wav, sr=sr, hop_length=256) +features = librosa.feature.melspectrogram(y=wav, sr=sr, n_fft=400, n_mels=80, hop_length=160, win_length=400) +features = librosa.power_to_db(features, ref=1.0, amin=1e-10, top_db=None) ``` ## 生成数据列表 @@ -108,17 +109,23 @@ if __name__ == '__main__': ```python class CustomDataset(Dataset): - def __init__(self, data_list_path, model='train', spec_len=128): + def __init__(self, data_list_path, model='train', sr=16000, chunk_duration=3): super(CustomDataset, self).__init__() with open(data_list_path, 'r') as f: self.lines = f.readlines() self.model = model - self.spec_len = spec_len + self.sr = sr + self.chunk_duration = chunk_duration def __getitem__(self, idx): - audio_path, label = self.lines[idx].replace('\n', '').split('\t') - spec_mag = load_audio(audio_path, mode=self.model, spec_len=self.spec_len) - return spec_mag, np.array(int(label), dtype=np.int64) + try: + audio_path, label = self.lines[idx].replace('\n', '').split('\t') + spec_mag = load_audio(audio_path, mode=self.model, sr=self.sr, chunk_duration=self.chunk_duration) + return spec_mag, np.array(int(label), dtype=np.int64) + except Exception as ex: + print(f"[{datetime.now()}] 数据: {self.lines[idx]} 出错,错误信息: {ex}", file=sys.stderr) + rnd_idx = np.random.randint(self.__len__()) + return self.__getitem__(rnd_idx) def __len__(self): return len(self.lines) @@ -127,40 +134,59 @@ class CustomDataset(Dataset): 下面是在训练时或者测试时读取音频数据,训练时对转换的梅尔频谱数据随机裁剪,如果是测试,就取前面的,最好要执行归一化。 ```python -def load_audio(audio_path, mode='train', spec_len=128): +def load_audio(audio_path, mode='train', sr=16000, chunk_duration=3): # 读取音频数据 - wav, sr = librosa.load(audio_path, sr=16000) - spec_mag = librosa.feature.melspectrogram(y=wav, sr=sr, hop_length=256) + wav, sr_ret = librosa.load(audio_path, sr=sr) if mode == 'train': - crop_start = random.randint(0, spec_mag.shape[1] - spec_len) - spec_mag = spec_mag[:, crop_start:crop_start + spec_len] - else: - spec_mag = spec_mag[:, :spec_len] - mean = np.mean(spec_mag, 0, keepdims=True) - std = np.std(spec_mag, 0, keepdims=True) - spec_mag = (spec_mag - mean) / (std + 1e-5) - spec_mag = spec_mag[np.newaxis, :] - return spec_mag + # 随机裁剪 + num_wav_samples = wav.shape[0] + # 数据太短不利于训练 + if num_wav_samples < sr: + raise Exception(f'音频长度不能小于1s,实际长度为:{(num_wav_samples / sr):.2f}s') + num_chunk_samples = int(chunk_duration * sr) + if num_wav_samples > num_chunk_samples + 1: + start = random.randint(0, num_wav_samples - num_chunk_samples - 1) + stop = start + num_chunk_samples + wav = wav[start:stop] + # 对每次都满长度的再次裁剪 + if random.random() > 0.5: + wav[:random.randint(1, sr // 2)] = 0 + wav = wav[:-random.randint(1, sr // 2)] + elif mode == 'eval': + # 为避免显存溢出,只裁剪指定长度 + num_wav_samples = wav.shape[0] + num_chunk_samples = int(chunk_duration * sr) + if num_wav_samples > num_chunk_samples + 1: + wav = wav[:num_chunk_samples] + features = librosa.feature.melspectrogram(y=wav, sr=sr, n_fft=400, n_mels=80, hop_length=160, win_length=400) + features = librosa.power_to_db(features, ref=1.0, amin=1e-10, top_db=None) + # 归一化 + mean = np.mean(features, 0, keepdims=True) + std = np.std(features, 0, keepdims=True) + features = (features - mean) / (std + 1e-5) + features = features.astype('float32') + return features ``` ## 训练 -接着就可以开始训练模型了,创建 `train.py`。我们搭建简单的卷积神经网络,如果音频种类非常多,可以适当使用更大的卷积神经网络模型。通过把音频数据转换成梅尔频谱,数据的shape也相当于灰度图,所以为 `(1, 128, 128)`。然后定义优化方法和获取训练和测试数据。要注意 `CLASS_DIM`参数的值,这个是类别的数量,要根据你数据集中的分类数量来修改。 +接着就可以开始训练模型了,创建 `train.py`。我们搭建简单的卷积神经网络,如果音频种类非常多,可以适当使用更大的卷积神经网络模型。通过把音频数据转换成梅尔频谱。然后定义优化方法和获取训练和测试数据。要注意 `args.num_classes`参数的值,这个是类别的数量,要根据你数据集中的分类数量来修改。 ```python def train(args): - # 数据输入的形状 - input_shape = eval(args.input_shape) # 获取数据 - train_dataset = CustomDataset(args.train_list_path, model='train', spec_len=input_shape[3]) - train_loader = DataLoader(dataset=train_dataset, batch_size=args.batch_size, shuffle=True, num_workers=args.num_workers) - - test_dataset = CustomDataset(args.test_list_path, model='test', spec_len=input_shape[3]) - test_loader = DataLoader(dataset=test_dataset, batch_size=args.batch_size, num_workers=args.num_workers) - + train_dataset = CustomDataset(args.train_list_path, model='train') + train_loader = DataLoader(dataset=train_dataset, batch_size=args.batch_size, shuffle=True, collate_fn=collate_fn, num_workers=args.num_workers) + + test_dataset = CustomDataset(args.test_list_path, model='eval') + test_loader = DataLoader(dataset=test_dataset, batch_size=args.batch_size, collate_fn=collate_fn, num_workers=args.num_workers) + # 获取分类标签 + with open(args.label_list_path, 'r', encoding='utf-8') as f: + lines = f.readlines() + class_labels = [l.replace('\n', '') for l in lines] # 获取模型 device = torch.device("cuda") - model = resnet34(num_classes=args.num_classes) + model = EcapaTdnn(num_classes=args.num_classes) model.to(device) # 获取优化方法 @@ -168,13 +194,22 @@ def train(args): lr=args.learning_rate, weight_decay=5e-4) # 获取学习率衰减函数 - scheduler = StepLR(optimizer, step_size=args.learning_rate, gamma=0.8, verbose=True) + scheduler = CosineAnnealingLR(optimizer, T_max=args.num_epoch) + + # 恢复训练 + if args.resume is not None: + model.load_state_dict(torch.load(os.path.join(args.resume, 'model.pth'))) + state = torch.load(os.path.join(args.resume, 'model.state')) + last_epoch = state['last_epoch'] + optimizer_state = torch.load(os.path.join(args.resume, 'optimizer.pth')) + optimizer.load_state_dict(optimizer_state) + print(f'成功加载第 {last_epoch} 轮的模型参数和优化方法参数') # 获取损失函数 loss = torch.nn.CrossEntropyLoss() ``` -最后执行训练,每100个batch打印一次训练日志,训练一轮之后执行测试和保存模型,在测试时,把每个batch的输出都统计,最后求平均值。保存的模型为预测模型,方便之后的预测使用。 +最后执行训练,每100个batch打印一次训练日志,训练一轮之后执行测试和保存模型,在测试时,把每个batch的输出都统计,最后求平均值。 ```python for epoch in range(args.num_epoch): @@ -191,7 +226,7 @@ def train(args): optimizer.step() # 计算准确率 - output = torch.nn.functional.softmax(output) + output = torch.nn.functional.softmax(output, dim=-1) output = output.data.cpu().numpy() output = np.argmax(output, axis=1) label = label.data.cpu().numpy() @@ -199,62 +234,56 @@ def train(args): accuracies.append(acc) loss_sum.append(los) if batch_id % 100 == 0: - print('[%s] Train epoch %d, batch: %d/%d, loss: %f, accuracy: %f' % ( - datetime.now(), epoch, batch_id, len(train_loader), sum(loss_sum) / len(loss_sum), sum(accuracies) / len(accuracies))) + print(f'[{datetime.now()}] Train epoch [{epoch}/{args.num_epoch}], batch: {batch_id}/{len(train_loader)}, ' + f'lr: {scheduler.get_last_lr()[0]:.8f}, loss: {sum(loss_sum) / len(loss_sum):.8f}, ' + f'accuracy: {sum(accuracies) / len(accuracies):.8f}') scheduler.step() - # 评估模型 - acc = test(model, test_loader, device) - print('='*70) - print('[%s] Test %d, accuracy: %f' % (datetime.now(), epoch, acc)) - print('='*70) - model_path = os.path.join(args.save_model, 'resnet34.pth') - if not os.path.exists(os.path.dirname(model_path)): - os.makedirs(os.path.dirname(model_path)) - torch.jit.save(torch.jit.script(model), model_path) ``` +每轮训练结束之后都会执行一次评估,和保存模型。评估会出来输出准确率,还保存了混合矩阵图片,如下。 +![混合矩阵](./images/image1.png) + # 预测 -在训练结束之后,我们得到了一个预测模型,有了预测模型,执行预测非常方便。我们使用这个模型预测音频,在执行预测之前,需要把音频转换为梅尔频谱数据,并把数据shape转换为(1, 1, 128, 128),第一个为输入数据的batch大小,如果想多个音频一起数据,可以把他们存放在list中一起预测。最后输出的结果即为预测概率最大的标签。 +在训练结束之后,我们得到了一个模型参数文件,我们使用这个模型预测音频,在执行预测之前,需要把音频转换为梅尔频谱数据,最后输出的结果即为预测概率最大的标签。 ```python -model_path = 'models/resnet34.pth' +parser = argparse.ArgumentParser(description=__doc__) +add_arg = functools.partial(add_arguments, argparser=parser) +add_arg('audio_path', str, 'dataset/UrbanSound8K/audio/fold5/156634-5-2-5.wav', '图片路径') +add_arg('num_classes', int, 10, '分类的类别数量') +add_arg('label_list_path', str, 'dataset/label_list.txt', '标签列表路径') +add_arg('model_path', str, 'models/model.pth', '模型保存的路径') +args = parser.parse_args() + + +# 获取分类标签 +with open(args.label_list_path, 'r', encoding='utf-8') as f: + lines = f.readlines() +class_labels = [l.replace('\n', '') for l in lines] +# 获取模型 device = torch.device("cuda") -model = torch.jit.load(model_path) +model = EcapaTdnn(num_classes=args.num_classes) model.to(device) +model.load_state_dict(torch.load(args.model_path)) model.eval() -# 读取音频数据 -def load_data(data_path, spec_len=128): - # 读取音频 - wav, sr = librosa.load(data_path, sr=16000) - spec_mag = librosa.feature.melspectrogram(y=wav, sr=sr, hop_length=256).astype(np.float32) - mean = np.mean(spec_mag, 0, keepdims=True) - std = np.std(spec_mag, 0, keepdims=True) - spec_mag = (spec_mag - mean) / (std + 1e-5) - spec_mag = spec_mag[np.newaxis, np.newaxis, :] - return spec_mag - - -def infer(audio_path): - data = load_data(audio_path) +def infer(): + data = load_audio(args.audio_path, mode='infer') + data = data[np.newaxis, :] data = torch.tensor(data, dtype=torch.float32, device=device) # 执行预测 output = model(data) - result = torch.nn.functional.softmax(output) + result = torch.nn.functional.softmax(output, dim=-1) result = result.data.cpu().numpy() - print(result) # 显示图片并输出结果最大的label lab = np.argsort(result)[0][-1] - return lab + print(f'音频:{args.audio_path} 的预测结果标签为:{class_labels[lab]}') if __name__ == '__main__': - # 要预测的音频文件 - path = 'dataset/UrbanSound8K/audio/fold5/156634-5-2-5.wav' - label = infer(path) - print('音频:%s 的预测结果标签为:%d' % (path, label)) + infer() ``` # 其他 @@ -359,16 +388,9 @@ if __name__ == '__main__': crop_wav('save_audio', crop_len) ``` -创建 `infer_record.py`,这个程序是用来不断进行录音识别,录音时间之所以设置为6秒,就是要保证裁剪后的音频长度大于等于2.97秒。因为识别的时间比较短,所以我们可以大致理解为这个程序在实时录音识别。通过这个应该我们可以做一些比较有趣的事情,比如把麦克风放在小鸟经常来的地方,通过实时录音识别,一旦识别到有鸟叫的声音,如果你的数据集足够强大,有每种鸟叫的声音数据集,这样你还能准确识别是那种鸟叫。如果识别到目标鸟类,就启动程序,例如拍照等等。 +创建 `infer_record.py`,这个程序是用来不断进行录音识别,录音时间之所以设置为6秒,所以我们可以大致理解为这个程序在实时录音识别。通过这个应该我们可以做一些比较有趣的事情,比如把麦克风放在小鸟经常来的地方,通过实时录音识别,一旦识别到有鸟叫的声音,如果你的数据集足够强大,有每种鸟叫的声音数据集,这样你还能准确识别是那种鸟叫。如果识别到目标鸟类,就启动程序,例如拍照等等。 ```python -# 加载模型 -model_path = 'models/resnet34.pth' -device = torch.device("cuda") -model = torch.jit.load(model_path) -model.to(device) -model.eval() - # 录音参数 CHUNK = 1024 FORMAT = pyaudio.paInt16 @@ -385,19 +407,6 @@ stream = p.open(format=FORMAT, input=True, frames_per_buffer=CHUNK) - -# 读取音频数据 -def load_data(data_path, spec_len=128): - # 读取音频 - wav, sr = librosa.load(data_path, sr=16000) - spec_mag = librosa.feature.melspectrogram(y=wav, sr=sr, hop_length=256).astype(np.float32) - mean = np.mean(spec_mag, 0, keepdims=True) - std = np.std(spec_mag, 0, keepdims=True) - spec_mag = (spec_mag - mean) / (std + 1e-5) - spec_mag = spec_mag[np.newaxis, np.newaxis, :] - return spec_mag - - # 获取录音数据 def record_audio(): print("开始录音......") @@ -420,29 +429,26 @@ def record_audio(): # 预测 def infer(audio_path): - data = load_data(audio_path) + data = load_audio(audio_path, mode='infer') + data = data[np.newaxis, :] data = torch.tensor(data, dtype=torch.float32, device=device) # 执行预测 output = model(data) - result = torch.nn.functional.softmax(output) + result = torch.nn.functional.softmax(output, dim=-1) result = result.data.cpu().numpy() - print(result) # 显示图片并输出结果最大的label lab = np.argsort(result)[0][-1] - return lab + return class_labels[lab] if __name__ == '__main__': try: while True: - try: - # 加载数据 - audio_path = record_audio() - # 获取预测结果 - label = infer(audio_path) - print('预测的标签为:%d' % label) - except: - pass + # 加载数据 + audio_path = record_audio() + # 获取预测结果 + label = infer(audio_path) + print(f'预测的标签为:{label}') except Exception as e: print(e) stream.stop_stream() diff --git a/create_data.py b/create_data.py index 0a4a738..6983af0 100644 --- a/create_data.py +++ b/create_data.py @@ -10,8 +10,10 @@ def get_data_list(audio_path, list_path): f_train = open(os.path.join(list_path, 'train_list.txt'), 'w') f_test = open(os.path.join(list_path, 'test_list.txt'), 'w') + f_label = open(os.path.join(list_path, 'label_list.txt'), 'w') for i in range(len(audios)): + f_label.write(f'{audios[i]}\n') sounds = os.listdir(os.path.join(audio_path, audios[i])) for sound in sounds: if '.wav' not in sound:continue @@ -24,7 +26,7 @@ def get_data_list(audio_path, list_path): f_train.write('%s\t%d\n' % (sound_path, i)) sound_sum += 1 print("Audio:%d/%d" % (i + 1, len(audios))) - + f_label.close() f_test.close() f_train.close() diff --git a/eval.py b/eval.py new file mode 100644 index 0000000..1be5f56 --- /dev/null +++ b/eval.py @@ -0,0 +1,66 @@ +import argparse +import functools + +import numpy as np +import torch +from sklearn.metrics import confusion_matrix +from torch.utils.data import DataLoader + +from utils.ecapa_tdnn import EcapaTdnn +from utils.reader import CustomDataset, collate_fn +from utils.utility import add_arguments, print_arguments, plot_confusion_matrix + +parser = argparse.ArgumentParser(description=__doc__) +add_arg = functools.partial(add_arguments, argparser=parser) +add_arg('batch_size', int, 32, '训练的批量大小') +add_arg('num_workers', int, 4, '读取数据的线程数量') +add_arg('num_classes', int, 10, '分类的类别数量') +add_arg('learning_rate', float, 1e-3, '初始学习率的大小') +add_arg('test_list_path', str, 'dataset/test_list.txt', '测试数据的数据列表路径') +add_arg('label_list_path', str, 'dataset/label_list.txt', '标签列表路径') +add_arg('model_path', str, 'models/model.pth', '模型保存的路径') +args = parser.parse_args() + + +def evaluate(): + test_dataset = CustomDataset(args.test_list_path, model='eval') + test_loader = DataLoader(dataset=test_dataset, batch_size=args.batch_size, collate_fn=collate_fn, num_workers=args.num_workers) + # 获取分类标签 + with open(args.label_list_path, 'r', encoding='utf-8') as f: + lines = f.readlines() + class_labels = [l.replace('\n', '') for l in lines] + # 获取模型 + device = torch.device("cuda") + model = EcapaTdnn(num_classes=args.num_classes) + model.to(device) + model.load_state_dict(torch.load(args.model_path)) + model.eval() + + accuracies, preds, labels = [], [], [] + for batch_id, (spec_mag, label) in enumerate(test_loader): + spec_mag = spec_mag.to(device) + label = label.numpy() + output = model(spec_mag) + output = output.data.cpu().numpy() + pred = np.argmax(output, axis=1) + preds.extend(pred.tolist()) + labels.extend(label.tolist()) + acc = np.mean((pred == label).astype(int)) + accuracies.append(acc.item()) + acc = float(sum(accuracies) / len(accuracies)) + cm = confusion_matrix(labels, preds) + FP = cm.sum(axis=0) - np.diag(cm) + FN = cm.sum(axis=1) - np.diag(cm) + TP = np.diag(cm) + TN = cm.sum() - (FP + FN + TP) + # 精确率 + precision = TP / (TP + FP + 1e-6) + # 召回率 + recall = TP / (TP + FN + 1e-6) + print('分类准确率: {:.4f}, 平均精确率: {:.4f}, 平均召回率: {:.4f}'.format(acc, np.mean(precision), np.mean(recall))) + plot_confusion_matrix(cm=cm, save_path='log/混淆矩阵_eval.png', class_labels=class_labels) + + +if __name__ == '__main__': + print_arguments(args) + evaluate() diff --git a/images/image1.png b/images/image1.png new file mode 100644 index 0000000000000000000000000000000000000000..0f18b23f940620bdd9ee0b1c7ffd2205ecb4cffc GIT binary patch literal 64003 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 JY2WYX|387&tMC8- literal 0 HcmV?d00001 diff --git a/infer.py b/infer.py index cbdfdf8..ffd3875 100644 --- a/infer.py +++ b/infer.py @@ -1,43 +1,47 @@ +import argparse +import functools + import librosa import numpy as np import torch -# 加载模型 -model_path = 'models/resnet34.pth' +from utils.ecapa_tdnn import EcapaTdnn +from utils.reader import load_audio +from utils.utility import add_arguments + +parser = argparse.ArgumentParser(description=__doc__) +add_arg = functools.partial(add_arguments, argparser=parser) +add_arg('audio_path', str, 'dataset/UrbanSound8K/audio/fold5/156634-5-2-5.wav', '图片路径') +add_arg('num_classes', int, 10, '分类的类别数量') +add_arg('label_list_path', str, 'dataset/label_list.txt', '标签列表路径') +add_arg('model_path', str, 'models/model.pth', '模型保存的路径') +args = parser.parse_args() + + +# 获取分类标签 +with open(args.label_list_path, 'r', encoding='utf-8') as f: + lines = f.readlines() +class_labels = [l.replace('\n', '') for l in lines] +# 获取模型 device = torch.device("cuda") -model = torch.jit.load(model_path) +model = EcapaTdnn(num_classes=args.num_classes) model.to(device) +model.load_state_dict(torch.load(args.model_path)) model.eval() -# 读取音频数据 -def load_data(data_path): - # 读取音频 - wav, sr = librosa.load(data_path, sr=16000) - spec_mag = librosa.feature.melspectrogram(y=wav, sr=sr, hop_length=256).astype(np.float32) - mean = np.mean(spec_mag, 0, keepdims=True) - std = np.std(spec_mag, 0, keepdims=True) - spec_mag = (spec_mag - mean) / (std + 1e-5) - spec_mag = spec_mag[np.newaxis, np.newaxis, :] - spec_mag = spec_mag.astype('float32') - return spec_mag - - -def infer(audio_path): - data = load_data(audio_path) +def infer(): + data = load_audio(args.audio_path, mode='infer') + data = data[np.newaxis, :] data = torch.tensor(data, dtype=torch.float32, device=device) # 执行预测 output = model(data) - result = torch.nn.functional.softmax(output) + result = torch.nn.functional.softmax(output, dim=-1) result = result.data.cpu().numpy() - print(result) # 显示图片并输出结果最大的label lab = np.argsort(result)[0][-1] - return lab + print(f'音频:{args.audio_path} 的预测结果标签为:{class_labels[lab]}') if __name__ == '__main__': - # 要预测的音频文件 - path = 'dataset/UrbanSound8K/audio/fold5/156634-5-2-5.wav' - label = infer(path) - print('音频:%s 的预测结果标签为:%d' % (path, label)) + infer() diff --git a/infer_record.py b/infer_record.py index 6da50a5..a311e15 100644 --- a/infer_record.py +++ b/infer_record.py @@ -1,14 +1,32 @@ +import argparse +import functools import wave -import librosa + import numpy as np import pyaudio import torch -# 加载模型 -model_path = 'models/resnet34.pth' +from utils.ecapa_tdnn import EcapaTdnn +from utils.reader import load_audio +from utils.utility import add_arguments + +parser = argparse.ArgumentParser(description=__doc__) +add_arg = functools.partial(add_arguments, argparser=parser) +add_arg('num_classes', int, 10, '分类的类别数量') +add_arg('label_list_path', str, 'dataset/label_list.txt', '标签列表路径') +add_arg('model_path', str, 'models/model.pth', '模型保存的路径') +args = parser.parse_args() + + +# 获取分类标签 +with open(args.label_list_path, 'r', encoding='utf-8') as f: + lines = f.readlines() +class_labels = [l.replace('\n', '') for l in lines] +# 获取模型 device = torch.device("cuda") -model = torch.jit.load(model_path) +model = EcapaTdnn(num_classes=args.num_classes) model.to(device) +model.load_state_dict(torch.load(args.model_path)) model.eval() # 录音参数 @@ -28,19 +46,6 @@ frames_per_buffer=CHUNK) -# 读取音频数据 -def load_data(data_path): - # 读取音频 - wav, sr = librosa.load(data_path, sr=16000) - spec_mag = librosa.feature.melspectrogram(y=wav, sr=sr, hop_length=256).astype(np.float32) - mean = np.mean(spec_mag, 0, keepdims=True) - std = np.std(spec_mag, 0, keepdims=True) - spec_mag = (spec_mag - mean) / (std + 1e-5) - spec_mag = spec_mag[np.newaxis, np.newaxis, :] - spec_mag = spec_mag.astype('float32') - return spec_mag - - # 获取录音数据 def record_audio(): print("开始录音......") @@ -63,29 +68,26 @@ def record_audio(): # 预测 def infer(audio_path): - data = load_data(audio_path) + data = load_audio(audio_path, mode='infer') + data = data[np.newaxis, :] data = torch.tensor(data, dtype=torch.float32, device=device) # 执行预测 output = model(data) - result = torch.nn.functional.softmax(output) + result = torch.nn.functional.softmax(output, dim=-1) result = result.data.cpu().numpy() - print(result) # 显示图片并输出结果最大的label lab = np.argsort(result)[0][-1] - return lab + return class_labels[lab] if __name__ == '__main__': try: while True: - try: - # 加载数据 - audio_path = record_audio() - # 获取预测结果 - label = infer(audio_path) - print('预测的标签为:%d' % label) - except: - pass + # 加载数据 + audio_path = record_audio() + # 获取预测结果 + label = infer(audio_path) + print(f'预测的标签为:{label}') except Exception as e: print(e) stream.stop_stream() diff --git a/reader.py b/reader.py deleted file mode 100644 index 6b6d051..0000000 --- a/reader.py +++ /dev/null @@ -1,41 +0,0 @@ -import random - -import librosa -import numpy as np -from torch.utils.data import Dataset - - -# 加载并预处理音频 -def load_audio(audio_path, mode='train', spec_len=128): - # 读取音频数据 - wav, sr = librosa.load(audio_path, sr=16000) - spec_mag = librosa.feature.melspectrogram(y=wav, sr=sr, hop_length=256) - if mode == 'train': - crop_start = random.randint(0, spec_mag.shape[1] - spec_len) - spec_mag = spec_mag[:, crop_start:crop_start + spec_len] - else: - spec_mag = spec_mag[:, :spec_len] - mean = np.mean(spec_mag, 0, keepdims=True) - std = np.std(spec_mag, 0, keepdims=True) - spec_mag = (spec_mag - mean) / (std + 1e-5) - spec_mag = spec_mag[np.newaxis, :] - spec_mag = spec_mag.astype('float32') - return spec_mag - - -# 数据加载器 -class CustomDataset(Dataset): - def __init__(self, data_list_path, model='train', spec_len=128): - super(CustomDataset, self).__init__() - with open(data_list_path, 'r') as f: - self.lines = f.readlines() - self.model = model - self.spec_len = spec_len - - def __getitem__(self, idx): - audio_path, label = self.lines[idx].replace('\n', '').split('\t') - spec_mag = load_audio(audio_path, mode=self.model, spec_len=self.spec_len) - return spec_mag, np.array(int(label), dtype=np.int64) - - def __len__(self): - return len(self.lines) diff --git a/resnet.py b/resnet.py deleted file mode 100644 index 815d18d..0000000 --- a/resnet.py +++ /dev/null @@ -1,160 +0,0 @@ -import torch -import torch.nn as nn - - -class BasicBlock(nn.Module): - expansion = 1 - - def __init__(self, - inplanes, - planes, - stride=1, - downsample=None, - groups=1, - base_width=64, - dilation=1, - norm_layer=None): - super(BasicBlock, self).__init__() - if norm_layer is None: - norm_layer = nn.BatchNorm2d - - if dilation > 1: - raise NotImplementedError( - "Dilation > 1 not supported in BasicBlock") - - self.conv1 = nn.Conv2d( - inplanes, planes, 3, padding=1, stride=stride) - self.bn1 = norm_layer(planes) - self.relu = nn.ReLU() - self.conv2 = nn.Conv2d(planes, planes, 3, padding=1) - self.bn2 = norm_layer(planes) - self.downsample = downsample - self.stride = stride - - def forward(self, x): - identity = x - - out = self.conv1(x) - out = self.bn1(out) - out = self.relu(out) - - out = self.conv2(out) - out = self.bn2(out) - - if self.downsample is not None: - identity = self.downsample(x) - - out += identity - out = self.relu(out) - - return out - - -class ResNet(nn.Module): - """ResNet model from - `"Deep Residual Learning for Image Recognition" `_ - - Args: - Block (BasicBlock|BottleneckBlock): block module of model. - depth (int): layers of resnet, default: 50. - num_classes (int): output dim of last fc layer. If num_classes <=0, last fc layer - will not be defined. Default: 1000. - with_pool (bool): use pool before the last fc layer or not. Default: True. - - Examples: - .. code-block:: python - - from paddle.vision.models import ResNet - from paddle.vision.models.resnet import BottleneckBlock, BasicBlock - - resnet50 = ResNet(BottleneckBlock, 50) - - resnet18 = ResNet(BasicBlock, 18) - - """ - - def __init__(self, block, depth, num_classes=1000, with_pool=True): - super(ResNet, self).__init__() - layer_cfg = { - 18: [2, 2, 2, 2], - 34: [3, 4, 6, 3], - 50: [3, 4, 6, 3], - 101: [3, 4, 23, 3], - 152: [3, 8, 36, 3] - } - layers = layer_cfg[depth] - self.num_classes = num_classes - self.with_pool = with_pool - self._norm_layer = nn.BatchNorm2d - - self.inplanes = 64 - self.dilation = 1 - - self.conv1 = nn.Conv2d( - 1, - self.inplanes, - kernel_size=7, - stride=2, - padding=3) - self.bn1 = self._norm_layer(self.inplanes) - self.relu = nn.ReLU() - self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) - self.layer1 = self._make_layer(block, 64, layers[0]) - self.layer2 = self._make_layer(block, 128, layers[1], stride=2) - self.layer3 = self._make_layer(block, 256, layers[2], stride=2) - self.layer4 = self._make_layer(block, 512, layers[3], stride=2) - if with_pool: - self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) - - if num_classes > 0: - self.fc = nn.Linear(512 * block.expansion, num_classes) - - def _make_layer(self, block, planes, blocks, stride=1, dilate=False): - norm_layer = self._norm_layer - downsample = None - previous_dilation = self.dilation - if dilate: - self.dilation *= stride - stride = 1 - if stride != 1 or self.inplanes != planes * block.expansion: - downsample = nn.Sequential( - nn.Conv2d( - self.inplanes, - planes * block.expansion, - 1, - stride=stride), - norm_layer(planes * block.expansion), ) - - layers = [] - layers.append( - block(self.inplanes, planes, stride, downsample, 1, 64, - previous_dilation, norm_layer)) - self.inplanes = planes * block.expansion - for _ in range(1, blocks): - layers.append(block(self.inplanes, planes, norm_layer=norm_layer)) - - return nn.Sequential(*layers) - - def forward(self, x): - x = self.conv1(x) - x = self.bn1(x) - x = self.relu(x) - x = self.maxpool(x) - x = self.layer1(x) - x = self.layer2(x) - x = self.layer3(x) - x = self.layer4(x) - - if self.with_pool: - x = self.avgpool(x) - - if self.num_classes > 0: - x = torch.flatten(x, 1) - x = self.fc(x) - - return x - - -def resnet34(**kwargs): - model = ResNet(BasicBlock, 34, **kwargs) - return model diff --git a/train.py b/train.py index d6ef7c5..3481127 100644 --- a/train.py +++ b/train.py @@ -5,56 +5,64 @@ import numpy as np import torch +from sklearn.metrics import confusion_matrix +from torch.optim.lr_scheduler import CosineAnnealingLR from torch.utils.data import DataLoader -from resnet import resnet34 -from torch.optim.lr_scheduler import StepLR -from reader import CustomDataset -from utility import add_arguments, print_arguments + +from utils.ecapa_tdnn import EcapaTdnn +from utils.reader import CustomDataset, collate_fn +from utils.utility import add_arguments, print_arguments, plot_confusion_matrix parser = argparse.ArgumentParser(description=__doc__) add_arg = functools.partial(add_arguments, argparser=parser) add_arg('batch_size', int, 32, '训练的批量大小') add_arg('num_workers', int, 4, '读取数据的线程数量') -add_arg('num_epoch', int, 50, '训练的轮数') +add_arg('num_epoch', int, 30, '训练的轮数') add_arg('num_classes', int, 10, '分类的类别数量') add_arg('learning_rate', float, 1e-3, '初始学习率的大小') -add_arg('input_shape', str, '(None, 1, 128, 128)', '数据输入的形状') add_arg('train_list_path', str, 'dataset/train_list.txt', '训练数据的数据列表路径') add_arg('test_list_path', str, 'dataset/test_list.txt', '测试数据的数据列表路径') +add_arg('label_list_path', str, 'dataset/label_list.txt', '标签列表路径') add_arg('save_model', str, 'models/', '模型保存的路径') +add_arg('resume', str, 'models/', '恢复训练的模型文件夹,当为None则不使用恢复模型') args = parser.parse_args() # 评估模型 -def test(model, test_loader, device): +@torch.no_grad() +def evaluate(model, test_loader, device): model.eval() - accuracies = [] + accuracies, preds, labels = [], [], [] for batch_id, (spec_mag, label) in enumerate(test_loader): spec_mag = spec_mag.to(device) - label = label.to(device).long() + label = label.numpy() output = model(spec_mag) output = output.data.cpu().numpy() - output = np.argmax(output, axis=1) - label = label.data.cpu().numpy() - acc = np.mean((output == label).astype(int)) + pred = np.argmax(output, axis=1) + preds.extend(pred.tolist()) + labels.extend(label.tolist()) + acc = np.mean((pred == label).astype(int)) accuracies.append(acc.item()) model.train() - return float(sum(accuracies) / len(accuracies)) + acc = float(sum(accuracies) / len(accuracies)) + cm = confusion_matrix(labels, preds) + return acc, cm def train(args): - # 数据输入的形状 - input_shape = eval(args.input_shape) # 获取数据 - train_dataset = CustomDataset(args.train_list_path, model='train', spec_len=input_shape[3]) - train_loader = DataLoader(dataset=train_dataset, batch_size=args.batch_size, shuffle=True, num_workers=args.num_workers) - - test_dataset = CustomDataset(args.test_list_path, model='test', spec_len=input_shape[3]) - test_loader = DataLoader(dataset=test_dataset, batch_size=args.batch_size, num_workers=args.num_workers) + train_dataset = CustomDataset(args.train_list_path, model='train') + train_loader = DataLoader(dataset=train_dataset, batch_size=args.batch_size, shuffle=True, collate_fn=collate_fn, num_workers=args.num_workers) + test_dataset = CustomDataset(args.test_list_path, model='eval') + test_loader = DataLoader(dataset=test_dataset, batch_size=args.batch_size, collate_fn=collate_fn, num_workers=args.num_workers) + # 获取分类标签 + with open(args.label_list_path, 'r', encoding='utf-8') as f: + lines = f.readlines() + class_labels = [l.replace('\n', '') for l in lines] # 获取模型 device = torch.device("cuda") - model = resnet34(num_classes=args.num_classes) + model = EcapaTdnn(num_classes=args.num_classes) model.to(device) # 获取优化方法 @@ -62,7 +70,16 @@ def train(args): lr=args.learning_rate, weight_decay=5e-4) # 获取学习率衰减函数 - scheduler = StepLR(optimizer, step_size=args.learning_rate, gamma=0.8, verbose=True) + scheduler = CosineAnnealingLR(optimizer, T_max=args.num_epoch) + + # 恢复训练 + if args.resume is not None: + model.load_state_dict(torch.load(os.path.join(args.resume, 'model.pth'))) + state = torch.load(os.path.join(args.resume, 'model.state')) + last_epoch = state['last_epoch'] + optimizer_state = torch.load(os.path.join(args.resume, 'optimizer.pth')) + optimizer.load_state_dict(optimizer_state) + print(f'成功加载第 {last_epoch} 轮的模型参数和优化方法参数') # 获取损失函数 loss = torch.nn.CrossEntropyLoss() @@ -82,7 +99,7 @@ def train(args): optimizer.step() # 计算准确率 - output = torch.nn.functional.softmax(output) + output = torch.nn.functional.softmax(output, dim=-1) output = output.data.cpu().numpy() output = np.argmax(output, axis=1) label = label.data.cpu().numpy() @@ -90,18 +107,21 @@ def train(args): accuracies.append(acc) loss_sum.append(los) if batch_id % 100 == 0: - print('[%s] Train epoch %d, batch: %d/%d, loss: %f, accuracy: %f' % ( - datetime.now(), epoch, batch_id, len(train_loader), sum(loss_sum) / len(loss_sum), sum(accuracies) / len(accuracies))) + print(f'[{datetime.now()}] Train epoch [{epoch}/{args.num_epoch}], batch: {batch_id}/{len(train_loader)}, ' + f'lr: {scheduler.get_last_lr()[0]:.8f}, loss: {sum(loss_sum) / len(loss_sum):.8f}, ' + f'accuracy: {sum(accuracies) / len(accuracies):.8f}') scheduler.step() # 评估模型 - acc = test(model, test_loader, device) + acc, cm = evaluate(model, test_loader, device) + plot_confusion_matrix(cm=cm, save_path=f'log/混淆矩阵_{epoch}.png', class_labels=class_labels, show=False) print('='*70) - print('[%s] Test %d, accuracy: %f' % (datetime.now(), epoch, acc)) + print(f'[{datetime.now()}] Test {epoch}, accuracy: {acc}') print('='*70) - model_path = os.path.join(args.save_model, 'resnet34.pth') - if not os.path.exists(os.path.dirname(model_path)): - os.makedirs(os.path.dirname(model_path)) - torch.jit.save(torch.jit.script(model), model_path) + # 保存模型 + os.makedirs(args.save_model, exist_ok=True) + torch.save(model.state_dict(), os.path.join(args.save_model, 'model.pth')) + torch.save({'last_epoch': torch.tensor(epoch)}, os.path.join(args.save_model, 'model.state')) + torch.save(optimizer.state_dict(), os.path.join(args.save_model, 'optimizer.pth')) if __name__ == '__main__': diff --git a/utility.py b/utility.py deleted file mode 100644 index 7d68953..0000000 --- a/utility.py +++ /dev/null @@ -1,17 +0,0 @@ -import distutils.util - - -def print_arguments(args): - print("----------- Configuration Arguments -----------") - for arg, value in sorted(vars(args).items()): - print("%s: %s" % (arg, value)) - print("------------------------------------------------") - - -def add_arguments(argname, type, default, help, argparser, **kwargs): - type = distutils.util.strtobool if type == bool else type - argparser.add_argument("--" + argname, - default=default, - type=type, - help=help + ' 默认: %(default)s.', - **kwargs) diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/ecapa_tdnn.py b/utils/ecapa_tdnn.py new file mode 100644 index 0000000..aced40e --- /dev/null +++ b/utils/ecapa_tdnn.py @@ -0,0 +1,119 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +from torch.nn import Parameter + + +class Res2Conv1dReluBn(nn.Module): + def __init__(self, channels, kernel_size=1, stride=1, padding=0, dilation=1, bias=False, scale=4): + super().__init__() + assert channels % scale == 0, "{} % {} != 0".format(channels, scale) + self.scale = scale + self.width = channels // scale + self.nums = scale if scale == 1 else scale - 1 + + self.convs = [] + self.bns = [] + for i in range(self.nums): + self.convs.append(nn.Conv1d(self.width, self.width, kernel_size, stride, padding, dilation, bias=bias)) + self.bns.append(nn.BatchNorm1d(self.width)) + self.convs = nn.ModuleList(self.convs) + self.bns = nn.ModuleList(self.bns) + + def forward(self, x): + out = [] + spx = torch.split(x, self.width, 1) + for i in range(self.nums): + if i == 0: + sp = spx[i] + else: + sp = sp + spx[i] + # Order: conv -> relu -> bn + sp = self.convs[i](sp) + sp = self.bns[i](F.relu(sp)) + out.append(sp) + if self.scale != 1: + out.append(spx[self.nums]) + out = torch.cat(out, dim=1) + return out + + +class Conv1dReluBn(nn.Module): + def __init__(self, in_channels, out_channels, kernel_size=1, stride=1, padding=0, dilation=1, bias=False): + super().__init__() + self.conv = nn.Conv1d(in_channels, out_channels, kernel_size, stride, padding, dilation, bias=bias) + self.bn = nn.BatchNorm1d(out_channels) + + def forward(self, x): + return self.bn(F.relu(self.conv(x))) + + +class SE_Connect(nn.Module): + def __init__(self, channels, s=2): + super().__init__() + assert channels % s == 0, "{} % {} != 0".format(channels, s) + self.linear1 = nn.Linear(channels, channels // s) + self.linear2 = nn.Linear(channels // s, channels) + + def forward(self, x): + out = x.mean(dim=2) + out = F.relu(self.linear1(out)) + out = torch.sigmoid(self.linear2(out)) + out = x * out.unsqueeze(2) + return out + + +def SE_Res2Block(channels, kernel_size, stride, padding, dilation, scale): + return nn.Sequential( + Conv1dReluBn(channels, channels, kernel_size=1, stride=1, padding=0), + Res2Conv1dReluBn(channels, kernel_size, stride, padding, dilation, scale=scale), + Conv1dReluBn(channels, channels, kernel_size=1, stride=1, padding=0), + SE_Connect(channels) + ) + + +class AttentiveStatsPool(nn.Module): + def __init__(self, in_dim, bottleneck_dim): + super().__init__() + # Use Conv1d with stride == 1 rather than Linear, then we don't need to transpose inputs. + self.linear1 = nn.Conv1d(in_dim, bottleneck_dim, kernel_size=1) # equals W and b in the paper + self.linear2 = nn.Conv1d(bottleneck_dim, in_dim, kernel_size=1) # equals V and k in the paper + + def forward(self, x): + # DON'T use ReLU here! In experiments, I find ReLU hard to converge. + alpha = torch.tanh(self.linear1(x)) + alpha = torch.softmax(self.linear2(alpha), dim=2) + mean = torch.sum(alpha * x, dim=2) + residuals = torch.sum(alpha * x ** 2, dim=2) - mean ** 2 + std = torch.sqrt(residuals.clamp(min=1e-9)) + return torch.cat([mean, std], dim=1) + + +class EcapaTdnn(nn.Module): + def __init__(self, num_classes, input_size=80, channels=512, embd_dim=192): + super().__init__() + self.layer1 = Conv1dReluBn(input_size, channels, kernel_size=5, padding=2, dilation=1) + self.layer2 = SE_Res2Block(channels, kernel_size=3, stride=1, padding=2, dilation=2, scale=8) + self.layer3 = SE_Res2Block(channels, kernel_size=3, stride=1, padding=3, dilation=3, scale=8) + self.layer4 = SE_Res2Block(channels, kernel_size=3, stride=1, padding=4, dilation=4, scale=8) + + cat_channels = channels * 3 + out_channels = cat_channels * 2 + self.conv = nn.Conv1d(cat_channels, cat_channels, kernel_size=1) + self.pooling = AttentiveStatsPool(cat_channels, 128) + self.bn1 = nn.BatchNorm1d(out_channels) + self.linear = nn.Linear(out_channels, embd_dim) + self.bn2 = nn.BatchNorm1d(embd_dim) + self.fc = nn.Linear(embd_dim, num_classes) + + def forward(self, x): + out1 = self.layer1(x) + out2 = self.layer2(out1) + out1 + out3 = self.layer3(out1 + out2) + out1 + out2 + out4 = self.layer4(out1 + out2 + out3) + out1 + out2 + out3 + + out = torch.cat([out2, out3, out4], dim=1) + out = F.relu(self.conv(out)) + out = self.bn1(self.pooling(out)) + out = self.bn2(self.linear(out)) + return out diff --git a/utils/reader.py b/utils/reader.py new file mode 100644 index 0000000..3bbd923 --- /dev/null +++ b/utils/reader.py @@ -0,0 +1,89 @@ +import random +import sys +from datetime import datetime + +import librosa +import numpy as np +import torch +from torch.utils.data import Dataset + + +# 加载并预处理音频 +def load_audio(audio_path, mode='train', sr=16000, chunk_duration=3): + # 读取音频数据 + wav, sr_ret = librosa.load(audio_path, sr=sr) + if mode == 'train': + # 随机裁剪 + num_wav_samples = wav.shape[0] + # 数据太短不利于训练 + if num_wav_samples < sr: + raise Exception(f'音频长度不能小于1s,实际长度为:{(num_wav_samples / sr):.2f}s') + num_chunk_samples = int(chunk_duration * sr) + if num_wav_samples > num_chunk_samples + 1: + start = random.randint(0, num_wav_samples - num_chunk_samples - 1) + stop = start + num_chunk_samples + wav = wav[start:stop] + # 对每次都满长度的再次裁剪 + if random.random() > 0.5: + wav[:random.randint(1, sr // 2)] = 0 + wav = wav[:-random.randint(1, sr // 2)] + elif mode == 'eval': + # 为避免显存溢出,只裁剪指定长度 + num_wav_samples = wav.shape[0] + num_chunk_samples = int(chunk_duration * sr) + if num_wav_samples > num_chunk_samples + 1: + wav = wav[:num_chunk_samples] + features = librosa.feature.melspectrogram(y=wav, sr=sr, n_fft=400, n_mels=80, hop_length=160, win_length=400) + features = librosa.power_to_db(features, ref=1.0, amin=1e-10, top_db=None) + # 归一化 + mean = np.mean(features, 0, keepdims=True) + std = np.std(features, 0, keepdims=True) + features = (features - mean) / (std + 1e-5) + features = features.astype('float32') + return features + + +# 数据加载器 +class CustomDataset(Dataset): + def __init__(self, data_list_path, model='train', sr=16000, chunk_duration=3): + super(CustomDataset, self).__init__() + with open(data_list_path, 'r') as f: + self.lines = f.readlines() + self.model = model + self.sr = sr + self.chunk_duration = chunk_duration + + def __getitem__(self, idx): + try: + audio_path, label = self.lines[idx].replace('\n', '').split('\t') + spec_mag = load_audio(audio_path, mode=self.model, sr=self.sr, chunk_duration=self.chunk_duration) + return spec_mag, np.array(int(label), dtype=np.int64) + except Exception as ex: + print(f"[{datetime.now()}] 数据: {self.lines[idx]} 出错,错误信息: {ex}", file=sys.stderr) + rnd_idx = np.random.randint(self.__len__()) + return self.__getitem__(rnd_idx) + + def __len__(self): + return len(self.lines) + + +# 对一个batch的数据处理 +def collate_fn(batch): + # 找出音频长度最长的 + batch = sorted(batch, key=lambda sample: sample[0].shape[1], reverse=True) + freq_size = batch[0][0].shape[0] + max_audio_length = batch[0][0].shape[1] + batch_size = len(batch) + # 以最大的长度创建0张量 + inputs = np.zeros((batch_size, freq_size, max_audio_length), dtype='float32') + labels = [] + for x in range(batch_size): + sample = batch[x] + tensor = sample[0] + labels.append(sample[1]) + seq_length = tensor.shape[1] + # 将数据插入都0张量中,实现了padding + inputs[x, :, :seq_length] = tensor[:, :] + labels = np.array(labels, dtype='int64') + # 打乱数据 + return torch.tensor(inputs), torch.tensor(labels) diff --git a/utils/utility.py b/utils/utility.py new file mode 100644 index 0000000..99f78ac --- /dev/null +++ b/utils/utility.py @@ -0,0 +1,59 @@ +import distutils.util +import os + +import matplotlib.pyplot as plt +import numpy as np + + +def print_arguments(args): + print("----------- Configuration Arguments -----------") + for arg, value in sorted(vars(args).items()): + print("%s: %s" % (arg, value)) + print("------------------------------------------------") + + +def add_arguments(argname, type, default, help, argparser, **kwargs): + type = distutils.util.strtobool if type == bool else type + argparser.add_argument("--" + argname, + default=default, + type=type, + help=help + ' 默认: %(default)s.', + **kwargs) + + + +def plot_confusion_matrix(cm, save_path, class_labels, title='Confusion Matrix', show=True): + plt.figure(figsize=(12, 8), dpi=100) + np.set_printoptions(precision=2) + # 在混淆矩阵中每格的概率值 + ind_array = np.arange(len(class_labels)) + x, y = np.meshgrid(ind_array, ind_array) + for x_val, y_val in zip(x.flatten(), y.flatten()): + c = cm[y_val][x_val] / (np.sum(cm[:, x_val]) + 1e-6) + # 忽略值太小的 + if c < 1e-4:continue + plt.text(x_val, y_val, "%0.2f" % (c,), color='red', fontsize=15, va='center', ha='center') + m = np.max(cm) + plt.imshow(cm / m, interpolation='nearest', cmap=plt.cm.binary) + plt.title(title) + plt.colorbar() + xlocations = np.array(range(len(class_labels))) + plt.xticks(xlocations, class_labels, rotation=90) + plt.yticks(xlocations, class_labels) + plt.ylabel('Actual label') + plt.xlabel('Predict label') + + # offset the tick + tick_marks = np.array(range(len(class_labels))) + 0.5 + plt.gca().set_xticks(tick_marks, minor=True) + plt.gca().set_yticks(tick_marks, minor=True) + plt.gca().xaxis.set_ticks_position('none') + plt.gca().yaxis.set_ticks_position('none') + plt.grid(True, which='minor', linestyle='-') + plt.gcf().subplots_adjust(bottom=0.15) + # 保存图片 + os.makedirs(os.path.dirname(save_path), exist_ok=True) + plt.savefig(save_path, format='png') + if show: + # 显示图片 + plt.show()