Tensorflow-3-使用RNN生成中文小說

這篇文章不涉及RNN的基本原理,只是從選擇數據集開始,到最後生成文本,展示一個RNN使用實例的過程。

對於深度學習的應用者,最應該關注的除了算法和模型,還應該關注如何預處理好自己的數據,合理降噪,以及如何在數據量不同的情況下選擇合理的超參,來達到最理想的訓練結果。

在經過近三個月的學習之後,我打算使用Tensorflow,創建一個LSTM RNN模型,使用中文小說作爲數據源,訓練RNN來生成中文小說文本。平時做練習訓練數據都是英文,我想看看換成中文之後會是什麼結果,能不能寫出一些語義通順的句子。

數據選取的是起點中文網的獲獎歷史小說『寒門首輔』,作者一袖乾坤。

整個notebook在我的github上,感興趣的同學可以下載使用,不斷嘗試訓練自己喜歡的風格的小說生成器。強烈建議大家使用notebook進行嘗試,可以實時看到每一步的輸出,學習效果更好。

以下就是整個應用過程。

來試試用起點中文網的歷史小說『寒門首輔(一袖乾坤 著)』來做訓練數據,看看這個RNN網絡能產生一些什麼樣子的文本。 嘗試過程中必遇到問題,也藉此加深一些對RNN的理解。

首先我從網上下載到了『寒門首輔』的txt版本,打開時候發現有很多空行,還包含了很多不必要的鏈接,看起來是這樣的。

這裏寫圖片描述

預處理一下數據。

import helper

讀入數據

dir = './data/寒門首輔.txt'
text = helper.load_text(dir)

設置一下要用多少個字來訓練,方便調試。這裏先用100000字進行訓練

num_words_for_training = 100000

text = text[:num_words_for_training]

看看有多少行

lines_of_text = text.split('\n')

print(len(lines_of_text))

Out: 4329

先看看前15行是什麼內容

print(lines_of_text[:15])

Out: ['《寒門首輔》', '作者:一袖乾坤', '', '內容簡介:    弘治五年,四海靖平。徐溥春風得意當了一朝首輔,李東陽初出茅廬做了會試考官。劉健熬成了文淵閣大學士,謝遷尚未入閣成就賢相美名。楊廷和奉旨參修《憲宗實錄》,劉大夏一把火燒了《鄭和海圖》。王陽明抱着書本埋頭苦讀準備着即將到來的鄉試,弘治皇帝與張皇后悠然自得的逗弄着繞膝玩耍的萌娃正德......    羣賢畢至,少長鹹集。在這個大師雲集,名臣輩出的美好時代,春風迷醉的餘姚城裏出身貧寒的穿越少年謝慎登高遠望,心中已經埋下了夢想。    誰言寒門再難出貴子,我便要入一入內閣,做一做首輔,提兩壺美酒,擁一方佳人。    世人有云:謝閣老一隻禿筆安社稷,一張薄紙定乾坤。無人不知謝文正,無人不曉謝餘姚......  ', '寒門首輔txt下載:http://www.80txt.com/txtxz/63028/down.html', '寒門首輔最新章節:http://www.80txt.com/txtxz/63028/down.html', '================================', '免責聲明:寒門首輔txt全集下載由80TXT電子書(WwW.80txt.com)書友收集整理自網絡,版權歸原作者所有,僅作學習交流使用,不可用於任何商業途徑,如非免費資源,請在試用之後24小時內立即刪除,如果喜歡該資源請購買正版謝謝合作;如不慎該資源侵犯了您的權利,請麻煩通知我及時刪除,謝謝!', '================================', 'A:【更多精彩好書,更多原創TXT手機電子書,我們因你而專業,TXT格式電子書下載 請登陸 80TXT電子書 --www.80txt.com】', 'B: 如果發現未完....登錄(80TXT小說網)www.80txt.com 提供24小時循環更新!!', '', '', '', '章節目錄 第一章 積善之家']

把『章節目錄』之前的行全部砍掉,一大堆沒用的東西。

lines_of_text = lines_of_text[14:]

再來看看,第一行應該就進入正題了。

print(lines_of_text[:5])

Out: ['章節目錄 第一章 積善之家', '', '    弘治五年,春和景明。[求書網qiushu.cc更新快,網站頁面清爽,廣告少,無彈窗,最喜歡這種網站了,一定要好評]', '', '    浙江承宣布政使司,紹興府,餘姚縣。']

我查看了一下,這個小說一共有129萬字左右。
先把空行去掉吧。去掉空行之後應該就只有一半左右的行數了。

lines_of_text = [lines for lines in lines_of_text if len(lines) > 0]

print(len(lines_of_text))

Out: 2158

打印前20行看看什麼情況

print(lines_of_text[:20])

Out: ['章節目錄 第一章 積善之家', '    弘治五年,春和景明。[求書網qiushu.cc更新快,網站頁面清爽,廣告少,無彈窗,最喜歡這種網站了,一定要好評]', '    浙江承宣布政使司,紹興府,餘姚縣。', '    縣城裏靠近城隍廟的一處小巷口,一個年約十二,身着淡藍色粗布長衫,頭戴黑色襆頭的少年望着不遠處熙熙攘攘的人羣不發一言。', '    他叫謝慎,是土生土長的餘姚人。但是也不盡然,因爲他的靈魂來自後世,是穿越而來奪舍附着在這個與他同名同姓的少年身上。事情的經過他並不是很清楚,只知道這個少年應該是不慎落水,被人救上後就一直昏迷不醒,奄奄一息,直到自己穿越才鳩佔鵲巢,成了這具身體的主人。', '    不過有些奇特的是,謝慎還兼有原先身體裏的一部分記憶,他拼命檢索這纔對身處的環境有了一個大概的認識。', '    如今是弘治五年,當今天子朱祐樘立志中興,招賢納士,敕令各省提學官巡視省內各府、州、縣學,選拔參加鄉試的人選。今年是鄉試之年,明年則是會試,殿試連着兩場大試,是出進士的年份。朝爲田舍郎,暮登天子堂,在明朝讀書自然是最有前途的事情。不過這似乎和他沒有什麼關係,雖然原先的謝慎也算是個讀書人,可卻並沒有功名在身,最多隻能算一個半吊子童生。', '    謝慎前世可是苦修明史的研究生,雖然主攻方向是嘉靖萬曆兩朝,但對弘治朝多少也有些瞭解。浙江一直是科舉強省,在弘治朝也是絕對的霸主,而紹興府則是浙江省中出進士最多的府。mianhuatang.cc [棉花糖小說網]若要落在縣一級,那魁首非餘姚莫屬。山陰,會稽兩縣加在一起,所中的進士數目都不比餘姚的多。這麼說來,餘姚絕對是科舉的死亡之組了。王陽明,謝遷,王華,一想到餘姚出的那些名人,謝慎便免不了自嘲。', '    謝慎雖然前世苦修明史,對八股文也有所研究,但要讓他在明代科場競爭最恐怖的餘姚縣脫穎而出,確實有些艱難。', '    可當他垂下頭看了看自己瘦弱的身體,不免搖了搖頭,長嘆了一聲。', '    不科舉還能幹什麼呢,就他這副竹竿子般的身體,肩不能抗,手不能提,殺雞沒準還被雞調戲......', '    何況在這個萬般皆下品惟有讀書高的年代,不科舉就只能爲民,不管是務農還是經商終歸都是被官府壓着,沒有出頭的機會。大明朝那個著名的沈萬三不就是最好的例子,家財萬貫富可敵國還不是隨意一個罪名就成了別人茶餘飯後唏噓慨嘆的談資?', '    讀書,還是得讀書,便前方是刀山火海硬着頭皮也得上。他才十二歲,還有可塑性......', '    “小郎,你怎麼在這兒呢,快快隨我回家去。你落水後被救起身子本就虛弱,若是在此時再染上了風寒,那可了不得。你嫂嫂給你特地煮了雞湯,你多喝上幾碗也好補補身子。”', '    一箇中年男子的聲音打斷了謝慎的沉思,他擡了擡頭見來人是自己的胞兄謝方,心中不由得泛起一抹暖意。', '    謝家門丁不旺,到了他們這一代只有他和謝方二人,不過這個便宜大哥似乎對他還不錯,吃穿用度上絕不短着缺着。', '    謝方在餘姚縣城裏開着一家茶鋪,將每年收上的茶葉運到縣城售賣,質地更好的一些會有人收走在府城售賣。', '    明代浙江紹興府各縣幾乎都種植茶葉。最著名的要數杭州府錢塘縣的龍井茶以及寧波府象山縣的珠山茶。至於餘姚雖然也種植茶樹,但其出產的茶葉並不像其種植棉花所產的“浙花”那麼出名。', '    不過似乎餘姚本地的百姓很喜歡這種清淡的姚江茶,其在本縣的銷路並不差。', '    靠着辛勤的工作,謝方也算撐起了一個家。在謝方的一再堅持下,謝慎從小便被送去開蒙,如今已經學了六年了。不過原先的謝慎最多隻能算是中上之資,跟神童絕對沾不上邊,照着原有軌跡發展下去,能不能取得秀才功名都是一個問題。']

下一步,把每行裏面的『空格』,『[]裏的內容』,『<>裏的內容』都去掉。

# 去掉每行首尾空格
lines_of_text = [lines.strip() for lines in lines_of_text]

看下情況如何,打印前20句話。

print(lines_of_text[:20])

Out: ['章節目錄 第一章 積善之家', '弘治五年,春和景明。[求書網qiushu.cc更新快,網站頁面清爽,廣告少,無彈窗,最喜歡這種網站了,一定要好評]', '浙江承宣布政使司,紹興府,餘姚縣。', '縣城裏靠近城隍廟的一處小巷口,一個年約十二,身着淡藍色粗布長衫,頭戴黑色襆頭的少年望着不遠處熙熙攘攘的人羣不發一言。', '他叫謝慎,是土生土長的餘姚人。但是也不盡然,因爲他的靈魂來自後世,是穿越而來奪舍附着在這個與他同名同姓的少年身上。事情的經過他並不是很清楚,只知道這個少年應該是不慎落水,被人救上後就一直昏迷不醒,奄奄一息,直到自己穿越才鳩佔鵲巢,成了這具身體的主人。', '不過有些奇特的是,謝慎還兼有原先身體裏的一部分記憶,他拼命檢索這纔對身處的環境有了一個大概的認識。', '如今是弘治五年,當今天子朱祐樘立志中興,招賢納士,敕令各省提學官巡視省內各府、州、縣學,選拔參加鄉試的人選。今年是鄉試之年,明年則是會試,殿試連着兩場大試,是出進士的年份。朝爲田舍郎,暮登天子堂,在明朝讀書自然是最有前途的事情。不過這似乎和他沒有什麼關係,雖然原先的謝慎也算是個讀書人,可卻並沒有功名在身,最多隻能算一個半吊子童生。', '謝慎前世可是苦修明史的研究生,雖然主攻方向是嘉靖萬曆兩朝,但對弘治朝多少也有些瞭解。浙江一直是科舉強省,在弘治朝也是絕對的霸主,而紹興府則是浙江省中出進士最多的府。mianhuatang.cc [棉花糖小說網]若要落在縣一級,那魁首非餘姚莫屬。山陰,會稽兩縣加在一起,所中的進士數目都不比餘姚的多。這麼說來,餘姚絕對是科舉的死亡之組了。王陽明,謝遷,王華,一想到餘姚出的那些名人,謝慎便免不了自嘲。', '謝慎雖然前世苦修明史,對八股文也有所研究,但要讓他在明代科場競爭最恐怖的餘姚縣脫穎而出,確實有些艱難。', '可當他垂下頭看了看自己瘦弱的身體,不免搖了搖頭,長嘆了一聲。', '不科舉還能幹什麼呢,就他這副竹竿子般的身體,肩不能抗,手不能提,殺雞沒準還被雞調戲......', '何況在這個萬般皆下品惟有讀書高的年代,不科舉就只能爲民,不管是務農還是經商終歸都是被官府壓着,沒有出頭的機會。大明朝那個著名的沈萬三不就是最好的例子,家財萬貫富可敵國還不是隨意一個罪名就成了別人茶餘飯後唏噓慨嘆的談資?', '讀書,還是得讀書,便前方是刀山火海硬着頭皮也得上。他才十二歲,還有可塑性......', '“小郎,你怎麼在這兒呢,快快隨我回家去。你落水後被救起身子本就虛弱,若是在此時再染上了風寒,那可了不得。你嫂嫂給你特地煮了雞湯,你多喝上幾碗也好補補身子。”', '一箇中年男子的聲音打斷了謝慎的沉思,他擡了擡頭見來人是自己的胞兄謝方,心中不由得泛起一抹暖意。', '謝家門丁不旺,到了他們這一代只有他和謝方二人,不過這個便宜大哥似乎對他還不錯,吃穿用度上絕不短着缺着。', '謝方在餘姚縣城裏開着一家茶鋪,將每年收上的茶葉運到縣城售賣,質地更好的一些會有人收走在府城售賣。', '明代浙江紹興府各縣幾乎都種植茶葉。最著名的要數杭州府錢塘縣的龍井茶以及寧波府象山縣的珠山茶。至於餘姚雖然也種植茶樹,但其出產的茶葉並不像其種植棉花所產的“浙花”那麼出名。', '不過似乎餘姚本地的百姓很喜歡這種清淡的姚江茶,其在本縣的銷路並不差。', '靠着辛勤的工作,謝方也算撐起了一個家。在謝方的一再堅持下,謝慎從小便被送去開蒙,如今已經學了六年了。不過原先的謝慎最多隻能算是中上之資,跟神童絕對沾不上邊,照着原有軌跡發展下去,能不能取得秀才功名都是一個問題。']

可以看到空格都沒了。下一步用正則去掉『[]』和『<>』中的內容,像上面的什麼『[棉花糖小說網]』這些的,後面還有一些是包含在『<>』裏的,一併去掉。

import re

# 生成一個正則,負責找『[]』包含的內容
pattern = re.compile(r'\[.*\]')

# 將所有指定內容替換成空
lines_of_text = [pattern.sub("", lines) for lines in lines_of_text]

打印看效果

print(lines_of_text[:20])

Out: ['章節目錄 第一章 積善之家', '弘治五年,春和景明。', '浙江承宣布政使司,紹興府,餘姚縣。', '縣城裏靠近城隍廟的一處小巷口,一個年約十二,身着淡藍色粗布長衫,頭戴黑色襆頭的少年望着不遠處熙熙攘攘的人羣不發一言。', '他叫謝慎,是土生土長的餘姚人。但是也不盡然,因爲他的靈魂來自後世,是穿越而來奪舍附着在這個與他同名同姓的少年身上。事情的經過他並不是很清楚,只知道這個少年應該是不慎落水,被人救上後就一直昏迷不醒,奄奄一息,直到自己穿越才鳩佔鵲巢,成了這具身體的主人。', '不過有些奇特的是,謝慎還兼有原先身體裏的一部分記憶,他拼命檢索這纔對身處的環境有了一個大概的認識。', '如今是弘治五年,當今天子朱祐樘立志中興,招賢納士,敕令各省提學官巡視省內各府、州、縣學,選拔參加鄉試的人選。今年是鄉試之年,明年則是會試,殿試連着兩場大試,是出進士的年份。朝爲田舍郎,暮登天子堂,在明朝讀書自然是最有前途的事情。不過這似乎和他沒有什麼關係,雖然原先的謝慎也算是個讀書人,可卻並沒有功名在身,最多隻能算一個半吊子童生。', '謝慎前世可是苦修明史的研究生,雖然主攻方向是嘉靖萬曆兩朝,但對弘治朝多少也有些瞭解。浙江一直是科舉強省,在弘治朝也是絕對的霸主,而紹興府則是浙江省中出進士最多的府。mianhuatang.cc 若要落在縣一級,那魁首非餘姚莫屬。山陰,會稽兩縣加在一起,所中的進士數目都不比餘姚的多。這麼說來,餘姚絕對是科舉的死亡之組了。王陽明,謝遷,王華,一想到餘姚出的那些名人,謝慎便免不了自嘲。', '謝慎雖然前世苦修明史,對八股文也有所研究,但要讓他在明代科場競爭最恐怖的餘姚縣脫穎而出,確實有些艱難。', '可當他垂下頭看了看自己瘦弱的身體,不免搖了搖頭,長嘆了一聲。', '不科舉還能幹什麼呢,就他這副竹竿子般的身體,肩不能抗,手不能提,殺雞沒準還被雞調戲......', '何況在這個萬般皆下品惟有讀書高的年代,不科舉就只能爲民,不管是務農還是經商終歸都是被官府壓着,沒有出頭的機會。大明朝那個著名的沈萬三不就是最好的例子,家財萬貫富可敵國還不是隨意一個罪名就成了別人茶餘飯後唏噓慨嘆的談資?', '讀書,還是得讀書,便前方是刀山火海硬着頭皮也得上。他才十二歲,還有可塑性......', '“小郎,你怎麼在這兒呢,快快隨我回家去。你落水後被救起身子本就虛弱,若是在此時再染上了風寒,那可了不得。你嫂嫂給你特地煮了雞湯,你多喝上幾碗也好補補身子。”', '一箇中年男子的聲音打斷了謝慎的沉思,他擡了擡頭見來人是自己的胞兄謝方,心中不由得泛起一抹暖意。', '謝家門丁不旺,到了他們這一代只有他和謝方二人,不過這個便宜大哥似乎對他還不錯,吃穿用度上絕不短着缺着。', '謝方在餘姚縣城裏開着一家茶鋪,將每年收上的茶葉運到縣城售賣,質地更好的一些會有人收走在府城售賣。', '明代浙江紹興府各縣幾乎都種植茶葉。最著名的要數杭州府錢塘縣的龍井茶以及寧波府象山縣的珠山茶。至於餘姚雖然也種植茶樹,但其出產的茶葉並不像其種植棉花所產的“浙花”那麼出名。', '不過似乎餘姚本地的百姓很喜歡這種清淡的姚江茶,其在本縣的銷路並不差。', '靠着辛勤的工作,謝方也算撐起了一個家。在謝方的一再堅持下,謝慎從小便被送去開蒙,如今已經學了六年了。不過原先的謝慎最多隻能算是中上之資,跟神童絕對沾不上邊,照着原有軌跡發展下去,能不能取得秀才功名都是一個問題。']

『[]』的內容已經沒了。下一步去掉『<>』中的內容,方法同上。

# 將上面的正則換成負責找『<>』包含的內容
pattern = re.compile(r'<.*>')

# 將所有指定內容替換成空
lines_of_text = [pattern.sub("", lines) for lines in lines_of_text]

下一步,把每句話最後的『……』換成『。』。

# 將上面的正則換成負責找『......』包含的內容
pattern = re.compile(r'\.+')

# 將所有指定內容替換成空
lines_of_text = [pattern.sub("。", lines) for lines in lines_of_text]

打印看效果

print(lines_of_text[:20])

Out: ['章節目錄 第一章 積善之家', '弘治五年,春和景明。', '浙江承宣布政使司,紹興府,餘姚縣。', '縣城裏靠近城隍廟的一處小巷口,一個年約十二,身着淡藍色粗布長衫,頭戴黑色襆頭的少年望着不遠處熙熙攘攘的人羣不發一言。', '他叫謝慎,是土生土長的餘姚人。但是也不盡然,因爲他的靈魂來自後世,是穿越而來奪舍附着在這個與他同名同姓的少年身上。事情的經過他並不是很清楚,只知道這個少年應該是不慎落水,被人救上後就一直昏迷不醒,奄奄一息,直到自己穿越才鳩佔鵲巢,成了這具身體的主人。', '不過有些奇特的是,謝慎還兼有原先身體裏的一部分記憶,他拼命檢索這纔對身處的環境有了一個大概的認識。', '如今是弘治五年,當今天子朱祐樘立志中興,招賢納士,敕令各省提學官巡視省內各府、州、縣學,選拔參加鄉試的人選。今年是鄉試之年,明年則是會試,殿試連着兩場大試,是出進士的年份。朝爲田舍郎,暮登天子堂,在明朝讀書自然是最有前途的事情。不過這似乎和他沒有什麼關係,雖然原先的謝慎也算是個讀書人,可卻並沒有功名在身,最多隻能算一個半吊子童生。', '謝慎前世可是苦修明史的研究生,雖然主攻方向是嘉靖萬曆兩朝,但對弘治朝多少也有些瞭解。浙江一直是科舉強省,在弘治朝也是絕對的霸主,而紹興府則是浙江省中出進士最多的府。mianhuatang。cc 若要落在縣一級,那魁首非餘姚莫屬。山陰,會稽兩縣加在一起,所中的進士數目都不比餘姚的多。這麼說來,餘姚絕對是科舉的死亡之組了。王陽明,謝遷,王華,一想到餘姚出的那些名人,謝慎便免不了自嘲。', '謝慎雖然前世苦修明史,對八股文也有所研究,但要讓他在明代科場競爭最恐怖的餘姚縣脫穎而出,確實有些艱難。', '可當他垂下頭看了看自己瘦弱的身體,不免搖了搖頭,長嘆了一聲。', '不科舉還能幹什麼呢,就他這副竹竿子般的身體,肩不能抗,手不能提,殺雞沒準還被雞調戲。', '何況在這個萬般皆下品惟有讀書高的年代,不科舉就只能爲民,不管是務農還是經商終歸都是被官府壓着,沒有出頭的機會。大明朝那個著名的沈萬三不就是最好的例子,家財萬貫富可敵國還不是隨意一個罪名就成了別人茶餘飯後唏噓慨嘆的談資?', '讀書,還是得讀書,便前方是刀山火海硬着頭皮也得上。他才十二歲,還有可塑性。', '“小郎,你怎麼在這兒呢,快快隨我回家去。你落水後被救起身子本就虛弱,若是在此時再染上了風寒,那可了不得。你嫂嫂給你特地煮了雞湯,你多喝上幾碗也好補補身子。”', '一箇中年男子的聲音打斷了謝慎的沉思,他擡了擡頭見來人是自己的胞兄謝方,心中不由得泛起一抹暖意。', '謝家門丁不旺,到了他們這一代只有他和謝方二人,不過這個便宜大哥似乎對他還不錯,吃穿用度上絕不短着缺着。', '謝方在餘姚縣城裏開着一家茶鋪,將每年收上的茶葉運到縣城售賣,質地更好的一些會有人收走在府城售賣。', '明代浙江紹興府各縣幾乎都種植茶葉。最著名的要數杭州府錢塘縣的龍井茶以及寧波府象山縣的珠山茶。至於餘姚雖然也種植茶樹,但其出產的茶葉並不像其種植棉花所產的“浙花”那麼出名。', '不過似乎餘姚本地的百姓很喜歡這種清淡的姚江茶,其在本縣的銷路並不差。', '靠着辛勤的工作,謝方也算撐起了一個家。在謝方的一再堅持下,謝慎從小便被送去開蒙,如今已經學了六年了。不過原先的謝慎最多隻能算是中上之資,跟神童絕對沾不上邊,照着原有軌跡發展下去,能不能取得秀才功名都是一個問題。']

最後,還是把每句話裏面包含的空格,都轉換成『,』,就像『章節目錄 第一章』,換成『章節目錄,第一章』,感覺這一步可有可無了。

# 將上面的正則換成負責找行中的空格
pattern = re.compile(r' +')

# 將所有指定內容替換成空
lines_of_text = [pattern.sub(",", lines) for lines in lines_of_text]

print(lines_of_text[:20])

Out: ['章節目錄,第一章,積善之家', '弘治五年,春和景明。', '浙江承宣布政使司,紹興府,餘姚縣。', '縣城裏靠近城隍廟的一處小巷口,一個年約十二,身着淡藍色粗布長衫,頭戴黑色襆頭的少年望着不遠處熙熙攘攘的人羣不發一言。', '他叫謝慎,是土生土長的餘姚人。但是也不盡然,因爲他的靈魂來自後世,是穿越而來奪舍附着在這個與他同名同姓的少年身上。事情的經過他並不是很清楚,只知道這個少年應該是不慎落水,被人救上後就一直昏迷不醒,奄奄一息,直到自己穿越才鳩佔鵲巢,成了這具身體的主人。', '不過有些奇特的是,謝慎還兼有原先身體裏的一部分記憶,他拼命檢索這纔對身處的環境有了一個大概的認識。', '如今是弘治五年,當今天子朱祐樘立志中興,招賢納士,敕令各省提學官巡視省內各府、州、縣學,選拔參加鄉試的人選。今年是鄉試之年,明年則是會試,殿試連着兩場大試,是出進士的年份。朝爲田舍郎,暮登天子堂,在明朝讀書自然是最有前途的事情。不過這似乎和他沒有什麼關係,雖然原先的謝慎也算是個讀書人,可卻並沒有功名在身,最多隻能算一個半吊子童生。', '謝慎前世可是苦修明史的研究生,雖然主攻方向是嘉靖萬曆兩朝,但對弘治朝多少也有些瞭解。浙江一直是科舉強省,在弘治朝也是絕對的霸主,而紹興府則是浙江省中出進士最多的府。mianhuatang。cc,若要落在縣一級,那魁首非餘姚莫屬。山陰,會稽兩縣加在一起,所中的進士數目都不比餘姚的多。這麼說來,餘姚絕對是科舉的死亡之組了。王陽明,謝遷,王華,一想到餘姚出的那些名人,謝慎便免不了自嘲。', '謝慎雖然前世苦修明史,對八股文也有所研究,但要讓他在明代科場競爭最恐怖的餘姚縣脫穎而出,確實有些艱難。', '可當他垂下頭看了看自己瘦弱的身體,不免搖了搖頭,長嘆了一聲。', '不科舉還能幹什麼呢,就他這副竹竿子般的身體,肩不能抗,手不能提,殺雞沒準還被雞調戲。', '何況在這個萬般皆下品惟有讀書高的年代,不科舉就只能爲民,不管是務農還是經商終歸都是被官府壓着,沒有出頭的機會。大明朝那個著名的沈萬三不就是最好的例子,家財萬貫富可敵國還不是隨意一個罪名就成了別人茶餘飯後唏噓慨嘆的談資?', '讀書,還是得讀書,便前方是刀山火海硬着頭皮也得上。他才十二歲,還有可塑性。', '“小郎,你怎麼在這兒呢,快快隨我回家去。你落水後被救起身子本就虛弱,若是在此時再染上了風寒,那可了不得。你嫂嫂給你特地煮了雞湯,你多喝上幾碗也好補補身子。”', '一箇中年男子的聲音打斷了謝慎的沉思,他擡了擡頭見來人是自己的胞兄謝方,心中不由得泛起一抹暖意。', '謝家門丁不旺,到了他們這一代只有他和謝方二人,不過這個便宜大哥似乎對他還不錯,吃穿用度上絕不短着缺着。', '謝方在餘姚縣城裏開着一家茶鋪,將每年收上的茶葉運到縣城售賣,質地更好的一些會有人收走在府城售賣。', '明代浙江紹興府各縣幾乎都種植茶葉。最著名的要數杭州府錢塘縣的龍井茶以及寧波府象山縣的珠山茶。至於餘姚雖然也種植茶樹,但其出產的茶葉並不像其種植棉花所產的“浙花”那麼出名。', '不過似乎餘姚本地的百姓很喜歡這種清淡的姚江茶,其在本縣的銷路並不差。', '靠着辛勤的工作,謝方也算撐起了一個家。在謝方的一再堅持下,謝慎從小便被送去開蒙,如今已經學了六年了。不過原先的謝慎最多隻能算是中上之資,跟神童絕對沾不上邊,照着原有軌跡發展下去,能不能取得秀才功名都是一個問題。']

貌似還忘了一個要處理的,我們看看最後20行的情況。(如果你是用全文本來訓練,最後很多行文本中會包括\r這樣的特殊符號,要去掉。這裏只用了100000字,所以看不到有\r的情況。)。如果有\\r的情況,用下面的方式去掉。

# 將上面的正則換成負責找句尾『\\r』的內容
pattern = re.compile(r'\\r')

# 將所有指定內容替換成空
lines_of_text = [pattern.sub("", lines) for lines in lines_of_text]

到這裏數據就處理完了。再看看有多少行數據

print(len(lines_of_text))

Out: 2158

因爲模型只認識數字,不認識中文,所以將文字對應到數字,分別創建文字對應數字和數字對應文字的兩個字典

def create_lookup_tables(input_data):

    vocab = set(input_data)

    # 文字到數字的映射
    vocab_to_int = {word: idx for idx, word in enumerate(vocab)}

    # 數字到文字的映射
    int_to_vocab = dict(enumerate(vocab))

    return vocab_to_int, int_to_vocab

創建一個符號查詢表,把逗號,句號等符號與一個標誌一一對應,用於將『我。』和『我』這樣的類似情況區分開來,排除標點符號的影響。

def token_lookup():

    symbols = set(['。', ',', '“', "”", ';', '!', '?', '(', ')', '——', '\n'])

    tokens = ["P", "C", "Q", "T", "S", "E", "M", "I", "O", "D", "R"]

    return dict(zip(symbols, tokens))

預處理一下數據,並保存到磁盤,一遍下次直接讀取。

helper.preprocess_and_save_data(''.join(lines_of_text), token_lookup, create_lookup_tables)

讀取我們需要的數據。

int_text, vocab_to_int, int_to_vocab, token_dict = helper.load_preprocess()

檢查改一下當前Tensorflow的版本以及是否有GPU可以使用

import problem_unittests as tests
from distutils.version import LooseVersion
import warnings
import tensorflow as tf
import numpy as np

# Check TensorFlow Version
assert LooseVersion(tf.__version__) >= LooseVersion('1.0'), 'Please use TensorFlow version 1.0 or newer'
print('TensorFlow Version: {}'.format(tf.__version__))

# Check for a GPU
if not tf.test.gpu_device_name():
    warnings.warn('No GPU found. Please use a GPU to train your neural network.')
else:
    print('Default GPU Device: {}'.format(tf.test.gpu_device_name()))

Out: TensorFlow Version: 1.0.0
Default GPU Device: /gpu:0

如果沒有GPU可以使用,看不到第二行輸出,而會是一個警告。

這裏的數據量還是很大的,129萬左右個字符。建議使用GPU來訓練。或者可以修改代碼,只用一小部分數據來訓練,節省時間。

正式進入創建RNN的階段了。

我們的RNN不是原始RNN了,中間使用到LSTMword2vec的功能。下面將基於Tensorflow,創建一個帶2層LSTM層的RNN網絡來進行訓練。

首先設置一下超參。

# 訓練循環次數
num_epochs = 200

# batch大小
batch_size = 256

# lstm層中包含的unit個數
rnn_size = 512

# embedding layer的大小
embed_dim = 512

# 訓練步長
seq_length = 30

# 學習率
learning_rate = 0.003

# 每多少步打印一次訓練信息
show_every_n_batches = 30

# 保存session狀態的位置
save_dir = './save'

創建輸入,目標以及學習率的placeholder

def get_inputs():

    # inputs和targets的類型都是整數的
    inputs = tf.placeholder(tf.int32, [None, None], name='inputs')
    targets = tf.placeholder(tf.int32, [None, None], name='targets')
    learning_rate = tf.placeholder(tf.float32, name='learning_rate')

    return inputs, targets, learning_rate

創建rnn cell,使用lstm cell,並創建相應層數的lstm層,應用dropout,以及初始化lstm層狀態。

def get_init_cell(batch_size, rnn_size):
    # lstm層數
    num_layers = 2

    # dropout時的保留概率
    keep_prob = 0.8

    # 創建包含rnn_size個神經元的lstm cell
    cell = tf.contrib.rnn.BasicLSTMCell(rnn_size)

    # 使用dropout機制防止overfitting等
    drop = tf.contrib.rnn.DropoutWrapper(cell, output_keep_prob=keep_prob)

    # 創建2層lstm層
    cell = tf.contrib.rnn.MultiRNNCell([drop for _ in range(num_layers)])

    # 初始化狀態爲0.0
    init_state = cell.zero_state(batch_size, tf.float32)

    # 使用tf.identify給init_state取個名字,後面生成文字的時候,要使用這個名字來找到緩存的state
    init_state = tf.identity(init_state, name='init_state')

    return cell, init_state

創建embedding layer,提升效率

def get_embed(input_data, vocab_size, embed_dim):

    # 先根據文字數量和embedding layer的size創建tensorflow variable
    embedding = tf.Variable(tf.random_uniform((vocab_size, embed_dim)), dtype=tf.float32)

    # 讓tensorflow幫我們創建lookup table
    return tf.nn.embedding_lookup(embedding, input_data)

創建rnn節點,使用dynamic_rnn方法計算出outputfinal_state

def build_rnn(cell, inputs):

    '''
    cell就是上面get_init_cell創建的cell
    '''

    outputs, final_state = tf.nn.dynamic_rnn(cell, inputs, dtype=tf.float32)

    # 同樣給final_state一個名字,後面要重新獲取緩存
    final_state = tf.identity(final_state, name="final_state")

    return outputs, final_state

用上面定義的方法創建rnn網絡,並接入最後一層fully_connected layer計算rnn的logits

def build_nn(cell, rnn_size, input_data, vocab_size, embed_dim):

    # 創建embedding layer
    embed = get_embed(input_data, vocab_size, rnn_size)

    # 計算outputs 和 final_state
    outputs, final_state = build_rnn(cell, embed)

    # remember to initialize weights and biases, or the loss will stuck at a very high point
    logits = tf.contrib.layers.fully_connected(outputs, vocab_size, activation_fn=None,
                                               weights_initializer = tf.truncated_normal_initializer(stddev=0.1),
                                               biases_initializer=tf.zeros_initializer())

    return logits, final_state

那麼大的數據量不可能一次性都塞到模型裏訓練,所以用get_batches方法一次使用一部分數據來訓練

def get_batches(int_text, batch_size, seq_length):

    # 計算有多少個batch可以創建
    n_batches = (len(int_text) // (batch_size * seq_length))

    # 計算每一步的原始數據,和位移一位之後的數據
    batch_origin = np.array(int_text[: n_batches * batch_size * seq_length])
    batch_shifted = np.array(int_text[1: n_batches * batch_size * seq_length + 1])

    # 將位移之後的數據的最後一位,設置成原始數據的第一位,相當於在做循環
    batch_shifted[-1] = batch_origin[0]

    batch_origin_reshape = np.split(batch_origin.reshape(batch_size, -1), n_batches, 1)
    batch_shifted_reshape = np.split(batch_shifted.reshape(batch_size, -1), n_batches, 1)

    batches = np.array(list(zip(batch_origin_reshape, batch_shifted_reshape)))

    return batches

創建整個RNN網絡模型

# 導入seq2seq,下面會用他計算loss
from tensorflow.contrib import seq2seq

train_graph = tf.Graph()
with train_graph.as_default():
    # 文字總量
    vocab_size = len(int_to_vocab)

    # 獲取模型的輸入,目標以及學習率節點,這些都是tf的placeholder
    input_text, targets, lr = get_inputs()

    # 輸入數據的shape
    input_data_shape = tf.shape(input_text)

    # 創建rnn的cell和初始狀態節點,rnn的cell已經包含了lstm,dropout
    # 這裏的rnn_size表示每個lstm cell中包含了多少的神經元
    cell, initial_state = get_init_cell(input_data_shape[0], rnn_size)

    # 創建計算loss和finalstate的節點
    logits, final_state = build_nn(cell, rnn_size, input_text, vocab_size, embed_dim)

    # 使用softmax計算最後的預測概率
    probs = tf.nn.softmax(logits, name='probs')

    # 計算loss
    cost = seq2seq.sequence_loss(
        logits,
        targets,
        tf.ones([input_data_shape[0], input_data_shape[1]]))

    # 使用Adam提督下降
    optimizer = tf.train.AdamOptimizer(lr)

    # 裁剪一下Gradient輸出,最後的gradient都在[-1, 1]的範圍內
    gradients = optimizer.compute_gradients(cost)
    capped_gradients = [(tf.clip_by_value(grad, -1., 1.), var) for grad, var in gradients if grad is not None]
    train_op = optimizer.apply_gradients(capped_gradients)

開始訓練模型

# 獲得訓練用的所有batch
batches = get_batches(int_text, batch_size, seq_length)

# 打開session開始訓練,將上面創建的graph對象傳遞給session
with tf.Session(graph=train_graph) as sess:
    sess.run(tf.global_variables_initializer())

    for epoch_i in range(num_epochs):
        state = sess.run(initial_state, {input_text: batches[0][0]})

        for batch_i, (x, y) in enumerate(batches):
            feed = {
                input_text: x,
                targets: y,
                initial_state: state,
                lr: learning_rate}
            train_loss, state, _ = sess.run([cost, final_state, train_op], feed)

            # 打印訓練信息
            if (epoch_i * len(batches) + batch_i) % show_every_n_batches == 0:
                print('Epoch {:>3} Batch {:>4}/{}   train_loss = {:.3f}'.format(
                    epoch_i,
                    batch_i,
                    len(batches),
                    train_loss))

    # 保存模型
    saver = tf.train.Saver()
    saver.save(sess, save_dir)
    print('Model Trained and Saved')

將使用到的變量保存起來,以便下次直接讀取。

helper.save_params((seq_length, save_dir))

下次使用訓練好的模型,從這裏開始就好

import tensorflow as tf
import numpy as np
import helper
import problem_unittests as tests

_, vocab_to_int, int_to_vocab, token_dict = helper.load_preprocess()
seq_length, load_dir = helper.load_params()

要使用保存的模型,我們要講保存下來的變量(tensor)通過指定的name獲取到

def get_tensors(loaded_graph):

    inputs = loaded_graph.get_tensor_by_name("inputs:0")

    initial_state = loaded_graph.get_tensor_by_name("init_state:0")

    final_state = loaded_graph.get_tensor_by_name("final_state:0")

    probs = loaded_graph.get_tensor_by_name("probs:0")

    return inputs, initial_state, final_state, probs
def pick_word(probabilities, int_to_vocab):

    chances = []

    for idx, prob in enumerate(probabilities):
        if prob >= 0.05:
            chances.append(int_to_vocab[idx])

    rand = np.random.randint(0, len(chances))

    return str(chances[rand])

使用訓練好的模型來生成自己的小說

# 生成文本的長度
gen_length = 500

# 文章開頭的字,指定一個即可,這個字必須是在訓練詞彙列表中的
prime_word = '章'


loaded_graph = tf.Graph()
with tf.Session(graph=loaded_graph) as sess:
    # 加載保存過的session
    loader = tf.train.import_meta_graph(load_dir + '.meta')
    loader.restore(sess, load_dir)

    # 通過名稱獲取緩存的tensor
    input_text, initial_state, final_state, probs = get_tensors(loaded_graph)

    # 準備開始生成文本
    gen_sentences = [prime_word]
    prev_state = sess.run(initial_state, {input_text: np.array([[1]])})

    # 開始生成文本
    for n in range(gen_length):
        dyn_input = [[vocab_to_int[word] for word in gen_sentences[-seq_length:]]]
        dyn_seq_length = len(dyn_input[0])

        probabilities, prev_state = sess.run(
            [probs, final_state],
            {input_text: dyn_input, initial_state: prev_state})

        pred_word = pick_word(probabilities[dyn_seq_length - 1], int_to_vocab)

        gen_sentences.append(pred_word)

    # 將標點符號還原
    novel = ''.join(gen_sentences)
    for key, token in token_dict.items():
        ending = ' ' if key in ['\n', '(', '“'] else ''
        novel = novel.replace(token.lower(), key)
    novel = novel.replace('\n ', '\n')
    novel = novel.replace('( ', '(')

    print(novel)

這是我用100000個字訓練200個循環之後產生的文本

章,正後一屁股文。在這一幫寒門謝氏,但揚聲修呼。拉他的手藝推着而出了院門,不是四門中,謝慎注意一息,剛這日一旁不何從世家。王守文在竹門的王守仁作寫來已經站候,幾十板子,想要去布包嗎,不得我到了縣尊讓謝慎心裏一詩會就想在一個木籃子,他也算孔教諭都拱問而即便只有在這些學生,他需要受呼謝方四書一次。這樣的人早,只得一個人約的回景,絕不會謝慎便在縣學裏加靠考下。如果不睡在,謝方和劉老夫子捋了下樓的時間有些啊。“這個王守仁的人情出了。”謝慎一邊又要通過身後。曹主簿纔不是一個不會再爲何事情。他這個時文讀洞人要有何證不像提一跳的,大哥謝慎還是想聽有人擔心的。謝慎也不能掉以輕早一隻有意授嗎?”謝丕笑了點頭,目送着二人,柔聲道:“本然比何好關,這纔會出他什麼可是了心門,你一直不必再拘束。他若不想一個自己拿到官府,他們卻是三十大板,會有考好時文,他的境遇更發生了拉近。他自己不能在縣試取什麼,留是功名,他需要你還有一段時時也能做上。他這個時候出城真也得不太一合寫,畢竟餘姚名學說的青石頭最多一貫情,但佈置的學生自稱是爲自己的。但果然謝慎是想不到四門謝氏的嫡公子那裏不遠,一種五家小王守文的部二。那樣俊望的計”這下

可以看到模型已經能產生一些有意義的詞語了,比如『寒門』,『手藝』,『已經』,『佈置』等。但是還沒有形成通順的句子。

這個RNN的架構遵照了

Input -> LSTM -> Dropout -> LSTM -> Dropout -> Fully Connected

這樣的形式。

在使用完10萬個字進行訓練之後,我嘗試使用整個數據集也就是129萬個字符去訓練,但是受困於計算能力有限,沒有完成(我在亞馬遜雲的EC2內存和GPU有限,無法完成這麼大數據量的訓練,說白了是沒錢哈哈~)。大家有資源的可以嘗試。

有不足的地方請大家留言或者ISSUE,我也還在不斷學習成長中,歡迎交流。

EDIT 6月12日:

前面一次訓練只使用了100000的詞彙來訓練,之後我申請到了亞馬遜雲一個p2.16xlarge的實例來訓練全部數據,使用如下的超參。

# 訓練循環次數
num_epochs = 200

# batch大小
batch_size = 256

# lstm層中包含的unit個數
rnn_size = 1024

# embedding layer的大小
embed_dim = 1500

# 訓練步長
seq_length = 60

# 學習率
learning_rate = 0.001

經過200個循環,loss大概降到了1.3。參數還需要進一步調整,小於0.5loss才接近理想。不過1.3loss可以生成一些通順的語句了,比之前只有詞語的情況進步了不少。

章,如果百年世家不想出現的這些士子,一個孩子多那麼誇,竟是得好不起的。謝慎不由得感慨道:“怎麼,有幾日潞安百姓謝某不過府面子的外轉,實話足夠啊。?”我又說說沒有顧大才吧;我這次便是舍頭大明皇帝去打啊, 我現在還沒出息,你們要不去。陳虎兒見謝慎面色古怪了,可也不知道該怎麼做點出真手錯了。就是說,這個譚芳還不得不起自己,也沒有多想着謝慎這般拘束他們的表演。不曾想這世上也太不避嫌了。謝方和大人平日閒來得到唐寅身來到西湖身上闖乘,王守仁自然也就沒有考慮。回來再四不徐徐芊芊,喝水直是君業,想想他也是可以將直接的成本。不過有些事謝慎不能再找個面子,看看明軍心中的架勢。這些百姓不是個外人。真想知行來是不會有人拿出來好的,若不是她不選識及防之力就可以讓水芸姑娘相互邀請一家。那些倒也沒有在他揚名上啊,而這是讀書人對太子的才子加足了的。他不需要太多的道理,只不過他還想不到京師生員上面臨時起雲,怎麼可能拒絕低調,真得一起吃酒啊。慎賢弟,怎麼,本

像『我現在還沒出息』,『王守仁自然也就沒有考慮』,『他不需要太多的道理』,都是通順且有意義的短句了。不過短句前後,還是沒有形成語義的連貫。1.3loss還是太高。有時間再繼續調整吧~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章