热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Transformer的numpy实现

下面的代码自下而上的实现Transformer的相关模块功能。这份文档只实现了主要代码。由于时间关系,我无法实现所有函数。对于没有实现的函数,默认用全大写函数名指出,如SOFTMA

下面的代码自下而上的实现Transformer的相关模块功能。这份文档只实现了主要代码。由于时间关系,我无法实现所有函数。对于没有实现的函数,默认用全大写函数名指出,如SOFTMAX

由于时间限制,以下文档只是实现了Transformer前向传播的过程。


输入层

输入层包括Word Embedding和Positional Encoding。Word Embedding可以认为是预训练的词向量,Positional Encoding用于捕获词语的相对位置信息。

\[
\begin{aligned} PE(pos, 2i) &= sin(pos / 10000^{\frac{2i}{d}}) \\ PE(pos, 2i+1) &= cos(pos / 10000^{\frac{2i}{d}}) \end{aligned}
\]

import numpy as np
# Word embedding matrix。通常从文件读入,这里随机初始化
# word_embedding = np.arange(10)
# word_embedding.reshape(vocabulary_size, word_embedding_size)
max_seq_len = 200 # 假定的最大序列长度
position_size = 512 # Position Embedding的维度
# position_encoding是一个类似于word embeding的二维矩阵
# 其中pos是序列中词语的位置,j是维度
position_encoding = np.array([
[pos / np.power(10000, 2.0 * (j // 2) / position_size) for j in range(position_size)]
for pos in range(max_seq_len)])
print("Shape of position encoding: {}".format(position_encoding.shape))
# 每个position encoding的偶数列使用sin,奇数列使用cos处理
position_encoding[:, 0::2] = np.sin(position_encoding[:, 0::2])
position_encoding[:, 1::2] = np.cos(position_encoding[:, 1::2])
# 为了对文本长度对齐,加上Padding行
padding = np.zeros(position_size)
position_encoding = np.vstack((padding, position_encoding))
print("Shape of position encoding after adding padding: {}".format(position_encoding.shape))
def position_encoding(sentence_lens):
"""
给定一个batch的句子,输出这些句子的Position Embedding
"""
# 模拟输入,batch_size=4
sentence_lens = np.array([3,4,5,6])
print("Shape of input: {}".format(input_len.shape))

# 生成输入的位置索引,shape[batch_size, max_seq_len]
# 避开0的索引,不够长度的部分采用0填充
pos_index = np.array([list(range(1, len+1)) + [0] * (max_seq_len - len) for len in sentence_lens])

# 利用pos_index在position_encoding中进行Lookup
position_embedding = LOOKUP(pos_index, position_encoding)

# 返回维度[batch_size, max_seq_len, position_size]
return position_embedding
def word_embdding(sentence_words):
"""
给定一个batch句子,输出这些句子的Word Embedding
"""
# 将word转换为index,通常输入前就做完了
word_index = WORD2INDEX(sentences_words)
word_embedding = LOOKUP(word_index, word_embedding)

# 返回维度[batch_size, max_seq_len, word_embedding_size]
return word_embedding

Shape of position encoding: (200, 512)
Shape of position encoding after adding padding: (201, 512)

得到positional encoding和word embedding之后,将两部分拼接,得到输入向量


层标准化

层标准化将数据标准化为均值为0,标准差为1.以下是实现代码

\[BN(x_i)=\alpha \times \frac{x_i - \mu}{\sqrt{\delta^2 + \epsilon}}+\beta\]

def base_layer_norm(x):
"""
标准化张量x,假设x是三维张量,即
x.shape = (B, L, D)
通常第2维是我们要标准化的维度
"""
# 求均值
mean = np.mean(x, axis=2)
# 求标准差
std = np.std(x, axis=2)

return (x - mean) / std
def layer_norm(x):
"""
引入可学习参数gamma、beta, epsilon用来防止发生数值计算错误
"""
# 求均值
mean = np.mean(x, axis=2, keepdims=True)
# 求标准差
std = np.std(x, axis=2, keepdims=True)

return gamma * (x - mean) / ((std + epsilon) + beta)

缩放点积

因为缩放点积(Scaled dot-product Attention)是Self-Attention的基础,因此这里先实现它。该模块输入是K,Q,V三个张量,输出Context上下文张量和Attention张量

\[Attention(Q,K,V)=softmax(\frac{QK^T}{\sqrt{d_k}})V\]

def scaled_dot_product_attention(query, key, value):
"""
Args:
query: [batch_size, query_len, query_size]
key: [batch_size, key_len, key_size]
value: [batch_size, value_len, value_size]
"""
scale = 1 / np.sqrt(key_size) # 缩放比例
att = np.matmul(query, key.swapaxes(1,2)) / scale
# 利用softmax将att转换为一个概率分布
att = SOFTMAX(att)
# 得到上下文张量
cOntext= np.matmul(att, value)

return contenx, att

Multi-head Attention

论文中使用了8个head,也就是把上述的K,Q,V三个张量按照维度分为8份,每份都经过仿射变换后送入到缩放点积中。

主要流程为:将K,Q,V进行仿射变换,得到对应的query,key和value;然后将它们根据head数目进行维度划分,送入到对应的缩放点积模块进行训练,得到Context张量和Attention张量;多个head的Context张量拼接后经过线性变换就得到了全局的Context张量;最后为了使模型能够更深,收敛更快,对输出加上了dropout,残差连接和层标准化。

下面是代码实现:

\[ MultiHead(Q,K,V)=Concat(head_1, head_2,\cdots,head_h)W_c + b_c\]

def multihead_attention(query, key, value, num_heads=8, input_dim=512):
"""
Args:
query, key, value和缩放点积部分一致
num_heads: multi-head attention 个数
input_dim: 输入维度
"""
# 恒等映射的残差,先保存下来
residual = query

# 每个head分到的维度大小
per_head = input_dim // num_heads

# 对query,key,value进行仿射运算
# W_q,W_k,W_v是三个可学习二维矩阵,shape=[input_dim, (input_dim // num_heads)*num_heads]
query= np.matmul(query, W_q) + b_q
key = np.matmul(key, W_k) + b_k
value = np.matmul(key, W_v) + b_v

# 根据每个head分到的维度对query,key,value重新切分
qeury = query.reshape(batch_size * num_heads, -1, per_head)
key = key.reshape(batch_size * num_heads, -1, per_head)
value = value.reshape(batch_size * num_heads, -1, per_head)

# 对切分的query,key,value进行缩放点积
context, att = scaled_dot_product_attention(query, key, value)

# 将各个head的上下文向量拼接得到最终的context向量
cOntext= context.reshape(batch_size, -1, per_head * num_heads)

# context还需要经过一个线性变换,其中W_c是可学习二维矩阵,shape=[input_dim, input_dim]
cOntext= np.matmul(context, W_c) + b_c

# dropout层
cOntext= DROPOUT(context)

# 输出前进行残差连接和层标准化
output = layer_norm(residual + context)

# 输出
return output, att

Mask

Transformer中有Padding Mask和Sequence Mask。Padding Mask在计算Attention时用来消除某些位置的Attention值,使其在上下文张量中不起作用。Sequence Mask用于Decoder部分,主要是Mask掉当前输出词之后的序列,因为解码过程中是不知道后续词信息的。

为简单起见,上面的Attention都没有考虑Padding Mask。


Feed Forward层

该全连接网络首先将输入x做了一次仿射变换,然后经过ReLU激活函数,再做一次仿射变化,得到最终的输出。

\[FFN(x)=ReLU(xW_1+b_1)W_2 + b_2\]

def feed_forward(x):
# 进行一次仿射变换,其中W_1和b_1分别为矩阵和偏置
out = np.matmul(x, W_1) + b_1
# 施加激活函数
out = ReLU(out)
# 再进行仿射运算,其中W_2和b_2分别为矩阵和偏置
out = np.matmul(out, W_2) + b_2

# Dropout
out = DROPOUT(out)

# 添加残差连接和层标准化
return layer_norm(x + out)

Encoder

整个的Encoder有流程,每一层都是Multi-head Attention和Feed Forward模块组成。代码如下:

class EncoderLayer(object):
"""
Encoder部分一层的结构表示
每层中有Multi-head Attention和Feed Forward前向网络
"""

def __init__(self):
"""
一些参数设置,如head大小,输入维度等
"""
pass

def encode(self, inputs):
# Multi-head Attention

# 先从inputs中获得对应的query,key,value
query = inputs.GET_QUERY()
key = inputs.GET_KEY()
value = inputs.GET_VALUE()
context, attention = multihead_attention(query, key, value, num_heads, input_dim)

# Feed forward层
output = feed_forward(context)

return output, attention

class Encoder(object):
"""
完整Encoder的表示
"""

def __init__(self):
# 定义Encoder所有的层
self.encoder_layers = [layer1, layer2, ... ,layer6]

def forward(self, inputs, input_lens):
# 获得嵌入表示
word_embedding = word_embedding(inputs)
position_embedding = position_encoding(inputs_lens)
final_embedding = word_embedding + position_embedding

# 一层层进行编码
final_attention = []
for layer in self.encoder_layers:
output, attention = layer.encode(final_embedding)
final_attention.append(attention)

# output只返回最后一层,attention全部返回
return output, attention

Decoder

Decoder的除了和Encoder一样,有Multi-head Attention和Feed Forward外,还有一层Masked Multi-head Attention在最下面。代码如下:

class DecoderLayer(object):
"""
Decoder部分一层的结构表示
每层中有两个Multi-head Attention和一个Feed Forward前向网络模块
"""

def decode(self, encoder_output, decoder_inputs):
"""
与Encoder不同,Decoder不仅关注自己的输入,还要考虑Encoder的输出
"""
# 下层Multi-head Attention
# 先从decoder_inputs中获得对应的query,key,value
query = decoder_inputs.GET_QUERY()
key = decoder_inputs.GET_KEY()
value = decoer_inputs.GET_VALUE()
output, attention1 = multihead_attention(query, key, value, num_heads, input_dim)

# 上层Multi-head Attention
# 再从encoder_outputs中获取key和value,decoder的output中获取query
query = output.GET_QUERY()
key = encoder_outputs.GET_KEY()
value = encoder_output.GET_VALUE()
output, attention2 = multihead_attention(query, key, value, num_heads, input_dim)

# Feed forward层
output = feed_forward(output)

return output, attention1, attention2

class Decoder(object):
"""
完整的Decoder表示
"""
def __init__(self):
# 定义Dncoder所有的层
self.decoder_layers = [layer1, layer2, ... ,layer6]

def forward(self, inputs, input_lens, encoder_outputs):
# 获得嵌入表示
word_embedding = word_embedding(inputs)
position_embedding = position_encoding(inputs_lens)
final_embedding = word_embedding + position_embedding

# Sequence Mask。解码过程中要做Sequence Mask
seq_mask = SEQUENCE_MASK(inputs)

# 一层层进行解码
self_attentiOns= []
context_attentiOns= []
for layer in self.decoder_layers:
output, self_attention, context_attention = layer.decode(encoder_outputs, final_embedding)
self_attentions.append(self_attention)
context_attentions.append(context_attention)

# output只返回最后一层,attention全部返回
return output, self_attentions, context_attentions

Transformer整体

class Transformer(object):
"""
Transformer整体代码
"""
def __init__(self):
"""
参数设置:参数主要有
Args:
src_vocab_size: 源语言词汇表大小
src_max_len: 源语言语句最大长度
tgt_vocab_size: 目标语言词汇表大小
tgt_max_len: 目标语言语句最大长度
num_layers=6: 默认Encoder和Decoder为6层
inputs_dim=512: 输入维度默认为512
num_heads=8: 默认Multi-head Attention个数为8
feed_forward_dim=2048:前馈网络维度
drop_out=0.2: Dropout概率
"""
self.encoder = Encoder() # 构造编码器
self.decoder = Decoder() # 构造解码器

def forward(self, src_seq, src_len, tgt_seq, tgt_len):
"""
编解码一个batch的过程
Args:
src_seq: 源语言序列
src_len: 源语言序列长度
tgt_seq: 目标语言序列
tgt_len: 目标语言序列长度
"""
# 编码过程
output, encoder_attention = self.encoder.forward(src_seq, src_len)

# 解码过程
output, self_attention, context_attention = self.decoder.forward(tgt_seq, tgt_len, output)

# 最终要输出概率,所以最终结果还要经过线性层和softmax层
output = np.matmul(output, W_T) + b_T # 其中,W_T和b_T是线性层的二维矩阵和偏置
# 输出概率
output = SOFTMAX(output)

return output, encoder_attention, self_attention, context_attention


推荐阅读
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 本文介绍了iOS数据库Sqlite的SQL语句分类和常见约束关键字。SQL语句分为DDL、DML和DQL三种类型,其中DDL语句用于定义、删除和修改数据表,关键字包括create、drop和alter。常见约束关键字包括if not exists、if exists、primary key、autoincrement、not null和default。此外,还介绍了常见的数据库数据类型,包括integer、text和real。 ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • IOS开发之短信发送与拨打电话的方法详解
    本文详细介绍了在IOS开发中实现短信发送和拨打电话的两种方式,一种是使用系统底层发送,虽然无法自定义短信内容和返回原应用,但是简单方便;另一种是使用第三方框架发送,需要导入MessageUI头文件,并遵守MFMessageComposeViewControllerDelegate协议,可以实现自定义短信内容和返回原应用的功能。 ... [详细]
  • 如何自行分析定位SAP BSP错误
    The“BSPtag”Imentionedintheblogtitlemeansforexamplethetagchtmlb:configCelleratorbelowwhichi ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • 本文介绍了使用PHP实现断点续传乱序合并文件的方法和源码。由于网络原因,文件需要分割成多个部分发送,因此无法按顺序接收。文章中提供了merge2.php的源码,通过使用shuffle函数打乱文件读取顺序,实现了乱序合并文件的功能。同时,还介绍了filesize、glob、unlink、fopen等相关函数的使用。阅读本文可以了解如何使用PHP实现断点续传乱序合并文件的具体步骤。 ... [详细]
  • 本文讨论了Kotlin中扩展函数的一些惯用用法以及其合理性。作者认为在某些情况下,定义扩展函数没有意义,但官方的编码约定支持这种方式。文章还介绍了在类之外定义扩展函数的具体用法,并讨论了避免使用扩展函数的边缘情况。作者提出了对于扩展函数的合理性的质疑,并给出了自己的反驳。最后,文章强调了在编写Kotlin代码时可以自由地使用扩展函数的重要性。 ... [详细]
  • 本文介绍了机器学习手册中关于日期和时区操作的重要性以及其在实际应用中的作用。文章以一个故事为背景,描述了学童们面对老先生的教导时的反应,以及上官如在这个过程中的表现。同时,文章也提到了顾慎为对上官如的恨意以及他们之间的矛盾源于早年的结局。最后,文章强调了日期和时区操作在机器学习中的重要性,并指出了其在实际应用中的作用和意义。 ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • Android系统源码分析Zygote和SystemServer启动过程详解
    本文详细解析了Android系统源码中Zygote和SystemServer的启动过程。首先介绍了系统framework层启动的内容,帮助理解四大组件的启动和管理过程。接着介绍了AMS、PMS等系统服务的作用和调用方式。然后详细分析了Zygote的启动过程,解释了Zygote在Android启动过程中的决定作用。最后通过时序图展示了整个过程。 ... [详细]
author-avatar
Sn_杀手_451
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有