一起用 GAN 來寫嘻哈歌詞
來自專欄 NLPer 的成長之路8 人贊了文章
前言
非常有幸參與了 2018 Dee Camp 人工智慧夏令營,在這一個月的時間裡面,我們享受了來自學界、業界各種大咖的分享,同時也和導師一起參與到一個項目中,將所學知識進行落地。我們組的題目是 AI 有嘻哈,利用相關技術進行嘻哈歌詞的生成。
先來看看模型的效果,給定第一句,生成接下來的幾句,猜猜下面哪句是模型生成的哪句是原作?
不是樂理專修 做點兒曲式研究 我們的力量來自宇宙 自己的節奏
不是樂理專修 所有聽的觀眾 打破他們傳統 進到環球 繼續讓你感受
再來一個:
自己就帶上了有色眼鏡 金錢摧毀多少事情 瓦解你的中樞神經
自己就帶上了有色眼鏡 我只想把世界分的更清 卻發現自己模糊了心
正確答案是:第一行都是模型生成的,第二行是原作,你答對了嗎?現場展示的時候,甚至連李開復老師都答錯了哦,值得一提的是:輸入都是來自於測試集中,沒有參與到模型的訓練中。這篇文章就和大家分享一下項目背後的一些技術細節。
數據和預處理
創新工場為我們提供了 10 w 條嘻哈歌詞,並且已經將一些不符合社會主義核心價值觀的句子標註了出來。數據的預處理主要步驟如下:
- 在對句子進行篩選之後,我們利用 Jieba 進行分詞,觀察到單句長度集中在 8~10 左右;
- 在利用 Tensorflow 中的 Tokenizer 進行 tokenize 並構建 word2idex 字典後,詞表大小在 11000 左右,考慮到這個大小還可以接受,沒有做限制詞表大小的操作;
- 利用
pad_sequence
將句子 padding 到 20(和 SeqGAN 中相同); - 構建 x-y pair,利用上一句預測下一句(導師後來建議可以借鑒用 Skip-gram 的思路,同時預測上一句和下一句,但沒有時間去嘗試了),分割數據集
模型
我們的生成模型的整體基於 SeqGAN,並對其做了一些修改,模型架構如下:
主要改動有兩點:
- 增加輸入語句的編碼:這一點類似 Seq2Seq 的 Encoder,SeqGAN 原本的 initial state 是全 0 的,為了將上文的信息傳遞給生成器,我們採用了一個簡單的全連接層(Fully Connected Layer),將輸入句子的 Word Embedding 經過一個線性變化之後作為生成器的 LSTM。事實上也可以嘗試使用 RNN(LSTM)來作為 Encoder,不過這樣模型的速度可能會比較慢。
- 將原先 Generator 的 Loss Function 改為 Penalty-based Objective:在訓練模型的過程中我們發現,模型在 Adversarial Training 多輪之後出現了嚴重的 mode collapse 問題。比如
別質疑自己 遮罩錯的消息 不要過得消極 世間人都笑我太瘋癲 世間人都笑我太瘋癲
守護地獄每座墳墓 世間人都笑我太瘋癲 你不知道rapper付出多少才配紙醉金迷 世間人都笑我太瘋癲但卻從來沒有心狠過 如果你再想聽 你不知道rapper付出多少才配紙醉金迷 你不知道rapper付出多少才配紙醉金迷
可以看到 世間人都笑我太瘋癲 和 你不知道rapper付出多少才配紙醉金迷 佔據了我們生成的結果。mode collapse,簡單來說就是輸入的改變不會影響生成的結果。為此我們調研了一些 Paper,最終採用了SentiGAN 中提出的 Penalty-based Objective Function:
SeqGAN 是將原先 Discriminator 的認為 Generator 生成句子為真的概率作為獎賞,Generator 通過最大化獎賞(最小化其相反數)來更新自己的梯度;而 SentiGAN 則恰好相反,將 Discriminator 判斷為假的概率作為懲罰,Generator 通過最小化懲罰來更新自己,並且去掉了概率上的 log 函數。作者分析其起作用的效果在於:一、去掉 log,可以視作 WGAN 中的 generator loss(這一點我持懷疑態度,因為代碼中並沒有對梯度進行裁剪來滿足 WGAN 所需的 Lipschitz 條件);二、懲罰和獎賞存在一個 ? 的關係,根據 ? ,這樣會讓 generator 偏向於一個更小的 ? ,即概率比較小的句子,從而避免生成很多重複(概率比較大)的句子。Anyway,在我們的模型上,SentiGAN 的 loss 立竿見影,更多的細節請大家參考原作。押韻
嘻哈歌詞非常重要的一個特點就是句與句之間的押韻,我們在實現這一功能的時候嘗試了兩種方案:
- Reward based,在 reward 函數上增加額外的押韻獎賞項, ?:對 Generator 的生成的句子和輸入的句子進行押韻的判斷,如果押韻,則提供額外的獎賞。
- Rule-based,生成時只對押韻的詞進行採樣:在生成句尾的詞的概率分布時候,通過獲取和輸入句尾押韻的詞,只在這些押韻的詞進行採樣。
方法一,如果能夠通過設計 reward function 就能實現押韻的功能,那模型就是完全 end2end,非常 fancy 了。但是理想很豐滿,現實很骨感,經過幾天的調整押韻獎賞的權重,都沒能看到押韻率(我們設置的用於檢測押韻獎賞效果的指標,每個 batch 中和 input 押韻的句子的比例) 。我們懷疑是這種獎賞的結合會讓 Generator 產生混淆,並不能明確自己 reward 來自何處,應該需要更加具體的一些限制才能夠實現這一方法。
方法二,一開始我是拒絕這麼做的,用基於規則的方法不是我的理想,但是為了做出產品來,我還是屈服了,效果還非常不錯。
但還有一個問題擺在面前:怎麼知道生成的是句尾呢?導師提醒我們,我們可以把輸入倒過來。這是 NMT 中常用的一個手段,對於 LSTM,句子是真的還是反的差別不大,即使有差別,也可以通過一個 Bi-LSTM 來捕獲不同順序的信息。而為了知道哪些字詞是押韻的,我們實現製作了一張 vocab_size x vocab_size
的大表 rhyme
,如果兩個詞(index 分別為 i, j)押韻,則 rhyme[i, j]
非 0,否則為 0。據此,我們就可以對生成過程的第一個詞的詞表分布進行一個 mask 操作,使得非押韻的詞的概率都變成 0,就能夠保證押韻了。
如上圖所示,如果我們的輸入為「你真美麗」,句尾詞為「美麗」,韻腳為 i;最終採樣結果只會在押韻的詞中採樣,示例的採樣結果為「春泥」。
結語
在 DeeCamp 的一個月里,除了講座以外,更讓我覺得收穫許多的是認識了很多非常優秀的小夥伴,以及開拓了自己的視野,看到了更大的舞台。也再次向大家安利一波 DeeCamp,如果你對人工智慧感興趣,明年暑假,千萬別錯過~ 最後希望 DeeCamp 能夠一直辦下去,並且越辦越好!
推薦閱讀: