赞
踩
最近动手玩了一下Transformer,找到了一个很适合练手的小例子,基于https://github.com/cxl-ustb/AISTransforemr的代码做了一些修改(感谢原作者),改进后的代码地址:GitHub - BITcsy/AISTransformer: 利用transformer进行船舶轨迹预测。
该代码功能是处理船只的轨迹、状态预测(经度,维度,速度,朝向)。每条数据涵盖11个点,输入是完整的11个点(Encoder输入前10个点,Decoder输入后10个点,模型整体输出后10个点),如下图,训练数据140条,测试数据160条。整个任务本身并没有什么意义(已知轨迹再输出部分轨迹),并没有做什么预测任务。不过整体例子简单明了,调试难度小,比较适合做入门练手的例子。
原作者实现了比较完整的Transformer结构,当然对于轨迹预测类任务很多结构是冗余的,因为Decoder那部分输入在语音识别上可能有用,但是在轨迹预测上应该没有必要,不过可以学习一下。
这网上很多,这里引用几个个人觉得比较好的:
Transformer: NLP里的变形金刚 --- 详述 - 知乎
多图详解attention和mask。从循环神经网络、transformer到GPT2,我悟了 - 知乎
费劲把这个代码里的整体数据流和主要的模型结构整理了下,可以看出来和最典型的Transformer的结构是一样的。其中标颜色的几个模块单独再打开来看吧,左下角的几个变量和word embedding及positional encoding相关,也单独来看。
word embedding参考资料:词嵌入向量(Word Embedding)的原理和生成方法 - 程序员大本营
nn.embedding: PyTorch中的nn.Embedding - 知乎
positional encoding: positional encoding位置编码详解:绝对位置与相对位置编码对比_夕小瑶的博客-CSDN博客
Positional Encoding的原理和计算 - 知乎 有比较形象的图形解释。
总之,word embedding想要对序列化的输入(比如一句话,或者这里的一条轨迹)进行降维表达,把input映射成embedding。疑点1:nn.embedding这个模块貌似比较适合NLP任务这种比较稀疏的embedding表达,做轨迹类任务不一定合适。那代码中的n_src_vocab, n_trg_vocab应该就是对应于字典的长度,d_word_src, d_model应该就是隐层要用多少维去表达这些词。疑点2:按道理d_word_src应该小于n_src_vocab才对,但是代码里分别设置了512和500。
positional encoding的作用是为了解决attention这种并行计算的结构丢失了RNN、LSTM这种具有先后顺序的网络特性,因为NLP或者轨迹预测类任务还是需要看特征的时序上的改变的,因此用positional encoding来区分不同的位置,结合其公式应该比较容易理解。代码中position设置为200,按道理这个数设置为大于最大序列长度的数就可以了(本代码最大序列长度就是10)。
word embedding和positional encoding这块的整体计算原理大概如下图,在这个代码里,d_word和d_model其实是一个意思,但是如果是其他场景,d_model的含义应该更广,毕竟是dimension of the model。
- class MultiHeadAttention(nn.Module):
- ''' Multi-Head Attention module '''
-
- def __init__(self, n_head, d_model, d_k, d_v, dropout=0.1):
- super().__init__()
-
- self.n_head = n_head
- self.d_k = d_k
- self.d_v = d_v
-
- self.w_qs = nn.Linear(d_model, n_head * d_k, bias=False)
- self.w_ks = nn.Linear(d_model, n_head * d_k, bias=False)
- self.w_vs = nn.Linear(d_model, n_head * d_v, bias=False)
- self.fc = nn.Linear(n_head * d_v, d_model, bias=False)
-
- self.attention = ScaledDotProductAttention(temperature=d_k ** 0.5)
-
- self.dropout = nn.Dropout(dropout)
- self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
-
-
- def forward(self, q, k, v, mask=None):
-
- d_k, d_v, n_head = self.d_k, self.d_v, self.n_head
- sz_b, len_q, len_k, len_v = q.size(0), q.size(1), k.size(1), v.size(1)
-
- residual = q
- # Pass through the pre-attention projection: b x lq x (n*dv)
- # Separate different heads: b x lq x n x dv
- q = self.w_qs(q).view(sz_b, len_q, n_head, d_k)
- k = self.w_ks(k).view(sz_b, len_k, n_head, d_k)
- v = self.w_vs(v).view(sz_b, len_v, n_head, d_v)
-
- # Transpose for attention dot product: b x n x lq x dv
- q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
-
- if mask is not None:
- mask = mask.unsqueeze(1) # For head axis broadcasting.
-
- q, attn = self.attention(q, k, v, mask=mask)
-
- # Transpose to move the head dimension back: b x lq x n x dv
- # Combine the last two dimensions to concatenate all the heads together: b x lq x (n*dv)
- q = q.transpose(1, 2).contiguous().view(sz_b, len_q, -1)
- q = self.dropout(self.fc(q))
- q += residual
-
- q = self.layer_norm(q)
-
- return q, attn
根据上面的代码,画了一下attention详细的计算过程,如下图:
几个需要注意的地方
(1)attention的mask是可选的,在时序任务里是需要的,因为时序是有因果性的。
(2)Q*K才是attention,它的维度是Batch*heads*lens*lens,也就意味着是有明确物理意义的,只和最初的序列长度有关系,可以可视化出来做分析用。
(3)ScaledDotProduct算完后,最后一维是由dv决定的,想回到q的维度,还需要做linear的操作。
(4)contiguous & view:代码里的操作实际就是把dv数据按照heads concate到了一起,具体为何需要使用contiguous的原因PyTorch中的contiguous - 知乎,其实可以用reshape来代替 PyTorch:view() 与 reshape() 区别详解_reshape和view_地球被支点撬走啦的博客-CSDN博客。
(5)residual:类似于Resnet的设计。
(1)DataLoader:原代码直接用的pickle来的raw data,为了更好的进行训练控制,我改写成了Dataloader的形式,实测同参数训练要比原来慢1/10到1/5左右。
(2)tqdm:给训练加了个进度条,方便监测比较细节的训练进度。
(3)tensorboard:方便调试(tensorboardX和tensorboard都要安装,tensorboard --logdir XXX)。
(4)模型断点训练:因为自己电脑是10年前的机器,性能很差,所以增加了一个断点续训的功能,避免每次都要从头训练。这里是用checkpoint形式写的保存细节信息。
(5)pt2onxx.py里面用netron可以查看网络结构,略改了一下,可以实现,不过感觉netron显示的太细节,比较难看出最主要的网络设计。
(1)learning rate & 模型大小:原代码直接拿过来可以训,但是收敛不了,loss一直是好几千。分析出来2个主要问题:
i. 原learning rate做了特殊设计,但是用tessorboard看,原learning rate是随训练次数上升的,很令人困惑,所以改写逐步下降的形式。
ii. 改了learning rate还是收敛不了的,分析到训练数据只有140条,但是又搞了个这么大的模型,可能根本喂不饱网络,所以直接把attention部分的大小改小了,multiheadattention的head数8->2,attention layer数6->1,这样改完网络收敛了,至少在训练集上可以过拟合,loss到1左右。
(2)模型保存
关于模型保存,有一类需求,就是本地需要load一个已有的模型pth文件,然后接着训练调试。有一个很奇怪的地方,当我load一个已存的pth文件后,模型做forward的时候,凭什么知道找我源码里同名model的forward函数?我尝试改了一下模型的类名(把Transformer改成了Transformers),果然test的时候就报错了,所以在load模型后,会自动找同名模型的实现:
- return super().find_class(mod_name, name)
- AttributeError: Can't get attribute 'Transformer' on <module 'transformer.Models' from '/home/XXX/AISTransformer/AISTransformer/transformer/Models.py'>
(3)训练集表现 & 关于model.eval的问题
在测试的过程中发现了一个很奇怪的现象,就是如果我把训好的模型save后,再重新load上来,用训练集做测试,loss差距很大(训练的loss是1.x,但是重新load的模型在同数据集下loss是1000+),做了几个尝试
(4)attention & 测试集表现
在训练集上loss约为1,在测试集上测试loss是2k-3k,泛化能力很差,当然数据太少是一个比较显著的问题。为了在小数据集上提升模型泛化能力,可以通过attention来加入一些先验信息引导模型训练。可能的尝试:
(5)Shuffle的问题:因为发现测试集上表现不好,考虑到原代码训练时并没有shuffle,可能会让网络学到一些关于顺序的东西(https://zhuanlan.zhihu.com/p/57108650),所以在引入Dataloader,加了下shuffle,但是训出来感觉测试集上差别不大。
(6)结构对比:做这个简单的任务用这么复杂的模型可能往往是难以达到较好的效果的,还有用了例如word embedding这样的结构,看上去比较奇怪。用MLP简单实现了下这个网络。
输入 | epochs | 训练时长 | train loss | test loss | |
transformer | 1. traj[:,:-1,:] 2. traj[:,1:,:] | 8000 | >1h@cpu(11min@GTX1080) | 1.0+ | 2k+ |
MLP | traj[:,-1:,:] | 30000 | 2min | 550+ | 550+ |
MLP | traj[:,:,-1:] | 30000 | 2min | 20+ | 20+ |
MLP | 1. traj[:,:-1,:] 2. traj[:,1:,:] | 30000 | 2min | 0.01 | 0.03 |
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。