LABORATÓRIO · MINI TRANSFORMER

Treine atenção causal para prever a próxima palavra.

Um modelo reduzido transforma tokens em embeddings de três dimensões, calcula uma cabeça de Self-Attention e treina uma saída Softmax sobre um corpus pequeno.

tokensfraseE + posiçãovetoresQ K VatençãoSoftmaxpróximo token
Escopo didático: usamos uma cabeça, embeddings fixos e WQ = WK = WV = I. O treinamento ajusta a camada de saída. Transformers reais treinam essas matrizes, usam várias cabeças e muitas camadas.
ÉPOCAS0
LOSS MÉDIA
PERPLEXIDADE
VOCABULÁRIO0

Exemplos criados do texto

Cada prefixo deve prever a palavra seguinte. Clique em uma linha para inspecionar sua atenção.

Corpus preparado

Embeddings + posição

Cada token recebe três valores de embedding e três valores posicionais.

Matriz de Self-Attention causal

Cada linha é uma consulta. As células mais escuras possuem maior peso αᵢⱼ; o triângulo futuro permanece mascarado.

Distribuição do próximo token

A saída Softmax compara todos os tokens do vocabulário.

Treinamento selecionado, passo a passo

Da frase até a atualização de um peso da camada de saída.

Erro × resposta

A curva vermelha mostra a loss normalizada; a verde mostra a acurácia de próximo token.

loss normalizadaacurácia

CÓDIGO COMPLETO E EXECUTÁVEL

Treine e use um pequeno preditor com autoatenção

Este é um Transformer educacional reduzido: tokeniza o corpus, cria embeddings, calcula atenção causal, treina a camada de saída por entropia cruzada e prevê novos tokens. Os embeddings e a atenção ficam fixos para manter a matemática legível; em um Transformer real, essas matrizes também recebem gradientes.

prepararTexto()atencao()treinar()preverProximo()gerar()
// ============================================================
// MINI TRANSFORMER DIDÁTICO PARA PREVISÃO DO PRÓXIMO TOKEN
// Pode ser executado inteiro no console do navegador ou no Node.
// ============================================================
var textoTreino = 'redes neurais aprendem padrões ' +
  'redes neurais processam dados ' +
  'redes neurais fazem previsões';

var dimensao = 4;
var contextoMaximo = 4;
var vocabulario = [];
var tokensTreino = [];
var embeddings = [];
var pesosSaida = [];
var biasSaida = [];

function tokenizar(texto) {
  var limpo = texto.toLowerCase().replace(/[^a-záàâãéêíóôõúç0-9]+/g, ' ').trim();
  if (limpo === '') return [];
  return limpo.split(' ');
}

// Cria o vocabulário e converte palavras em índices inteiros.
function prepararTexto(texto) {
  var palavras = tokenizar(texto);
  vocabulario = [];
  tokensTreino = [];

  for (var i = 0; i < palavras.length; i++) {
    var id = vocabulario.indexOf(palavras[i]);
    if (id === -1) {
      vocabulario.push(palavras[i]);
      id = vocabulario.length - 1;
    }
    tokensTreino.push(id);
  }
}

// Inicialização determinística: facilita repetir o exemplo em sala.
function iniciarParametros() {
  embeddings = [];
  pesosSaida = [];
  biasSaida = [];

  for (var token = 0; token < vocabulario.length; token++) {
    embeddings[token] = [];
    pesosSaida[token] = [];
    biasSaida[token] = 0;
    for (var d = 0; d < dimensao; d++) {
      embeddings[token][d] = Math.sin((token + 1) * (d + 1)) * 0.5;
      pesosSaida[token][d] = 0;
    }
  }
}

function produtoEscalar(a, b) {
  var soma = 0;
  for (var i = 0; i < a.length; i++) soma = soma + a[i] * b[i];
  return soma;
}

function softmax(logits) {
  var maior = logits[0];
  for (var i = 1; i < logits.length; i++) {
    if (logits[i] > maior) maior = logits[i];
  }

  var probabilidades = [];
  var soma = 0;
  for (i = 0; i < logits.length; i++) {
    probabilidades[i] = Math.exp(logits[i] - maior);
    soma = soma + probabilidades[i];
  }
  for (i = 0; i < logits.length; i++) probabilidades[i] /= soma;
  return probabilidades;
}

// Q, K e V usam os embeddings diretamente nesta versão reduzida.
function atencao(prefixo) {
  var consulta = embeddings[prefixo[prefixo.length - 1]];
  var escores = [];

  for (var i = 0; i < prefixo.length; i++) {
    var chave = embeddings[prefixo[i]];
    escores[i] = produtoEscalar(consulta, chave) / Math.sqrt(dimensao);
  }

  var pesosAtencao = softmax(escores);
  var contexto = [];
  for (var d = 0; d < dimensao; d++) contexto[d] = 0;

  for (i = 0; i < prefixo.length; i++) {
    var valor = embeddings[prefixo[i]];
    for (d = 0; d < dimensao; d++) {
      contexto[d] = contexto[d] + pesosAtencao[i] * valor[d];
    }
  }
  return [contexto, pesosAtencao];
}

function forward(prefixo) {
  var resultadoAtencao = atencao(prefixo);
  var contexto = resultadoAtencao[0];
  var logits = [];

  for (var token = 0; token < vocabulario.length; token++) {
    logits[token] = produtoEscalar(pesosSaida[token], contexto) + biasSaida[token];
  }
  return [softmax(logits), contexto, resultadoAtencao[1]];
}

// Gradiente da Softmax + entropia cruzada: probabilidade - alvo.
function ajustarSaida(contexto, probabilidades, alvo, taxa) {
  for (var token = 0; token < vocabulario.length; token++) {
    var esperado = 0;
    if (token === alvo) esperado = 1;
    var gradiente = probabilidades[token] - esperado;

    for (var d = 0; d < dimensao; d++) {
      pesosSaida[token][d] -= taxa * gradiente * contexto[d];
    }
    biasSaida[token] -= taxa * gradiente;
  }
}

function treinar(epocas, taxa) {
  var perdaMedia = 0;
  for (var epoca = 0; epoca < epocas; epoca++) {
    var perdaTotal = 0;

    for (var posicao = 1; posicao < tokensTreino.length; posicao++) {
      var inicio = posicao - contextoMaximo;
      if (inicio < 0) inicio = 0;
      var prefixo = tokensTreino.slice(inicio, posicao);
      var alvo = tokensTreino[posicao];
      var resultado = forward(prefixo);

      perdaTotal -= Math.log(resultado[0][alvo] + 0.0000001);
      ajustarSaida(resultado[1], resultado[0], alvo, taxa);
    }
    perdaMedia = perdaTotal / (tokensTreino.length - 1);
  }
  return perdaMedia;
}

function converterPrompt(texto) {
  var palavras = tokenizar(texto);
  var ids = [];
  for (var i = 0; i < palavras.length; i++) {
    var id = vocabulario.indexOf(palavras[i]);
    if (id !== -1) ids.push(id);
  }
  if (ids.length > contextoMaximo) ids = ids.slice(ids.length - contextoMaximo);
  return ids;
}

function preverProximo(texto) {
  var prefixo = converterPrompt(texto);
  if (prefixo.length === 0) return '[token desconhecido]';
  var probabilidades = forward(prefixo)[0];
  var melhor = 0;
  for (var i = 1; i < probabilidades.length; i++) {
    if (probabilidades[i] > probabilidades[melhor]) melhor = i;
  }
  return vocabulario[melhor];
}

function gerar(textoInicial, quantidade) {
  var texto = textoInicial;
  for (var i = 0; i < quantidade; i++) texto += ' ' + preverProximo(texto);
  return texto;
}

// ----------------------- USO DO MODELO -----------------------
prepararTexto(textoTreino);
iniciarParametros();
var perdaFinal = treinar(600, 0.08);
console.log('Vocabulário:', vocabulario);
console.log('Perda final:', perdaFinal);
console.log('Próximo token de "redes neurais":', preverProximo('redes neurais'));
console.log('Geração:', gerar('redes neurais', 4));