File size: 8,751 Bytes
a9b9ea6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
"""
model.py β€” Transformer Decoder-Only (estilo GPT) implementado do zero em PyTorch.

Arquitetura:
  Embedding de tokens + Positional Encoding absoluto aprendido
  β†’ N x (Causal Self-Attention + FFN com residual + LayerNorm prΓ©-norma)
  β†’ Linear head projetando de d_model β†’ vocab_size

NΓ£o usa nenhum peso externo. Todos os pesos sΓ£o inicializados aleatoriamente
e aprendidos do zero durante o treino.
"""

import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.checkpoint import checkpoint

from config import BabelConfig


# ── Bloco de AtenΓ§Γ£o Causal ────────────────────────────────────────────────────

class CausalSelfAttention(nn.Module):
    """
    Multi-head self-attention com mΓ‘scara causal (cada token sΓ³ enxerga
    os tokens anteriores e a si mesmo β€” nunca os futuros).
    """

    def __init__(self, config: BabelConfig):
        super().__init__()
        assert config.d_model % config.n_heads == 0, (
            f"d_model ({config.d_model}) deve ser divisΓ­vel por n_heads ({config.n_heads})"
        )

        self.n_heads = config.n_heads
        self.d_head = config.d_model // config.n_heads
        self.d_model = config.d_model
        self.dropout = config.dropout

        self.qkv_proj = nn.Linear(config.d_model, 3 * config.d_model, bias=False)
        self.out_proj = nn.Linear(config.d_model, config.d_model, bias=False)
        self.resid_dropout = nn.Dropout(config.dropout)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        B, T, C = x.shape

        qkv = self.qkv_proj(x)
        q, k, v = qkv.split(self.d_model, dim=2)

        q = q.view(B, T, self.n_heads, self.d_head).transpose(1, 2)
        k = k.view(B, T, self.n_heads, self.d_head).transpose(1, 2)
        v = v.view(B, T, self.n_heads, self.d_head).transpose(1, 2)

        # Flash Attention via PyTorch 2.x β€” is_causal=True substitui a mΓ‘scara manual
        dropout_p = self.dropout if self.training else 0.0
        out = F.scaled_dot_product_attention(q, k, v, dropout_p=dropout_p, is_causal=True)

        out = out.transpose(1, 2).contiguous().view(B, T, C)
        return self.resid_dropout(self.out_proj(out))


# ── Feed-Forward Network ───────────────────────────────────────────────────────

class FeedForward(nn.Module):
    """
    FFN com dois lineares e GELU. d_ff = 4 * d_model por convenΓ§Γ£o GPT.
    """

    def __init__(self, config: BabelConfig):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(config.d_model, config.d_ff, bias=False),
            nn.GELU(),
            nn.Linear(config.d_ff, config.d_model, bias=False),
            nn.Dropout(config.dropout),
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.net(x)


# ── Bloco Transformer ──────────────────────────────────────────────────────────

class TransformerBlock(nn.Module):
    """
    Bloco padrΓ£o: prΓ©-norma β†’ atenΓ§Γ£o β†’ residual β†’ prΓ©-norma β†’ FFN β†’ residual.

    PrΓ©-norma (LayerNorm antes das sub-camadas) Γ© mais estΓ‘vel em treinos
    do zero do que a pΓ³s-norma do paper original.
    """

    def __init__(self, config: BabelConfig):
        super().__init__()
        self.ln1 = nn.LayerNorm(config.d_model)
        self.attn = CausalSelfAttention(config)
        self.ln2 = nn.LayerNorm(config.d_model)
        self.ffn = FeedForward(config)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        x = x + self.attn(self.ln1(x))
        x = x + self.ffn(self.ln2(x))
        return x


# ── Modelo Completo ────────────────────────────────────────────────────────────

class BabelTransformer(nn.Module):
    """
    Transformer decoder-only treinado do zero.

    Uso:
        config = get_config("babel_25M")
        model = BabelTransformer(config)
        logits, loss = model(input_ids, targets)
    """

    def __init__(self, config: BabelConfig):
        super().__init__()
        self.config = config

        self.token_emb = nn.Embedding(config.vocab_size, config.d_model)
        self.pos_emb = nn.Embedding(config.context_length, config.d_model)
        self.emb_dropout = nn.Dropout(config.dropout)

        self.blocks = nn.ModuleList(
            [TransformerBlock(config) for _ in range(config.n_layers)]
        )

        self.ln_final = nn.LayerNorm(config.d_model)

        # ProjeΓ§Γ£o final: d_model β†’ vocab_size (sem bias, como GPT-2)
        self.lm_head = nn.Linear(config.d_model, config.vocab_size, bias=False)

        # Weight tying: token_emb e lm_head compartilham os mesmos pesos.
        # Reduz parΓ’metros e melhora qualidade (Press & Wolf, 2017).
        self.lm_head.weight = self.token_emb.weight

        self._init_weights()

    def _init_weights(self):
        """InicializaΓ§Γ£o normal com std 0.02, igual ao GPT-2."""
        for module in self.modules():
            if isinstance(module, (nn.Linear, nn.Embedding)):
                nn.init.normal_(module.weight, mean=0.0, std=0.02)
                if isinstance(module, nn.Linear) and module.bias is not None:
                    nn.init.zeros_(module.bias)
            elif isinstance(module, nn.LayerNorm):
                nn.init.ones_(module.weight)
                nn.init.zeros_(module.bias)

    def forward(
        self,
        input_ids: torch.Tensor,                  # (B, T)
        targets: torch.Tensor | None = None,      # (B, T) β€” None em inferΓͺncia
    ) -> tuple[torch.Tensor, torch.Tensor | None]:

        B, T = input_ids.shape
        assert T <= self.config.context_length, (
            f"SequΓͺncia de tamanho {T} excede context_length={self.config.context_length}"
        )

        positions = torch.arange(T, device=input_ids.device).unsqueeze(0)  # (1, T)
        x = self.emb_dropout(self.token_emb(input_ids) + self.pos_emb(positions))

        for block in self.blocks:
            if self.config.gradient_checkpointing and self.training:
                x = checkpoint(block, x, use_reentrant=False)
            else:
                x = block(x)

        x = self.ln_final(x)
        logits = self.lm_head(x)   # (B, T, vocab_size)

        loss = None
        if targets is not None:
            # Cross-entropy: achata (B*T, V) vs (B*T,)
            loss = F.cross_entropy(
                logits.view(-1, self.config.vocab_size),
                targets.view(-1),
                ignore_index=-1,
            )

        return logits, loss

    @torch.no_grad()
    def generate(
        self,
        input_ids: torch.Tensor,   # (1, T) β€” um ΓΊnico prompt
        max_new_tokens: int = 200,
        temperature: float = 0.8,
        top_p: float = 0.9,
        repetition_penalty: float = 1.3,
    ) -> torch.Tensor:
        """
        Gera texto auto-regressivamente com top-p (nucleus) sampling.
        Retorna tensor (1, T + max_new_tokens).
        """
        self.eval()
        for _ in range(max_new_tokens):
            # Trunca se ultrapassar context_length
            ctx = input_ids[:, -self.config.context_length:]
            logits, _ = self(ctx)
            logits = logits[:, -1, :] / temperature   # apenas o ΓΊltimo token

            # Penaliza tokens jΓ‘ gerados para reduzir repetiΓ§Γ£o
            if repetition_penalty != 1.0:
                for token_id in input_ids[0].tolist():
                    if logits[0, token_id] > 0:
                        logits[0, token_id] /= repetition_penalty
                    else:
                        logits[0, token_id] *= repetition_penalty

            # Top-p sampling
            sorted_logits, sorted_idx = torch.sort(logits, descending=True)
            cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
            # Remove tokens acima do threshold top_p
            sorted_logits[cumulative_probs - F.softmax(sorted_logits, dim=-1) > top_p] = float("-inf")
            probs = F.softmax(sorted_logits, dim=-1)
            next_token_sorted = torch.multinomial(probs, num_samples=1)
            next_token = sorted_idx.gather(-1, next_token_sorted)

            input_ids = torch.cat([input_ids, next_token], dim=1)

        return input_ids

    def count_parameters(self) -> int:
        return sum(p.numel() for p in self.parameters() if p.requires_grad)