tarent heißt jetzt Qvest Digital AG | mehr erfahren

KI gestützte Bildersuche: Modelle im Browser nutzen

Sven Schumann
SENIOR SOFTWARE DEVELOPER
Veröffentlicht 9. Januar 2025

KI gestützte Bildersuche: Modelle im Browser nutzen


In dem zweiten Teil der Blog-Serie "KI gestützte Bildersuche" haben wir gesehen, wie wir Bilder und Texte miteinander vergleichen können. Dazu haben wir das CLIP-Modell von Hugging Face benutzt. Damit könnte man nun eine Backend-Applikation bauen, die Bilder analysiert und durchsuchbar macht. Das empfand ich als Schade, da Nutzer ihre Bilder an das Backend hochladen müssten. Und damit dem Backend vertrauen müssten. Eine lokale Anwendung in Python zu implementieren, ist jetzt auch nicht so das Gelbe vom Ei. Als ich jedoch etwas in der kurzen Modell-Dokumentation gescrollt habe, bin ich auf Transformers.js gestoßen

KI im Browser

Warum ist das jetzt so toll?

Nun, vieles findet heute nur noch im Browser statt:

  • - E-Mails
  • - Nachrichten
  • - Soziale Netzwerke
  • - Video Streaming
  • - Shopping


Ich weiß nicht, wie es bei euch ist, aber: Wenn ich nach einer Anwendung suche, mache ich das im Browser und stoße ggf. auf die Herstellerseite. Wenn man jetzt nichteinmal etwas herunterladen muss, sondern direkt ausprobieren kann, bin ich schon direkt dabei! Natürlich muss der Browser auch die Features unterstützen, die die Anwendung benötigt. Aber heutzutage können die Browser schon einiges:

Man darf auch nicht vergessen, dass Rechenleistung benötigt wird - auch wenn die Modelle schon austrainiert sind - die bezahlt werden will. So kann man die Rechenleistung des Endgeräts nutzen! Das ist allerdings eine Gratwanderung, da nicht jedes Endgerät auch die nötige Leistung bringt, um den Nutzer nicht abzuschrecken... Und noch ein Punkt: Browser gibt es auf fast allen Geräten! Egal ob:

  • PC
  • Tablet
  • Smartphone
  • SmartTV

Und eine Anwendung zu implementieren, ist doch besser, als für jedes Endgerät eine eigene zu entwickeln, oder? Gut, dass der Browser mit der besten Unterstützung von den meisten Nutzern verwendet wird: Chrome.

Bildersuche im Browser

Gehen wir mal die Punkte durch, die wir für eine Web-Anwendung benötigen, um eine Bildersuche zu implementieren:

Dateizugriff

Die Anwendung muss auf die lokalen Bilder zugreifen können. Dafür gibt es die File API. Der Browser kann aber nicht "einfach so" auf die Dateien zugreifen. Es benötigt eine Benutzerinteraktion um Dateien oder Verzeichnisse freizugeben. Das ist auch gut so! Stellt euch vor, jede Webseite könnte auf eure Dateien zugreifen...

async function listFiles(root, dirHandle) {
  const result = {
    directory: dirHandle,
    files: [],
    subDirectories: [],
  };
  // Für jedes Element im Verzeichnis
  for await (const entry of dirHandle.values()) {
    if (entry.kind === 'directory') {
      // Wenn es ein Verzeichnis ist, gehe auch dieses durch
      const subDirectory = await listFiles(root + '/' + entry.name, entry);
      result.subDirectories.push(subDirectory);
    } else if (entry.kind === 'file') {
      // Wenn es eine Datei ist, füge sie zur Liste hinzu
      result.files.push(entry);
    }
  }
  return result;
}
// lass den Benutzer ein Verzeichnis auswählen (Dialog vom Browser wird geöffnet)
const dirHandle = await window.showDirectoryPicker();
// Gehe das Verzeichnis durch
const directoryContent = await listFiles(dirHandle.name, dirHandle);

(Leider) ist der Zugriff auf ein Verzeichnis bisher nicht in allen Browser möglich. Zu beachten ist, dass wir nicht den konkreten Pfad des vom Benutzer gewählten Verzeichnis bekommen - sondern nur den Namen!

Transformers.js

Transformers.js ist wie Transformers von Hugging Face, nur eben in JavaScript implementiert. Hiermit können wir also das gewünschte KI-Modell direkt im Browser nutzen!

Bild -> Vektor

Mit diesem Code können wir also die Bilder in Vektoren umwandeln. Hierbei wird das Modell automatisch heruntergeladen. Dies wird auch im Browser-Cache gespeichert - sollte also nur einmal heruntergeladen werden.

import { AutoProcessor, CLIPVisionModelWithProjection, RawImage } from '@huggingface/transformers';
// Lade das Modell/Prozessor für die Bildverarbeitung
const processor = await AutoProcessor.from_pretrained('Xenova/clip-vit-base-patch32');
const visionModel = await CLIPVisionModelWithProjection.from_pretrained('jinaai/jina-clip-v1');
const rawImages = [];
for (const file of directoryContent.files) {
  // Lade das Bild aus der Datei
  const fileBlob = await file.getFile();
  const image = await RawImage.fromBlob(fileBlob);
  rawImages.push(image);
}
const imageInputs = await processor(rawImages);
// Berechne die Vektoren der einzelnen Bilder
const { image_embeds } = await visionModel(imageInputs);
for (let i = 0; i < directoryContent.files.length; i++) {
  console.log(`Vektor für ${directoryContent.files[i].name}:`, image_embeds[i].data);
}

Text -> Vektor

Mit diesem Code können wir die Suchanfragen in Vektoren umwandeln.

import { AutoTokenizer, CLIPTextModelWithProjection } from '@huggingface/transformers';
// Lade das Modell/Prozessor für die Textverarbeitung
const tokenizer = await AutoTokenizer.from_pretrained('jinaai/jina-clip-v1');
const textModel = await CLIPTextModelWithProjection.from_pretrained('jinaai/jina-clip-v1');
// Suchanfragen
const queries = ['A yellow bird', 'A striped cat', 'A carport'];
const textInputs = tokenizer(queries, { padding: true, truncation: true });
// Berechne die Vektoren der einzelnen Suchanfragen
const { text_embeds } = await textModel(textInputs);
for (let i = 0; i < queries.length; i++) {
  console.log(`Vektor für "${queries[i]}":`, text_embeds[i].data);
}

Ähnlichkeit berechnen

Der Vergleich zwischen den Vektoren ist dann auch kein Problem mehr. Und benötigt auch kein KI-Modell! Ist also recht "günstig" in der Ausführung.

import { cos_sim } from '@huggingface/transformers';
for (let i = 0; i < queries.length; i++) {
  const query = queries[i];
  const queryVector = text_embeds[i].data;
  console.log(query);
  for (let j = 0; j < directoryContent.files.length; j++) {
    const image = directoryContent.files[j];
    const imageVector = image_embeds[j].data;
    // Berechne die Ähnlichkeit zwischen Suchanfrage und Bild
    console.log(cos_sim(queryVector, imageVector));
  }
}

Vektor-Speicher

Die Bild-Vektoren sollten nicht jedes Mal neu berechnet werden müssen. Denn das ist mit Abstand das Rechenintensivste. Besser wäre es, die Vektoren in einer Datenbank zu persistieren. Im Browser bietet sich dafür die IndexedDB an.

let database = null
const req = indexedDB.open("awesome-app", 1)
req.onupgradeneeded = () => {
  // Datenbank ist noch nicht vorhanden - Erstelle die Tabelle(n)
  const db = req.result
  db.createObjectStore('vector', {autoIncrement: true})
}
req.onsuccess = () => {
  // Datenbank erstellt und geöffnet
  database = req.result
}
function saveVector(path, vector) {
  const tx = database.transaction('vector', 'readwrite')
  const req = tx.objectStore('vector').put({path, vector})
  return new Promise((resolve, reject) => {
    req.onsuccess = () => resolve(Number(req.result))
    req.onerror = reject
  })
}
function iterate(calbackFn) {
  const tx = database.transaction('vector', 'readonly')
  const req = tx.objectStore('vector').openCursor()
  return new Promise((resolve, reject) => {
    req.onsuccess = () => {
      const cursor = req.result
      if (cursor) {
        // Eintrag gefunden
        calbackFn(cursor.value)
        // Nächster Eintrag beantragen
        cursor.continue()
      } else {
        // Keine weiteren Einträge
        resolve()
      }
    }
    req.onerror = reject
  })
}
// Speichere die Bild-Vektoren in der Datenbank
for (let i = 0; i < directoryContent.files.length; i++) {
  await saveVector(directoryContent.files[i].name, image_embeds[i].data)
}
// Lade die Bild-Vektoren aus der Datenbank
await iterate((entry) => {
  for (let i = 0; i < queries.length; i++) {
    const query = queries[i]
    const queryVector = text_embeds[i].data
    // Berechne die Ähnlichkeit zwischen Suchanfrage und Bild
    const similarity = cos_sim(queryVector, entry.vector)
    console.log(`Ähnlichkeit für ${entry.path}: ${similarity}`)
  }
})

GPU-Unterstützung

Transformers.js sollte die best möglichste Hardware nutzen, die zur Verfügung steht. Das bedeutet, wenn eine GPU vorhanden ist, diese auch genutzt wird. Sollte sie nicht vorhanden sein, wird auf CPU umgeschaltet. Was allerdings bedeutet, dass die Berechnung (viel) länger dauern wird. Die WebGPU API ist noch nicht in allen Browsern verfügbar.

Mit diesem Code wird versucht, die GPU zu nutzen. Sollte sie nicht verfügbar sein, wird auf die CPU umgeschaltet.

let deviceKey = 'cpu'
if (navigator.gpu) {
  try {
    const adapter = await navigator.gpu.requestAdapter()
    const device = await adapter.requestDevice()
    console.log('GPU detected', device)
    // Nutze die GPU
    deviceKey = 'gpu'
  } catch (e) {
    console.warn('Error while detecting GPU', e)
  }
} else {
    console.warn('No GPU detected')
}
const visionModel = await CLIPVisionModelWithProjection.from_pretrained('jinaai/jina-clip-v1', { device: deviceKey })
const textModel = await CLIPTextModelWithProjection.from_pretrained('jinaai/jina-clip-v1', { device: deviceKey })

Was lernen wir daraus?

1. KI-Modelle können auch im Browser genutzt werden
2. Die Browser bieten schon teilweise Features, die für KI-Anwendungen benötigt werden
3. GPU Unterstützung ist noch nicht flächendeckend verfügbar