Attention机制--concat方式

本文深入探讨了使用 PyTorch 实现的 Seq2Seq 模型,并介绍了如何结合 Attention 机制以提高翻译等任务的性能。文章首先介绍了 Seq2Seq 基础,然后详细解释了 Attention 机制的工作原理,特别是 Concat 式 Attention 的计算过程。接着,给出了 Encoder 和 Decoder 的实现代码,并展示了 Attention 层的输出。最后,演示了整个解码过程,强调了 Attention 在解码中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文主要参考github上一个开源的seq2seq教程,在此基础上稍作修改

https://github.com/bentrevett/pytorch-seq2seq/blob/master/3%20-%20Neural%20Machine%20Translation%20by%20Jointly%20Learning%20to%20Align%20and%20Translate.ipynb

1.Seq2Seq模型

Seq2Seq模型
在我上一篇文章有这个代码,原理就是一开始利用编码器的hidden,解码器去生成相应的字符。

2.Attention机制

虽然Seq2Seq模型可以通过encoder生成的上文信息来生成相应的字符或者词语,但是却不能理解encoder中输入序列中句子内部的词语和词语,字符和字符之间潜在的关系。例如我们做翻译的时候,要将英文中的动词转化为中文,我们应该更关注英文中的动词词汇。这就是attention机制,它可以告诉我们每次翻译时更应该关注英文中的哪一部分。
在这里插入图片描述
计算权重的方式有很多,这里介绍最基础的一种:concat
原理是通过encoder每层最后一个状态和encoder中每个输入进行相应计算,最后得到一个权重向量。如图所示,将encoder的输入和s0叠加之后,经过两个线性变换得到权重向量。得到的权重每次decoder做解码的时候都要更新一次。因此attention的计算量要比传统的Seq2Seq要大的多。
在这里插入图片描述

在这里插入图片描述

3.代码

首先是Encoder,和传统的Seq2Seq基本没有区别,只是多了一个要输出最后一层的状态。

# 此例子默认编码器解码器的hidden_size相同
class Encoder(nn.Module):
    def __init__(self, input_dim, emb_dim, hidden_size, n_layers, dropout=0.5, bidirectional=True):
        super(Encoder, self).__init__()

        self.hidden_size = hidden_size
        self.n_layers = n_layers

        self.embedding = nn.Embedding(input_dim, emb_dim)  # input_dim数量等于源语言的字符数
        self.gru = nn.GRU(emb_dim, hidden_size, n_layers, dropout=dropout, bidirectional=bidirectional)
        self.fc = nn.Linear(hidden_size*2, hidden_size)

    def forward(self, input_seqs):
        # input_seqs  [seq_len, batch]

        embedded = self.embedding(input_seqs)
        # embedded  [seq_len, batch, embed_dim]
        
        outputs, hidden = self.gru(embedded)
        # outputs  [seq_len, batch, hidden_size * 2]
        # hidden is stacked [forward_1, backward_1, forward_2, backward_2, ...]
        # outputs are always from the last layer
        h_hat = torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim=1)
        # h_hat [batch, hidden_size*2]
        # h_hat 是最后一层的最后一个状态,用于计算注意力权重
        # 通过线性层将h_hat转为编码器每次最后一层所输出的形式
        h_hat = torch.tanh(self.fc(h_hat))
        # h_hat [bacth, hidden]
        return outputs, hidden, h_hat

任意取一组数据输入到Encoder

INPUT_DIM = 8
OUTPUT_DIM = 6
HIDDEN_SIZE = 10
N_LAYERS = 2
EMB_SIZE = 12
BATCH_SIZE = 2
SEQ_LEN = 5
encoder = Encoder(INPUT_DIM, EMB_SIZE, HIDDEN_SIZE, N_LAYERS)
x = torch.randint(1, 7, (SEQ_LEN, BATCH_SIZE))
en_out, hidden, h_hat = encoder(x)

重点来了,Attention部分的代码如下:
这里有很多的拼接操作

class Attention(nn.Module):
    def __init__(self, hidden_size):
        super().__init__()
        self.attn = nn.Linear(3*hidden_size, hidden_size)
        self.v = nn.Linear(hidden_size, 1, bias = False)
        
    def forward(self, h_hat, encoder_outputs):
        
        # h_hat [batch, hidden_size]
        # encoder_outputs [seq_len, bacth, hidden_size*2]
        
        batch_size = encoder_outputs.shape[1]
        src_len = encoder_outputs.shape[0]  # 序列长度(时间步)
        
        # 编码器每个时间步最后的输出都要和h_hat做拼接,因此需要将h_hat复制序列长度份
        h_hat = h_hat.unsqueeze(1).repeat(1, src_len, 1)
        
        encoder_outputs = encoder_outputs.permute(1, 0, 2)
        
        #h_hat = [batch, seq_len, hidden_size]
        #encoder_outputs = [batch, seq_len, hidden_size * 2]
        
        energy = torch.tanh(self.attn(torch.cat((h_hat, encoder_outputs), dim = 2))) 
        
        #energy  [batch, seq_len, hidden_size]

        attention = self.v(energy).squeeze(2)
        
        # attention [batch, seq_len]
        # 返回每个batch每个时间步的权重
        return F.softmax(attention, dim=1)

我们这里可以测试一下Attention的输出:

atten = Attention(HIDDEN_SIZE)
print(atten(h_hat, en_out))
"""
tensor([[0.1896, 0.1891, 0.2035, 0.2095, 0.2083],
        [0.1951, 0.2025, 0.2011, 0.1986, 0.2026]], grad_fn=<SoftmaxBackward>)
"""

最后是Decoder

class Decoder(nn.Module):
    def __init__(self, output_dim, emb_dim, hidden_size, n_layers, attention, dropout=0.5, bidirectional=True):
        super().__init__()

        self.output_dim = output_dim
        self.attention = attention
        
        self.embedding = nn.Embedding(output_dim, emb_dim)
        
        self.rnn = nn.GRU(2*hidden_size + emb_dim, hidden_size, n_layers, bidirectional=True)
        
        self.fc_out = nn.Linear(4*hidden_size+emb_dim, output_dim)
        
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, input, h_hat, hidden, encoder_outputs):
             
        # input  [batch]
        # h_hat  [batch, hidden_size]
        # encoder_outputs  [seq_len, batch, hidden_size * 2]
        # hidden [direction*n_layers, bacth, hidden_size]
        
        input = input.unsqueeze(0)
        
        # input  [1, batch]
        
        embedded = self.dropout(self.embedding(input))
        
        #embedded  [1, batch, emb_dim]
        
        a = self.attention(h_hat, encoder_outputs)
        
        # a  [batch, seq_len]
        
        a = a.unsqueeze(1)
        
        # a  [batch, 1, seq_len]
        
        encoder_outputs = encoder_outputs.permute(1, 0, 2)
        
        # encoder_outputs  [batch, seq_len, hidden_size * 2]
        
        weighted = torch.bmm(a, encoder_outputs)
        
        # weighted  [batch, 1, hidden_size * 2]
        
        weighted = weighted.permute(1, 0, 2)
        
        # weighted  [1, batch, hidden_size * 2]
        
        rnn_input = torch.cat((embedded, weighted), dim = 2)
        #rnn_input  [1, batch, 2*hidden_size+emb_dim]
        
        output, hidden = self.rnn(rnn_input, hidden)
        # output  [1, batch, hidden_size*2]
        # hidden  [layers * directions, batch, hidden_size]
        
        embedded = embedded.squeeze(0)
        output = output.squeeze(0)
        weighted = weighted.squeeze(0)
        
        prediction = self.fc_out(torch.cat((output, weighted, embedded), dim = 1))
        
        # prediction = [batch, output dim]
        # hidden [n_layers*directions, batch, hidden_size]
        return prediction, hidden

最后进行测试:

decoder = Decoder(OUTPUT_DIM, EMB_SIZE, HIDDEN_SIZE, N_LAYERS, atten)
decoder_input = torch.tensor([1, 2])
pre, hidden = decoder(decoder_input, h_hat, hidden, en_out)
print(pre.shape)
print(hidden.shape)

"""
torch.Size([2, 6])
torch.Size([4, 2, 10])
"""
### Attention U-Net 中门控注意力机制的实现原理 Attention U-Net 是一种通过引入注意门 (Attention Gates, AGs) 来增强传统 U-Net 性能的方法。其核心思想在于利用门控机制动态地调整不同特征通道的重要性,从而突出目标区域并抑制背景噪声或其他无关信息[^3]。 #### 1. 注意门的设计目的 注意门的主要目的是过滤掉那些对当前任务无意义的空间位置上的特征激活值。具体而言,它能够帮助网络专注于特定的目标区域(如医学图像中的器官),同时忽略其他干扰因素。这种设计特别适合于处理小型或形状复杂的对象分割问题,例如胰腺等小尺寸器官的检测。 #### 2. 门控机制的具体实现 门控注意力机制的核心组件由三个部分组成:输入特征 \(X\)、上下文特征 \(G\) 和最终输出的加权特征 \(O\)。以下是其实现过程: - **输入特征 (\(X\))**: 这是从编码器路径中提取到的低级空间特征图。 - **上下文特征 (\(G\))**: 它们来自解码器路径更高层次的信息,通常携带全局语义理解能力。 两者经过独立卷积操作后相乘再过非线性变换得到权重系数 \(\alpha_{ij}\),最后将此权重作用回原始输入特征上形成新的响应映射作为输出给后续层使用。 公式表达如下: \[ f(X,G)=σ(W_x∗X+W_g∗G+b),α=f(X,G)\odot X,O=γ∗α+(1−γ)∗X \] 其中, - \(W_x,W_g,b,\gamma\) 都是可学习参数; - 符号 "\(*\)" 表示标准二维卷积运算符,“\(\sigma\)" 指代sigmoid函数用于生成概率分布形式的掩模矩阵;"\(\odot\)" 则代表逐元素乘法操作。 上述计算流程确保只有当某个像素既存在于局部视场又符合整体场景描述时才会被保留下来贡献到最后决策过程中去。 #### 3. 训练策略优化 为了更好地发挥AG的效果,在实际应用中有两点值得注意: - 使用预训练好的U-Net权重来初始化整个架构有助于稳定初始阶段的学习行为。 - 对于某些复杂数据集可能还需要采用分步式或者渐进式的教学计划逐步引导模型掌握更深层次的知识点。 ```python import torch.nn as nn class AttentionGate(nn.Module): def __init__(self, input_channels, gating_channels, inter_channels=None): super().__init__() self.input_conv = nn.Conv2d(input_channels, inter_channels or input_channels//2, kernel_size=1) self.gating_conv = nn.Conv2d(gating_channels, inter_channels or input_channels//2, kernel_size=1) self.final_conv = nn.Conv2d(inter_channels or input_channels//2, input_channels, kernel_size=1) def forward(self, x, g): theta_x = self.input_conv(x) phi_g = self.gating_conv(g) concat_features = F.relu(theta_x + phi_g) psi = self.final_conv(concat_features) sigmoid_attention = torch.sigmoid(psi) return x * sigmoid_attention ``` 以上代码片段展示了一个简单的PyTorch版本的注意门类定义方法。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值