音频识别(Audio Classification)学习笔记
音頻分類(audio classification)
- 音頻分類(audio classification)
- 一.音頻的定義以及音頻三要素
- 二.音頻數據的存儲方式
- 三.關于音頻的一些專業名詞
- 1.采樣率
- 2.采樣位數
- 3.比特率
- 4.音頻編碼
- 5.聲道數
- 6.碼率
- 7.音頻幀
- 8.音頻格式
- 四.python處理音頻文件
- 1.wave包處理音頻并繪制模擬信號圖
- 2.音頻數據的準備
- (1)wave文件的讀入(torchaudio.load)
- (2)wave文件的聲道統一化
- (3)wave文件的采樣率統一化
- (4)調整為相同長度大小
- 3.數據的變換與增廣
- (1)時移增廣
- (2)梅爾譜圖(講解請看前面)
- (3)數據增廣:時間和頻率屏蔽
- 4.數據的載入
- 5.模型建立
- 6.訓練模型
一.音頻的定義以及音頻三要素
音頻(audio)在不同場合有不同的定義。一般來說,音頻就是人耳可以聽到的頻率在20Hz-20kHz之間的聲波。當然,也可以指像.wav這樣的存儲音頻信息的文件。
既然是聲波,通過中學物理知識我們知道:聲波其實就是聲帶或者其他物體通過振動產生的能量的傳遞,也是一種運動狀態的傳遞。
聲波歸根結底還是波,那就有三個基本要素來定義波:
1.振幅:決定聲波的振動大小,也就是響度。
2.頻率:決定聲波的振動頻率,一般比較刺耳的聲音,頻率就比較大,也就是音調比較高。
3.波形:決定聲波的形狀,可以決定聲音的音色如何。
二.音頻數據的存儲方式
我們已經知道了音頻就是一種波,那么我們得使用什么方法去記錄和存儲呢?一般來說,我們可以使用脈沖編碼調制(PCM,pulse code modulation)。
所謂的PCM,是對連續變化的模擬信號進行抽樣、量化和編碼產生的數字信號。其主要過程包括:
1.抽樣:將連續時間模擬信號變為離散時間、連續幅度的抽樣信號。因為我們都知道,聲波是連續的,但是我們要訓練的數據是離散的。
根據奈奎斯特采樣定理:采樣頻率不小于模擬信號頻譜中最高頻率的的2倍。
2.量化:將抽樣信號變為離散時間、離散幅度的數字信號。
3.編碼:對每一組數據的幅度進行編碼。(這個就類似于哈夫曼編碼了)就比如說給你515組數據,一共有32組不同的幅度,那么你就可以用5位的二進制數來表示,也就是5bytes。
三.關于音頻的一些專業名詞
1.采樣率
剛剛已經提到了,就是每單位時間需要采集的樣本點個數。
2.采樣位數
模擬信號的數據是連續的,而在數字信號中,信號是不連續的,也就是說量化之后的振幅值需要取得一個近似整數,采樣器就會固定使用一個位數來進行采樣。
3.比特率
表示編碼過后的音頻數據每秒需要用多少個比特來表示,通常單位為kbps。
4.音頻編碼
剛剛也已經提到了,就是僅僅由0和1構成的編碼表示不同的振幅。
5.聲道數
記錄聲音的時候,如果每次生成一個聲波數據,稱為單聲道;每次生成兩個聲波數據,稱為雙聲道。
6.碼率
指的是數據流中,每一秒能夠通過的信息量,單位為(bps,bit per second)
7.音頻幀
視頻的每一幀就是圖像,對于PCM流來說,采樣率為ER,采樣位數EN,通道數為c,那么每一秒的音頻大小為:
ER×EN×c8(單位:字節)\frac{ER\times EN\times c}{8}(單位:字節) 8ER×EN×c?(單位:字節)
8.音頻格式
常見的音頻格式有:.wav、.MP3、.MIDI、.AAC。
在計算機處理音頻文件的時候,也是要對數據進行**數字信號-模擬信號(數-模,A/D)**轉換的,這個過程同樣是由采樣和量化構成的。
不同的采樣和量化方法有不同的效果,所以音頻格式就是存放音頻數據的文件格式。
四.python處理音頻文件
1.wave包處理音頻并繪制模擬信號圖
import wave import matplotlib.pyplot as plt import os import random import pandas as pd import numpy as np # tips:我們使用的是read-bytes模式,如果想要更改wav文件則需要以write模式讀入 # write模式和read兩種模式的方法和能使用的函數不同 f=wave.open('./1_02_3.wav','rb') params=f.getparams() nchannels,sampwidth,framerate,nframes=params[:4] print(params) _wave_params(nchannels=1, sampwidth=2, framerate=48000, nframes=28490, comptype='NONE', compname='not compressed') str_bytes_data=f.readframes(nframes=nframes) # 輸入采樣后得到的幀數返回一個音頻數據的二進制數據字符串 wavedata=np.frombuffer(str_bytes_data,dtype=np.int16) wavedata=wavedata*1.0/(max(abs(wavedata))) # 歸一化處理 time=np.arange(0,nframes)*(1.0/framerate) plt.plot(time,wavedata) [<matplotlib.lines.Line2D at 0x1cbc6dba370>]為了后面數據載入方便,我們還是寫一個函數方便讀取音譜圖:
def get_wave_plot(wave_path,plot_save_path=None,plot_save=False):f=wave.open(wave_path,'rb')params=f.getparams()nchannels,sampwidth,framerate,nframes=params[:4]str_bytes_data=f.readframes(nframes=nframes) wavedata=np.frombuffer(str_bytes_data,dtype=np.int16) wavedata=wavedata*1.0/(max(abs(wavedata))) time=np.arange(0,nframes)*(1.0/framerate)plt.plot(time,wavedata)if plot_save:plt.savefig(plot_save_path,bbox_inches='tight')2.音頻數據的準備
關于音頻的變換其實就是說——目標檢測一樣,一般我們用txt存儲每一條數據信息,但是輸入的并不是這個txt,而是txt指向的數據本身并且還要進行一定處理。
本文所使用的是一個記錄了60個人的0-9數字的英文發音,但是我只取了其中一部分。
import torch import torchaudio from tqdm import tqdm from torchaudio import transforms from IPython.display import Audio from torch.optim import Adam from torch.autograd import Variabledata_path='./datasets' test_wave_path='./1_02_3.wav'另外,剛剛我們使用librosa讀入了wav文件,其實深度學習框架pytorch中有對應的處理包——torchaudio(圖像就是torchvision),接下來我們來學習一下如何使用這個包來創造一個“音頻讀取器”。
跟我們的librosa一樣,讀取audio的過程就是“采樣——量化——編碼”,唯一比較不同的地方就是pytorch一般我們習慣用面向對象的編程方式。
(1)wave文件的讀入(torchaudio.load)
我們先來看看這個load到底載入入了啥?
# help(torchaudio.load)Args:
filepath: Path to audio fileout: An optional output tensor to use instead of creating one. (Default: ``None``)normalization: Optional normalization.If boolean `True`, then output is divided by `1 << 31`.Assuming the input is signed 32-bit audio, this normalizes to `[-1, 1]`.If `float`, then output is divided by that number.If `Callable`, then the output is passed as a paramete to the given function,then the output is divided by the result. (Default: ``True``)channels_first: Set channels first or length first in result. (Default: ``True``)num_frames: Number of frames to load. 0 to load everything after the offset.(Default: ``0``)offset: Number of frames from the start of the file to begin data loading.(Default: ``0``)signalinfo: A sox_signalinfo_t type, which could be helpful if theaudio type cannot be automatically determined. (Default: ``None``)encodinginfo: A sox_encodinginfo_t type, which could be set if theaudio type cannot be automatically determined. (Default: ``None``)filetype: A filetype or extension to be set if sox cannot determine itautomatically. (Default: ``None``)Returns:(Tensor, int): An output tensor of size `[C x L]` or `[L x C]` whereL is the number of audio frames andC is the number of channels.An integer which is the sample rate of the audio (as listed in the metadata of the file)簡單解釋一下就是說:
常用的輸入參數有如下幾個:
1.wav文件路徑:字符串,沒啥好說的
2.normalization:布爾值,歸一化,將我們的數據x、y方向也就是時間和振幅控制在一定范圍內。
3.channel_first:布爾值,最后我們輸出的是已經數字化的tensor。(可以參考上面的實例)
4.num_frames:整型,指定取多少個“起始點幀數”后面的幀。
5.offset:整型,”起始點幀數“。
輸出:
[tensor,sample_rate]
sample_rate就是采樣率。
tensor的話,如果是channel_first,那么輸出就是[channel_num,frames_num],否則顛倒過來。
sig,sr=torchaudio.load('./1_02_3.wav',channels_first=True) print(sig) print(sig.shape) print(sr) tensor([[1.2207e-04, 9.1553e-05, 1.2207e-04, ..., 6.1035e-05, 6.1035e-05,3.0518e-05]]) torch.Size([1, 28490]) 48000看得出來采樣率sr是48000——每秒48000幀,那么知道了nframes為28490,相除就可以得到我們的采樣時長。
并且通過sig(信號)的size就可以知道他是一個單聲道。
為了方便我們后續操作,我們不妨寫一個函數:
def audio_open(audio_path):"""audio_path -> [tensor:channel*frames,int:sample_rate]"""sig,sr=torchaudio.load(audio_path,channels_first=True)return [sig,sr](2)wave文件的聲道統一化
我們載入了wave的文件后,得到的是tensor和sr,接下里就是數據的歸一化了,首先對tensor的size的channels進行歸一化:
def regular_channels(audio,new_channels):"""torchaudio-file([tensor,sample_rate])+target_channel -> new_tensor"""sig,sr=audioif sig.shape[0]==new_channels:return audioif new_channels==1:new_sig=sig[:1,:] # 直接取得第一個channel的frame進行操作即可else:# 融合(賦值)第一個通道new_sig=torch.cat([sig,sig],dim=0) # c*f->2c*f# 順帶提一句——return [new_sig,sr]其實這一步就是把單聲道和立體聲(雙通道)統一化。
(3)wave文件的采樣率統一化
接下來對采樣率進行標準化。
有些細心的同志就會發現上面的librosa進入rb模式后是不能更改sr的,不用單項,torchaudio已經為我們寫好了一個類——Resample。
是不是感覺有點像torchvision的Reshaple?
def regular_sample(audio,new_sr):sig,sr=audioif sr==new_sr:return audiochannels=sig.shape[0]re_sig=torchaudio.transforms.Resample(sr,new_sr)(sig[:1,:])if channels>1:re_after=torchaudio.transforms.Resample(sr,new_sr)(sig[1:,:])re_sig=torch.cat([re_sig,re_after])# 順帶提一句torch.cat類似np.concatenate,默認dim=0return [re_sig,new_sr](4)調整為相同長度大小
這幾部就是將音頻樣本的大小調整為同一個長度,方法就是使用靜默填充或者通過截斷的方式來延長采樣時間。所謂靜默填充,顧名思義就是一段時間后的音頻振幅為0。
def regular_time(audio,max_time):sig,sr=audiorows,len=sig.shapemax_len=sr//1000*max_timeif len>max_len:sig=sig[:,:max_len]elif len<max_len:pad_begin_len=random.randint(0,max_len-len)pad_end_len=max_len-len-pad_begin_len# 這一步就是隨機取兩個長度分別加在信號開頭和信號結束pad_begin=torch.zeros((rows,pad_begin_len))pad_end=torch.zeros((rows,pad_end_len))sig=torch.cat((pad_begin,sig,pad_end),1) # 注意哦我們不是增加通道數,所以要制定維度為1return [sig,sr]3.數據的變換與增廣
再進行了數據的統一化后,按照圖像分類中數據預處理的基本步驟,接下來在數據導入之前應該先進行數據的變換來提升訓練的難度。如果數據量不夠大,還可以通過數據增廣的方式增加數據量。
(1)時移增廣
顧名思義,就是用時間偏移將音頻向左或向右移動隨機量來對原始音頻信號進行數據增廣。
def time_shift(audio,shift_limit):sig,sr=audiosig_len=sig.shape[1]shift_amount=int(random.random()*shift_limit*sig_len) # 移動量return (sig.roll(shift_amount),sr)Tips:
Tensor.roll:Roll the tensor along the given dimension
是torch函數roll的實例,返回一個視圖,不會進行原地修改。
簡單來說就是將Tensor的元素沿著某個dim(參數)滾動shifts(參數)個單位。
意思就是說,所有元素沿著一個人方向移動n個單位,如果出界,回到第一位。
t1=torch.randint(0,9,(4,2)) print(t1) # 沿著列的方向移動(橫向方向) t2=t1.roll(3,1) t3=t1.roll(4,1) print(t2) print(t3) # 沿著縱軸方向 t4=t1.roll(3,0) print(t4) tensor([[2, 2],[3, 2],[0, 8],[8, 3]]) tensor([[2, 2],[2, 3],[8, 0],[3, 8]]) tensor([[2, 2],[3, 2],[0, 8],[8, 3]]) tensor([[3, 2],[0, 8],[8, 3],[2, 2]])(2)梅爾譜圖
我們可以將增廣后的音頻轉換為梅爾圖譜,獲取圖譜基本特征,一般都是是音頻是數據輸入到深度學習模型中常用的、合適的方法。
# get Spectrogram def get_spectro_gram(audio,n_mels=64,n_fft=1024,hop_len=None):sig,sr=audiotop_db=80spec=transforms.MelSpectrogram(sr,n_fft=n_fft,hop_length=hop_len,n_mels=n_mels)(sig)spec=transforms.AmplitudeToDB(top_db=top_db)(spec)return spec(3)數據增廣:時間和頻率屏蔽
在得到梅爾頻譜的基礎上可以利用一種SpecAugment的技術:
1.頻率屏蔽,在頻譜圖上添加水平條來隨機屏蔽一些列連續頻率。
2.時間掩碼,利用縱軸方向隨機遮擋了一定時間范圍。
def spectro_augment(spec,max_mask_pct=0.1,n_freq_masks=1,n_time_masks=1):_,n_mels,n_steps=spec.shapemask_value=spec.mean()aug_spec=specfreq_mask_param=max_mask_pct*n_melsfor _ in range(n_freq_masks):aug_spec=transforms.FrequencyMasking(freq_mask_param)(aug_spec,mask_value)return aug_spec4.數據的載入
在經過了以上依次處理后,我們得到的的x就是一個梅爾頻譜tensor(shape=(channels,mel_freqbands,time_steps))+sample_rate。
現在就對我們的數據進行載入,后面的操作跟識別手寫數字數據集沒太大差別了。
我們可以先用pandas看看存儲數據信息的dataframe。
csv_f=pd.read_csv("./datasets/data_info.csv",encoding='utf-8') csv_f.head()| 0 | 0_01_0.wav | 0 | ./datasets/data/0_01_0.wav |
| 1 | 0_01_1.wav | 0 | ./datasets/data/0_01_1.wav |
| 2 | 0_01_10.wav | 0 | ./datasets/data/0_01_10.wav |
| 3 | 0_01_11.wav | 0 | ./datasets/data/0_01_11.wav |
| 4 | 0_01_12.wav | 0 | ./datasets/data/0_01_12.wav |
可以看一下有多少類:
print(set(csv_f['label'])) {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}然后編寫一個數據集(torch.util.data.DataSet)子類用于加載自己的數據集。
from torch.utils.data import Dataset,DataLoader,random_splitclass SoundDataSet(Dataset):"""初始化函數:輸入:info_data文件的數據框格式讀取信息+數據集路徑并且寫入相關屬性。"""def __init__(self,df,data_path):self.df=dfself.data_path=data_pathself.duration=4000 # 統一時長self.sr=44100 # 統一采樣率self.channel=2 # 統一聲道self.shift_pct=0.4# 一般重寫這三個方法就夠了def __len__(self):return len(os.listdir(self.data_path+'/'+'data'))def __getitem__(self,index):class_id=torch.Tensor(self.df['label'].tolist())[index]audio_file=audio_open(self.df['filepath'][index]) re_sample_file=regular_sample(audio_file,self.sr)re_channel_file=regular_channels(re_sample_file,self.channel)re_duration_file=regular_time(re_channel_file,self.duration)shift_time_file=time_shift(re_duration_file,self.shift_pct)sgram=get_spectro_gram(shift_time_file,n_mels=64,n_fft=1024,hop_len=None)aug_sgram=spectro_augment(sgram,max_mask_pct=0.1,n_freq_masks=2,n_time_masks=2)return aug_sgram.float(),class_id.float()Tips:
其實之前我一直是按照模板來寫這3個方法的,但是后面我學習了一些代碼,我是這樣猜想的。
len(class_name):按照這個形式調用的話就會去調用這個__len__方法,后面我們實例化DataLoader的時候,DataLoader會自動調用len方法把該準備好的文件都調用完。
__getitem__:這個方法是一個關鍵,它決定DataLoader在load你的DataSet會返回哪些東西到實例化的變量中去。因此,在這里你就得返回你的x,y。
但是值得特別注意的是:
這個方法會輸入一個index參數用于后續DataLoader遍歷你的數據集返回item(每一個數據項)。因此,我剛開始誤以為是直接返回zip好的x_train,y_train(shape(2,1,sample_size,-1,-1))。其實應該返回單個數據項。(畢竟人家叫做get"item")
init:為啥要把init函數放在最后?因為getitem和len方法不可能只用它們自己的輸入參數,更多的,還需要通過self調用實例化屬性來得到一些“額外信息”,eg:數據集路徑、數據集信息文件等等。
不然getitem從哪里得到數據項呢?
來看看效果如何?
dataframe=pd.read_csv('./datasets/data_info.csv') data_path='./datasets' mydatasets=SoundDataSet(dataframe,data_path) num_items=len(mydatasets) num_train=round(num_items*0.8) num_test=num_items-num_train train_dataset,test_dataset=random_split(mydatasets,[num_train,num_test])train_dataloader=DataLoader(train_dataset,batch_size=16,shuffle=True) test_dataloader=DataLoader(test_dataset,batch_size=16,shuffle=True)5.模型建立
通過上面的數據處理,輸入網絡的tenor應該為:
(batch_size,channels,mel_freq_bands,time_steps),在經過conv層后,進入分類器得到10個類別的預測值。
6.訓練模型
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") model=AudioClassificationModel() model=model.to(device) # 遍歷模型參數并訪問device屬性判斷cuda是否用上了 print(next(model.parameters()).device)# 定義超參數 learning_rate=1e-2 epochs=5 batch_size=16 cuda:0順便寫一個訓練日志記錄:
from datetime import datetime def train_log_generator(train_theme_name,optimizer,model,epochs,lr,batchsize,training_opeartion):nowtime=datetime.now()year=str(nowtime.year)month=str(nowtime.month)day=str(nowtime.day)hour=str(nowtime.hour)minute=str(nowtime.minute)second=str(nowtime.second)state={'net':model.state_dict(), 'optimizer':optimizer.state_dict(), 'epoch':epochs}nowtime_strings=year+'/'+month+'/'+day+'/'+hour+':'+minute+':'+secondlog_path='./log/'workplace_path=os.getcwd()content=""" Theme:{}\n batch size:{}\n learning rate:{}\n epochs:{}\n Date:{}\n workplace:{}\n Optimizer:\n{}\n Model:\n{}\n, Train:\n{}\n""".format(train_theme_name,batchsize,lr,epochs,nowtime_strings,workplace_path,str(optimizer),str(model),training_opeartion)log_name='{}_{}_{}_{}_{}_{}.log'.format(train_theme_name,year,month,day,hour,minute,second)log_save_path=log_path+log_namefile=open(log_save_path,'w',encoding='utf-8')file.write(content)file.close()torch.save(state,'./weight/{}_{}_{}_{}_{}_{}.pth'.format(train_theme_name,year,month,day,hour,minute,second))開始訓練:
loss_fn=CrossEntropyLoss() train_operation="" optim=Adam(model.parameters(),lr=learning_rate) for epoch in range(epochs):running_loss=0.correct_prediction=0accuracy=0.total_prediction_number=0train_operation=train_operation+"Epoch:{}/{}\n".format(epoch+1,epochs)print("Epoch:{}/{}".format(epoch+1,epochs))print("-"*10+'training'+"-"*10)train_operation=train_operation+"-"*10+'training'+"-"*10+'\n'for item in tqdm(iter(train_dataloader)):x_train,y_train=item[0].to(device),item[1].to(device).long()optim.zero_grad()outputs=model(x_train)loss=loss_fn(outputs,y_train)loss.backward()optim.step()running_loss+=loss.datatrain_operation=train_operation+"-"*10+'testing'+"-"*10+'\n'print("-"*10+'testing'+"-"*10)for item1 in tqdm(iter(test_dataloader)):x_test,y_test=item1[0].to(device),item1[1].to(device)prediction=model(x_test)_,pred=torch.max(prediction,1)correct_prediction+=(pred==y_test).sum().datatotal_prediction_number+=pred.shape[0]accuracy=correct_prediction/total_prediction_numbertrain_operation=train_operation+"train loss:{} test accuracy:{}.\n".format(running_loss,accuracy)print("train loss:{} test accuracy:{}.\n".format(running_loss,accuracy))train_log_generator('mnist_audio_classification',optimizer=optim,model=model,epochs=epoch,lr=learning_rate,batchsize=batch_size,training_opeartion=train_operation) Epoch:1/5 ----------training----------0%| | 0/225 [00:00<?, ?it/s]C:\Users\22704\Anaconda3\envs\Pytorch\lib\site-packages\torch\functional.py:515: UserWarning: stft will require the return_complex parameter be explicitly specified in a future PyTorch release. Use return_complex=False to preserve the current behavior or return_complex=True to return a complex output. (Triggered internally at ..\aten\src\ATen\native\SpectralOps.cpp:653.)return _VF.stft(input, n_fft, hop_length, win_length, window, # type: ignore C:\Users\22704\Anaconda3\envs\Pytorch\lib\site-packages\torch\functional.py:515: UserWarning: The function torch.rfft is deprecated and will be removed in a future PyTorch release. Use the new torch.fft module functions, instead, by importing torch.fft and calling torch.fft.fft or torch.fft.rfft. (Triggered internally at ..\aten\src\ATen\native\SpectralOps.cpp:590.)return _VF.stft(input, n_fft, hop_length, win_length, window, # type: ignore 100%|██████████| 225/225 [03:37<00:00, 1.04it/s]----------testing----------100%|██████████| 57/57 [00:54<00:00, 1.05it/s]train loss:361.5733642578125 test accuracy:0.648888885974884.Epoch:2/5 ----------training----------100%|██████████| 225/225 [03:43<00:00, 1.01it/s]----------testing----------100%|██████████| 57/57 [00:54<00:00, 1.04it/s]train loss:123.7612533569336 test accuracy:0.8799999952316284.Epoch:3/5 ----------training----------100%|██████████| 225/225 [03:42<00:00, 1.01it/s]----------testing----------100%|██████████| 57/57 [00:54<00:00, 1.04it/s]train loss:52.503448486328125 test accuracy:0.9588888883590698.Epoch:4/5 ----------training----------100%|██████████| 225/225 [03:40<00:00, 1.02it/s]----------testing----------100%|██████████| 57/57 [00:54<00:00, 1.04it/s]train loss:30.632572174072266 test accuracy:0.9677777886390686.Epoch:5/5 ----------training----------100%|██████████| 225/225 [03:41<00:00, 1.02it/s]----------testing----------100%|██████████| 57/57 [00:55<00:00, 1.03it/s]train loss:22.01243019104004 test accuracy:0.9688889384269714.訓練日志展示:
音頻識別,本質上還是圖像分類的過程,圖像分類的本質其實就是分類任務,面對一個新領域的classification,我認為最重要的是如何進行數據的預處理+選擇什么樣子的網絡模型。在音頻識別過程我們可以看到,最開始處理的tensor是(channels,nframs)的,但是2維信息不夠充足以達到較好的訓練效果,所以我們通過梅爾譜圖將其擴充到了(channels,mel_freq_bands,time_steps)(頻率域空間),后續的內容就是一個非常簡單的輸入tensor進行訓練了,但是該過程中還需要對數據集進行制作,對于不同領域可能又有不同的數據增廣+數據變換的方法,所以如果沒有思路了,不妨回到最初的tensor回歸練練手,再重裝上陣。
部分學習資料來源:
使用深度學習進行音頻分類的端到端示例和解釋
csdn blog:音頻的基本概念
github:音頻mnist處理數據集
傅里葉級數和傅里葉變換是什么關系?
總結
以上是生活随笔為你收集整理的音频识别(Audio Classification)学习笔记的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 安卓抓包测试流程
- 下一篇: 考研线性代数深入理解