NLP——利用lstm生成莫言小說

利用lstm生成莫言小說

1. 項目背景

這個項目是建立一個能夠自動生成一片文章的深度學習模型,我們可以通過給出錢幾個字就自動生成一篇文章的模型。
項目地址:https://github.com/audier/my_deep_project/tree/master/NLP

2. 項目數據

項目數據使用了莫言小說《生死疲勞》,內容如下:

# ========讀取原始數據========
with open('data.txt', 'r', encoding='utf-8') as f:
    data = f.readlines()
print(data[0])
  我的故事,從1950年1月1日講起。在此之前兩年多的時間裏,我在陰曹地府裏受盡了人間難以想象的 酷刑。每次提審,我都會鳴冤叫屈。我的聲音悲壯淒涼,傳播到閻羅大殿的每個角落,激發出重重疊疊的 回聲。我身受酷刑而絕不改悔,掙得了一個硬漢子的名聲。我知道許多鬼卒對我暗中欽佩,我也知道閻王 老子對我不勝厭煩。爲了讓我認罪服輸,他們使出了地獄酷刑中最歹毒的一招,將我扔到沸騰的油鍋裏, 翻來覆去,像炸(又鳥)一樣炸了半個時辰,痛苦之狀,難以言表。鬼卒還用叉子把我叉起來,高高舉着, 一步步走上通往大殿的臺階。兩邊的鬼卒嘬口吹哨,如同成羣的吸血蝙蝠鳴叫。我的身體滴油淅瀝,落在 臺階上,冒出一簇簇黃煙……鬼卒小心翼翼地將我安放在閻羅殿前的青石板上,跪下向閻王報告:“大王 ,炸好了。”

3. 數據處理

3.1數據清洗

首先需要將括號裏的內容刪除掉。

import re
# 生成一個正則,負責找'()'包含的內容
pattern = re.compile(r'\(.*\)')
# 將其替換爲空
data = [pattern.sub('', lines) for lines in data]
print(data[0])
  我的故事,從1950年1月1日講起。在此之前兩年多的時間裏,我在陰曹地府裏受盡了人間難以想象的 酷刑。每次提審,我都會鳴冤叫屈。我的聲音悲壯淒涼,傳播到閻羅大殿的每個角落,激發出重重疊疊的 回聲。我身受酷刑而絕不改悔,掙得了一個硬漢子的名聲。我知道許多鬼卒對我暗中欽佩,我也知道閻王 老子對我不勝厭煩。爲了讓我認罪服輸,他們使出了地獄酷刑中最歹毒的一招,將我扔到沸騰的油鍋裏, 翻來覆去,像炸一樣炸了半個時辰,痛苦之狀,難以言表。鬼卒還用叉子把我叉起來,高高舉着, 一步步走上通往大殿的臺階。兩邊的鬼卒嘬口吹哨,如同成羣的吸血蝙蝠鳴叫。我的身體滴油淅瀝,落在 臺階上,冒出一簇簇黃煙……鬼卒小心翼翼地將我安放在閻羅殿前的青石板上,跪下向閻王報告:“大王 ,炸好了。”

然後我們將省略號’…‘替換爲句號’。'

# 將.....替換爲句號
data = [line.replace('……', '。') for line in data if len(line) > 1]
print(data[0])
  我的故事,從1950年1月1日講起。在此之前兩年多的時間裏,我在陰曹地府裏受盡了人間難以想象的 酷刑。每次提審,我都會鳴冤叫屈。我的聲音悲壯淒涼,傳播到閻羅大殿的每個角落,激發出重重疊疊的 回聲。我身受酷刑而絕不改悔,掙得了一個硬漢子的名聲。我知道許多鬼卒對我暗中欽佩,我也知道閻王 老子對我不勝厭煩。爲了讓我認罪服輸,他們使出了地獄酷刑中最歹毒的一招,將我扔到沸騰的油鍋裏, 翻來覆去,像炸一樣炸了半個時辰,痛苦之狀,難以言表。鬼卒還用叉子把我叉起來,高高舉着, 一步步走上通往大殿的臺階。兩邊的鬼卒嘬口吹哨,如同成羣的吸血蝙蝠鳴叫。我的身體滴油淅瀝,落在 臺階上,冒出一簇簇黃煙。鬼卒小心翼翼地將我安放在閻羅殿前的青石板上,跪下向閻王報告:“大王 ,炸好了。”

只保留有效的數據,包括漢字、字母、數字、中文符號等信息,其他亂碼進行清除

# ==============判斷char是否是亂碼===================
def is_uchar(uchar):
    """判斷一個unicode是否是漢字"""
    if uchar >= u'\u4e00' and uchar<=u'\u9fa5':
            return True
    """判斷一個unicode是否是數字"""
    if uchar >= u'\u0030' and uchar<=u'\u0039':
            return True       
    """判斷一個unicode是否是英文字母"""
    if (uchar >= u'\u0041' and uchar<=u'\u005a') or (uchar >= u'\u0061' and uchar<=u'\u007a'):
            return True
    if uchar in (',','。',':','?','“','”','!',';','、','《','》','——'):
            return True
    return False

# 將每行的list合成一個長字符串
data = ''.join(data)
data = [char for char in data if is_uchar(char)]
data = ''.join(data)
print(data[:100])
我的故事,從1950年1月1日講起。在此之前兩年多的時間裏,我在陰曹地府裏受盡了人間難以想象的酷刑。每次提審,我都會鳴冤叫屈。我的聲音悲壯淒涼,傳播到閻羅大殿的每個角落,激發出重重疊疊的回聲。我身受酷

3.2 生成字典

我們需要將漢字映射爲能夠輸入到模型中的數字信息,就需要建立一個映射關係,需要生成漢字和數字互相映射的字典。

# =====生成字典=====
vocab = set(data)
id2char = list(vocab)
char2id = {c:i for i,c in enumerate(vocab)}

print('字典長度:', len(vocab))
字典長度: 3892

3.3 轉換輸入數據格式

建立字典後,將文本數據映射爲數字數據形式,並整理爲矩陣格式。

import numpy as np
# =====轉換數據爲數字格式======
numdata = [char2id[char] for char in data]
numdata = np.array(numdata)

print('數字數據信息:\n', numdata[:100])
print('\n文本數據信息:\n', ''.join([id2char[i] for i in numdata[:100]]))
數字數據信息:
 [ 841  146 1063 1305 2317 3333 3011 3032 1932 3675 2677 3011 3118 3011
 3010 2883 2348 2104 3307 3060 3122 3198  556 2677 3375  146 1345 3585
 2640 2317  841 3307  409   36 3531 3830 2640  290  308 3201 3882 3585
 3117 3249 2012 1901  146  141 1547 2104 2122  835 1983 2146 2317  841
 1940 1304 3518 1968 1447 2137 2104  841  146 2986 3886 3829 3267 1043
 2110 2317 3800 2903 3141   64 2333 3432 3430  146 2122 2032   98  325
 2317 2988  875 1664  738  738  924  924  146 3764 2986 2104  841 1540
  290  141]

文本數據信息:
 我的故事,從1950年1月1日講起。在此之前兩年多的時間裏,我在陰曹地府裏受盡了人間難以想象的酷刑。每次提審,我都會鳴冤叫屈。我的聲音悲壯淒涼,傳播到閻羅大殿的每個角落,激發出重重疊疊的回聲。我身受酷

3.4 設計數據生成器

這篇文章有幾十萬個字:

print(len(data))
377480

我們通常不會將數據一股腦扔到網絡中進行訓練,而是將數據分爲一個batch一個batch的進行訓練。
下面的函數實現了將數據切分爲一個個的[batch_size, time_steps]形式,這種數據也是循環神經網絡訓練中使用的格式。
通過觀察輸入數據和輸出數據我們發現,輸入數據總是比輸出數據提前一個time_step。如下圖所示:
在這裏插入圖片描述
這是因爲我們要建立的模型實現的功能是希望通過輸入:3001 3472 3811 1021 271 能夠成功的預測下一個單詞:1644

# =======設計數據生成器=========
def data_generator(data, batch_size, time_stpes):
	samples_per_batch = batch_size * time_stpes
	batch_nums = len(data) // samples_per_batch
	data = data[:batch_nums*samples_per_batch]
	data = data.reshape((batch_size, batch_nums, time_stpes))
	for i in range(batch_nums):
		x = data[:, i, :]
		y = np.zeros_like(x)
		y[:, :-1] = x[:, 1:]
		try:
			y[:, -1] = data[:, i+1, 0]
		except:
			y[:, -1] = data[:, 0, 0]
		yield x, y

# 打印輸出數據
data_batch = data_generator(numdata, 2, 5)
x, y = next(data_batch)
print('input data:', x[0], '\noutput data:', y[0])
input data: [ 841  146 1063 1305 2317] 
output data: [ 146 1063 1305 2317 3333]

4. 模型選擇與建模

我們選擇rnn來作爲文本生成模型結構如下:
在這裏插入圖片描述
我們選擇lstm來做爲其中的隱藏層:
在這裏插入圖片描述

4.1 使用tensorflow進行建模:

import tensorflow as tf
# ====================================搭建模型===================================
class RNNModel():
	"""docstring for RNNModel"""
	def __init__(self, BATCH_SIZE, HIDDEN_SIZE, HIDDEN_LAYERS, VOCAB_SIZE, learning_rate):
		super(RNNModel, self).__init__()
		self.BATCH_SIZE = BATCH_SIZE
		self.HIDDEN_SIZE = HIDDEN_SIZE
		self.HIDDEN_LAYERS = HIDDEN_LAYERS
		self.VOCAB_SIZE = VOCAB_SIZE
		
		# ======定義佔位符======
		with tf.name_scope('input'):
			self.inputs = tf.placeholder(tf.int32, [BATCH_SIZE, None])
			self.targets = tf.placeholder(tf.int32, [BATCH_SIZE, None])
			self.keepprb = tf.placeholder(tf.float32)

		# ======定義詞嵌入層======
		with tf.name_scope('embedding'):
			embedding = tf.get_variable('embedding', [VOCAB_SIZE, HIDDEN_SIZE])
			emb_input = tf.nn.embedding_lookup(embedding, self.inputs)
			emb_input = tf.nn.dropout(emb_input, self.keepprb)

		# ======搭建lstm結構=====
		with tf.name_scope('rnn'):
			lstm = tf.contrib.rnn.LSTMCell(HIDDEN_SIZE, state_is_tuple=True)
			lstm = tf.contrib.rnn.DropoutWrapper(lstm, output_keep_prob=self.keepprb)
			cell = tf.contrib.rnn.MultiRNNCell([lstm] * HIDDEN_LAYERS)
			self.initial_state = cell.zero_state(BATCH_SIZE, tf.float32)
			outputs, self.final_state = tf.nn.dynamic_rnn(cell, emb_input, initial_state=self.initial_state)
            
		# =====重新reshape輸出=====
		with tf.name_scope('output_layer'):
			outputs = tf.reshape(tf.concat(outputs, 1), [-1, HIDDEN_SIZE])
			w = tf.get_variable('outputs_weight', [HIDDEN_SIZE, VOCAB_SIZE])
			b = tf.get_variable('outputs_bias', [VOCAB_SIZE])
			logits = tf.matmul(outputs, w) + b

		# ======計算損失=======
		with tf.name_scope('loss'):
			self.loss = tf.contrib.legacy_seq2seq.sequence_loss_by_example([logits], [tf.reshape(self.targets, [-1])], 
															[tf.ones([BATCH_SIZE * TIME_STEPS], dtype=tf.float32)])
			self.cost = tf.reduce_sum(self.loss) / BATCH_SIZE

		# =============優化算法==============
		with tf.name_scope('opt'):
            # =============學習率衰減==============
			global_step = tf.Variable(0)
			learning_rate = tf.train.exponential_decay(learning_rate, global_step, BATCH_NUMS, 0.99, staircase=True)

			# =======通過clip_by_global_norm()控制梯度大小======
			trainable_variables = tf.trainable_variables()
			grads, _ = tf.clip_by_global_norm(tf.gradients(self.cost, trainable_variables), MAX_GRAD_NORM)
			self.opt = tf.train.AdamOptimizer(learning_rate).apply_gradients(zip(grads, trainable_variables))

		# ==============預測輸出=============
		with tf.name_scope('predict'):
			self.predict = tf.argmax(logits, 1)

4.2 定義訓練參數及模型參數:

# =======預定義模型參數========
VOCAB_SIZE = len(vocab)
EPOCHS = 1
BATCH_SIZE = 8
TIME_STEPS = 100
BATCH_NUMS = len(numdata) // (BATCH_SIZE * TIME_STEPS)
HIDDEN_SIZE = 128
HIDDEN_LAYERS = 2
MAX_GRAD_NORM = 1
learning_rate = 0.003

4.3 模型訓練和保存

# ===========模型訓練===========
model = RNNModel(BATCH_SIZE, HIDDEN_SIZE, HIDDEN_LAYERS, VOCAB_SIZE, learning_rate)

# 保存模型
saver = tf.train.Saver()
with tf.Session() as sess:
	writer = tf.summary.FileWriter('logs/tensorboard', tf.get_default_graph())

	sess.run(tf.global_variables_initializer())
	for k in range(EPOCHS):
		state = sess.run(model.initial_state)
		train_data = data_generator(numdata, BATCH_SIZE, TIME_STEPS)
		total_loss = 0.
		for i in range(BATCH_NUMS):
			xs, ys = next(train_data)
			feed = {model.inputs: xs, model.targets: ys, model.keepprb: 0.8, model.initial_state: state}
			costs, state, _ = sess.run([model.cost, model.final_state, model.opt], feed_dict=feed)
			total_loss += costs
			if (i+1) % 50 == 0:
				print('epochs:', k + 1, 'iter:', i + 1, 'cost:', total_loss / i + 1)

	saver.save(sess, './checkpoints/lstm.ckpt')

writer.close()
epochs: 1 iter: 50 cost: 690.2989925462373
epochs: 1 iter: 100 cost: 669.3652756816209
epochs: 1 iter: 150 cost: 661.2328949998689
epochs: 1 iter: 200 cost: 655.4240072432475
epochs: 1 iter: 250 cost: 651.5086140460279
epochs: 1 iter: 300 cost: 646.8923176538984
epochs: 1 iter: 350 cost: 642.3045925369919
epochs: 1 iter: 400 cost: 637.6013468118539
epochs: 1 iter: 450 cost: 632.4249917351058

5. 評估準則與效果

文本生成的評估準則我們選擇loss和文本生成的效果作爲評估效果,其他的評估效果我自己確實也不是很瞭解。

# ============模型測試============
tf.reset_default_graph()
evalmodel = RNNModel(1, HIDDEN_SIZE, HIDDEN_LAYERS, VOCAB_SIZE, learning_rate)
# 加載模型
saver = tf.train.Saver()
with tf.Session() as sess:
	saver.restore(sess, './checkpoints/lstm.ckpt')
	new_state = sess.run(evalmodel.initial_state)
	x = np.zeros((1, 1)) + 8
	samples = []
	for i in range(100):
		feed = {evalmodel.inputs: x, evalmodel.keepprb: 1., evalmodel.initial_state: new_state}
		c, new_state = sess.run([evalmodel.predict, evalmodel.final_state], feed_dict=feed)
		x[0][0] = c[0]
		samples.append(c[0])
	print('test:', ''.join([id2char[index] for index in samples]))

INFO:tensorflow:Restoring parameters from ./checkpoints/lstm.ckpt
test: 人,我們的個人的個人的個人的個人的個人的個人的個人的個人的個人的個人的個人的個人的個人的個人的個人的個人的個人的個人的個人的個人的個人的個人的個人的個人的個人的個人的個人的個人的個人的個人的個人的個人

6. 模型的優化與提升

我使用的網絡結構比較簡單,2層lstm,每一層有128個節點,可以看出得到的結果也很一般。
模型的優化可以從網絡結構和學習率優化上進行提升。個人電腦能力有限,我選擇的網絡結構較爲簡單,可以嘗試更深的結構,更多的隱藏層節點數。就能得到更好的結果。

  • 下面是我用3層,512個節點在服務器上訓練了50個epochs得到的結果:
臭的氣味,這是燃燒玉米、寬大的、高傲的香氣。我的朋友頗爲一個人,都是一個人,都是一個陌生的人,我們的開放,是縣長的眷戀。我們的開放痛苦地說,是我們的驢踢死了。我們的開放怒罵着:“你們的驢,你們的驢,你
  • 下面是我用3層,1024個節點在服務器上訓練了50個epochs得到的結果:
子上的適食,都是我們的英雄,但他們的臉都沒有打死。我們的開放趴在臺階上,一個蹲在駕駛座上。小鐵匠將一個厚重的方凳放在了門外。我們的開放趴在臺階上,一個蹲在駕駛座上。吉普車沒有熄火,汽油味兒和機油昧兒和
  • 兩種結構的損失下降:
    在這裏插入圖片描述
  • 除此之外,還可以嘗試按照詞級別對應一個index進行建模方法建模。
import jieba
word_data = list(jieba.cut(data))

# =====生成字典=====
word_vocab = set(word_data)
id2word = list(word_vocab)
word2id = {c:i for i,c in enumerate(vocab)}

print(word_data[:100])
print(id2word[:100])
print(len(vocab))
Building prefix dict from the default dictionary ...
Loading model from cache C:\Users\Hongwen\AppData\Local\Temp\jieba.cache
Loading model cost 0.765 seconds.
Prefix dict has been built succesfully.


['我', '的', '故事', ',', '從', '1950', '年', '1', '月', '1', '日', '講', '起', '。', '在此之前', '兩年', '多', '的', '時間', '裏', ',', '我', '在', '陰曹地府', '裏', '受盡', '了', '人間', '難以想象', '的', '酷刑', '。', '每次', '提審', ',', '我', '都', '會', '鳴冤叫屈', '。', '我', '的', '聲音', '悲壯', '淒涼', ',', '傳播', '到', '閻羅', '大殿', '的', '每個', '角落', ',', '激發', '出', '重重疊疊', '的', '回聲', '。', '我', '身受', '酷刑', '而', '絕不', '改悔', ',', '掙得', '了', '一個', '硬漢子', '的', '名聲', '。', '我', '知道', '許多', '鬼', '卒', '對', '我', '暗中', '欽佩', ',', '我', '也', '知道', '閻王', '老子', '對', '我', '不勝', '厭煩', '。', '爲了', '讓', '我', '認罪', '服輸', ',']
['噴發', '木雕', '憂愁', '之期', '老狗', '嘮叨', '金龍略', '始終', '似乎', '堆成', '燒壞', '徵爲', '雲天', '養蠶', '十掛', '工業', '英武', '豈', '嘆息聲', '這句', '最', '幾十頭', '爆響', '黃毛', '孤孤單單', '兩棵樹', '密友', '狂奔', '女宿舍', '守舊', '灌', '時來運轉', '仔細', '撩起', '腳丫子', '盛傳', '悔恨交加', '表', '熊熊', '嗚嗚咽咽', '懸崖勒馬', '白天', '呼呼地', '邊際', '章魚', '表情', '在場', '扶持', '擒', '哨子', '箭簇', '地接', '鑽石戒指', '貸到', '忙不迭', '大丈夫', '噼裏啪啦', '法國梧桐', '的', '道聽途說', '甩幹', '慢慢來', '自明', '面兒', '玻璃器皿', '間或', '投', '禮儀', '反而', '相聚', '初產時', '馬上', '方家', '吉普車', '跌跌撞撞', '牛蛋', '心冷成', '持刀', '不哭', '燙得', '正道', '流出', '轉文', '白酒', '點點滴滴', '木匠', '沐浴', '空杯', '欠下', '權利', '高', '畏首', '錢花', '梢', '地去', '盤算着', '爲妻', '全喫', '當官', '跳下']
3892
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章