我有朋友叫老張,他是做傳統(tǒng)單片機(jī)的。他經(jīng)常搞一些簡(jiǎn)單的硬件發(fā)明,比如他家窗簾的打開和關(guān)閉,就是他親自設(shè)計(jì)的電路板控制的。他布線很亂,從電視柜的插座直接扯電線到陽臺(tái)的窗戶,電線就像蜘蛛網(wǎng)一樣縱橫交錯(cuò)。
老張的媳婦是個(gè)強(qiáng)迫癥、完美主義者,沒法忍受亂扯的電線。但是,她又害怕影響自己的丈夫成為大發(fā)明家。于是,她就順著電線綁上綠蘿,這樣就把電線隱藏起來了,絲毫不影響房間的美觀。
上周,老張邀請(qǐng)我去他家里,參觀這個(gè)電動(dòng)窗簾。老張很激動(dòng),趕緊拿來凳子,站在凳子上,用拖把桿去戳一個(gè)紅色的按鈕。他激動(dòng)地差點(diǎn)滑下來。我問他,你為什么要把開關(guān)放那么偏僻。老張說,是為了避免3歲的兒子頻繁地按開關(guān)。老張站在凳子上,像跳天鵝湖一樣,墊著腳尖努力去戳按鈕,給我演示窗簾的開和關(guān)。
我趕緊夸他的窗簾非常棒。我不知道,就這種情況,如果他摔下來,從法律上講我有沒有連帶責(zé)任。
我連忙轉(zhuǎn)移注意力:哎,你家的綠蘿長(zhǎng)得挺好。我不自覺地摸了一下葉子,感覺手指頭麻了一下。我去!老張你家綠蘿帶刺!
老張說,不是帶刺,可能是帶電。
我并沒有太驚奇,因?yàn)槲艺J(rèn)識(shí)老張10多年了。我們先是高中3年同學(xué),后來4年大學(xué)同學(xué),后來又2年同事。在老張這里,什么奇怪的事情都可能發(fā)生。我還記得,高中時(shí),他晚上抱著半個(gè)西瓜插著勺子,去廁所蹲坑,上下同步進(jìn)行。他絲毫沒有尷尬的意思,并稱之為豁達(dá)。
我回憶起往事,痛苦不堪。今天被綠蘿電了,也會(huì)成為往事。我起身準(zhǔn)備要走。老張說,我搞了好多年嵌入式板子,你知道為什么一直沒有起色?
我說,什么?你對(duì)電路板還能起色?!
老張說,不。我意思是說,我搞嵌入式工作這么多年,一直是平平無奇。主要原因,我感覺就是沒有結(jié)合高新技術(shù),比如人工智能。而你,現(xiàn)在就在搞人工智能。
我說,我能讓你起色嗎?
老張說,是的。我研究的巡邏小車,都是靠無線電控制的,我一按,就發(fā)送個(gè)電波。你幫我搞一個(gè)語音控制的。我一喊:跑哇!它就往前走。我一喊:站??!它就停止。我一喊:往左,往右!它就轉(zhuǎn)彎。
我說,這個(gè)不難。但是,這有用嗎?
老張說,是的,這很起色。據(jù)我所知,我們車間里,還沒有人能想出來我這個(gè)想法。而我,馬上就能做出來了。
我說,可以。你這個(gè)不復(fù)雜。但是,我也有個(gè)要求。那就是,我把你這個(gè)事情,寫到博客里,也讓網(wǎng)友了解一下,可以嗎?
老張說:沒問題!
人工智能有三大常用領(lǐng)域,視覺、文字和語音。前兩者,我寫過很多。這次,開始對(duì)語音領(lǐng)域下手。
以下代碼,環(huán)境要求 TensorFlow 2.6(2021年10之后版) + python 3.9。
我們所看到的,聽到的,都是數(shù)據(jù)。體現(xiàn)到計(jì)算機(jī),就是數(shù)字。
比如,我們看到下面這張像素圖,是4*4的像素點(diǎn)。圖上有兩個(gè)紫色的點(diǎn)。你看上去,是這樣。
其實(shí),如果是黑白單通道,數(shù)據(jù)是這樣:
[[255, 255, 255, 255],
[255, 131, 255, 255],
[255, 131, 255, 255],
[255, 255, 255, 255]]
如果是多通道,也就是彩色的,數(shù)據(jù)是下面這樣:
[[[255, 255,255],[255, 255, 255],[255, 255, 255],[255, 255, 255]],
[[255, 255, 255],[198, 102, 145],[255, 255, 255],[255, 255, 255]],
[[255, 255, 255],[198, 102, 145],[255, 255, 255],[255, 255, 255]],
[[255, 255, 255],[255, 255, 255],[255, 255, 255],[255, 255, 255]]]
我們看到,空白都是255
。只是那2
個(gè)紫色的格子有變化。彩色值是[198, 102, 145]
,單色值是131
??梢哉f,一切皆數(shù)據(jù)。
語音是被我們耳朵聽到的。但是,實(shí)際上,它也是數(shù)據(jù)。
你要不信,我們來解析一個(gè)音頻文件。
tensorflow tf audio_binary tf.io.read_file() audio, rate tf.audio.decode_wav(contentsaudio_binary)
使用tf.audio.decode_wav
可以讀取和解碼音頻文件,返回音頻文件的數(shù)據(jù)audio
和采樣率rate
。
其中,解析的數(shù)據(jù)audio
打印如下:
<tf.Tensor: shape=(11146, 1), dtype=float32, numpy=
array([[-0.00238037],
[-0.0038147 ],
[-0.00335693],
...,
[-0.00875854],
[-0.00198364],
[-0.00613403]], dtype=float32)>
上面的數(shù)據(jù),形式是[[x]]
。這表示,這段音頻是單聲道(類比黑白照片)。x
是聲道里面某一時(shí)刻具體的數(shù)值。其實(shí)它是一個(gè)波形,我們可以把它畫出來。
matplotlib.pyplot plt plt.plot(audio) plt.show()
這個(gè)波的大小,就是推動(dòng)你耳朵鼓膜的力度。
上面的圖是11146個(gè)采樣點(diǎn)的形狀。下面,我們打印10個(gè)點(diǎn)的形狀。這10個(gè)點(diǎn)就好比是推了你耳朵10下。
matplotlib.pyplot plt plt.plot(audio[:]) plt.show()
至此,我們可以看出,音頻實(shí)際上就是幾組帶有序列的數(shù)字。
要識(shí)別音頻,就得首先分析音頻數(shù)據(jù)的特征。
每個(gè)個(gè)體都有自己的組成成分,他們是獨(dú)一無二的。就像你一樣。
但是,多個(gè)個(gè)體之間,也有相似之處。就像我們都是程序員。于是,我們可以用一種叫“譜”的東西來描述一個(gè)事物。比如,辣子雞的菜譜。正是菜譜描述了放多少辣椒,用哪個(gè)部位的雞肉,切成什么形狀。這才讓我們看到成品時(shí),大喊一聲:辣子雞,而非糖醋魚。
聲音也有“譜”,一般用頻譜
描述。
聲音是振動(dòng)發(fā)生的,這個(gè)振動(dòng)的頻率是有譜的。
把一段聲音分析出來包含哪些固定頻率,就像是把一道菜分析出來由辣椒、雞肉、豆瓣醬組成。再通過分析食材,最終我們判斷出來是什么菜品。
聲音也是一樣,一段聲波可以分析出來它的頻率組成。如果想要詳細(xì)了解“頻譜”的知識(shí),我有一篇萬字長(zhǎng)文詳解了《傅里葉變換》??赐晷枰雮€(gè)小時(shí)。
我上面說的,谷歌公司早就知道了。因此,他們?cè)?code style="color: rgb(0, 0, 0);font-family: Menlo, Monaco, Consolas, "Courier New", monospace;background-color: rgb(240, 240, 240);border-radius: 3px;padding-top: 0.2em;padding-bottom: 0.2em;font-size: 0.85em !important;">TensorFlow框架中,早就內(nèi)置了獲取音頻頻譜的函數(shù)。它采用的是短時(shí)傅里葉變換stft
。
waveform tf.squeeze(audio, axis) spectrogram tf.signal.stft(waveform, frame_length, frame_step)
我們上面通過tf.audio.decode_wav
解析了音頻文件,它返回的數(shù)據(jù)格式是[[-0.00238037][-0.0038147 ]]
這種形式。
你可能好奇,它為什么不是[-0.00238037, -0.0038147 ]
這種形式,非要外面再套一層。回憶一下,我們的紫色像素的例子,一個(gè)像素點(diǎn)表示為[[198, 102, 145]]
,這表示RGB三個(gè)色值通道描述一個(gè)彩色像素。其實(shí),這里也一樣,是兼容了多聲道的情況。
但是,我們只要一個(gè)通道就好。所以需要通過tf.squeeze(audio, axis=-1)
對(duì)數(shù)據(jù)進(jìn)行降一個(gè)維度,把[[-0.00238037][-0.0038147 ]]
變?yōu)?code style="color: rgb(0, 0, 0);font-family: Menlo, Monaco, Consolas, "Courier New", monospace;background-color: rgb(240, 240, 240);border-radius: 3px;padding-top: 0.2em;padding-bottom: 0.2em;font-size: 0.85em !important;">[-0.00238037, -0.0038147 ]。這,才是一個(gè)純粹的波形。嗯,這樣才能交給傅里葉先生進(jìn)行分析。
tf.signal.stft
里面的參數(shù),是指取小樣的規(guī)則。就是從總波形里面,每隔多久取多少小樣本進(jìn)行分析。分析之后,我們也是可以像繪制波形一樣,把分析的頻譜結(jié)果繪制出來的。
看不懂上面的圖沒有關(guān)系,這很正常,非常正常,極其正常。因?yàn)?,我即便用了一萬多字,50多張圖,專門做了詳細(xì)的解釋。但是依然,有20%左右的讀者還是不明白。
不過,此時(shí),你需要明白,一段聲音的特性是可以通過科學(xué)的方法抽取出來的。這,就夠了。
把特性抽取出來之后,我們就交給人工智能框架去訓(xùn)練了。
上面,我們已經(jīng)成功地獲取到一段音頻的重要靈魂:頻譜。
下面,就該交給神經(jīng)網(wǎng)絡(luò)模型去訓(xùn)練了。
在正式交給模型之前,其實(shí)還有一些預(yù)處理工作要做。比如,給它切一切毛邊,疊一疊,整理成同一個(gè)形狀。
正如計(jì)算機(jī)只能識(shí)別0和1,很多框架也是只能接收固定的結(jié)構(gòu)化數(shù)據(jù)。
舉個(gè)簡(jiǎn)單的例子,你在訓(xùn)練古詩(shī)的時(shí)候,有五言的和七言的。比如:“床前明月光”和“一頓不吃餓得慌”兩句。那么,最終都需要處理成一樣的長(zhǎng)短。要么前面加0,要么后邊加0,要么把長(zhǎng)的裁短。總之,必須一樣長(zhǎng)度才行。
床前明月光〇〇
一頓不吃餓得慌
蜀道難〇〇〇〇
那么,我們的音頻數(shù)據(jù)如何處理呢?我們的音波數(shù)據(jù)經(jīng)過短時(shí)傅里葉變換之后,格式是這樣的:
<tf.Tensor: shape=(86, 129, 1), dtype=float32, numpy=
array([[[4.62073803e-01],
...,
[2.92062759e-05]],
[[3.96062881e-01],
[2.01166332e-01],
[2.09505502e-02],
...,
[1.43915415e-04]]], dtype=float32)>
這是因?yàn)槲覀?1146長(zhǎng)度的音頻,經(jīng)過tf.signal.stft
的frame_step=128
分割之后,可以分成86份。所以我們看到shape=(86, 129, 1)
。那么,如果音頻的長(zhǎng)度變化,那么這個(gè)結(jié)構(gòu)也會(huì)變。這樣不好。
因此,我們首先要把音頻的長(zhǎng)度規(guī)范一下。因?yàn)椴蓸勇适?code style="color: rgb(0, 0, 0);font-family: Menlo, Monaco, Consolas, "Courier New", monospace;background-color: rgb(240, 240, 240);border-radius: 3px;padding-top: 0.2em;padding-bottom: 0.2em;font-size: 0.85em !important;">16000,也就是1秒鐘記錄16000
次音頻數(shù)據(jù)。那么,我們不妨就拿1秒音頻,也就是16000
個(gè)長(zhǎng)度,為一個(gè)標(biāo)準(zhǔn)單位。過長(zhǎng)的,我們就裁剪掉后面的。過短的,我們就在后面補(bǔ)上0。
我說的這一系列操作,反映到代碼上,就是下面這樣:
waveform tf.squeeze(audio, axis) input_len waveform waveform[:input_len] zero_padding tf.zeros([] tf.shape(waveform),dtypetf.float32) waveform tf.cast(waveform, dtypetf.float32) equal_length tf.concat([waveform, zero_padding], ) spectrogram tf.signal.stft(equal_length, frame_length, frame_step) spectrogram tf.(spectrogram) spectrogram spectrogram[..., tf.newaxis]
這時(shí)候,再來看看我們的頻譜數(shù)據(jù)結(jié)構(gòu):
<tf.Tensor: shape=(124, 129, 1), dtype=float32, numpy=
array([[[4.62073803e-01],
...,
[2.92062759e-05]],
...
[0.00000000e+00],
...,
[0.00000000e+00]]], dtype=float32)>
現(xiàn)在,不管你輸入任何長(zhǎng)短的音頻,最終它的頻譜都是shape=(124, 129, 1)
。從圖上我們也可以看出,不足的就算后面補(bǔ)0,也得湊成個(gè)16000長(zhǎng)度。
下面,真的要開始構(gòu)建神經(jīng)網(wǎng)絡(luò)了。
依照老張的要求……我現(xiàn)在不想提他,因?yàn)槲业氖种副痪G蘿電的還有點(diǎn)發(fā)麻。
依照要求……他要四種命令,分別是:前進(jìn)、停止、左轉(zhuǎn)、右轉(zhuǎn)。那么,我就搞了四種音頻,分別放在對(duì)應(yīng)的文件夾下面。
從文件夾讀取數(shù)據(jù)、將輸入輸出結(jié)對(duì)、按照比例分出數(shù)據(jù)集和驗(yàn)證集,以及把datasets
劃分為batch
……這些操作,在TensorFlow
中已經(jīng)很成熟了。而且,隨著版本的更新,越來越成熟。體現(xiàn)在代碼上,就是字?jǐn)?shù)越來越少。此處我就不說了,我會(huì)把完整代碼上傳到github
,供諸君參考。
下面,我重點(diǎn)說一下,本例子中,實(shí)現(xiàn)語音分類,它的神經(jīng)網(wǎng)絡(luò)的結(jié)構(gòu),以及模型訓(xùn)練的配置。
tensorflow tf tensorflow.keras layers tensorflow.keras models model models.Sequential([ layers.Input(shape (, , )), layers.Resizing(, ), layers.Normalization(), layers.Conv2D(, , activation), layers.Conv2D(, , activation), layers.MaxPooling2D(), layers.Dropout(), layers.Flatten(), layers.Dense(, activation), layers.Dropout(), layers.Dense(), ]) model.( optimizertf.keras.optimizers.Adam(), losstf.keras.losses.SparseCategoricalCrossentropy(from_logits), metrics[] )
其實(shí),我感覺人工智能應(yīng)用層面的開發(fā),預(yù)處理和后處理比較難。中間的模型基本上都是有固定招式的。
第1層layers.Input(shape= (124, 129, 1))
叫輸入層,是訓(xùn)練樣本的數(shù)據(jù)結(jié)構(gòu)。就是我們上一節(jié)湊成16000
之后,求頻譜得出的(124, 129)
這個(gè)結(jié)構(gòu)。
最后一層layers.Dense(4)
,是輸出層。我們搞了“走”,“?!?,“左”,“右”4個(gè)文件夾分類,最終結(jié)果是4類,所以是4
。
頭尾基本固定后,這個(gè)序列Sequential
就意味著:吃音頻文件,然后排出它是4個(gè)分類中的哪一種。
那么中間我們就可以自己操作了。Normalization
是歸一化。Conv2D
是做卷積。MaxPooling2D
是做池化。Dropout(0.25)
是隨機(jī)砍掉一定比例(此處是25%)的神經(jīng)網(wǎng)絡(luò),以保證其健壯性。快結(jié)束時(shí),通過Flatten()
將多維數(shù)據(jù)拉平為一維數(shù)據(jù)。后面給個(gè)激活函數(shù),收縮神經(jīng)元個(gè)數(shù),準(zhǔn)備降落。最后,對(duì)接到Dense(4)
。
這就實(shí)現(xiàn)了,將前面16000
個(gè)音頻采樣點(diǎn),經(jīng)過一系列轉(zhuǎn)化后,最終輸出為某個(gè)分類。
最后,進(jìn)行訓(xùn)練和保存模型。
model create_model() cp_callback tf.keras.callbacks.ModelCheckpoint(filepath, save_weights_only, save_best_only) history model.fit( train_ds, validation_dataval_ds, epochs, callbacks[cp_callback] )
filepath='model/model.ckpt'
表示訓(xùn)練完成后,存儲(chǔ)的路徑。save_weights_only=True
只存儲(chǔ)權(quán)重?cái)?shù)據(jù)。save_best_only=True
意思是只存儲(chǔ)最好的訓(xùn)練的結(jié)果。調(diào)用訓(xùn)練很簡(jiǎn)單,調(diào)用model.fit
,傳入訓(xùn)練集、驗(yàn)證集、訓(xùn)練輪數(shù)、以及訓(xùn)練回調(diào)就可以啦。
上一節(jié)中,我們指定了模型的保存路徑,調(diào)用model.fit
后會(huì)將結(jié)果保存在對(duì)應(yīng)的路徑下。這就是我們最終要的產(chǎn)物:
我們可以加載這些文件,這樣就讓我們的程序具備了多年功力??梢詫?duì)外來音頻文件做預(yù)測(cè)。
model create_model() os.path.exists(): model.load_weights() labels [, , , ] audio get_audio_data() audios np.array([audio]) predictions model(audios) index np.argmax(predictions[]) (labels[index])
上面代碼中,先加載了歷史模型。然后,將我錄制的一個(gè)mysound.wav
文件進(jìn)行預(yù)處理,方式就是前面說的湊成16000
,然后通過短時(shí)傅里葉解析成(124, 129)
結(jié)構(gòu)的頻譜數(shù)據(jù)。這也是我們訓(xùn)練時(shí)的模樣。
最后,把它輸入到模型。出于慣性,它會(huì)順勢(shì)輸出這是'go'
分類的語音指令。盡管這個(gè)模型,從來沒有見過我這段動(dòng)聽的嗓音。但是它也能識(shí)別出來,我發(fā)出了一個(gè)包含'go'
聲音特性的聲音。
以上,就是利用TensorFlow
框架,實(shí)現(xiàn)聲音分類的全過程。
再次提醒大家:要求TensorFlow 2.6(2021年10之后版) + python 3.9。因?yàn)?,里面用了很多新特性。舊版本是跑不通的,具體體現(xiàn)在TensorFlow各種找不到層。
音頻分類項(xiàng)目開源地址:https://github.com/hlwgy/sound
我?guī)е晒フ依蠌?。老張沉默了一?huì)兒,不說話。
我說,老張啊,你就說吧。你不說話,我心里沒底,不知道會(huì)發(fā)生啥。
老張說,兄弟啊,其實(shí)語音小車這個(gè)項(xiàng)目,沒啥創(chuàng)意。我昨天才知道,我們車間老王,三年前,自己一個(gè)人,就做出來過了。說完,老張又沉默了。
我安慰他說,沒關(guān)系的。這個(gè)不行,你就再換一個(gè)唄。
老張猛然抬起頭,眼睛中閃著光,他說:兄弟,宇宙飛船相關(guān)的軟件,你搞得定嗎?!火星車也行。
我不緊不忙地關(guān)閉服務(wù),并把電腦收進(jìn)包里。
我穿上鞋,然后拿上包。打開門,回頭跟老張說了一句:兄弟,三個(gè)月內(nèi),我們先不聯(lián)系了吧。
我是IT男,帶你從IT角度看世界。
聯(lián)系客服