自然语言处理N天-实现一个Transformer构建模块

新建 Microsoft PowerPoint 演示文稿 (2).jpg

这个算是在课程学习之外的探索,不过希望能尽快用到项目实践中。在文章里会引用较多的博客,文末会进行reference。
搜索Transformer机制,会发现高分结果基本上都源于一篇论文Jay Alammar的《The Illustrated Transformer》(图解Transformer),提到最多的Attention是Google的《Attention Is All You Need》。

  • 对于Transformer的运行机制了解即可,所以会基于这篇论文来学习Transformer,结合《Sklearn+Tensorflow》中Attention注意力机制一章完成基本的概念学习;
  • 找一个基于Transformer的项目练手

5.代码实现

构建Transformer模块

在这里,作者是严格按照《attention is all you need》中的推导步骤来做的。我们可以参考论文来进行学习。《attention is all you need》
本文还参考了整理 聊聊 Transformer

引入必要库
import numpy as np
import tensorflow as tf
实现层归一

参看论文3.1 Encoder and Decoder Stacks(编码和解码堆栈),Transformer由encoder和decoder构成。
encoder由6个相同的层组成,每一层分别由2部分组成:

  • 第一部分是 multi-head self-attention
  • 第二部分是 position-wise feed-forward network,是一个全连接层
    在每两个子层(sub-layers)之间使用残差连接(residual connection),再接一个层归一(layer normalization)

decoder由6个相同的层组成,每一层分别由3部分组成:

  • 第一个部分是 multi-head self-attention mechanism
  • 第二部分是 multi-head context-attention mechanism
  • 第三部分是一个 position-wise feed-forward network
    在每三个子层(sub-layers)之间使用残差连接(residual connection),再接一个层归一(layer normalization)

tensorflow 在实现 Batch Normalization(各个网络层输出的归一化)时,主要用到nn.moments和batch_normalization

  • moments作用是统计矩,mean 是一阶矩,variance 则是二阶中心矩
  • tf.nn.moments 计算返回的 mean 和 variance 作为 tf.nn.batch_normalization 参数进一步调用
def ln(inputs, epsilon=1e-8, scope='ln'):
    '''
    使用层归一layer normalization
    tensorflow 在实现 Batch Normalization(各个网络层输出的归一化)时,主要用到nn.moments和batch_normalization
    其中moments作用是统计矩,mean 是一阶矩,variance 则是二阶中心矩
    tf.nn.moments 计算返回的 mean 和 variance 作为 tf.nn.batch_normalization 参数进一步调用
    :param inputs: 一个有2个或更多维度的张量,第一个维度是batch_size
    :param epsilon: 很小的数值,防止区域划分错误
    :param scope: 
    :return: 返回一个与inputs相同shape和数据的dtype
    '''
    with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
        inputs_shape = inputs.get_shape()
        params_shape = inputs_shape[-1:]

        mean, variance = tf.nn.moments(inputs, [-1], keep_dims=True)
        beta = tf.get_variable("beta", params_shape, initializer=tf.ones_initializer())
        gamma = tf.get_variable("gamma", params_shape, initializer=tf.ones_initializer())

        normalized = (inputs - mean) / ((variance + epsilon) ** (.5))
        outputs = gamma * normalized + beta
    return outputs
构建token嵌入

这里做的就是注意力,也就是加权值

def get_token_embeddings(vocab_size, num_units, zero_pad=True):
    '''
    构建token嵌入矩阵
    :param vocab_size: 标量V
    :param num_units: 嵌入维度E
    :param zero_pad: 布尔值。如果为True,则第一行(id = 0)的所有值应为常数零
    要轻松应用查询/键掩码,请打开零键盘。
    :return: 权重参数(V,E)
    '''
    with tf.variable_scope("shared_weight_matrix"):
        embeddings = tf.get_variable(
            'weight_mat',
            dtype=tf.float32,
            shape=(vocab_size, num_units),
            initializer=tf.contrib.layers.xavier_initializer()
        )
        if zero_pad:
            embeddings = tf.concat((tf.zeros(shape=[1, num_units]), embeddings[1:, :]), 0)
    return embeddings
构建decoder的mask

mask 表示掩码,它对某些值进行掩盖,使其在参数更新时不产生效果。Transformer 模型里面涉及两种 mask,分别是 padding mask 和 sequence mask。

  • padding mask 在所有的 scaled dot-product attention 里面都需要用到
  • sequence mask 只有在 decoder 的 self-attention 里面用到
def mask(inputs, queries=None, keys=None, type=None):
    '''
    对Keys或Queries进行遮盖
    :param inputs: (N, T_q, T_k)
    :param queries: (N, T_q, d)
    :param keys: (N, T_k, d)
    :return: 
    '''
    padding_num = -2 ** 32 + 1
    if type in ("k", "key", "keys"):
        # Generate masks
        masks = tf.sign(tf.reduce_sum(tf.abs(keys), axis=-1))  # (N, T_k)
        masks = tf.expand_dims(masks, 1)  # (N, 1, T_k)
        masks = tf.tile(masks, [1, tf.shape(queries)[1], 1])  # (N, T_q, T_k)

        # Apply masks to inputs
        paddings = tf.ones_like(inputs) * padding_num
        outputs = tf.where(tf.equal(masks, 0), paddings, inputs)  # (N, T_q, T_k)
    elif type in ("q", "query", "queries"):
        # Generate masks
        masks = tf.sign(tf.reduce_sum(tf.abs(queries), axis=-1))  # (N, T_q)
        masks = tf.expand_dims(masks, -1)  # (N, T_q, 1)
        masks = tf.tile(masks, [1, 1, tf.shape(keys)[1]])  # (N, T_q, T_k)

        # Apply masks to inputs
        outputs = inputs * masks
    elif type in ("f", "future", "right"):
        diag_vals = tf.ones_like(inputs[0, :, :])  # (T_q, T_k)
        tril = tf.linalg.LinearOperatorLowerTriangular(diag_vals).to_dense()  # (T_q, T_k)
        masks = tf.tile(tf.expand_dims(tril, 0), [tf.shape(inputs)[0], 1, 1])  # (N, T_q, T_k)

        paddings = tf.ones_like(masks) * padding_num
        outputs = tf.where(tf.equal(masks, 0), paddings, inputs)
    else:
        print("Check if you entered type correctly!")

    return outputs
构建Context-Attention

查看原论文中3.2.1attention计算公式。
context-attention 是 encoder 和 decoder 之间的 attention,是两个不同序列之间的attention,与来源于自身的 self-attention 相区别。context-attention有很多,这里使用的是scaled dot-product。
通过 query 和 key 的相似性程度来确定 value 的权重分布。

def scaled_dot_product_attention(Q, K, V, causality=False, dropout_rate=0., training=True,
                                 scope='scaled_dot_product_attention'):
    '''
    查看原论文中3.2.1attention计算公式:Attention(Q,K,V)=softmax(Q K^T /√dk ) V
    :param Q: 查询,三维张量,[N, T_q, d_k].
    :param K: keys值,三维张量,[N, T_k, d_v].
    :param V: values值,三维张量,[N, T_k, d_v].
    :param causality: 布尔值,如果为True,就会对未来的数值进行遮盖
    :param dropout_rate: 0到1之间的一个数值
    :param training: 布尔值,用来控制dropout
    :param scope: 
    '''
    with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
        d_k = Q.get_shape().as_list()[-1]
        # dot product
        outputs = tf.matmul(Q, tf.transpose(K, [0, 2, 1]))  # (N, T_q, T_k)
        # scale
        outputs /= d_k ** 0.5
        # key mask
        outputs = mask(outputs, Q, K, type="key")
        # causality or future blinding masking
        if causality:
            outputs = mask(outputs, type='future')

        outputs = tf.nn.softmax(outputs)
        attention = tf.transpose(outputs, [0, 2, 1])
        tf.summary.image("attention", tf.expand_dims(attention[:1], -1))

        outputs = mask(outputs, Q, K, type="query")
        # dropout
        outputs = tf.layers.dropout(outputs, rate=dropout_rate, training=training)

        # weighted sum (context vectors)
        outputs = tf.matmul(outputs, V)  # (N, T_q, d_v)
    return outputs
构建Multi-head attention

论文提到,他们发现将 Q、K、V 通过一个线性映射之后,分成 h 份,对每一份进行 scaled dot-product attention 效果更好。然后,把各个部分的结果合并起来,再次经过线性映射,得到最终的输出。这就是所谓的 multi-head attention。上面的超参数 h 就是 heads 的数量。论文默认是 8。

def multihead_attention(queries, keys, values,
                        num_heads=8,
                        dropout_rate=0,
                        training=True,
                        causality=False,
                        scope="multihead_attention"):
    '''
    查看原论文中3.2.2中multihead_attention构建,这里是将不同的Queries、Keys和values方式线性地投影h次是有益的。线性投影分别为dk,dk和dv尺寸。在每个预计版本进行queries、keys、values,然后并行执行attention功能,产生dv维输出值。这些被连接并再次投影,产生最终值
    :param queries: 三维张量[N, T_q, d_model]
    :param keys: 三维张量[N, T_k, d_model]
    :param values: 三维张量[N, T_k, d_model]
    :param num_heads: heads数
    :param dropout_rate: 
    :param training: 控制dropout机制
    :param causality: 控制是否遮盖
    :param scope: 
    :return: 三维张量(N, T_q, C) 
    '''
    d_model=queries.get_shape().as_list()[-1]
    with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
        # Linear projections
        Q = tf.layers.dense(queries, d_model)  # (N, T_q, d_model)
        K = tf.layers.dense(keys, d_model)  # (N, T_k, d_model)
        V = tf.layers.dense(values, d_model)  # (N, T_k, d_model)

        # Split and concat
        Q_ = tf.concat(tf.split(Q, num_heads, axis=2), axis=0)  # (h*N, T_q, d_model/h)
        K_ = tf.concat(tf.split(K, num_heads, axis=2), axis=0)  # (h*N, T_k, d_model/h)
        V_ = tf.concat(tf.split(V, num_heads, axis=2), axis=0)  # (h*N, T_k, d_model/h)

        # Attention
        outputs=scaled_dot_product_attention(Q_,K_,V_,causality,dropout_rate,training)

        outputs=tf.concat(tf.split(outputs,num_heads,axis=0),axis=2)

        outputs+= queries
        # 归一
        outputs=ln(outputs)

    return outputs
神经网络的前向传播
def ff(inputs, num_units,scope='positionwise_feedforward'):
    '''
    参看论文3.3,实现feed forward net
    :param inputs: 
    :param num_units: 
    :param scope: 
    :return: 
    '''
    with tf.variable_scope(scope,reuse=tf.AUTO_REUSE):
        # Inner layer
        outputs = tf.layers.dense(inputs, num_units[0], activation=tf.nn.relu)

        # Outer layer
        outputs = tf.layers.dense(outputs, num_units[1])

        # Residual connection
        outputs += inputs

        # Normalize
        outputs = ln(outputs)

    return outputs

def label_smoothing(inputs,epsilon=0.1):
    '''
    参看论文5.4,这会降低困惑,因为模型学习会更加不确定,提高了准确性和BLEU分数
    :param inputs: 
    :param epsilon: 
    :return: 
    '''
    V = inputs.get_shape().as_list()[-1]  # number of channels
    return ((1 - epsilon) * inputs) + (epsilon / V)
实现Positional Embedding

现在的 Transformer 架构还没有提取序列顺序的信息,这个信息对于序列而言非常重要,如果缺失了这个信息,可能我们的结果就是:所有词语都对了,但是无法组成有意义的语句。
因此,模型对序列中的词语出现的位置进行编码。在偶数位置,使用正弦编码,在奇数位置,使用余弦编码。

def positional_encoding(inputs,maxlen,masking=True,scope="positional_encoding"):
    '''
    参看论文3.5,由于模型没有循环和卷积,为了让模型知道句子的编号,就必须加入某些绝对位置信息,来表示token之间的关系。  
    positional encoding和embedding有相同的维度,这两个能够相加。
    :param inputs: 
    :param maxlen: 
    :param masking: 
    :param scope: 
    :return: 
    '''
    E = inputs.get_shape().as_list()[-1]  # static
    N, T = tf.shape(inputs)[0], tf.shape(inputs)[1]  # dynamic
    with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
        # position indices
        position_ind = tf.tile(tf.expand_dims(tf.range(T), 0), [N, 1])  # (N, T)

        # 根据论文给的公式,构造出PE矩阵
        position_enc = np.array([
            [pos / np.power(10000, (i - i % 2) / E) for i in range(E)]
            for pos in range(maxlen)])

        # 在偶数位置,使用正弦编码,在奇数位置,使用余弦编码。
        position_enc[:, 0::2] = np.sin(position_enc[:, 0::2])  # dim 2i
        position_enc[:, 1::2] = np.cos(position_enc[:, 1::2])  # dim 2i+1
        position_enc = tf.convert_to_tensor(position_enc, tf.float32)  # (maxlen, E)

        # lookup
        outputs = tf.nn.embedding_lookup(position_enc, position_ind)

        # masks
        if masking:
            outputs = tf.where(tf.equal(inputs, 0), inputs, outputs)

    return tf.to_float(outputs)
Noam计划学习率衰减
def noam_scheme(init_lr, global_step, warmup_steps=4000.):
    '''
    
    :param init_lr: 
    :param global_step: 
    :param warmup_steps: 
    :return: 
    '''
    step = tf.cast(global_step + 1, dtype=tf.float32)
    return init_lr * warmup_steps ** 0.5 * tf.minimum(step * warmup_steps ** -1.5, step ** -0.5)

最关键的模块构建到这里就完成了,总结一下会发现总共有以下九个模块。

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

推荐阅读更多精彩内容