Transformer From Scratch
待完成
- 增加decoder inference模块
- 前言
架构分四个大块
- encoder
- encoder-layer
- decoder
- decoder-layer
细节三种mask
- encoder-mask
- decoder-mask
- cross-mask
Embedding
句子表示为 [token1, token2, …tokens]
句子1 = [token_1, token_2, …token_x]
句子2 = [token_1, token_2, …token_y] x 不一定等于 y
token 构造
1 | import torch.nn.functional as F |
词向量空间 映射
1 | import torch.nn as nn |
Key_mask
由于在encoder_layer pad的位置经过softmax 也会分得或多或少注意力分数,这些pad不是我们希望模型从数据中学到的,所以这里我们引入Key_mask 帮助encoder更好的关注在需要关注的位置上。 也是特别重要的一个细节。
- 前置
1 | max_len = 6 |
- pad
1 | token_pad_ids = [F.pad(x, (0, max_len-x.shape[0])).unsqueeze(0) for x in token_ids] |
- 取得embedding
1 | src_embedding = nn.Embedding(vacab_max+1, embed_dim) |
- 查看True False ,这里我们去src_ids 的第四个,因为零比较多,(embedding的零行是pad的词向量)
1 | pad = src_embedding.weight[0] |
- 由于encoder的输入都是src 所以Q*K.T 的维度为(bs, src_len, src_len), mask就直接可以写了
1 | a = token_pad_ids[3].unsqueeze(-1) |
- 一批量查看mask
1 | mask = torch.matmul(token_pad_ids.unsqueeze(-1),token_pad_ids.unsqueeze(1)) ==0 |
- 上刺刀
1 | scores = torch.randn(6, 6, 6) |
同时我们可以看见 全都是pad自身的注意力分数是平均的,也就是跟乱猜一样,没有意义。
Position Embedding
按公式写就行
1 | pe = torch.zeros(max_len, d_model) |
emb+pe
1 | import torch.nn as nn |
Multi-head Attention
Query_mask & Scaled_Attention
1 | def ScaledAttention(query, key, value, d_k, mask=None, dropout=None): |
- 第一次matmul: 维度变化为 Q@K.T —> (batch_size * n_head, tgt_len, src_len)
- 中间的
scores.masked_fill(mask, -1e9)
根据不同的mask做掩码填充
- 中间的
- 第二次matmul: 维度变化为 scores@V —> (batch_size, tgt_len, embed_dim) 且有注意力分数分配权重
- 这样就是decoder中的目标序列长度
1 | import torch |
关于contiguous
clones之后的linear列表有4个layer
zip函数 对不通长度的对象直接以最小长度进行截断,所以return那里可以用linear列表的最后一个返回输出
```python
a = list(range(6)) # 0 - 5 六个数
b = list(‘asdfg’) # 5个字母
list(zip(a,b))‘’’
[(0, ‘a’), (1, ‘s’), (2, ‘d’), (3, ‘f’), (4, ‘g’)]
‘’’1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
+ contiguous()可以开辟新的内存存储数据,is_contiguous()可以判断数据的底层内存是否连续存取,这里配合view使用
+ ```python
t = torch.arange(12).reshape(3,4)
'''
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])'''
t.stride()
# (4, 1)
t2 = t.transpose(0,1)
'''
tensor([[ 0, 4, 8],
[ 1, 5, 9],
[ 2, 6, 10],
[ 3, 7, 11]])'''
t2.stride()
# (1, 4)
t.data_ptr() == t2.data_ptr() # 底层数据是同一个一维数组
# True
t.is_contiguous(),t2.is_contiguous() # t连续,t2不连续
# (True, False)stride(d1,d2)表示这个维度上的单位元素之间的内存距离。 如原本0-11是连续的,他们在【0】维上的距离是4。
view(b, -1, self.h * self.d_k) 要求其对象在内存上是连续的
- 即上面的数组
[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
如此在内存上排列 - 即使view不报错,直接在t2上做view返回的也不会是
[ 0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11]
- 即上面的数组
ADD & Norm
- 展示一下实现方式,具体只调用Pytorch的接口
layer_norm 做层级别的归一化,如果说Batch_norm是在 [B, C, H, W]
的channel上得到RGB的均值方差,做归一化;
layer_norm就是在 [B, C, H, W]
的batch上得到归一化,比如有3个批次,就分别得到【0】【1】【2】的均值方差。
1 | class LayerNorm(nn.Module): |
Encoder Layer
1 | import torch |
Decoder
1 | import torch |
mask
目的是想模仿真实场景中,每次翻译下个词只能在前面产生答案的基础上,所以不会一次产生所有loss
使用torch.functional.cross_entropy
的ignore_index
参数将想要mask的位置填为-100即可将loss mask掉
使用ignore_index参数 要配合 reduction = ‘none’ 参数
返回所有损失,而不是平均或者加和后的损失
1 | import torch |
汽车人变形!
1 | import torch |
输出
1 | def evaluate(encoder, decoder, sentence, max_length=MAX_LENGTH): |
- 将encoder解出来
- decoder传入
<BOS>
- while 判断是否输出
<EOS>
- encoder (64, 24,784) 准备好 KV
- decoder (64, 1, 784)传入的是 SOS
- Q@K.T 得到 (64,1,24)的scores
- scores@V 得到 (64,1,784)
- 送出去(64,1,56233)做softmax取得token1
- 得到目前时刻的解码
[<BOS>、token1]
送入下一时刻
- 得到目前时刻的解码
- 下一次就是(64,2,56233)
- 得到目前时刻的解码
[<BOS>、token1、token2]
送入下一时刻
- 得到目前时刻的解码
- 逐渐解码到
[<BOS>、token1、token2....<EOS>]
条件成立结束输出
返回的attention_scores
可以做相关度矩阵分析
os.environ[“CUDA_VISIBLE_DEVICES”] = “1” 坑的一逼,指定你的gpu的代号,device = torch.device(“cuda”) if torch.cuda.is_available() else torch.device(“cpu”) 就只能用1号,就算你只有一块也是1号,要在device(“cuda:1”)指定坑的一逼