# Explorador de Conjuntos de Datos Visuales

Los conjuntos de datos modernos siempre contienen millones de datos no estructurados como imágenes, clips de audio e incluso videos. Consultar los vecinos más cercanos en dichos conjuntos de datos es un desafío: 1. Medir distancias entre datos no estructurados es ambiguo; 2. Ordenar datos en función de miles de millones de distancias también requiere un esfuerzo adicional. Afortunadamente, la primera barrera ha sido superada por investigaciones recientes como CLIP (opens new window), y la segunda puede ser mejorada mediante algoritmos avanzados de búsqueda de vectores. MyScale proporciona una solución de base de datos unificada para aplicaciones de BD+IA, permitiendo una búsqueda de alto rendimiento en conjuntos de datos grandes. En este ejemplo, demostraremos cómo se puede construir una aplicación de BD+IA entrenando un clasificador de granularidad fina utilizando la técnica de minería de negativos difíciles.

En esta demostración, hemos adoptado el Conjunto de Datos unsplash-25k (opens new window), un conjunto de datos que contiene alrededor de 25 mil imágenes, como nuestro campo de juego. Sus fotos cubren escenas y objetos complicados.

# ¿Por qué estamos trabajando con una base de datos?

Para alguien que preguntó sobre el papel de la base de datos, necesito profundizar un poco en algunos conceptos de IA. Todos sabemos que un clasificador convencional siempre requiere toneladas de datos, anotaciones y trucos de entrenamiento para obtener una alta precisión en la vida real. Esos son suficientes pero no realmente necesarios para obtener un clasificador preciso. Una suposición más cercana nos ayudará a llegar al óptimo más rápido. Gracias a CLIP, ahora podemos obtener un buen punto de partida para el clasificador. Y lo único que nos queda es enfocarnos en ejemplos que sean similares pero no iguales, lo que se refiere a la técnica de minería de negativos difíciles en términos de IA. Ahora es el momento de que brille una base de datos de vectores, por ejemplo, MyScale.

MyScale es una base de datos de vectores que admite búsquedas de alto rendimiento entre miles de millones de vectores. Operaciones costosas como la minería de negativos difíciles nunca serán un obstáculo para aplicaciones de IA e investigación con MyScale. Encontrar negativos difíciles solo lleva milisegundos. Por lo tanto, todo el proceso de ajuste fino solo puede llevar unos pocos clics en la página web.

# ¿Cómo jugar con la demostración?

¿Por qué no probar nuestra demostración en línea (opens new window)?

# Instalación de los requisitos previos

  • transformers: Ejecutar el modelo CLIP
  • tqdm: Barra de progreso bonita para humanos
  • clickhouse-connect: Cliente de base de datos MyScale
  • streamlit: Servidor web de Python para ejecutar la aplicación
python3 -m pip install transformers tqdm clickhouse-connect streamlit pandas lmdb torch

Puede descargar metadatos si desea construir su propia base de datos.

# Descarga el conjunto de datos Unsplash 25K
wget https://unsplash-datasets.s3.amazonaws.com/lite/latest/unsplash-research-dataset-lite-latest.zip
# Descomprímelo...
unzip unsplash-research-dataset-lite-latest.zip
# Tendrás un archivo llamado `photos.tsv000` en tu directorio de trabajo actual
# Luego puedes extraer la característica CLIP del conjunto de datos

# Construyendo una base de datos con vectores

# Adentrándonos en los datos

Primero, veamos la estructura del Conjunto de Datos Unsplash-25k. El archivo photos.tsv000 contiene metadatos y anotaciones para todas las imágenes del conjunto de datos. Una sola fila se ve así:

photo_id photo_url photo_image_url ...
xapxF7PcOzU https://unsplash.com/photos/xapxF7PcOzU https://images.unsplash.com/photo-1421992617193-7ce245f5cb08 ...

La primera columna se refiere al identificador único de esta imagen. La siguiente columna es la URL de su página de descripción, que muestra su autor y otra información meta. La tercera columna contiene las URL de las imágenes. Las URL de las imágenes se pueden usar directamente para recuperar la imagen con la API de Unsplash (opens new window). Aquí tienes un ejemplo de la columna photo_image_url mencionada anteriormente:

Agradecimientos especiales a  y al autor de la foto Timothy Kolczak

Entonces usamos el siguiente código para cargar los datos:

import pandas as pd
from tqdm import tqdm
images = pd.read_csv(args.dataset, delimiter='\t')

# Creando una tabla de base de datos MyScale

# Trabajando con la base de datos

Necesitas una conexión a un backend de base de datos para crear una tabla en MyScale. Puedes consultar la guía detallada sobre el cliente de Python en esta página.

Si estás familiarizado con SQL (Structured Query Language), te resultará mucho más fácil trabajar con MyScale. MyScale combina la consulta estructurada con la búsqueda de vectores, lo que significa que crear una base de datos de vectores es exactamente lo mismo que crear bases de datos convencionales. Y así es como creamos una base de datos de vectores en SQL:

CREATE TABLE IF NOT EXISTS unsplash_25k(
        id String,
        url String,
        vector Array(Float32),
        CONSTRAINT vec_len CHECK length(vector) = 512
        ) ENGINE = MergeTree ORDER BY id;

Definimos el id de la imagen como cadenas, las url como cadenas y los vectores de características vector como una matriz de longitud fija con un tipo de datos de número flotante de 32 bits y una dimensión de 512. En otras palabras, un vector de características de una imagen contiene 512 números flotantes de 32 bits. Podemos ejecutar este SQL con la conexión que acabamos de crear:

client.command(
"CREATE TABLE IF NOT EXISTS unsplash_25k (\
        id String,\
        url String,\
        vector Array(Float32),\
        CONSTRAINT vec_len CHECK length(vector) = 512\
) ENGINE = MergeTree ORDER BY id")

# Extrayendo características y llenando la base de datos

CLIP (opens new window) es un método popular que empareja datos de diferentes formas (o adoptamos el término académico "modal") en un espacio unificado, lo que permite una recuperación cruzada de alto rendimiento. Por ejemplo, puedes usar el vector de características de la frase "una foto de una casa junto a un lago" para buscar fotos similares y viceversa.

Varios pasos de minería de negativos difíciles pueden entrenar un clasificador preciso utilizando un clasificador de cero disparos como inicialización. Podemos tomar un vector CLIP generado a partir del texto como nuestro parámetro inicial para el clasificador. Luego podemos proceder a la parte de minería de negativos difíciles: buscar todas las muestras similares y excluir todas las negativas. Aquí tienes un ejemplo de código que muestra cómo extraer características de una sola imagen:

from torch.utils.data import DataLoader
from transformers import CLIPProcessor, CLIPModel
model_name = "openai/clip-vit-base-patch32"
# Es posible que necesites varios minutos para descargar el modelo CLIP
model = CLIPModel.from_pretrained(model_name).to(device)
# El procesador preprocesará la imagen
processor = CLIPProcessor.from_pretrained(model_name)
# Usando los datos que acabamos de cargar en la sección anterior
row = images.iloc[0]
# Obtén la URL y el identificador único de la imagen
url = row['photo_image_url']
_id = row['photo_id']
import requests
from io import BytesIO
# Descarga la imagen y cárgala
response = requests.get(url)
img = Image.open(BytesIO(response.content))
# Preprocesa la imagen y devuelve un tensor de PyTorch
ret = self.processor(text=None, images=img, return_tensor='pt')
# Obtén los valores de la imagen
img = ret['pixel_values']
# Obtén el vector de características (float32, 512d)
out = model.get_image_features(pixel_values=img)
# Normaliza el vector antes de insertarlo en la BD
out = out / torch.norm(out, dim=-1, keepdims=True)

Hasta ahora ya hemos recopilado todos los datos que necesitamos para construir la tabla. Solo queda una pieza en este rompecabezas: insertar datos en MyScale. Para obtener información detallada sobre el uso de la cláusula INSERT, puedes consultar la referencia de SQL.

# Una muestra para insertar una sola fila en la tabla
# Necesitas convertir el vector de características en listas de Python
transac = [_id, url, out.cpu().numpy().squeeze().tolist()]
# solo inserta el vector como un SQL normal
client.insert("unsplash_25k", transac)

# Aprendizaje con pocos ejemplos en el clasificador

# Inicializando los parámetros del clasificador

Como discutimos anteriormente, podemos usar la característica de texto para inicializar nuestro clasificador.

from transformers import CLIPTokenizerFast, CLIPModel
# Inicializa el Tokenizador
tokenizer = CLIPTokenizerFast.from_pretrained(model_name)
# Ingresa cualquier cosa que desees buscar
prompt = 'a house by the lake'
# obtén el prompt tokenizado y su característica
inputs = tokenizer(prompt, return_tensors='pt')
out = model.get_text_features(**inputs)
xq = out.squeeze(0).cpu().detach().numpy().tolist()

Con el vector de características de texto, podemos obtener un centroide aproximado de las imágenes que deseamos, que será el parámetro inicial del clasificador. Por lo tanto, se puede definir una clase de clasificador como:

DIMS = 512
class Classifier:
    def __init__(self, xq: list):
        # inicializa el modelo con un tamaño de entrada DIMS y una salida de 1
        # ten en cuenta que se ignora el sesgo, ya que solo nos enfocamos en el resultado del producto interno
        self.model = torch.nn.Linear(DIMS, 1, bias=False)
        # convierte la consulta inicial `xq` en un parámetro tensor para inicializar los pesos
        init_weight = torch.Tensor(xq).reshape(1, -1)
        self.model.weight = torch.nn.Parameter(init_weight)
        # inicializa la pérdida y el optimizador
        self.loss = torch.nn.BCEWithLogitsLoss()
        self.optimizer = torch.optim.SGD(self.model.parameters(), lr=0.1)

Recordando , for , and , un clasificador lineal con función de activación es exactamente igual a la métrica de similitud para CLIP, realizando un producto interno simple mapeado. Por lo tanto, puedes tratar la salida como la puntuación de similitud dada el vector de entrada y el vector de decisión . Por supuesto, puedes usar la característica de texto que está cerca de las imágenes consultadas como el parámetro inicial para el clasificador. Además, la BCEwithLogitsLoss (Pérdida de entropía cruzada binaria con logits) se utiliza para alejar las muestras negativas y acercar el vector de decisión a las muestras positivas. Esto te dará una idea intuitiva de lo que está sucediendo en la parte de IA.

# Obteniendo muestras similares con la BD

Finalmente, hemos construido la base de datos de vectores con MyScale y el clasificador con nuestro texto de consulta. Ahora podemos usar su función de búsqueda de similitud para realizar la minería de negativos difíciles en el clasificador. Pero primero, debemos decirle a la base de datos qué métrica se utiliza para medir la similitud de las características.

MyScale proporciona muchos algoritmos diferentes para acelerar tu búsqueda en varias métricas. Se admiten métricas comunes como L2, coseno y IP. En este ejemplo, seguimos la configuración de CLIP y elegimos la distancia coseno como nuestra métrica para buscar los vecinos más cercanos y utilizamos un algoritmo de búsqueda de vecinos más cercanos aproximado llamado MSTG para indexar nuestra característica.

-- Creamos un índice de vector vindex en la columna vector
-- con los parámetros `metric` y `ncentroids`
ALTER TABLE unsplash_25k ADD VECTOR INDEX vindex vector TYPE MSTG('metric_type=Cosine')

Una vez que se haya construido el índice de vector, ahora podemos buscar utilizando el operador distance para realizar la recuperación del vecino más cercano.

-- Ten en cuenta que el vector de consulta debe convertirse en una cadena antes de ejecutarlo
SELECT id, url, vector, distance(vector, <vector-de-consulta>) AS dist FROM unsplash_25k ORDER BY dist LIMIT 9

Ten en cuenta: para cualquier verbo SQL que devuelva valores como SELECT, debes usar client.query() para obtener el resultado.

Y también puedes realizar una consulta mixta que filtre las filas no deseadas:

SELECT id, url, vector, distance(vector, <query-vector>) AS dist
        FROM unsplash_25k WHERE id NOT IN ('U5pTkZL8JI4','UvdzJDxcJg4','22o6p17bCtQ', 'cyPqQXNJsG8')
        ORDER BY dist LIMIT 9

Suponiendo que hemos nombrado la sentencia SQL anterior en una cadena qstr, entonces la consulta en Python se puede hacer de la siguiente manera:

q = client.query(qstr).named_results()

El valor devuelto q tiene múltiples objetos similares a diccionarios. En este caso, tenemos 9 objetos devueltos ya que solicitamos los 9 vecinos más cercanos. Podemos usar los nombres de las columnas para obtener valores de cada elemento de q. Por ejemplo, si queremos obtener todos los ids y su distancia al vector de consulta, podemos hacerlo de la siguiente manera en Python:

id_dist = [(_q["id"], _q["dist"]) for _q in q]

# Ajuste fino del clasificador

Con el poder de MyScale, ahora podemos recuperar los vecinos más cercanos en la BD en un abrir y cerrar de ojos. El último paso de esta aplicación será ajustar el clasificador en función de la supervisión del usuario.

Omitiré el paso de diseño de la interfaz de usuario porque es demasiado narrativo para escribir en este blog 😛 Iré directo al punto cuando ocurra el entrenamiento del modelo.

# NOTA: Agrega esto al Clasificador anterior
def fit(self, X: list, y: list, iters: int = 5):
# convierte X e y a tensores
X = torch.Tensor(X)
y = torch.Tensor(y).reshape(-1, 1)
for i in range(iters):
    # reinicia los gradientes
    self.optimizer.zero_grad()
    # Normaliza el peso antes de la inferencia
    # Esto limitará el gradiente o tendrás una explosión en el vector de consulta
    self.model.weight.data = self.model.weight.data / torch.norm(self.model.weight.data, p=2, dim=-1)
    # paso hacia adelante
    out = self.model(X)
    # calcula la pérdida
    loss = self.loss(out, y)
    # paso hacia atrás
    loss.backward()
    # actualiza los pesos
    self.optimizer.step()

El código anterior te brinda un pipeline de aprendizaje con pocos ejemplos para entrenar el clasificador existente. Con solo unas pocas imágenes anotadas, el clasificador puede converger y darte una precisión impresionante para el concepto en tu mente.

El proceso de entrenamiento es trivial. Primero, recordamos que el vector de peso es generalmente un indicador que mide la similitud entre la consulta y lo deseado. Puedes considerarlo como un centroide de un cono de una esfera con el parámetro del clasificador como su vector direccional y el umbral de puntuación como su radio. Todo dentro del cono se tratará como positivo, mientras que lo exterior será negativo. Los pasos de entrenamiento empujarán el vector para cubrir tantos positivos como sea posible y alejarlo de los negativos. Siguiendo con la teoría del vector del cono, solo necesitamos un vector normalizado para describir el centroide del cono. Por lo tanto, debemos normalizar el parámetro aprendido después de cada iteración. También podemos pensar de otra manera: los positivos, que son vectores no normalizados, tirarán del centroide hacia sus posiciones, y podríamos terminar con un vector que es muy largo en magnitud pero pobre en describir la dirección entre los positivos. Esto degradará el rendimiento de la búsqueda de similitud. Normalizar el vector solo mantendrá el componente perpendicular del gradiente. Esto estabilizará nuestro resultado visual en nuestra demostración.

# En resumen

En esta demostración, revisamos cómo construir una demostración que entrena un clasificador aprendido con pocos ejemplos con MyScale. Más importante aún, también presentamos cómo usar MyScale para almacenar, indexar y buscar utilizando SQL extendido con su avanzado motor de búsqueda de vectores. ¡Espero que disfrutes de este blog!

Referencias:

  1. MultiLingual CLIP: https://huggingface.co/M-CLIP/XLM-Roberta-Large-Vit-B-32 (opens new window)
  2. CLIP: https://huggingface.co/openai/clip-vit-base-patch32 (opens new window)
  3. Conjunto de Datos Unsplash 25K: https://github.com/unsplash/datasets (opens new window)