| import math |
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| from torch import Tensor |
| from typing import Tuple, Optional |
|
|
|
|
| class MultiHeadSelfAttention(nn.Module): |
| def __init__(self, d_model: int, num_heads: int, dropout: float = 0.1): |
| super().__init__() |
| assert d_model % num_heads == 0, "d_model ต้องหาร num_heads ลงตัว" |
|
|
| self.d_model = d_model |
| self.num_heads = num_heads |
| self.d_k = d_model // num_heads |
|
|
| |
| self.qkv_proj = nn.Linear(d_model, d_model * 3, bias=False) |
| self.out_proj = nn.Linear(d_model, d_model) |
| self.dropout = nn.Dropout(dropout) |
|
|
| def forward( |
| self, |
| x: Tensor, |
| key_padding_mask: Optional[Tensor] = None, |
| attn_mask: Optional[Tensor] = None, |
| ) -> Tuple[Tensor, Tensor]: |
|
|
| B, T, _ = x.shape |
|
|
| |
| |
| qkv = self.qkv_proj(x) |
| Q, K, V = qkv.chunk(3, dim=-1) |
|
|
| |
| def split_heads(t: Tensor) -> Tensor: |
| return t.view(B, T, self.num_heads, self.d_k).transpose(1, 2) |
|
|
| Q, K, V = split_heads(Q), split_heads(K), split_heads(V) |
|
|
| |
| |
| scale = math.sqrt(self.d_k) |
| scores = torch.matmul(Q, K.transpose(-2, -1)) / scale |
|
|
| |
| if attn_mask is not None: |
| scores = scores + attn_mask |
|
|
| |
| if key_padding_mask is not None: |
| |
| mask = key_padding_mask[:, None, None, :] |
| scores = scores.masked_fill(mask, float('-inf')) |
|
|
| |
| attn_weights = F.softmax(scores, dim=-1) |
|
|
| |
| attn_weights = torch.nan_to_num(attn_weights, nan=0.0) |
|
|
| attn_weights = self.dropout(attn_weights) |
|
|
| |
| out = torch.matmul(attn_weights, V) |
|
|
| |
| out = out.transpose(1, 2).contiguous().view(B, T, self.d_model) |
|
|
| |
| out = self.out_proj(out) |
|
|
| |
| return out, attn_weights.mean(dim=1) |
|
|
| if __name__ == "__main__": |
| |
| mha = MultiHeadSelfAttention(d_model=256, num_heads=8) |
| mha.eval() |
| x = torch.randn(2, 16, 256) |
| out, weights = mha(x) |
|
|
| assert out.shape == (2, 16, 256), f"wrong output shape: {out.shape}" |
| assert weights.shape == (2, 8, 16, 16), f"wrong weights shape: {weights.shape}" |
| assert not torch.isnan(out).any(), "NaN in output!" |
| assert abs(weights[0, 0, 0].sum().item() - 1.0) < 1e-5, "weights ไม่ sum to 1!" |
| print("attention OK") |