精华内容
下载资源
问答
  • 本文扩展了BERT模型,以在文本摘要上达到最新的分数。在此博客中,我将解释本文以及如何使用此模型进行工作。单文档文本摘要是自动生成文档的较短版本,同时保留其最重要信息的任务。该任务在自然语言处理社区中受到...

    简介

    BERT是经过预先训练的Transformer模型,已在多个NLP任务上取得了突破性的性能。最近,我遇到了BERTSUM,这是爱丁堡的Liu的论文。本文扩展了BERT模型,以在文本摘要上达到最新的分数。在此博客中,我将解释本文以及如何使用此模型进行工作。

    单文档文本摘要是自动生成文档的较短版本,同时保留其最重要信息的任务。该任务在自然语言处理社区中受到了很多关注。由于它对于各种信息访问应用程序具有巨大的潜力。示例包括提取文本内容(例如,新闻,社交媒体,评论),回答问题或提供建议的工具。汇总模型可以有两种类型:

    提取摘要-类似于使用荧光笔。我们从原始文本中选择文本的子段,以创建一个很好的摘要

    抽象性摘要-类似于用笔书写。创建摘要以提取要点,并且可以使用原始文本中未包含的词。这对于机器来说更难

    文本摘要系统的性能通过其ROUGE得分来衡量。 ROUGE得分用​​于衡量预测的摘要与基本事实摘要之间的重叠。

    BERT的主要技术创新是将流行的注意力模型Transformer的双向培训应用于语言建模。它的成功表明,经过双向训练的语言模型比单向语言模型可以更深刻地理解语言环境和流程。这是学习BERT的绝佳链接。

    BERT也可用于下一句预测。该模型接收成对的句子作为输入,并学习预测成对的第二句话是否是原始文档中的后续句子。在训练期间,输入的50%是一对,其中第二句话是原始文档中的后续句子。而在其他50%中,从语料库中随机选择一个句子作为第二个句子。

    使用BERT提取文本摘要— BERTSUM Model

    修改了BERT模型,以生成多个句子的句子嵌入。这是通过在每个句子的开头之前插入[CLS]令牌来完成的。然后,输出是每个句子的句子向量。然后,将句子向量传递到多层,从而轻松捕获文档级功能。将最终的汇总预测与基本事实进行比较,并将损失用于训练汇总层和BERT模型。

    Architecture of BERTSUM Model.png

    BERTSUM模型架构

    该模型在CNN /每日邮件和NYT注释的语料库上进行了训练。由于来自两个语料库的基本事实是抽象摘要,因此创建了新的基本事实。贪心算法用于为每个文档生成预言摘要。该算法贪婪地选择可以使ROUGE得分最大化的句子作为预言句。我们将标签1分配给oracle摘要中选择的句子,否则分配0。

    本文显示了文本摘要非常精确的结果,优于最新的抽象和提取摘要模型。见下表。这里的第一行是指针生成器模型,在我的博客中有更详细的解释。

    BERTSUM Result.png

    展开全文
  • 最近在学习seq2seq模型,以实现常见的问答、机器翻译、文本摘要功能。 所以采用三种方式 (1)利用keras库搭建seq2seq (2)利用keras_transformer库 (3)利用fastnlp框架 实现问答机器人、机器翻译、文本摘要...

    最近在学习seq2seq模型,以实现常见的问答、机器翻译、文本摘要功能。

    所以采用三种方式

    (1)利用keras库搭建seq2seq

    (2)利用keras_transformer库

    (3)利用fastnlp框架

    实现问答机器人、机器翻译、文本摘要等功能

    下面是github源码:

    https://github.com/yingdajun/seq2seqForExample

     

    展开全文
  • 【关于 Transformer 代码实战(文本摘要任务篇)】 那些你不知道的事 作者:杨夕 项目地址:https://github.com/km1994/nlp_paper_study 个人介绍:大佬们...目录【关于 Transformer 代码实战(文本摘要任务篇)】 ...

    e27049a08f4dcbcc50a30fd510ed54a0.png

    【关于 Transformer 代码实战(文本摘要任务篇)】 那些你不知道的事

    作者:杨夕
    项目地址:https://github.com/km1994/nlp_paper_study
    个人介绍:大佬们好,我叫杨夕,该项目主要是本人在研读顶会论文和复现经典论文过程中,所见、所思、所想、所闻,可能存在一些理解错误,希望大佬们多多指正。

    目录

    • 【关于 Transformer 代码实战(文本摘要任务篇)】 那些你不知道的事
    • 目录
    • 引言
    • 一、文本摘要数据集介绍
    • 二、数据集加载介绍
      • 2.1 数据加载
      • 2.2 数据字段抽取
    • 三、 数据预处理
      • 3.1 summary 数据 处理
      • 3.2 编码处理
      • 3.3 获取 encoder 词典 和 decoder 词典 长度
      • 3.4 确定 encoder 和 decoder 的 maxlen
      • 3.5 序列 填充/裁剪
    • 四、创建数据集 pipeline
    • 五、组件构建
      • 5.1 位置编码
      • 5.1.1 问题
      • 5.1.2 目的
      • 5.1.3 思路
      • 5.1.4 位置向量的作用
      • 5.1.5 步骤
      • 5.1.6 计算公式
      • 5.1.7 代码实现
      • 5.2 Masking 操作
      • 5.2.1 介绍
      • 5.2.3 类别:padding mask and sequence mask
        • padding mask
        • sequence mask
    • 六、模型构建
      • 6.1 self-attention
      • 6.1.1 动机
      • 6.1.2 传统 Attention
      • 6.1.3 核心思想
      • 6.1.4 目的
      • 6.1.5 公式
      • 6.1.6 步骤
      • 6.1.7 代码实现
      • 6.2 Multi-Headed Attention
        • 思路
        • 步骤
        • 代码实现
      • 6.3 前馈网络
        • 思路
        • 目的
        • 代码实现
      • 6.4 Transformer encoder 单元
        • 结构
        • 代码实现
      • 6.5 Transformer decoder 单元
        • 结构
        • 代码实现
    • 七、Encoder 和 Decoder 模块构建
      • 7.1 Encoder 模块构建
      • 7.2 Dncoder 模块构建
    • 八、Transformer 构建
    • 九、模型训练
      • 9.1 配置类
      • 9.2 优化函数定义
      • 9.3 Loss 损失函数 和 评测指标 定义
      • 9.3.1 Loss 损失函数 定义
      • 9.4 Transformer 实例化
      • 9.5 Mask 实现
      • 9.6 模型结果保存
      • 9.7 Training Steps
      • 9.8 训练

    引言

    之前给 小伙伴们 写过 一篇 【【关于Transformer】 那些的你不知道的事】后,有一些小伙伴联系我,并和我请教了蛮多细节性问题,针对该问题,小菜鸡的我 也 想和小伙伴 一起 学习,所以就 找到了 一篇【Transformer 在文本摘要任务 上的应用】作为本次学习的 Coding!

    一、文本摘要数据集介绍

    本任务采用的 文本摘要数据集 为 Kaggle 比赛 之 Inshorts Dataset,该数据集 包含以下字段:

    序号字段名字段介绍举例
    1Headline标题4 ex-bank officials booked for cheating bank of ₹209 crore
    2Short短文The CBI on Saturday booked four former officials of Syndicate Bank and six others for cheating, forgery, criminal conspiracy and causing ₹209 crore loss to the state-run bank. The accused had availed home loans and credit from Syndicate Bank on the basis of forged and fabricated documents. These funds were fraudulently transferred to the companies owned by the accused persons.
    3Source数据来源The New Indian Express
    4Time发表时间9:25:00
    5Publish Date发表日期2017/3/26
    注:这里我们只 用到 Headline[摘要] 和 Short[长文本] 作为 文本摘要任务 实验数据

    二、数据集加载介绍

    2.1 数据加载

    本文将数据集存储在 Excel 文件中,通过 pandas 的 read_excel() 方法 获取数据集,代码如下:

    news = pd.read_excel("data/news.xlsx")

    2.2 数据字段抽取

    在 一、文本摘要数据集介绍 中,我们说过,我们只用到 Headline[摘要] 和 Short[长文本] 作为 文本摘要任务 实验数据,所以我们需要 清除 其他字段。代码如下:

    news.drop(['Source ', 'Time ', 'Publish Date'], axis=1, inplace=True)

    可以采用以下命令,查看结果:

    news.head()
        news.shape   # (55104, 2)

    3b1e61f8c58d8b3082eab1cae7b13334.png

    方便后期操作,我们这里直接 从 DataFrame 中分别抽取 出 Headline[摘要] 和 Short[长文本] 数据:

    document = news['Short']
        summary = news['Headline']
        document[30], summary[30]
        >>>
        ('According to the Guinness World Records, the most generations alive in a single family have been seven.  The difference between the oldest and the youngest person in the family was about 109 years, when Augusta Bunge's great-great-great-great grandson was born on January 21, 1989. The family belonged to the United States of America.',
        'The most generations alive in a single family have been 7')

    三、 数据预处理

    3.1 summary 数据 处理

    summary 数据 作为 decoder 序列数据,我们需要做一些小处理【前后分别加一个标识符】,如下所示:

    # for decoder sequence
        summary = summary.apply(lambda x: '<go> ' + x + ' <stop>')
        summary[0]
        >>>
        '<go> 4 ex-bank officials booked for cheating bank of ₹209 crore <stop>'

    3.2 编码处理

    在 进行 文本摘要任务 之前,我们需要 将 文本进行编码:

    1. 变量定义
    # since < and > from default tokens cannot be removed
        filters = '!"#$%&()*+,-./:;=?@[]^_`{|}~tn'    # 文本中特殊符号清洗 
        oov_token = '<unk>'                               # 未登录词 表示
    1. 定义 文本预处理 tf.keras.preprocessing.text.Tokenizer() 编码类【用于后期 文本编码处理】
    document_tokenizer = tf.keras.preprocessing.text.Tokenizer(oov_token=oov_token) 
        summary_tokenizer = tf.keras.preprocessing.text.Tokenizer(filters=filters, oov_token=oov_token)
    Tokenizer : 一个将文本向量化,转换成序列的类。用来文本处理的分词、嵌入 。
    keras.preprocessing.text.Tokenizer(num_words=None, 
                                        filters='!"#$%&()*+,-./:;<=>?@[]^_`{|}~tn', 
                                        lower=True, 
                                        split=' ', 
                                        char_level=False, 
                                        oov_token=None, 
                                        document_count=0)
    • 参数说明:
      • num_words: 默认是None处理所有字词,但是如果设置成一个整数,那么最后返回的是最常见的、出现频率最高的num_words个字词。一共保留 num_words-1 个词。
      • filters: 过滤一些特殊字符,默认上文的写法就可以了。
      • lower: 是否全部转为小写。
      • split: 分词的分隔符字符串,默认为空格。因为英文分词分隔符就是空格。
      • char_level: 分字。
      • oov_token: if given, it will be added to word_index and used to replace out-of-vocabulary words during text_to_sequence calls 参考文档:Keras分词器 Tokenizer
    • 构建词典库
    # 构建词典库
        document_tokenizer.fit_on_texts(document)
        summary_tokenizer.fit_on_texts(summary)
    1. 文本列表 转 序列的列表 【列表中每个序列对应于一段输入文本】
    # 文本列表 转 序列的列表 【列表中每个序列对应于一段输入文本】
        inputs = document_tokenizer.texts_to_sequences(document)
        targets = summary_tokenizer.texts_to_sequences(summary)
        # 举例测试
        summary_tokenizer.texts_to_sequences(["This is a test"])         # [[184, 22, 12, 71]]
        summary_tokenizer.sequences_to_texts([[184, 22, 12, 71]])        # ['this is a test']

    3.3 获取 encoder 词典 和 decoder 词典 长度

    encoder_vocab_size = len(document_tokenizer.word_index) + 1
        decoder_vocab_size = len(summary_tokenizer.word_index) + 1
        # vocab_size
        encoder_vocab_size, decoder_vocab_size
        >>>
        (76362, 29661)

    3.4 确定 encoder 和 decoder 的 maxlen

    1. 分别进行 documents 和 summarys 中每个 序列长度
    document_lengths = pd.Series([len(x) for x in document])
        summary_lengths = pd.Series([len(x) for x in summary])
    1. 对 document_lengths 和 summary_lengths 进行 统计分析
    2. 对 document 进行 统计分析
    document_lengths.describe()
        >>>
        count    55104.000000
        mean       368.003049
        std         26.235510
        min        280.000000
        25%        350.000000
        50%        369.000000
        75%        387.000000
        max        469.000000
        dtype: float64
    • 对 summary 进行 统计分析
    summary_lengths.describe()
        >>>
        count    55104.000000
        mean        63.620282
        std          7.267463
        min         20.000000
        25%         59.000000
        50%         63.000000
        75%         69.000000
        max         96.000000
        dtype: float64
    1. 确定 encoder 和 decoder 的 maxlen
    # 取值>并同时四舍五入到第75个百分位数,而不会留下高方差
        encoder_maxlen = 400
        decoder_maxlen = 75

    3.5 序列 填充/裁剪

    #  对 序列 进行 填充/裁剪 ,是所有序列长度 都 等于 maxlen
        inputs = tf.keras.preprocessing.sequence.pad_sequences(inputs, maxlen=encoder_maxlen, padding='post', truncating='post')
        targets = tf.keras.preprocessing.sequence.pad_sequences(targets, maxlen=decoder_maxlen, padding='post', truncating='post')

    四、创建数据集 pipeline

    对数据集的顺序进行打乱,并 进行分 batch

    # 数据类型 转为 为 tf.int32
        inputs = tf.cast(inputs, dtype=tf.int32)
        targets = tf.cast(targets, dtype=tf.int32)
    
        BUFFER_SIZE = 20000
        BATCH_SIZE = 64
    
        dataset = tf.data.Dataset.from_tensor_slices((inputs, targets)).shuffle(BUFFER_SIZE).batch(BATCH_SIZE)

    五、组件构建

    5.1 位置编码

    5.1.1 问题

    • 介绍:缺乏 一种 表示 输入序列中 单词顺序 的方法
    • 说明:因为模型不包括Recurrence/Convolution,因此是无法捕捉到序列顺序信息的,例如将K、V按行进行打乱,那么Attention之后的结果是一样的。但是序列信息非常重要,代表着全局的结构,因此必须将序列的分词相对或者绝对position信息利用起来

    5.1.2 目的

    加入词序信息,使 Attention 能够分辨出不同位置的词

    5.1.3 思路

    在 encoder 层和 decoder 层的输入添加了一个额外的向量Positional Encoding,维度和embedding的维度一样,让模型学习到这个值

    5.1.4 位置向量的作用

    • 决定当前词的位置;
    • 计算在一个句子中不同的词之间的距离

    5.1.5 步骤

    • 将每个位置编号,
    • 然后每个编号对应一个向量,
    • 通过将位置向量和词向量相加,就给每个词都引入了一定的位置信息。

    5.1.6 计算公式

    ff4d6e01f43dd15c21216ef238b93931.png
    • 论文的位置编码是使用三角函数去计算的。好处:
    • 值域只有[-1,1]
    • 容易计算相对位置。

    ba5173181acd6cf288a1aa6a3a1459cf.png
    注:
    pos 表示当前词在句子中的位置
    i 表示向量中每个值 的 index
    在偶数位置:使用 正弦编码 sin();
    在奇数位置:使用 余弦编码 cos();

    5.1.7 代码实现

    # 位置编码 类
        class Positional_Encoding():
            def __init__(self):
                pass
            # 功能:计算角度 函数
            def get_angles(self, position, i, d_model):
                '''
                    功能:计算角度 函数
                    input:
                        position    单词在句子中的位置
                        i           维度 
                        d_model     向量维度
                '''
                angle_rates = 1 / np.power(10000, (2 * (i // 2)) / np.float32(d_model))
                return position * angle_rates
            # 功能:位置编码 函数
            def positional_encoding(self, position, d_model):
                '''
                    功能:位置编码 函数
                    input:
                        position    单词在句子中的位置
                        d_model     向量维度
                '''
                angle_rads = self.get_angles(
                    np.arange(position)[:, np.newaxis],
                    np.arange(d_model)[np.newaxis, :],
                    d_model
                )
    
                # apply sin to even indices in the array; 2i
                angle_rads[:, 0::2] = np.sin(angle_rads[:, 0::2])
    
                # apply cos to odd indices in the array; 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)

    5.2 Masking 操作

    5.2.1 介绍

    掩盖某些值的信息,让模型信息不到该信息;

    5.2.3 类别:padding mask and sequence mask

    padding mask

    • 作用域:每一个 scaled dot-product attention 中
    • 动机:输入句子的长度不一问题
    • 方法:
    • 短句子:后面 采用 0 填充
    • 长句子:只截取 左边 部分内容,其他的丢弃
    • 原因:对于 填充 的位置,其所包含的信息量 对于 模型学习 作用不大,所以 self-attention 应该 抛弃对这些位置 进行学习;
    • 做法:在这些位置上加上 一个 非常大 的负数(负无穷),使 该位置的值经过 Softmax 后,值近似 0,利用 padding mask 标记哪些值需要做处理;
    • 实现:
    # 功能: padding mask
        def create_padding_mask(seq):
            '''
                功能: padding mask
                input:
                    seq    序列
            '''
            seq = tf.cast(tf.math.equal(seq, 0), tf.float32)
            return seq[:, tf.newaxis, tf.newaxis, :]

    sequence mask

    • 作用域:只作用于 decoder 的 self-attention 中
    • 动机:不可预测性;
    • 目标:sequence mask 是为了使得 decoder 不能看见未来的信息。也就是对于一个序列,在 time_step 为 t 的时刻,我们的解码输出应该只能依赖于 t 时刻之前的输出,而不能依赖 t 之后的输出。因此我们需要想一个办法,把 t 之后的信息给隐藏起来。
    • 做法:产生一个上三角矩阵,上三角的值全为0。把这个矩阵作用在每一个序列上,就可以达到我们的目的
    • 实现:
    # 功能:sequence mask
        def create_look_ahead_mask(size):
            '''
                功能: sequence mask
                input:
                    seq    序列
            '''
            mask = 1 - tf.linalg.band_part(tf.ones((size, size)), -1, 0)
            return mask

    六、模型构建

    6.1 self-attention

    6.1.1 动机

    • CNN 所存在的长距离依赖问题;
    • RNN 所存在的无法并行化问题【虽然能够在一定长度上缓解 长距离依赖问题】;

    6.1.2 传统 Attention

    • 方法:基于源端和目标端的隐向量计算Attention,
    • 结果:源端每个词与目标端每个词间的依赖关系 【源端->目标端】
    • 问题:忽略了 远端或目标端 词与词间 的依赖关系

    6.1.3 核心思想

    • 介绍: self-attention的结构在计算每个token时,总是会考虑整个序列其他token的表达;
    • 举例:“我爱中国”这个序列,在计算"我"这个词的时候,不但会考虑词本身的embedding,也同时会考虑其他词对这个词的影响

    6.1.4 目的

    学习句子内部的词依赖关系,捕获句子的内部结构。

    6.1.5 公式

    c3c184a6049ef10c168fe8c9ae72e760.png

    6b5426e9549619bfe0e8ce71a89dd3ae.png

    6.1.6 步骤

    建议阅读 [Transformer#self-attention-长怎么样](https://github.com/km1994/nlp_paper_study/tree/master/transformer_study/Transformer#self-attention-长怎么样)

    6.1.7 代码实现

    def scaled_dot_product_attention(q, k, v, mask):
            # s1:权重 score 计算:查询向量 query 点乘 key
            matmul_qk = tf.matmul(q, k, transpose_b=True)
            # s2:scale 操作:除以 sqrt(dk),将 Softmax 函数推入梯度极小的区域
            dk = tf.cast(tf.shape(k)[-1], tf.float32)
            scaled_attention_logits = matmul_qk / tf.math.sqrt(dk)
            # s3:
            if mask is not None:
                scaled_attention_logits += (mask * -1e9)  
            # s4:Softmax 归一化
            attention_weights = tf.nn.softmax(scaled_attention_logits, axis=-1)
            # s5:加权求和
            output = tf.matmul(attention_weights, v)
            return output, attention_weights

    6.2 Multi-Headed Attention

    思路

    • 相当于 $h$ 个 不同的 self-attention 的集成
    • 就是把self-attention做 n 次,取决于 head 的个数;论文里面是做了8次。

    步骤

    • step 1 : 初始化 N 组 $Q,K,V$矩阵(论文为 8组);
    • step 2 : 每组 分别 进行 self-attention;
    • step 3:
    • 问题:多个 self-attention 会得到 多个 矩阵,但是前馈神经网络没法输入8个矩阵;
    • 目标:把8个矩阵降为1个
    • 步骤:
      • 每次self-attention都会得到一个 Z 矩阵,把每个 Z 矩阵拼接起来,
      • 再乘以一个Wo矩阵,
      • 得到一个最终的矩阵,即 multi-head Attention 的结果;

    f1faf2007027f047783a80b83f116056.png

    代码实现

    class MultiHeadAttention(tf.keras.layers.Layer):
            def __init__(self, d_model, num_heads):
                super(MultiHeadAttention, self).__init__()
                self.num_heads = num_heads
                self.d_model = d_model
    
                assert d_model % self.num_heads == 0
    
                self.depth = d_model // self.num_heads
                # 初始化 Q,K,V 矩阵
                self.wq = tf.keras.layers.Dense(d_model)
                self.wk = tf.keras.layers.Dense(d_model)
                self.wv = tf.keras.layers.Dense(d_model)
    
                self.dense = tf.keras.layers.Dense(d_model)
    
            def split_heads(self, x, batch_size):
                x = tf.reshape(x, (batch_size, -1, self.num_heads, self.depth))
                return tf.transpose(x, perm=[0, 2, 1, 3])
    
            def call(self, v, k, q, mask):
                batch_size = tf.shape(q)[0]
                # step 1:利用矩阵计算 q,k,v
                q = self.wq(q)
                k = self.wk(k)
                v = self.wv(v)
                # step 2:
                q = self.split_heads(q, batch_size)
                k = self.split_heads(k, batch_size)
                v = self.split_heads(v, batch_size)
                # step 3:每组 分别 进行 self-attention
                scaled_attention, attention_weights = scaled_dot_product_attention(
                    q, k, v, mask)
                # step 4:矩阵拼接
                scaled_attention = tf.transpose(scaled_attention, perm=[0, 2, 1, 3])
                concat_attention = tf.reshape(scaled_attention, (batch_size, -1, self.d_model))
                # step 5:全连接层
                output = self.dense(concat_attention)
                return output, attention_weights

    6.3 前馈网络

    思路

    经过一层前馈网络以及 Add&Normalize,(线性转换+relu+线性转换 如下式)

    7f61a3cfd27733d4bc3543b7c6e58f0a.png

    目的

    增加非线性的表达能力,毕竟之前的结构基本都是简单的矩阵乘法。若前馈网络的隐向量是512维,则结构最后输出100*512;

    代码实现

    def point_wise_feed_forward_network(d_model, dff):
            return tf.keras.Sequential([
                tf.keras.layers.Dense(dff, activation='relu'),
                tf.keras.layers.Dense(d_model)
            ])

    6.4 Transformer encoder 单元

    结构

    d69c0d9d6685fddb5acc97433daf731a.png

    代码实现

    class EncoderLayer(tf.keras.layers.Layer):
            def __init__(self, d_model, num_heads, dff, rate=0.1):
                super(EncoderLayer, self).__init__()
    
                self.mha = MultiHeadAttention(d_model, num_heads)
                self.ffn = point_wise_feed_forward_network(d_model, dff)
    
                self.layernorm1 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
                self.layernorm2 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
    
                self.dropout1 = tf.keras.layers.Dropout(rate)
                self.dropout2 = tf.keras.layers.Dropout(rate)
    
            def call(self, x, training, mask):
                # step 1:多头自注意力
                attn_output, _ = self.mha(x, x, x, mask)
                # step 2:前馈网络
                attn_output = self.dropout1(attn_output, training=training)
                # step 3:Layer Norml 
                out1 = self.layernorm1(x + attn_output)
                # step 4:前馈网络
                ffn_output = self.ffn(out1)
                ffn_output = self.dropout2(ffn_output, training=training)
                # step 5:Layer Norml
                out2 = self.layernorm2(out1 + ffn_output)
    
                return out2

    6.5 Transformer decoder 单元

    结构

    00e25dc42a41dfe56f19cd2dd5f9200c.png

    代码实现

    class DecoderLayer(tf.keras.layers.Layer):
            def __init__(self, d_model, num_heads, dff, rate=0.1):
                super(DecoderLayer, self).__init__()
    
                self.mha1 = MultiHeadAttention(d_model, num_heads)
                self.mha2 = MultiHeadAttention(d_model, num_heads)
    
                self.ffn = point_wise_feed_forward_network(d_model, dff)
    
                self.layernorm1 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
                self.layernorm2 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
                self.layernorm3 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
    
                self.dropout1 = tf.keras.layers.Dropout(rate)
                self.dropout2 = tf.keras.layers.Dropout(rate)
                self.dropout3 = tf.keras.layers.Dropout(rate)
    
    
            def call(self, x, enc_output, training, look_ahead_mask, padding_mask):
                # step 1:带 sequence mask 的 多头自注意力
                attn1, attn_weights_block1 = self.mha1(x, x, x, look_ahead_mask)
                attn1 = self.dropout1(attn1, training=training)
                # step 2:Layer Norm
                out1 = self.layernorm1(attn1 + x)
                # step 3:带 padding mask 的 多头自注意力
                attn2, attn_weights_block2 = self.mha2(enc_output, enc_output, out1, padding_mask)
                attn2 = self.dropout2(attn2, training=training)
                # step 4:Layer Norm
                out2 = self.layernorm2(attn2 + out1)
                # step 5:前馈网络
                ffn_output = self.ffn(out2)
                ffn_output = self.dropout3(ffn_output, training=training)
                # step 6:Layer Norm
                out3 = self.layernorm3(ffn_output + out2)
                return out3, attn_weights_block1, attn_weights_block2

    七、Encoder 和 Decoder 模块构建

    7.1 Encoder 模块构建

    class Encoder(tf.keras.layers.Layer):
            def __init__(self, num_layers, d_model, num_heads, dff, input_vocab_size, maximum_position_encoding, rate=0.1):
                super(Encoder, self).__init__()
    
                self.d_model = d_model
                self.num_layers = num_layers     # encoder 层数
                # 词嵌入
                self.embedding = tf.keras.layers.Embedding(input_vocab_size, d_model)
                # 位置编码
                self.positional_encoding_obj = Positional_Encoding()
                self.pos_encoding = self.positional_encoding_obj.positional_encoding(maximum_position_encoding, self.d_model)
                # Encoder 模块构建
                self.enc_layers = [EncoderLayer(d_model, num_heads, dff, rate) for _ in range(num_layers)]
    
                self.dropout = tf.keras.layers.Dropout(rate)
    
            def call(self, x, training, mask):
                seq_len = tf.shape(x)[1]
                # step 1:词嵌入
                x = self.embedding(x)
                x *= tf.math.sqrt(tf.cast(self.d_model, tf.float32))
                # step 2:位置编码
                x += self.pos_encoding[:, :seq_len, :]
    
                x = self.dropout(x, training=training)
                # step 3:Encoder 模块构建
                for i in range(self.num_layers):
                    x = self.enc_layers[i](x, training, mask)
    
                return x

    7.2 Dncoder 模块构建

    class Decoder(tf.keras.layers.Layer):
            def __init__(self, num_layers, d_model, num_heads, dff, target_vocab_size, maximum_position_encoding, rate=0.1):
                super(Decoder, self).__init__()
    
                self.d_model = d_model
                self.num_layers = num_layers            # encoder 层数
                # 词嵌入
                self.embedding = tf.keras.layers.Embedding(target_vocab_size, d_model)
                # 位置编码
                self.positional_encoding_obj = Positional_Encoding()
                self.pos_encoding = self.positional_encoding_obj.positional_encoding(maximum_position_encoding, d_model)
                # Dncoder 模块构建
                self.dec_layers = [DecoderLayer(d_model, num_heads, dff, rate) for _ in range(num_layers)]
                self.dropout = tf.keras.layers.Dropout(rate)
    
            def call(self, x, enc_output, training, look_ahead_mask, padding_mask):
                seq_len = tf.shape(x)[1]
                attention_weights = {}
                # step 1:词嵌入
                x = self.embedding(x)
                x *= tf.math.sqrt(tf.cast(self.d_model, tf.float32))
                # step 2:位置编码
                x += self.pos_encoding[:, :seq_len, :]
    
                x = self.dropout(x, training=training)
                # step 3:Dncoder 模块构建
                for i in range(self.num_layers):
                    x, block1, block2 = self.dec_layers[i](x, enc_output, training, look_ahead_mask, padding_mask)
    
                    attention_weights['decoder_layer{}_block1'.format(i+1)] = block1
                    attention_weights['decoder_layer{}_block2'.format(i+1)] = block2
    
                return x, attention_weights

    八、Transformer 构建

    class Transformer(tf.keras.Model):
            def __init__(self, num_layers, d_model, num_heads, dff, input_vocab_size, target_vocab_size, pe_input, pe_target, rate=0.1):
                super(Transformer, self).__init__()
                # Encoder 模块
                self.encoder = Encoder(num_layers, d_model, num_heads, dff, input_vocab_size, pe_input, rate)
                # Decoder 模块
                self.decoder = Decoder(num_layers, d_model, num_heads, dff, target_vocab_size, pe_target, rate)
                # 全连接层
                self.final_layer = tf.keras.layers.Dense(target_vocab_size)
    
            def call(self, inp, tar, training, enc_padding_mask, look_ahead_mask, dec_padding_mask):
                # step 1: encoder
                enc_output = self.encoder(inp, training, enc_padding_mask)
                # step 2:decoder
                dec_output, attention_weights = self.decoder(tar, enc_output, training, look_ahead_mask, dec_padding_mask)
                # step 3:全连接层
                final_output = self.final_layer(dec_output)
    
                return final_output, attention_weights

    九、模型训练

    9.1 配置类

    # hyper-params
        class Config():
            def __init__(self):
                self.num_layers = 4        # encoder 和 decoder 层数
                self.d_model = 128         # 向量维度
                self.dff = 512             # 序列维度
                self.num_heads = 8         # 多头自注意力 头数
                self.EPOCHS = 10           # 训练 次数
        config = Config()

    9.2 优化函数定义

    Adam optimizer with custom learning rate scheduling

    class CustomSchedule(tf.keras.optimizers.schedules.LearningRateSchedule):
            def __init__(self, d_model, warmup_steps=4000):
                super(CustomSchedule, self).__init__()
    
                self.d_model = d_model
                self.d_model = tf.cast(self.d_model, tf.float32)
    
                self.warmup_steps = warmup_steps
    
            def __call__(self, step):
                arg1 = tf.math.rsqrt(step)
                arg2 = step * (self.warmup_steps ** -1.5)
    
                return tf.math.rsqrt(self.d_model) * tf.math.minimum(arg1, arg2)
    
        learning_rate = CustomSchedule(config.d_model)
    
        optimizer = tf.keras.optimizers.Adam(learning_rate, beta_1=0.9, beta_2=0.98, epsilon=1e-9)

    9.3 Loss 损失函数 和 评测指标 定义

    9.3.1 Loss 损失函数 定义

    # 功能:损失函数 定义
        def loss_function(real, pred):
            mask = tf.math.logical_not(tf.math.equal(real, 0))
            # 稀疏分类交叉熵:将数字编码转化成one-hot编码格式,然后对one-hot编码格式的数据(真实标签值)与预测出的标签值使用交叉熵损失函数。
            loss_object = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True, reduction='none')
            loss_ = loss_object(real, pred)
    
            mask = tf.cast(mask, dtype=loss_.dtype)
            loss_ *= mask
    
            return tf.reduce_sum(loss_)/tf.reduce_sum(mask)
        # 实例化
        train_loss = tf.keras.metrics.Mean(name='train_loss')
    稀疏分类交叉熵与稀疏分类交叉熵的使用差异,sparsecategoricalcrossentropy,和,SparseCategoricalCrossentropy,用法,区别

    9.4 Transformer 实例化

    transformer = Transformer(
            config.num_layers, 
            config.d_model, 
            config.num_heads, 
            config.dff,
            encoder_vocab_size, 
            decoder_vocab_size, 
            pe_input=encoder_vocab_size, 
            pe_target=decoder_vocab_size,
        )

    9.5 Mask 实现

    def create_masks(inp, tar):
            enc_padding_mask = create_padding_mask(inp)
            dec_padding_mask = create_padding_mask(inp)
    
            look_ahead_mask = create_look_ahead_mask(tf.shape(tar)[1])
            dec_target_padding_mask = create_padding_mask(tar)
            combined_mask = tf.maximum(dec_target_padding_mask, look_ahead_mask)
    
            return enc_padding_mask, combined_mask, dec_padding_mask

    9.6 模型结果保存

    checkpoint_path = "checkpoints"
    
        ckpt = tf.train.Checkpoint(transformer=transformer, optimizer=optimizer)
    
        ckpt_manager = tf.train.CheckpointManager(ckpt, checkpoint_path, max_to_keep=5)
    
        if ckpt_manager.latest_checkpoint:
            ckpt.restore(ckpt_manager.latest_checkpoint)
            print ('Latest checkpoint restored!!')

    9.7 Training Steps

    @tf.function
        def train_step(inp, tar):
            tar_inp = tar[:, :-1]
            tar_real = tar[:, 1:]
    
            enc_padding_mask, combined_mask, dec_padding_mask = create_masks(inp, tar_inp)
    
            with tf.GradientTape() as tape:
                predictions, _ = transformer(
                    inp, tar_inp, 
                    True, 
                    enc_padding_mask, 
                    combined_mask, 
                    dec_padding_mask
                )
                loss = loss_function(tar_real, predictions)
    
            gradients = tape.gradient(loss, transformer.trainable_variables)    
            optimizer.apply_gradients(zip(gradients, transformer.trainable_variables))
    
            train_loss(loss)

    9.8 训练

    for epoch in range(config.EPOCHS):
            start = time.time()
    
            train_loss.reset_states()
    
            for (batch, (inp, tar)) in enumerate(dataset):
                train_step(inp, tar)
    
                # 55k samples
                # we display 3 batch results -- 0th, middle and last one (approx)
                # 55k / 64 ~ 858; 858 / 2 = 429
                if batch % 429 == 0:
                    print (f'Epoch {epoch + 1} Batch {batch} Loss {train_loss.result()}')
    
            if (epoch + 1) % 5 == 0:
                ckpt_save_path = ckpt_manager.save()
                print ('Saving checkpoint for epoch {} at {}'.format(epoch+1, ckpt_save_path))
    
            print ('Epoch {} Loss {:.4f}'.format(epoch + 1, train_loss.result()))
    
            print ('Time taken for 1 epoch: {} secsn'.format(time.time() - start))
            >>>
            Epoch 1 Batch 0 Loss 2.4681
            Epoch 1 Batch 429 Loss 2.4650
            Epoch 1 Batch 858 Loss 2.5071
            Epoch 1 Loss 2.5077
            Time taken for 1 epoch: 308.9519073963165 secs
    
            Epoch 2 Batch 0 Loss 2.3482
            Epoch 2 Batch 429 Loss 2.4071
            Epoch 2 Batch 858 Loss 2.4461
            Epoch 2 Loss 2.4464
            Time taken for 1 epoch: 299.0744743347168 secs
            ...
    展开全文
  • 上一篇我们介绍了 Transformer 的原理,今天我们来根据论文动手编写一个 Transformer,并将其应用在我们前面讲过的机器翻译的例子上,大家感兴趣也可以进行对比学习。 这里的数据部分和前面机器翻译那篇是一样的,...

    上一篇我们介绍了 Transformer 的原理,今天我们来根据论文动手编写一个 Transformer,并将其应用在我们前面讲过的机器翻译的例子上,大家感兴趣也可以进行对比学习。

    这里的数据部分和前面机器翻译那篇是一样的,使用 ManyThings.org 的英语—法语的数据集,同样为了简化只选取 20 句。因为例子比较小,我们也会对论文中设置的一些参数进行简化。

    准备数据

    import tensorflow as tf
    import numpy as np
    import unicodedata
    import re
    import time
    import matplotlib.pyplot as plt
    
    raw_data = (
        ('What a ridiculous concept!', 'Quel concept ridicule !'),
        ('Your idea is not entirely crazy.', "Votre idée n'est pas complètement folle."),
        ("A man's worth lies in what he is.", "La valeur d'un homme réside dans ce qu'il est."),
        ('What he did is very wrong.', "Ce qu'il a fait est très mal."),
        ("All three of you need to do that.", "Vous avez besoin de faire cela, tous les trois."),
        ("Are you giving me another chance?", "Me donnez-vous une autre chance ?"),
        ("Both Tom and Mary work as models.", "Tom et Mary travaillent tous les deux comme mannequins."),
        ("Can I have a few minutes, please?", "Puis-je avoir quelques minutes, je vous prie ?"),
        ("Could you close the door, please?", "Pourriez-vous fermer la porte, s'il vous plaît ?"),
        ("Did you plant pumpkins this year?", "Cette année, avez-vous planté des citrouilles ?"),
        ("Do you ever study in the library?", "Est-ce que vous étudiez à la bibliothèque des fois ?"),
        ("Don't be deceived by appearances.", "Ne vous laissez pas abuser par les apparences."),
        ("Excuse me. Can you speak English?", "Je vous prie de m'excuser ! Savez-vous parler anglais ?"),
        ("Few people know the true meaning.", "Peu de gens savent ce que cela veut réellement dire."),
        ("Germany produced many scientists.", "L'Allemagne a produit beaucoup de scientifiques."),
        ("Guess whose birthday it is today.", "Devine de qui c'est l'anniversaire, aujourd'hui !"),
        ("He acted like he owned the place.", "Il s'est comporté comme s'il possédait l'endroit."),
        ("Honesty will pay in the long run.", "L'honnêteté paye à la longue."),
        ("How do we know this isn't a trap?", "Comment savez-vous qu'il ne s'agit pas d'un piège ?"),
        ("I can't believe you're giving up.", "Je n'arrive pas à croire que vous abandonniez."),
    )
    

    数据预处理:

    • 将 Unicode 转换为 ASCII
    • 将字符串规范化,除了 (a-z, A-Z, ".", "?", "!", ",") 之外,将不需要的标记替换成空格,
    • 在紧跟单词后面的标点前面加上空格,例如:"he is a boy." => "he is a boy ."
    def unicode_to_ascii(s):
        return ''.join(
            c for c in unicodedata.normalize('NFD', s)
            if unicodedata.category(c) != 'Mn')
    
    def normalize_string(s):
        s = unicode_to_ascii(s)
        s = re.sub(r'([!.?])', r' \1', s)
        s = re.sub(r'[^a-zA-Z.!?]+', r' ', s)    # 除了 (a-z, A-Z, ".", "?", "!", ",") 之外,将不需要的标记替换成空格
        s = re.sub(r'\s+', r' ', s)                # 在紧跟单词后面的标点前面加上空格,eg: "he is a boy." => "he is a boy ."
        return s
    

    给翻译句的前后加上这两个标记:

    • 将数据中的英语和法语句子分别拿出来,形成原句和翻译句两个部分,
    • 并在翻译句的开始和结尾标记 <start><end>
    raw_data_en, raw_data_fr = list(zip(*raw_data))
    raw_data_en, raw_data_fr = list(raw_data_en), list(raw_data_fr)
    
    raw_data_en = [normalize_string(data) for data in raw_data_en]
    raw_data_fr_in = ['<start> ' + normalize_string(data) for data in raw_data_fr]
    raw_data_fr_out = [normalize_string(data) + ' <end>' for data in raw_data_fr]
    

    接下里我们需要用 Tokenizer 将字符串转换为整数序列:

    • Tokenizer 里面的 filters 指的是要去掉所有标点符号,因为前面已经进行了预处理,这里就设置为空。
    • fit_on_texts:是根据传入文本中每个单词出现的频数,给出对应单词的索引号。

    例如 "The cat sat on the mat.",the 出现了两次排在第一位,所以 word_index["the"] = 1,索引越小的说明这个单词出现的频数越大。

    en_tokenizer = tf.keras.preprocessing.text.Tokenizer(filters='')
    en_tokenizer.fit_on_texts(raw_data_en)
    
    print(en_tokenizer.word_index)
    
    {
     '.': 1, 'you': 2, '?': 3, 'the': 4, 'a': 5, 'is': 6, 'he': 7,
     'what': 8, 'in': 9, 'do': 10, 'can': 11, 't': 12, 'did': 13, 'giving': 14
     ...
    }
    
    • 再用 texts_to_sequences 将文本数据转化为整数序列数据,也就是将单词替换成 word_index 中相应的索引号。
    • 此外还需要将句子长度补齐,使它们具有相同的长度,在后面创建 tf.data.Dataset 时候会用到,因为到时需要对输入序列使用词嵌入,对输出序列进行热编码等操作。
    data_en = en_tokenizer.texts_to_sequences(raw_data_en)
    data_en = tf.keras.preprocessing.sequence.pad_sequences(data_en, padding='post')
    
    print(data_en[:3])
    
    [[ 8  5 21 22 23  0  0  0  0  0]
     [24 25  6 26 27 28  1  0  0  0]
     [ 5 29 30 31 32  9  8  7  6  1]]
    

    我们再用同样的预处理步骤将目标语言的句子进行处理:

    fr_tokenizer = tf.keras.preprocessing.text.Tokenizer(filters='')
    
    fr_tokenizer.fit_on_texts(raw_data_fr_in)
    fr_tokenizer.fit_on_texts(raw_data_fr_out)
    
    data_fr_in = fr_tokenizer.texts_to_sequences(raw_data_fr_in)
    data_fr_in = tf.keras.preprocessing.sequence.pad_sequences(data_fr_in,
                                                               padding='post')
    
    data_fr_out = fr_tokenizer.texts_to_sequences(raw_data_fr_out)
    data_fr_out = tf.keras.preprocessing.sequence.pad_sequences(data_fr_out,
                                                               padding='post')
    

    建立数据集 Dataset:

    用 from_tensor_slices 可以保持三个集合的独立性,shuffle 随机打乱数据集中的数据,Batch 将数据集划分成几份。

    ```
    dataset = tf.data.Dataset.fromtensorslices( (dataen, datafrin, datafr_out)) dataset = dataset.shuffle(20).batch(5)

    接下来我们就按照 Transformer 的结构图一块一块构建模型,首先建立基础组件 Positional Encoding 和 Multi-Head Attention。
    
    ### Positional Encoding
    
    我们知道在单词进入模型之前要先做 word embedding,会将单词表示成一个长度为 MODEL\_SIZE 的向量,不过这个只是词语含义的特征,并没有位置的特征,于是需要做 positional\_embedding,它是对单词的位置信息进行编码,会将单词的位置这个特征也编码成一个长度为 MODEL_SIZE 的向量,计算公式如下所示:
    
    `$$ PE(pos, 2i) = sin( \frac{pos}{ 10000^{2i/d_{model}} } ) $$`
    
    `$$ PE(pos, 2i + 1) = cos( \frac{pos}{ 10000^{2i/d_{model}} } ) $$`
    
    下面的 positional\_embedding 函数会接收两个参数:pos 代表句子中每个单词的序号,本例中句子的最大长度 = 14,所以序号从 0 到 13,以及编码向量的长度 `MODEL_SIZE = 128`。
    
    这个函数要做的事情是给定单词的位置 pos 后,编码成 model\_size 维的向量,它会在 128 个位置中的偶数位置上使用正弦编码,在奇数位置上使用余弦编码,这样向量中的每一个维度对应着正弦曲线,波长构成了从 `$2\pi$ 到 $10000*2\pi$` 的等比序列。
    
    因为本例中最长的句子长度是 14,所以会建立 14 个位置的编码向量,最后生成的 pes 的维度是 14*128,之后对于每个句子在相应的位置上,会在词向量的基础上加上该单词所在的位置的编码向量。
    

    def positionalembedding(pos, modelsize): PE = np.zeros((1, model_size))

    for i in range(model_size):
        if i % 2 == 0:
            PE[:, i] = np.sin(pos / 10000 ** (i / model_size))
        else:
            PE[:, i] = np.cos(pos / 10000 ** ((i - 1) / model_size))
    
    return PE
    

    maxlength = max(len(dataen[0]), len(datafrin[0])) MODEL_SIZE = 128

    pes = [] for i in range(maxlength): pes.append(positionalembedding(i, MODEL_SIZE))

    pes = np.concatenate(pes, axis=0) pes = tf.constant(pes, dtype=tf.float32)

    我们可以将生成的 14 个向量可视化看一下效果:
    

    plt.pcolormesh(pes, cmap='RdBu') plt.xlabel('Depth') plt.xlim((0, 128)) plt.ylabel('Position') plt.colorbar() plt.show()

    ![](https://images.gitbook.cn/f80e57a0-8ed1-11ea-86f7-9b5ee2f8f4da)
    
    
    ### Multi-Head Attention
    
    下面建立 Transformer 的核心部分为 Multi-Head Attention,它的作用时可以注意到来自多个角度不同表示空间的信息。
    
    在这里我们要建立如下图的模型结构:
    
    ![](https://images.gitbook.cn/229296d0-8ed2-11ea-8e9a-9bc3d95576e7)
    
    从图中可以看到这个结构大体有下面几个步骤。
    
    首先 Q、K、V 的初始大小是 model_size,我们需要将它们分成 h 份。
    
    然后对其中每一份进行 Scaled Dot-Product Attention 机制,具体结构为:
    
    ![](https://images.gitbook.cn/4e336990-8ed2-11ea-9144-a708da03c9c4)
    
    用公式表示为:
    
    `$$ Attention(Q, K, V) = softmax( \frac{ QK^T }{ \sqrt{d_k} } ) V $$`
    
    如果不考虑多个 head 的话,Scaled Dot-Product Attention 可以用下面的代码实现,它其实和 Luong Attention 基本差不多:
    
    def call(self, query, value):
    
        score = tf.matmul(query, value, transpose_b=True) / tf.math.sqrt(tf.dtypes.cast(self.key_size, tf.float32))
    
        alignment = tf.nn.softmax(score, axis=2)
    
        context = tf.matmul(alignment, value)
    
        return context
    
    如果考虑多个 head,用一个 for 循环来解决。
    
    把 h 个注意力结果合并起来,这里 Multi-Head Attention 的公式表示为:
    
    `$$ MultiHead(Q, K, V) = Concat( head_1, ... , head_h) W^o $$`
    
    其中,
    
    `$$ head_i = Attention( QW_i^Q, KW_i^K, VW_i^V) $$`
    
    最后经过线性映射得到最终的输出。
    
    论文里面 `d_{model}=512,h=8`,作为简化我们这里 `d_{model}=128,h=2`,下面按照公式构建模型:
    

    class MultiHeadAttention(tf.keras.Model): def init(self, modelsize, h): super(MultiHeadAttention, self).init() self.querysize = modelsize // h self.keysize = modelsize // h self.valuesize = model_size // h self.h = h

        self.wq = [tf.keras.layers.Dense(self.query_size) for _ in range(h)]
        self.wk = [tf.keras.layers.Dense(self.key_size) for _ in range(h)]
        self.wv = [tf.keras.layers.Dense(self.value_size) for _ in range(h)]
        self.wo = tf.keras.layers.Dense(model_size)
    
    def call(self, query, value):
        # query 的维度为 (batch, query_len, model_size)
        # value 的维度为 (batch, value_len, model_size)
        heads = []
    
        # 一共 h 份,对每一份实现 scaled dot-product attention 机制的公式
        for i in range(self.h):
            score = tf.matmul(self.wq[i](query), self.wk[i](value), transpose_b=True)
    
            # score 的维度为 (batch, query_len, value_len)
            score /= tf.math.sqrt(tf.dtypes.cast(self.key_size, tf.float32))
    
            # alignment 的维度为 (batch, query_len, value_len)
            alignment = tf.nn.softmax(score, axis=2)           
    
            # head 的维度为 (batch, decoder_len, value_size)
            head = tf.matmul(alignment, self.wv[i](value))
    
            heads.append(head)
    
        # 将所有的 head 连接起来
        heads = tf.concat(heads, axis=2)
        heads = self.wo(heads)
        # heads 的维度为 (batch, query_len, model_size)
    
        return heads
    
    ### Encoder
    
    完成了 Positional Encoding 和 Multi-head Attention 的搭建,接下来要构建 Encoder 和 Decoder 了。
    
    在这里我们要建立如下图所示的模型结构:
    
    ![](https://images.gitbook.cn/bf335470-8ed2-11ea-861a-9398d62a6944)
    
    可以看到 Encoder 的结构包括下面几个部分:
    
    - 一个 Embedding 层和 Positional Encoding 层
    - 一个或者多个 Encoder 组件,论文中是 6 个,我们这里用 2 个
    -     一个 Multi-Head Attention 层
    -     一个 Normalization 层
    -     一个 Feed-Forward Network
    -     一个 Normalization 层
    
    在前向计算 call 中:
    
    1. 首先将 Positional Encoding 加到 word embedding 上,拼接到一起得到 sub\_in,维度为 `(batch_size, length, model_size)`;
    2. 然后进入 `num_layers = 2` 个 (Attention + FFN) 中:
    3. 先做 Multi-Head Attention,得到结果 sub\_out;
    4. 然后是 Residual 连接,就是将 sub\_out 与 sub\_in 相加得到新的 sub\_out;紧接着给 sub\_out 做 Normalization;
    5. 然后进入 FFN 层,它的输入 ffn\_in 就是前面的 sub\_out;
    6. 用同样的方式对 FFN 做 Residual 连接和 Normalization;
    7. 最后当前 Encoder 组块的 FFN 的输出将作为下一个 (Attention + FFN) 的输入:`sub_in = ffn_out`。
    

    class Encoder(tf.keras.Model):

    def __init__(self, vocab_size, model_size, num_layers, h):
        super(Encoder, self).__init__()
        self.model_size = model_size
        self.num_layers = num_layers
        self.h = h
    
        # Embedding 层
        self.embedding = tf.keras.layers.Embedding(vocab_size, model_size)
    
        # num_layers 个 Multi-Head Attention 层和 Normalization 层
        self.attention = [MultiHeadAttention(model_size, h) for _ in range(num_layers)]
        self.attention_norm = [tf.keras.layers.BatchNormalization() for _ in range(num_layers)]
    
        # num_layers 个 FFN 和 Normalization 层
        self.dense_1 = [tf.keras.layers.Dense(model_size * 4, activation='relu') for _ in range(num_layers)]
        self.dense_2 = [tf.keras.layers.Dense(model_size) for _ in range(num_layers)]
        self.ffn_norm = [tf.keras.layers.BatchNormalization() for _ in range(num_layers)]
    
    def call(self, sequence):
    
        sub_in = []
        for i in range(sequence.shape[1]):
            # 计算嵌入向量
            embed = self.embedding(tf.expand_dims(sequence[:, i], axis=1))
    
            # 将 Positional Encoding 加到 embedded 向量上
            sub_in.append(embed + pes[i, :])
    
        # 拼接结果,维度为 (batch_size, length, model_size)
        sub_in = tf.concat(sub_in, axis=1)
    
        # 一共有 num_layers 个 (Attention + FFN)
        for i in range(self.num_layers):
    
            sub_out = []
    
            # 遍历序列长度
            for j in range(sub_in.shape[1]):
                # 计算上下文向量
                attention = self.attention[i](
                    tf.expand_dims(sub_in[:, j, :], axis=1), sub_in)
    
                sub_out.append(attention)
    
            # 合并结果维度为 (batch_size, length, model_size)
            sub_out = tf.concat(sub_out, axis=1)
    
            # Residual 连接
            sub_out = sub_in + sub_out
            # Normalization 层
            sub_out = self.attention_norm[i](sub_out)
    
            # FFN 层
            ffn_in = sub_out
            ffn_out = self.dense_2[i](self.dense_1[i](ffn_in))
    
            # 加上 residual 连接
            ffn_out = ffn_in + ffn_out
            # Normalization 层
            ffn_out = self.ffn_norm[i](ffn_out)
    
            # FFN 的输出作为下一个 Multi-Head Attention 的输入
            sub_in = ffn_out
    
        return ffn_out
    
    ### Decoder
    
    我们要构建的 Decoder 模型结构为:
    
    ![](https://images.gitbook.cn/331ec680-8ed3-11ea-a141-a13a4f8833dd)
    
    在每个 Decoder 组块中有两个注意力层,一个是位于底部的 Multi-Head Attention 记为 attention\_bot,一个是位于中间的记为 attention_mid。
    
    1. 第一步和 Encoder 中一样的,做完 word embedding 后加上 Positional Encoding;
    2. 这个结果将作为第一个 Multi-Head Attention 的输入:`bot_sub_in = embed_out`;论文中指出这个注意力层需要做 mask,我们用 `values = bot_sub_in[:, :j, :]` 来实现,在当前指标 j 的右侧的数据就不要了;
    3. 然后进入第二个 Multi-Head Attention 层,它的输入有来自上面 Multi-Head Attention 层的输出,还有 Encoder 的输出:encoder_output;
    4. 接着进入 FFN 层,这里和 Encoder 中是一样的;
    5. 另外 Decoder 比 Encoder 多了 `logits = self.dense(ffn_out)`,用来计算最后的输出结果。
    

    class Decoder(tf.keras.Model): def init(self, vocabsize, modelsize, numlayers, h): super(Decoder, self).init() self.modelsize = modelsize self.numlayers = num_layers self.h = h

        self.embedding = tf.keras.layers.Embedding(vocab_size, model_size)
    
        self.attention_bot = [MultiHeadAttention(model_size, h) for _ in range(num_layers)]
        self.attention_bot_norm = [tf.keras.layers.BatchNormalization() for _ in range(num_layers)]
    
        self.attention_mid = [MultiHeadAttention(model_size, h) for _ in range(num_layers)]
        self.attention_mid_norm = [tf.keras.layers.BatchNormalization() for _ in range(num_layers)]
    
        self.dense_1 = [tf.keras.layers.Dense(model_size * 4, activation='relu') for _ in range(num_layers)]
        self.dense_2 = [tf.keras.layers.Dense(model_size) for _ in range(num_layers)]
        self.ffn_norm = [tf.keras.layers.BatchNormalization() for _ in range(num_layers)]
    
        self.dense = tf.keras.layers.Dense(vocab_size)
    
    
    def call(self, sequence, encoder_output):
        # Embedding 和 Positional Embedding,和 Encoder 中一样的
        embed_out = []
        for i in range(sequence.shape[1]):
            embed = self.embedding(tf.expand_dims(sequence[:, i], axis=1))
            embed_out.append(embed + pes[i, :])
    
        embed_out = tf.concat(embed_out, axis=1)
    
    
        bot_sub_in = embed_out
    
        for i in range(self.num_layers):
            # 模型中最底部的 Multi-Head Attention 层,即 target 序列的 self-attention,这一层需要做 mask
            bot_sub_out = []
    
            for j in range(bot_sub_in.shape[1]):
                # 这里做了 mask,在当前 j 的右侧的数据就不要了
                values = bot_sub_in[:, :j, :]
                attention = self.attention_bot[i](
                    tf.expand_dims(bot_sub_in[:, j, :], axis=1), values)
    
                bot_sub_out.append(attention)
    
            bot_sub_out = tf.concat(bot_sub_out, axis=1)
            bot_sub_out = bot_sub_in + bot_sub_out
            bot_sub_out = self.attention_bot_norm[i](bot_sub_out)
    
            # 中间的 Multi-Head Attention 层,它的输入来自上面 Multi-Head Attention 层的输出和 Encoder 的输出 encoder_output
            mid_sub_in = bot_sub_out
    
            mid_sub_out = []
            for j in range(mid_sub_in.shape[1]):
                attention = self.attention_mid[i](
                    tf.expand_dims(mid_sub_in[:, j, :], axis=1), encoder_output)
    
                mid_sub_out.append(attention)
    
            mid_sub_out = tf.concat(mid_sub_out, axis=1)
            mid_sub_out = mid_sub_out + mid_sub_in
            mid_sub_out = self.attention_mid_norm[i](mid_sub_out)
    
            # FFN
            ffn_in = mid_sub_out
    
            ffn_out = self.dense_2[i](self.dense_1[i](ffn_in))
            ffn_out = ffn_out + ffn_in
            ffn_out = self.ffn_norm[i](ffn_out)
    
            bot_sub_in = ffn_out
    
        logits = self.dense(ffn_out)
    
        return logits
    
    ### Loss Function 和 Optimizer
    
    损失函数用 SparseCategoryCrossentropy,并带有 mask 用来过滤掉之前数据预处理时填充 0 的地方。
    

    crossentropy = tf.keras.losses.SparseCategoricalCrossentropy( from_logits=True)

    def lossfunc(targets, logits): mask = tf.math.logicalnot(tf.math.equal(targets, 0)) mask = tf.cast(mask, dtype=tf.int64) loss = crossentropy(targets, logits, sample_weight=mask)

    return loss
    

    optimizer = tf.keras.optimizers.Adam()

    ### 训练函数
    
    前面的组件都搭建完毕,训练过程也很直观明了了,从 Encoder 那里得到输出结果,传递给 Decoder,最后计算预测结果和实际结果之间的损失,用优化算法对模型的参数进行学习更新。
    

    @tf.function def trainstep(sourceseq, targetseqin, targetseqout): with tf.GradientTape() as tape: encoderoutput = encoder(sourceseq)

        decoder_output = decoder(target_seq_in, encoder_output)
    
        loss = loss_func(target_seq_out, decoder_output)
    
    variables = encoder.trainable_variables + decoder.trainable_variables
    gradients = tape.gradient(loss, variables)
    optimizer.apply_gradients(zip(gradients, variables))
    
    return loss
    

    ```

    预测函数

    1. 当不指定要预测的数据时,就从训练集中随机选择一个句子作为输入;
    2. 然后对这个句子要做同样的预处理过程;
    3. 将处理好的数据输入给 Encoder 后得到一个输出 en_output;
    4. 第一个 Decoder 的输入包括 <start> 所对应的标记,以及 en_output。然后根据 Decoder 的输出 de_output 可以得到一个预测结果 new_word;
    5. 接下来就和以往的 seq2seq 略有不同。

    之前的模型预测的一般过程是:

    先将 <start> 输入给模型,最后一个 output 是预测词,将预测词加入到结果中,将预测词和它相关联的状态合在一起给模型并重复步骤 2。

    在 Transformer 中,因为缺少序列机制,步骤变为:

    先将 <start> 输入给模型,最后一个 output 是预测词,将预测词加入到结果中,将整个结果给模型并重复步骤 2。

    所以这时要将 new_word 直接添加到前面所有的 de_input 上作为新的 de_input,在进入下一个 Decoder 时,要将整个 de_input 以及 en_output 都加入到模型中。

    ``` def predict(testsourcetext=None): # 没有测试序列时,从训练数据中随机选择一个 if testsourcetext is None: testsourcetext = rawdataen[np.random.choice(len(rawdataen))]

    print(test_source_text)
    
    # 预处理
    test_source_seq = en_tokenizer.texts_to_sequences([test_source_text])
    print(test_source_seq)
    
    en_output = encoder(tf.constant(test_source_seq))
    
    de_input = tf.constant([[fr_tokenizer.word_index['<start>']]], dtype=tf.int64)
    
    out_words = []
    
    while True:
        de_output = decoder(de_input, en_output)
    
        # 最后的 token 作为预测值
        new_word = tf.expand_dims(tf.argmax(de_output, -1)[:, -1], axis=1)
        out_words.append(fr_tokenizer.index_word[new_word.numpy()[0][0]])
    
        # 下一个输入是一个新的序列,包括输入序列和预测值
        de_input = tf.concat((de_input, new_word), axis=-1)
    
        # 当遇到 <end> 或者长度超过 14
        if out_words[-1] == '<end>' or len(out_words) >= 14:
            break
    
    print(' '.join(out_words))
    

    ```

    训练模型

    我们将论文中模型进行了简化,只采用 H = 2 两个多头注意力,以及在每个 Encoder 和 Decoder 中都采用 NUM_LAYERS = 2 个组块。

    ``` H = 2 NUM_LAYERS = 2

    envocabsize = len(entokenizer.wordindex) + 1 encoder = Encoder(envocabsize, MODELSIZE, NUMLAYERS, H)

    frvocabsize = len(frtokenizer.wordindex) + 1 maxlenfr = datafrin.shape[1] decoder = Decoder(frvocabsize, MODELSIZE, NUMLAYERS, H)

    NUM_EPOCHS = 100

    starttime = time.time() for e in range(NUMEPOCHS): for batch, (sourceseq, targetseqin, targetseqout) in enumerate(dataset.take(-1)): loss = trainstep(sourceseq, targetseqin, targetseq_out)

    print('Epoch {} Loss {:.4f}'.format(
          e + 1, loss.numpy()))
    
    try:
      predict()
    except Exception as e:
      print(e)
      continue
    

    ```

    训练 100 次后,观察结果可以发现:

    Epoch 1 Loss 1.1374
    Your idea is not entirely crazy .
    [[24, 25, 6, 26, 27, 28, 1]]
    tom idee idee travaillent completement completement scientifiques . <end>
    

    Epoch = 1 时,得到的翻译结果为:

    tom idee idee travaillent completement completement scientifiques .
    

    这句法语如果用谷歌翻译成英语的结果是:

    tom idee idee work completely completely scientific. 
    

    除了 completely 和 entirely 意思相近外,整个句子和原句很难看出直接的关系。

    Epoch 100 Loss 0.0002
    Excuse me . Can you speak English ?
    [[67, 15, 1, 11, 2, 68, 69, 3]]
    je vous prie de m excuser ! savez vous parler anglais ? <end>
    

    而在 Epoch = 100 时,模型预测的结果为:

    je vous prie de m excuser ! savez vous parler anglais ?
    

    这句法语用谷歌翻译成英语的结果是:

    please excuse me ! can you speak english?
    

    可以看到和原句基本一致了。


    参考文献:

    展开全文
  • 文本摘要是自然语言处理(NLP)的一项任务,其目的是生成源文本的简明摘要。不像摘录摘要,摘要不仅仅简单地从源文本复制重要的短语,还要提出新的相关短语,这可以被视为释义。摘要在不同的领域产生了大量的应用,从...
  • 创新实训(12)-生成式文本摘要之T5 1.简介 T5:Text-To-Text-Transfer-Transformer的简称,是Google在2019年提出的一个新的NLP模型。它的基本思想就是Text-to-Text,即NLP的任务基本上都可以归为从文本到文本的处理...
  • 摘要:搭建Transformer模型解决文本分类问题
  • 摘要 使用Transformer 代替RNN进行sentence-level context的encode Requirements Pytorch &amp;gt;= 0.4.1 Python &amp;gt;= 3.5 General Usage Transformer 用于进行encode的transformer我...
  • Transformer模型详解

    万次阅读 多人点赞 2019-01-11 12:32:01
    这篇论文中提出一个全新的模型,叫 Transformer,抛弃了以往深度学习任务里面使用到的 CNN 和 RNN ,目前大热的Bert就是基于Transformer构建的,这个模型广泛应用于NLP领域,例如机器翻译,问答系统,文本摘要和语音...
  • 最近要开始使用Transformer去做一些事情了,特地把与此相关的知识点记录下来,构建相关的、完整的知识结构体系,以下是要写的文章,本文是这个系列的第三篇:Transformer:Attention集大成者GPT-1 & 2: 预训练+...
  • 1.Transformers介绍 1.1 RNNs存在的问题 (1)计算速度慢: 只能顺序计算,处理完上一个输入后才能计算下一个输入,不支持并行计算 (2)梯度消失与信息丢失问题: ...1.2 Transformer概述 定义:...
  • transformer4.sh

    2020-08-03 11:15:53
    transformer文本摘要的参数,如果没指定则采用默认设置。也就是attention is all you need中默认的参数。
  • Transformer详解

    2020-07-11 20:44:19
    目前大热的Bert就是基于Transformer构建的,这个模型广泛应用于NLP领域,例如机器翻译,问答系统,文本摘要和语音识别等等方向。 2. Transformer结构 2.1 总体结构 Transformer的结构和Attention模
  • Transformer neural network

    2021-03-09 15:30:25
    目前大热的Bert就是基于Transformer构建的,这个模型广泛应用于NLP领域,例如机器翻译,问答系统,文本摘要和语音识别等等方向。 ———————————————— 其实没懂,太复杂了,之后有机会再深入研究 .
  • Transformer简介

    2021-02-06 04:44:57
    它能够很好地应用到很多任务中,包括阅读理解、摘要文本蕴涵,以及独立于任务的句子表示。端到端的网络一般都是基于循环注意力机制而不是序列对齐循环,并且已经有证据表明在简单语言问答和语言建模任务上表现很好...
  • Transformer 与BERT模型

    2019-03-22 22:33:37
    Transformer 与BERT模型1. Transformer1.1 序列到序列任务与Encoder-Decoder框架...序列到序列(Sequence-to-Sequence)是自然语言处理中的一个常见任务,主要用来做泛文本生成的任务,像机器翻译、文本摘要、歌词...
  • 这篇论文中提出一个全新的模型,叫 Transformer,抛弃了以往深度学习任务里面使用到的 CNN 和 RNN ,目前大热的Bert就是基于Transformer构建的,这个模型广泛应用于NLP领域,例如机器翻译,问答系统,文本摘要和语音...
  • Attention is all you need 是一篇将 Attention 思想发挥到极致的论文,出自 Google。...这个模型广泛应用于NLP领域,例如机器翻译,问答系统,文本摘要和语音识别等等方向。可以说是非常牛逼。今天,我打算...
  • NLP学习笔记——Transformer模型 1.概述 《Attention Is All You Need》是Google在2017年提出的一篇将...Transformer模型广泛应用于NLP领域,机器翻译、文本摘要、问答系统等等,目前火热的Bert模型就是基于Trans
  • 与循环神经网络一样,Transformers 旨在处理顺序数据(例如自然语言),以执行翻译和文本摘要等任务。但是,与 RNN 不同,Transformers 不需要按顺序处理顺序数据。 Why do we need the Transformer? RNN 是最...
  • transformer模型的工作...目前大热的Bert就是基于Transformer构建的,这个模型广泛应用于NLP领域,例如机器翻译,问答系统,文本摘要和语音识别等等方向。 以下是论文引用: “Transformer 是第一个完全依赖自注意力
  • Transformer ...transformer这个模型广泛应用于NLP领域,例如机器翻译,问答系统,文本摘要和语音识别等等方向。 其总体结构为: 首先从输入来看:input层首先进入input Embedding,然后进入Positi...
  • 我们提出了一种通过神经网络生成超过数千个单词的长文档的文本摘要的方法。在生成摘要之前,我们执行一个简单的抽取步骤,然后将其用于根据相关信息对transformer语言模型进行条件调整,然后再负责生成摘要。实验...
  • 1. 什么是Transformer 《Attention Is All You Need》是一篇Google提出的将Attention思想...目前大热的Bert就是基于Transformer构建的,这个模型广泛应用于NLP领域,例如机器翻译,问答系统,文本摘要和语音识别等...
  • 文章目录长短距离注意力(LSRA)实验设置架构实验结果机器翻译与自动化设计模型的对比文本摘要 转载来源:https://zhuanlan.zhihu.com/p/146448576 Transformer 的高性能依赖于极高的算力,这让移动端 NLP 严重受限...
  • 通常,在自然语言生成任务(机器翻译,文本摘要,对话系统等)中,RNN和Transfomer都是常用的算法。下面,我们浅谈下采用RNN和Transformer的区别。 2.RNN模型简介: 相比于词袋模型和前馈神经网络模型,RNN可以...

空空如也

空空如也

1 2 3 4
收藏数 79
精华内容 31
关键字:

transformer文本摘要