El primer paso (y más importante) de cualquier proceso de ajuste fino es la recopilación de datos. Aquí, extraí pares de títulos de lima de mi canal en un proceso de 2 pasos.
Primero, utilicé la API de búsqueda de YouTube para Extraer las identificaciones de video Para todos los videos de mi canal. En segundo lugar, utilicé la API de video de YouTube para Extraiga el título y la URL de miniatura de cada uno de mis videos de forma larga (es decir, más de 3 minutos).
# imports
from top_secret import my_key
import requests
from isodate import parse_durationimport pandas as pd
import numpy as np
from sentence_transformers import SentenceTransformer
from datasets import DatasetDict, Dataset
channel_id = 'UCa9gErQ9AE5jT2DZLjXBIdA' # my YouTube channel ID
page_token = None # initialize page token
url = 'https://www.googleapis.com/youtube/v3/search' # YouTube search API # extract video data across multiple search result pages
video_id_list = []
while page_token != 0:
params = {
"key": my_key,
'channelId': channel_id,
'part': ["snippet","id"],
'order': "date",
'maxResults':50,
'pageToken': page_token
}
response = requests.get(url, params=params)
for raw_item in dict(response.json())['items']:
# only execute for youtube videos
if raw_item['id']['kind'] != "youtube#video":
continue
# grab video ids
video_id_list.append(raw_item['id']['videoId'])
try:
# grab next page token
page_token = dict(response.json())['nextPageToken']
except:
# if no next page token kill while loop
page_token = 0
Tenga en cuenta que necesitará una tecla API de YouTube para ejecutar el código Python anterior, que puede crear usando el Consola de Google Cloud. Para adaptar esto a su canal, solo necesita cambiar el Channel_id variable.
# extract video titles and thumbnails
url = "https://www.googleapis.com/youtube/v3/videos"
video_data_list = []for video_id in video_id_list:
params = {
"part": ["snippet","contentDetails"],
"id": video_id,
"key": my_key,
}
response = requests.get(url, params=params)
raw_dict = dict(response.json())['items'][0]
# only process videos longer than 3 minutes
iso_duration = raw_dict['contentDetails']["duration"]
if parse_duration(iso_duration).total_seconds() < 180:
continue
# extract video data
video_data = {}
video_data['video_id'] = video_id
video_data['title'] = raw_dict['snippet']['title']
video_data['thumbnail_url'] = raw_dict['snippet']['thumbnails']['high']['url']
# append data to list
video_data_list.append(video_data)
Como un paso adicional, yo creó pares negativos para miniaturas de titulares. Podemos usarlos durante el proceso de capacitación no solo para guiar el modelo con ejemplos de los cuales la incrustación debe estar muy juntos (es decir, un par positivo), sino también qué incrustación debe estar muy separada (es decir, pares negativos).
Para hacer esto, calculé la similitud entre todos los pares de título posibles utilizando la biblioteca del transformador de oraciones. Luego, para cada par positivo, coincidí con el título menos similar como un ejemplo negativo (asegurando que no hubiera duplicados).
# store data in dataframe
df = pd.DataFrame(video_data_list)# Load the model
model = SentenceTransformer("all-mpnet-base-v2")
# Encode all titles
embeddings = model.encode(df['title'].to_list())
# compute similarities
similarities = model.similarity(embeddings, embeddings)
# match least JDs least similar to positive match as the negative match
similarities_argsorted = np.argsort(similarities.numpy(), axis=1)
negative_pair_index_list = []
for i in range(len(similarities)):
# Start with the smallest similarity index for the current row
j = 0
index = int(similarities_argsorted[i][j])
# Ensure the index is unique
while index in negative_pair_index_list:
j += 1 # Move to the next smallest index
index = int(similarities_argsorted[i][j]) # Fetch next smallest index
negative_pair_index_list.append(index)
# add negative pairs to df
df['title_neg'] = df['title'].iloc[negative_pair_index_list].values
Finalmente, creé un división de prueba de válido y empujó el conjunto de datos al centro de la cara abrazando.
# Shuffle the dataset
df = df.sample(frac=1, random_state=42).reset_index(drop=True)# Split into train, validation, and test sets
train_frac = 0.7
valid_frac = 0.15
test_frac = 0.15
# define train and validation size
train_size = int(train_frac * len(df))
valid_size = int(valid_frac * len(df))
# create train, validation, and test datasets
df_train = df[:train_size]
df_valid = df[train_size:train_size + valid_size]
df_test = df[train_size + valid_size:]
# Convert the pandas DataFrames back to Hugging Face Datasets
train_ds = Dataset.from_pandas(df_train)
valid_ds = Dataset.from_pandas(df_valid)
test_ds = Dataset.from_pandas(df_test)
# Combine into a DatasetDict
dataset_dict = DatasetDict({
'train': train_ds,
'valid': valid_ds,
'test': test_ds
})
# push data to hub
dataset_dict.push_to_hub("shawhin/yt-title-thumbnail-pairs")
Aunque tenemos todos los datos que necesitamos para ajustar, todavía no es un formato adecuado para el entrenamiento. Más específicamente, necesitamos Convierta nuestras URL de imagen a objetos de imagen PIL y Organizar nuestros datos en (Anchor, Positive, negativo) trillizos, es decir, una miniatura, su título correspondiente y título negativo, respectivamente.
Podemos procesar las tres divisiones de datos (es decir, tren, válido y probar) de la siguiente manera utilizando la biblioteca de conjuntos de datos de abrazaderas.
from PIL import Image# load dataset
dataset = load_dataset("shawhin/yt-title-thumbnail-pairs")
# define preprocessing function
def preprocess(batch):
"""
Preprocessing data without augmentations for test set
"""
# get images from urls
image_list = [Image.open(requests.get(url, stream=True).raw)
for url in batch["thumbnail_url"]]
# return columns with standard names
return {
"anchor": image_list,
"positive": batch["title"],
"negative": batch["title_neg"]
}
# remove columns not relevant to training
columns_to_remove = [col for col in dataset['train'].column_names
if col not in ['anchor', 'positive', 'negative']]
# apply transformations
dataset = dataset.map(preprocess, batched=True,
remove_columns=columns_to_remove)
Es importante que ordenemos nuestras columnas como trillizos (ancla, positivo, negativo) porque Este es el formato esperado por la función de pérdida Usaremos durante el entrenamiento (que aprendí de la manera difícil).
La capacitación implica optimizar los parámetros de un modelo para minimizar una función de pérdida. Sin embargo, este valor (es decir, una pérdida de contraste) rara vez es útil en Evaluar el rendimiento del modelo en una tarea aguas abajo (por ejemplo, títulos coincidentes con las miniaturas).
Una cantidad que es más perspicaz, en este caso, es la capacidad del modelo para correctamente coincidir con una miniatura dada con el título correcto entre varios candidatos. Esto se denota Recuerde@1.
Podemos implementar un evaluador compatible con la biblioteca de transformadores de oraciones para calcular esta métrica. Como el código es bastante largo, no lo pegaré aquí, pero el lector curioso puede encontrarlo en la celda 12 de este cuaderno.
# function to create new evaluator given data split
def create_recall_evaluator(set_name, k=1):
"""
Create triplet evaluator for "train", "valid", or "test" split
"""return ImageTextRetrievalEvaluator(
images=dataset[f"{set_name}"]["anchor"],
texts=dataset[f"{set_name}"]["positive"],
name=f"yt-title-thumbnail-{set_name}",
k=k
)
# Create new evaluator with Recall@k
evaluator_recall_train = create_recall_evaluator("train", k=1)
evaluator_recall_valid = create_recall_evaluator("valid", k=1)
print("Train:", evaluator_recall_train(model))
print("Valid:", evaluator_recall_valid(model))
# >> Train: {'yt-title-thumbnail-train_Recall@1': 0.660377358490566}
# >> Valid: {'yt-title-thumbnail-valid_Recall@1': 0.6363636363636364}
Podemos ver que el modelo ya tiene un rendimiento decente fuera de la caja, con títulos correctos que coinciden el 66% del tiempo.
Hay 3 cosas clave Debemos hacerlo antes de entrenar el modelo. A saber, elija qué parámetros entrenar, elegir una función de pérdida y establecer hiperparámetros.
Parámetros entrenables
La limitación clave de este proyecto es que solo he publicado 76 videos de YouTube (a partir de escribir esto). Con las divisiones de validación y prueba, esto deja Solo 53 ejemplos para capacitación.
Ya que tenemos tan pocos ejemplos de capacitación, Limitar el número de parámetros que entrenamos es una buena idea. En este caso, solo entreno la capa de proyección final del modelo, que mapea los incrustaciones de texto y imagen en un espacio vectorial compartido. Esto es aproximadamente 1M parámetros en total.
# import model
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("sentence-transformers/clip-ViT-L-14")# pick specific layers to train (note: you can add more layers to this list)
trainable_layers_list = ['projection']
# Apply freezing configuration
for name, param in model.named_parameters():
# freeze all params
param.requires_grad = False
# unfreeze layers in trainable_layers_list
if any(layer in name for layer in trainable_layers_list):
param.requires_grad = True
# Count total and trainable parameters
total_params = sum(p.numel() for p in model.parameters())
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)print(f"Total parameters: {total_params:,}")
print(f"Trainable parameters: {trainable_params:,}")
print(f"% of trainable parameters: {100*trainable_params/total_params:.2f}%")
# >> Total parameters: 427,616,513
# >> Trainable parameters: 1,376,256
# >> % of trainable parameters: 0.32%
Función de pérdida
Aquí, uso el Múltiples pérdidas de clasificación negativa de la biblioteca de transformadores de oración (que funciona con negativos individuales como en este caso). Funciona por Maximizando la similitud entre pares positivos mientras minimizar la similitud entre pares negativos. Así es como se ve la función de pérdida para el caso negativo único [2].
from sentence_transformers.losses import MultipleNegativesRankingLoss# define loss
loss = MultipleNegativesRankingLoss(model)
Hiperparámetros
Para los hiperparámetros, experimenté con un puñado de opciones manualmente y elegí la elección con la mejor pérdida de validación y el rendimiento de recuperación@1. Aquí están las opciones finales.
from sentence_transformers import SentenceTransformerTrainingArguments# hyperparameters
num_epochs = 2
batch_size = 16
lr = 1e-4
finetuned_model_name = "clip-title-thumbnail-embeddings"
train_args = SentenceTransformerTrainingArguments(
output_dir=f"models/{finetuned_model_name}",
num_train_epochs=num_epochs,
per_device_train_batch_size=batch_size,
per_device_eval_batch_size=batch_size,
learning_rate=lr,
# Evaluation settings
eval_strategy="epoch",
eval_steps=1,
logging_steps=1,
)
Con nuestra pérdida e hiperparámetros definidos, podemos entrenar el modelo utilizando SentencErtansformerStrainer ().
from sentence_transformers import SentenceTransformerTrainertrainer = SentenceTransformerTrainer(
model=model,
args=train_args,
train_dataset=dataset["train"],
eval_dataset=dataset["valid"],
loss=loss,
evaluator=[evaluator_recall_train, evaluator_recall_valid],
)
trainer.train()
El entrenamiento modelo es un proceso iterativo Donde puede explorar docenas de modelos para diferentes opciones de parámetros capacitables, funciones de pérdida e hiperparámetros.
Sin embargo, lo recomiendo encarecidamente Mantener estos experimentos lo más simple posible. Si te encuentras pasando demasiado tiempo ajustando los argumentos de capacitación para que tu modelo converja, probablemente haya algo fundamentalmente malo con tus datos (hablando de la experiencia 😅).
Como paso final, podemos evaluar el puntaje de recuperación del modelo@1 en el conjunto de pruebas. Estos datos no se utilizaron para el entrenamiento o el ajuste de los hiperparámetros, por lo que nos brinda una evaluación imparcial del modelo.
evaluator_recall_test = create_recall_evaluator("test")print("Train:", evaluator_recall_train(model))
print("Valid:", evaluator_recall_valid(model))
print("Test:", evaluator_recall_test(model))
# >> Train: {'yt-title-thumbnail-train_Recall@1': 0.8490566037735849}
# >> Valid: {'yt-title-thumbnail-valid_Recall@1': 0.9090909090909091}
# >> Test: {'yt-title-thumbnail-test_Recall@1': 0.75}
Vemos que el modelo funciona bien en los tres conjuntos de datos con 75% de recuperación@1 en el conjunto de pruebas. En otras palabras, el 75% del tiempo, el modelo coincide correctamente con una miniatura dada con su título original. ¡Además, el retiro para el conjunto de datos de validación aumenta en un 27%!
Los modelos de incrustación multimodal, como el clip, desbloquean innumerables casos de uso de 0 disparos, como la clasificación y recuperación de imágenes. Aquí, vimos cómo podemos ajustar ese modelo para adaptarlo a un dominio especializado (es decir, mis títulos y miniaturas de YouTube).
Aunque el clip es un modelo pequeño según los estándares actuales (~ 500m parámetros) y nuestro conjunto de datos de entrenamiento fue pequeño, El modelo final aún demostró un fuerte rendimiento en esta tarea. Esto resalta el poder del ajuste.
Si tiene alguna pregunta o sugerencia para contenido futuro, avíseme en los comentarios 🙂
Más sobre IA multimodal 👇