Cortex-0 / src /model /text_model.py
Frankenstein-Labs's picture
feat(vlm): Phase 6 — VLM génératif image+texte
886cf7c verified
Raw History Blame Contribute Delete
4.83 kB
"""Transformer texte decoder-only de Cortex-0 — écrit à la main.
Assemblage : embeddings de tokens + N blocs pre-norm (attention causale RoPE +
FFN GELU) + LayerNorm finale + tête de langage (`lm_head`).
Le `lm_head` peut être **lié** aux embeddings (weight tying), ce qui réduit le
nombre de paramètres et aide l'entraînement sur petits corpus.
"""
from __future__ import annotations
from dataclasses import dataclass
import torch
import torch.nn as nn
from src.model.block import TransformerBlock
from src.model.rope import build_rope_cache
@dataclass
class TextConfig:
vocab_size: int = 32000
n_layer: int = 6
n_head: int = 6
n_embd: int = 384
ctx_len: int = 512
dropout: float = 0.0
bias: bool = False
ffn_mult: int = 4
rope_theta: float = 10000.0
use_rope: bool = True
tie_embeddings: bool = True
init_std: float = 0.02
class CortexTextLM(nn.Module):
"""Modèle de langage autorégressif (decoder-only) de Cortex-0."""
def __init__(self, config: TextConfig):
super().__init__()
self.config = config
self.token_embedding = nn.Embedding(config.vocab_size, config.n_embd)
self.dropout = nn.Dropout(config.dropout)
self.blocks = nn.ModuleList(
[
TransformerBlock(
config.n_embd,
config.n_head,
dropout=config.dropout,
bias=config.bias,
ffn_mult=config.ffn_mult,
rope_theta=config.rope_theta,
use_rope=config.use_rope,
)
for _ in range(config.n_layer)
]
)
self.ln_f = nn.LayerNorm(config.n_embd, bias=config.bias)
self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)
if config.tie_embeddings:
self.lm_head.weight = self.token_embedding.weight
self.apply(self._init_weights)
# Initialisation plus fine sur les projections résiduelles (comme GPT-2).
for name, param in self.named_parameters():
if name.endswith("out_proj.weight") or name.endswith("fc2.weight"):
nn.init.normal_(param, mean=0.0, std=config.init_std / (2 * config.n_layer) ** 0.5)
@staticmethod
def _init_weights(module: nn.Module) -> None:
if isinstance(module, nn.Linear):
nn.init.normal_(module.weight, mean=0.0, std=0.02)
if module.bias is not None:
nn.init.zeros_(module.bias)
elif isinstance(module, nn.Embedding):
nn.init.normal_(module.weight, mean=0.0, std=0.02)
def forward(
self,
input_ids: torch.Tensor | None = None,
targets: torch.Tensor | None = None,
inputs_embeds: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor | None]:
"""Retourne ``(logits, loss)``.
``input_ids`` : ``(B, T)`` ou ``inputs_embeds`` : ``(B, T, n_embd)``
(permet d'injecter des embeddings multimodaux). ``targets`` : ``(B, T)`` ;
les positions à ignorer (``-100``) ne comptent pas dans la perte.
"""
hidden = self.encode(input_ids=input_ids, inputs_embeds=inputs_embeds)
logits = self.lm_head(hidden)
loss = None
if targets is not None:
loss = nn.functional.cross_entropy(
logits.reshape(-1, logits.size(-1)), targets.reshape(-1),
ignore_index=-100,
)
return logits, loss
def encode(
self,
input_ids: torch.Tensor | None = None,
inputs_embeds: torch.Tensor | None = None,
) -> torch.Tensor:
"""Retourne les états cachés finaux ``(B, T, n_embd)`` (avant la tête)."""
if inputs_embeds is None:
if input_ids is None:
raise ValueError("fournir input_ids ou inputs_embeds")
x = self.dropout(self.token_embedding(input_ids))
else:
x = self.dropout(inputs_embeds)
T = x.size(1)
if T > self.config.ctx_len:
raise ValueError(f"longueur {T} > ctx_len {self.config.ctx_len}")
cos, sin = build_rope_cache(T, self.config.n_embd // self.config.n_head,
self.config.rope_theta, x.device, x.dtype)
for block in self.blocks:
x = block(x, cos, sin)
return self.ln_f(x)
def count_parameters(self, non_embedding: bool = False) -> int:
"""Compte les paramètres (option : hors embeddings de tokens et lm_head)."""
n = sum(p.numel() for p in self.parameters())
if non_embedding:
n -= self.token_embedding.weight.numel()
if not self.config.tie_embeddings:
n -= self.lm_head.weight.numel()
return n