RNN & LSTM

如果没有安装 keras 和 tensorflow 库

请使用 pip install keras tensorflow 安装

如果使用conda虚拟环境

请使用conda install -c conda-forge keras

conda install -c conda-forge tensorflow

In [1]:
import itertools
import jieba
import numpy as np
from collections import Counter
from keras.models import Model, Sequential
from keras.layers import InputLayer, Embedding, LSTM, Dense, TimeDistributed, SimpleRNN
from keras.optimizers import SGD, Adam, Adadelta, RMSprop
Using TensorFlow backend.

build_vocab函数的作用是对输入的text列表建立词典vocab和vocab_inv

其中vocab是单词到编号的映射

vocab_inv是编号到单词的映射

In [2]:
def build_vocab(text, vocab_lim):
    # Counter工具可以对列表对象进行计数
    word_cnt = Counter(text)
    # most_common(vocab_lim)可以返回一列(单词,次数)的元组
    # x[0]就是把单词从元组里取出来
    vocab_inv = [x[0] for x in word_cnt.most_common(vocab_lim)]
    # 有了vocab_inv以后,我们可以建立它的逆映射
    vocab = {x: index for index, x in enumerate(vocab_inv)}
    return vocab, vocab_inv
In [3]:
vocab, vocab_inv = build_vocab(['a', 'a', 'b', 'c', 'a', 'd', 'd', 'e'], 10)
print(vocab)
print(vocab_inv)
{'a': 0, 'd': 1, 'b': 2, 'c': 3, 'e': 4}
['a', 'd', 'b', 'c', 'e']

process_file的作用是读入路径为file_name的文件,并根据use_char_based_model来选择对文件内容的切割方式

如果use_char_based_model = True,那么返回的列表是将原文件内容按字分开

如果use_char_based_model = False, 那么返回的列表是将原文件中文分词

In [4]:
def process_file(file_name, use_char_based_model):
    raw_text = []
    # 打开文件
    with open(file_name, "r") as f:
        # 遍历文件中的每一行
        for line in f:
            if (use_char_based_model):
                # 分字
                raw_text.extend([str(ch) for ch in line])
            else:
                # 分词
                raw_text.extend([word for word in jieba.cut(line)])
    return raw_text

build_matrix用于构建训练数据,输入文本text和词典vocab,length是一对数据的长度,step表示每隔step个位置取一对数据

训练数据是一个输入输出对,根据我们对RNN语言模型的理解,输入是$(w_{i}, w_{i+1}, \dots, w_{i+length})$的话,输出是$(w_{i+1}, w_{i+2}, \dots, w_{i+length+1})$

In [5]:
def build_matrix(text, vocab, length, step):
    M = []
    # 将字符数组text转化为对应的编号数组M
    for word in text:
        # 得到word的编号
        index = vocab.get(word)
        if (index is None):
            # 如果word不在词典里,统一编号为vocab的长度,比如vocab原来有4000个单词,它们标号为0~3999,4000是给不在词典里的词统一预留的编号
            M.append(len(vocab))
        else:
            # 否则就取词典里的编号
            M.append(index)
    num_sentences = len(M) // length

    X = []
    Y = []
    # 从0开始,每隔step个位置取一对数据
    for i in range(0, len(M) - length, step):
        # [M_{i}, M_{i+1}, ..., M_{i+length}]是输入
        X.append(M[i : i + length])
        # [M_{i+1}, M_{i+2}, ..., M_{i+length+1}]是输出
        Y.append([[x] for x in M[i + 1 : i + length + 1]])
    return np.array(X), np.array(Y)
In [6]:
# 设置每对数据的长度为11
seq_length = 11
# 得到文本分割后的序列,我们采用分字模型
raw_text = process_file("../input/poetry.txt", True)
# 建立词典,词频排名4000名以后的词不计入词典
vocab, vocab_inv = build_vocab(raw_text, 4000)
print(len(vocab), len(vocab_inv))
# 构建训练数据
X, Y = build_matrix(raw_text, vocab, seq_length, seq_length)
print(X.shape)
print(Y.shape)
print(X[0])
print(Y[0])
4000 4000
(85698, 11)
(85698, 11, 1)
[ 42 168 360 935 567   0  12 369 110  65  80]
[[168]
 [360]
 [935]
 [567]
 [  0]
 [ 12]
 [369]
 [110]
 [ 65]
 [ 80]
 [  1]]
In [7]:
# 搭建模型
model = Sequential()
# 输入层 设定形状为(None,)可以使模型接受变长输入
# 注意到我们在构建训练数据的时候让每次输入的长度都是seq_length,是因为我们要用batch训练的方式来提高并行度
# 同样长度的数据可以在一次运算中完成,不同长度的只能逐次计算
# 这一层输出的形状是(seq_length, )
model.add(InputLayer(input_shape=(None, )))
# 词嵌入层,input_dim表示词典的大小,大小4000的词典一共有0~4000个标号的词,每种标号对应一个output_dim维的词向量,设置trainable=True使得词向量矩阵可以调整
# 这一层的输出形状是(seq_length, 256, )
model.add(Embedding(input_dim=len(vocab) + 1, output_dim=256, trainable=True))
# RNN层,输出形状是(seq_length, 256, )
model.add(SimpleRNN(units=256, return_sequences=True))
# 每个时刻输出的256维向量需要经过一次线性变换(也就是Dense层)转化为4001维的向量,用softmax变换转化为一个4001维的概率概率分布,第i维表示下一个时刻的词是i号单词的概率
# Dense即线性变换,TimeDistributed表示对每个时刻都如此作用
# 这一层的输出形状是(seq_length, len(vocab) + 1)
model.add(TimeDistributed(Dense(units=len(vocab) + 1, activation='softmax')))

# 定义loss函数为交叉熵,优化器为Adam,学习率为0.01
model.compile(optimizer=Adam(lr=0.001), loss='sparse_categorical_crossentropy')
model.summary()
Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding_1 (Embedding)      (None, None, 256)         1024256   
_________________________________________________________________
simple_rnn_1 (SimpleRNN)     (None, None, 256)         131328    
_________________________________________________________________
time_distributed_1 (TimeDist (None, None, 4001)        1028257   
=================================================================
Total params: 2,183,841
Trainable params: 2,183,841
Non-trainable params: 0
_________________________________________________________________
In [8]:
# 训练
model.fit(X, Y, batch_size=512, epochs=18, verbose=1, validation_split=0.1)
Train on 77128 samples, validate on 8570 samples
Epoch 1/18
77128/77128 [==============================] - 4s 51us/step - loss: 6.3816 - val_loss: 6.0000
Epoch 2/18
77128/77128 [==============================] - 3s 42us/step - loss: 5.7861 - val_loss: 5.5775
Epoch 3/18
77128/77128 [==============================] - 3s 43us/step - loss: 5.4058 - val_loss: 5.3178
Epoch 4/18
77128/77128 [==============================] - 3s 42us/step - loss: 5.2146 - val_loss: 5.2097
Epoch 5/18
77128/77128 [==============================] - 3s 42us/step - loss: 5.0957 - val_loss: 5.1119
Epoch 6/18
77128/77128 [==============================] - 3s 43us/step - loss: 4.9761 - val_loss: 5.0260
Epoch 7/18
77128/77128 [==============================] - 3s 43us/step - loss: 4.8733 - val_loss: 4.9607
Epoch 8/18
77128/77128 [==============================] - 3s 42us/step - loss: 4.7859 - val_loss: 4.9087
Epoch 9/18
77128/77128 [==============================] - 3s 42us/step - loss: 4.7093 - val_loss: 4.8694
Epoch 10/18
77128/77128 [==============================] - 3s 42us/step - loss: 4.6386 - val_loss: 4.8396
Epoch 11/18
77128/77128 [==============================] - 3s 43us/step - loss: 4.5734 - val_loss: 4.8069
Epoch 12/18
77128/77128 [==============================] - 3s 42us/step - loss: 4.5120 - val_loss: 4.7847
Epoch 13/18
77128/77128 [==============================] - 3s 42us/step - loss: 4.4555 - val_loss: 4.7608
Epoch 14/18
77128/77128 [==============================] - 3s 42us/step - loss: 4.4020 - val_loss: 4.7458
Epoch 15/18
77128/77128 [==============================] - 3s 43us/step - loss: 4.3521 - val_loss: 4.7336
Epoch 16/18
77128/77128 [==============================] - 3s 42us/step - loss: 4.3044 - val_loss: 4.7256
Epoch 17/18
77128/77128 [==============================] - 3s 42us/step - loss: 4.2606 - val_loss: 4.7171
Epoch 18/18
77128/77128 [==============================] - 3s 42us/step - loss: 4.2187 - val_loss: 4.7167
Out[8]:
<keras.callbacks.callbacks.History at 0x7fed7111ecf8>
In [9]:
st = '明月松间照'
print(st, end='')

vocab_inv.append(' ')

for i in range(200):
    X_sample = np.array([[vocab.get(x, len(vocab)) for x in st]])
    pdt = (-model.predict(X_sample)[:, -1: , :][0][0]).argsort()[:4]
    if vocab_inv[pdt[0]] == '\n' or vocab_inv[pdt[0]] == ',' or vocab_inv[pdt[0]] == '。':
        ch = vocab_inv[pdt[0]]
    else:
        ch = vocab_inv[np.random.choice(pdt)]
    print(ch, end='')
    st = st + ch
明月松间照,秋来雪里春。
一年一相见,况乃长天路。
一旦暮云里,何人无路难。
一日无心事,何时见我情。
不见青山客,相随日月中。
不知人不得,犹在故人家。
一叶风光晚,秋风叶未生。
不堪秋草树,时向白衣生。
白日无尘起,江湖有意多。
秋山满江水,月影入秋塘。
草色生秋草,秋深不到山。
山河一半月,风雨夜中秋。
草木秋山近,林花雨后新。
春光不相送,春去一沾巾。
一日风前月,空门月上楼。
不知人不得,不得更无