基於MCRA-OMLSA的語音降噪(三):實現(續)
上篇文章(基於MCRA-OMLSA的語音降噪(二):實現 )講了基於MCRA-OMLSA的語音降噪的軟體實現。本篇繼續講,主要講C語言下怎麼對數學庫里的求平方根(sqrt())、求自然指數(exp())、求自然對數(log())的函數做替換。
1,求平方根
求平方根最常用的方法是牛頓迭代法。下圖是y = f(x)的曲線,當f(x) =0時的值(α)就是該方程的根。
可以通過多次迭代逼近的方法求得這個根,原理如下:
任取一個x0,這個值對應的y值為f(x0)。在x0處畫y = f(x)的切線,與x軸交點為x1。根據斜率的定義,在x0處的斜率如下:
又斜率是函數的一次導數f』(x0),所以
可求得
基於x1再畫一條切線,運用上面的求法得到與x軸交點為x2,一直迭代下去可得x3,…….,xn,xn+1等,從而求得xn+1與xn的關係如下式:
這些值會向方程的根α無限逼近。當| xn+1 – xn| < ε (ε是事先設定的一個精度)時就停止迭代,這時xn+1就是方程f(x) = 0的根。
具體到求平方根,x2 = v (v是一個大於等於0的實數值),x2 – v = 0,令f(x) = x2 – v ,得到f』(x) = 2x,把f(x)和f』(x)帶入上式得到
處理後得到
上式就是求平方根的迭代數學表達式。設定好精度後就可求出平方根,與C數學庫的sqrt()結果比較,值是非常接近的。
2,求自然指數
求自然指數是基於論文《指數函數ex的快速計算方法》。用這個方法前得搞清楚浮點數的二進位存儲表示方法,浮點數包括單精度浮點數(float)和雙精度浮點數(double)。先看float的二進位存儲表示,float的搞明白了,double的類似,也好懂。
float佔4個位元組,32比特,存儲格式如下圖:
其中第0-22位共23位表示尾碼M,第23-31位共8位表示階碼E,第31位共1位表示符號位S。符號位好理解,0表示正數,1表示負數。以0.625為例,是正數,所以符號位是0。至於階碼和尾碼,方便理解,依舊以0.625為例。0.625 = 1.25 * 2-1 = (1 + 0.25) * 2-1 = (1 + x) * 2y,其中x表示小數部分,y表示指數。
階碼E = y + 127 的二進位表示。這裡y = -1,所以E = -1 + 127 = 126,表示成二進位就是1111110,用8位二進位表示就是01111110。
尾碼M = x * 223的二進位表示。這裡x = 0.25,所以0.25 * 223= 2097152,用23位的二進位表示,M = 01000000000000000000000。
最終0.625的二進位存儲表示如下圖:
double佔8個位元組,64比特,存儲格式如下圖:
它的二進位表示跟float類似,不同的是階碼E = y + 1023。依舊以0.625為例,
階碼E = -1 + 1023 = 1022,表示成二進位就是1111111110,用11位二進位表示就是01111111110。
尾碼M = x * 252的二進位表示。這裡x = 0.25,所以0.25 * 252= 1125899906842624,用52位的二進位表示,M = 0100000000000000000000000000000000000000000000000000。符號位還是0。最終0.625的二進位存儲表示如下圖:
浮點數的存儲機制搞明白了,現在看怎麼求自然指數。求自然指數的傳統方法是用指數函數的冪級數展開式,如下式:
該論文用了一種計算速度更快的方法。下面具體看怎麼做的。為簡單起見,令x > 0,當x < 0時,只要用1除就可以了。
令 y = ex,所以 。log2e是個定值1.4426950408889634,這裡令為a,即a = log2e = 1.4426950408889634。從而log2y = ax,即 y = 2ax。令n是ax的整數部分,即 n = [ax],從而ax的小數部分為ax – n,令其為D,即D = ax – n。所以 ax = n + D,y = 2ax = 2n+D = 2D2n 。因為 0 < D < 1,所以1 < 2D < 2,從而可以寫成1 + α(0 < α < 1)的形式,所以 y = (1 + α)2n。對標C數學庫里exp()用的是double型,這裡也用double型。根據上文double型的二進位存儲形式,可知n+1023就是階碼,α*252就是尾碼。n很好求,ax取整就可以了。下面看α怎麼求。α = 2D – 1,2D求出,α就有了。
令p = 2D,從而 。令x00 = Dln2,有p = ex00。因為 0 < D < 1,又ln2 = 0.69314718056,所以 0 < x00 < 0.69314718056。此時若直接用ex00的冪級數展開式求p,計算時間還很長,若適當選取x0和Δx,使得Δx << 1,且 x00 = x0 + Δx,則有 p = ex0 + Δx = ex0eΔx。可分別求ex0和eΔx,然後再相乘就得到p。論文中用查表法求ex0,用冪級數展開法求eΔx。先看怎麼求ex0。將x00轉換為16進位數表示,改寫成x00 = 0.q1q2q3q4q5n = 0.q1q2q3 + 0.000q4q5n = x0 + Δx,其中x0 = 0.q1q2q3 = q1 * 16-1 + q2 * 16-2 + q3 * 16-3,Δx = 0.000q4q5n = q4 * 16-4 + q5 * 16-5 + …。所以ex0 = eq1 * 16-1 + q2 * 16-2 + q3 * 16-3 = eq1 * 16-1eq2 * 16-2eq3 * 16-3。因為x0 < x00 < 0.69314718056 < 0.75 = 12/16,所以q1的取值範圍是[0, 11],q2的取值範圍是[0, 15],q3的取值範圍是[0, 15]。根據qx的有限個不同取值將eq1 * 16-1 、eq2 * 16-2 和eq3 * 16-3 分別預先算出做成表,計算時通過查表得到三個相應的值,再將這三個值相乘就得到ex0的值了。再來看怎麼求eΔx。0 < Δx = 0.000q4q5n < 16-3 = 1/4096 << 1,用冪級數展開式求eΔx只要取前面4項即可保證精度了,所以用冪級數展開式求eΔx。
下面給出軟體實現時的步驟:
1) 定義結構體如下,其中s放符號位,e放階碼,m放尾碼,dat是自然指數運算的返回值。
typedef union {
double dat;
struct{
unsigned long m:52;
unsigned e:11;
unsigned s:1;
}jw;
}FREXP;
2) 求符號位和階碼。因為自然指數均大於0,所以符號位均為0。對ax取整加1023就可得階碼。
3) 求尾碼。通過查表法求ex0,通過冪級數展開式求eΔx,p = ex0eΔx即可求得,α = p – 1也可求得。尾碼 =α*252就得到了。
4) 返回dat值就是自然指數的結果了。
3,求自然對數
自然對數是自然指數的逆運算,y = ex,根據y求x。給定一個y,通過上面定義的結構體FREXP可以得到它的階碼E和尾碼M(Mi表示每一位上的值),表示如下式:
又 y = ex,所以
上式兩邊取自然對數,得到:
即:
其中
b好求,主要看a怎麼求。a是ln(1 + x)的形式,泰勒展開式如下:
所以可以用泰勒展開式求b。a和b都求出來了,一個數y的自然對數x = a + b就求出來了。
下面給出軟體實現時的步驟(與自然指數共用結構體):
1) 得到階碼E和尾碼Mi
2) 根據階碼E求得b
3) 根據尾碼Mi利用泰勒展開式得到a
4) 將a和b相加就得到自然對數