Objetivo da Aula
Criar e manipular tensores de 0D a 3D com TensorFlow.js, entendendo o que cada dimensão representa
Implementar produto escalar e similaridade cosseno usando operações tensoriais
Explicar a diferença entre ML local (cliente) e ML via API (servidor) com trade-offs concretos de custo, latência e privacidade
Interpretar shapes de tensores no contexto de NLP: [batch, seq_len, embedding_dim]
Aplicar broadcasting para operações entre tensores de shapes diferentes
Por que isso importa
TensorFlow.js não é “ML de brinquedo”. Ele roda modelos reais no browser — o Google usa TF.js para reconhecimento de gestos na câmera, autocomplete de formulários, detecção de spam client-side. Mas o mais relevante para você: entender tensores é entender a linguagem que todos os frameworks de ML falam.
Quando você usa LLMs via API, você está usando tensores sem saber. Cada chamada de client.messages.create() internamente: 1. Tokeniza o texto em inteiros 2. Mapeia inteiros para tensores de embedding (matrizes de floats) 3. Passa por camadas de atenção (multiplicação de matrizes) 4. Produz um tensor de logits (probabilidades sobre vocabulário) 5. Amostra o próximo token
Entender tensores te dá poder para: - Otimizar custo: saber que batch de requests é mais eficiente que requests individuais (multiplicação de matrizes é mais eficiente para muitas amostras simultaneamente) - Diagnosticar lentidão: shapes grandes = mais computação. Um tensor [batch=32, seq=1024, dim=4096] requer ordens de magnitude mais compute que [batch=1, seq=128, dim=768] - Entender limitações: por que LLMs não conseguem processar vídeo frame por frame em tempo real — o custo computacional de tensores de alta dimensão explica isso
Conceitos Fundamentais
Tensores: a estrutura universal de dados em ML
Um tensor é simplesmente um array N-dimensional de números. O que diferencia tensores de arrays JavaScript comuns é: - Tipo fixo: todos os elementos são do mesmo tipo (float32, int32) - Operações otimizadas: implementadas em WebGL (GPU) ou WASM para paralelismo - Gerenciamento de memória: necessário dispose() explícito para liberar GPU memory
Escalar (0D): um único número
tf.scalar(42) → 42
Vetor (1D): linha de números
tf.tensor1d([1, 2, 3]) → shape: [3]
Matriz (2D): grade de números
tf.tensor2d([[1,2], [3,4]]) → shape: [2, 2]
Tensor 3D: cubo de números
shape: [2, 3, 4] → 2 matrizes, cada uma 3×4Por que isso importa para NLP?
Em processamento de linguagem natural, shapes têm semântica específica:
Shape de embedding de uma palavra: [embedding_dim]
[0.23, -0.87, 0.14, ..., -0.33] (768 dimensões no BERT)
Shape de embedding de uma frase: [seq_len, embedding_dim]
"gato come peixe" → [3, 768]
(3 tokens, cada um com 768 dimensões)
Shape de um batch de frases: [batch_size, seq_len, embedding_dim]
32 frases de 128 tokens com 768 dimensões → [32, 128, 768]
Total de números: 32 × 128 × 768 = 3.145.728 floatsFundamento: Tensores e Shapes
Um tensor é um array N-dimensional de números. O shape descreve suas dimensões: [32, 128, 768] significa 32 exemplos (batch), cada um com 128 posições (tokens), cada posição com 768 números (dimensões do embedding). O shape é o "tamanho do problema" — e memória de GPU = produto das dimensões × bytes por número. [32, 128, 768] em float32 (4 bytes) = 12,6 MB só para esse tensor. Quase toda operação de ML se reduz a multiplicação de matrizes — por isso GPU é essencial: ela paraleliza esse produto por design.
Saber ler shapes é saber ler o tamanho do problema. Um tensor [32, 128, 768] de float32 (4 bytes cada) ocupa 12 MB de memória de GPU.
Operações tensoriais: a aritmética do ML
Element-wise (ponto a ponto): operações aplicadas em cada posição correspondente
const a = tf.tensor1d([1, 2, 3])
const b = tf.tensor1d([4, 5, 6])
a.add(b) // [5, 7, 9] — soma cada posição
a.mul(b) // [4, 10, 18] — multiplica cada posição
a.sub(b) // [-3, -3, -3] — subtrai cada posiçãoProduto escalar (dot product): multiplica pares e soma. Resultado é um número.
a = [1, 2, 3]
b = [4, 5, 6]
a · b = (1×4) + (2×5) + (3×6) = 4 + 10 + 18 = 32O produto escalar é o núcleo do mecanismo de atenção. Calcular “quão relevante é token A para token B” é calcular o produto escalar entre seus vetores de Query e Key.
Reshape: muda a forma do tensor sem alterar os dados
// 6 números podem ser organizados de múltiplas formas:
tf.tensor1d([1,2,3,4,5,6]).reshape([2, 3]) // 2×3 matrix
tf.tensor1d([1,2,3,4,5,6]).reshape([3, 2]) // 3×2 matrix
tf.tensor1d([1,2,3,4,5,6]).reshape([6, 1]) // coluna verticalReshape é necessário para compatibilidade de shapes em operações. Por exemplo, tf.dot() exige matrizes 2D, então vetores 1D precisam de reshape antes.
Broadcasting: aplica operações entre tensores de shapes diferentes automaticamente
Matriz [3, 2] + Vetor [2] = ?
Regra: vetor [2] é "expandido" para [3, 2]:
[[10, 20], [10, 20], [10, 20]]
Resultado: cada linha da matriz recebe o biasBroadcasting elimina loops Python explícitos, tornando o código mais eficiente e legível.
ML local vs ML via API: quando usar cada um
Critério | ML Local (TF.js) | ML via API (Claude/GPT) |
Latência | < 10ms (sem rede) | 500ms–5s (rede + inferência) |
Privacidade | Dados não saem do dispositivo | Dados enviados ao servidor |
Custo | Zero por inferência (CPU/GPU do cliente) | $0.001–$0.10 por chamada |
Capacidade | Modelos pequenos (< 100MB) | Modelos bilhões de parâmetros |
Offline | Funciona sem internet | Requer conectividade |
Atualização | Bundle novo para cada versão | Transparente para o cliente |
Regra prática: use ML local para tarefas simples e frequentes (classificação binária, detecção de face, similaridade de curto texto). Use API para tarefas complexas que exigem raciocínio, geração de texto, ou compreensão semântica profunda.
Aprofundamento Técnico
Multiplicação de matrizes: o coração do ML
Quase toda operação em uma rede neural se reduz a multiplicação de matrizes. Entender como funciona explica por que GPUs são tão importantes.
Multiplicação A × B:
A shape: [m, n]
B shape: [n, p]
Resultado: [m, p]
Para cada posição [i, j] do resultado:
resultado[i][j] = Σ(A[i][k] × B[k][j]) para k em 0..n
Exemplo:
A = [[1, 2], [3, 4]] shape: [2, 2]
B = [[5, 6], [7, 8]] shape: [2, 2]
resultado[0][0] = 1×5 + 2×7 = 19
resultado[0][1] = 1×6 + 2×8 = 22
resultado[1][0] = 3×5 + 4×7 = 43
resultado[1][1] = 3×6 + 4×8 = 50Por que GPU? Para multiplicar matrizes grandes, cada posição [i, j] pode ser calculada em paralelo — elas são independentes. Uma GPU com 3.000 cores pode calcular 3.000 posições simultaneamente. Uma CPU com 8 cores calcula 8 por vez. Para matrizes de 4096×4096 (comuns em LLMs), a diferença é de horas vs minutos.
Gradientes e backpropagation em TF.js
TensorFlow.js faz diferenciação automática — calcula gradientes (derivadas) automaticamente para qualquer operação com tensores.
Fundamento: Diferenciação Automática e Learning Rate
O framework registra cada operação feita no tensor e, ao chamar a função de gradiente, percorre o grafo computacional de trás para frente calculando derivadas automáticamente — você não as escreve à mão. O learning rate é o "tamanho do passo": muito alto, o ajuste oscila e diverge; muito baixo, o treino é lento demais. É um hiperparâmetro que você vai encontrar de novo na Parte IX, no fine-tuning. A intuição é simples: você está descendo uma montanha dando passos proporcionais à inclinação — o learning rate define o comprimento de cada passo.
const x = tf.variable(tf.scalar(3.0))
// Função que queremos minimizar: f(x) = x² - 4x + 4 = (x-2)²
// Mínimo em x=2
const f = () => x.square().sub(x.mul(4)).add(4)
// tf.variableGrads() calcula df/dx automaticamente
const { value, grads } = tf.variableGrads(f)
console.log('f(3):', value.dataSync()[0]) // 1.0 → (3-2)² = 1
console.log('df/dx em x=3:', grads.x.dataSync()[0]) // 2.0 → 2×(3-2) = 2
// Gradient descent manual (um passo):
// x_novo = x - learning_rate × gradiente
// x_novo = 3 - 0.1 × 2 = 2.8 (mais próximo do mínimo 2.0)Este é exatamente o mecanismo que treina redes neurais — a diferença é que, em vez de uma variável, existem bilhões de parâmetros e o gradiente é calculado com respeito a todos eles simultaneamente.
Gerenciamento de memória com tf.tidy()
Fundamento: Memória de GPU e Garbage Collection
Tensores em TensorFlow.js vivem fora do heap gerenciado pelo runtime JavaScript — ficam na memória da GPU ou na WebGL. O garbage collector do JS não os coleta. Se você não liberar explicitamente com .dispose(), a memória da GPU vazará até o navegador travar. tf.tidy() cria um escopo: todos os tensores criados dentro dele são liberados automaticamente ao sair. .dataSync() é uma operação bloqueante que copia dados da GPU para a CPU — equivalente a um I/O síncrono. Em produção com Node.js ou serviços de inferência, o mesmo princípio se aplica: libere tensores depois de cada request, ou a memória da GPU cresce até o processo morrer.
Tensores em TF.js alocam memória na GPU. Se não liberados, causam memory leak.
// ❌ Problemático: tensores intermediários ficam na GPU indefinidamente
function calcularSimilaridade(a, b) {
const produto = a.mul(b) // aloca memória
const soma = produto.sum() // aloca memória
return soma.dataSync()[0] // produto ainda na GPU!
}
// ✓ Correto: tf.tidy() libera automaticamente tensores temporários
function calcularSimilaridade(a, b) {
return tf.tidy(() => {
const produto = a.mul(b)
return produto.sum().dataSync()[0]
// quando tidy() termina, todos tensores intermediários são liberados
})
}Em loops longos ou aplicações que rodam continuamente (como um chatbot com reconhecimento de face), memory leaks de tensores causam degradação progressiva de performance até crash.
Exemplos Anotados
Exemplo 1: Similaridade cosseno com TF.js
const tf = require('@tensorflow/tfjs-node')
function norma(tensor) {
// Elevar ao quadrado cada elemento, somar tudo, extrair raiz
// Isso é o teorema de Pitágoras generalizado: √(x₁² + x₂² + ... + xₙ²)
return Math.sqrt(tensor.mul(tensor).sum().dataSync()[0])
}
function similCosseno(vecA, vecB) {
// tf.dot() exige tensores 2D — reshape [n] → [1, n] e [n] → [n, 1]
// Isso transforma "vetor linha" em matriz linha e "vetor coluna" em matriz coluna
const n = vecA.shape[0]
const produtoEscalar = tf
.dot(vecA.reshape([1, n]), vecB.reshape([n, 1]))
.dataSync()[0] // [0] extrai o único elemento do tensor resultante
// norma() retorna um número JavaScript, não tensor
// Por isso podemos usar divisão JavaScript normal aqui
const normaA = norma(vecA)
const normaB = norma(vecB)
// Protegemos contra divisão por zero (vetores nulos não têm direção definida)
if (normaA === 0 || normaB === 0) return 0
return produtoEscalar / (normaA * normaB)
}
// Embeddings de 5 dimensões: [tecnologia, animal, comida, transporte, emoção]
const embeddings = {
gato: tf.tensor1d([0.1, 0.9, 0.2, 0.0, 0.3]),
cachorro: tf.tensor1d([0.1, 0.8, 0.2, 0.0, 0.4]),
computador: tf.tensor1d([0.9, 0.0, 0.0, 0.1, 0.1]),
pizza: tf.tensor1d([0.0, 0.0, 0.9, 0.0, 0.3]),
}
// tf.tidy() para evitar memory leak dos tensores intermediários em similCosseno
const pares = [['gato', 'cachorro'], ['computador', 'gato'], ['gato', 'pizza']]
for (const [a, b] of pares) {
// Tensores em embeddings são mantidos (não liberados pelo tidy)
// apenas tensores temporários dentro de similCosseno são liberados
const sim = tf.tidy(() => similCosseno(embeddings[a], embeddings[b]))
console.log(`'${a}' ↔ '${b}': ${sim.toFixed(4)}`)
}Como o arquiteto lê este código
O código processa uma imagem de câmera em loop. tf.tidy() funciona como um bloco try-finally que libera recursos GPU: tudo dentro dele é descartado ao sair. O .dispose() explícito é para tensores criados fora do tidy que precisam de liberação manual — como fechar uma conexão de banco que ficou fora do bloco gerenciado. .dataSync() é o ponto onde a GPU para de processar e devolve os números para a CPU: é o único momento onde você pode ler os valores. Leia como: "processe na GPU, bloqueie, leia resultado na CPU, libere memória".
Output esperado:
'gato' ↔ 'cachorro': 0.9810
'computador' ↔ 'gato': 0.0851
'gato' ↔ 'pizza': 0.1793Exemplo 2: Batch de embeddings e operações matriciais
// Simula como um transformer processa um batch de sentenças
// Shape: [batch_size, seq_len, embedding_dim]
const batchSize = 2
const seqLen = 3
const embeddingDim = 4
// Batch de embeddings (aleatório para demonstração)
// Na prática, cada [i, j, :] seria o embedding do j-ésimo token da i-ésima sentença
const batchEmbeddings = tf.randomNormal([batchSize, seqLen, embeddingDim])
console.log('Shape do batch:', batchEmbeddings.shape)
// → [2, 3, 4]: 2 sentenças, 3 tokens cada, 4 dimensões de embedding
// Média dos embeddings por sentença (uma forma simples de "sentença embedding")
// keepDims: true mantém a dimensão seq_len (necessário para shapes consistentes)
const sentencaEmbeddings = batchEmbeddings.mean(1) // média ao longo do eixo 1 (seq_len)
console.log('Shape após média:', sentencaEmbeddings.shape)
// → [2, 4]: 2 embeddings de sentença, 4 dimensões cada
// Normalização L2: divide cada vetor pela sua norma
// Após normalização, todos os vetores têm magnitude 1
// Isso torna dot product equivalente a similaridade cosseno (sem divisão extra)
const normas = sentencaEmbeddings.norm('euclidean', 1, true) // norma por linha
const normalizados = sentencaEmbeddings.div(normas)
console.log('Shape após normalização:', normalizados.shape) // → [2, 4]
// Matriz de similaridade: similaridade de cada sentença com cada outra
// matMul(A, B^T) onde B^T é a transposta de B
const matrizSim = tf.matMul(normalizados, normalizados.transpose())
console.log('Matriz de similaridade (2×2):')
matrizSim.print()
// Diagonal deve ser 1.0 (cada sentença é idêntica a si mesma)Output esperado:
Shape do batch: [2, 3, 4]
Shape após média: [2, 4]
Shape após normalização: [2, 4]
Matriz de similaridade (2×2):
Tensor
[[1, 0.xxx],
[0.xxx, 1 ]]Padrões e Armadilhas
Padrões recomendados
Padrão 1: Sempre use tf.tidy() em funções que criam tensores temporários Qualquer função que cria tensores mas retorna apenas um valor JavaScript (não tensor) deve envolver suas operações em tf.tidy(). Se a função retorna um tensor, o chamador é responsável por fazer dispose() quando terminar.
Padrão 2: Leia .shape antes de depurar operações Quando uma operação falha com erro de dimensão, o primeiro debug é imprimir o shape de todos os tensores envolvidos. Incompatibilidade de shape é a causa de 80% dos erros em código de ML.
Padrão 3: Use modelo local para decisões frequentes e de baixo custo, API para raciocínio Classificação de sentimento em tempo real (ex: moderar comentários enquanto o usuário digita) → TF.js local. Análise de documento completo para extração de insights → API de LLM. A fronteira é: frequência × complexidade × privacidade.
Armadilhas comuns
⚠️ Armadilha 1: Esquecer reshape antes de tf.dot() O que acontece: tf.dot(vecA, vecB) falha com erro de shape porque vecA e vecB são 1D. Versão correta: tf.dot(vecA.reshape([1, n]), vecB.reshape([n, 1])). tf.dot() requer tensores 2D. Alternativa mais simples: vecA.dot(vecB) para produto escalar direto de 1D.
⚠️ Armadilha 2: Chamar .dataSync() dentro de loops quentes O que acontece: .dataSync() sincroniza GPU→CPU, forçando o browser a parar e aguardar. Em loops que rodam a cada frame, isso causa jank (freezes) visíveis. Versão correta: compute na GPU (operações tensoriais) e extraia resultados com .dataSync() apenas no final, fora do loop.
⚠️ Armadilha 3: Comparar tensores com === em vez de .equal() O que acontece: tensorA === tensorB sempre retorna false — você está comparando referências de objeto, não valores. Versão correta: tensorA.equal(tensorB).all().dataSync()[0] para comparação element-wise, ou compare os arrays com .dataSync(): JSON.stringify(a.dataSync()) === JSON.stringify(b.dataSync()).
Se não for realizar o laboratório, pule para o próximo capítulo.
Ponte para o Lab
O starter tem 3 TODOs todos dentro da função similCosseno():
TODO 1 — Produto escalar com tf.dot() Seção de referência: “Conceitos Fundamentais → Operações tensoriais” e “Exemplos Anotados → Exemplo 1”. Implemente: const produtoEscalar = tf.dot(vecA.reshape([1, n]), vecB.reshape([n, 1])).dataSync()[0] O n deve ser vecA.shape[0] (tamanho do vetor). O .dataSync()[0] extrai o valor numérico do tensor resultado.
TODO 2 — Normas com a função norma() já implementada Seção de referência: “Conceitos Fundamentais → Operações tensoriais → Dot product”. A função norma() já está implementada acima de similCosseno. Chame simplesmente: const normaA = norma(vecA); const normaB = norma(vecB). Não reimplemente — reutilize.
TODO 3 — Divisão final Seção de referência: “Conceitos Fundamentais → Embeddings → Similaridade cosseno”. Após ter produtoEscalar, normaA, normaB, o retorno é: return produtoEscalar / (normaA * normaB). Adicione proteção: if (normaA === 0 || normaB === 0) return 0.
Dica para o TODO mais difícil (TODO 1): o erro mais comum é esquecer o reshape. tf.dot() falha silenciosamente ou com erro confuso se os tensores são 1D. Verifique o shape com console.log(vecA.shape) antes de chamar tf.dot.
Agora você está pronto para o lab.