tensorflow2: attention机制实现

代码参考网络上资料,如有侵权,可联系删除

1. 为什么进行attention

物理意义: 将 Q, K 投影在不同的空间上, 然后学习相似度。v 是key的内容表示。初始化 WQ,WK不同是self-attention具有泛化能力的原因, 从而学习到 序列中依赖语义关系。
如果不设置 W_Q、W_k、W_v,我们计算的权重很大程度上依赖于我们如何确定原始输入向量。

2. 官网实现

tf.keras.layers.Attention(
use_scale=False, **kwargs
)
输入为形状[batch_size,Tq,dim]的查询张量,形状[batch_size,Tv,dim]的值张量和形状[batch_size,Tv,dim]的键张量

计算遵循以下步骤:
计算形状为[batch_size,Tq,Tv]的分数作为查询键点积:
scores = tf.matmul(query,key,transpose_b = True)。

使用分数来计算形状为[batch_size,Tq,Tv]的分布:
distribution = tf.nn.softmax(scores)。

使用distribution创建具有形状[batch_size,Tq,dim]的值的线性组合:
返回tf.matmul(distribution,value)。

参数:
use_scale:如果为True,将创建一个标量变量来缩放scores
causal:设置为True可使解码器self-attention。添加一个罩,使位置i无法参与位置j> i。这样可以防止信息流从未来传递到过去。
dropout:attention scores下降的百分比

3. 自定义self-attention

# 缩放点注意力机制
def scaled_dot_product_attention(q, k, v, mask):
    """
    Args:
    q: shape == (...., seq_len_q, depth)
    k: shape == (...., seq_len_k, depth)
    v: shape == (...., seq_len_v, depth_v)
    seq_len_v = seq_len_k
    mask : shape == (seq_len_q, seq_len_k)

    Returns:
    -  output: weighted sum
    -  attention_weights: weights of attention
    """
    print('---q', tf.shape(q))
    print('---k', tf.shape(k))
    print('---v', tf.shape(v))


    # matmul_qk.shapes: [....., seq_len_q, seq_len_k]
    matmul_qk = tf.matmul(q, k, transpose_b=True)
    print('---qk', tf.shape(matmul_qk))

    dk = tf.cast(tf.shape(k)[-1], tf.float32)
    # key的向量维度。 为什么选择 key, 因为 向量初始化时是0,1的正太分布。 q,k 矩阵相乘时,在key的纬度上进行累加,方差变为k。因此做样的处理。
    # matmul_qk.shapes: [....., seq_len_q, seq_len_k]
    scaled_attention_logits = matmul_qk / tf.math.sqrt(dk)

    if mask is not None:
        scaled_attention_logits += (mask * -1e9)  # 给mask 乘一个最小的值。 该值加到对应的padding=0位置,这时 softmax结果是趋近0。

    # attention_weights.shape == [...., seq_len_q, seq_len_k]
    attention_weights = tf.nn.softmax(scaled_attention_logits, axis=-1)  # -1 表示在最后一个纬度熵进行softmax
    print('---attention_weights', tf.shape(attention_weights))

    # out_puts.shape == [...., seq_len_q, depth_v]
    out_puts = tf.matmul(attention_weights, v)
    print('---out_puts', tf.shape(out_puts))
    return out_puts, attention_weights

# test dot-attention
# def print_scaled_dot_product_attention(q, k, v):
#     temp_out, temp_att = scaled_dot_product_attention(q, k, v, None)
#     return temp_out, temp_att
#
# temp_k = tf.constant([[1, 3, 0],
#                       [2, 1, 0],
#                       [3, 2, 0],
#                       [4, 1, 0]
#                       ], dtype=tf.float32)
#
# temp_v = tf.constant([[1, 2],
#                       [2, 1],
#                       [3, 2],
#                       [4, 1]
#                       ], dtype=tf.float32)   # seq_len_v = seq_len_k
#
# temp_q = tf.constant([[1, 3, 0],
#                       [2, 3, 0],
#                       [4, 1, 0]
#                       ], dtype=tf.float32)  # q 的depth 与 k的depth 需要相同
#
# temp_out, temp_att = print_scaled_dot_product_attention(temp_q, temp_k, temp_v)
#
# print(temp_out)
# print(temp_att)
#

# 带mask 的attention
def attention_mask():
    attention = layers.Attention(use_scale=False, dropout=0)
    #
    # key: [batch_sz, key_dims, dims] = [1, 4, 2]
    enc_outputs = tf.constant([[1, 1], [2, 2], [3, 3], [4, 4]], dtype=tf.float32)
    # value: [batch_sz, value_dims, dims] = [1, 4, 2 ]
    # mask
    #如果给定, mask==False的位置输出为0
    #如果给定, mask==False的位置不会对输出产生贡献.
    value_mask = tf.constant([[True, True, False, False]], dtype=tf.bool)  #
    # query: [batch_sz, query_dims, dims] = [1, 1, 2]
    dec_outputs = tf.constant([[[1, 1]]], dtype=tf.float32)
    atten = attention([dec_outputs, enc_outputs, enc_outputs], [None, value_mask])  # query key value

    # 自定义验证
    # tf.reduce_sum(enc_outputs * dec_outputs, 2)  等价 matmul_qk = tf.matmul(q, k, transpose_b=True)
    score = tf.reduce_sum(enc_outputs * dec_outputs, 2) - 1.e9 * (1 - tf.cast(value_mask, dtype=tf.float32))
    weight = keras.activations.softmax(score, axis=1)
    att = tf.expand_dims(weight, 2) * enc_outputs
    att = tf.reduce_sum(att, 1)
    # 验证 官网mask 方法与自定义mask方法结果一致

    #点击缩放后,发现与官网给的use_scale有差异
    # dk = tf.cast(tf.shape(enc_outputs)[-1], tf.float32)
    # score = tf.reduce_sum(enc_outputs * dec_outputs, 2)/tf.math.sqrt(dk) - 1.e9 * (1 - tf.cast(value_mask, dtype=tf.float32))

# 单头self-attention
class OneHeadAttention(keras.layers.Layer):
    """
    理论上
    x --Wq0 -->q0
    x --Wk0 --k0
    x --Wv0 --v0
    self attention, qkv 一样的?
    实战中:, qkv 不一样
    x --Wq0 -->q0
    x --Wk0 --k0
    x --Wv0 --v0

    小矩阵乘法,变为大矩阵的乘法。
    q -->Wq(大的矩阵)->Q -- split -> q0, q1, q2 [batch_size * seq_len,  depth]
    k, v 一样
    """
    def __init__(self, d_model):
        super(OneHeadAttention, self).__init__()
        self.d_model = d_model  #
        self.WQ = keras.layers.Dense(self.d_model)
        self.WK = keras.layers.Dense(self.d_model)
        self.WV = keras.layers.Dense(self.d_model)

        self.dense = keras.layers.Dense(self.d_model)

    def call(self, q, k, v, mask):
        batch_size = tf.shape(q)[0]

        # 生成大的 q, k, v矩阵
        q = self.WQ(q)  # q.shape:(batch_size, seq_len_q, depth)
        k = self.WK(k)  # k.shape:(batch_size, seq_len_k, depth)
        v = self.WV(v)  # k.shape:(batch_size, seq_len_v, depth)

        # scaled_attention_out_puts.shape: [batch_size,  seq_len_q, depth ]
        # attention_weights.shape: [batch_size,  seq_len_q, seq_len_k],
        scaled_attention_out_puts, attention_weights = \
            scaled_dot_product_attention(q, k, v, mask)

        output = self.dense(scaled_attention_out_puts)
        return output, attention_weights


#  test OneHeadAttention
temp_mha = OneHeadAttention(d_model=128)   # 类初始化
y = tf.random.uniform((1, 60, 64))  # [batch_size, seq_len_q, dim]  因此,输入的dim可以忽略。 最终输出的是v的dim
output, attention_weights = temp_mha(y, y, y, mask=None)  # 初始化 q, k, v ,默认调用call 函数。


# MultiHeadAttention 实现
from tensorflow import keras
class MultiHeadAttention(keras.layers.Layer):
    """
    理论上
    x --Wq0 -->q0
    x --Wk0 --k0
    x --Wv0 --v0
    self attention, qkv 一样的?
    实战中:, qkv 不一样
    x --Wq0 -->q0
    x --Wk0 --k0
    x --Wv0 --v0

    小矩阵乘法,变为大矩阵的乘法。
    q -->Wq(大的矩阵)->Q -- split -> q0, q1, q2 [batch_size * seq_len,  depth]
    k, v 一样
    """
    def __init__(self, d_model, num_heads):
        super(MultiHeadAttention, self).__init__()
        self.num_heads = num_heads  # head 的个数
        self.d_model = d_model  # 多头最终concat 的dim
        assert self.d_model % self.num_heads == 0  # model的维度必须是head的整数倍

        self.depth = self.d_model // self.num_heads

        self.WQ = keras.layers.Dense(self.d_model)
        self.WK = keras.layers.Dense(self.d_model)
        self.WV = keras.layers.Dense(self.d_model)

        self.dense = keras.layers.Dense(self.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, q, k, v, mask):
        batch_size = tf.shape(q)[0]

        # 生成大的 q, k, v矩阵
        q = self.WQ(q)  # q.shape:(batch_size, seq_len_q, depth)
        k = self.WK(k)  # k.shape:(batch_size, seq_len_k, depth)
        v = self.WV(v)  # k.shape:(batch_size, seq_len_v, depth)

        # q.shape :[batch_size, num_heads, seq_len_q, depth]
        q = self.split_heads(q, batch_size)
        # k.shape :[batch_size, num_heads, seq_len_k, depth]
        k = self.split_heads(k, batch_size)
        # v.shape :[batch_size, num_heads, seq_len_k, depth]
        v = self.split_heads(v, batch_size)

        # scaled_attention_out_puts.shape: [batch_size,  num_heads, seq_len_q, depth ]
        # attention_weights.shape: [batch_size, num_heads, seq_len_q, seq_len_k], 
        scaled_attention_out_puts, attention_weights = \
            scaled_dot_product_attention(q, k, v, mask)
        
        # before :scaled_attention_out_puts.shape:[batch_size, num_heads, seq_len_q,  depth]
        # after :scaled_attention_out_puts.shape:[batch_size, seq_len_q, num_heads, depth]
        scaled_attention_out_puts = tf.transpose(
            scaled_attention_out_puts, perm=[0, 2, 1, 3])  # 
        # 将num_heads 进行降纬度。 即多个头进行合并
        # concat_attention.shape: [batch_size, seq_len_q, d_model]
        concat_attention = tf.reshape(scaled_attention_out_puts, (batch_size, -1, self.d_model))

        output = self.dense(concat_attention)
        return output, attention_weights

#  test MultiHeadAttention
temp_mha = MultiHeadAttention(d_model=512, num_heads=8)   # 类初始化
y = tf.random.uniform((1, 60, 256))  # [batch_size, seq_len_q, dim]  因此,输入的dim可以忽略。 最终输出的是v的dim
output, attention_weights = temp_mha(y, y, y, mask=None)  # 初始化 q, k, v ,默认调用call 函数。

print(output)
print(attention_weights)


4. 通过attention api 实现

# 使用 tf.keras.layers.Attention() 进行attention
# 存在问题, 目前还没弄明白 mask 如何加, 待更新。
def attention_test():
    """
    batch_size: batch的大小,
    seq_len: 序列的长度
    embedding_dim: 序列中单个元素的向量纬度
    """
    # input layers
    # input.shape :(batch_size, seq_len)
    input_query_char = tf.keras.layers.Input(shape=(maxlen,), name="input_q_char")

    # 序列进行embedding
    # emb_q_char.shape: (batch_size, seq_len, embedding_dim)
    emb_q_char = tf.keras.layers.Embedding(
        input_dim=vocab_size,
        output_dim=embedding_dim,
        input_length=maxlen,
        name='emb_q_char')(input_query_char)
    print('----emb_q_char', emb_q_char.shape)

    # 初始化 q, k
    # q.shape: (batch_size, seq_len_q, embedding_dim)
    # k.shape: (batch_size, seq_len_k, embedding_dim)
    WQ = keras.layers.Dense(embedding_dim)
    WK = keras.layers.Dense(embedding_dim)
    q = WQ(emb_q_char)
    k = WK(emb_q_char)

    # 调用attention层
    # attention_outputs.shape :(batch_size, seq_len_q, embedding_dim)
    # attention_weights.shape: (batch_size, seq_len_q, seq_len_k)
    attention_outputs, attention_weights = tf.keras.layers.Attention()([q, k],
                                                                       return_attention_scores=True)
    print('---attention_outputs', attention_outputs.shape)
    print('---attention weights', attention_weights.shape)

    # Add&Norm :
    # x 与 attention 进行进行concat
    # 标注化处理, axis=1 ,表示在行上进行标准化处理
    # norm_data.shape: (batch_size, seq_len, embedding_dim)
    layer_normal = tf.keras.layers.LayerNormalization(epsilon=1e-6)
    norm_data = layer_normal(emb_q_char + attention_outputs)
    print('----norm_data shape', norm_data.shape)

    # FeedWork:前馈神经网络
    # diff : 前馈神经网络的维度, 具体这个纬度如何定义需要确认下。
    # ff_data.shape : (batch_size, seq_len_q, embedding_dim)
    fcc = tf.keras.layers.Dense(1024, activation='relu', name='fcc')(norm_data)
    ff_data = tf.keras.layers.Dense(embedding_dim)(fcc)
    print("----fcc shape", fcc.shape)
    print("----ff data", ff_data.shape)

    # 结果进行 sum pool
    # attention_sum_pool.shape:(batch_size,embedding_dim )
    attention_sum_pool = tf.reduce_sum(ff_data, 1)
    print("----attention_sum shape", attention_sum_pool.shape)

    # 关于attention最后结果处理两种方法:
    # 方法1: 经过feed work net, 然后sum pool
    # 方法2: attention结果sum pool + input进行mean pool (后期再验证)
   
    # DNN 建模
    query_merge_vector = tf.keras.layers.concatenate([
        attention_sum_pool
    ])
    query_vector_l1 = tf.keras.layers.Dense(100, activation='relu', name='query_d1')(query_merge_vector)
    query_vector_l2 = tf.keras.layers.Dense(100, activation='relu',
                                            name="query_d4", kernel_regularizer='l2')(query_vector_l1)
    output = tf.keras.layers.Dense(1, activation='sigmoid', name='output')(query_vector_l2)
    model = keras.models.Model(
        inputs=[input_query_char],
        outputs=[output])
    model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
    model.summary()
    return model

if __name__ == '__main__':
    attention_test()
  • 3
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值