編者按:Shopify數(shù)據(jù)科學(xué)家Ruslan Nikolaev通過(guò)歌詞生成這一例子介紹了語(yǔ)言模型、RNN、LSTM以及NLP數(shù)據(jù)預(yù)處理流程。
在所有未來(lái)的AI應(yīng)用中,一個(gè)重頭戲是創(chuàng)建能夠從某個(gè)數(shù)據(jù)集中學(xué)習(xí),接著生成原創(chuàng)內(nèi)容。應(yīng)用這一想法到自然語(yǔ)言處理(NLP),AI社區(qū)研發(fā)了語(yǔ)言模型(Language Model)
語(yǔ)言模型的假定是學(xué)習(xí)句子是如何在文本中組織的,并使用這一知識(shí)生成新內(nèi)容
在我的案例中,我希望進(jìn)行一個(gè)有趣的業(yè)余項(xiàng)目,嘗試生成說(shuō)唱歌詞,看看我是否能夠重現(xiàn)很受歡迎的加拿大說(shuō)唱歌手Drake(#6god)的歌詞。
同時(shí)我也希望分享一個(gè)通用的機(jī)器學(xué)習(xí)項(xiàng)目流程,因?yàn)槲野l(fā)現(xiàn),如果你不是很清楚從哪里開(kāi)始,自己創(chuàng)建一些新東西經(jīng)常是非常困難的。
1. 獲取數(shù)據(jù)
首先我們需要構(gòu)建一個(gè)包含所有Drake歌曲的數(shù)據(jù)集。我編寫(xiě)了一個(gè)python腳本,抓取歌詞網(wǎng)站metrolyrics.com的網(wǎng)頁(yè)。
import urllib.request as urllib2
from bs4 importBeautifulSoup
import pandas as pd
import re
from unidecode import unidecode
quote_page = 'http://metrolyrics.com/{}-lyrics-drake.html'
filename = 'drake-songs.csv'
songs = pd.read_csv(filename)
for index, row in songs.iterrows():
page = urllib2.urlopen(quote_page.format(row['song']))
soup = BeautifulSoup(page, 'html.parser')
verses = soup.find_all('p', attrs={'class': 'verse'})
lyrics = ''
for verse in verses:
text = verse.text.strip()
text = re.sub(r"\[.*\]\n", "", unidecode(text))
if lyrics == '':
lyrics = lyrics + text.replace('\n', '|-|')
else:
lyrics = lyrics + '|-|' + text.replace('\n', '|-|')
songs.at[index, 'lyrics'] = lyrics
print('saving {}'.format(row['song']))
songs.head()
print('writing to .csv')
songs.to_csv(filename, sep=',', encoding='utf-8')
我使用了知名的BeautifulSoup包,我花了5分鐘,看了Justin Yek寫(xiě)的How to scrape websites with Python and BeautifulSoup教程,了解了BeautifulSoup的用法。你可能已經(jīng)注意到了,在上面的代碼中,我迭代了songs這一dataframe。是的,實(shí)際上,我預(yù)先定義了想要抓取的歌名。
運(yùn)行我編寫(xiě)的python爬蟲(chóng)后,.csv文件中包含了所有的歌詞。是時(shí)候開(kāi)始預(yù)處理數(shù)據(jù)并創(chuàng)建模型了。
songs = pd.read_csv('data/drake-songs.csv')
songs.head(10)
DataFrame中儲(chǔ)存了所有歌詞
關(guān)于模型
現(xiàn)在,我們將討論文本生成的模型,這是本文的重頭戲。
創(chuàng)建語(yǔ)言模型的兩種主要方法:(一)字符層次模型;(二)單詞層次模型
這兩種方法的主要差別在于輸入和輸出。下面我將介紹這兩種方法到底是如何工作的。
字符層次模型
字符層次模型的輸入是一系列字符seed(種子),模型負(fù)責(zé)預(yù)測(cè)下一個(gè)字符new_char。接著使用seed + new_char生成下一個(gè)字符,以此類推。注意,由于網(wǎng)絡(luò)輸入必須保持同一形狀,在每一次迭代中,實(shí)際上我們將從種子丟棄一個(gè)字符。下面是一個(gè)簡(jiǎn)單的可視化:
在每一次迭代中,基本上模型根據(jù)給定的種子字符預(yù)測(cè)最可能出現(xiàn)的下一個(gè)字符,用條件概率可以表達(dá)為,尋找P(new_char | seed)的最大值,其中new_char是字母表中的任意字符。在我們的例子中,字符表是所有英語(yǔ)字母,加上空格字符。(注意,你的字母表可能大不一樣,取決于模型適用的語(yǔ)言,字母表可以包含任何你需要的字符。)
單詞層次模型
單詞層次模型幾乎和字符層次模型一模一樣,只不過(guò)生成下一個(gè)單詞,而不是下一個(gè)字符。下面是一個(gè)簡(jiǎn)單的例子:
在單詞層次模型中,我們預(yù)測(cè)的單位不再是字符,而是單詞。也就是,P(new_word | seed),其中new_word是詞匯表中的任何單詞。
注意,現(xiàn)在我們要搜索的空間比之前大很多。在字符層次模型中,每次迭代只需搜索幾十種可能性,而在單詞層次模型中,每次迭代的搜索項(xiàng)多很多。因此,單詞層次算法需要在每次迭代上花費(fèi)更多的時(shí)間,好在由于每次迭代生成的是一個(gè)完整的單詞,而不是單個(gè)字符,所以其實(shí)并沒(méi)有那么糟。
另外,在單詞層次模型中,我們可能會(huì)有一個(gè)非常多樣化的詞匯表。通常,我們通過(guò)在數(shù)據(jù)集中查找所有獨(dú)特的單詞構(gòu)建詞匯表(一般在數(shù)據(jù)預(yù)處理階段完成)。由于詞匯表可能變得無(wú)限大,有很多技術(shù)用于提升算法的效率,比如詞嵌入,以后我會(huì)專門(mén)寫(xiě)文章介紹詞嵌入。
就本文而言,我將使用字符層次模型,因?yàn)樗菀讓?shí)現(xiàn),同時(shí),對(duì)字符層次模型的理解可以很容易地遷移到單詞層次模型。其實(shí)在我撰寫(xiě)本文的時(shí)候,我已經(jīng)創(chuàng)建了一個(gè)單詞層次的模型——以后我會(huì)另外寫(xiě)一篇文章加以介紹。
2. 數(shù)據(jù)預(yù)處理
就字符層次模型而言,我們將依照如下方式預(yù)處理數(shù)據(jù):
將數(shù)據(jù)集切分為token我們不能直接將字符串傳給模型,因?yàn)槟P徒邮茏址鳛檩斎?。所以我們需要將每行歌詞切分為字符列表。
定義字母表上一步讓我們得到了所有可能出現(xiàn)在歌詞中的字符,我們將查找所有獨(dú)特的字符。為了簡(jiǎn)化問(wèn)題,再加上整個(gè)數(shù)據(jù)集不怎么大(我只使用了140首歌),我將使用英語(yǔ)字母表,加上一些特殊字符(比如空格),并忽略數(shù)字和其他東西(由于數(shù)據(jù)集較小,我將選擇讓模型預(yù)測(cè)較少種字符)。
創(chuàng)建訓(xùn)練序列我們將使用滑窗(sliding window)技術(shù),通過(guò)在序列上滑動(dòng)固定尺寸的窗口創(chuàng)建訓(xùn)練樣本集。
每次移動(dòng)一個(gè)字符,我們生成20個(gè)字符長(zhǎng)的輸入,以及單個(gè)字符輸出。此外,我們得到了一個(gè)附帶的好處,由于我們每次移動(dòng)一個(gè)字符,實(shí)際上我們顯著擴(kuò)展了數(shù)據(jù)集的尺寸。
標(biāo)簽編碼訓(xùn)練序列最后,由于我們不打算讓模型處理原始字符(不過(guò)理論上這是可行的,因?yàn)榧夹g(shù)上字符即數(shù)字,你幾乎可以說(shuō)ASCII為我們編碼了所有字符),我們將給字母表中的每個(gè)字符分配一個(gè)整數(shù),你也許聽(tīng)說(shuō)過(guò)這一做法的名稱,標(biāo)簽編碼(Label Encoding)。我們創(chuàng)建了映射character-to-index和index-to-character。有了這兩個(gè)映射,我們總是能夠?qū)⑷魏巫址幋a為獨(dú)特的整數(shù),同時(shí)解碼模型輸出的索引數(shù)字為原本的字符。
one-hot編碼數(shù)據(jù)集由于我們處理的是類別數(shù)據(jù)(字符屬于某一類別),因此我們將編碼輸入列。關(guān)于one-hot編碼,可以參考Rakshith Vasudev撰寫(xiě)的What is One Hot Encoding? Why And When do you have to use it?一文。
當(dāng)我們完成以上5步后,我們只需創(chuàng)建模型并加以訓(xùn)練。如果你對(duì)以上步驟的細(xì)節(jié)感興趣,可以參考下面的代碼。
加載所有歌曲,并將其合并為一個(gè)巨大的字符串。
songs = pd.read_csv('data/drake-songs.csv')
for index, row in songs['lyrics'].iteritems():
text = text + str(row).lower()
找出所有獨(dú)特的字符。
chars = sorted(list(set(text)))
創(chuàng)建character-to-index和index-to-character映射。
char_indices = dict((c, i) for i, c in enumerate(chars))
indices_char = dict((i, c) for i, c in enumerate(chars))
切分文本為序列。
maxlen = 20
step = 1
sentences = []
next_chars = []
# 迭代文本并保存序列
for i in range(0, len(text) - maxlen, step):
sentences.append(text[i: i + maxlen])
next_chars.append(text[i + maxlen])
為輸入和輸出創(chuàng)建空矩陣,然后將所有字符轉(zhuǎn)換為數(shù)字(標(biāo)簽編碼和one-hot向量化)。
x = np.zeros((len(sentences), maxlen, len(chars)), dtype=np.bool)
y = np.zeros((len(sentences), len(chars)), dtype=np.bool)
for i, sentence in enumerate(sentences):
for t, char in enumerate(sentence):
x[i, t, char_indices[char]] = 1
y[i, char_indices[next_chars[i]]] = 1
3. 創(chuàng)建模型
為了使用之前的一些字符預(yù)測(cè)接下來(lái)的字符,我們將使用循環(huán)神經(jīng)網(wǎng)絡(luò)(RNN),具體來(lái)說(shuō)是長(zhǎng)短時(shí)記憶網(wǎng)絡(luò)(LSTM)。如果你不熟悉這兩個(gè)概念,我建議你參考以下兩篇文章:循環(huán)神經(jīng)網(wǎng)絡(luò)入門(mén)和一文詳解LSTM網(wǎng)絡(luò)。如果你只是想溫習(xí)一下這兩個(gè)概念,或者信心十足,下面是一個(gè)快速的總結(jié)。
RNN
你通常見(jiàn)到的神經(jīng)網(wǎng)絡(luò)像是一張蜘蛛網(wǎng),從許多節(jié)點(diǎn)收斂至單個(gè)輸出。就像這樣:
圖片來(lái)源:neuralnetworksanddeeplearning.com/
在這里我們有單個(gè)輸入和單個(gè)輸出。這樣的網(wǎng)絡(luò)對(duì)非連續(xù)輸入效果很好,其中輸入的順序不影響輸出。但在我們的例子中,字符的順序非常重要,因?yàn)檎亲址奶囟樞驑?gòu)建了單詞。
RNN接受連續(xù)的輸入,使用前一節(jié)點(diǎn)的激活作為后一節(jié)點(diǎn)的參數(shù)。
LSTM
簡(jiǎn)單的RNN有一個(gè)問(wèn)題,它們不是非常擅長(zhǎng)從非常早的單元將信息傳遞到之后的單元。例如,如果我們正查看句子Tryna keep it simple is a struggle for me,如果不能回頭查看之前出現(xiàn)的其他單詞,預(yù)測(cè)最后一個(gè)單詞me(可能是任何人或物,比如:Bake、cat、potato)是非常難的。
LSTM增加了一些記憶,儲(chǔ)存之前發(fā)生的某些信息。
LSTM可視化;圖片來(lái)源:吳恩達(dá)的深度學(xué)習(xí)課程
除了傳遞a激活之外,同時(shí)傳遞包含之前節(jié)點(diǎn)發(fā)生信息的c。這正是LSTM更擅長(zhǎng)保留上下文信息,一般而言在語(yǔ)言模型中能做出更好預(yù)測(cè)的原因。
代碼實(shí)現(xiàn)
我以前學(xué)過(guò)一點(diǎn)Keras,所以我使用這一框架構(gòu)建網(wǎng)絡(luò)。事實(shí)上,我們可以手工編寫(xiě)網(wǎng)絡(luò),唯一的差別只不過(guò)是需要多花許多時(shí)間。
創(chuàng)建網(wǎng)絡(luò),并加上LSTM層:
model = Sequential()
model.add(LSTM(128, input_shape=(maxlen, len(chars))))
增加softmax層,以輸出單個(gè)字符:
model.add(Dense(len(chars)))
model.add(Activation('softmax'))
選擇損失函數(shù)(交叉熵)和優(yōu)化器(RMSprop),然后編譯模型:
model.compile(loss='categorical_crossentropy', optimizer=RMSprop(lr=0.01))
訓(xùn)練模型(我們使用了batch進(jìn)行分批訓(xùn)練,略微加速了訓(xùn)練過(guò)程):
model.fit(x, y, batch_size=128, epochs=30)
4. 生成歌詞
訓(xùn)練網(wǎng)絡(luò)之后,我們將使用某個(gè)隨機(jī)種子(用戶輸入的字符串)作為輸入,讓網(wǎng)絡(luò)預(yù)測(cè)下一個(gè)字符。我們將重復(fù)這一過(guò)程,直到創(chuàng)建了足夠多的新行。
下面是一些生成歌詞的樣本(歌詞未經(jīng)審查)。
你也許注意到了,有些單詞沒(méi)有意義,這是字符層次模型的一個(gè)十分常見(jiàn)的問(wèn)題,輸入數(shù)據(jù)經(jīng)常在單詞中間切開(kāi),使得網(wǎng)絡(luò)學(xué)習(xí)并生成奇怪的新單詞,并通過(guò)某種方式賦予其“意義”。
單詞層面的模型能夠克服這一問(wèn)題,不過(guò)對(duì)于一個(gè)不到200行代碼的項(xiàng)目而言,字符層次模型仍然十分令人印象深刻。
其他應(yīng)用
字符層次網(wǎng)絡(luò)的想法可以擴(kuò)展到其他許多比歌詞生成更實(shí)際的應(yīng)用中。
例如,預(yù)測(cè)手機(jī)輸入:
想象一下,如果你創(chuàng)建了一個(gè)足夠精確的Python語(yǔ)言模型,它不僅可以自動(dòng)補(bǔ)全關(guān)鍵字或變量名,還能自動(dòng)補(bǔ)全大量代碼,大大節(jié)省程序員的時(shí)間。
你也許注意到了,這里的代碼并不是完整的,有些部分缺失了,完整代碼見(jiàn)我的GitHub倉(cāng)庫(kù)nikolaevra/drake-lyric-generator。在那里你可以深入所有細(xì)節(jié),希望這有助于你自己創(chuàng)建類似項(xiàng)目。
-
機(jī)器學(xué)習(xí)
+關(guān)注
關(guān)注
66文章
8481瀏覽量
133866 -
python
+關(guān)注
關(guān)注
56文章
4822瀏覽量
85876 -
自然語(yǔ)言
+關(guān)注
關(guān)注
1文章
291瀏覽量
13569
原文標(biāo)題:使用Keras和LSTM生成說(shuō)唱歌詞
文章出處:【微信號(hào):jqr_AI,微信公眾號(hào):論智】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
【大語(yǔ)言模型:原理與工程實(shí)踐】大語(yǔ)言模型的預(yù)訓(xùn)練
FPGA也能做RNN
數(shù)據(jù)探索與數(shù)據(jù)預(yù)處理
深度分析RNN的模型結(jié)構(gòu),優(yōu)缺點(diǎn)以及RNN模型的幾種應(yīng)用

評(píng)論