import math import torch import torch.nn as nn import torch.nn.functional as F from torch import Tensor import numpy as np from typing import Optional, Tuple class ScaledDotProductAttention(nn.Module): """ Scaled Dot-Product Attention proposed in "Attention Is All You Need" Compute the dot products of the query with all keys, divide each by sqrt(dim), and apply a softmax function to obtain the weights on the values Args: dim, mask dim (int): dimention of attention mask (torch.Tensor): tensor containing indices to be masked Inputs: query, key, value, mask - **query** (batch, q_len, d_model): tensor containing projection vector for decoder. - **key** (batch, k_len, d_model): tensor containing projection vector for encoder. - **value** (batch, v_len, d_model): tensor containing features of the encoded input sequence. - **mask** (-): tensor containing indices to be masked Returns: context, attn - **context**: tensor containing the context vector from attention mechanism. - **attn**: tensor containing the attention (alignment) from the encoder outputs. """ def __init__(self, dim: int): super(ScaledDotProductAttention, self).__init__() self.sqrt_dim = np.sqrt(dim) def forward(self, query: Tensor, key: Tensor, value: Tensor, mask: Optional[Tensor] = None) -> Tuple[ Tensor, Tensor]: score = torch.bmm(query, key.transpose(1, 2)) / self.sqrt_dim if mask is not None: score.masked_fill_(mask.view(score.size()), -float('Inf')) attn = F.softmax(score, -1) context = torch.bmm(attn, value) return context, attn class DotProductAttention(nn.Module): """ Compute the dot products of the query with all values and apply a softmax function to obtain the weights on the values """ def __init__(self, hidden_dim): super(DotProductAttention, self).__init__() self.normalize = nn.LayerNorm(hidden_dim) def forward(self, query: Tensor, value: Tensor) -> Tuple[Tensor, Tensor]: batch_size, hidden_dim, input_size = query.size(0), query.size(2), value.size(1) score = torch.bmm(query, value.transpose(1, 2)) attn = F.softmax(score.view(-1, input_size), dim=1).view(batch_size, -1, input_size) context = torch.bmm(attn, value) return context, attn class AdditiveAttention(nn.Module): """ Applies a additive attention (bahdanau) mechanism on the output features from the decoder. Additive attention proposed in "Neural Machine Translation by Jointly Learning to Align and Translate" paper. Args: hidden_dim (int): dimesion of hidden state vector Inputs: query, value - **query** (batch_size, q_len, hidden_dim): tensor containing the output features from the decoder. - **value** (batch_size, v_len, hidden_dim): tensor containing features of the encoded input sequence. Returns: context, attn - **context**: tensor containing the context vector from attention mechanism. - **attn**: tensor containing the alignment from the encoder outputs. Reference: - **Neural Machine Translation by Jointly Learning to Align and Translate**: https://arxiv.org/abs/1409.0473 """ def __init__(self, hidden_dim: int) -> None: super(AdditiveAttention, self).__init__() self.query_proj = nn.Linear(hidden_dim, hidden_dim, bias=False) self.key_proj = nn.Linear(hidden_dim, hidden_dim, bias=False) self.bias = nn.Parameter(torch.rand(hidden_dim).uniform_(-0.1, 0.1)) self.score_proj = nn.Linear(hidden_dim, 1) def forward(self, query: Tensor, key: Tensor, value: Tensor) -> Tuple[Tensor, Tensor]: score = self.score_proj(torch.tanh(self.key_proj(key) + self.query_proj(query) + self.bias)).squeeze(-1) attn = F.softmax(score, dim=-1) context = torch.bmm(attn.unsqueeze(1), value) return context, attn class LocationAwareAttention(nn.Module): """ Applies a location-aware attention mechanism on the output features from the decoder. Location-aware attention proposed in "Attention-Based Models for Speech Recognition" paper. The location-aware attention mechanism is performing well in speech recognition tasks. We refer to implementation of ClovaCall Attention style. Args: hidden_dim (int): dimesion of hidden state vector smoothing (bool): flag indication whether to use smoothing or not. Inputs: query, value, last_attn, smoothing - **query** (batch, q_len, hidden_dim): tensor containing the output features from the decoder. - **value** (batch, v_len, hidden_dim): tensor containing features of the encoded input sequence. - **last_attn** (batch_size * num_heads, v_len): tensor containing previous timestep`s attention (alignment) Returns: output, attn - **output** (batch, output_len, dimensions): tensor containing the feature from encoder outputs - **attn** (batch * num_heads, v_len): tensor containing the attention (alignment) from the encoder outputs. Reference: - **Attention-Based Models for Speech Recognition**: https://arxiv.org/abs/1506.07503 - **ClovaCall**: https://github.com/clovaai/ClovaCall/blob/master/las.pytorch/models/attention.py """ def __init__(self, hidden_dim: int, smoothing: bool = True) -> None: super(LocationAwareAttention, self).__init__() self.hidden_dim = hidden_dim self.conv1d = nn.Conv1d(in_channels=1, out_channels=hidden_dim, kernel_size=3, padding=1) self.query_proj = nn.Linear(hidden_dim, hidden_dim, bias=False) self.value_proj = nn.Linear(hidden_dim, hidden_dim, bias=False) self.score_proj = nn.Linear(hidden_dim, 1, bias=True) self.bias = nn.Parameter(torch.rand(hidden_dim).uniform_(-0.1, 0.1)) self.smoothing = smoothing def forward(self, query: Tensor, value: Tensor, last_attn: Tensor) -> Tuple[Tensor, Tensor]: batch_size, hidden_dim, seq_len = query.size(0), query.size(2), value.size(1) # Initialize previous attention (alignment) to zeros if last_attn is None: last_attn = value.new_zeros(batch_size, seq_len) conv_attn = torch.transpose(self.conv1d(last_attn.unsqueeze(1)), 1, 2) score = self.score_proj(torch.tanh( self.query_proj(query.reshape(-1, hidden_dim)).view(batch_size, -1, hidden_dim) + self.value_proj(value.reshape(-1, hidden_dim)).view(batch_size, -1, hidden_dim) + conv_attn + self.bias )).squeeze(dim=-1) if self.smoothing: score = torch.sigmoid(score) attn = torch.div(score, score.sum(dim=-1).unsqueeze(dim=-1)) else: attn = F.softmax(score, dim=-1) context = torch.bmm(attn.unsqueeze(dim=1), value).squeeze(dim=1) # Bx1xT X BxTxD => Bx1xD => BxD return context, attn class MultiHeadLocationAwareAttention(nn.Module): """ Applies a multi-headed location-aware attention mechanism on the output features from the decoder. Location-aware attention proposed in "Attention-Based Models for Speech Recognition" paper. The location-aware attention mechanism is performing well in speech recognition tasks. In the above paper applied a signle head, but we applied multi head concept. Args: hidden_dim (int): The number of expected features in the output num_heads (int): The number of heads. (default: ) conv_out_channel (int): The number of out channel in convolution Inputs: query, value, prev_attn - **query** (batch, q_len, hidden_dim): tensor containing the output features from the decoder. - **value** (batch, v_len, hidden_dim): tensor containing features of the encoded input sequence. - **prev_attn** (batch_size * num_heads, v_len): tensor containing previous timestep`s attention (alignment) Returns: output, attn - **output** (batch, output_len, dimensions): tensor containing the feature from encoder outputs - **attn** (batch * num_heads, v_len): tensor containing the attention (alignment) from the encoder outputs. Reference: - **Attention Is All You Need**: https://arxiv.org/abs/1706.03762 - **Attention-Based Models for Speech Recognition**: https://arxiv.org/abs/1506.07503 """ def __init__(self, hidden_dim: int, num_heads: int = 8, conv_out_channel: int = 10) -> None: super(MultiHeadLocationAwareAttention, self).__init__() self.hidden_dim = hidden_dim self.num_heads = num_heads self.dim = int(hidden_dim / num_heads) self.conv1d = nn.Conv1d(num_heads, conv_out_channel, kernel_size=3, padding=1) self.loc_proj = nn.Linear(conv_out_channel, self.dim, bias=False) self.query_proj = nn.Linear(hidden_dim, self.dim * num_heads, bias=False) self.value_proj = nn.Linear(hidden_dim, self.dim * num_heads, bias=False) self.score_proj = nn.Linear(self.dim, 1, bias=True) self.bias = nn.Parameter(torch.rand(self.dim).uniform_(-0.1, 0.1)) def forward(self, query: Tensor, value: Tensor, last_attn: Tensor) -> Tuple[Tensor, Tensor]: batch_size, seq_len = value.size(0), value.size(1) if last_attn is None: last_attn = value.new_zeros(batch_size, self.num_heads, seq_len) loc_energy = torch.tanh(self.loc_proj(self.conv1d(last_attn).transpose(1, 2))) loc_energy = loc_energy.unsqueeze(1).repeat(1, self.num_heads, 1, 1).view(-1, seq_len, self.dim) query = self.query_proj(query).view(batch_size, -1, self.num_heads, self.dim).permute(0, 2, 1, 3) value = self.value_proj(value).view(batch_size, -1, self.num_heads, self.dim).permute(0, 2, 1, 3) query = query.contiguous().view(-1, 1, self.dim) value = value.contiguous().view(-1, seq_len, self.dim) score = self.score_proj(torch.tanh(value + query + loc_energy + self.bias)).squeeze(2) attn = F.softmax(score, dim=1) value = value.view(batch_size, seq_len, self.num_heads, self.dim).permute(0, 2, 1, 3) value = value.contiguous().view(-1, seq_len, self.dim) context = torch.bmm(attn.unsqueeze(1), value).view(batch_size, -1, self.num_heads * self.dim) attn = attn.view(batch_size, self.num_heads, -1) return context, attn class MultiHeadAttention(nn.Module): """ Multi-Head Attention proposed in "Attention Is All You Need" Instead of performing a single attention function with d_model-dimensional keys, values, and queries, project the queries, keys and values h times with different, learned linear projections to d_head dimensions. These are concatenated and once again projected, resulting in the final values. Multi-head attention allows the model to jointly attend to information from different representation subspaces at different positions. MultiHead(Q, K, V) = Concat(head_1, ..., head_h) · W_o where head_i = Attention(Q · W_q, K · W_k, V · W_v) Args: d_model (int): The dimension of keys / values / quries (default: 512) num_heads (int): The number of attention heads. (default: 8) Inputs: query, key, value, mask - **query** (batch, q_len, d_model): In transformer, three different ways: Case 1: come from previoys decoder layer Case 2: come from the input embedding Case 3: come from the output embedding (masked) - **key** (batch, k_len, d_model): In transformer, three different ways: Case 1: come from the output of the encoder Case 2: come from the input embeddings Case 3: come from the output embedding (masked) - **value** (batch, v_len, d_model): In transformer, three different ways: Case 1: come from the output of the encoder Case 2: come from the input embeddings Case 3: come from the output embedding (masked) - **mask** (-): tensor containing indices to be masked Returns: output, attn - **output** (batch, output_len, dimensions): tensor containing the attended output features. - **attn** (batch * num_heads, v_len): tensor containing the attention (alignment) from the encoder outputs. """ def __init__(self, d_model: int = 512, num_heads: int = 8): super(MultiHeadAttention, self).__init__() assert d_model % num_heads == 0, "d_model % num_heads should be zero." self.d_head = int(d_model / num_heads) self.num_heads = num_heads self.scaled_dot_attn = ScaledDotProductAttention(self.d_head) self.query_proj = nn.Linear(d_model, self.d_head * num_heads) self.key_proj = nn.Linear(d_model, self.d_head * num_heads) self.value_proj = nn.Linear(d_model, self.d_head * num_heads) def forward( self, query: Tensor, key: Tensor, value: Tensor, mask: Optional[Tensor] = None ) -> Tuple[Tensor, Tensor]: batch_size = value.size(0) query = self.query_proj(query).view(batch_size, -1, self.num_heads, self.d_head) # BxQ_LENxNxD key = self.key_proj(key).view(batch_size, -1, self.num_heads, self.d_head) # BxK_LENxNxD value = self.value_proj(value).view(batch_size, -1, self.num_heads, self.d_head) # BxV_LENxNxD query = query.permute(2, 0, 1, 3).contiguous().view(batch_size * self.num_heads, -1, self.d_head) # BNxQ_LENxD key = key.permute(2, 0, 1, 3).contiguous().view(batch_size * self.num_heads, -1, self.d_head) # BNxK_LENxD value = value.permute(2, 0, 1, 3).contiguous().view(batch_size * self.num_heads, -1, self.d_head) # BNxV_LENxD if mask is not None: mask = mask.unsqueeze(1).repeat(1, self.num_heads, 1, 1) # BxNxQ_LENxK_LEN context, attn = self.scaled_dot_attn(query, key, value, mask) context = context.view(self.num_heads, batch_size, -1, self.d_head) context = context.permute(1, 2, 0, 3).contiguous().view(batch_size, -1, self.num_heads * self.d_head) # BxTxND return context, attn class RelativeMultiHeadAttention(nn.Module): """ Multi-head attention with relative positional encoding. This concept was proposed in the "Transformer-XL: Attentive Language Models Beyond a Fixed-Length Context" Args: d_model (int): The dimension of model num_heads (int): The number of attention heads. dropout_p (float): probability of dropout Inputs: query, key, value, pos_embedding, mask - **query** (batch, time, dim): Tensor containing query vector - **key** (batch, time, dim): Tensor containing key vector - **value** (batch, time, dim): Tensor containing value vector - **pos_embedding** (batch, time, dim): Positional embedding tensor - **mask** (batch, 1, time2) or (batch, time1, time2): Tensor containing indices to be masked Returns: - **outputs**: Tensor produces by relative multi head attention module. """ def __init__( self, d_model: int = 512, num_heads: int = 16, dropout_p: float = 0.1, ): super(RelativeMultiHeadAttention, self).__init__() assert d_model % num_heads == 0, "d_model % num_heads should be zero." self.d_model = d_model self.d_head = int(d_model / num_heads) self.num_heads = num_heads self.sqrt_dim = math.sqrt(d_model) self.query_proj = nn.Linear(d_model, d_model) self.key_proj = nn.Linear(d_model, d_model) self.value_proj = nn.Linear(d_model, d_model) self.pos_proj = nn.Linear(d_model, d_model, bias=False) self.dropout = nn.Dropout(p=dropout_p) self.u_bias = nn.Parameter(torch.Tensor(self.num_heads, self.d_head)) self.v_bias = nn.Parameter(torch.Tensor(self.num_heads, self.d_head)) torch.nn.init.xavier_uniform_(self.u_bias) torch.nn.init.xavier_uniform_(self.v_bias) self.out_proj = nn.Linear(d_model, d_model) def forward( self, query: Tensor, key: Tensor, value: Tensor, pos_embedding: Tensor, mask: Optional[Tensor] = None, ) -> Tensor: batch_size = value.size(0) query = self.query_proj(query).view(batch_size, -1, self.num_heads, self.d_head) key = self.key_proj(key).view(batch_size, -1, self.num_heads, self.d_head).permute(0, 2, 1, 3) value = self.value_proj(value).view(batch_size, -1, self.num_heads, self.d_head).permute(0, 2, 1, 3) pos_embedding = self.pos_proj(pos_embedding).view(batch_size, -1, self.num_heads, self.d_head) content_score = torch.matmul((query + self.u_bias).transpose(1, 2), key.transpose(2, 3)) pos_score = torch.matmul((query + self.v_bias).transpose(1, 2), pos_embedding.permute(0, 2, 3, 1)) pos_score = self._compute_relative_positional_encoding(pos_score) score = (content_score + pos_score) / self.sqrt_dim if mask is not None: mask = mask.unsqueeze(1) score.masked_fill_(mask, -1e9) attn = F.softmax(score, -1) attn = self.dropout(attn) context = torch.matmul(attn, value).transpose(1, 2) context = context.contiguous().view(batch_size, -1, self.d_model) return self.out_proj(context) def _compute_relative_positional_encoding(self, pos_score: Tensor) -> Tensor: batch_size, num_heads, seq_length1, seq_length2 = pos_score.size() zeros = pos_score.new_zeros(batch_size, num_heads, seq_length1, 1) padded_pos_score = torch.cat([zeros, pos_score], dim=-1) padded_pos_score = padded_pos_score.view(batch_size, num_heads, seq_length2 + 1, seq_length1) pos_score = padded_pos_score[:, :, 1:].view_as(pos_score) return pos_score class CustomizingAttention(nn.Module): r""" Customizing Attention Applies a multi-head + location-aware attention mechanism on the output features from the decoder. Multi-head attention proposed in "Attention Is All You Need" paper. Location-aware attention proposed in "Attention-Based Models for Speech Recognition" paper. I combined these two attention mechanisms as custom. Args: hidden_dim (int): The number of expected features in the output num_heads (int): The number of heads. (default: ) conv_out_channel (int): The dimension of convolution Inputs: query, value, last_attn - **query** (batch, q_len, hidden_dim): tensor containing the output features from the decoder. - **value** (batch, v_len, hidden_dim): tensor containing features of the encoded input sequence. - **last_attn** (batch_size * num_heads, v_len): tensor containing previous timestep`s alignment Returns: output, attn - **output** (batch, output_len, dimensions): tensor containing the attended output features from the decoder. - **attn** (batch * num_heads, v_len): tensor containing the alignment from the encoder outputs. Reference: - **Attention Is All You Need**: https://arxiv.org/abs/1706.03762 - **Attention-Based Models for Speech Recognition**: https://arxiv.org/abs/1506.07503 """ def __init__(self, hidden_dim: int, num_heads: int = 4, conv_out_channel: int = 10) -> None: super(CustomizingAttention, self).__init__() self.hidden_dim = hidden_dim self.num_heads = num_heads self.dim = int(hidden_dim / num_heads) self.scaled_dot_attn = ScaledDotProductAttention(self.dim) self.conv1d = nn.Conv1d(1, conv_out_channel, kernel_size=3, padding=1) self.query_proj = nn.Linear(hidden_dim, self.dim * num_heads, bias=True) self.value_proj = nn.Linear(hidden_dim, self.dim * num_heads, bias=False) self.loc_proj = nn.Linear(conv_out_channel, self.dim, bias=False) self.bias = nn.Parameter(torch.rand(self.dim * num_heads).uniform_(-0.1, 0.1)) def forward(self, query: Tensor, value: Tensor, last_attn: Tensor) -> Tuple[Tensor, Tensor]: batch_size, q_len, v_len = value.size(0), query.size(1), value.size(1) if last_attn is None: last_attn = value.new_zeros(batch_size * self.num_heads, v_len) loc_energy = self.get_loc_energy(last_attn, batch_size, v_len) # get location energy query = self.query_proj(query).view(batch_size, q_len, self.num_heads * self.dim) value = self.value_proj(value).view(batch_size, v_len, self.num_heads * self.dim) + loc_energy + self.bias query = query.view(batch_size, q_len, self.num_heads, self.dim).permute(2, 0, 1, 3) value = value.view(batch_size, v_len, self.num_heads, self.dim).permute(2, 0, 1, 3) query = query.contiguous().view(-1, q_len, self.dim) value = value.contiguous().view(-1, v_len, self.dim) context, attn = self.scaled_dot_attn(query, value) attn = attn.squeeze() context = context.view(self.num_heads, batch_size, q_len, self.dim).permute(1, 2, 0, 3) context = context.contiguous().view(batch_size, q_len, -1) return context, attn def get_loc_energy(self, last_attn: Tensor, batch_size: int, v_len: int) -> Tensor: conv_feat = self.conv1d(last_attn.unsqueeze(1)) conv_feat = conv_feat.view(batch_size, self.num_heads, -1, v_len).permute(0, 1, 3, 2) loc_energy = self.loc_proj(conv_feat).view(batch_size, self.num_heads, v_len, self.dim) loc_energy = loc_energy.permute(0, 2, 1, 3).reshape(batch_size, v_len, self.num_heads * self.dim) return loc_energy