这里列出自己在看论文撸代码时碰到一些问题,希望帮助到跟我一样的NLP小白用户,带着问题上路,更有助于思考
《自然语言处理(NLP)的基本概念》
还可以从另一个角度看Attention,那就是键值查询。键值查询应该有三个基本元素:索引(Query
),键(Key
)和值(Value
),你可以理解为这是一个查字典的过程,Key-Value
对构成一个字典,用户给一个Query,系统找到与之相同的Key,返回对应的Value。那么问题来了,字典里没有与Query相同的Key怎么办?答案是分别计算Query和每一个已有的Key的相似度 w w w,作为权重分配到所有的Value上,并返回它们的加权求和。对应到上面机器翻译的例子,输出序列的局部信息是Query,输入序列的局部信息是Key, w w w是二者的相似度,而Value设为1即可。从上面的分析看出,Attention也可以理解为某种相似性度量。 (引用自《深度学习中的注意力机制》中“Attention Mechanism”章节中“键值查询”的介绍。深入浅出,值得学习。)
在看上面两张图,结合《Transformer模型笔记》中“2. 细节: Multi-Head Attention 与 Scaled Dot-Product Attention”的query, key, value介绍。右图输入input
由n个tokens
(分词)构成,经过线性变换得到n个Embedding
向量,这几个向量分别跟 W O W^{O} WO、 W K W^{K} WK、 W V W^{V} WV相乘得到左图的Q、K、V,Q与K相乘再经过Scale
、Mask
和SoftMax
操作得到相似度得分 w w w ,作为权重分配到对应的V上,并返回它们的加权求和。前文提到,“对应到上面机器翻译的例子,输出序列的局部信息是Query,输入序列的局部信息是Key, w w w是二者的相似度”,而这里的Q和K对应的都是输入序列的局部信息,因此这种Attention可以理解为Self-Attention,这样encoder的每个位置都能去关注前一层encoder输出的所有位置,最终学习的是不同句子内部的联系(语法结构等)。引入Self-Attention的好处在于可以在O(1) 的代价联系序列中两个长期依赖的特征,对于RNN结构可能需要累积更多的时间步骤才能反应过来,因此Self-Attention能够提升网络的可并行性。
第一个问题“query, key, value的理解” 解决了Q、K、V的困惑,那么,Scaled Dot-Product Attention结构中的Mask
又有什么作用呢?这里参考Transformer模型笔记中“attention map”的介绍,来回答一下这个问题。
注意 Attention ( Q , K , V ) = softmax ( Q K T d k ) V \text { Attention }(Q, K, V)=\operatorname{softmax}\left(\frac{Q K^{T}}{\sqrt{d_{k}}}\right) V Attention (Q,K,V)=softmax(dkQKT)V这个公式, Q K T Q K^{T} QKT其实就会组成一个word2word的attention map!(加了softmax之后就是一个和为1的权重了)。比如说你的输入是一句话 “i have a dream” 总共4个单词, 这里就会形成一张4x4的注意力机制的图(或者NxN的Attention Map,N表示序列的长度或者分词的个数)。注意encoder里面是叫self-attention(应该是未使用Mask),decoder里面是叫masked self-attention
,这里的masked就是要在做language modelling(或者像翻译)的时候,不给模型看到未来的信息。
具体地, I I I作为第一个单词,只能有和 I I I自己的attention。 h a v e have have作为第二个单词,有和 I I I、 h a v e have have两个attention。 a a a作为第三个单词,有和 I I I、 h a v e have have、 a a a前面三个单词的attention。到了最后一个单词 d r e a m dream dream的时候,才有对整个句子4个单词的attention。
前两个问题就Scaled Dot-Product Attention中的Q、K、V和Mask做了详细的分析,接下来,我们分析一下Multi-Head Attention是如何引入的?
a. self-attention中,如果输入的句子特别长,那就为形成一个 NxN的attention map,这就会导致内存爆炸。
为此,文章提出使用Multi-Head Attention
机制来提升Attention的性能,具体表现在两个方面:
b. 问题又来了:feed-forward层
并不希望有8个矩阵作为输入,这时候该怎么将{ z 0 \mathbf{z}_{0} z0,…, z 7 \mathbf{z}_{7} z7}压缩一下呢?
如上面右图图所示,采用 W o W^{o} Wo乘上concat后的矩阵{ z 0 \mathbf{z}_{0} z0,…, z 7 \mathbf{z}_{7} z7},得到 z \mathbf{z} z即可.
c. 下面,我们来看看Multi-Head Attention
的完整过程:
d. 本文使用的是Multi-Head Attention,具体体现在三个方面。
深度学习中的注意力机制对attention理解的非常到位,这里忍不住引用过来,记录一下,关于更多细节,请跳转至原作者的博客。
“变形金刚”为何强大:从模型到代码全面解析Google Tensor2Tensor系统
深度学习:transformer模型
a. Positional Encoding要解决什么问题?
到目前为止,我们的Transformer模型还不具备捕捉输入序列中单词顺序的能力。Self-Attention机制建模序列的方式,既不是RNN的时序观点,也不是CNN的结构化观点,而是一种词袋(bag of words)的观点。进一步阐述的话,应该说该机制视一个序列为扁平的结构,因为不论看上去距离多远的词,在self-attention机制中都为1。这样的建模方式,实际上会丢失词之间的相对距离关系。举个例子就是,“牛 吃了 草”、“草 吃了 牛”,“吃了 牛 草”三个句子建模出来的每个词对应的表示,会是一致的,也就是说无论句子的结构怎么打乱,Transformer都会得到类似的结果。
b. 如何用Position Vector来表征序列单词的顺序呢?
为了解决“Transformer模型不具备捕序列捉顺序的能力”的问题,transformer会以input embedding
w = ( w 1 , … , w m ) \mathbf{w}=\left(w_{1}, \dots, w_{m}\right) w=(w1,…,wm)作为输入,让模型学习出某种特殊表征,得到一个Position Vector
p = ( p 1 , … , p m ) \mathbf{p}=\left(p_{1}, \dots, p_{m}\right) p=(p1,…,pm),直觉告诉,最简单的方式是,通过加和得到一个input element
的表征向量 e = ( w 1 + p 1 , … , w m + p m ) \mathbf{e}=\left(w_{1}+p_{1}, \ldots, w_{m}+p_{m}\right) e=(w1+p1,…,wm+pm)。如下图所示:
至此,我们熟悉了positional embedding的通用定义,更多细节请参考文章:(Convolutional Sequence to Sequence Learning)
c. paper中的Positional Encoding又是如何得到Position Vector的呢?
首先,看一下Position Vector
究竟长什么样?
下图展示了由20个单词通过positional encoding
得到的Position Vector
p = ( p 1 , … , p m ) \mathbf{p}=\left(p_{1}, \dots, p_{m}\right) p=(p1,…,pm),这是一个20x512
的矩阵,20行分别对应20个不同的单词,图中每行都表示对应单词通过positional encoding
得到的对应20行Position Vector
,其embedding size=512,值域为[-1,1]。
其次,为什么这20个单词的Position Vector Matrix
在中间看起来断裂了呢?
这是因为左边是通过sine函数产生,右边是通过cosine函数产生。这里难以理解的一个点是,横坐标仅仅表示有512个位置,但并不是跟position一一对应,即左图是用 d m o d e l = 2 i d_{m o d e l}=2i dmodel=2i的位置,通过sin函数得到,右图是用 d m o d e l = 2 i + 1 d_{m o d e l}=2i+1 dmodel=2i+1的位置,通过cosine函数得到,最后再将这两个图片表示的向量拼接到一起,而非按照 1 , 2 , . . . , 512 {1,2,...,512} 1,2,...,512这样顺序排列。,另一种解释是“还需要指出的是,论文中根据维度下标的奇偶性来交替使用sin和cos函数的说法,在代码中并不是这样实现的,而是前一半的维度使用sin函数,后一半的维度使用cos函数,并没有考虑奇偶性”(引用自Tensor2Tensor系统解析)。
最后,给出得到Position Vector
的计算公式:
P E ( p o s , 2 i ) = sin ( pos / 1000 0 2 i / d m o d e l ) P E ( p o s , 2 i + 1 ) = cos ( p o s / 1000 0 2 i / d m o d e l ) \begin{array}{l}{P E_{(p o s, 2 i)}=\sin \left(\operatorname{pos} / 10000^{2 i / d_{m o d e l}}\right)} \\ {P E_{(p o s, 2 i+1)}=\cos \left(p o s / 10000^{2 i / d_{m o d e l}}\right)}\end{array} PE(pos,2i)=sin(pos/100002i/dmodel)PE(pos,2i+1)=cos(pos/100002i/dmodel)
其中, p o s pos pos是word所在位置, i i i表示单词的维度, d m o d e l d_model dmodel表示embedding维度512。当然了,最后别忘了加和操作 e = ( w 1 + p 1 , … , w m + p m ) \mathbf{e}=\left(w_{1}+p_{1}, \ldots, w_{m}+p_{m}\right) e=(w1+p1,…,wm+pm)
d. 为什么sin和cos可以表征位置信息呢?
关于位置编码,作者还尝试了learned positional embedding的方法,所得结果几乎相同。作者最终选择了这种正弦曲线编码的方式是因为,这种方式还适用于test中句子比train中长的情况。(在BERT中使用的是learn的方法)。(引用自[论文笔记]Attention is All You Need)
这里记录一个未理解到位的问题:
为什么positional embedding受限于词典大小,而三角公式明显不受序列长度的限制,可以对“比所遇到序列的更长的序列”进行表示?
e. Position Encoding的具体代码实现,过几天再理解一下
关于位置编码的实现可在Google开源的算法中get_timing_signal_1d()函数和 The Annotated Transformer找到对应的代码,这里摘录如下,注释有删减:
# tensorflow version
def get_timing_signal_1d(length,
channels,
min_timescale=1.0,
max_timescale=1.0e4,
start_index=0):
position = tf.to_float(tf.range(length) + start_index)
num_timescales = channels // 2
log_timescale_increment = (
math.log(float(max_timescale) / float(min_timescale)) /
(tf.to_float(num_timescales) - 1))
inv_timescales = min_timescale * tf.exp(
tf.to_float(tf.range(num_timescales)) * -log_timescale_increment)
scaled_time = tf.expand_dims(position, 1) * tf.expand_dims(inv_timescales, 0)
signal = tf.concat([tf.sin(scaled_time), tf.cos(scaled_time)], axis=1)
signal = tf.pad(signal, [[0, 0], [0, tf.mod(channels, 2)]])
signal = tf.reshape(signal, [1, length, channels])
return signal
# pytorch version
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 = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1)
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 = pe.unsqueeze(0)
self.register_buffer('pe', pe)
def forward(self, x):
x = x + Variable(self.pe[:, :x.size(1)],
requires_grad=False)
return self.dropout(x)
class Batch:
中src和tgt的shape跟src_mask的shape不一致,光看ipynb代码不太好追溯,要用pycharm去debug一下就能很好的理解了class Batch:
self.trg和self.trg_y的含义#!pip install torchtext spacy
#!python -m spacy download en
#!python -m spacy download de
# For data loading.
from torchtext import data, datasets
if True:
import spacy
spacy_de = spacy.load('de')
spacy_en = spacy.load('en')
def tokenize_de(text):
return [tok.text for tok in spacy_de.tokenizer(text)]
def tokenize_en(text):
return [tok.text for tok in spacy_en.tokenizer(text)]
BOS_WORD = ''
EOS_WORD = ''
BLANK_WORD = ""
SRC = data.Field(tokenize=tokenize_de, pad_token=BLANK_WORD)
TGT = data.Field(tokenize=tokenize_en, init_token = BOS_WORD,
eos_token = EOS_WORD, pad_token=BLANK_WORD)
MAX_LEN = 100
train, val, test = datasets.IWSLT.splits(
exts=('.de', '.en'), fields=(SRC, TGT),
filter_pred=lambda x: len(vars(x)['src']) <= MAX_LEN and
len(vars(x)['trg']) <= MAX_LEN)
MIN_FREQ = 2
SRC.build_vocab(train.src, min_freq=MIN_FREQ)
TGT.build_vocab(train.trg, min_freq=MIN_FREQ)
之前看代码的时候一直困惑数据到底长啥样,debug模式下终于拨开了云雾。
class Batch:
"Object for holding a batch of data with mask during training."
def __init__(self, src, trg=None, pad=0):
self.src = src
self.src_mask = (src != pad).unsqueeze(-2)
if trg is not None:
self.trg = trg[:, :-1]
self.trg_y = trg[:, 1:]
self.trg_mask = \
self.make_std_mask(self.trg, pad)
self.ntokens = (self.trg_y != pad).data.sum()
@staticmethod
def make_std_mask(tgt, pad):
"Create a mask to hide padding and future words."
tgt_mask = (tgt != pad).unsqueeze(-2)
tgt_mask = tgt_mask & Variable(
subsequent_mask(tgt.size(-1)).type_as(tgt_mask.data))
return tgt_mask
def rebatch(pad_idx, batch):
"Fix order in torchtext to match ours"
src, trg = batch.src.transpose(0, 1), batch.trg.transpose(0, 1)
return Batch(src, trg, pad_idx) #调用Batch制作数据
这里通过Fake数据的合成,加深对NLP数据格式的理解
def data_gen(V, batch, nbatches):
"Generate random data for a src-tgt copy task."
for i in range(nbatches):
data = torch.from_numpy(np.random.randint(1, V, size=(batch, 10)))
data[:, 0] = 1
src = Variable(data, requires_grad=False)
tgt = Variable(data, requires_grad=False)
yield Batch(src, tgt, 0)
我们在训练中采用三种正则化方法。
我们将dropout应用于每个子层的输出,然后将其添加到子层输入,并进行正则化。我们还将dropout应用于编码器和解码器堆栈中嵌入和位置编码的总和。对于基本的模型,我们使用 P d r o p = 0.1 P_{d r o p}=0.1 Pdrop=0.1。
这里LabelSmoothing求的是KL散度值。
During training, we employed label smoothing of value ϵ l s = 0.1 \epsilon_{ls}=0.1 ϵls=0.1 (cite). This hurts perplexity, as the model learns to be more unsure, but improves accuracy and BLEU score.
Label smoothing actually starts to penalize the model if it gets very confident about a given choice.
这里的图片不是很理解???
#Example of label smoothing.
crit = LabelSmoothing(5, 0, 0.4)
predict = torch.FloatTensor([[0, 0.2, 0.7, 0.1, 0],
[0, 0.2, 0.7, 0.1, 0],
[0, 0.2, 0.7, 0.1, 0]])
v = crit(Variable(predict.log()),
Variable(torch.LongTensor([2, 1, 0])))
# Show the target distributions expected by the system.
plt.imshow(crit.true_dist)
None
crit = LabelSmoothing(5, 0, 0.1)
def loss(x):
d = x + 3 * 1
predict = torch.FloatTensor([[0, x / d, 1 / d, 1 / d, 1 / d],
])
#print(predict)
return crit(Variable(predict.log()),
Variable(torch.LongTensor([1]))).data[0]
plt.plot(np.arange(1, 100), [loss(x) for x in range(1, 100)])
None
# pytorch version
class LabelSmoothing(nn.Module):
"Implement label smoothing."
def __init__(self, size, padding_idx, smoothing=0.0):
super(LabelSmoothing, self).__init__()
self.criterion = nn.KLDivLoss(size_average=False)
self.padding_idx = padding_idx
self.confidence = 1.0 - smoothing
self.smoothing = smoothing
self.size = size
self.true_dist = None
def forward(self, x, target):
assert x.size(1) == self.size
true_dist = x.data.clone()
true_dist.fill_(self.smoothing / (self.size - 2))
true_dist.scatter_(1, target.data.unsqueeze(1), self.confidence)
true_dist[:, self.padding_idx] = 0
mask = torch.nonzero(target.data == self.padding_idx)
if mask.dim() > 0:
true_dist.index_fill_(0, mask.squeeze(), 0.0)
self.true_dist = true_dist
return self.criterion(x, Variable(true_dist, requires_grad=False))
# tensorflow version
# please refer to
https://github.com/tensorflow/cleverhans/blob/f70ca7e000dadd6ace5aeff15bba0e960e8c1384/cleverhans_tutorials/mnist_tutorial_tf.py#L126
We used the Adam optimizer (cite) with β 1 = 0.9 \beta_1=0.9 β1=0.9, β 2 = 0.98 \beta_2=0.98 β2=0.98 and ϵ = 1 0 − 9 \epsilon=10^{-9} ϵ=10−9. We varied the learning rate over the course of training, according to the formula:
lrate = d model − 0.5 ⋅ min ( ste p − n u m − 0.5 , ste p − n u m ⋅ warmu p − steps − 1.5 ) \text {lrate}=d_{\text { model }}^{-0.5} \cdot \min \left(\operatorname{ste}p_{-} n u m^{-0.5}, \text { ste} p_{-} n u m \cdot \operatorname{warmu} p_{-} \text {steps}^{-1.5}\right) lrate=d model −0.5⋅min(step−num−0.5, step−num⋅warmup−steps−1.5)This corresponds to increasing the learning rate linearly for the first w a r m u p _ s t e p s warmup\_steps warmup_steps training steps, and decreasing it thereafter proportionally to the inverse square root of the step number. We used w a r m u p _ s t e p s = 4000 warmup\_steps=4000 warmup_steps=4000.
我们使用了Adam优化器, β 1 = 0.9 \beta_1=0.9 β1=0.9, β 2 = 0.98 \beta_2=0.98 β2=0.98 and ϵ = 1 0 − 9 \epsilon=10^{-9} ϵ=10−9。根据公式,我们在整个训练过程中改变了学习率。根据公式,我们在整个训练过程中改变了学习率。这对应于在第一个warmup_steps 训练steps中线性的增加学习率,然后与步数的平方成比例地减少学习率。
令 ste p − n u m − 0.5 > ste p − n u m ⋅ warmup − steps − 1.5 \operatorname{ste}p_{-} n u m^{-0.5} >\text { ste} p_{-} n u m \cdot \operatorname{warmup}_{-} \text {steps}^{-1.5} step−num−0.5> step−num⋅warmup−steps−1.5,推出 warmu p − steps 3 / 2 > ste p − n u m 3 / 2 \operatorname{warmu} p_{-} \text {steps}^{3/2} > \operatorname{ste}p_{-}n u m^{3/2} warmup−steps3/2>step−num3/2 => warmup − steps > ste p − n u m \operatorname{warmup}_{-} \text {steps} > \operatorname{ste}p_{-} n u m warmup−steps>step−num,即
lrate = { d model − 0.5 ⋅ ste p − n u m ⋅ warmup − steps − 1.5 , if ste p − n u m < warmup − steps d model − 0.5 ⋅ ste p − n u m − 0.5 , if ste p − n u m < warmup − steps \text {lrate}= \left\{\begin{array}{l}{d_{\text { model }}^{-0.5} \cdot\text { ste} p_{-} n u m \cdot \operatorname{warmup}_{-} \text {steps}^{-1.5}, \text { if } \operatorname{ste}p_{-} n u m < \operatorname{warmup}_{-} \text {steps} } \\ {d_{\text { model }}^{-0.5} \cdot \operatorname{ste}p_{-} n u m^{-0.5} , \text { if } \operatorname{ste}p_{-} n u m < \operatorname{warmup}_{-} \text {steps}} \end{array}\right. lrate={d model −0.5⋅ step−num⋅warmup−steps−1.5, if step−num<warmup−stepsd model −0.5⋅step−num−0.5, if step−num<warmup−steps
# pytorch version
# refer to "Optimizer" part of https://github.com/harvardnlp/annotated-transformer/blob/master/The%20Annotated%20Transformer.ipynb
def rate(self, step = None):
"Implement `lrate` above"
if step is None:
step = self._step
return self.factor * \
(self.model_size ** (-0.5) *
min(step ** (-0.5), step * self.warmup ** (-1.5)))
# tensorflow version
# refer to https://github.com/Kyubyong/transformer/blob/6715edcb79022b1a92ba7b9edd1b3c6b53cebf28/modules.py#L303
def noam_scheme(init_lr, global_step, warmup_steps=4000.):
'''Noam scheme learning rate decay
init_lr: initial learning rate. scalar.
global_step: scalar.
warmup_steps: scalar. During warmup_steps, learning rate increases
until it reaches init_lr.
'''
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)
the Transformer (big) model trained for English-to-French used dropout rate Pdrop = 0.1, instead of 0.3.
We can begin by trying out a simple copy-task. Given a random set of input symbols from a small vocabulary, the goal is to generate back those same symbols.
这里包含
For more information, prelase refer to https://github.com/harvardnlp/annotated-transformer/blob/master/The Annotated Transformer.ipynb
介绍基于OpenNMT实现的transformer模型的其他特性: