使用transformer实现超高质量唐诗生成

0、概述

唐诗生成在汉语的nlp领域应用非常广泛,从传统的RNN、LSTM、Attention生成质量被不断提升。随着Transformer模型提出很多NLP的深度学习模型都被改写。那么Transformer在唐诗生成领域的表现如何呢。我们来看一下,本文通过通过实例的方式详细描述了transformer的基本结构,以及唐诗生成的基本步骤。本文使用的框架为tensorflow2.2.

1、加载环境

import matplotlib as mpl
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
import pandas as pd
import sklearn
import os
import sys
import time
import tensorflow as tf
from tensorflow import keras

import re
import jieba
import opencc
import io

1-1、优化cpu按需使用

gpus = tf.config.experimental.list_physical_devices('GPU')
if gpus:
    try:
        # Currently, memory growth needs to be the same across GPUs
        for gpu in gpus:
            tf.config.experimental.set_memory_growth(gpu, True)
            logical_gpus = tf.config.experimental.list_logical_devices('GPU')
            print(len(gpus), "Physical GPUs,", len(logical_gpus), "Logical GPUs")
    except RuntimeError as e:
        # Memory growth must be set before GPUs have been initialized
        print(e)

2、加载数据集

将繁体字转换为简体字

cc = opencc.OpenCC('t2s')
def preprocess_sentence_cn(w):
  #将繁体字转换为简体字  
    w = cc.convert(w)
    w = ' '.join(list(w))
    w = re.sub(r'[" "]+', " ", w)
    w = w.strip().rstrip()
    w = '<start> ' + w + ' <end>'
    return w

创建数据集

def create_dataset(path):
    lines = io.open(path,encoding='utf8').read().strip().split('\n')
   
    sentence_pairs = [[preprocess_sentence_cn(w) for w in line.split(' ')] for line in lines]
    return zip(*sentence_pairs)
train,targ=create_dataset('../data/poem5.txt')

预览数据

print(len(train),len(targ))
print(train[0],targ[0])

输出结果如下

570159 570159
<start> 秦 川 雄 帝 宅 <end> <start> 函 谷 壮 皇 居 <end>

构建字典 id 与 汉字的双向映射函数

def tokenize(lang):
    tokenizer=keras.preprocessing.text.Tokenizer(filters='')
    tokenizer.fit_on_texts(lang)
    tensor=tokenizer.texts_to_sequences(lang)
    tensor=keras.preprocessing.sequence.pad_sequences(tensor,padding='post')
    return tensor,tokenizer
def load_dataset(inp_lang,targ_lang):
    input_tensor, inp_lang_tokenizer = tokenize(inp_lang)
    target_tensor, targ_lang_tokenizer = tokenize(targ_lang)
    return input_tensor, target_tensor, inp_lang_tokenizer, targ_lang_tokenizer

测试代码

input_tensor, target_tensor, inp_lang, targ_lang = load_dataset(train,targ)
for item in input_tensor[0]:
    print('%d ---> %s'%(item,inp_lang.index_word[item]))
print("===================================================")
for item in target_tensor[0]:
    print('%d ---> %s'%(item,targ_lang.index_word[item]))

输出效果如下

1 ---> <start>
540 ---> 秦
411 ---> 川
853 ---> 雄
443 ---> 帝
755 ---> 宅
2 ---> <end>
===================================================
1 ---> <start>
2168 ---> 函
471 ---> 谷
813 ---> 壮
660 ---> 皇
205 ---> 居
2 ---> <end>

 取句子最大汉字数

def max_length(tensor):
    return max(len(t) for t in tensor)
max_length_targ, max_length_inp = max_length(target_tensor), max_length(input_tensor)

2-1 分割数据集

from sklearn.model_selection import train_test_split
input_tensor_train, input_tensor_val, target_tensor_train, target_tensor_val=sklearn.model_selection.train_test_split(input_tensor,target_tensor,test_size=0.2)

2-2 生成数据集

# 定义缓冲区大小 2000
BUFFER_SIZE = len(input_tensor_train)
#定义批次内数据量
BATCH_SIZE = 512
#定义每一轮训练需要经过多少批次
steps_per_epoch = len(input_tensor_train)//BATCH_SIZE
# 定义字典大小,由于字典本身从1开始,0默认作为padding元素所以真正的字典大小需要加1
vocab_inp_size = len(inp_lang.word_index)+1
vocab_tar_size = len(targ_lang.word_index)+1
dataset = tf.data.Dataset.from_tensor_slices((input_tensor_train, target_tensor_train)).shuffle(BUFFER_SIZE)
dataset = dataset.cache()
dataset = dataset.batch(BATCH_SIZE, drop_remainder=True)
dataset = dataset.prefetch(tf.data.experimental.AUTOTUNE)

2-3 验证数据集形状

example_input_batch, example_target_batch = next(iter(dataset))
example_input_batch.shape, example_target_batch.shape

输出如下

(TensorShape([2048, 7]), TensorShape([2048, 7]))

3 构建transformer模型

3-1 详解位置编码

3-1-1 位置编码设计意图

  • 它能为每个时间步输出一个独一无二的编码;
  • 不同长度的句子之间,任何两个时间步之间的距离应该保持一致;
  • 模型应该能毫不费力地泛化到更长的句子。它的值应该是有界的;
  • 它必须是确定性的。

3-1-2 位置编码公式

PE_{(pos,2i)}=sin(pos/{10000^{2i/d_{model}}})

PE_{(pos,2i+1)}=cos(pos/{10000^{2i/d_{model}}})

  • 其中 i 为 1到 d/2 的均匀分布
  • pos 为当前单词在整个单词序列中的位置 取值范围 0 到 pos-1
  • d_{model}代表向量的维度由于后期在做self_attention时需要和单词的 embedding相加所有该项取值为 embedding_dim = 256

3-1-3 编程实现位置编码

def get_angles(pos, i, d_model):
    angle_rates = 1 / np.power(10000, (2 * (i//2)) / np.float32(d_model))
    return pos * angle_rates
def positional_encoding(position, d_model):
    angle_rads = get_angles(np.arange(position)[:, np.newaxis],
                          np.arange(d_model)[np.newaxis, :],
                          d_model)

    # 将 sin 应用于数组中的偶数索引(indices);2i
    angle_rads[:, 0::2] = np.sin(angle_rads[:, 0::2])

    # 将 cos 应用于数组中的奇数索引;2i+1
    angle_rads[:, 1::2] = np.cos(angle_rads[:, 1::2])

    pos_encoding = angle_rads[np.newaxis, ...]

    return tf.cast(pos_encoding, dtype=tf.float32)

3-1-4 为何采用这样的编码公式?

1 可以表示位置之间的线性关系
根据三角函数定理有如下公式
\begin{cases} sin(\alpha+\beta)=sin(\alpha)cos(\beta)+cos(\alpha)sin(\beta) \\ cos(\alpha+\beta)=cos(\alpha)cos(\beta)-sin(\alpha)sin(\beta) \end{cases}
 假设当前位置为pos, 以及其对应的位置编码,现在可以得到pos+k位置的编码为

由上面可以发现位置向量可以表示为相对位置的线性表示

2 位置编码采用正弦余弦波的形式进行编码,由于该三角函数为周期函数如何能正确的表示相对位置而不产生混淆

poscode=positional_encoding(100,512).numpy()
poscode=poscode[0]
x=np.arange(100)
plt.figure(figsize=(20,100))
j=1
for i in np.arange(0,500,50):
    plt.subplot(100,1,j)
    j+=1
    plt.plot(x,poscode[:,i])
plt.show()

编码波形如下

plt.figure(figsize=(20,100))
j=1
for i in np.arange(0,500,50):
    plt.subplot(100,1,j)
    j+=1
    plt.plot(x,poscode[:,i+1])
plt.show()

编码效果如下

3、从上面的图像中可以看出

  • 整体位置编码是正弦与余弦组合的形式出现,在i取值越高,整体图像趋近于线性变换
  • 由于正弦与余弦图像是完全互补的如果正弦图像的变化不明显时可以采用其对应的余弦图像进行补齐
  • 虽然正弦与余弦时周期性函数,在多种组合的情况下就会产生唯一性编码(很重要)

3-2 构建遮挡(mask)

3-2-1 构建填充遮挡(padding mask)

遮挡一批序列中所有的填充标记(pad tokens)。这确保了模型不会将填充作为输入。该 mask 表明填充值 0 出现的位置:在这些位置 mask 输出 1,否则输出 0。

def create_padding_mask(seq):
    seq = tf.cast(tf.math.equal(seq, 0), tf.float32)

    # 添加额外的维度来将填充加到
    # 注意力对数(logits)。
    return seq[:, tf.newaxis, tf.newaxis, :]  # (batch_size, 1, 1, seq_len)
x = tf.constant([[7, 6, 0, 0, 1], [1, 2, 3, 0, 0], [0, 0, 0, 4, 5]])
create_padding_mask(x)

代码输出如下

<tf.Tensor: shape=(3, 1, 1, 5), dtype=float32, numpy=
array([[[[0., 0., 1., 1., 0.]]],
       [[[0., 0., 0., 1., 1.]]],
       [[[1., 1., 1., 0., 0.]]]], dtype=float32)>

3-2-2 前瞻遮挡(look-ahead mask)

前瞻遮挡(look-ahead mask)用于遮挡一个序列中的后续标记(future tokens)。换句话说,该 mask 表明了不应该使用的条目。保证只能看到已经出现的单词

def create_look_ahead_mask(size):
    mask = 1 - tf.linalg.band_part(tf.ones((size, size)), -1, 0)
    return mask  # (seq_len, seq_len)
x = tf.random.uniform((1, 3))
temp = create_look_ahead_mask(x.shape[1])
temp

代码输出如下

<tf.Tensor: shape=(3, 3), dtype=float32, numpy=
array([[0., 1., 1.],
       [0., 0., 1.],
       [0., 0., 0.]], dtype=float32)>

3-3 self attention

3-3-1 缩放点积注意力

def scaled_dot_product_attention(q, k, v, mask):
    """计算注意力权重。
    q, k, v 必须具有匹配的前置维度。
    k, v 必须有匹配的倒数第二个维度,例如:seq_len_k = seq_len_v。
    虽然 mask 根据其类型(填充或前瞻)有不同的形状,
    但是 mask 必须能进行广播转换以便求和。

    参数:
    q: 请求的形状 == (..., seq_len_q, depth)
    k: 主键的形状 == (..., seq_len_k, depth)
    v: 数值的形状 == (..., seq_len_v, depth_v)
    mask: Float 张量,其形状能转换成
          (..., seq_len_q, seq_len_k)。默认为None。

    返回值:
    输出,注意力权重
    """
#     print('k shape',k.shape)
#     print('q shape',q.shape)
    matmul_qk = tf.matmul(q, k, transpose_b=True)  # (..., seq_len_q, seq_len_k)
#     print('qk shape',matmul_qk.shape)
    # 缩放 matmul_qk
    dk = tf.cast(tf.shape(k)[-1], tf.float32)
    scaled_attention_logits = matmul_qk / tf.math.sqrt(dk)
  
  • 4
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值