Frameworki do machine learningu w JavaScript: TensorFlow.js, ml5 i konkurencja

0
86
4.5/5 - (2 votes)

Nawigacja:

Dlaczego machine learning w JavaScript w ogóle ma sens

Przesunięcie z serwera do przeglądarki i na urządzenie

Uczenie maszynowe przez lata kojarzyło się z ciężkimi modelami w Pythonie, trenowanymi na serwerach z potężnymi GPU. JavaScript w tym świecie był co najwyżej „warstwą widoku” – HTML, CSS, trochę interakcji w przeglądarce. Pojawienie się TensorFlow.js i podobnych bibliotek zmieniło ten układ sił: modele mogą działać bezpośrednio w przeglądarce użytkownika lub w środowisku Node.js, bez konieczności wysyłania danych do zewnętrznego API.

Taka zmiana paradygmatu nie oznacza, że Python przestaje być potrzebny. Raczej powstaje nowa warstwa: ML bliżej użytkownika. Część zadań – zwłaszcza predykcja (inferencja) – może zostać przeniesiona do frontendu, a ciężki trening i tak odbywa się w ekosystemie Python/TensorFlow, PyTorch czy scikit-learn. JavaScript staje się klejem między interfejsem użytkownika, logiką biznesową i modelem.

W praktyce oznacza to, że ten sam model, który trenował ktoś w Pythonie i zapisał w formacie obsługiwanym przez TensorFlow.js lub ONNX, można potem wczytać w przeglądarce, podpiąć pod formularz, kamerę lub mikrofon i uzyskać natychmiastową odpowiedź – lokalnie, bez serwera ML.

Korzyści: prywatność, opóźnienia, UX

Najważniejsza zaleta machine learningu w przeglądarce to brak konieczności wysyłania wrażliwych danych na serwer. Gdy model działa lokalnie, surowe dane (obraz z kamery, tekst, nagranie audio) nie opuszczają urządzenia użytkownika. To kluczowe przy:

  • analizie obrazu twarzy (wyrażenia, spojrzenie, obecność osoby),
  • transkrypcji lub klasyfikacji krótkich nagrań głosowych,
  • przetwarzaniu formularzy z danymi osobowymi, zanim zostaną wysłane na serwer.

Kolejny element to opóźnienia. Każde żądanie do API ML w chmurze oznacza podróż pakietów przez sieć. Nawet przy szybkim łączu odczuwalne jest to w interaktywnych aplikacjach, np. przy analizie obrazu z kamery 30 razy na sekundę. Model działający lokalnie może odpowiadać w kilka–kilkanaście milisekund, co przekłada się na płynne doświadczenie użytkownika.

Do tego dochodzi niezależność od łączności. Aplikacja ML w JavaScript może działać offline, jeśli model i kod zostały wcześniej pobrane. Wystarczy cache przeglądarki lub Service Worker. To realna przewaga nad podejściem „czysty frontend + REST do Pythona”, które bez sieci przestaje istnieć.

JavaScript jako język środowiska uruchomieniowego

JavaScript nie jest już wyłącznie językiem przeglądarki. Działa w Node.js, Deno, Cloudflare Workers, AWS Lambda@Edge, a także w aplikacjach hybrydowych (Electron, React Native). Frameworki machine learningowe w JS mogą zatem działać w:

  • przeglądarce – typowe use case’y: interaktywne aplikacje webowe, wizualizacje, gry, kreatywne narzędzia,
  • Node.js – backend, skrypty batchowe, narzędzia CLI, przetwarzanie danych,
  • edge functions – logika ML uruchamiana „na krawędzi” sieci CDN, bliżej użytkownika niż centralny serwer.

Dzięki temu biblioteki takie jak TensorFlow.js czy ONNX Runtime Web mogą spinać całą architekturę: model jest trenowany w Pythonie, eksportowany do formatu wspieranego przez JS, a potem ładowany w przeglądarce, na serwerze Node lub w funkcji serverless – zależnie od potrzeb.

Typowe scenariusze użycia ML po stronie JS

Frameworki do uczenia maszynowego w JavaScript najczęściej pojawiają się w lekkich i interaktywnych zastosowaniach. Kilka realnych przykładów:

  • Interaktywne strony edukacyjne – użytkownik przeciąga punkty na wykresie, a w tle prosty model regresji aktualizuje linię trendu w czasie rzeczywistym.
  • Aplikacje kreatywne – detekcja pozycji ciała (PoseNet, MoveNet) do sterowania muzyką lub wizualizacjami przy pomocy gestów.
  • Leady i scoring w formularzach – model w JavaScript ocenia prawdopodobieństwo, że dany użytkownik zostanie klientem i dynamicznie dopasowuje dodatkowe pytania.
  • Lekkie asystenty – klasyfikacja krótkich komend tekstowych lub prostych intencji użytkownika bez integracji z „ciężkim” backendem NLP.

W wielu takich przypadkach pełny stack Pythonowy byłby przesadą – wymaga osobnej infrastruktury, utrzymania API i skalowania. Mały model w przeglądarce rozwiązuje problem w prostszy, tańszy sposób.

Naturalne ograniczenia podejścia „ML w JS”

Maszyna użytkownika to nie jest dedykowany serwer z GPU. Frameworki takie jak TensorFlow.js czy ml5 działają w środowisku, w którym:

  • brakuje gwarantowanej mocy obliczeniowej – stary telefon będzie miał gigantyczne lagi,
  • przeglądarka ogranicza dostęp do sprzętu – brak pełnej kontroli nad GPU, brak sterowników klasy CUDA,
  • rozmiar modeli ma znaczenie – duży model to dłuższe pobieranie, większe zużycie RAM i baterii,
  • praca długo trwających zadań może być ubijana lub „przycinana” przez system (szczególnie na mobile).

Dlatego machine learning w JavaScript nadaje się bardziej do inferencji lekkich modeli i drobnego dostrajania (transfer learning) niż do trenowania od zera wielkich sieci. Ciężki trening zwykle lepiej pozostawić Pythonowi i serwerom GPU, a do przeglądarki dostarczyć jedynie zoptymalizowany, skompresowany model.

Ekran komputera z kodem JavaScript i menu akcji sztucznej inteligencji
Źródło: Pexels | Autor: Daniil Komov

Przegląd ekosystemu ML w JavaScript – mapa narzędzi

Ciężkie frameworki vs przyjazne biblioteki

Ekosystem machine learningu w JavaScript można z grubsza podzielić na dwa nurty:

  • „Ciężkie” frameworki – niskopoziomowe silniki obliczeń tensorowych, często zbliżone do TensorFlow:
    • TensorFlow.js – najpopularniejszy, z bogatym ekosystemem modeli,
    • ONNX Runtime Web – silnik do uruchamiania modeli w formacie ONNX,
    • WebDNN, WebNN (w fazie standaryzacji) – bardziej eksperymentalne kierunki.
  • „Przyjazne” biblioteki – wysokopoziomowe API, prostsze w użyciu:
    • ml5.js – edukacyjne, oparte na TensorFlow.js,
    • Brain.js – proste sieci neuronowe i algorytmy klasyfikacji w JS,
    • Synaptic (starsza biblioteka) – klasyczne sieci neuronowe.

Ciężkie frameworki dają większą elastyczność, lepszą wydajność, dostęp do backendów GPU (WebGL, WebGPU, Node). Przyjazne biblioteki skracają czas między pomysłem a działającym prototypem, ukrywając detale typu kształty tensorów, optymalizatory czy funkcje straty.

Inferencja vs trenowanie w przeglądarce

Trzeba jasno rozdzielić dwa etapy pracy z modelami ML:

  • Trenowanie (training) – model uczy się na danych, aktualizuje wagi, wymaga wielu iteracji,
  • Inferencja (prediction) – model już wytrenowany przyjmuje dane i zwraca wynik.

JavaScript radzi sobie przyzwoicie z inferencją lekkich modeli i niewielkim dostrajaniem (np. ostatnich warstw przy transfer learningu). Trenowanie od zera dużych sieci jest możliwe, ale niepraktyczne – ograniczenia sprzętowe szybko dają o sobie znać.

Większość sensownych przepływów pracy wygląda tak:

  1. Trenowanie pełnego modelu w Pythonie (TensorFlow, PyTorch, scikit-learn).
  2. Eksport do formatu kompatybilnego z TensorFlow.js lub ONNX.
  3. Załadowanie modelu w przeglądarce/Node.js i wykorzystanie do predykcji.
  4. Ewentualne lekkie dostrojenie (transfer learning) na danych użytkownika, już w JS.

Biblioteki takie jak ml5.js w ogóle nie koncentrują się na ciężkim treningu – dają za to bardzo prostą obsługę inferencji gotowych modeli, co w większości aplikacji webowych jest absolutnie wystarczające.

Warstwa niskopoziomowa i wysokopoziomowe API

Frameworki ML w JS działają zwykle w dwóch warstwach:

  • Silnik obliczeniowy – implementacja operacji na tensorach, optymalizacja pod WebGL, WebGPU, CPU lub natywne biblioteki (Node.js). Przykład: @tensorflow/tfjs-core, ONNX Runtime Web.
  • Warstwa modelowania – API, które pozwala definiować, ładować i uruchamiać modele. Tu wchodzą:
    • tf.layers i tf.Sequential w TensorFlow.js,
    • tf.loadLayersModel, tf.loadGraphModel,
    • API ml5.js: ml5.imageClassifier, ml5.poseNet, ml5.neuralNetwork.

Taka architektura daje dużo elastyczności. Można korzystać z wysokopoziomowego API (np. ml5) do szybkich prototypów, a gdy potrzebna jest większa kontrola nad modelami i optymalizacją, zejść do „gołego” TensorFlow.js lub nawet bezpośrednio do warstwy tfjs-core.

Jak JS-owe biblioteki dogadują się z Pythonem i formatami modeli

JavaScript nie zastępuje Pythona jako języka do trenowania złożonych modeli. Raczej uzupełnia go o warstwę uruchamiania modeli w nowych środowiskach. Wygląda to tak:

  • TensorFlow.js:
    • obsługuje modele zapisane jako tf.SavedModel lub HDF5 (Keras), po konwersji przy pomocy tensorflowjs_converter,
    • pozwala też tworzyć modele od zera w JS (tf.sequential, tf.model), choć do poważnego treningu rzadko to jest optymalny wybór.
  • ONNX Runtime Web:
    • uruchamia modele w formacie ONNX, który potrafią wyeksportować m.in. PyTorch, scikit-learn (przez skl2onnx),
    • umożliwia więc przenoszenie szerokiej gamy modeli do przeglądarki bez przywiązania do TensorFlow.

Dzięki temu większość poważnego ML pozostaje tam, gdzie ma najlepsze narzędzia (Python, GPU), a JavaScript odgrywa rolę „platformy wykonawczej” i integratora z UI oraz logiką biznesową.

Kiedy lepiej zostać przy podejściu serwerowym

Nie każdy projekt skorzysta na przenoszeniu ML do przeglądarki. Sensowniej pozostać przy klasycznym podejściu serwerowym, gdy:

  • model jest duży i ciężki obliczeniowo (np. duże modele językowe, rozbudowane sieci CNN do obrazów wysokiej rozdzielczości),
  • trzeba wykonać wiele predykcji jednocześnie (np. batch scoring tysięcy rekordów),
  • chcesz centralnie kontrolować wersje modeli, monitorować ich jakość i logować każde wywołanie,
  • masz silne wymagania regulacyjne lub bezpieczeństwa – wtedy preferowany jest backend, który można w pełni kontrolować i audytować.

Dobry kompromis to architektura hybrydowa: proste modele i część logiki decyzyjnej w JS (np. weryfikacja danych wejściowych, prosta klasyfikacja), a ciężkie zadania i tak w Pythonie, z dostępem przez API.

Podstawy TensorFlow.js – architektura, tryby działania, środowiska

Czym właściwie jest TensorFlow.js

TensorFlow.js to port ekosystemu TensorFlow do JavaScriptu, ale jednocześnie samodzielny silnik obliczeń tensorowych. Oferuje:

  • warstwę niskopoziomową @tensorflow/tfjs-core – operacje na tensorach, gradienty, optymalizatory,
  • warstwę wysokopoziomową @tensorflow/tfjs – definicja modeli, ładowanie, trenowanie, zapisywanie,
  • specjalne buildy i backendy dla przeglądarki i Node.js.

Pod spodem TensorFlow.js używa różnych backendów obliczeniowych, które można dynamicznie przełączać. Dzięki temu ten sam kod może działać na różnych urządzeniach, korzystając z dostępnego sprzętu: CPU, GPU przez WebGL, a w przyszłości coraz częściej WebGPU.

Tryby użycia: od tfjs-core do gotowych modeli

TensorFlow.js można używać na kilku poziomach szczegółowości:

  • tfjs-core – najniższy poziom. Bezpośrednia praca na tensorach:
    const a = tf.tensor([1, 2, 3]);
    const b = tf.tensor([4, 5, 6]);
    const c = a.add(b);

    Ten poziom jest przydatny, jeśli tworzysz własne operacje lub potrzebujesz maksymalnej kontroli.

  • Modele warstwowe (Layers API) i sekwencyjne

    Na co dzień najwygodniej korzysta się z warstwy tf.layers, która przypomina Keras z Pythona. Pozwala opisać model krok po kroku, bez ręcznego liczenia gradientów. Do wyboru są dwa główne style:

  • tf.sequential() – model ułożony warstwa po warstwie „w linii prostej”,
  • tf.model() – bardziej elastyczne podejście funkcyjne, przydatne przy skomplikowanych architekturach (np. kilka wejść, kilka wyjść).

Dla klasycznych zadań, jak przewidywanie liczby czy prosta klasyfikacja, w zupełności wystarcza model sekwencyjny. Definicja modelu przypomina składanie klocków:

const model = tf.sequential();

model.add(tf.layers.dense({
  units: 16,
  activation: 'relu',
  inputShape: [3]   // np. 3 cechy wejściowe
}));

model.add(tf.layers.dense({
  units: 1,
  activation: 'linear'
}));

model.compile({
  optimizer: tf.train.adam(0.01),
  loss: 'meanSquaredError'
});

Najpierw powstają warstwy, potem model jest „kompilowany” – czyli TensorFlow.js przygotowuje wszystko do trenowania: dobiera optymalizator, funkcję straty i metryki. Ta część jest bardzo podobna do tego, co robi się w Kerasie w Pythonie.

Backendy obliczeniowe: CPU, WebGL, WebGPU i Node.js

TensorFlow.js ma kilka „silników pod maską”. Twój kod wysokopoziomowy się nie zmienia, zmienia się tylko to, gdzie faktycznie wykonują się obliczenia.

  • tfjs-backend-cpu – czysty JavaScript, działa wszędzie, ale jest najwolniejszy.
  • tfjs-backend-webgl – używa WebGL jako „substytutu” GPU, zwykle dużo szybciej niż CPU, szczególnie przy większych tensorach.
  • tfjs-backend-webgpu – nowsze podejście oparte na WebGPU (tam, gdzie jest dostępne), cel: bardziej nowoczesny dostęp do GPU niż WebGL.
  • tfjs-node / tfjs-node-gpu – backendy dla Node.js:
    • tfjs-node używa natywnych bibliotek CPU (libtensorflow),
    • tfjs-node-gpu potrafi korzystać z CUDA na maszynach z kartą NVIDII.

Backend można zmieniać dynamicznie, co bywa przydatne przy testach lub gdy chcesz wymusić określony tryb pracy:

import * as tf from '@tensorflow/tfjs';
import '@tensorflow/tfjs-backend-webgl';

await tf.setBackend('webgl');
await tf.ready();

console.log(tf.getBackend()); // 'webgl'

W przeglądarce TensorFlow.js sam próbuje wybrać najlepszy backend, ale w projektach produkcyjnych często opłaca się jawnie wymusić WebGL/WebGPU i sprawdzić, czy wszystko działa poprawnie na docelowych urządzeniach.

Zarządzanie pamięcią: tf.tidy i ręczne sprzątanie

Javascriptowy garbage collector nie widzi pamięci przydzielanej na GPU przez TensorFlow.js. Tą częścią trzeba zarządzać samodzielnie. Gdy o tym zapomnisz, po kilku minutach intensywnych obliczeń aplikacja zacznie „puchnąć”, aż w końcu przeglądarka się podda.

Na szczęście większość przypadków pokrywa funkcja tf.tidy. W środku bloku tworzone są tensory, które po zakończeniu zostaną automatycznie zwolnione (z wyjątkiem tych, które jawnie zwrócisz):

const result = tf.tidy(() => {
  const a = tf.randomNormal([1000, 1000]);
  const b = tf.randomNormal([1000, 1000]);
  const c = a.matMul(b);
  return c; // tylko 'c' przetrwa poza tidy
});

// a i b zostały już zwolnione

Do tego dochodzi ręczne zwalnianie pojedynczych tensorów przez dispose(). Przy inferencji gotowych modeli wystarczy otoczyć obliczenia tf.tidy, aby skutecznie unikać wycieków pamięci.

Środowiska uruchomieniowe: przeglądarka vs Node.js

Praca z TensorFlow.js w przeglądarce i w Node.js trochę się różni, choć podstawowa składnia pozostaje ta sama.

  • Przeglądarka:
    • ładowanie modeli i danych z URL (HTTP, CDN, lokalne pliki statyczne),
    • integracja z DOM i Canvas (np. przechwytywanie obrazu z kamery),
    • ograniczenia czasu wykonywania i niższa moc obliczeniowa.
  • Node.js:
    • łatwy dostęp do systemu plików (file://), baz danych, kolejek,
    • możliwość korzystania z backendu natywnego (tfjs-node, tfjs-node-gpu),
    • lepsza kontrola nad zasobami, brak ograniczeń sandboxu przeglądarki.

Typowy scenariusz: Node.js używa TensorFlow.js po stronie serwera do zadań, które muszą być w JS (np. integracja z istniejącą platformą), a przeglądarka zajmuje się lżejszą inferencją lub personalizacją modelu pod danego użytkownika.

Zbliżenie ekranu z kodem JavaScript i menu debugowania wspieranym przez AI
Źródło: Pexels | Autor: Daniil Komov

Pierwsze kroki z TensorFlow.js – prosty przypadek użycia krok po kroku

Przykład: predykcja „ceny” na podstawie jednej cechy

Najprostsze, a zarazem pouczające ćwiczenie to nauczenie sieci przewidywania liczby na podstawie innej liczby. Można to traktować jak „inteligentną linię prostą”: model ma odgadnąć zależność typu y = 2x + 1, ale poznaje ją jedynie na przykładach.

Podstawowe kroki:

  1. Przygotowanie danych wejściowych i wyjściowych.
  2. Definicja struktury modelu.
  3. Kompilacja modelu (optimizer, loss).
  4. Trenowanie (fit).
  5. Inferencja – użycie modelu do predykcji.

Przygotowanie środowiska w przeglądarce

Najprościej skorzystać z gotowego pakietu z CDN. W pliku HTML wystarczy dodać skrypt:

<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest"></script>
<script>
// kod TensorFlow.js trafi tutaj
</script>

W projektach produkcyjnych lepszym wyjściem jest instalacja przez npm i bundler (np. Vite, Webpack), ale do nauki CDN sprawdza się znakomicie.

Krok 1: dane treningowe jako tensory

Dane można potraktować jak zwykłe tablice JS, które następnie zamienia się na tensory. Załóżmy, że zależność jest mniej więcej liniowa: im większa wartość wejściowa, tym większa wyjściowa.

const xs = tf.tensor1d([1, 2, 3, 4, 5]);
const ys = tf.tensor1d([3, 5, 7, 9, 11]);
// docelowo chcemy, żeby model nauczył się zależności zbliżonej do y = 2x + 1

Tensor 1D to po prostu wektor. Przy bardziej złożonych problemach (obrazy, sekwencje) potrzebne będą tensory 2D, 3D i wyższych wymiarów, ale idea jest ta sama – tablica liczb zmienia się w obiekt, z którym TensorFlow.js potrafi pracować na GPU.

Krok 2: definicja i kompilacja modelu

Model będzie miał jedno wejście i jedno wyjście, a w środku prostą warstwę w pełni połączoną (dense). Dla zadania regresji nie potrzebujemy złożonej architektury.

const model = tf.sequential();
model.add(tf.layers.dense({
  units: 1,
  inputShape: [1]  // pojedyncza liczba na wejściu
}));

model.compile({
  optimizer: tf.train.sgd(0.1), // prosty gradient prosty (SGD)
  loss: 'meanSquaredError'
});

Optymalizator decyduje, jak model będzie aktualizował wagi. SGD (stochastic gradient descent) to klasyk; do wielu zadań używa się też adam, który zwykle szybciej zbiega.

Krok 3: trenowanie modelu

Trening polega na wielokrotnym „przepuszczeniu” danych przez sieć i poprawianiu wag. W API model.fit przyjmuje tensory wejściowe i wyjściowe, liczbę epok (pełnych przejść przez dane) i rozmiar paczki (batchSize).

async function trainModel() {
  const history = await model.fit(xs, ys, {
    epochs: 100,
    batchSize: 5,
    callbacks: {
      onEpochEnd: (epoch, logs) => {
        console.log(`Epoka ${epoch}: loss = ${logs.loss.toFixed(4)}`);
      }
    }
  });
  return history;
}

trainModel().then(() => {
  console.log('Trening zakończony');
});

Callback onEpochEnd daje szybki podgląd, czy błąd (loss) faktycznie spada. To prosta forma „telemetrii” podczas nauki modelu.

Krok 4: użycie modelu do predykcji

Po treningu można wykorzystać model do przewidywania wartości, których wcześniej nie widział. Predykcja zwraca tensory, więc po obliczeniach trzeba je zamienić na „zwykłe” liczby lub tablice.

function predictValue(x) {
  return tf.tidy(() => {
    const input = tf.tensor1d([x]);
    const output = model.predict(input);
    const value = output.dataSync()[0]; // pobranie pierwszej wartości
    return value;
  });
}

const pred = predictValue(6);
console.log('Predykcja dla x=6:', pred);

W idealnym świecie model powinien zwrócić wartość bliską 13 (bo 2×6 + 1). Niewielkie różnice to wynik przybliżeń numerycznych i losowej inicjalizacji wag.

Wizualizacja postępów uczenia

Nawet przy prostych modelach pomocna bywa wizualizacja, choćby jako wykres błędu w czasie. Na froncie łatwo połączyć TensorFlow.js z bibliotekami typu Chart.js lub D3. Jeden z popularnych trików to puszczanie treningu w krótkich seriach epok i po każdej serii odświeżanie wykresu:

async function trainInChunks() {
  for (let i = 0; i < 10; i++) {
    const history = await model.fit(xs, ys, { epochs: 10 });
    const lastLoss = history.history.loss.slice(-1)[0];
    updateChart(i * 10 + 10, lastLoss); // własna funkcja rysująca wykres
  }
}

Użytkownik widzi, że model faktycznie się „uczy”, zamiast patrzeć w pusty ekran przez kilka sekund.

Gotowe modele i transfer learning w TensorFlow.js

Modele z katalogu @tensorflow-models

Nad TensorFlow.js zbudowano spory katalog gotowych modeli, publikowanych jako osobne paczki npm lub dostępnych z CDN. Kilka najbardziej praktycznych przykładów:

  • @tensorflow-models/mobilenet – klasyfikacja obrazów (rozpoznawanie obiektów),
  • @tensorflow-models/coco-ssd – detekcja obiektów z zaznaczeniem ich położenia (bounding boxes),
  • @tensorflow-models/posenet i moveNet – detekcja pozycji ciała (punkty stawów),
  • @tensorflow-models/body-pix – segmentacja sylwetki (np. „wycięcie” człowieka z tła),
  • @tensorflow-models/universal-sentence-encoder – osadzanie (embeddingi) zdań dla zadań NLP.

Te modele są już wytrenowane i zoptymalizowane. Wystarczy je załadować i podać im dane wejściowe. Dla wielu aplikacji to zupełnie wystarcza – nie trzeba zaglądać w szczegóły architektury.

Przykład: klasyfikacja obrazów z mobilenet

Klasycznym przykładem jest rozpoznawanie obiektów na obrazie z kamery. Schemat użycia mobilenet wygląda następująco:

<video id="webcam" autoplay playsinline width="224" height="224"></video>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/mobilenet"></script>
<script>
  let net;
  async function loadModel() {
    net = await mobilenet.load();
    console.log('Model mobilenet załadowany');
  }

  async function classifyFrame() {
    const video = document.getElementById('webcam');
    const predictions = await net.classify(video);
    console.log(predictions);
    // predictions to tablica etykiet i prawdopodobieństw
    requestAnimationFrame(classifyFrame);
  }

  loadModel().then(() => {
    // po ustawieniu streamu z kamery:
    classifyFrame();
  });
</script>

Model przyjmuje element DOM (np. <video> lub <img>) i zwraca listę najbardziej prawdopodobnych klas. To „ML-as-a-function” dla frontendu – cała złożoność sprowadza się do jednego wywołania.

Intuicja transfer learningu

Transfer learning to ponowne wykorzystanie modelu wytrenowanego na dużym, ogólnym zbiorze (np. milionach obrazów) jako „ekstraktora cech” do nowego, węższego zadania. Zamiast trenować sieć od zera, bierzesz jej początkowe warstwy (które nauczyły się rozpoznawać ogólne wzory: krawędzie, kształty, tekstury), zamrażasz je, a na końcu dodajesz kilka swoich warstw, dopasowanych do nowego problemu.

Transfer learning w praktyce: własna klasyfikacja obrazów

Najbardziej namacalny przykład transfer learningu w TensorFlow.js to stworzenie własnego klasyfikatora obrazów na bazie mobilenet. Zamiast uczyć sieć od zera, używasz mobilenet do wydobywania cech, a na końcu dokładany jest lekki model rozróżniający twoje kategorie (np. różne typy produktów, gatunki liści, rodzaje narzędzi).

Schemat postępowania wygląda następująco:

  1. Załadowanie pretrenowanego modelu (np. mobilenet) w trybie bez ostatniej warstwy klasyfikacyjnej.
  2. Zbieranie obrazów dla swoich klas (np. poprzez kamerę w przeglądarce).
  3. Przepuszczanie tych obrazów przez mobilenet i zapisywanie wektorów cech.
  4. Trenowanie małego modelu (np. kilku warstw dense) na tych wektorach.
  5. Wykorzystanie połączonego zestawu (mobilenet + twój klasyfikator) do predykcji w czasie rzeczywistym.

Dzięki temu na frontendzie da się zbudować interaktywny „uczący się klasyfikator”, w którym użytkownik pokazuje kamerze kilka przykładów, nadaje im etykietę, a model uczy się na bieżąco rozpoznawać podobne obiekty.

Prosty pipeline transfer learningu z mobilenet

Minimalny szkic kodu może wyglądać tak (pomijając konfigurację strumienia z kamery):

// 1. Ładowanie mobilenet w trybie "feature extractor"
const mobilenetModule = await mobilenet.load({ version: 2, alpha: 1.0 });

// 2. Kontener na dane (cechy + etykiety)
const featureVectors = [];
const labels = [];

async function captureExample(label) {
  const video = document.getElementById('webcam');
  // wyciągnięcie cech z obrazu
  const activation = mobilenetModule.infer(video, true); // true = bez ostatniej warstwy
  featureVectors.push(activation);
  labels.push(label);
}

// 3. Przygotowanie prostego modelu klasyfikującego
const classifier = tf.sequential();
classifier.add(tf.layers.flatten({ inputShape: mobilenetModule.outputs[0].shape.slice(1) }));
classifier.add(tf.layers.dense({ units: 64, activation: 'relu' }));
classifier.add(tf.layers.dense({ units: NUM_CLASSES, activation: 'softmax' }));

classifier.compile({
  optimizer: tf.train.adam(0.0001),
  loss: 'categoricalCrossentropy',
  metrics: ['accuracy']
});

// 4. Trenowanie na zebranych przykładach
async function trainClassifier() {
  const xs = tf.concat(featureVectors);
  const ys = tf.oneHot(tf.tensor1d(labels, 'int32'), NUM_CLASSES);
  await classifier.fit(xs, ys, {
    epochs: 20,
    shuffle: true
  });
  xs.dispose();
  ys.dispose();
}

Kluczowy fragment to wywołanie mobilenetModule.infer(video, true). Zwracany tensor nie zawiera już końcowej warstwy klasyfikacyjnej mobilenet, tylko „surowe cechy” obrazu. Na nich uczy się nowy, mały model, który da się trenować nawet na kilkudziesięciu przykładach.

Łączenie modeli: mobilenet + własny klasyfikator

Po treningu klasyfikatora można zbudować funkcję, która łączy oba modele i zwraca etykietę w czasie rzeczywistym:

async function classifyCustom(video) {
  return tf.tidy(() => {
    const activation = mobilenetModule.infer(video, true);
    const prediction = classifier.predict(activation);
    const probs = prediction.dataSync();
    const classId = probs.indexOf(Math.max(...probs));
    return { classId, probs };
  });
}

W prawdziwej aplikacji warto zmapować classId na przyjazne nazwy klas (np. „kawa”, „herbata”), przechowywane w tablicy lub bazie po stronie użytkownika.

Przenoszenie modeli między środowiskami

TensorFlow.js pozwala ładować modele wytrenowane w innych wersjach TensorFlow (np. w Pythonie) oraz zapisywać modele stworzono w JS i wykorzystywać je gdzie indziej. To przydatne, gdy cięższe zadania treningowe wykonuje backend, a frontend tylko serwuje lekki model inferencyjny.

Główne formaty zapisów:

  • Format tfjs Layers – modele stworzone wysokopoziomowym API (tf.sequential, tf.model), zapisywane jako JSON + binarne pliki wag,
  • Graph model – modele konwertowane z TensorFlow (SavedModel, Keras H5) za pomocą tfjs-converter.

Przykładowy zapis modelu po treningu w przeglądarce:

await model.save('downloads://mój-model');

Przeglądarka ściągnie pliki model.json i group1-shard1of1.bin. Ten sam model można potem załadować np. w aplikacji Node.js:

const tf = require('@tensorflow/tfjs-node');
async function loadModel() {
  const model = await tf.loadLayersModel('file://models/mój-model/model.json');
  return model;
}

Dzięki temu raz wytrenowany model może żyć w wielu kontekstach: na serwerze, w aplikacji desktopowej Electron, a nawet jako część rozszerzenia przeglądarki.

Optymalizacja modeli pod przeglądarkę

Modele po Pythonie bywają zbyt ciężkie na potrzeby frontendu. Można je odchudzić, stosując:

  • pruning – przycinanie mało istotnych wag,
  • quantization – zamianę liczb 32-bitowych na 16 lub 8 bitów,
  • distillation – trenowanie mniejszego modelu tak, aby naśladował duży model-nauczyciela.

Część z tych technik realizuje się jeszcze po stronie TensorFlow w Pythonie, a następnie konwertuje gotowy, „odchudzony” model do formatu tfjs. Różnica potrafi być ogromna – czas ładowania zmniejsza się z kilku sekund do ułamka, co w świecie webowym decyduje o tym, czy użytkownik poczeka, czy zamknie kartę.

Zbliżenie ekranu z kolorowym kodem programistycznym
Źródło: Pexels | Autor: Pixabay

ml5.js – „machine learning dla reszty świata”

ml5.js to biblioteka zbudowana na szczycie TensorFlow.js, ale z zupełnie inną filozofią. Zamiast wystawiać całe API niskiego poziomu, serwuje prosty, przyjazny interfejs, który da się wytłumaczyć w kilku zdaniach osobie niezajmującej się programowaniem zawodowo.

Cel jest prosty: obniżyć próg wejścia. Wiele osób zaczyna przygodę z ML od kreatywnych projektów – wizualizacji, gier, instalacji artystycznych. Dla nich ważniejsze jest „czy to działa i czy mogę to połączyć z p5.js”, niż rozróżnianie optymalizatorów czy formatów wag.

Filozofia i możliwości ml5.js

Zamiast dziesiątek klas i setek metod, ml5.js oferuje wyraźnie nazwane moduły. Typowe klocki to:

  • ImageClassifier – klasyfikacja obrazów (często na bazie mobilenet),
  • PoseNet / Handpose / FaceMesh – detekcja szkieletu ciała, dłoni, twarzy,
  • KNNClassifier – klasyfikator k-najbliższych sąsiadów nad wektorami cech,
  • NeuralNetwork – wysokopoziomowe, deklaratywne API do tworzenia i trenowania sieci,
  • SpeechCommands / SoundClassifier – klasyfikacja dźwięków i prostych komend głosowych.

Wiele przykładów ml5.js integruje się z p5.js, popularną biblioteką do kreatywnego kodowania. Efekt: w kilkunastu liniach można zbudować aplikację, która reaguje na ruch ciała, gesty dłoni albo dźwięk.

Pierwsze użycie: klasyfikacja obrazów w kilku linijkach

Najprostszy przykład: klasyfikacja obrazu za pomocą wbudowanego modelu. Zamiast osobno ładować TensorFlow.js i @tensorflow-models/mobilenet, w ml5 wszystko dzieje się w jednym kroku.

<img id="inputImage" src="kot.jpg" />
<script src="https://unpkg.com/ml5@latest/dist/ml5.min.js"></script>
<script>
  let classifier;

  function setupClassifier() {
    classifier = ml5.imageClassifier('MobileNet', modelReady);
  }

  function modelReady() {
    console.log('Model ml5 MobileNet gotowy');
    classifyImage();
  }

  function classifyImage() {
    const img = document.getElementById('inputImage');
    classifier.classify(img, (err, results) => {
      if (err) {
        console.error(err);
        return;
      }
      console.log(results); // etykieta + prawdopodobieństwo
    });
  }

  setupClassifier();
</script>

Całość sprowadza się do ml5.imageClassifier('MobileNet', ...) i classifier.classify. Pod spodem działa TensorFlow.js, ale użytkownik nie musi o tym wiedzieć.

Interaktywny klasyfikator z KNNClassifier

Popularny scenariusz w ml5 to interaktywne tworzenie własnych kategorii. Biblioteka wystawia moduł KNNClassifier, który przyjmuje wektory cech (np. z mobilenet) i etykiety, a potem klasyfikuje nowe przykłady na podstawie podobieństwa.

Szkic użycia:

let featureExtractor;
let knnClassifier;

async function setup() {
  featureExtractor = ml5.featureExtractor('MobileNet', () => {
    console.log('FeatureExtractor gotowy');
  });
  knnClassifier = ml5.KNNClassifier();
}

function addExample(video, label) {
  const features = featureExtractor.infer(video);
  knnClassifier.addExample(features, label);
}

async function classify(video) {
  const features = featureExtractor.infer(video);
  const result = await knnClassifier.classify(features);
  console.log(result.label, result.confidencesByLabel);
}

W praktyce taki kod często łączy się z prostym interfejsem: trzy przyciski „Dodaj przykład klasy A/B/C” i podgląd z kamery. Użytkownik klika, zbiera kilka przykładów, a potem model natychmiast zaczyna klasyfikować nowe ujęcia.

NeuralNetwork w ml5 – deklaratywne sieci neuronowe

Moduł ml5.NeuralNetwork pozwala zbudować model z minimalną ilością konfiguracji. Zamiast tworzyć warstwy ręcznie, podaje się opis problemu: jakie są cechy wejściowe, jakie wyjścia, czy to regresja czy klasyfikacja. Resztą zajmuje się biblioteka.

Przykład: uczymy model przewidywania wyniku na podstawie dwóch cech: wieku i liczby godzin snu (zmyślony przypadek, ale mechanizm jest realny).

const options = {
  task: 'regression',
  debug: true
};

const nn = ml5.neuralNetwork(options);

// Dodawanie przykładów
nn.addData({ age: 25, sleep: 7 }, { score: 0.8 });
nn.addData({ age: 40, sleep: 6 }, { score: 0.6 });
// ... więcej przykładów

// Normalizacja danych i trening
nn.normalizeData();

nn.train({ epochs: 50 }, () => {
  console.log('Trening zakończony');
  makePrediction();
});

function makePrediction() {
  nn.predict({ age: 30, sleep: 8 }, (err, result) => {
    console.log(result);
  });
}

Biblioteka sama zadba o zamianę obiektów wejściowych na tensory, normalizację danych, a nawet zapis modelu. Dla osób uczących się ML to wygodny sposób, by skupić się na eksperymentach, a nie szczegółach API.

Integracja ml5.js z p5.js

W połączeniu z p5.js praca z ml5 staje się bardzo „wizualna”. Zamiast ukrywać wyniki w konsoli, można rysować je na canvasie, sterować animacjami, a nawet muzyką.

Minimalny przykład szkicowy (w pseudokodzie) może wyglądać tak:

let video;
let poseNet;
let poses = [];

function setup() {
  createCanvas(640, 480);
  video = createCapture(VIDEO);
  video.hide();

  poseNet = ml5.poseNet(video, () => console.log('PoseNet gotowy'));
  poseNet.on('pose', results => {
    poses = results;
  });
}

function draw() {
  image(video, 0, 0);

  // Rysowanie punktów stawów
  for (let i = 0; i < poses.length; i++) {
    const pose = poses[i].pose;
    for (let j = 0; j < pose.keypoints.length; j++) {
      const keypoint = pose.keypoints[j];
      if (keypoint.score > 0.2) {
        fill(255, 0, 0);
        noStroke();
        ellipse(keypoint.position.x, keypoint.position.y, 10, 10);
      }
    }
  }
}

Taki szkic pozwala w kilka minut zbudować prostą grę sterowaną ruchem rąk albo instalację, która rysuje abstrakcyjne wzory w oparciu o taniec użytkownika.

Ograniczenia ml5.js

ml5.js celowo upraszcza API, co oznacza również ograniczenia:

  • Brak pełnej kontroli nad architekturą modelu – nie zdefiniuje się skomplikowanej sieci krok po kroku.
  • Stosunkowo niewielki zestaw wbudowanych zadań w porównaniu z pełnym ekosystemem TensorFlow.js.
  • Focus na frontendzie – brak wsparcia dla środowisk serwerowych takich jak Node.js w tym samym stopniu, co w raw TensorFlow.js.

Do szybkich prototypów i projektów edukacyjnych ml5 jest jednak wręcz idealne. Gdy projekt rośnie, zawsze można sięgnąć niżej – po „czyste” API TensorFlow.js – i stopniowo przenosić logikę z ml5 do bardziej elastycznych konstrukcji.

Porównanie TensorFlow.js, ml5.js i alternatyw

Patrząc szerzej na ekosystem ML w JavaScript, TensorFlow.js i ml5.js to dwa różne bieguny. Obok nich istnieją jednak inne narzędzia, które w niektórych scenariuszach sprawdzą się lepiej: ONNX Runtime Web, Brain.js czy opencv.js z wbudowanymi modelami klasyfikacji.

TensorFlow.js vs ml5.js – na co się zdecydować

Dobór biblioteki zwykle wynika z odpowiedzi na kilka prostych pytań: jak bardzo trzeba kontrolować architekturę, gdzie będzie działał kod i kto ma nad nim pracować.

Najczęściej zadawane pytania (FAQ)

Czy machine learning w JavaScript ma sens, skoro jest Python?

Tak, ale do innych zadań niż klasyczny Pythonowy backend. Python świetnie nadaje się do ciężkiego trenowania modeli na serwerach z GPU. JavaScript sprawdza się tam, gdzie model ma działać blisko użytkownika: w przeglądarce, w aplikacji mobilnej lub na edge’u (np. Cloudflare Workers).

Typowy podział ról wygląda tak: model jest trenowany w Pythonie, eksportowany do formatu obsługiwanego przez TensorFlow.js lub ONNX, a następnie ładowany w JavaScript do szybkiej predykcji (inferencji). Dzięki temu można mieć moc Pythona „pod maską”, a wygodę i interaktywność JS w warstwie użytkowej.

Do czego praktycznie używać TensorFlow.js i innych frameworków ML w przeglądarce?

Frameworki takie jak TensorFlow.js czy ml5.js najlepiej sprawdzają się w lekkich, interaktywnych zastosowaniach. Przykłady: analiza obrazu z kamery w czasie rzeczywistym (pozycja ciała, gesty), inteligentne formularze (dynamiczne pytania w zależności od odpowiedzi) albo proste asystenty tekstowe działające bezpośrednio w przeglądarce.

Dobrze radzą sobie też z projektami edukacyjnymi i kreatywnymi – od interaktywnych wizualizacji danych, po gry sterowane ruchem. W takich zadaniach liczy się szybka reakcja i brak konieczności utrzymywania rozbudowanej infrastruktury serwerowej.

Jakie są główne zalety uruchamiania modeli ML w JavaScript po stronie użytkownika?

Najważniejsze plusy to prywatność, niskie opóźnienia i działanie offline. Dane (np. obraz twarzy, nagranie głosu, treść formularza) nie muszą opuszczać urządzenia, więc ogranicza się ryzyko wycieku i upraszcza kwestie prawne. Model przetwarza wszystko lokalnie, a na serwer można wysłać już tylko zanonimizowany wynik.

Brak „podróży do chmury” oznacza też szybszą odpowiedź – ważne przy przetwarzaniu obrazu czy dźwięku klatka po klatce. Jeśli kod i model są zbuforowane w przeglądarce, aplikacja może działać także przy słabym łączu lub całkowitym braku internetu, korzystając np. z Service Workera.

Czym różni się TensorFlow.js od ml5.js i innych „przyjaznych” bibliotek?

TensorFlow.js to raczej „silnik” obliczeń: daje niskopoziomowy dostęp do tensorów, warstw sieci, optymalizatorów i backendów (CPU, WebGL, WebGPU, Node). Jest elastyczny i wydajny, ale wymaga większej wiedzy o ML – trzeba samodzielnie dbać o kształty danych czy architekturę modelu.

ml5.js, Brain.js i podobne biblioteki celują w prostotę. Oferują gotowe, wysokopoziomowe funkcje typu „wczytaj model do rozpoznawania obrazów” albo „utwórz prostą sieć do klasyfikacji”. Nadają się świetnie do prototypów, edukacji i projektów, gdzie programista nie chce zagłębiać się w szczegóły matematyczne, tylko „po prostu coś uruchomić”.

Czy da się trenować modele od zera w przeglądarce przy użyciu JavaScript?

Da się, ale zazwyczaj jest to mało praktyczne. Przeglądarka działa na sprzęcie użytkownika, który może być słaby, przegrzewający się i ograniczany przez system (szczególnie na telefonach). Dłuższe treningi mogą „zamrażać” interfejs, szybko zjadać baterię albo zostać przerwane.

JavaScript radzi sobie znacznie lepiej z trenowaniem małych modeli lub z tzw. transfer learningiem – lekkim dostrajaniem już wytrenowanej sieci na kilku nowych przykładach. Pełne trenowanie dużych sieci neuronowych sensowniej zrealizować w Pythonie, a do przeglądarki dostarczyć jedynie skompresowany model do inferencji.

Jak przenieść model z Pythona do JavaScript (TensorFlow.js lub ONNX)?

Najczęściej wygląda to tak: najpierw trenujesz model w TensorFlow lub PyTorch, a potem eksportujesz go do formatu obsługiwanego przez wybrany runtime. Dla TensorFlow.js służą do tego specjalne konwertery, które zapisują model i jego wagi w formie możliwej do wczytania w przeglądarce lub Node.js.

Alternatywnie można użyć formatu ONNX – wspólnego „języka” dla modeli ML. Po wyeksportowaniu modelu do ONNX wykorzystujesz ONNX Runtime Web, który uruchomi go w JavaScript (np. na WebGL, WebGPU lub CPU). Dzięki temu ten sam model może działać na serwerze, w przeglądarce i w edge functions, bez przepisywania logiki.

Jakie są główne ograniczenia ML w JavaScript i kiedy lepiej zostać przy klasycznym backendzie?

Największe bariery to moc obliczeniowa i ograniczenia środowiska. Nie ma gwarancji, że użytkownik ma szybki komputer czy nowy telefon; przeglądarka nie daje też takiego dostępu do GPU jak serwer z CUDA. Duże modele dłużej się pobierają, zajmują sporo pamięci i szybciej drenają baterię, co bywa nieakceptowalne w aplikacjach mobilnych.

Jeśli potrzebujesz ciężkiego trenowania, dużych modeli NLP/vision, przetwarzania ogromnych zbiorów danych albo ścisłej kontroli nad infrastrukturą, klasyczny backend w Pythonie (TensorFlow, PyTorch, scikit-learn) i API w chmurze będzie lepszym wyborem. JavaScript sprawdzi się jako uzupełnienie – warstwa inferencji i interaktywnej prezentacji wyników bliżej użytkownika.

Najważniejsze punkty

  • Machine learning w JavaScript przesuwa część zadań z serwera do przeglądarki i na urządzenie użytkownika: ciężki trening zwykle odbywa się w Pythonie, a lekka inferencja i interakcja z modelem dzieje się lokalnie w JS.
  • Uruchamianie modeli w przeglądarce poprawia prywatność, bo surowe dane (obraz, głos, tekst z formularzy) nie opuszczają urządzenia, co jest kluczowe np. przy analizie twarzy, krótkich komend głosowych czy wrażliwych formularzy.
  • Lokalne modele znacząco zmniejszają opóźnienia i uniezależniają aplikację od sieci – analiza obrazu z kamery czy szybka klasyfikacja tekstu może działać płynnie nawet offline, jeśli model został wcześniej pobrany.
  • JavaScript jest dziś językiem całego środowiska uruchomieniowego (przeglądarka, Node.js, edge functions, aplikacje hybrydowe), więc te same modele mogą działać na froncie, w backendzie i na „krawędzi” sieci, spinając całą architekturę.
  • Typowe zastosowania ML po stronie JS to lekkie, interaktywne scenariusze: edukacyjne wizualizacje, kreatywne narzędzia reagujące na gesty, scoring leadów w formularzach czy proste asystenty tekstowe bez ciężkiego backendu NLP.
  • Ograniczenia mocy obliczeniowej i środowiska przeglądarki sprawiają, że JS nadaje się głównie do inferencji małych modeli i transfer learningu, natomiast trenowanie dużych sieci lepiej pozostawić ekosystemowi Python + GPU.