用Keras实现RNN+LSTM的模型自动编写古诗

记录下用Keras实现LSTM模型来自动编写古诗的过程查看原文
代码地址: Github

简介

目前RNN循环神经网络可以说是最强大的神经网络模型之一了,可以处理大量的序列数据,目前已经广泛用于语音识别、文本分类、自然语言处理中了。

现在有很多deep learning的框架能很方便的实现RNN的模型。因为作者比较倾向于Keras,所以本文的 代码都是以Keras框架编写的。

当然,网上也有很多其他框架实现的例子,本文中也借鉴了前人的一些代码。只是在学习的过程中,遇到了一些其他文章中没有讲清楚的问题,在这里记录下来。

模型

model.png

模型很简单,首先是两个LSTM+Dropout层,最后一层为全连接层,激活函数用的softmax。

语料数据

poetry.png

语料是从网上下载的,由于下载的时候没有记住来源,所以,默默地感谢前人已经做的准备工作呢。

一共四万多首古诗,每行一首诗,以分隔标题和诗句,让模型学习的是诗句,所以标题在预处理的时候会去掉。

文件预处理

首先,机器并不懂每个中文汉字代表的是什么,所以要将文字转换为机器能理解的形式。这里我们采用one-hot的形式,具体内容请自行查询。简单地说下就是将所有的文字组成一个字典,每个字就能用该字在字典里的序号表示。比如“我爱吃香蕉”,一共五个字,字典就是["我", "爱", "吃", "香", "蕉"],那么,"我"就能用[1,0,0,0,0]表示,所以“香蕉”用这种形式表示出来就是一个维度为(2, 5)的向量。

类似地,处理当前的诗句文件也是将所有的字组成一个字典,这样诗句中的每个字都能用向量来表示。

def preprocess_file(Config):
    # 语料文本内容
    files_content = ''
    with open(Config.poetry_file, 'r', encoding='utf-8') as f:
        for line in f:
            # 每行的末尾加上"]"符号代表一首诗结束
            files_content += line.strip() + "]".split(":")[-1]

    words = sorted(list(files_content))
    counted_words = {}
    for word in words:
        if word in counted_words:
            counted_words[word] += 1
        else:
            counted_words[word] = 1

    # 去掉低频的字
    erase = []
    for key in counted_words:
        if counted_words[key] <= 2:
            erase.append(key)
    for key in erase:
        del counted_words[key]
    wordPairs = sorted(counted_words.items(), key=lambda x: -x[1])

    words, _ = zip(*wordPairs)
    words += (" ",)
    # word到id的映射
    word2num = dict((c, i) for i, c in enumerate(words))
    num2word = dict((i, c) for i, c in enumerate(words))
    word2numF = lambda x: word2num.get(x, len(words) - 1)
    return word2numF, num2word, words, files_content

在每行末尾加上]符号是为了标识这首诗已经结束了。我们给模型学习的方法是,给定前六个字,生成第七个字,所以在后面生成训练数据的时候,会以6的跨度,1的步长截取文字,生成语料。比如“我要吃香蕉”,现在以3的跨度生成训练数据就是("我要吃", “香”)("要吃香", "蕉")。跨度为6的句子中,前后每个字都是有关联的。如果出现了]符号,说明]符号之前的语句和之后的语句是两首诗里面的内容,两首诗之间是没有关联关系的,所以我们后面会舍弃掉包含]符号的训练数据。

生成训练数据

再生成训练数据的时候先看下配置项:

class Config(object):
    # 语料文本
    poetry_file = 'poetry.txt'
    # 保存模型的文件名
    weight_file = 'poetry_model.h5'
    # 跨度
    max_len = 6
    # batch_size
    batch_size = 32
    # learning_rate 
    learning_rate = 0.001

配置很简单,主要就是语料文本的路径、保存模型的文件名等。
下面生成训练数据:

    def data_generator(self):
        '''生成数据'''
        i = 0
        while 1:
            x = self.files_content[i: i + self.config.max_len]
            y = self.files_content[i + self.config.max_len]

            if ']' in x or ']' in y:
                i += 1
                continue

            y_vec = np.zeros(
                shape=(1, len(self.words)),
                dtype=np.bool
            )
            y_vec[0, self.word2numF(y)] = 1.0

            x_vec = np.zeros(
                shape=(1, self.config.max_len, len(self.words)),
                dtype=np.bool
            )

            for t, char in enumerate(x):
                x_vec[0, t, self.word2numF(char)] = 1.0

            yield x_vec, y_vec
            i += 1

x表示输入,y表示输出,输入就是前六个字,输出即为第七个字。再将文字转换成向量的形式。
需要注意的是,这边的生成器是一个while 1的无限循环的过程,官网上是这样说的:

a tuple (inputs, targets, sample_weights). This tuple (a single output of the generator) makes a single batch. Therefore, all arrays in this tuple must have the same length (equal to the size of this batch). Different batches may have different sizes. For example, the last batch of the epoch is commonly smaller than the others, if the size of the dataset is not divisible by the batch size. The generator is expected to loop over its data indefinitely. An epoch finishes when steps_per_epoch batches have been seen by the model.

但是实际上,在我们的生成器中,当i+max_len > len(file_content)的时候,下标就已经超过语料的长度了。所以我们在后面会限制模型学习的循环次数。

构建模型

    def build_model(self):
        '''建立模型'''

        # 输入的dimension
        input_tensor = Input(shape=(self.config.max_len, len(self.words)))
        lstm = LSTM(512, return_sequences=True)(input_tensor)
        dropout = Dropout(0.6)(lstm)
        lstm = LSTM(256)(dropout)
        dropout = Dropout(0.6)(lstm)
        dense = Dense(len(self.words), activation='softmax')(dropout)
        self.model = Model(inputs=input_tensor, outputs=dense)
        optimizer = Adam(lr=self.config.learning_rate)
        self.model.compile(loss='categorical_crossentropy', optimizer=optimizer, metrics=['accuracy'])

这边没什么难度,因为Keras对各种layer都已经做了非常好的封装,我们只需要填好参数就好了。这边我有个建议是,在构建自己的神经网络的过程中,最好自己先把每层数据的维度计算出来,有益于排除错误。特别是input的维度。这里说个坑,在Keras中,需要区分两种维度的表示方式,channels_lastchannels_first,也就是tensorflow和Theao对维度的表示方法。

训练模型

 def train(self):
        '''训练模型'''
        number_of_epoch = len(self.words) // self.config.batch_size

        if not self.model:
            self.build_model()

        self.model.fit_generator(
            generator=self.data_generator(),
            verbose=True,
            steps_per_epoch=self.config.batch_size,
            epochs=number_of_epoch,
            callbacks=[
                keras.callbacks.ModelCheckpoint(self.config.weight_file, save_weights_only=False),
                LambdaCallback(on_epoch_end=self.generate_sample_result)
            ]
        )

steps_per_epoch表示在一个epoch中调用几次generator生成数据进行训练
number_of_epoch表示要训练多少个epoch
上面所说的,i+max_len的下标不能超过文本总长度,所以,当给定batch_size时,用文本总长度除以batch_size得到number_of_epoch

callbacks中定义了保存模型的回调函数和每个epoch结束后,随机生成诗句的函数,这样在学习的过程中,可以看到随着训练数据的正常,生成的诗句也越来越正常。

结果

训练次数较少的时候生成的诗句:


训练结束时生成的诗句:


虽然训练到最后写出的诗句都不怎么看得懂,但是,可以看到模型在学习到越来越多的数据后,从一开始标点符号都不会标,到最后写出了有一点点模样的诗句,能看到模型变得越来越聪明了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351

推荐阅读更多精彩内容