直接上结论,我这里出现不收敛的原因是:一般网上直接找的别人的位置编码PositionalEncoding()只能对(batch_size, seq_len, …)类型的input起到位置编码的作用,batch_size得在第一维度,下面我会展示一下代码,人家就这么设计的,当然要是想改也是可以的,不过我比较懒直接就拿来主义了【doge】。 若input为非batch_first型会导致训练无法收敛。
具体原理与位置编码的设计有关,位置信息蕴含在纹理中,大部分讲解transformer模型的博客都没有强调位置编码为什么要设计为sin,cos型都是直接就拿来用了,我大概了解了一下这么设计的原因其实是为了使得例如“我爱你中国”这五个字不管放在一个句子(一个batch)中的哪个位置(哪个pos),句子中词之间的相对位置不会改变(我和国这两个字之间隔了三个字),这部分涉及一些数学知识,感兴趣请自行了解(位置编码详解),这里只简单介绍解决方法。
观察位置编码函数中的写法,看吧,最后是在第零维升了一个维度,seq_len和encoding_dim被挪到了第二维度和第三维度。
如果传入的x是(seq_len,batch_size,encoding_dim)的话,位置编码实际上就变成了对batch编码,这显然是不合理的。
下面是我碰到的问题以及解决方案,我由于torch版本只有1.8.1,nn.Transformer类中无法设置batch_first=True(在1.9.1版本就可以辣,但是GPU版本的torch我没找到1.9.1),故我没法设置batch_first,因此我的src和tgt都是(seq_len,batch_size)类型的。但我学的transformer的时候别人用的都是设置了batch_first=True因此我先是碰到:
问题1及解决方案:src_key_padding_mask的维度不匹配。查阅源码我发现,不管你是否设置了batch_first,key_padding_mask都要将batch_size放在第一维度(最前面),因此直接用一个transpose(0,1)实现转置即可。
问题2及解决方案:这下不报错了,我以为完事了,结果无论如何loss都不收敛,查了好久,也看了很多教程,发现我这里添加位置信息时用的input是经过编码后的src,维度是(seq_len,batch_size,embedding_dim),batch_size不在第一维度,这就导致位置编码未能起到添加位置信息的作用,因此导致训练不收敛。解决方法就是在进行位置编码前把batch提到第一维度,完事后在转回第二维度。
修改前的代码:(再次提醒一下,输入的都是(seq_len,batch_size)类型的)
class CopyTaskModel(nn.Module):
def __init__(self, d_model=128):
super(CopyTaskModel, self).__init__()
# 定义词向量,词典数为10。我们不预测两位数。 embedding层的输入格式必须为(seq_len,batch_size)
self.embedding = nn.Embedding(num_embeddings=10, embedding_dim=128)
# 定义Transformer。超参是我拍脑袋想的
self.transformer = nn.Transformer(d_model=128, num_encoder_layers=6, num_decoder_layers=6, dim_feedforward=512)
# 定义位置编码器
self.positional_encoding = PositionalEncoding(d_model, dropout=0)
# 定义最后的线性层,这里并没有用Softmax,因为没必要。
# 因为后面的CrossEntropyLoss中自带了
self.predictor = nn.Linear(d_model, 10)
def forward(self, src, tgt):
# 生成mask 这里输入的格式为(seq_len,batch_size),因此token的长度为tgt的第0个维度,用.size()和.shape是一样的,返回的都是<class 'torch.Size'>
tgt_mask = self.transformer.generate_square_subsequent_mask(tgt.shape[0]) # 这是一个静态函数,形参self随便填,填None也行,不影响结果
src_key_padding_mask = CopyTaskModel.get_key_padding_mask(src) # 因为实现的是静态类,所以不用self调用
tgt_key_padding_mask = CopyTaskModel.get_key_padding_mask(tgt)
# 对src和tgt进行编码
src = self.embedding(src)
tgt = self.embedding(tgt)
# 给src和tgt的token增加位置信息
src = self.positional_encoding(src)
tgt = self.positional_encoding(tgt)
# 将准备好的数据送给transformer
out = self.transformer(src, tgt,
tgt_mask=tgt_mask,
src_key_padding_mask=src_key_padding_mask,
tgt_key_padding_mask=tgt_key_padding_mask)
return out
@staticmethod
def get_key_padding_mask(tokens):
"""
用于key_padding_mask
"""
key_padding_mask = torch.zeros(tokens.size(), dtype=torch.float)
key_padding_mask[tokens == 2] = (-math.inf) # inf表示一个无穷大的数
# 由于transformer的输入参数中的src_key_padding_mask的格式为(batch_size,seq_len),因此需要转置一下
key_padding_mask = key_padding_mask.transpose(0, 1)
return key_padding_mask
修改后的代码:
class CopyTaskModel(nn.Module):
def __init__(self, d_model=128):
super(CopyTaskModel, self).__init__()
# 定义词向量,词典数为10。我们不预测两位数。 embedding层的输入格式必须为(seq_len,batch_size)
self.embedding = nn.Embedding(num_embeddings=10, embedding_dim=128)
# 定义Transformer。超参是我拍脑袋想的
self.transformer = nn.Transformer(d_model=128, num_encoder_layers=2, num_decoder_layers=2, dim_feedforward=512)
# 定义位置编码器
self.positional_encoding = PositionalEncoding(d_model, dropout=0)
# 定义最后的线性层,这里并没有用Softmax,因为没必要。
# 因为后面的CrossEntropyLoss中自带了
self.predictor = nn.Linear(d_model, 10)
def forward(self, src, tgt):
# 生成mask 这里输入的格式为(seq_len,batch_size),因此token的长度为tgt的第0个维度,用.size()和.shape是一样的,返回的都是<class 'torch.Size'>
tgt_mask = self.transformer.generate_square_subsequent_mask(tgt.shape[0]) # 这是一个静态函数,形参self随便填,填None也行,不影响结果
# key_padding_mask也要求batch_size在第一维度
# 转置是为了将 batch_size 挪到第一维度
src_key_padding_mask = CopyTaskModel.get_key_padding_mask(src).transpose(0, 1) # 因为实现的是静态类,所以不用self调用
tgt_key_padding_mask = CopyTaskModel.get_key_padding_mask(tgt).transpose(0, 1)
# 对src和tgt进行编码
srce = self.embedding(src)
tgte = self.embedding(tgt)
# 给src和tgt的token增加位置信息,注意:位置编码的input必须为batch_first类型,否则添加的位置信息会无效
srcp = self.positional_encoding(srce.transpose(0, 1)) # 转为batch_first进行添加位置编码
tgtp = self.positional_encoding(tgte.transpose(0, 1))
src = srcp.transpose(0, 1)
tgt = tgtp.transpose(0, 1) # 非batch_first
# 将准备好的数据送给transformer
out = self.transformer(src, tgt,
tgt_mask=tgt_mask,
src_key_padding_mask=src_key_padding_mask,
tgt_key_padding_mask=tgt_key_padding_mask)
return out # 非batch_first
@staticmethod
def get_key_padding_mask(tokens):
"""
用于key_padding_mask
"""
key_padding_mask = torch.zeros(tokens.size(), dtype=torch.float)
key_padding_mask[tokens == 2] = (-math.inf) # inf表示一个无穷大的数
# 由于transformer的输入参数中的src_key_padding_mask的格式为(batch_size,seq_len),因此需要转置一下
return key_padding_mask