老張說:快過年了,搞個AI作曲,用TensorFlow訓練midi檔案
theme: devui-blue
一、老張需求:AI作曲營造新年氛圍
我有一個搞嵌入式的朋友老張,全名叫張三。是真的,他的身份證上就叫張三。據說,出生時,他父母準備了一堆名字。但是兩人各執一派,大打出手。吵鬧聲引來隔壁李大爺:實在不行叫張三,我叫李四,也活得挺好。於是,互不相讓的一對年輕夫妻,給孩子上了戶口,起名叫:張三。
老張長大後,一直不和李大爺說話。李大爺告訴小張三:當時,如果,不是我衝進去,急中生智給你定下名字,你可能就沒命了。聽到這裡,小張三才稍微得以釋懷,並且給李大爺磕了個頭,以示感謝。李大爺說:客氣了,我起名字時,我父母也一樣,最後還是你爺爺給起的李四,咱兩家是世交。
我打斷了老張:快說,找我麼事?
老張說,其實我一直覺得,我不是普通的凡人。
“嗯,你特別的煩人”。
老張說,是平凡的“凡”。我今年40歲了,哎,你知道嗎?我前天剛過完40歲生日,買了一個大蛋……
“說事情!”
老張說,兄弟,幫幫忙吧,我想搞一個發明,需要你人工智慧方向的幫助。
我說,啊,你又搞發明?這次什麼想法。
老張說,現在快過年了,我想搞一個仿老式留聲機的盒子,安裝到餐廳裡面。
機器採用自助模式,只要顧客付錢,盒子就會自動播放一段,由AI生成的原創新年音樂,給他們送去祝福,掃碼還可以下載儲存。
我問老張,你有銷售渠道嗎?
老張說,放心吧,餐廳我都談好了。他們提供場地和網路,我們只承擔電費就行。
我沉思了一會,問老張:老張啊,你認識一個“耿”姓做手工的人嗎?
老張說,相信我,我絕對不認識他,他的那些發明,不是沒用,是真沒用。
我點了點頭:那就好,我支援你!
二、midi格式:行動式音樂描述檔案
想讓AI
學會作曲,首先要找到一批音樂樣本,讓它學。
AI
作曲,遵循“種瓜得瓜,種豆得豆”的原則:你給它訓練什麼風格的樣本,它最終就會生成什麼風格的音樂。
因此,我們需要找一些輕鬆活潑的音樂,這適合新年播放。
音樂檔案的格式,我們選擇MIDI
格式。MIDI
的全稱是:Musical Instrument Digital Interface
,翻譯成中文就是:樂器數字介面。
這是一種什麼格式?為什麼會有這種格式呢?
話說,隨著計算機的普及,電子樂器也出現了。電子樂器的出現,極大地節省了成本,帶來了便利。
基本上有一個電子樂器,世間的樂器就都有了。
這個按鈕是架子鼓,那個按鈕是薩克斯。而在此之前,你想要發出這類聲音,真的得敲架子鼓或者吹薩克斯管。
而且還有更為放肆的事情。你想用架子鼓一秒敲五下,得有專業的技能。但是用電子樂器,一秒敲五十下也毫不費力,因為程式就給搞定了。
這些新生事物的出現,常常讓老藝術家們口吐鮮血。
電子樂器既然可以演奏音樂,那麼就有樂譜。這樂譜還得有標準,因為它得在所有電子樂器中都起作用。這個“計算機能理解的樂譜”,就是MIDI
格式。
下面我們就來解析一下MIDI
檔案。看看它的結構是怎麼樣的。
我找到一個機器貓(哆啦A夢)的主題曲,採用python
做一下解析:
```python import pretty_midi
載入樣本檔案
pm = pretty_midi.PrettyMIDI("jqm.midi")
迴圈樂器列表
for i, instrument in enumerate(pm.instruments): instrument_name = pretty_midi.program_to_instrument_name(instrument.program) print(i, instrument_name) # 輸出樂器名稱 ```
這個音樂,相信大家都很熟悉,就是:哦、哦、哦,哆啦A夢和我一起,讓夢想發光……
通過pretty_midi
庫載入MIDI
檔案,獲取它的樂器列表pm.instruments
,列印如下:
Acoustic Grand Piano(原聲大鋼琴)、Glockenspiel(鋼片琴)、String Ensemble(絃樂合奏) 、
Muted Trumpet(悶音小號)、Trombone(長號)、
Electric Bass(電貝斯)、Acoustic Guitar(原聲吉他)、
Flute(長笛)、Acoustic Grand Piano(原聲大鋼琴)、
Harmonica(口琴)、Vibraphone(電顫琴)、Bagpipe(蘇格蘭風笛)、Marimba(馬林巴琴)……
我們看到,短短一個片頭曲,就動用了近20
種樂器。如果不是專門分析它,我們還真的聽不出來吶。
那麼,每種樂器的音符可以獲取到嗎?我們來試試:
``` python
承接上個程式碼片段,假設選定了樂器instrument
for j, note in enumerate(instrument.notes): # 音高轉音符名稱 note_name = pretty_midi.note_number_to_name(note.pitch) info = ("%s:%.2f->%.2f")%(note_name, note.start, note.end) ```
列印如下:
Acoustic Grand Piano
F#3:1.99->2.04 F#2:1.98->2.06 E2:1.99->2.07 C2:1.98->2.08 F#3:2.48->2.53 F#2:2.48->2.56 F#3:2.98->3.03 F#2:2.98->3.06 ……
通過獲取instrument
的notes
,可以讀到此樂器的演奏資訊。包含:pitch
音高,start
開始時間,end
結束時間,velocity
演奏力度。
| 名稱 | pitch | start | end | velocity | | --- | --- | --- | --- | --- | | 示例 | 24 | 1.98 | 2.03 | 82 | | 解釋 | 音高(C1、C2) | 開始時間 | 結束時間 | 力度 | | 範圍 | 128個音高 | 單位為秒 | 單位為秒 | 最高100 |
上面的例子中,F#3:1.99->2.04
表示:音符F#3
,演奏時機是從1.99
秒到2.04
秒。
如果把這些資料全都展開,其實挺壯觀的,應該是如下這樣:
其實,MIDI
檔案對於一首樂曲來說,就像是一個程式的原始碼,也像是一副藥的配方。MIDI
檔案裡,描述了樂器的構成,以及該樂器的演奏資料。
這類檔案要比WAV
、MP3
這些波形檔案小得多。一段30
分鐘鋼琴曲的MIDI
檔案,大小一般不超過100KB
。
因此,讓人工智慧去學習MIDI
檔案,並且實現自動作曲,這是一個很好的選擇。
三、實戰:TensorFlow實現AI作曲
我在datasets
目錄下,放了一批節奏歡快的MIDI
檔案。
這批檔案,除了節奏歡快適合在新年播放,還有一個特點:全部是鋼琴曲。也就是說,如果列印他的樂器的話,只有一個,那就是:Acoustic Grand Piano
(原聲大鋼琴)。
這麼做降低了樣本的複雜性,僅需要對一種樂器進行訓練和預測。同時,當它有朝一日練成了AI
作曲神功,你也別妄想它會鑼鼓齊鳴,它仍然只會彈鋼琴。
多樂器的複雜訓練當然可行。但是目前在業內,還沒有足夠的資料集來支撐這件事情。
開搞之前,我們必須得先通盤考慮一下。不然,我們都不知道該把資料搞成麼個形式。
AI
作曲,聽起來很高階。其實跟文字生成、詩歌生成,沒有什麼區別。我之前講過很多相關的例子《NLP實戰:基於LSTM自動生成原創宋詞》《NLP實戰:基於GRU的自動對春聯》《NLP實戰:基於RNN的自動藏頭詩》。也講過很多關於NLP的知識點《NLP知識點:Tokenizer分詞器》《NLP知識點:文字資料的預處理》等。如果感興趣,大家可以先預習一下。不看也不要緊,後面我也會簡單描述,但深度肯定不如上面的專項介紹。
利用RNN
,生成莎士比亞文集,是NLP
領域的HelloWorld
入門程式。那麼,AI
作曲,只不過是引入了音樂的概念。另外,在出入引數上,維度也豐富了一些。但是,從本質上講,它還是那一套思路。
所有AI
自動生成的模式,基本上都是給定一批輸入值+
輸出值。然後,讓機器去學習,自己找規律。最後,學成之後,再給輸入,讓它自己預測出下一個值。
舉個例子,莎士比亞文集的生成,樣本如下:
First Citizen: Before we proceed any further, hear me speak.
All: Speak, speak.
它是如何讓AI
訓練和學習呢?其實,就是從目前的資料不斷觀察,觀察出現下一個字元的概率。
| 當前 | 下一個 | 經驗值 | | --- | --- | --- | | F | i | ☆ | | Fi | r | ☆ | | Fir | s | ☆ | | Firs | t | ☆☆ | | …… | …… | …… |
F
後面大概率會出現i
。如果現在是Fi
,那麼它的後面該出現r
了。這些,AI
作為經驗記了下了。
這種記錄概率的經驗,在少量樣本的情況下,是無意義的。
但是,當這個樣本變成人類語言庫的時候,那麼這個概率就是語法規範,就是上帝視角。
舉個例子,當我說:冬天了,窗外飄起了__!
你猜,飄起了什麼?是的,窗外飄起了雪。
當AI
分析過人類歷史上,出現過的所有語言之後。當它進行資料分析的時候,最終它會計算出:在人類的語言庫裡,冬天出現飄雪花的情況,要遠遠高於冬天飄落葉的情況。所以,它肯定也會告訴你那個空該填:雪花。
這就是AI
自動作詞、作曲、作畫的本質。它的技術支撐是帶有鏈式的迴圈神經網路(RNN
),資料支撐就是大量成型的作品。
3.1 準備:構建資料集
首先,讀取這些資料,然後把它們加工成輸入input
和輸出output
。
展開一個MIDI
檔案,我們再來看一下原始資料:
Note(start=1.988633, end=2.035121, pitch=54, velocity=82),
Note(start=1.983468, end=2.060947, pitch=42, velocity=78),
Note(start=2.479335, end=2.525823, pitch=54, velocity=82)……
我們可以把前幾組,比如前24
組音符資料作為輸入,然後第25
個作為預測值。後面依次遞推。把這些資料交給AI
,讓它研究去。
訓練完成之後,我們隨便給24
個音符資料,讓它預測第25
個。然後,再拿著2~25
,讓它預測第26
個,以此迴圈往後,連綿不絕。
這樣可以嗎?
可以(能訓練)。但存在問題(結果非所願)。
在使用迴圈神經網路的時候,前後之間要帶有通用規律的聯絡。比如:前面有“冬天”做鋪墊,後面遇到“飄”時,可以更準確地推測出來是“飄雪”。
我們看上面的資料,假設我們忽略velocity
(力度)這個很專業的引數。僅僅看pitch
音高和start
、end
起始時間。其中,音高是128
個音符。它是普遍有規律的,值是1~128
,不會出圈兒。但是這個起始時間卻很隨機,這裡可以是啊1
秒開始,再往後可能就是100
秒開始。
如果,我們只預測2
個音符,結果200
秒的時間出現的概率高。那麼,第二個音符豈不是到等到3
分鐘後再演奏。另外,很顯然演奏是有先後順序的,因此要起止時間遵從隨機的概率分佈,是不靠譜的。
我覺得,一個音符會演奏多久,以及前後音符的時間間距,這兩項相對來說是比較穩定的。他們更適合作為訓練引數。
因此,我們決定把音符預處理成如下格式:
Note(duration=0.16, step=0.00, pitch=54),
Note(duration=0.56, step=0.31, pitch=53),
Note(duration=0.26, step=0.22, pitch=24),
……
duration
表示演奏時長,這個音符會響多久,它等於end-start
。
step
表示步長,本音符距離上一個出現的時間間隔,它等於start2-start1
。
原始資料格式[start,end]
,同預處理後的資料格式[duration,step]
,兩者是可以做到相互轉化的。
我們把所有的訓練集檔案整理一下:
``` python import pretty_midi import tensorflow as tf
midi_inputs = [] # 存放所有的音符 filenames = tf.io.gfile.glob("datasets/*.midi")
迴圈所有midi檔案
for f in filenames:
pm = pretty_midi.PrettyMIDI(f) # 載入一個檔案
instruments = pm.instruments # 獲取樂器
instrument = instruments[0] # 取第一個樂器,此處是原聲大鋼琴
notes = instrument.notes # 獲取樂器的演奏資料
# 以開始時間start做個排序。因為預設是依照end排序
sorted_notes = sorted(notes, key=lambda note: note.start)
prev_start = sorted_notes[0].start
# 迴圈各項指標,取出前後關聯項
for note in sorted_notes:
step = note.start - prev_start # 此音符與上一個距離
duration = note.end - note.start # 此音符的演奏時長
prev_start = note.start # 此音符開始時間作為最新
# 指標項:[音高(音符),同前者的間隔,自身演奏的間隔]
midi_inputs.append([note.pitch, step, duration])
``
上面的操作,是把所有的
MIDI檔案,依照預處理的規則,全部處理成
[pitch, step, duration]格式,然後存放到
midi_inputs`陣列中。
這只是第一步操作。後面我們要把這個樸素的格式,拆分成輸入和輸出的結對。然後,轉化為TensorFlow
框架需要的資料集格式。
``` python seq_length = 24 # 輸入序列長度 vocab_size = 128 # 分類數量
將序列拆分為輸入和輸出標籤對
def split_labels(sequences): inputs = sequences[:-1] # 去掉最後一項最為輸入 # 將音高除以128,便於 inputs_x = inputs/[vocab_size,1.0,1.0] y = sequences[-1] # 擷取最後一項作為輸出 labels = {"pitch":y[0], "step":y[1],"duration":y[2]} return inputs_x, labels
搞成tensor,便於流操作,比如notes_ds.window
notes_ds = tf.data.Dataset.from_tensor_slices(midi_inputs) cut_seq_length = seq_length+1 # 擷取的長度,因為要拆分為輸入+輸出,因此+1
每次滑動一個數據,每次擷取cut_seq_length個長度
windows = notes_ds.window(cut_seq_length, shift=1, stride=1,drop_remainder=True) flatten = lambda x: x.batch(cut_seq_length, drop_remainder=True) sequences = windows.flat_map(flatten)
將25,拆分為24+1。24是輸入,1是預測。進行訓練
seq_ds = sequences.map(split_labels, num_parallel_calls=tf.data.AUTOTUNE) buffer_size = len(midi_inputs) - seq_length
拆分批次,快取等優化
train_ds = (seq_ds.shuffle(buffer_size) .batch(64, drop_remainder=True) .cache().prefetch(tf.data.experimental.AUTOTUNE)) ```
我們先分析split_labels
這個方法。它接收一段序列陣列。然後將其分為兩段,最後1
項作為後段,其餘部分作為前段。
我們把seq_length
定義為24
,從總資料midi_inputs
中,利用notes_ds.window
實現每次取25
個數據,取完了向後移動1
格,再繼續取資料。直到湊不齊25
個數據(drop_remainder=True
意思是不足25
棄掉)停止。
至此,我們就有了一大批以25
為單位的資料組。其實,他們是:1~25
、2~26
、3~27
……
然後,我們再呼叫split_labels
,將其全部搞成24+1
的輸入輸出對。此時資料就變成了:(1~24,25)
、(2~25,26)
……。接著,再呼叫batch
方法,把他們搞成每64
組為一個批次。這一步是框架的要求。
至此,我們就把準備工作做好了。後面,就該交給神經網路訓練去了。
3.2 訓練:構建神經網路結構
這一步,我們將構建一個神經網路模型。它將不斷地由24
個音符觀察下一個出現的音符。它記錄,它思考,它嘗試推斷,它默寫並對照答案。一旦見得多了,量變就會引起質變,它將從整個音樂庫的角度,給出作曲的最優解。
好了,上程式碼:
``` python input_shape = (seq_length, 3) # 輸入形狀 inputs = tf.keras.Input(input_shape) x = tf.keras.layers.LSTM(128)(inputs)
輸出形狀
outputs = {
'pitch': tf.keras.layers.Dense(128, name='pitch')(x),
'step': tf.keras.layers.Dense(1, name='step')(x),
'duration': tf.keras.layers.Dense(1, name='duration')(x),
}
model = tf.keras.Model(inputs, outputs)
``
上面程式碼我們定義了輸入和輸出的格式,然後中間加了個
LSTM`層。
先說輸入。因為我們給的格式是[音高,間隔,時長]
共3
個關鍵指標。而且每24
個音,預測下一個音。所以input_shape = (24, 3)
。
再說輸出。我們最終期望AI
可以自動預測音符,當然要包含音符的要素,那也就是outputs = {'pitch','step','duration'}
。其中,step
和duration
是一個數就行,也就是Dense(1)
。但是,pitch
卻不同,它需要是128
個音符中的一個。因此,它是Dense(128)
。
最後說中間層。我們期望有人能將輸入轉為輸出,而且最好還有記憶。前後之間要能綜合起來,要根據前面的鋪墊,後面給出帶有相關性的預測。那麼,這個長短期記憶網路LSTM
(Long Short-Term Memory)就是最佳的選擇了。
最終,model.summary()
列印結構如下所示:
|Layer (type) | Output Shape | Param | Connected to| | --- | --- | --- | --- | | input (InputLayer) | [(None, 24, 3)] | 0 | [] | | lstm (LSTM) | (None, 128) | 67584 | ['input[0][0]'] | | duration (Dense) | (None, 1) | 129 | ['lstm[0][0]'] | | pitch (Dense) | (None, 128) | 16512 | ['lstm[0][0]'] | | step (Dense) | (None, 1) | 129 | ['lstm[0][0]'] | | Total params: 84,354 | | |
後面,配置訓練引數,開始訓練:
``` python checkpoint_path = 'model/model.ckpt' # 模型存放路徑 model.compile( # 配置引數 loss=loss, loss_weights={'pitch': 0.05,'step': 1.0,'duration':1.0}, optimizer=tf.keras.optimizers.Adam(learning_rate=0.01), )
模型儲存引數
cp_callback = tf.keras.callbacks.ModelCheckpoint( filepath=checkpoint_path ,save_weights_only=True, save_best_only=True)
啟動訓練,訓練50個週期
model.fit(train_ds, validation_data=train_ds
, epochs=50,callbacks=[cp_callback])
``
訓練完成之後,會將模型儲存在
'model/model.ckpt'目錄下。而且,我們設定了只儲存最優的一個模型
save_best_only=True`。
上面有個需要特別說明的地方,那就是在model.compile
中,給損失函式加了一個權重loss_weights
的配置。這是因為,在輸出的三個引數中,pitch
音高的128
分類跨度較大,一旦預測有偏差,就會導致損失函式的值很大。而step
和duration
本身數值就很小,都是0.0x
秒,損失函式的值變化較小。這種不匹配,會導致後兩個引數的變化被忽略,只關心pitch
的訓練。因此需要降低pitch
的權重平衡一下。至於具體的數值,是調試出來的。
出於講解的需要,上面的程式碼僅僅是關鍵程式碼片段。文末我會把完整的專案地址公佈出來,那個是可以執行的。
好了,訓練上50
輪,儲存完結果模型。下面,就該去做預測了。
3.3 預測和播放:實現AI作曲
現在這個模型,已經可以根據24
個音符去推測出下一個音符了。我們來試一下。
``` python
載入模型
if os.path.exists(checkpoint_path + '.index'): model.load_weights(checkpoint_path)
從音符庫中隨機拿出24個音符,當然你也可以自己編
sample_notes = random.sample(midi_inputs, 24) num_predictions = 600 # 預測後面600個
迴圈600次,每次取最新的24個
for i in range(num_predictions):
# 拿出最後24個
n_notes = sample_notes[-seq_length:]
# 主要給音高做一個128分類歸一化
notes = []
for input in n_notes:
notes.append([input[0]/vocab_size,input[1],input[2]])
# 將24個音符交給模型預測
predictions = model.predict([notes])
# 取出預測結果
pitch_logits = predictions['pitch']
pitch = tf.random.categorical(pitch_logits, num_samples=1)[0]
step = predictions['step'][0]
duration = predictions['duration'][0]
pitch, step, duration = int(pitch), float(step), float(duration)
# 將預測值新增到音符陣列中,進行下一次迴圈
sample_notes.append([pitch, step, duration])
``
其實,關鍵程式碼就一句
predictions = model.predict([notes])。根據
24個音符,預測出來下一個音符的
pitch、
step和
duration`。其他的,都是輔助操作。
我們從素材庫裡,隨機生成了24
個音符。其實,如果你懂聲樂,你也可以自己編24
個音符。這樣,起碼能給音樂定個基調。因為,後面的預測都是根據前面特徵來的。當然,也可以不是24
個,根據2
個生成1
個也行。那前提是,訓練的時候也得是2+1
的模式。但是,我感覺還是24
個好,感情更深一些。
從24
個生成1
個後,變成了25
個。然後再取這25
箇中的最後24
個,繼續生成下一個。迴圈600
次,最後生成了624
個音符。列印一下:
[[48, 0.001302083333371229, 0.010416666666628771],
[65, 0.11979166666674246, 0.08463541666651508]
……
[72, 0.03634712100028992, 0.023365378379821777],
[41, 0.04531348496675491, 0.011086761951446533]]
但是,這是預處理後的特徵,並非是可以直接演奏的音符。是否還記得duration = end-start
以及step=start2-start1
。我們需要把它們還原成為MIDI
體系下的屬性:
``` python
復原midi資料
prev_start = 0 midi_notes = [] for m in sample_notes: pitch, step, duration = m start = prev_start + step end = start + duration prev_start = start midi_notes.append([pitch, start, end]) ```
這樣,就把[pitch, step, duration]
轉化成了[pitch, start, end]
。列印midi_notes
如下:
[[48, 0.001302083333371229, 0.01171875],
[65, 0.12109375000011369, 0.20572916666662877],
……
[72, 32.04372873653976, 32.067094114919584],
[41, 32.08904222150652, 32.100128983457964]]
我們從資料可以看到,最後播放到了32
秒。也就說我們AI
生成的這段600
多個音符的樂曲,可以播放32
秒。
聽一聽效果,那就把它寫入MIDI
檔案吧。
``` python
寫入midi檔案
pm = pretty_midi.PrettyMIDI() instrument = pretty_midi.Instrument( program=pretty_midi.instrument_name_to_program("Acoustic Grand Piano")) for n in midi_notes: note = pretty_midi.Note(velocity=100,pitch=n[0],start=n[1],end=n[2]) instrument.notes.append(note) pm.instruments.append(instrument) pm.write("out.midi") ```
MIDI
檔案有5
個必需的要素。其中,樂器我們設定為"Acoustic Grand Piano"
原聲大鋼琴。velocity
沒有參與訓練,但也需要,我們設為固定值100
。其他的3
個引數,都是AI
生成的,依次代入。最後,把結果生成到out.midi
檔案中。
使用Window
自帶的Media Player
就可以直接播放這個檔案。你聽不到,我可以替你聽一聽。
聽完了,我談下感受吧。
怎麼描述呢?我覺得,說好聽對不起良心,反正,不難聽。
好了,AI
作曲就到此為止了。
原始碼已上傳到GitHub
地址是:http://github.com/hlwgy/ai_music。
做完了,我還得去找老張談談。
四、合作:你果然還是這樣的老張
我騎電動車去找老張,我告訴他,AI
作曲哥們搞定了。
老張問我,你陽了沒有。
我說,沒有。
老張告訴我,他表弟陽了。
我說,你不用擔心,畢竟你們離得那麼遠。
老張說,他陽了後,我們的專案也泡湯了。
我問為什麼。
老張說:我談好的那家飯店,就是表弟開的。他陽了之後,現在不承認了。
我的電腦還停留在開機介面,我強制關機。走了。
我記得,上一次,我告訴老張,三個月不要聯絡我《老張讓我用TensorFlow識別語音命令》。
而這一次,我什麼也沒有說。就走了。
我是TF男孩,帶你從IT視角看世界。
- 60分鐘的文心一言釋出會:我帶你5分鐘看完
- 程式設計師寫小說:我甘心當韭菜
- docx格式文件詳解:xml解析並用html還原
- 評論二則:年齡大了能否寫程式碼、上大學不喜歡IT專業咋整
- ChatGPT火了,我連夜詳解AIGC原理,並實戰生成動漫頭像
- 咱不吃虧,也不能過度自衛
- 兔年了,利用AI風格化實現剪紙兔、年畫兔、煙花兔
- 傻嗎?談男人們飯桌的拼酒現象
- 他扔出一張對話截圖:王總說的
- 老張說:快過年了,搞個AI作曲,用TensorFlow訓練midi檔案
- 為什麼大家都看中學歷?
- 年底了,裁兄弟當職員那哥們兒,如今咋樣了?
- 老張讓我用TensorFlow識別語音命令:前進、停止、左轉、右轉
- 在掘金第一次記錄失眠
- 十五分鐘簡介人工智慧,以聽懂為目的
- 認知史 12:積累階段,沒資格攀比
- 認知史 16:人過留名,雁過留聲
- 認知史 4:普遍共識
- 一文講通OCR文字識別原理與技術全流程(科普版)
- 認知史 7:選擇和努力