基于梅尔频谱的音频信号分类识别(Pytorch)
本項目將使用Pytorch,實現一個簡單的的音頻信號分類器,可應用于機械信號分類識別,鳥叫聲信號識別等應用場景。?
項目使用librosa進行音頻信號處理,backbone使用mobilenet_v2,在Urbansound8K數據上,最終收斂的準確率在訓練集99%,測試集96%,如果想進一步提高識別準確率可以使用更重的backbone和更多的數據增強方法。
完整的項目代碼:https://download.csdn.net/download/guyuealian/30306697
尊重原創,轉載請注明出處:https://panjinquan.blog.csdn.net/article/details/120601437
目錄
1. 項目結構
2. 環境配置
3.音頻識別基礎知識
(1) STFT和聲譜圖(spectrogram) ? ?
(2)?梅爾頻譜
(3)?梅爾頻率倒譜MFCC
(4) MFCC特征的過程
4.數據處理
(1)數據集Urbansound8K?
(2)自定義數據集
(3)音頻特征提取:?
5.訓練Pipeline
6.預測demo.py
1. 項目結構
2. 環境配置
pytorch==1.7.1,其他依賴庫,請使用pip命令安裝libsora和pyaudio,pydub等庫 librosa==0.8.1 pyaudio==0.2.11 pydub==0.23.13.音頻識別基礎知識
(1) STFT和聲譜圖(spectrogram) ? ?
聲音信號是一維信號,直觀上只能看到時域信息,不能看到頻域信息。通過傅里葉變換(FT)可以變換到頻域,但是丟失了時域信息,無法看到時頻關系。為了解決這個問題,產生了很多方法,短時傅里葉變換,小波等都是很常用的時頻分析方法。 ?
短時傅里葉變換(STFT),就是對短時的信號做傅里葉變換。原理如下:對一段長語音信號,分幀、加窗,再對每一幀做傅里葉變換,之后把每一幀的結果沿另一維度堆疊,得到一張圖(類似于二維信號),這張圖就是聲譜圖。
(2)?梅爾頻譜
人耳能聽到的頻率范圍是20-20000HZ,但是人耳對HZ單位不是線性敏感,而是對低HZ敏感,對高HZ不敏感,將HZ頻率轉化為梅爾頻率,則人耳對頻率的感知度就變為線性。 ? ?
例如如果我們適應了1000Hz的音調,如果把音調頻率提高到2000Hz,我們的耳朵只能覺察到頻率提高了一點點,根本察覺不到頻率提高了一倍 。
將普通頻率轉化到Mel頻率的公式是
在Mel頻域內,人對音調的感知度為線性關系。舉例來說,如果兩段語音的Mel頻率相差兩倍,則人耳聽起來兩者的音調也相差兩倍。
上圖是HZ到Mel的映射關系圖,由于二者為log關系,在頻率較低時,Mel隨HZ變化較快;當頻率較高時,曲線斜率小,變化緩慢。(3)?梅爾頻率倒譜MFCC
:梅爾頻率倒譜系數,簡稱MFCC,Mel-frequency cepstral coefficients)
- 梅爾頻譜就是一般的頻譜圖加上梅爾濾波函數,這一步是為了模擬人耳聽覺對實際頻率的敏感程度
- 梅爾倒譜就是再對梅爾頻譜進行一次頻譜分析,具體就是對梅爾頻譜取對數,然后做DCT變換,目的是抽取頻譜圖的輪廓信息,這個比較能代表語音的特征。
- 如果取低頻的13位,就是最經典的語音特征mfcc了
(4) MFCC特征的過程
- 先對語音進行預加重、分幀和加窗;
- 對每一個短時分析窗,通過FFT得到對應的頻譜;
- 將上面的頻譜通過Mel濾波器組得到Mel頻譜;
- 在Mel頻譜上面進行倒譜分析(取對數,做逆變換,實際逆變換一般是通過DCT離散余弦變換來實現,
- 取DCT后的第2個到第13個系數作為MFCC系數),獲得Mel頻率倒譜系數MFCC,這個MFCC就是這幀語音的特征了
4.數據處理
(1)數據集Urbansound8K?
?Urbansound8K是目前應用較為廣泛的用于自動城市環境聲分類研究的公共數據集,
包含10個分類:空調聲、汽車鳴笛聲、兒童玩耍聲、狗叫聲、鉆孔聲、引擎空轉聲、槍聲、手提鉆、警笛聲和街道音樂聲,類別定義如下:
data/UrbanSound8K/class_name.txt
air_conditioner car_horn children_playing dog_bark drilling engine_idling gun_shot jackhammer siren street_music
數據集下載:https://zenodo.org/record/1203745/files/UrbanSound8K.tar.gz
(2)自定義數據集
可以自己錄制音頻信號,制作自己的數據集,參考[audio/dataloader/record_audio.py]
每個文件夾存放一個類別的音頻數據,每條音頻數據長度在3秒左右,建議每類的音頻數據均衡
生產train和test數據列表:參考[audio/dataloader/create_data.py],train.txt和test.txt格式如下:
[path/to/audio.wav,labels_name]
如:?
ID1/audio.wav,label_names1 ID2/audio.wav,label_names2 ... IDn/audio.wav,label_namesn同時,你需要定義class_name,指定類別的序列,訓練代碼會根據你的class_name進行訓練:
label_names1 label_names2 ... label_namesn(3)音頻特征提取:?
音頻信號是一維的語音信號,不能直接用于模型訓練,需要使用librosa將音頻轉為梅爾頻譜(Mel Spectrogram)。
librosa提供python接口,在音頻、樂音信號的分析中經常用到
wav, sr = librosa.load(data_path, sr=16000) # 使用librosa獲得音頻的梅爾頻譜 spec_image = librosa.feature.melspectrogram(y=wav, sr=sr, hop_length=256) # 計算音頻信號的MFCC spec_image = librosa.feature.mfcc(y=wav, sr=sr)librosa.feature.mfcc實現MFCC源碼如下:
# -- Mel spectrogram and MFCCs -- # def mfcc(y=None, sr=22050, S=None, n_mfcc=20, dct_type=2, norm="ortho", lifter=0, **kwargs):"""Mel-frequency cepstral coefficients (MFCCs)"""if S is None:# 先調用melspectrogram,計算梅爾頻譜,然后取對數:power_to_dbS = power_to_db(melspectrogram(y=y, sr=sr, **kwargs))# 最后進行DCT變換M = scipy.fftpack.dct(S, axis=0, type=dct_type, norm=norm)[:n_mfcc]if lifter > 0:M *= (1 + (lifter / 2) * np.sin(np.pi * np.arange(1, 1 + n_mfcc, dtype=M.dtype) / lifter)[:, np.newaxis])return Melif lifter == 0:return Melse:raise ParameterError("MFCC lifter={} must be a non-negative number".format(lifter))可以看到,librosa庫中,梅爾倒譜就是再對梅爾頻譜取對數,然后做DCT變換
關于librosa的使用方法,請參考:
5.訓練Pipeline
(1)構建訓練和測試數據
def build_dataset(self, cfg):"""構建訓練數據和測試數據"""input_shape = eval(cfg.input_shape)# 獲取數據train_dataset = AudioDataset(cfg.train_data, data_dir=cfg.data_dir, mode='train', spec_len=input_shape[3])train_loader = DataLoader(dataset=train_dataset, batch_size=cfg.batch_size, shuffle=True,num_workers=cfg.num_workers)test_dataset = AudioDataset(cfg.test_data, data_dir=cfg.data_dir, mode='test', spec_len=input_shape[3])test_loader = DataLoader(dataset=test_dataset, batch_size=cfg.batch_size, shuffle=False,num_workers=cfg.num_workers)print("train nums:{}".format(len(train_dataset)))print("test nums:{}".format(len(test_dataset)))return train_loader, test_loader由于librosa.load加載音頻數據特別慢,建議使用cache先進行緩存,方便加速
def load_audio(audio_file, cache=False):"""加載并預處理音頻:param audio_file::param cache: librosa.load加載音頻數據特別慢,建議使用進行緩存進行加速:return:"""# 讀取音頻數據cache_path = audio_file + ".pk"# t = librosa.get_duration(filename=audio_file)if cache and os.path.exists(cache_path):tmp = open(cache_path, 'rb')wav, sr = pickle.load(tmp)else:wav, sr = librosa.load(audio_file, sr=16000)if cache:f = open(cache_path, 'wb')pickle.dump([wav, sr], f)f.close()# Compute a mel-scaled spectrogram: 梅爾頻譜圖spec_image = librosa.feature.melspectrogram(y=wav, sr=sr, hop_length=256)return spec_image(2)構建backbone模型
backbone是一個基于CNN+FC的網絡結構,與圖像CNN分類模型不同的是,圖像CNN分類模型的輸入維度(batch,3,H,W)輸入數據depth=3,而音頻信號的梅爾頻譜圖是深度為depth=1,可以認為是灰度圖,輸入維度(batch,1,H,W),因此實際使用中,只需要將傳統的CNN圖像分類的backbone的第一層卷積層的in_channels=1即可。需要注意的是,由于維度不一致,導致不能使用imagenet的pretrained模型。
當然可以將梅爾頻譜圖(灰度圖)是轉為3通道RGB圖,這樣就跟普通的RGB圖像沒有什么區別了,也可以imagenet的pretrained模型,如
# 將梅爾頻譜圖(灰度圖)是轉為為3通道RGB圖 spec_image = cv2.cvtColor(spec_image, cv2.COLOR_GRAY2RGB) def build_model(self, cfg):if cfg.net_type == "mbv2":model = mobilenet_v2.mobilenet_v2(num_classes=cfg.num_classes)elif cfg.net_type == "resnet34":model = resnet.resnet34(num_classes=args.num_classes)elif cfg.net_type == "resnet18":model = resnet.resnet18(num_classes=args.num_classes)else:raise Exception("Error:{}".format(cfg.net_type))model.to(self.device)return model(3)訓練參數配置
相關的命令行參數,可參考:
def get_parser():data_dir = "/home/dataset/UrbanSound8K/audio"train_data = 'data/UrbanSound8K/train.txt'test_data = 'data/UrbanSound8K/test.txt'class_name = 'data/UrbanSound8K/class_name.txt'parser = argparse.ArgumentParser(description=__doc__)parser.add_argument('--batch_size', type=int, default=32, help='訓練的批量大小')parser.add_argument('--num_workers', type=int, default=8, help='讀取數據的線程數量')parser.add_argument('--num_epoch', type=int, default=100, help='訓練的輪數')parser.add_argument('--class_name', type=str, default=class_name, help='類別文件')parser.add_argument('--learning_rate', type=float, default=1e-3, help='初始學習率的大小')parser.add_argument('--input_shape', type=str, default='(None, 1, 128, 128)', help='數據輸入的形狀')parser.add_argument('--gpu_id', type=int, default=0, help='GPU ID')parser.add_argument('--net_type', type=str, default="mbv2", help='backbone')parser.add_argument('--data_dir', type=str, default=data_dir, help='數據路徑')parser.add_argument('--train_data', type=str, default=train_data, help='訓練數據的數據列表路徑')parser.add_argument('--test_data', type=str, default=test_data, help='測試數據的數據列表路徑')parser.add_argument('--work_dir', type=str, default='work_space/', help='模型保存的路徑')return parser配置好數據路徑,其他參數默認設置,即可以開始訓練了:
# data_dir是你的數據路徑 python train.py --data_dir="dataset/UrbanSound8K/audio1"訓練完成,使用mobilenet_v2,最終訓練集準確率99%左右,測試集96%左右,看起來有點過擬合了。
如果想進一步提高識別準確率可以使用更重的backbone,如resnet34,采用更多的數據增強方法,提高模型的泛發性。
完整的訓練代碼train.py:
import argparse import os import numpy as np import torch import tensorboardX as tensorboard from datetime import datetime from easydict import EasyDict from tqdm import tqdm from torch.utils.data import DataLoader from torch.optim.lr_scheduler import StepLR, MultiStepLR from audio.dataloader.audio_dataset import AudioDataset from audio.utils.utility import print_arguments from audio.utils import file_utils from audio.models import mobilenet_v2, resnetclass Train(object):"""Training Pipeline"""def __init__(self, cfg):cfg = EasyDict(cfg.__dict__)self.device = "cuda:{}".format(cfg.gpu_id) if torch.cuda.is_available() else "cpu"self.num_epoch = cfg.num_epochself.net_type = cfg.net_typeself.work_dir = os.path.join(cfg.work_dir, self.net_type)self.model_dir = os.path.join(self.work_dir, "model")self.log_dir = os.path.join(self.work_dir, "log")file_utils.create_dir(self.model_dir)file_utils.create_dir(self.log_dir)self.tensorboard = tensorboard.SummaryWriter(self.log_dir)self.train_loader, self.test_loader = self.build_dataset(cfg)# 獲取模型self.model = self.build_model(cfg)# 獲取優化方法self.optimizer = torch.optim.Adam(params=self.model.parameters(),lr=cfg.learning_rate,weight_decay=5e-4)# 獲取學習率衰減函數self.scheduler = MultiStepLR(self.optimizer, milestones=[50, 80], gamma=0.1)# 獲取損失函數self.losses = torch.nn.CrossEntropyLoss()def build_dataset(self, cfg):"""構建訓練數據和測試數據"""input_shape = eval(cfg.input_shape)# 加載訓練數據train_dataset = AudioDataset(cfg.train_data,class_name=cfg.class_name,data_dir=cfg.data_dir,mode='train',spec_len=input_shape[3])train_loader = DataLoader(dataset=train_dataset, batch_size=cfg.batch_size, shuffle=True,num_workers=cfg.num_workers)cfg.class_name = train_dataset.class_namecfg.class_dict = train_dataset.class_dictcfg.num_classes = len(cfg.class_name)# 加載測試數據test_dataset = AudioDataset(cfg.test_data,class_name=cfg.class_name,data_dir=cfg.data_dir,mode='test',spec_len=input_shape[3])test_loader = DataLoader(dataset=test_dataset, batch_size=cfg.batch_size, shuffle=False,num_workers=cfg.num_workers)print("train nums:{}".format(len(train_dataset)))print("test nums:{}".format(len(test_dataset)))return train_loader, test_loaderdef build_model(self, cfg):"""構建模型"""if cfg.net_type == "mbv2":model = mobilenet_v2.mobilenet_v2(num_classes=cfg.num_classes)elif cfg.net_type == "resnet34":model = resnet.resnet34(num_classes=args.num_classes)elif cfg.net_type == "resnet18":model = resnet.resnet18(num_classes=args.num_classes)else:raise Exception("Error:{}".format(cfg.net_type))model.to(self.device)return modeldef epoch_test(self, epoch):"""模型測試"""loss_sum = []accuracies = []self.model.eval()with torch.no_grad():for step, (inputs, labels) in enumerate(tqdm(self.test_loader)):inputs = inputs.to(self.device)labels = labels.to(self.device).long()output = self.model(inputs)# 計算損失值loss = self.losses(output, labels)# 計算準確率output = torch.nn.functional.softmax(output, dim=1)output = output.data.cpu().numpy()output = np.argmax(output, axis=1)labels = labels.data.cpu().numpy()acc = np.mean((output == labels).astype(int))accuracies.append(acc)loss_sum.append(loss)acc = sum(accuracies) / len(accuracies)loss = sum(loss_sum) / len(loss_sum)print("Test epoch:{:3.3f},Acc:{:3.3f},loss:{:3.3f}".format(epoch, acc, loss))print('=' * 70)return acc, lossdef epoch_train(self, epoch):"""模型訓練"""loss_sum = []accuracies = []self.model.train()for step, (inputs, labels) in enumerate(tqdm(self.train_loader)):inputs = inputs.to(self.device)labels = labels.to(self.device).long()output = self.model(inputs)# 計算損失值loss = self.losses(output, labels)self.optimizer.zero_grad()loss.backward()self.optimizer.step()# 計算準確率output = torch.nn.functional.softmax(output, dim=1)output = output.data.cpu().numpy()output = np.argmax(output, axis=1)labels = labels.data.cpu().numpy()acc = np.mean((output == labels).astype(int))accuracies.append(acc)loss_sum.append(loss)if step % 50 == 0:lr = self.optimizer.state_dict()['param_groups'][0]['lr']print('[%s] Train epoch %d, batch: %d/%d, loss: %f, accuracy: %f,lr:%f' % (datetime.now(), epoch, step, len(self.train_loader), sum(loss_sum) / len(loss_sum),sum(accuracies) / len(accuracies), lr))acc = sum(accuracies) / len(accuracies)loss = sum(loss_sum) / len(loss_sum)print("Train epoch:{:3.3f},Acc:{:3.3f},loss:{:3.3f}".format(epoch, acc, loss))print('=' * 70)return acc, lossdef run(self):# 開始訓練for epoch in range(self.num_epoch):train_acc, train_loss = self.epoch_train(epoch)test_acc, test_loss = self.epoch_test(epoch)self.tensorboard.add_scalar("train_acc", train_acc, epoch)self.tensorboard.add_scalar("train_loss", train_loss, epoch)self.tensorboard.add_scalar("test_acc", test_acc, epoch)self.tensorboard.add_scalar("test_loss", test_loss, epoch)self.scheduler.step()self.save_model(epoch, test_acc)def save_model(self, epoch, acc):"""保持模型"""model_path = os.path.join(self.model_dir, 'model_{:0=3d}_{:.3f}.pth'.format(epoch, acc))if not os.path.exists(os.path.dirname(model_path)):os.makedirs(os.path.dirname(model_path))torch.jit.save(torch.jit.script(self.model), model_path)def get_parser():data_dir = "/home/dataset/UrbanSound8K/audio"train_data = 'data/UrbanSound8K/train.txt'test_data = 'data/UrbanSound8K/test.txt'class_name = 'data/UrbanSound8K/class_name.txt'parser = argparse.ArgumentParser(description=__doc__)parser.add_argument('--batch_size', type=int, default=32, help='訓練的批量大小')parser.add_argument('--num_workers', type=int, default=8, help='讀取數據的線程數量')parser.add_argument('--num_epoch', type=int, default=100, help='訓練的輪數')parser.add_argument('--class_name', type=str, default=class_name, help='類別文件')parser.add_argument('--learning_rate', type=float, default=1e-3, help='初始學習率的大小')parser.add_argument('--input_shape', type=str, default='(None, 1, 128, 128)', help='數據輸入的形狀')parser.add_argument('--gpu_id', type=int, default=0, help='GPU ID')parser.add_argument('--net_type', type=str, default="mbv2", help='backbone')parser.add_argument('--data_dir', type=str, default=data_dir, help='數據路徑')parser.add_argument('--train_data', type=str, default=train_data, help='訓練數據的數據列表路徑')parser.add_argument('--test_data', type=str, default=test_data, help='測試數據的數據列表路徑')parser.add_argument('--work_dir', type=str, default='work_space/', help='模型保存的路徑')return parserif __name__ == '__main__':parser = get_parser()args = parser.parse_args()print_arguments(args)t = Train(args)t.run()6.預測demo.py
import os import cv2 import argparse import librosa import torch import numpy as np from audio.dataloader.audio_dataset import load_audio, normalization from audio.dataloader.record_audio import record_audio from audio.utils import file_utils, image_utilsclass Predictor(object):def __init__(self, cfg):# self.device = "cuda:{}".format(cfg.gpu_id) if torch.cuda.is_available() else "cpu"self.device = "cpu"self.class_name, self.class_dict = file_utils.parser_classes(cfg.class_name, split=None)self.input_shape = eval(cfg.input_shape)self.spec_len = self.input_shape[3]self.model = self.build_model(cfg.model_file)def build_model(self, model_file):# 加載模型model = torch.jit.load(model_file, map_location="cpu")model.to(self.device)model.eval()return modeldef inference(self, input_tensors):with torch.no_grad():input_tensors = input_tensors.to(self.device)output = self.model(input_tensors)return outputdef pre_process(self, spec_image):"""音頻數據預處理"""if spec_image.shape[1] > self.spec_len:input = spec_image[:, 0:self.spec_len]else:input = np.zeros(shape=(self.spec_len, self.spec_len), dtype=np.float32)input[:, 0:spec_image.shape[1]] = spec_imageinput = normalization(input)input = input[np.newaxis, np.newaxis, :]input_tensors = np.concatenate([input])input_tensors = torch.tensor(input_tensors, dtype=torch.float32)return input_tensorsdef post_process(self, output):"""輸出結果后處理"""scores = torch.nn.functional.softmax(output, dim=1)scores = scores.data.cpu().numpy()# 顯示圖片并輸出結果最大的labellabel = np.argmax(scores, axis=1)score = scores[:, label]label = [self.class_name[l] for l in label]return label, scoredef detect(self, audio_file):""":param audio_file: 音頻文件:return: label:預測音頻的labelscore: 預測音頻的置信度"""spec_image = load_audio(audio_file)input_tensors = self.pre_process(spec_image)# 執行預測output = self.inference(input_tensors)label, score = self.post_process(output)return label, scoredef detect_file_dir(self, file_dir):""":param file_dir: 音頻文件目錄:return:"""file_list = file_utils.get_files_lists(file_dir, postfix=["*.wav"])for file in file_list:print(file)label, score = self.detect(file)print("pred-label:{}, score:{}".format(label, score))print("---" * 20)def detect_record_audio(self, audio_dir):""":param audio_dir: 錄制音頻并進行識別:return:"""time = file_utils.get_time()file = os.path.join(audio_dir, time + ".wav")record_audio(file)label, score = self.detect(file)print(file)print("pred-label:{}, score:{}".format(label, score))print("---"*20)def get_parser():model_file = 'data/pretrained/model_075_0.965.pth'file_dir = "data/audio"class_name = 'data/UrbanSound8K/class_name.txt'parser = argparse.ArgumentParser(description=__doc__)parser.add_argument('--class_name', type=str, default=class_name, help='類別文件')parser.add_argument('--input_shape', type=str, default='(None, 1, 128, 128)', help='數據輸入的形狀')parser.add_argument('--net_type', type=str, default="mbv2", help='backbone')parser.add_argument('--gpu_id', type=int, default=0, help='GPU ID')parser.add_argument('--model_file', type=str, default=model_file, help='模型文件')parser.add_argument('--file_dir', type=str, default=file_dir, help='音頻文件的目錄')return parserif __name__ == '__main__':parser = get_parser()args = parser.parse_args()p = Predictor(args)p.detect_file_dir(file_dir=args.file_dir)# audio_dir = 'data/record_audio'# p.detect_record_audio(audio_dir=audio_dir)完整的項目代碼:https://download.csdn.net/download/guyuealian/30306697
更多AI博客,請參考:
人體關鍵點檢測需要用到人體檢測,請查看鄙人另一篇博客:2D Pose人體關鍵點實時檢測(Python/Android /C++ Demo)_pan_jinquan的博客-CSDN博客
總結
以上是生活随笔為你收集整理的基于梅尔频谱的音频信号分类识别(Pytorch)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: pytorch实现L2和L1正则化reg
- 下一篇: 结构光三维重建Projector-Cam