壹.古诗词
古诗词宽泛的来说,就是古人写的诗词,不过其大多具有以下特点:
1.句子的整齐
古典诗歌,除了词和曲以外,多数是句子长短整齐的,如《诗经》基本上是四言,《楚辞》大体上是六言加上“兮”字,古体诗和近体诗大多数是五言或七言。
2.平仄和对仗
平、仄是汉语声调的两大类。在近体诗和词、曲中,用字的平仄有相当严格的规定,在一些位置上,必须用平声字,在另一些位置上,必须用仄声字。比如:“国破山河在,城春草木深”(杜甫《春望》),就是用“仄仄仄平平,平平仄仄平”句式。
对仗指的是一联诗中,在上下句相同位置上的字要属于同一类,如“东圃青梅发,西园绿草开”。“东”和“西”,“圃”和“园”,“青梅”和“绿草”,“发”和“开”,各自相对。
3.词藻和句法
因为每一个汉字基本上都是一个兼具形音义的独立单位,而且很多汉字是一字多义的,字与字之间粘合的关系多种多样,所以,这种粘合而成的诗歌中的词藻,就显得异常复杂多样。例如,在“风”字后面再加一字,可以构成很多词语:风姿、风物、风雷、风霜、风鬟等。
在句法方面,由于汉语的特点,以及汉字的独立性,在古典诗歌中,常常出现把两个汉字拆开,或者把某些汉字从后面移到前面的情形,这叫倒装。典型的诗句是杜甫的“香稻啄余鹦鹉粒,碧梧栖老凤凰枝”,正常的句法应是“鹦鹉啄余香稻粒,凤凰栖老碧梧枝”。
4.节奏和韵律
从句式上看,古诗一般四字为二、二;五字为二、二、一;七字为二、二、二、一。从意义上看,有时因表意需要也有特殊情况如:势拔|五岳|掩|赤城,这样就成了二、二、一、二式。
古诗要求押韵,使音调和谐优美,如李白《静夜思》押ang韵。押韵韵脚的位置一般在偶数句末,如李白《静夜思》“床前明月光,疑是地上霜,举头望明月,低头思故乡。”“光”“霜”“乡”是韵脚。通常奇数句不押韵,首句入韵格式除外。
5.字数短小但表意丰富
诗词的字数一般只有几十个字,五言绝句是20个字,七言绝句是28个字,六言绝句是24个字,五言律诗,是40个字,七言律诗是56个字。但其表达的意义却是非常丰富的,如纳兰性德的《浣溪沙》:
谁念西风独自凉,萧萧黄叶闭疏窗,沉思往事立残阳。
被酒莫惊春睡重,赌书消得泼茶香,当时只道是寻常。
短短数十字,却将对妻子的思念之情表现的淋漓尽致,也留下了千古名句:“当时只道是寻常。”
表达时光易逝,流年匆匆的句子:
最是人间留不住,朱颜辞镜花辞树。
表达建功立业,驱逐外敌之心:
会挽雕弓如满月,西北望,射天狼。
男儿何不带吴钩,收取关山五十州。
请君暂上凌烟阁,若个书生万户侯。
表达人生志向:
安能摧眉折腰事权贵,使我不得开心颜!
表达思念之情:
衣带渐宽终不悔,为伊消得人憔悴。
.......
贰.代码
现在由于我们的语言体系,已经和以前的古语不一样,所以说我们的思维很少能够做到像古人那样用寥寥数字去表达自己的感情或者是描述看到的景象。
好在天无绝人之路,随着计算机的出现,深度学习的出现,人工智能的出现,这个问题变得可以解决。
我们的目的是学习古人的表达方式,创作出辞藻华丽,表意丰富的诗词。
那么我们可以用深度学习来实现这一功能:
基于循环神经网络实现的一个古诗生成器http://AaronJny/DeepLearningExamples/tf2-rnn-poetry-generator (https://github.com/AaronJny/DeepLearningExamples/tree/master/tf2-rnn-poetry-generator 训练数据是一个.txt文件,里面存储了大量的诗词:
我们 对数据进行以下几个方面的处理:
1.读取文本,按行切分,构成古诗列表。
2.将全角、半角的冒号统一替换成半角的。
3.按冒号切分诗的标题和内容,只保留诗的内容。
4.考虑到模型的大小,我们只保留内容长度小于一定长度的古诗。
5.统计保留的诗中的词频,去掉低频词,构建词汇表。
加载数据:
# 加载数据集
with open(settings.DATASET_PATH, 'r', encoding='utf-8') as f:
lines = f.readlines()
# 将冒号统一成相同格式
lines = [line.replace(':', ':') for line in lines]
# 数据集列表
poetry = []
# 逐行处理读取到的数据
for line in lines:
# 有且只能有一个冒号用来分割标题
if line.count(':') != 1:
continue
# 后半部分不能包含禁止词
__, last_part = line.split(':')
ignore_flag = False
for dis_word in disallowed_words:
if dis_word in last_part:
ignore_flag = True
break
if ignore_flag:
continue
# 长度不能超过最大长度
if len(last_part) > max_len - 2:
continue
poetry.append(last_part.replace('\n', ''))
统计词频:
# 统计词频
counter = Counter()
for line in poetry:
counter.update(line)
# 过滤掉低频词
_tokens = [(token, count) for token, count in counter.items() if count >= min_word_frequency]
# 按词频排序
_tokens = sorted(_tokens, key=lambda x: -x[1])
# 去掉词频,只保留词列表
_tokens = [token for token, count in _tokens]
# 将特殊词和数据集中的词拼接起来
_tokens = ['[PAD]', '[UNK]', '[CLS]', '[SEP]'] + _tokens
# 创建词典 token->id映射关系
token_id_dict = dict(zip(_tokens, range(len(_tokens))))
# 使用新词典重新建立分词器
tokenizer = Tokenizer(token_id_dict)
# 混洗数据
np.random.shuffle(poetry)
构造数据生成器:
class PoetryDataGenerator:
"""
古诗数据集生成器
"""
def __init__(self, data, random=False):
# 数据集
self.data = data
# batch size
self.batch_size = batch_size
# 每个epoch迭代的步数
self.steps = int(math.floor(len(self.data) / self.batch_size))
# 每个epoch开始时是否随机混洗
self.random = random
def sequence_padding(self, data, length=None, padding=None):
"""
将给定数据填充到相同长度
:param data: 待填充数据
:param length: 填充后的长度,不传递此参数则使用data中的最大长度
:param padding: 用于填充的数据,不传递此参数则使用[PAD]的对应编号
:return: 填充后的数据
"""
# 计算填充长度
if length is None:
length = max(map(len, data))
# 计算填充数据
if padding is None:
padding = tokenizer.token_to_id('[PAD]')
# 开始填充
outputs = []
for line in data:
padding_length = length - len(line)
# 不足就进行填充
if padding_length > 0:
outputs.append(np.concatenate([line, [padding] * padding_length]))
# 超过就进行截断
else:
outputs.append(line[:length])
return np.array(outputs)
def __len__(self):
return self.steps
def __iter__(self):
total = len(self.data)
# 是否随机混洗
if self.random:
np.random.shuffle(self.data)
# 迭代一个epoch,每次yield一个batch
for start in range(0, total, self.batch_size):
end = min(start + self.batch_size, total)
batch_data = []
# 逐一对古诗进行编码
for single_data in self.data[start:end]:
batch_data.append(tokenizer.encode(single_data))
# 填充为相同长度
batch_data = self.sequence_padding(batch_data)
# yield x,y
yield batch_data[:, :-1], tf.one_hot(batch_data[:, 1:], tokenizer.vocab_size)
del batch_data
def for_fit(self):
"""
创建一个生成器,用于训练
"""
# 死循环,当数据训练一个epoch之后,重新迭代数据
while True:
# 委托生成器
yield from self.__iter__()
构建模型:
# -*- coding: utf-8 -*-
# @File : model.py
# @Author : AaronJny
# @Time : 2020/01/01
# @Desc :
import tensorflow as tf
from dataset import tokenizer
# 构建模型
model = tf.keras.Sequential([
# 不定长度的输入
tf.keras.layers.Input((None,)),
# 词嵌入层
tf.keras.layers.Embedding(input_dim=tokenizer.vocab_size, output_dim=128),
# 第一个LSTM层,返回序列作为下一层的输入
tf.keras.layers.LSTM(128, dropout=0.5, return_sequences=True),
# 第二个LSTM层,返回序列作为下一层的输入
tf.keras.layers.LSTM(128, dropout=0.5, return_sequences=True),
# 对每一个时间点的输出都做softmax,预测下一个词的概率
tf.keras.layers.TimeDistributed(tf.keras.layers.Dense(tokenizer.vocab_size, activation='softmax')),
])
# 查看模型结构
model.summary()
# 配置优化器和损失函数
model.compile(optimizer=tf.keras.optimizers.Adam(), loss=tf.keras.losses.categorical_crossentropy)
编写随机生成诗词和随机生成藏头诗的函数:
# -*- coding: utf-8 -*-
# @File : utils.py
# @Author : AaronJny
# @Time : 2019/12/30
# @Desc :
import numpy as np
import settings
def generate_random_poetry(tokenizer, model, s=''):
"""
随机生成一首诗
:param tokenizer: 分词器
:param model: 用于生成古诗的模型
:param s: 用于生成古诗的起始字符串,默认为空串
:return: 一个字符串,表示一首古诗
"""
# 将初始字符串转成token
token_ids = tokenizer.encode(s)
# 去掉结束标记[SEP]
token_ids = token_ids[:-1]
while len(token_ids) < settings.MAX_LEN:
# 进行预测,只保留第一个样例(我们输入的样例数只有1)的、最后一个token的预测的、不包含[PAD][UNK][CLS]的概率分布
output = model(np.array([token_ids, ], dtype=np.int32))
_probas = output.numpy()[0, -1, 3:]
del output
# print(_probas)
# 按照出现概率,对所有token倒序排列
p_args = _probas.argsort()[::-1][:100]
# 排列后的概率顺序
p = _probas[p_args]
# 先对概率归一
p = p / sum(p)
# 再按照预测出的概率,随机选择一个词作为预测结果
target_index = np.random.choice(len(p), p=p)
target = p_args[target_index] + 3
# 保存
token_ids.append(target)
if target == 3:
break
return tokenizer.decode(token_ids)
def generate_acrostic(tokenizer, model, head):
"""
随机生成一首藏头诗
:param tokenizer: 分词器
:param model: 用于生成古诗的模型
:param head: 藏头诗的头
:return: 一个字符串,表示一首古诗
"""
# 使用空串初始化token_ids,加入[CLS]
token_ids = tokenizer.encode('')
token_ids = token_ids[:-1]
# 标点符号,这里简单的只把逗号和句号作为标点
punctuations = [',', '。']
punctuation_ids = {tokenizer.token_to_id(token) for token in punctuations}
# 缓存生成的诗的list
poetry = []
# 对于藏头诗中的每一个字,都生成一个短句
for ch in head:
# 先记录下这个字
poetry.append(ch)
# 将藏头诗的字符转成token id
token_id = tokenizer.token_to_id(ch)
# 加入到列表中去
token_ids.append(token_id)
# 开始生成一个短句
while True:
# 进行预测,只保留第一个样例(我们输入的样例数只有1)的、最后一个token的预测的、不包含[PAD][UNK][CLS]的概率分布
output = model(np.array([token_ids, ], dtype=np.int32))
_probas = output.numpy()[0, -1, 3:]
del output
# 按照出现概率,对所有token倒序排列
p_args = _probas.argsort()[::-1][:100]
# 排列后的概率顺序
p = _probas[p_args]
# 先对概率归一
p = p / sum(p)
# 再按照预测出的概率,随机选择一个词作为预测结果
target_index = np.random.choice(len(p), p=p)
target = p_args[target_index] + 3
# 保存
token_ids.append(target)
# 只有不是特殊字符时,才保存到poetry里面去
if target > 3:
poetry.append(tokenizer.id_to_token(target))
if target in punctuation_ids:
break
return ''.join(poetry)
进行训练:
# -*- coding: utf-8 -*-
# @File : train.py
# @Author : AaronJny
# @Time : 2020/01/01
# @Desc :
import tensorflow as tf
from dataset import PoetryDataGenerator, poetry, tokenizer
from model import model
import settings
import utils
import os
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
os.environ["CUDA_VISIBLE_DEVICES"] = "1"
os.environ["HDF5_USE_FILE_LOCKING"] = 'FALSE'
class Evaluate(tf.keras.callbacks.Callback):
"""
在每个epoch训练完成后,保留最优权重,并随机生成settings.SHOW_NUM首古诗展示
"""
def __init__(self):
super().__init__()
# 给loss赋一个较大的初始值
self.lowest = 1e10
def on_epoch_end(self, epoch, logs=None):
# 在每个epoch训练完成后调用
# 如果当前loss更低,就保存当前模型参数
if logs['loss'] <= self.lowest:
self.lowest = logs['loss']
model.save(settings.BEST_MODEL_PATH)
# 随机生成几首古体诗测试,查看训练效果
print()
for i in range(settings.SHOW_NUM):
print(utils.generate_random_poetry(tokenizer, model))
# 创建数据集
data_generator = PoetryDataGenerator(poetry, random=True)
# 开始训练
model.fit_generator(data_generator.for_fit(), steps_per_epoch=data_generator.steps, epochs=settings.TRAIN_EPOCHS,callbacks=Evaluate())
训练完成后保留一个权重文件,使用这个权重文件来进行模型的预测:
# -*- coding: utf-8 -*-
# @File : eval.py
# @Author : AaronJny
# @Time : 2020/01/01
# @Desc :
import tensorflow as tf
from dataset import tokenizer
import settings
import utils
# 加载训练好的模型
model = tf.keras.models.load_model(settings.BEST_MODEL_PATH)
# 随机生成一首诗
print(utils.generate_random_poetry(tokenizer, model))
# 给出部分信息的情况下,随机生成剩余部分
print(utils.generate_random_poetry(tokenizer, model, s='又岂在朝朝暮暮'))
# 生成藏头诗
print(utils.generate_acrostic(tokenizer, model, head='天下无贼'))
结果如下:
这样的诗词形式上与古人的表达相似,但是表意上就不知道说的什么了,希望能够找到表意准确且丰富的深度学习方法。
不过这也算是完成了写诗词的第一步:“写”,这也是一个很nice的事情了。