Transformer源码详解
文章目录
Transformer是用来做什么工作的
transformer提出之初适用于做机器翻译工作的,比如给一句中文“我爱你”,利用transformer将其翻译为英文“I love you”
Transformer架构图
上图展示transformer就是由两部分组成,分别为encoder和decoder,对于要被翻译的原文“我爱你”,先经过Embedding和位置编码后,输入到encoder中,encoder在实际应用过程中会经过多层,一般为6层,之后得到输出结果,再输入到decoder中,encoder向decoder的第二层的Multi-Head Attention提供K,V矩阵(K,V是由encoder编码后的结果经过线性变换得到的),同样decoder一般也为6层。具体的内部细节就不在这里赘述,毕竟咱这主要是对代码的讲解
下面我们将先给出Transformer整体架构的代码,而不对其内部实现细节做描述。其内部实现将从整体到局部,再到细节,拆解成以下几个部分逐个讲解
- Encoder
- LayerNorm
- SublayerConnection
- EncoderLayer
- Position-wise Feed-Forward Networks
- attention
- MultiHeadAttention
- Decoder
- DecoderLayer
- subseqent_mask
- Generater
- PositionalEncoding
- Embedding
- Mask
- get_src_pad_mask
- make_std_mask
transformer的整体架构代码:
class EncoderDecoder(nn.Module):
def __init__(self, encoder, decoder, src_embed, tgt_embed, generator):
super(EncoderDecoder, self).__init__()
self.encoder = encoder
self.decoder = decoder
self.src_embed = src_embed
self.tgt_embed = tgt_embed
self.generator = generator
def forward(self, src, tgt, src_mask, tgt_mask):
"这里encode输入的src是已经经过Embedding过后的数据"
return self.decode(self.encode(src, src_mask), src_mask, tgt, tgt_mask)
def encode(self, src, src_mask):
return self.encoder(self.src_embed(src), src_mask)
def decode(self, memory, src_mask, tgt, tgt_mask):
return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)
上面的代码没什么好说的就是一个简单的编码器-解码器类,实例化该类要给出我们写好的encoder,decoder,encoder和decoder输入数据的编码方式,和最后输出结果的generator,类中由两个方法encode和decode分别实现编码和解码
Encoder
class Encoder(nn.Module):
"这个编码器将包含多个encoderlayer"
def __init__(self, layer, N):
# layer: 就是要给出的encoderlayer
# N: 要叠加多少层的encoderlayer
super(Encoder, self).__init__()
# 首先克隆出多层encoderlayer放入属性layers中
self.layers = clones(layer, N)
# 每一层encoderlayer都要经过一层规范反再输入给下一层
# 同样Encoder过后将结果给Decoder之前也要进行规范化,这里我们定义了norm就是为了给 # Decoder之前将其规范法
self.norm = LayerNorm(layer.size)
def forward(self, x, mask):
for layer in self.layers:
x = layer(x, mask)
return self.norm(x)
上面的代码用到了clones函数和LayerNorm函数,它们的定义如下:
clones函数:
def clones(module, N):
"就是将一个module复制N个次年放到一个列表中"
return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])
LayerNorm
class LayerNorm(nn.Module):
def __init__(self, features, eps=1e-6):
super(LayerNorm, self).__init__()
# features: [batch_size, max_len, d_model]
self.a_2 = nn.Parameter(torch.ones(features))
self.b_2 = nn.Parameter(torch.zeros(features))
self.eps = eps
def forward(self, x):
# x: [batchsize, max_len, d_model]
mean = x.mean(-1, keepdim=True) # mean: [batch_size, max_len, 1]
std = x.std(-1, keepdim=True) # std: [batch_size, max_len, 1]
# 最后的return的结果实际上在最后的一个维度上发生了广播
# 具体是在运算self.a_2 * (x - mean) / (std + self.eps),运算乘法是发生的
return self.a_2 * (x - mean) / (std + self.eps) + self.b_2
Layernorm的具体计算公式:
μ
=
1
D
∑
i
=
1
D
x
i
\mu=\frac{1}{D}\sum_{i = 1}^{D}x_{i}
μ=D1i=1∑Dxi
σ
2
=
1
D
∑
i
=
1
D
(
x
i
−
μ
)
2
\sigma^{2}=\frac{1}{D}\sum_{i = 1}^{D}(x_{i}-\mu)^{2}
σ2=D1i=1∑D(xi−μ)2
然后进行归一化:
x
^
=
x
−
μ
σ
2
+
ϵ
\hat{x}=\frac{x-\mu}{\sqrt{\sigma^{2}+\epsilon}}
x^=σ2+ϵx−μ
其中 ϵ \epsilon ϵ是一个很小的常数,通常取 1 e − 5 1e - 5 1e−5或 1 e − 6 1e - 6 1e−6,用于防止分母为零。
最后进行线性变换:
y
=
γ
x
^
+
β
y=\gamma\hat{x}+\beta
y=γx^+β
其中 γ \gamma γ和 β \beta β是可学习的参数,分别用于缩放和平移归一化后的数据。
SublayerConnection
不论是encoderlayer中还是decoderlayer中内部都可以在细分子层,每一个子层中都会有一个Layernorm的过程所以我们定义一个SublayerConnection类用于实现子层的操作和Layernorm
就比如上图将encoder分为两个子层,将decoder分为三个子层。
class SublayerConnection(nn.Module):
def __init__(self, size, dropout):
super(SublayerConnection, self).__init__()
self.norm = LayerNorm(size)
self.dropout = nn.Dropout(dropout)
def forward(self, x, sublayer):
# return x + self.dropout(sublayer(self.norm(x)))
# 上面注释的代码是哈佛NLP团队的源码,我也不是很明白为什么要先norm再进行sublayer
return x + self.dropout(self.norm(sublayer(x)))
encoderlayer
class EncoderLayer(nn.Module):
"编码器层有两个子层,在第一个子层实现自注意力机制,第二个子层中实现前馈网络"
def __init__(self, size, self_attn, feed_forward, dropout):
super(EncoderLayer, self).__init__()
self.self_attn = self_attn
self.feed_forward = feed_forward
# 这一步克隆出两个子层
self.sublayer = clones(SublayerConnection(size, dropout), 2)
self.size = size
def forward(self, x, mask):
# 第一个sublayer实现自注意力机制,传入已经enbedding和加上位置编码的数据,和自注意 # 力实现的的函数
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
# 第二个子层实现前馈网络
return self.sublayer[1](x, self.feed_forward)
Position-wise Feed-Forward Networks
我们的编码器和解码器包含一个完全连接的前馈网络,分别应用于每个位置。这由两个线性变换组成,其之间用ReLu激活函数连接。
F
F
N
(
x
)
=
max
(
0
,
x
W
1
+
b
1
)
W
2
+
b
2
\mathrm{FFN}(x)=\max(0, xW_1 + b_1) W_2 + b_2
FFN(x)=max(0,xW1+b1)W2+b2
其输入输出的维度为
d
m
o
d
e
l
=
512
d_{model}=512
dmodel=512,并且层内转换的维度
d
f
f
=
2048
d_{ff}=2048
dff=2048。
class PositionwiseFeedForward(nn.Module):
"Implements FFN equation."
def __init__(self, d_model, d_ff, dropout=0.1):
super(PositionwiseFeedForward, self).__init__()
self.w_1 = nn.Linear(d_model, d_ff)
self.w_2 = nn.Linear(d_ff, d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
return self.w_2(self.dropout(self.w_1(x).relu()))
attention
在encoder自注意力实现机制中,每一条输入的x通过一层linear线性变换得到q,k,v,可见q,k,v三者它们是同源的(在decoder的第二个子层不是如此),我们一次有多个输入,所以我们用矩阵来表达:
A
t
t
e
n
t
i
o
n
(
Q
,
K
,
V
)
=
s
o
f
t
m
a
x
(
Q
K
T
d
k
)
V
\mathrm{Attention}(Q, K, V) = \mathrm{softmax}(\frac{QK^T}{\sqrt{d_k}})V
Attention(Q,K,V)=softmax(dkQKT)V
def attention(query, key, value, mask=None, dropout=None):
d_k = query.size(-1)
scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
# 这里将mask矩阵中值为0的地方设置为一个极小的数,这样在经过softmax后就几乎变为0了
# 表示当前的数据看不到后面的信息
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
# 对最后一个维度即某一个词对自己所在句子中其他词的相似度值做一个softmax得到重要度
p_attn = scores.softmax(dim=-1)
if dropout is not None:
p_attn = dropout(p_attn)
return torch.matmul(p_attn, value), p_attn
MultiHeadedAttention
对于多头自注意力的解释在这里我有讲解–怎么理解自注意力机制
对于代码的实现我们将每一个头的q,k,v的维度d_k设置为 d k = d m o d e l / n h e a d d_k=d_{model}/n_{head} dk=dmodel/nhead并且同时对多个头进行自注意力运算,最后将多个头得到的结果合并再经过一个线性层得到最终的结果。
M u l t i H e a d ( Q , K , V ) = C o n c a t ( h e a d 1 , . . . , h e a d h ) W O where h e a d i = A t t e n t i o n ( Q W i Q , K W i K , V W i V ) \mathrm{MultiHead}(Q, K, V) = \mathrm{Concat}(\mathrm{head_1}, ..., \mathrm{head_h})W^O \\ \text{where}~\mathrm{head_i} = \mathrm{Attention}(QW^Q_i, KW^K_i, VW^V_i) MultiHead(Q,K,V)=Concat(head1,...,headh)WOwhere headi=Attention(QWiQ,KWiK,VWiV)
上面可训练参数的维度: W i Q ∈ R d model × d k W^Q_i \in \mathbb{R}^{d_{\text{model}} \times d_k} WiQ∈Rdmodel×dk, W i K ∈ R d model × d k W^K_i \in \mathbb{R}^{d_{\text{model}} \times d_k} WiK∈Rdmodel×dk, W i V ∈ R d model × d v W^V_i \in \mathbb{R}^{d_{\text{model}} \times d_v} WiV∈Rdmodel×dv and W O ∈ R h d v × d model W^O \in \mathbb{R}^{hd_v \times d_{\text{model}}} WO∈Rhdv×dmodel.这里的 h d v hd_v hdv表示每个head运算过后得到新的v之后再拼接的维度
class MultiHeadedAttention(nn.Module):
def __init__(self, h, d_model, dropout=0.1):
super(MultiHeadedAttention, self).__init__()
# 对输入乡里那个维度//头数若有余数则报错
assert d_model % h == 0
# We assume d_v always equals d_k
self.d_k = d_model // h
# 头数
self.h = h
# 构建四个linear层,前三个用于Q,K,V的计算,最后一个用于对每个头拼接后的结果运算
self.linears = clones(nn.Linear(d_model, d_model), 4)
self.attn = None
self.dropout = nn.Dropout(p=dropout)
def forward(self, query, key, value, mask=None):
if mask is not None:
# Same mask applied to all h heads.
# 使用unsqueeze扩展维度,代表多头中的第n头
mask = mask.unsqueeze(1)
# 每个头的batch_size
nbatches = query.size(0)
# 1) Do all the linear projections in batch from d_model => h x d_k
# 这里分别对Q,K,V矩阵进行线性变换,三个矩阵的维度都是[nbatches, H, N, # d_k],这里的H表示encoder输入数据的某一句的词数即src_len,若是decoder则表示 # tgt_len
query, key, value = [
# x线性变换后: [nbatches, N, d_model]
# 经过view()方法x: [nbatches, N, H, d_k],将原本的d_model切分为H快,表示H # 个头
# 再交换1,2两个维度,x: [nbatches, H, N, d_k]
lin(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)
for lin, x in zip(self.linears, (query, key, value))
]
# 2) Apply attention on all the projected vectors in batch.
# 对所有的头进行自注意力计算,因为是数据是以矩阵的形势存在,所有头直接并行
x, self.attn = attention(
query, key, value, mask=mask, dropout=self.dropout
)
# 3) "Concat" using a view and apply a final linear.
# 将x的维度进行变换,即对所有头的结果拼接
# x: [nbatches, N, d_model]
x = (
x.transpose(1, 2)
.contiguous()
.view(nbatches, -1, self.h * self.d_k)
)
del query
del key
del value
return self.linears[-1](x)
上面的代码中对于Q,K,V矩阵的生成是比较难理解的,其中涉及了较多的维度上的变化,至于我们为什么要交换1,2两处维度。是因为python的高维矩阵间的运算只看最后两个维度,将头数放在前面就可以实现各个头内部进行运算。可以观察下面的示意图
多头自注意力在模型中的使用
Transformer 以三种不同的方式使用多头注意力:
- 在“编码器-解码器注意力”层中,查询来自 previous decoder 层,键K和值V来自编码器的输出。encoder的输出会流向decoder的每一层decoderlayer。
- 编码器包含自我注意层。在自我注意层的所有键、值和查询都是同源的,为encoder中上一层的输出。每个位置可以关注前一个层。
- 同样地,解码器中的自注意力层的所有键、值和查询也都是同源的,允许解码器中的每个位置关注到该位置及之前的所有位置。为了保持自回归属性,我们需要阻止解码器中的左向信息流。我们在缩放点积注意力内部通过掩码(设置为 )来实现这一点,即在 softmax 的输入中屏蔽后面本不应得到的信息。
Decoder
在前面已经将Encoder解释清楚,后面的Decoder基本同理
class Decoder(nn.Module):
"Generic N layer decoder with masking."
def __init__(self, layer, N):
super(Decoder, self).__init__()
self.layers = clones(layer, N)
self.norm = LayerNorm(layer.size)
def forward(self, x, memory, src_mask, tgt_mask):
"""
x: Decoder的输入,[batch_size, tgt_len, d_model]
memory: Encoder的输出,[batch_size, src_len, d_model]
src_mask: 对Encoder输入数据的掩码,一般就是对padding部分的掩码
tgt_mask: 为了防止解码器看到后面的信息需要将后面的信息擦除
"""
for layer in self.layers:
x = layer(x, memory, src_mask, tgt_mask)
return self.norm(x)
decoderlayer
除了每个编码器层中的两个子层之外,解码器插入第三个子层(在decoderlayer的中间),该子层对编码器堆栈的输出执行多头注意。与编码器类似,我们在每个子层周围使用残差连接,然后进行层规范化。
class DecoderLayer(nn.Module):
"Decoder is made of self-attn, src-attn, and feed forward (defined below)"
def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
super(DecoderLayer, self).__init__()
self.size = size
# 用于第一层注意力的函数
self.self_attn = self_attn
# 用于第二层自注意力的函数
self.src_attn = src_attn
self.feed_forward = feed_forward
self.sublayer = clones(SublayerConnection(size, dropout), 3)
def forward(self, x, memory, src_mask, tgt_mask):
"Follow Figure 1 (right) for connections."
m = memory
# 第一层做解码器输入的自注意力机制
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask))
# 第二层,由编码器提供K,V,做与编码器的自注意力机制
x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, src_mask))
return self.sublayer[2](x, self.feed_forward)
subsequent_mask
对于decoderlayer的输入在第一层做自注意力时,我们需要对其做一个掩码操作,因为在真正用该模型进行机器翻译时,机器时看不到后来的信息的,所以必须利用mask将后面的信息屏蔽掉
def subsequent_mask(size):
"Mask out subsequent positions."
# 这里的size=tgt_len,即decoder输入的一句话的词数
attn_shape = (1, size, size)
subsequent_mask = torch.triu(torch.ones(attn_shape), diagonal=1).type(
torch.uint8
)
return subsequent_mask == 0
上述代码运用到了torch.triu()方法,该方法的具体使用可以自己搜索一下。
最终得到的mask效果如下:
print(subsequent_mask(5))
"""
输出:
tensor([[[ True, False, False, False, False],
[ True, True, False, False, False],
[ True, True, True, False, False],
[ True, True, True, True, False],
[ True, True, True, True, True]]])
"""
其中False表示将该数据遮掩住。
Generator
class Generator(nn.Module):
"Define standard linear + softmax generation step."
def __init__(self, d_model, vocab):
# 初始化函数的输入参数有两个,d_model代表词嵌入维度,vocab.size代表词表大小
super(Generator, self).__init__()
self.proj = nn.Linear(d_model, vocab)
def forward(self, x):
# 将预测出来的词向量经过一个linear成转化为词表的长度,表示对词表里词的预测,再通过 # softmax对每个词的概率进行预测
return log_softmax(self.proj(x), dim=-1)
PositionalEncoding
注意一下transforme的位置编码是绝对位置编码,训练文本的每一句话分词过后的长度(词的个数)都是一样的,在位置编码函数内设置一个最大长度max_len,每句话的长度都不能超过他,所以每句话的每个词都有着其固定的位置编码,并且transformer的位置编码在训练阶段都不会发生改变,是固定的。
P
E
(
p
o
s
,
2
i
)
=
sin
(
p
o
s
/
10000
2
i
/
d
model
)
PE_{(pos,2i)} = \sin(pos / 10000^{2i/d_{\text{model}}})
PE(pos,2i)=sin(pos/100002i/dmodel)
P E ( p o s , 2 i + 1 ) = cos ( p o s / 10000 2 i / d model ) PE_{(pos,2i+1)} = \cos(pos / 10000^{2i/d_{\text{model}}}) PE(pos,2i+1)=cos(pos/100002i/dmodel)
其中 p o s pos pos是位置, i i i是维度, d m o d e l d_{model} dmodel是词向量的维度,不同的 p o s pos pos会得到不同的正弦和余弦值组合由于正弦和余弦函数的周期性和单调性,在给定的序列长度内,每个位置的编码都是唯一的,不会出现重复。
class PositionalEncoding(nn.Module):
"Implement the PE function."
def __init__(self, d_model, dropout, max_len=5000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
# Compute the positional encodings once in log space.
# pe: [max_len, d_model]
pe = torch.zeros(max_len, d_model)
# position: [max_len, 1]
position = torch.arange(0, max_len).unsqueeze(1)
# div_term: [256]
div_term = torch.exp(
torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model)
)
# 这里面涉及到了二维矩阵与一维矩阵的*乘,这里面涉及到了广播操作,建议打印出来观察一下
pe[:, 0::2] = torch.sin(position * div_term) # 偶数列位置编码
pe[:, 1::2] = torch.cos(position * div_term) # 奇数列位置编码
# 给pe增加一个维度,使其能与三维的x相加,x相较于pe多了一个batch_size维度
pe = pe.unsqueeze(0)
self.register_buffer("pe", pe)
def forward(self, x):
# pe[:, : x.size(1)]这样写是因为x的一句话的词数并不一定有max_len这么长,所以要将 # pe切割再相加
x = x + self.pe[:, : x.size(1)].requires_grad_(False)
return self.dropout(x)
上面的代码就是对位置编码公式的实现,里面有着一些python的运算机制,建议将其拆分一行一行的执行,看看它输出的到底是什么事什么样的形状,才能更好地理解。
Embedding
对于输入文本,在对其分词过后,每一个词就是一个token,可以在词向量表中找到其对应的词向量
class Embeddings(nn.Module):
def __init__(self, d_model, vocab):
super(Embeddings, self).__init__()
# 对于Embedding的输入参数,vocab:需要进行embedding的词数,d_model:每个词的词向 # 量维度
self.lut = nn.Embedding(vocab, d_model)
self.d_model = d_model
def forward(self, x):
return self.lut(x) * math.sqrt(self.d_model)
Mask
在transformer架构中有三处需要进项掩码操作:
- encoder的第一个子层的自注意力计算中需要对进行过padding操作的句子做掩码,因为padding加入的词使我们认为设置的,是无效信息,自注意力不应该获取它的信息
- decoder第一个子层的自注意力计算,对于机器翻译工作,解码器的输入应该是先给出一个开头的词,然后预测生成一个词后,在将开头词与刚刚预测出来的词再一起放入decoder的输入中,这样循环往复,所以我们需要对decoder的输入做掩码,让每个词看不到它后面词的信息,即subsequent_mask,这个我们在讲解Decoder已经实现
- decoder的第二个子层,与encoder的输出做自注意力机制,在本文借鉴的文章(哈佛NLP团队的代码)中,这一层的掩码,将subsequent_mask和get_src_pad_mask进行了叠加,即对encoder的输出做padding掩码,也对decoder的输入做掩码,但我觉得,transformer一开始提出是为了做机器翻译的工作,Encoder的输出是固定的全局信息,无需遮挡每个词的后继信息,所以应该只需要加padding掩码。
get_src_pad_mask
def get_attn_pad_mask(seq_q, seq_k, pad):
'''
seq_q: [batch_size, seq_len]
seq_k: [batch_size, seq_len]
seq_len could be src_len or it could be tgt_len
seq_len in seq_q and seq_len in seq_k maybe not equal
'''
batch_size, len_q = seq_q.size()
batch_size, len_k = seq_k.size()
# eq(zero) is PAD token
pad_attn_mask = (seq_k.data != pad).unsqueeze(-2) # [batch_size, 1, len_k], True is masked
return pad_attn_mask.expand(batch_size, len_q, len_k) # [batch_size, len_q, len_k]
不论是Encoder还是Decoder都可能需要对输入做一个padding的掩码,如果是在 Decoder 中调用的,seq_len就有可能等于src_len,也有可能等于tgt_len(因为 Decoder 有两次 mask)。
上面代码的核心是pad_attn_mask = (seq_k != pad).unsqueeze(-2)
即将seq_k.data
里的数据中如果等于pad则替换为False,若不为pad则替换为True。
补充:上面代码之所给了两个seq输入seq_q
和seq_k
,是因为decoder第二个子层Q,K分别来自解码器和编码器,二者的序列长度可能是不同的。
make_std_mask
def make_std_mask(tgt, pad):
"Create a mask to hide padding and future words."
# tgt输入同上src
tgt_mask = (tgt != pad).unsqueeze(-2)
tgt_mask = tgt_mask & subsequent_mask(tgt.size(-1)).type_as(
tgt_mask.data
)
return tgt_mask
tgt_mask = (tgt != pad).unsqueeze(-2)
这句代码中,将tgt中不等于pad的设置为True,等于pad这只为False,然后再与subsequent_mask
做一个‘与’操作,就得到了既对padding也对后置信息做掩码的mask矩阵。
在哈佛团队写的源码中,他讲get_attn_pad_mask和make_std_mask放在了一个类中,因为不论是encoder还是,decoder的输入数据都可能要做掩码操作
class Batch:
"""Object for holding a batch of data with mask during training."""
def __init__(self, src, tgt=None, pad=2): # 2 = <blank>
self.src = src
# 这里对src的输入应该是形如[batch_size, seq_len]
# 其中batch_size表示有多少个句子,sep_len表示一个句子里的token数(这里的token已 # 经经过了padding操作)
self.src_mask = (src != pad).unsqueeze(-2)
if tgt is not None:
self.tgt = tgt[:, :-1]
# 这里的tgt_y将第一个词去除,因为一般encoder和decoder输入句子首部都是s表示句 # 子的开始,末尾是e表示句子结束
# 所以tgt_y就表示着要往后预测的词有哪些
self.tgt_y = tgt[:, 1:]
self.tgt_mask = self.make_std_mask(self.tgt, pad)
# ntokens: 还要往后预测多少个词
self.ntokens = (self.tgt_y != pad).data.sum()
@staticmethod
def make_std_mask(tgt, pad):
"Create a mask to hide padding and future words."
# tgt输入同上src
tgt_mask = (tgt != pad).unsqueeze(-2)
tgt_mask = tgt_mask & subsequent_mask(tgt.size(-1)).type_as(
tgt_mask.data
)
return tgt_mask
测试
下面我们对上面的代码进行一个简单的测试,encoder的输入词为src = torch.LongTensor([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]])
这里面的一个数字表示的是一个token,就是一个词,将其大小输入给Embedding得到10个词向量。然后再通过transformer预测出10个新的数字。
-
定义自己的model
def make_model( src_vocab, tgt_vocab, N=6, d_model=512, d_ff=2048, h=8, dropout=0.1 ): "Helper: Construct a model from hyperparameters." c = copy.deepcopy attn = MultiHeadedAttention(h, d_model) ff = PositionwiseFeedForward(d_model, d_ff, dropout) position = PositionalEncoding(d_model, dropout) model = EncoderDecoder( Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N), Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout), N), nn.Sequential(Embeddings(d_model, src_vocab), c(position)), nn.Sequential(Embeddings(d_model, tgt_vocab), c(position)), Generator(d_model, tgt_vocab), ) # This was important from their code. # Initialize parameters with Glorot / fan_avg. for p in model.parameters(): if p.dim() > 1: # 对model里的参数进行一个初始化 nn.init.xavier_uniform_(p) return model
-
调用model预测
def inference_test(): # 我们的encoder维度应为10,预测的结果也是从[1, 10]中选择,这里的src_vocab, # tgt_vocab之所以是11,因为encoder和decoder的输入要有一个句子开头s, # 真正在训练师应该是12维了,因为不仅有开头s,还有结尾e test_model = make_model(11, 11, 2) test_model.eval() src = torch.LongTensor([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]) # 这里src_mask全为1,表示src无需进行padding操作 src_mask = torch.ones(1, 1, 10) memory = test_model.encode(src, src_mask) # ys表示预测结果,一开始给出一个数字(这个数字也是要经过embedding的),表示句子的开始s ys = torch.zeros(1, 1).type_as(src) print('memory', ':', memory) print(memory.shape) # 预测出10个数字 for i in range(9): out = test_model.decode( memory, src_mask, ys, subsequent_mask(ys.size(1)).type_as(src.data) ) # print(i, ':', out) prob = test_model.generator(out[:, -1]) # print(i, ':', prob) _, next_word = torch.max(prob, dim=1) next_word = next_word.data[0] # print(i, ':', next_word) ys = torch.cat( [ys, torch.empty(1, 1).type_as(src.data).fill_(next_word)], dim=1 ) # print(i, ':', ys) print("Example Untrained Model Prediction:", ys) # 进行10轮预测 def run_tests(): for _ in range(10): inference_test() run_tests()
输出:
Example Untrained Model Prediction: tensor([[0, 5, 9, 2, 5, 2, 5, 2, 5, 2]]) Example Untrained Model Prediction: tensor([[0, 4, 4, 4, 4, 4, 4, 4, 4, 4]]) Example Untrained Model Prediction: tensor([[0, 9, 9, 9, 6, 2, 0, 0, 8, 0]]) Example Untrained Model Prediction: tensor([[0, 4, 7, 0, 4, 7, 0, 4, 4, 4]]) Example Untrained Model Prediction: tensor([[0, 8, 3, 8, 3, 8, 3, 8, 2, 9]]) Example Untrained Model Prediction: tensor([[0, 8, 8, 8, 0, 6, 0, 0, 6, 6]]) Example Untrained Model Prediction: tensor([[0, 9, 7, 9, 9, 1, 9, 1, 9, 1]]) Example Untrained Model Prediction: tensor([[0, 3, 3, 3, 3, 3, 3, 3, 3, 3]]) Example Untrained Model Prediction: tensor([[0, 3, 5, 3, 3, 3, 1, 3, 1, 3]]) Example Untrained Model Prediction: tensor([[0, 0, 0, 2, 0, 2, 0, 2, 0, 2]])