丘吉尔的人物传记char级别的文本生成代码注释

读取数据

举个小小的例子,来看看LSTM是怎么玩的

我们这里用温斯顿丘吉尔的人物传记作为我们的学习语料。

第一步,一样,先导入各种库

关于LSTM的详细原理先看博客:如何从RNN起步,一步一步通俗理解LSTM

In [68]:

1
2
3
4
5
6
7
import numpy
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
from keras.layers import LSTM
from keras.callbacks import ModelCheckpoint
from keras.utils import np_utils

In [69]:

1
2
3
4
raw_text = open('./input/Winston_Churchil.txt').read()
# .read() 读入整个文件为一个字符串
raw_text = raw_text.lower() # 小写
print(raw_text[:100]) #打印前100个字符
1
2
3
project gutenberg’s real soldiers of fortune, by richard harding davis

this ebook is for the use o

既然我们是以每个字母为层级,字母总共才26个,所以我们可以很方便的用One-Hot来编码出所有的字母(当然,可能还有些标点符号和其他noise)

In [70]:

1
2
3
4
5
6
7
8
chars = sorted(list(set(raw_text))) 
# 这里去重不是单词去重,是字母去重,去重后只有英文字母和标点符号等字符
print(len(raw_text))
print(len(chars))
char_to_int = dict((c, i) for i, c in enumerate(chars))
# 去重后的字符排序好后,xi
int_to_char = dict((i, c) for i, c in enumerate(chars))
print(char_to_int)
1
2
3
276830
61
{'\n': 0, ' ': 1, '!': 2, '#': 3, '$': 4, '%': 5, '(': 6, ')': 7, '*': 8, ',': 9, '-': 10, '.': 11, '/': 12, '0': 13, '1': 14, '2': 15, '3': 16, '4': 17, '5': 18, '6': 19, '7': 20, '8': 21, '9': 22, ':': 23, ';': 24, '?': 25, '@': 26, '[': 27, ']': 28, '_': 29, 'a': 30, 'b': 31, 'c': 32, 'd': 33, 'e': 34, 'f': 35, 'g': 36, 'h': 37, 'i': 38, 'j': 39, 'k': 40, 'l': 41, 'm': 42, 'n': 43, 'o': 44, 'p': 45, 'q': 46, 'r': 47, 's': 48, 't': 49, 'u': 50, 'v': 51, 'w': 52, 'x': 53, 'y': 54, 'z': 55, '‘': 56, '’': 57, '“': 58, '”': 59, '\ufeff': 60}

构造训练集

我们这里简单的文本预测就是,给了前置的字母以后,下一个字母是谁?
我们需要把我们的raw text变成可以用来训练的x,y:

x 是前置字母们 y 是后一个字母

In [71]:

1
2
3
4
5
6
7
8
9
10
11
seq_length = 100 
# 输入的字符的长度,一个字符对应一个神经元,总共100个神经元
# 输入是100个字符,输出是预测的一个字符

x = []
y = []
for i in range(0, len(raw_text) - seq_length): # 每次循环都滑动一个字符距离
given = raw_text[i:i + seq_length] # 从零先取前100个字符作为输入
predict = raw_text[i + seq_length] # y是后一个字符
x.append([char_to_int[char] for char in given]) # 把字符转化为向量
y.append(char_to_int[predict]) # # 把字符转化为向量

In [72]:

1
2
3
4
print(x[:3]) 
print(y[:3])
print(set(y)) # 这里注意下,'\ufeff': 60这个字符是文本的首字符,只有这么一个,y是取不到的,所以y值的可能只有60种
print(len(set(y)))
1
2
3
4
[[60, 45, 47, 44, 39, 34, 32, 49, 1, 36, 50, 49, 34, 43, 31, 34, 47, 36, 57, 48, 1, 47, 34, 30, 41, 1, 48, 44, 41, 33, 38, 34, 47, 48, 1, 44, 35, 1, 35, 44, 47, 49, 50, 43, 34, 9, 1, 31, 54, 1, 47, 38, 32, 37, 30, 47, 33, 1, 37, 30, 47, 33, 38, 43, 36, 1, 33, 30, 51, 38, 48, 0, 0, 49, 37, 38, 48, 1, 34, 31, 44, 44, 40, 1, 38, 48, 1, 35, 44, 47, 1, 49, 37, 34, 1, 50, 48, 34, 1, 44], [45, 47, 44, 39, 34, 32, 49, 1, 36, 50, 49, 34, 43, 31, 34, 47, 36, 57, 48, 1, 47, 34, 30, 41, 1, 48, 44, 41, 33, 38, 34, 47, 48, 1, 44, 35, 1, 35, 44, 47, 49, 50, 43, 34, 9, 1, 31, 54, 1, 47, 38, 32, 37, 30, 47, 33, 1, 37, 30, 47, 33, 38, 43, 36, 1, 33, 30, 51, 38, 48, 0, 0, 49, 37, 38, 48, 1, 34, 31, 44, 44, 40, 1, 38, 48, 1, 35, 44, 47, 1, 49, 37, 34, 1, 50, 48, 34, 1, 44, 35], [47, 44, 39, 34, 32, 49, 1, 36, 50, 49, 34, 43, 31, 34, 47, 36, 57, 48, 1, 47, 34, 30, 41, 1, 48, 44, 41, 33, 38, 34, 47, 48, 1, 44, 35, 1, 35, 44, 47, 49, 50, 43, 34, 9, 1, 31, 54, 1, 47, 38, 32, 37, 30, 47, 33, 1, 37, 30, 47, 33, 38, 43, 36, 1, 33, 30, 51, 38, 48, 0, 0, 49, 37, 38, 48, 1, 34, 31, 44, 44, 40, 1, 38, 48, 1, 35, 44, 47, 1, 49, 37, 34, 1, 50, 48, 34, 1, 44, 35, 1]]
[35, 1, 30]
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59}
60

此刻,楼上这些表达方式,类似就是一个词袋,或者说 index。

接下来我们做两件事:

1.我们已经有了一个input的数字表达(index),我们要把它变成LSTM需要的数组格式: [样本数,时间步伐,特征]

2.对于output,我们在Word2Vec里学过,用one-hot做output的预测可以给我们更好的效果,相对于直接预测一个准确的y数值的话。

In [73]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
n_patterns = len(x) # 样本数量
n_vocab = len(chars) #

# 把x变成LSTM需要的样子
x = numpy.reshape(x, (n_patterns, seq_length, 1))
# n_patterns 样本的数量
# seq_length 每个样本每次训练100个字符
# 1 代表我们的一个字符的维度是1维,这里1维是特殊情况,如果我们的输入不是一个字符,
#是一个单词的话,单词是可以embedding成100维向量,那这里就是100.

x = x / float(n_vocab) # 简单normal到0-1之间,归一化,防止梯度爆炸
y = np_utils.to_categorical(y)
# y值总共取值61种,直接做个onehot编码
# 对类别进行onehot编码一个很重要的原因在于计算loss时的问题。loss一般用距离来表示,
# 如果用1~5来表示,那么1和2的距离是1,而1和5的距离是4,但是按道理1和2、1和5的距离应该一样。
# 如果用one hot编码表示,那么1和2的距离跟1和5的距离时一样的。

print(x[10][:10]) # 第10个样本的前10个字符向量
print(y[10])
print(y.shape) #
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[[0.81967213]
[0.80327869]
[0.55737705]
[0.70491803]
[0.50819672]
[0.55737705]
[0.7704918 ]
[0.59016393]
[0.93442623]
[0.78688525]]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0.
0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
(276730, 60)

构建和训练模型

In [74]:

1
2
3
4
5
6
7
8
9
10
model = Sequential()
# Sequential():序贯模型是多个网络层的线性堆叠,可以通过.add()一个一个添加层
model.add(LSTM(256, input_shape=(x.shape[1], x.shape[2])))
# 256是LSTM的隐藏层的维度。
# input_shape,不需要考虑样本数量,对模型本身结构没有意义
model.add(Dropout(0.2)) # 去掉20%的神经元
model.add(Dense(y.shape[1], activation='softmax'))
# y.shape[1] = 60,'softmax'转化为概率,概率和为1
model.compile(loss='categorical_crossentropy', optimizer='adam')
# 多分类交叉熵损失,adam优化器

In [75]:

1
2
model.fit(x, y, nb_epoch=1, batch_size=1024)
# 这里本地跑的太慢了,nb_epoch设置为1了
1
2
/Users/yyg/anaconda3/lib/python3.6/site-packages/ipykernel_launcher.py:1: UserWarning: The `nb_epoch` argument in `fit` has been renamed `epochs`.
"""Entry point for launching an IPython kernel.
1
2
Epoch 1/1
276730/276730 [==============================] - 1152s 4ms/step - loss: 3.0591

Out[75]:

1
<keras.callbacks.History at 0xb27cb07f0>

预测模型

In [95]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def predict_next(input_array):
x = numpy.reshape(input_array, (1, seq_length, 1))
# 同上,只有一个样本,reshape成lstm需要的维度。
x = x / float(n_vocab) # 归一化
y = model.predict(x) # y是60维的向量
return y

def string_to_index(raw_input):
res = []
for c in raw_input[(len(raw_input)-seq_length):]:
# 这步一个问题是len(raw_input)一定要大于seq_length:100,不然会报错
res.append(char_to_int[c]) # 得到输入句子中后100个字符的向量表示
return res

def y_to_char(y):
largest_index = y.argmax() # 取出概率最大的值对应的索引
c = int_to_char[largest_index] # 根据索引得到对应的字符
return c

In [96]:

1
2
3
4
5
6
7
8
9
def generate_article(init, rounds=200): 
# rounds=200 代表预测生成200个新的字符
# init是输入的字符串
in_string = init.lower() # 跟上面一样的预处理步骤
for i in range(rounds): # 每次模型预测一个字符
n = y_to_char(predict_next(string_to_index(in_string)))
# n是预测的新的字符
in_string += n # 把新的字符拼接,重新作为新的输入
return in_string

In [97]:

1
2
3
init = 'His object in coming to New York was to engage officers for that service. He came at an opportune moment'
article = generate_article(init)
print(article) # 这里上面只是迭代了一次,导致后面所有的预测都是空格
1
his object in coming to new york was to engage officers for that service. he came at an opportune moment

In [ ]:

1
 
文章目录
  1. 1. 读取数据
  2. 2. 构造训练集
  3. 3. 构建和训练模型
    1. 3.1. 预测模型
|