‘Ricerca di immagini con 🤗 dataset’

Image search with 🤗 dataset

🤗 datasets è una libreria che facilita l’accesso e la condivisione di dataset. Facilita anche l’elaborazione efficiente dei dati, inclusa la lavorazione di dati che non rientrano nella memoria.

Quando datasets è stato lanciato per la prima volta, era associato principalmente ai dati testuali. Tuttavia, di recente, datasets ha aggiunto un supporto maggiore anche per l’audio e le immagini. In particolare, ora esiste un tipo di dato datasets per le immagini. Un precedente post sul blog ha mostrato come datasets può essere utilizzato con 🤗 transformers per addestrare un modello di classificazione delle immagini. In questo post sul blog, vedremo come possiamo combinare datasets e alcune altre librerie per creare un’applicazione di ricerca delle immagini.

Prima di tutto, installeremo datasets. Poiché lavoreremo con le immagini, installeremo anche pillow. Avremo anche bisogno di sentence_transformers e faiss. Introdurremo questi in modo più dettagliato di seguito. Installeremo anche rich – lo useremo solo brevemente qui, ma è un pacchetto estremamente utile da avere a disposizione – consiglio vivamente di esplorarlo ulteriormente!

!pip install datasets pillow rich faiss-gpu sentence_transformers

Per iniziare, diamo un’occhiata alla caratteristica dell’immagine. Possiamo utilizzare la meravigliosa libreria rich per esplorare gli oggetti Python (funzioni, classi ecc.)

from rich import inspect
import datasets

inspect(datasets.Image, help=True)

╭───────────────────────── <class 'datasets.features.image.Image'> ─────────────────────────╮
│ class Image(decode: bool = True, id: Union[str, NoneType] = None) -> None:                │
│                                                                                           │
│ Caratteristica immagine per leggere dati immagine da un file immagine.                     │
│                                                                                           │
│ Input: La caratteristica Image accetta come input:                                        │
│ - Una :obj:`str`: Percorso assoluto del file immagine (ovvero è consentito l'accesso       │
│    casuale).                                                                             │
│ - Un :obj:`dict` con le chiavi:                                                           │
│                                                                                           │
│     - path: Stringa con percorso relativo del file immagine rispetto al file di archivio. │
│     - bytes: Byte del file immagine.                                                      │
│                                                                                           │
│   Questo è utile per i file archiviati con accesso sequenziale.                            │
│                                                                                           │
│ - Un :obj:`np.ndarray`: Array NumPy che rappresenta un'immagine.                           │
│ - Un :obj:`PIL.Image.Image`: Oggetto immagine PIL.                                        │
│                                                                                           │
│ Args:                                                                                     │
│     decode (:obj:`bool`, default ``True``): Se decodificare i dati immagine. Se `False`,   │
│         restituisce il dizionario sottostante nel formato {"path": image_path, "bytes":    │
│ image_bytes}.                                                                             │
│                                                                                           │
│  decode = True                                                                            │
│   dtype = 'PIL.Image.Image'                                                               │
│      id = None                                                                            │
│ pa_type = StructType(struct<bytes: binary, path: string>)                                 │
╰───────────────────────────────────────────────────────────────────────────────────────────╯

Possiamo vedere che ci sono diversi modi in cui possiamo passare le nostre immagini. Torneremo su questo tra poco.

Una caratteristica davvero interessante della libreria datasets (oltre alle funzionalità per l’elaborazione dei dati, il memory mapping, ecc.) è che si ottengono alcune cose interessanti “gratuitamente”. Una di queste è la possibilità di aggiungere un indice faiss a un dataset. faiss è una “libreria per la ricerca efficiente di similarità e clustering di vettori densi”.

La documentazione di datasets mostra un esempio di utilizzo di un indice faiss per il recupero del testo. In questo post vedremo se possiamo fare lo stesso per le immagini.

Il dataset: “Libri digitalizzati – Immagini identificate come abbellimenti. c. 1510 – c. 1900”

Si tratta di un dataset di immagini estratte da una collezione di libri digitalizzati della British Library. Queste immagini provengono da libri che coprono un ampio periodo di tempo e una vasta gamma di domini. Le immagini sono state estratte utilizzando le informazioni contenute nell’output OCR di ciascun libro. Di conseguenza, si sa da quale libro provengono le immagini, ma non necessariamente altro riguardo a quell’immagine, ad esempio cosa viene mostrato nell’immagine.

Alcuni tentativi per superare questo problema hanno incluso l’upload delle immagini su flickr. Questo permette alle persone di taggare le immagini o di inserirle in varie categorie diverse.

Ci sono stati anche progetti per etichettare il dataset utilizzando il machine learning. Questo lavoro rende possibile la ricerca per tag, ma potremmo volere una capacità di ricerca più “ricca”. Per questo esperimento particolare, lavoreremo con un sottoinsieme delle collezioni che contengono “decorazioni”. Questo dataset è un po’ più piccolo, quindi sarà migliore per sperimentare. Possiamo ottenere i dati completi dal repository dei dati della British Library: https://doi.org/10.21250/db17. Dato che il dataset completo è comunque abbastanza grande, probabilmente vorrai iniziare con un campione più piccolo.

Creazione del nostro dataset

Il nostro dataset è composto da una cartella che contiene sottocartelle all’interno delle quali si trovano le immagini. Questo è un formato abbastanza standard per la condivisione di dataset di immagini. Grazie a una richiesta di pull recentemente unita, possiamo caricare direttamente questo dataset utilizzando il caricatore datasets ImageFolder 🤯

from datasets import load_dataset
dataset = load_dataset("imagefolder", data_files="https://zenodo.org/record/6224034/files/embellishments_sample.zip?download=1")

Vediamo cosa otteniamo.

dataset

DatasetDict({
    train: Dataset({
        features: ['image', 'label'],
        num_rows: 10000
    })
})

Possiamo ottenere un DatasetDict e abbiamo un Dataset con le caratteristiche di immagine ed etichetta. Dato che qui non abbiamo alcuna divisione tra train/validation, prendiamo la parte train del nostro dataset. Vediamo anche un esempio del nostro dataset per vedere come appare.

dataset = dataset["train"]
dataset[0]

{'image': <PIL.JpegImagePlugin.JpegImageFile image mode=RGB size=358x461 at 0x7F9488DBB090>,
 'label': 208}

Iniziamo con la colonna delle etichette. Contiene la cartella genitore per le nostre immagini. In questo caso, la colonna delle etichette rappresenta l’anno di pubblicazione dei libri da cui sono tratte le immagini. Possiamo vedere le corrispondenze per questo utilizzando dataset.features:

dataset.features['label']

In questo particolare dataset, i nomi dei file delle immagini contengono anche alcuni metadati sul libro da cui è stata presa l’immagine. Ci sono alcuni modi per ottenere queste informazioni.

Quando guardiamo un esempio del nostro dataset, la caratteristica image era un PIL.JpegImagePlugin.JpegImageFile. Dato che le immagini PIL hanno un attributo di nome file, un modo in cui possiamo ottenere i nostri nomi file è accedendo a questo.

dataset[0]['image'].filename

/root/.cache/huggingface/datasets/downloads/extracted/f324a87ed7bf3a6b83b8a353096fbd9500d6e7956e55c3d96d2b23cc03146582/embellishments_sample/1920/000499442_0_000579_1_[The Ring and the Book  etc ]_1920.jpg

Dato che potremmo volere un accesso facile a queste informazioni in seguito, creiamo una nuova colonna per estrarre il nome del file. Per questo, utilizzeremo il metodo map.

dataset = dataset.map(lambda example: {"fname": example['image'].filename.split("/")[-1]})

Possiamo guardare un esempio per vedere come appare ora.

dataset[0]

{'fname': '000499442_0_000579_1_[The Ring and the Book  etc ]_1920.jpg',
 'image': <PIL.JpegImagePlugin.JpegImageFile image mode=RGB size=358x461 at 0x7F94862A9650>,
 'label': 208}

Ora abbiamo i nostri metadati. Vediamo alcune immagini già! Se accediamo a un esempio e indicizziamo nella colonna image, vedremo la nostra immagine 😃

dataset[10]['image']

Nota in una versione precedente di questo post del blog i passaggi per scaricare e caricare le immagini erano molto più complicati. Il nuovo caricatore ImageFolder rende questo processo molto più facile 😀 In particolare, non dobbiamo preoccuparci di come caricare le immagini poiché i dataset si sono occupati di questo per noi.

Pusha tutte le cose nell’hub!

Una delle cose fantastiche dell’ecosistema 🤗 è l’Hugging Face Hub. Possiamo usare l’Hub per accedere a modelli e dataset. Spesso viene utilizzato per condividere il lavoro con gli altri, ma può anche essere uno strumento utile per il lavoro in corso. datasets ha recentemente aggiunto un metodo push_to_hub che consente di caricare un dataset nell’Hub con il minimo sforzo. Questo può essere davvero utile consentendoti di passare un dataset con tutte le trasformazioni ecc. già fatte.

Per ora, caricheremo il dataset nell’Hub e lo manterremo privato inizialmente.

A seconda di dove stai eseguendo il codice, potrebbe essere necessario autenticarsi. Puoi farlo utilizzando il comando huggingface-cli login oppure, se stai eseguendo il codice in un notebook, utilizzando notebook_login

from huggingface_hub import notebook_login

notebook_login()

dataset.push_to_hub('davanstrien/embellishments-sample', private=True)

Nota: in una versione precedente di questo post del blog dovevamo eseguire alcuni passaggi aggiuntivi per garantire che le immagini fossero incorporate quando si utilizzava push_to_hub. Grazie a questa pull request non dobbiamo più preoccuparci di questi passaggi extra. Dobbiamo solo assicurarci che embed_external_files=True (che è il comportamento predefinito).

Passaggio a un’altra macchina

A questo punto, abbiamo creato un dataset e lo abbiamo spostato nell’Hub. Ciò significa che è possibile riprendere il lavoro/dataset altrove.

In questo esempio particolare, avere accesso a una GPU è importante. Utilizzando l’Hub come modo per passare i nostri dati, potremmo iniziare su un laptop e continuare il lavoro su Google Colab.

Se ci spostiamo su una nuova macchina, potremmo dover effettuare nuovamente l’accesso. Una volta fatto ciò, possiamo caricare il nostro dataset

from datasets import load_dataset

dataset = load_dataset("davanstrien/embellishments-sample", use_auth_token=True)

Creazione di embeddings 🕸

Ora abbiamo un dataset con un sacco di immagini al suo interno. Per iniziare a creare la nostra app di ricerca di immagini, dobbiamo incorporare queste immagini. Ci sono vari modi per provare a farlo, ma un possibile modo è utilizzare i modelli CLIP tramite la libreria sentence_transformers. Il modello CLIP di OpenAI apprende una rappresentazione congiunta per immagini e testo, che è molto utile per quello che vogliamo fare, poiché vogliamo inserire del testo e ottenere un’immagine in risposta.

Possiamo scaricare il modello utilizzando la classe SentenceTransformer.

from sentence_transformers import SentenceTransformer

model = SentenceTransformer('clip-ViT-B-32')

Questo modello prenderà in input un’immagine o del testo e restituirà un embedding. Possiamo utilizzare il metodo map di datasets per codificare tutte le nostre immagini utilizzando questo modello. Quando chiamiamo map, restituiamo un dizionario con la chiave embeddings che contiene gli embeddings restituiti dal modello. Passiamo anche device='cuda' quando chiamiamo il modello; in questo modo ci assicuriamo di eseguire la codifica sulla GPU.

ds_with_embeddings = dataset.map(
    lambda example: {'embeddings':model.encode(example['image'], device='cuda')}, batched=True, batch_size=32)

Possiamo ‘salvare’ il nostro lavoro caricando di nuovo nell’Hub utilizzando push_to_hub.

ds_with_embeddings.push_to_hub('davanstrien/embellishments-sample', private=True)

Se ci spostassimo su una macchina diversa, potremmo riprendere il nostro lavoro caricandolo dall’Hub 😃

from datasets import load_dataset

ds_with_embeddings = load_dataset("davanstrien/embellishments-sample", use_auth_token=True)

Ora abbiamo una nuova colonna che contiene gli embeddings per le nostre immagini. Potremmo cercare manualmente tra questi e confrontarli con un embedding di input, ma datasets ha un metodo add_faiss_index. Questo utilizza la libreria faiss per creare un indice efficiente per la ricerca degli embeddings. Per ulteriori informazioni su questa libreria, puoi guardare questo video su YouTube

ds_with_embeddings['train'].add_faiss_index(column='embeddings')

Dataset({
        features: ['fname', 'year', 'path', 'image', 'embeddings'],
        num_rows: 10000
    })

Nota che questi esempi sono stati generati dalla versione completa del dataset, quindi potresti ottenere risultati leggermente diversi.

Ora abbiamo tutto il necessario per creare una semplice ricerca di immagini. Possiamo utilizzare lo stesso modello che abbiamo usato per codificare le nostre immagini per codificare del testo di input. Questo fungerà da prompt per cercare esempi simili. Iniziamo con ‘una locomotiva a vapore’.

prompt = model.encode("Una locomotiva a vapore")

Possiamo utilizzare un altro metodo della libreria datasets, get_nearest_examples, per ottenere immagini che hanno una codifica simile alla nostra codifica di input. Possiamo specificare quante immagini vogliamo ottenere come risultato.

scores, retrieved_examples = ds_with_embeddings['train'].get_nearest_examples('embeddings', prompt, k=9)

Possiamo accedere al primo esempio che viene restituito:

retrieved_examples['image'][0]

Questa non è esattamente una locomotiva a vapore, ma non è nemmeno un risultato completamente strano. Possiamo visualizzare gli altri risultati per vedere cosa è stato restituito.

import matplotlib.pyplot as plt

plt.figure(figsize=(20, 20))
columns = 3
for i in range(9):
    image = retrieved_examples['image'][i]
    plt.subplot(9 / columns + 1, columns, i + 1)
    plt.imshow(image)

Alcuni di questi risultati sembrano abbastanza simili al nostro prompt di input. Possiamo incapsulare tutto ciò in una funzione in modo da poter sperimentare più facilmente con prompt diversi.

def get_image_from_text(text_prompt, number_to_retrieve=9):
    prompt = model.encode(text_prompt)
    scores, retrieved_examples = ds_with_embeddings['train'].get_nearest_examples('embeddings', prompt, k=number_to_retrieve)
    plt.figure(figsize=(20, 20))
    columns = 3
    for i in range(9):
        image = retrieved_examples['image'][i]
        plt.title(text_prompt)
        plt.subplot(9 / columns + 1, columns, i + 1)
        plt.imshow(image)

get_image_from_text("Un'illustrazione del sole dietro una montagna")

Provando diversi prompt ✨

Ora che abbiamo una funzione per ottenere alcuni risultati, possiamo provare diversi prompt:

  • Per alcuni di essi sceglierò prompt che rappresentano una categoria generale, ad esempio ‘uno strumento musicale’ o ‘un animale’, mentre altri saranno più specifici, ad esempio ‘una chitarra’.

  • A titolo informativo, ho anche provato un operatore booleano: “Un’illustrazione di un gatto o di un cane”.

  • Infine, ho provato qualcosa di un po’ più astratto: “un abisso vuoto”.

prompts = ["Uno strumento musicale", "Una chitarra", "Un animale", "Un'illustrazione di un gatto o di un cane", "un abisso vuoto"]

for prompt in prompts:
    get_image_from_text(prompt)

Possiamo vedere che questi risultati non sono sempre corretti, ma sono generalmente ragionevoli. Sembra già che questo potrebbe essere utile per cercare il contenuto semantico di un’immagine in questo dataset. Tuttavia, potrebbe essere opportuno evitare di condividerlo così com’è…

Creazione di uno spazio di Hugging Face? 🤷🏼

Un prossimo passo ovvio per questo tipo di progetto è creare una demo dello spazio di Hugging Face. Ho fatto questo per altri modelli.

È stato un processo abbastanza semplice configurare un’app Gradio dal punto in cui siamo arrivati finora. Ecco uno screenshot di questa app:

Tuttavia, sono un po’ preoccupato nel renderla pubblica immediatamente. Guardando la scheda del modello CLIP possiamo esaminarne gli usi primari:

Usi primari

Immaginiamo principalmente che il modello verrà utilizzato dai ricercatori per comprendere meglio la robustezza, la generalizzazione e altre capacità, pregiudizi e vincoli dei modelli di visione artificiale. fonte

Questo è abbastanza vicino a ciò che ci interessa qui. In particolare, potremmo essere interessati a valutare come il modello gestisce i tipi di immagini nel nostro dataset (illustrazioni principalmente tratte da libri del XIX secolo). Le immagini nel nostro dataset sono (probabilmente) piuttosto diverse dai dati di allenamento. Il fatto che alcune immagini contengano anche testo potrebbe aiutare CLIP in quanto mostra una certa abilità OCR.

Tuttavia, guardando i casi d’uso fuori dall’ambito nella scheda del modello:

Casi d’uso fuori dall’ambito

Ogni caso d’uso implementato del modello, sia commerciale che no, è attualmente fuori dall’ambito. I casi d’uso non implementati, come la ricerca di immagini in un ambiente vincolato, non sono raccomandati a meno che non sia stata effettuata un’attenta prova di dominio del modello con una specifica tassonomia di classi fissa. Ciò perché la nostra valutazione della sicurezza ha dimostrato un elevato bisogno di test specifici per l’attività, soprattutto data la variabilità delle prestazioni di CLIP con diverse tassonomie di classi. Ciò rende potenzialmente dannosa l’implementazione non testata e non vincolata del modello in qualsiasi caso d’uso attualmente. fonte

suggerisce che la ‘implementazione’ non sia una buona idea. Sebbene i risultati ottenuti siano interessanti, non ho ancora sperimentato abbastanza con il modello (e non ho fatto nulla di più sistematico per valutarne le prestazioni e i pregiudizi) per essere sicuro di ‘implementarlo’. Un’altra considerazione aggiuntiva è il dataset di destinazione stesso. Le immagini sono tratte da libri che coprono una varietà di soggetti e periodi storici. Ci sono molti libri che rappresentano atteggiamenti coloniali e di conseguenza alcune delle immagini incluse potrebbero rappresentare certi gruppi di persone in modo negativo. Questo potrebbe essere potenzialmente un brutto abbinamento con uno strumento che consente di codificare qualsiasi input di testo arbitrario come prompt.

Potrebbero esserci modi per risolvere questo problema, ma ciò richiederà un po’ più di riflessione.

Conclusioni

Sebbene non abbiamo una bella demo da mostrare, abbiamo visto come possiamo utilizzare datasets per:

  • caricare immagini nel nuovo tipo di feature Image
  • ‘salvare’ il nostro lavoro utilizzando push_to_hub e utilizzarlo per spostare dati tra macchine/sessioni
  • creare un indice faiss per le immagini che possiamo utilizzare per recuperare immagini da un input di testo (o immagine).