最近這段時間,學校裏的事情實在太多了,從七月下旬一直到八月底實驗室裏基本天天十二點或者通宵,實在是沒有精力和時間來寫博客。這周老師出國開會,也算有了一個短暫的休息機會,剛好寫點有意思的東西。
上週在天津的會議上碰到一個北交的姐們兒,她想利用小波變換來處理失超信號,剛好之前自己就有這個想法,所以回來後就想着把相關的內容好好複習複習,最相關的就是傅里葉分析和小波變換了。數學推導固然重要,但寫那個實在是太乏味了,然後想到之前網上一個新聞,說一個同學通過新聞裏記者撥號的聲音反推出了周鴻禕的手機號碼,就想着能不能自己也做一個這樣的號碼識別程序呢?
說做就做,首先整理一下思路,我覺得大概的流程應該包括一下幾步:
1 單鍵聲音的採集與分析
這是後面號碼識別的基礎,針對每個按鍵音分析其在頻域上的分佈規律進而得到一個基準,後面再採集到的聲音可據此進行判定。
2 聲音的降噪
自己錄的這些聲音總不會太完美,直接進行頻譜分析,會得到一個非常雜亂的結果,所以有效的聲音降噪可以幫助我們更加精確地進行判斷。
3 號碼識別思路
我想的方案主要是兩種,剛好也符合我最近複習的這兩種變換
A 對聲音文件進行有效區域劃分,然後對每個區域單獨進行頻譜分析,然後比對訓練數據,推斷每一處對應的號碼,最後輸出。
B 採用小波分析,輸出對應聲音文件的小波時頻圖,觀察其在不同時刻頻域各處強度的變化,進而確定所撥的號碼。
兩種方案我感覺應該都可以,不過第一種感覺相對要簡單一點,所以我們就先來試試第一個。
Ok,那第一步我們得先找到合適的單個按鍵音數據,這個上網一搜iphone按鍵音就找到了,下載下來發現剛好又是wav文件,可以用Python自帶的wave庫直接處理,簡直美滋滋。
簡單說一下wave庫,用它讀取一個wave文件後,我們可以獲取四個參數,包括通道數目,樣本寬度,採樣率以及採樣數目。根據通道數目,你可以確定是單通道和雙通道,如果是單通道你直接讀取採樣文件就行,但如果是雙通道,採樣是一左一右輪着來的,所以到時候你還得把它分成兩列,然後選擇其中一列來讀。從網上下載的這些音源剛好是單聲道的,所以直接讀取就行了,下面以0爲例,讀取它的波形並畫出來。
import numpy as np import wave from matplotlib import pyplot as plt file_path='C:\Users\**\Desktop\iphone\dtmf-0.wav' f=wave.open(file_path,'rb') num=file_path[-5] params=f.getparams() nchannels,samplewidth,framerate,nframes=params[:4] str_data=f.readframes(nframes) f.close() wave_data=np.fromstring(str_data,dtype=np.short) wave_data.shape=-1,1 if nchannels==2: wave_data.shape=-1,2 else: pass wave_data=wave_data.T time=np.arange(0,nframes)*(1.0/framerate) plt.subplot(211) plt.plot(time,wave_data[0],'r-') plt.xlabel('Time/s') plt.ylabel('Ampltitude') plt.title('Num '+num+' time/ampltitude') plt.show()
結果如下
其實波形看起來還是比較規律的,但明顯是有不同頻率的波疊加在一起,所以下一步我們採用傅里葉分析,看看它在頻域上是什麼樣的。Python中進行傅里葉分析是十分方便的,直接利用numpy中的fft就行,分解完成後,考慮到高頻的成分不需要,所以選擇4000做個閾值,超出這個區域的都舍掉。另外,因爲目前我並不考慮信號的重構,所以我直接把對應頻率的幅值去了絕對值,這樣方便我下一步尋找波峯。完成這些操作後,畫個頻譜圖看看有什麼規律。
df=framerate/(nframes-1) freq=[df*n for n in range(0,nframes)] transformed=np.fft.fft(wave_data[0]) d=int(len(transformed)/2) while freq[d]>4000: d-=10 freq=freq[:d] transformed=transformed[:d] for i,data in enumerate(transformed): transformed[i]=abs(data) plt.subplot(212) plt.plot(freq,transformed,'b-') plt.xlabel('Freq/Hz') plt.ylabel('Ampltitude') plt.title('Num '+num+' freq/ampltitude') plt.show()
結果如下
哎呀我去,這也太完美了吧……我都懷疑下的這些波形文件是電腦寫的,也太規整了,僅在兩個頻率處出現了峯值,其餘處爲0,感覺事情並不簡單,上網查了一圈發現Iphone的按鍵音採用的是DTMF,雙音多頻,每個按鍵音都是一個高頻加一個低頻信號的疊加。原來如此,那我就放心繼續做下去了。那麼下一步是提取這兩個頻率值,上面我已經說了,爲了方便我尋找這兩個波峯,我已經將平率對應幅度全部取絕對值,現在其實就是找到兩個局部極值對應的頻率就好。那首先找到兩個極大值,然後確定它們的位置再對應到頻域上就OK,代碼如下
local_max=[] for i in np.arange(1,len(transformed)-1): if transformed[i]>transformed[i-1] and transformed[i]>transformed[i+1]: local_max.append(transformed[i]) local_max=sorted(local_max) loc1=np.where(transformed==local_max[-1]) max_freq=freq[loc1[0][0]] loc1=np.where(transformed==local_max[-2]) min_freq=freq[loc1[0][0]] print 'Two freq ',max_freq,min_freq
結果如下
Two freq 1278 900
好的,針對一個數字的音頻分析完成了,那其他的數字也如法炮製,定義一個函數,再用個循環就好,最後把這些數字所對應的頻率畫在圖上,整體代碼如下:
import wave import numpy as np from matplotlib import pyplot as plt def wave_analysis(file_path): f=wave.open(file_path,'rb') num=file_path[-5] params=f.getparams() nchannels,samplewidth,framerate,nframes=params[:4] str_data=f.readframes(nframes) f.close() wave_data=np.fromstring(str_data,dtype=np.short) wave_data.shape=-1,1 if nchannels==2: wave_data.shape=-1,2 else: pass wave_data=wave_data.T time=np.arange(0,nframes)*(1.0/framerate) plt.subplot(211) plt.plot(time,wave_data[0],'r-') plt.xlabel('Time/s') plt.ylabel('Ampltitude') plt.title('Num '+num+' time/ampltitude') plt.show() df=framerate/(nframes-1) freq=[df*n for n in range(0,nframes)] transformed=np.fft.fft(wave_data[0]) d=int(len(transformed)/2) while freq[d]>4000: d-=10 freq=freq[:d] transformed=transformed[:d] for i,data in enumerate(transformed): transformed[i]=abs(data) plt.subplot(212) plt.plot(freq,transformed,'b-') plt.xlabel('Freq/Hz') plt.ylabel('Ampltitude') plt.title('Num '+num+' freq/ampltitude') local_max=[] for i in np.arange(1,len(transformed)-1): if transformed[i]>transformed[i-1] and transformed[i]>transformed[i+1]: local_max.append(transformed[i]) local_max=sorted(local_max) loc1=np.where(transformed==local_max[-1]) max_freq=freq[loc1[0][0]] loc1=np.where(transformed==local_max[-2]) min_freq=freq[loc1[0][0]] plt.show() print 'Two freq ',max_freq,min_freq return max_freq,min_freq def main(): x=[] y=[] for i in np.arange(0,10): path='C:\Users\<span style="font-family: Arial, Helvetica, sans-serif;">**</span><span style="font-family: Arial, Helvetica, sans-serif;">\Desktop\iphone\dtmf-'+str(i)+'.wav'</span> max_freq,min_freq=wave_analysis(path) x.append(i) y.append(max_freq) x.append(i) y.append(min_freq) plt.scatter(x,y,marker='*') plt.show() if __name__=='__main__': main()
中間的振幅和頻域的圖就不貼了,就看一下最後一張每個數字所對應的特定頻率圖
把數字填到表格裏是這樣
其實規律還是很容易看出來的,每個按鍵音對應於一個高頻和一個低頻,其中,123,456,789的低頻部分相似,147,258,369的高頻部分相似,這和DTMF是一致的,從百科上扒了一張圖,大家可以對比一下
展現出來的規律是相似的,但數值並不完全一樣,我們計算出來的似乎有那麼一點點偏小,但這並不重要,趕緊拿個實際錄音來看看吧。
先錄了10個單音,看看頻譜分析怎麼樣,完全同樣的方法,針對數字0,結果如下
Two freq 1283 1270
發現問題了沒
1 錄音的數據並不是全程都是按鍵音,很大一部分是無用的,整體進行傅里葉分析會產生非常大的干擾信號
2 錄音的數據裏疊加有很多噪聲,導致頻域分佈複雜。
3 頻域信號裏有很多的毛刺,這樣導致產生了很多局部極大值,導致我們之前用的尋找波峯的算法失效。
OKOK,那我們一步步來解決問題。
首先第一個相當於就是有效波形的提取了。高級的方法我也不會啊,但就直觀來看,明顯有效區域的波形幅度遠大於無效區,那我們是不是可以取個閾值,從第一個超過該閾值的開始記爲起始點,然後連續超過多少個點都小於閾值後記爲終止點,當然可以,在這裏我選擇的閾值是最大值的5%,事實表明這樣還是很有效的。
那對與第二個問題,其實噪聲很多都是高頻的,首先濾除高頻部分是一個選項,其次即使是噪聲,它往往也是週期性的,一個週期內的噪聲疊加起來往往爲0,所以我們選一個時間窗口,進行移動平均,這樣可以有效的消除噪聲的影響。同時,這也能解決第三步遇到的問題,移動平均最大的好處就是會使曲線變得光滑,不會出現很多的毛刺,這樣通過移動平均處理後的頻域圖又可以重新使用我們之前所說的尋找局部極值來確定頻率的方法。哦,對了對了,移動平均可以通過卷積操作來實現,非常簡單。代碼如下
import wave import numpy as np from matplotlib import pyplot as plt #load wave file and get params file_path='C:\\Users\\**\\Desktop\\iphone\\Test\\0.wav' f=wave.open(file_path,'rb') #num=file_path[-5] num=str(0) params=f.getparams() nchannels,samplewidth,framerate,nframes=params[:4] str_data=f.readframes(nframes) f.close() wave_data=np.fromstring(str_data,dtype=np.short) wave_data.shape=-1,1 if nchannels==2: wave_data.shape=-1,2 else: pass wave_data=wave_data.T #moving average def moving_average(data,n): weights=np.ones(n) weights/=weights.sum() ma=np.convolve(data,weights,mode='full')[:len(data)] ma[:n]=ma[n] return ma new_data=moving_average(wave_data[0],10) new_data=moving_average(new_data,10) new_data_2=[] max_wave=new_data.max() #look for the start point and end point index=0 flag=False for i in np.arange(0,len(new_data)): # index=0 if abs(new_data[i])>=0.05*max_wave: new_data_2.append(new_data[i]) index=0 if abs(new_data[i+1])<0.05*max_wave: for j in np.arange(1,40): if abs(new_data[i+1+j])<0.05*max_wave: index+=1 if index>=39: print index flag=True break if flag==True: break time=np.arange(0,len(new_data_2))*(1.0/framerate) plt.subplot(211) plt.plot(time,new_data_2,'r-') plt.xlabel('Time/s') plt.ylabel('Ampltitude') plt.title('Num '+num+' time/ampltitude') plt.show() df=framerate/(len(new_data_2)-1) freq=[df*n for n in range(0,len(new_data_2))] transformed=np.fft.fft(new_data_2) d=int(len(transformed)/2) while freq[d]>2000: d-=10 freq=freq[:d] transformed=transformed[:d] for i,data in enumerate(transformed): transformed[i]=abs(data) transformed=moving_average(transformed,5) transformed=moving_average(transformed,5) transformed=moving_average(transformed,5) plt.subplot(212) plt.plot(freq,transformed,'b-') plt.xlabel('Freq/Hz') plt.ylabel('Ampltitude') plt.title('Num '+num+' freq/ampltitude') plt.show() #look for local maximum local_max=[] for i in np.arange(1,len(transformed)-1): if transformed[i]>transformed[i-1] and transformed[i]>transformed[i+1]: local_max.append(transformed[i]) local_max=sorted(local_max) loc1=np.where(transformed==local_max[-1]) freq1=freq[loc1[0][0]] loc1=np.where(transformed==local_max[-2]) freq2=freq[loc1[0][0]] print 'Two freq ',freq1,freq2
結果如下
Two freq 1368 981
蛤蛤蛤,perfect!!!有效信號的提取以及頻譜分析都沒有問題了,唯一的遺憾就是分解出來的兩個頻率值又和之前下載的版本不一致了,不過沒關係,我們還是要相信實際結果。接下來依次對錄的其他號碼進行分析,得到新的各個號碼對應兩個頻率值。
得到了所有號碼對應的兩個頻率值之後,我們就可以對再採集的信號進行預測了。採用什麼預測方法呢?在之前博客裏寫過很多很多分類方法,但我覺得都不用,爲啥呢,訓練數據太少了,算法再高級也沒卵用,哎呀,好喪啊……那我們就用最樸素的方法吧,計算採樣數據的兩個頻率與所有訓練數據頻率的距離,選擇最小的那個作爲輸出,當然你也可以宣稱用的是KNN算法,沒毛病啊,就是最近鄰呀……
回到最開始說的,我們想做的是一個語音號碼識別,所以這裏還要考慮一段數據包含多個按鍵音的有效區域提取,因爲之前的算法時針對單音的。一開始我以爲只要把之前的跳出循環操作改成保存數據,標記置0就OK,後來發現我實在是天真了,這樣做的後果就是除了我們真實有效區域以外,偶爾有一個噪聲超過閾值的也全都提取出來了。所以除了幅度閾值之外,我對提取的有效區域長度也做了一個限制,進而保提取的區域都是我們的目標區域。
至此,全部的問題都解決了,整合一下代碼,最終如下
import wave import numpy as np from matplotlib import pyplot as plt #moving average def moving_average(data,n): weights=np.ones(n) weights/=weights.sum() ma=np.convolve(data,weights,mode='full')[:len(data)] ma[:n]=ma[n] return ma #get wave data def get_wave_data(file_path): f=wave.open(file_path,'rb') params=f.getparams() nchannels,samplewidth,framerate,nframes=params[:4] str_data=f.readframes(nframes) f.close() wave_data=np.fromstring(str_data,dtype=np.short) wave_data.shape=-1,1 if nchannels==2: wave_data.shape=-1,2 else: pass wave_data=wave_data.T return wave_data[0],framerate #find the efficient area in wave def find_efficient_area(wave_data): wave_data=moving_average(wave_data,10) wave_data=moving_average(wave_data,10) wave_data=moving_average(wave_data,10) wave_data=moving_average(wave_data,10) new_data=[] max_wave=wave_data.max() efficient_data=[] index=0 count=0 flag=False for i in np.arange(0,len(wave_data)): if abs(wave_data[i])>=0.05*max_wave: new_data.append(wave_data[i]) index=0 if i+1>=len(wave_data): break if abs(wave_data[i+1])<0.05*max_wave: for j in np.arange(1,60): if i+1+j>=len(wave_data): break if abs(wave_data[i+1+j])<0.05*max_wave: index+=1 if index>=59: flag=True if flag==True: if len(new_data)>2000: # plt.plot(new_data,'r-') # plt.show() efficient_data.append(new_data) count+=1 new_data=[] index=0 flag=False print 'Find ',count,' efficient wave' return efficient_data,count #Analysis single wave data def wave_analysis_single(efficient_data,framerate): time=np.arange(0,len(efficient_data[0]))*(1.0/framerate) # plt.plot(time,efficient_data[0],'r-') # plt.xlabel('Time/s') # plt.ylabel('Ampltitude') # plt.title(' time/ampltitude') # plt.show() df=framerate/(len(efficient_data[0])-1) freq=[df*n for n in range(0,len(efficient_data[0]))] transformed=np.fft.fft(efficient_data[0]) d=int(len(transformed)/2) while freq[d]>2000: d-=10 freq=freq[:d] transformed=transformed[:d] for i,data in enumerate(transformed): transformed[i]=abs(data) transformed=moving_average(transformed,5) transformed=moving_average(transformed,5) transformed=moving_average(transformed,5) # transformed=moving_average(transformed,5) # plt.plot(freq,transformed,'b-') # plt.xlabel('Freq/Hz') # plt.ylabel('Ampltitude') # plt.title(' freq/ampltitude') # plt.show() #look for local maximum local_max=[] for i in np.arange(1,len(transformed)-1): if transformed[i]>transformed[i-1] and transformed[i]>transformed[i+1]: local_max.append(transformed[i]) local_max=sorted(local_max) loc1=np.where(transformed==local_max[-1]) freq_1=freq[loc1[0][0]] loc1=np.where(transformed==local_max[-2]) freq_2=freq[loc1[0][0]] print 'Frequency',freq_1,freq_2 return freq_1,freq_2 #Get training data train_data={} for i in np.arange(0,10): file_path='C:\\Users\\**\\Desktop\\iphone\\Test\\'+str(i)+'.wav' wave_data,framerate=get_wave_data(file_path) data,count=find_efficient_area(wave_data) a,b=wave_analysis_single(data,framerate) train_data[i]=[a,b] #multi_number estimate def number_estimate(freq1,freq2): err=100000 num=0 for i in np.arange(0,10): tmp=(freq1-train_data[i][0])**2+(freq2-train_data[i][1])**2 if tmp<err: err=tmp num=i else: pass return num #Analysis multi wave data def wave_analysis_multi(efficient_data,framerate): num=[] for data in efficient_data: time=np.arange(0,len(data))*(1.0/framerate) plt.plot(time,data,'r-') plt.xlabel('Time/s') plt.ylabel('Ampltitude') plt.title(' time/ampltitude') plt.show() df=framerate/(len(data)-1) freq=[df*n for n in range(0,len(data))] transformed=np.fft.fft(data) d=int(len(transformed)/2) while freq[d]>2000: d-=10 freq=freq[:d] transformed=transformed[:d] for i,data in enumerate(transformed): transformed[i]=abs(data) transformed=moving_average(transformed,5) transformed=moving_average(transformed,5) transformed=moving_average(transformed,5) plt.plot(freq,transformed,'b-') plt.xlabel('Freq/Hz') plt.ylabel('Ampltitude') plt.title(' freq/ampltitude') plt.show() # look for local maximum local_max=[] for i in np.arange(1,len(transformed)-1): if transformed[i]>transformed[i-1] and transformed[i]>transformed[i+1]: local_max.append(transformed[i]) local_max=sorted(local_max) loc1=np.where(transformed==local_max[-1]) freq_1=freq[loc1[0][0]] loc1=np.where(transformed==local_max[-2]) freq_2=freq[loc1[0][0]] if freq_1<freq_2: freq_1,freq_2=freq_2,freq_1 print freq_1,freq_2 num.append(number_estimate(freq_1,freq_2)) print num file_path='C:\\Users\\**\Desktop\\iphone\\5201314999.wav' wave_data,framerate=get_wave_data(file_path) data,count=find_efficient_area(wave_data) wave_analysis_multi(data,framerate)
結果如下
[2, 2, 8, 1, 6, 1, 4, 3, 9, 9]
家裏領導很肉麻地錄的是5201314999,結果出來效果真的很垃圾啊……不過想想這訓練數據這麼少,並且採用了那麼樸素的判定方法,正確率高了才見鬼了好吧!!!
不過人的智慧是無窮的,通過改變數據移動平均的次數以及一個小trick
#A little trick train_data[1][0]=train_data[4][0]=train_data[7][0]=(train_data[1][0]+train_data[4][0]+train_data[7][0])/3 train_data[3][0]=train_data[6][0]=train_data[9][0]=(train_data[3][0]+train_data[6][0]+train_data[9][0])/3 train_data[1][1]=train_data[2][1]=train_data[3][1]=(train_data[1][1]+train_data[2][1]+train_data[3][1])/3 train_data[7][1]=train_data[8][1]=train_data[9][1]=(train_data[7][1]+train_data[8][1]+train_data[9][1])/3 train_data[2][0]=train_data[5][0]=train_data[8][0]=train_data[0][0]=(train_data[1][0]+train_data[3][0])/2 train_data[4][1]=train_data[5][1]=train_data[6][1]=(train_data[1][1]+train_data[7][1])/2
再次進行預測,結果爲
[5, 2, 0, 1, 3, 1, 4, 9, 9, 9]
蛤蛤蛤,很厲害有沒有?!
屁啦,沒有普適性,對預測能力並沒有質的提高,這個trick可以用,但是對數據進行移動平均的次數真的對預測結果有很大影響。
好啦,至此,我們第一個語音號碼識別程序的demo已經出來了,雖然正確率只有60%左右,但這是個十分類問題啊喂,比你亂猜還是靠譜多了好嗎!!!當然,提高的空間還很大,大家如果有興趣的可以一起討論討論,比如我覺得可以對從頻譜中得到的數據依照訓練數據做個拉普拉斯平滑~
這周花了很多時間在這個小玩意兒上,課題的東西一點也沒幹,感覺週一組會老闆回來要GoDie了,好慌啊,先歇個週六壓壓驚,週末愉快各位,蛤蛤蛤~~~
---------------------