Voltar ao blog

Classificador de Imagens no Navegador com TensorFlow.js e Aprendizagem por Transferência

Classificador de Imagens no Navegador com TensorFlow.js e Aprendizagem por Transferência

Introdução

Imagine um aplicativo web que classifica imagens em tempo real, direto no navegador. Tradicionalmente, construir um classificador de imagens personalizado exigiria servidores potentes e muito tempo de treinamento. Porém, com TensorFlow.js, podemos realizar aprendizado de máquina no próprio front-end. Isso significa que o usuário não precisa enviar suas fotos a um servidor — tudo é processado localmente, garantindo privacidade e menor latência. Além disso, usando aprendizagem por transferência, reaproveitamos um modelo já treinado (como o MobileNet) e treinamos apenas um pequeno classificador em cima, tornando o processo rápido e eficiente mesmo com poucos dados de exemplo.

Neste artigo, veremos como criar um classificador de imagens personalizado no navegador. Usaremos o TensorFlow.js para carregar o modelo MobileNet pré-treinado, extrair suas características, e então treinar novas camadas para reconhecer classes específicas que queremos ensinar. Também abordaremos técnicas de otimização de performance e manipulação de dados para garantir que o modelo rode em tempo real. Você aprenderá a configurar o ambiente, a preparar os dados de entrada, a treinar o modelo no navegador e a realizar previsões com ele.

TensorFlow.js e Aprendizagem por Transferência

Antes de partir para o código, vamos entender os conceitos básicos que usaremos:

  • TensorFlow.js: é uma biblioteca open-source de aprendizado de máquina desenvolvida pelo Google para JavaScript. Ela permite rodar redes neurais tanto no navegador (usando WebGL ou WebAssembly) quanto em aplicativos Node.js. Com o TF.js podemos criar, treinar e executar modelos de aprendizado de máquina sem sair do ambiente JavaScript. Isso abre a possibilidade de levar inteligência artificial diretamente ao front-end, aproveitando hardware do usuário (GPU via WebGL) e garantindo que os dados fiquem sempre no dispositivo.

  • Aprendizagem por Transferência (Transfer Learning): é uma técnica onde pegamos um modelo já treinado em uma grande base de dados e reaproveitamos seu conhecimento para uma nova tarefa. Por exemplo, o MobileNet foi treinado em milhões de imagens (do ImageNet) para reconhecer milhares de objetos. Em vez de treinar do zero, usamos MobileNet como uma base e treinamos apenas as camadas finais com nossas novas classes. Isso traz várias vantagens:

    • Menos dados necessários: como o modelo já “sabe” reconhecer formas e padrões gerais, precisamos de muito menos imagens do que se fosse do zero.
    • Treinamento mais rápido: apenas as camadas finais são ajustadas, reduzindo muito o tempo de treinamento.
    • Bom para o navegador: em dispositivos com recursos limitados (CPU/GPU do usuário), é fundamental economizar computação. Transferência permite usar um modelo compacto e treinar apenas um classificador simples em cima.

Vantagens da aprendizagem por transferência:

  • Permite reusar redes neurais complexas (por exemplo, MobileNet ou EfficientNet) sem precisar de recursos massivos.
  • Requer poquíssimas imagens de cada classe para atingir boa acurácia.
  • Reduz drasticamente o tempo de treinamento e o uso de memória.
  • Ideal para cenários de navegador, onde o processamento e memória são limitados.

Um exemplo didático: pense em como um ser humano aprende. Se você já sabe reconhecer carros e caminhões na estrada, identificar um modelo novo de carro exige pouca adaptação — você já reconhece formas de roda, faróis, e assim por diante. Da mesma forma, um modelo de rede neural já treinado em milhões de fotos já sabe distinguir bordas, linhas e texturas. Basta afinar suas camadas finais para aprender novas categorias específicas.

Configurando o Ambiente de Desenvolvimento

Para colocar tudo em prática, primeiro precisamos preparar nosso ambiente web. Vamos integrar as bibliotecas necessárias e carregar o modelo pré-treinado.

  1. Incluindo TensorFlow.js no projeto: Você pode usar o TensorFlow.js diretamente pelo CDN adicionando o script no <head> do seu HTML. Exemplo:

    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script> <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/mobilenet"></script>
    • O primeiro <script> carrega o TensorFlow.js (biblioteca principal).
    • O segundo carrega o modelo MobileNet pré-treinado (. Cada arquivo JavaScript disponibiliza a API necessária para usá-lo.
  2. Carregando o modelo MobileNet: Com os scripts incluídos, você pode usar a API do TensorFlow.js para carregar o modelo MobileNet pronto para uso. Por exemplo, em seu código JavaScript:

    // Aguarda o download e inicialização do MobileNet const mobileNet = await mobilenet.load(); console.log('MobileNet carregado com sucesso!');

    Aqui, mobilenet.load() retorna uma instância do modelo. Graças às APIs do tfjs-models, você não precisa se preocupar com URLs de arquivos JSON — é tudo feito internamente.Quando o modelo é carregado, ele já sabe classificar milhares de objetos típicos. Mas nosso objetivo é usar sua ‘coluna vertebral’ (as camadas iniciais) e adicionar novos neurônios finais para classes personalizadas.

  3. Selecionando o Backend de Execução: O TensorFlow.js pode usar diferentes "backends" de computação:

    • WebGL (GPU do navegador) – geralmente é o padrão para gráficos e é mais rápido para modelos grandes.
    • WASM (WebAssembly) – executa no processador, pode ser mais eficiente para modelos menores ou em dispositivos onde WebGL não está disponível.
    • CPU puro – fallback caso nenhum dos outros esteja disponível (o mais lento).
      Por padrão, o TF.js tenta usar o WebGL. Se você quiser forçar algum deles, pode fazer, por exemplo:
    await tf.setBackend('webgl'); // força o uso da GPU no navegador // ou await tf.setBackend('wasm'); // força o WebAssembly (CPU otimizado)

    Em geral, deixe o TensorFlow.js escolher automaticamente (que já privilegia WebGL). Mas é útil saber que essa opção existe para maximizar desempenho em diferentes cenários.

Construindo o Classificador com Transferência

Agora que temos o modelo base no navegador, o próximo passo é definir e treinar nosso classificador customizado. O fluxo geral é:

  1. Obter os dados de entrada (imagens). Pode ser via <input type="file">, captura da webcam ou imagens predefinidas. O importante é termos imagens de cada classe que queremos distinguir.
  2. Pré-processar as imagens. Converter as imagens em tensores (tf.Tensor) e ajustar seu tamanho/formato para o modelo. O MobileNet padrão espera entradas de tamanho 224x224 e valores normalizados.
  3. Extrair características com o MobileNet. Em vez de usar a saída final (1000 classes) do MobileNet, vamos pegar a saída de uma camada intermediária ou usar o método infer. Isso nos dá um resumo (embedding) útil da imagem.
  4. Definir o novo modelo classificatório. Crie uma pequena rede neural (geralmente algumas camadas densas) com saída igual ao número de classes que você quer.
  5. Treinar o novo modelo no navegador. Use os embeddings das imagens como entrada e as suas classes como rótulos.
  6. Fazer previsões em tempo real. Após o treinamento, use o modelo combinado (ou só a parte classificadora sobre as features) para classificar novas imagens.

Vamos ver cada um desses passos com exemplos de código.

Capturando e Pré-Processando as Imagens

Para treinar nós precisamos de exemplos rotulados. Suponha que queremos classificar imagens em duas categorias: “Gatos” e “Cachorros”. Podemos, por exemplo, pedir ao usuário para fazer upload de algumas fotos ou usar a webcam.

Suponha que temos um elemento <img> em nosso HTML onde mostraremos a imagem carregada:

<input type="file" id="image-selector" accept="image/*"> <img id="selected-image" />

Em JavaScript, podemos ler esse arquivo e exibir:

const imageSelector = document.getElementById('image-selector'); const selectedImage = document.getElementById('selected-image'); imageSelector.addEventListener('change', (event) => { const file = event.target.files[0]; const reader = new FileReader(); reader.onload = () => { selectedImage.src = reader.result; }; reader.readAsDataURL(file); });

Assim que a imagem aparece no <img>, podemos transformá-la em tensor para o TF.js:

async function processImageElement(imgElement) { // Converte o elemento <img> em Tensor (224x224 e normaliza) const tensor = tf.browser.fromPixels(imgElement) .resizeNearestNeighbor([224, 224]) // redimensiona .toFloat() .div(tf.scalar(255.0)) // normaliza para [0,1] .expandDims(); // adiciona dimensão batch [1, 224, 224, 3] return tensor; }

O passo tf.browser.fromPixels(img) lê os pixels do elemento de imagem. Em seguida, redimensionamos para 224x224 (MobileNet standard) e normalizamos dividindo por 255. Por fim, expandDims() adiciona a dimensão do lote (batch) no início, ficando [1, 224, 224, 3], como o modelo espera. Esses tensores carecem de limpeza automática de memória; por isso, é uma boa prática envolver operações temporárias dentro de tf.tidy() ou chamar .dispose() quando não for mais usar.

Extraindo Características com o MobileNet

Com a imagem processada em um tensor, vamos passar pelo MobileNet para obter as características de nível alto. Para fazer transferência de aprendizado, queremos as saídas de uma camada interna (ou usar o método infer). O modelo carregado pelo mobilenet.load() fornece uma API fácil:

// Supondo mobileNet já carregado como mostrado acima const features = mobileNet.infer(processedTensor, true);

Nesse código, infer(input, true) gera o “embedding” (vetor de características). Esse vetor é um tensor flat (por exemplo, de dimensão 1024 para MobileNetV1 com alpha=1). Essas características capturam informações visuais relevantes da imagem, mas não englobam as classes originais do MobileNet. Agora, podemos usar esses features como entrada do nosso modelo final.

Como alternativa, poderíamos criar um modelo tf.Model cortando o MobileNet em uma camada anterior e usando model.predict(). O método infer(img, true) é prático porque já retorna diretamente o embedding. Anchamos esses recursos junto com os rótulos das classes (por exemplo, 0 para gato, 1 para cachorro).

Definindo e Treinando o Modelo Personalizado

Agora vamos construir um novo modelo sequencial para classificar esses embeddings em nossas classes. Por exemplo, para duas classes podemos fazer:

// Número de classes do nosso problema (exemplo: gato, cachorro) const numClasses = 2; // Criamos um modelo simples de classificação const classifier = tf.sequential(); // Achata o embedding de [1, 1, 1024] (exemplo de mobilenet) para [1024] classifier.add(tf.layers.flatten({inputShape: features.shape.slice(1)})); // Camada densa intermediária classifier.add(tf.layers.dense({units: 128, activation: 'relu'})); // Saída com softmax para numClasses classes classifier.add(tf.layers.dense({units: numClasses, activation: 'softmax'})); // Compilamos o modelo com otimizador e função de perda classifier.compile({ optimizer: tf.train.adam(), loss: 'categoricalCrossentropy', metrics: ['accuracy'] });

Aqui usamos features.shape.slice(1) para pegar a forma do tensor de características sem a dimensão de lote. Supondo que features tenha forma [1, D], o flatten pode nem ser necessário, mas para generalidade colocamos; se fosse saída 4D, ele achata tudo. Em seguida, adicionamos camadas densas: a primeira com 128 neurônios (pode ajustar conforme o problema), e a última com numClasses neurônios de ativação softmax. Configuramos o otimizador (Adam) e a função de perda apropriada para classificação categórica.

Agora devemos coletar um conjunto de exemplos de entrada (os features) e seus rótulos para treinar. Exemplos:

// Suponha que temos as tensores de features e um array de rótulos correspondentes: const featureTensors = [featuresCat1, featuresCat2, /* ... */]; const labels = [0, 0, /* ... */, 1, 1, /* ... */]; // 0=gato, 1=cachorro // Empilhamos os tensores de features em um só tensor [numExemplos, D] const xTrain = tf.stack(featureTensors); // Criamos tensor de rótulos one-hot de forma [numExemplos, numClasses] const yTrain = tf.oneHot(tf.tensor1d(labels, 'int32'), numClasses); // Treina por algumas épocas await classifier.fit(xTrain, yTrain, { epochs: 10, batchSize: 16, callbacks: { onEpochEnd: (epoch, logs) => { console.log(`Época ${epoch+1}: perda=${logs.loss.toFixed(3)}, acurácia=${(logs.acc*100).toFixed(1)}%`); } } });

Em cada época, o TensorFlow.js ajusta os pesos da nossa rede de classificação. Note que não estamos atualizando o MobileNet — apenas o classifier. O MobileNet serve como extrator de recursos fixo. Você também pode optar por descongelar algumas camadas finais do MobileNet para um refinamento extra (chamado fine-tuning), mas isso exige mais dados e tempo.

Fazendo Previsões

Depois de treinado, podemos usar o modelo combinado na prática. Dado uma nova imagem (como um <img> ou frame de vídeo), fazemos algo semelhante: pré-processar, extrair recursos e usar o classificador para prever a classe.

// Suponha que a imagem esteja em <img id="selected-image"> const imgElement = document.getElementById('selected-image'); const imgTensor = await processImageElement(imgElement); const featuresNew = mobileNet.infer(imgTensor, true); const prediction = classifier.predict(featuresNew); const probabilities = prediction.dataSync(); // array de probabilidades console.log(`Probabilidade [Gato, Cachorro]: (${probabilities[0]}, ${probabilities[1]})`);

Isso dará algo como [0.95, 0.05] indicando 95% “gato” e 5% “cachorro”. Lembre-se de executar previsões dentro de tf.tidy() ou descartar tensores para não vazar memória:

tf.tidy(() => { const imgTensor = processImageElement(imgElement); const featuresNew = mobileNet.infer(imgTensor, true); const prediction = classifier.predict(featuresNew); // ... use prediction ... });

Otimizações de Performance e Boas Práticas

Rodar aprendizado de máquina no navegador exige alguns cuidados para manter tudo ágil e enxuto. Aqui vão algumas dicas essenciais:

  • Selecione o backend corretamente: Use WebGL (GPU) quando possível, pois ele acelera a maioria dos modelos pesados. O TF.js já tenta usar WebGL por padrão. Caso queira dar preferência ao WASM (CPU acelerado), faça tf.setBackend('wasm').
  • Use tf.tidy(): No JavaScript, os tensores não são descartados automaticamente. Por isso, todo cálculo intermediário deve ficar dentro de um tf.tidy(() => { ... }). Isso garante que, ao sair do bloco, os tensores intermediários sejam liberados, evitando estouro de memória (leaks).
  • Dispose de tensores antigos: Se salvou tensores em variáveis globais e não precisa mais, chame tensor.dispose(). Isso é importante, por exemplo, quando sobrepõe uma imagem antiga por outra.
  • Pré-processamento eficiente: Movimente operações de pré-processamento (resize, normalização) para Web Workers ou OffscreenCanvas se estiver fazendo em vídeo, para não travar a interface. Agrupe etapas de pré-processamento em uma só operação (fusão) sempre que possível.
  • Reduza a resolução se preciso: Embora MobileNet exija 224x224, para aplicações em tempo real teste tamanhos menores (ex: 160x160) para acelerar, sacrificando um pouco de acurácia.
  • Limite a frequência de inferências: Se estiver processando webcam, não é preciso executar classificação em cada quadro de vídeo. Pode pular frames (por exemplo, processar 10 vezes por segundo em vez de 30). Use requestAnimationFrame ou setInterval com throttling para manter a UI suave.
  • Quantidade de dados e batch: Use batches pequenos se o dispositivo for fraco, e poucos exemplos por época. Demasiados dados podem deixar o navegador lento.
  • Use modelos comprimidos se disponíveis: É possível quantizar os pesos (por exemplo, para FP16 ou int8) ao converter seu modelo para TF.js, diminuindo tamanho e ganhando velocidade sem grande perda de precisão. Ferramentas como tfjs-converter oferecem opções de otimização.

Essas práticas ajudam a manter a aplicação rápida e responsiva. Lembre-se: cada milissegundo conta na experiência do usuário. Em resumo, trate memória e processamento como recursos limitados: libere o que não usa, evite operações redundantes e escolha arquiteturas enxutas (MobileNet já é leve por design).

Exemplo Prático de Código

Vamos juntar tudo em um fluxograma simples de código, ilustrando as principais etapas em ordem:

// 1) Carregar TensorFlow.js e MobileNet await tf.setBackend('webgl'); // usa GPU const mobileNet = await mobilenet.load(); console.log('MobileNet pronto'); // 2) Capturar/preparar dados (exemplo: a partir de <img>) const imgElement = document.getElementById('selected-image'); // Suponha que a imagem já foi carregada pelo usuário const imgTensor = tf.browser.fromPixels(imgElement) .resizeNearestNeighbor([224,224]).toFloat().div(tf.scalar(255)).expandDims(); // 3) Extrair características via MobileNet (embedding) const features = mobileNet.infer(imgTensor, true); // tensor de características // 4) Montar o modelo de classificação (suponha 3 classes) const numClasses = 3; const classifier = tf.sequential(); classifier.add(tf.layers.flatten({inputShape: features.shape.slice(1)})); classifier.add(tf.layers.dense({units: 100, activation: 'relu'})); classifier.add(tf.layers.dense({units: numClasses, activation: 'softmax'})); classifier.compile({optimizer: 'adam', loss: 'categoricalCrossentropy'}); // 5) Preparar os dados de treinamento (features + labels one-hot) // (Aqui deveríamos coletar vários 'features' e seus 'labels' antes) const featuresDataset = [features1, features2, /* ... */]; const labelsArray = [0, 1, /* ... */]; const xTrain = tf.stack(featuresDataset); // [N, D] const yTrain = tf.oneHot(tf.tensor1d(labelsArray,'int32'), numClasses); // [N,3] // 6) Treinar o modelo no navegador await classifier.fit(xTrain, yTrain, { epochs: 5, batchSize: 8, callbacks: { onEpochEnd: (epoch, logs) => { console.log(`Época ${epoch+1}: perda=${logs.loss.toFixed(4)}, acurácia=${(logs.acc*100).toFixed(1)}%`); } } }); // 7) Inferência: usar o modelo treinado para classificar novas imagens const newImgTensor = tf.browser.fromPixels(newImageElement) .resizeNearestNeighbor([224,224]).toFloat().div(tf.scalar(255)).expandDims(); const newFeatures = mobileNet.infer(newImgTensor, true); const prediction = classifier.predict(newFeatures); const probabilities = Array.from(prediction.dataSync()); console.log('Probabilidades:', probabilities); // lista de probabilidades por classe

Neste trecho, mostramos o fluxo básico. Em uma aplicação real, você teria funções separadas para carregar dados, exibir a interface de usuário, controlar o início/parada do treinamento e assim por diante. Mas o ponto é: tudo roda única e exclusivamente no navegador, sem backend.

Conclusão

Neste artigo vimos como construir um classificador de imagens personalizado no navegador usando TensorFlow.js e Transfer Learning. Começamos entendendo que podemos aproveitar modelos pré-treinados (como MobileNet) para reduzir drasticamente o esforço de treinamento. Mostramos como configurar o ambiente web, carregar o modelo MobileNet e extrair características relevantes das imagens. Em seguida, criamos e treinamos um pequeno modelo final para nossas classes específicas, tudo isso no próprio front-end. Por fim, discutimos diversas otimizações (uso de WebGL, gerenciamento de memória, pré-processamento eficiente) essenciais para rodar modelos de deep learning em tempo real no browser.

Como próximos passos, você pode explorar algumas direções:

  • Colete seus próprios dados: experimente treinar um classificador para objetos de seu interesse. Por exemplo, classificar tipos de frutas, colocar/conferir etiquetas (tags) em fotos, ou até reconhecer gestos via webcam.
  • Melhore a arquitetura: experimente outras bases pré-treinadas (EfficientNet-Lite, MobileNetV2/V3, etc) para ver diferenças de acurácia e desempenho. Ou adicione mais camadas densas follando o modelo.
  • Integre com UI: implemente uma interface rica (pode usar React/Vue/etc) para que o usuário possa ver a webcam, dar upload de fotos, e visualizar as predições instantaneamente.
  • Performance extra: teste o uso de Web Workers para pré-processamento em paralelo, e fique de olho nas novidades como o WebGPU (que promete aceleradores ainda melhores para ML).
  • Salve seu modelo: utilize classifier.save('localstorage://my-model') ou .save('downloads://') para que o usuário possa baixar/armazenar o modelo treinado e reutilizá-lo depois sem treinar tudo de novo.

A capacidade de treinar modelos diretamente no navegador abre um leque de oportunidades incríveis de aplicações de IA no front-end. Com a combinação de TensorFlow.js e Transfer Learning, é possível democratizar o uso de modelos de deep learning, tornando-os acessíveis a qualquer desenvolvedor web. Experimente, brinque e inove — a inteligência artificial na web está ao alcance de todos!

Inscrever agora para a próxima turma do DevClub?

Que tal não perder esta oportunidade e já se inscrever agora para a próxima turma do DevClub?