Transformers documentation
Classification de vidéos
Classification de vidéos
La classification vidéo est la tâche qui consiste à attribuer un libellé ou une classe à une vidéo entière. On s'attend à ce que chaque vidéo ne possède qu'une seule classe. Les modèles de classification vidéo prennent une vidéo en entrée et renvoient une prédiction quant à la classe à laquelle la vidéo appartient. Ces modèles peuvent être utilisés pour catégoriser le sujet d'une vidéo. Une application concrète de la classification vidéo est la reconnaissance d'actions ou d'activités, qui est utile pour les applications de fitness. Elle est également bénéfique pour les personnes malvoyantes, en particulier lorsqu'elles se déplacent.
Ce guide vous montre comment :
- Effectuer l’ajustement fin de VideoMAE sur un sous-ensemble du jeu de données UCF101.
- Utilisez votre modèle affiné pour l'inférence.
Pour consulter toutes les architectures et points de contrôle compatibles avec cette tâche, nous vous recommandons de consulter la page de la tâche.
Avant de commencer, assurez-vous d'avoir installé toutes les bibliothèques nécessaires :
pip install -q pytorchvideo transformers evaluate
Vous utiliserez PyTorchVideo (dénommé pytorchvideo) pour traiter et préparer les vidéos.
Nous vous encourage à vous connecter à votre compte Hugging Face afin de pouvoir téléverser et partager votre modèle avec la communauté. Lorsque le système vous le demande, saisissez votre jeton pour vous connecter :
>>> from huggingface_hub import notebook_login
>>> notebook_login()Charger le jeu de données UCF101
Commencez par charger un sous-ensemble du jeu de données UCF-101. Cela vous permettra de faire des essais et de vous assurer que tout fonctionne correctement avant de consacrer plus de temps à l’entraînement sur l’ensemble du jeu de données.
>>> from huggingface_hub import hf_hub_download
>>> hf_dataset_identifier = "sayakpaul/ucf101-subset"
>>> filename = "UCF101_subset.tar.gz"
>>> file_path = hf_hub_download(repo_id=hf_dataset_identifier, filename=filename, repo_type="dataset")Une fois le sous-ensemble téléchargé, vous devez extraire l'archive compressée :
>>> import tarfile
>>> with tarfile.open(file_path) as t:
... t.extractall(".")De manière générale, le jeu de données est organisé de la façon suivante :
UCF101_subset/
train/
BandMarching/
video_1.mp4
video_2.mp4
...
Archery
video_1.mp4
video_2.mp4
...
...
val/
BandMarching/
video_1.mp4
video_2.mp4
...
Archery
video_1.mp4
video_2.mp4
...
...
test/
BandMarching/
video_1.mp4
video_2.mp4
...
Archery
video_1.mp4
video_2.mp4
...
...Vous pouvez ensuite compter le nombre total de vidéos.
>>> import pathlib
>>> dataset_root_path = "UCF101_subset"
>>> dataset_root_path = pathlib.Path(dataset_root_path)>>> video_count_train = len(list(dataset_root_path.glob("train/*/*.avi")))
>>> video_count_val = len(list(dataset_root_path.glob("val/*/*.avi")))
>>> video_count_test = len(list(dataset_root_path.glob("test/*/*.avi")))
>>> video_total = video_count_train + video_count_val + video_count_test
>>> print(f"Total videos: {video_total}")>>> all_video_file_paths = (
... list(dataset_root_path.glob("train/*/*.avi"))
... + list(dataset_root_path.glob("val/*/*.avi"))
... + list(dataset_root_path.glob("test/*/*.avi"))
... )
>>> all_video_file_paths[:5]Les chemins vidéo (sorted) apparaissent ainsi :
...
'UCF101_subset/train/ApplyEyeMakeup/v_ApplyEyeMakeup_g07_c04.avi',
'UCF101_subset/train/ApplyEyeMakeup/v_ApplyEyeMakeup_g07_c06.avi',
'UCF101_subset/train/ApplyEyeMakeup/v_ApplyEyeMakeup_g08_c01.avi',
'UCF101_subset/train/ApplyEyeMakeup/v_ApplyEyeMakeup_g09_c02.avi',
'UCF101_subset/train/ApplyEyeMakeup/v_ApplyEyeMakeup_g09_c06.avi'
...Vous remarquerez qu'il existe des extraits vidéo appartenant au même groupe / à la même scène, le groupe étant désigné par g dans les chemins d'accès aux fichiers vidéo. Par exemple, v_ApplyEyeMakeup_g07_c04.avi et v_ApplyEyeMakeup_g07_c06.avi.
Pour les sous-ensembles de validation et d'évaluation, vous ne souhaitez pas que des extraits vidéo issus du même groupe ou de la même scène soient présents, afin d'éviter la fuite de données. Le sous-ensemble utilisé dans ce tutoriel prend cette information en compte.
Ensuite, vous dériverez l'ensemble des étiquettes présentes dans le jeu de données. Créez également deux dictionnaires qui seront utiles lors de l'initialisation du modèle :
label2id: associe les noms de classes à des entiers.id2label: associe les entiers aux noms de classes.
>>> class_labels = sorted({str(path).split("/")[2] for path in all_video_file_paths})
>>> label2id = {label: i for i, label in enumerate(class_labels)}
>>> id2label = {i: label for label, i in label2id.items()}
>>> print(f"Unique classes: {list(label2id.keys())}.")
# Unique classes: ['ApplyEyeMakeup', 'ApplyLipstick', 'Archery', 'BabyCrawling', 'BalanceBeam', 'BandMarching', 'BaseballPitch', 'Basketball', 'BasketballDunk', 'BenchPress'].Il existe 10 classes uniques. Pour chaque classe, le jeu d'entraînement contient 30 vidéos.
Charger un modèle pour l'ajuster
Instanciez un modèle de classification vidéo à partir d'un point de contrôle pré-entraîné et de son processeur d'images associé. L'encodeur du modèle est fourni avec des paramètres pré-entraînés, tandis que la tête de classification est initialisée aléatoirement. Le processeur d'images sera utile lors de la rédaction du pipeline de prétraitement pour notre jeu de données.
>>> from transformers import VideoMAEImageProcessor, VideoMAEForVideoClassification
>>> model_ckpt = "MCG-NJU/videomae-base"
>>> image_processor = VideoMAEImageProcessor.from_pretrained(model_ckpt)
>>> model = VideoMAEForVideoClassification.from_pretrained(
... model_ckpt,
... label2id=label2id,
... id2label=id2label,
... ignore_mismatched_sizes=True, # provide this in case you're planning to fine-tune an already fine-tuned checkpoint
... )Pendant le chargement du modèle, vous pourriez remarquer l'avertissement suivant :
Some weights of the model checkpoint at MCG-NJU/videomae-base were not used when initializing VideoMAEForVideoClassification: [..., 'decoder.decoder_layers.1.attention.output.dense.bias', 'decoder.decoder_layers.2.attention.attention.key.weight']
- This IS expected if you are initializing VideoMAEForVideoClassification from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model).
- This IS NOT expected if you are initializing VideoMAEForVideoClassification from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model).
Some weights of VideoMAEForVideoClassification were not initialized from the model checkpoint at MCG-NJU/videomae-base and are newly initialized: ['classifier.bias', 'classifier.weight']
You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.L’avertissement nous indique que nous jetons certains poids (par exemple, les poids et le biais de la couche classifier) et que nous en initialisons d’autres aléatoirement (les poids et le biais d’une nouvelle couche classifier). C’est attendu dans ce cas, car nous ajoutons une nouvelle tête pour laquelle nous ne disposons pas de poids pré-entraînés ; la bibliothèque nous avertit donc que nous devrions affiner ce modèle avant de l’utiliser pour l’inférence, ce que nous allons faire exactement.
Note que ce point de contrôle offre de meilleures performances sur cette tâche, car il a été obtenu par fine-tuning sur une tâche en aval similaire présentant un chevauchement de domaine important. Vous pouvez consulter ce point de contrôle, obtenu par fine-tuning de MCG-NJU/videomae-base-finetuned-kinetics.
Préparer les jeux de données pour l'entraînement
Pour prétraiter les vidéos, vous utiliserez la bibliothèque PyTorchVideo. Commencez par importer les dépendances nécessaires.
>>> import pytorchvideo.data
>>> from pytorchvideo.transforms import (
... ApplyTransformToKey,
... Normalize,
... RandomShortSideScale,
... RemoveKey,
... ShortSideScale,
... UniformTemporalSubsample,
... )
>>> from torchvision.transforms import (
... Compose,
... Lambda,
... RandomCrop,
... RandomHorizontalFlip,
... Resize,
... )Pour les transformations du jeu de données d’entraînement, utilisez une combinaison de sous-échantillonnage temporel uniforme, de normalisation des pixels, de recadrage aléatoire et de retournement horizontal aléatoire. Pour les transformations des jeux de données de validation et d’évaluation, conservez la même chaîne de transformations, à l’exception du recadrage aléatoire et du retournement horizontal. Pour en savoir plus sur les détails de ces transformations, consultez la documentation officielle de PyTorchVideo.
Utilisez le image_processor associé au modèle pré-entraîné pour obtenir les informations suivantes :
- Moyenne et écart-type de l'image avec lesquels les pixels des images de la vidéo seront normalisés.
- Résolution spatiale à laquelle les images de la vidéo seront redimensionnées.
Commencez par définir quelques constantes.
>>> mean = image_processor.image_mean
>>> std = image_processor.image_std
>>> if "shortest_edge" in image_processor.size:
... height = width = image_processor.size["shortest_edge"]
>>> else:
... height = image_processor.size["height"]
... width = image_processor.size["width"]
>>> resize_to = (height, width)
>>> num_frames_to_sample = model.config.num_frames
>>> sample_rate = 4
>>> fps = 30
>>> clip_duration = num_frames_to_sample * sample_rate / fpsMaintenant, définissez les transformations spécifiques au jeu de données, ainsi que les jeux de données respectifs. Commençons par le jeu d'entraînement :
>>> train_transform = Compose(
... [
... ApplyTransformToKey(
... key="video",
... transform=Compose(
... [
... UniformTemporalSubsample(num_frames_to_sample),
... Lambda(lambda x: x / 255.0),
... Normalize(mean, std),
... RandomShortSideScale(min_size=256, max_size=320),
... RandomCrop(resize_to),
... RandomHorizontalFlip(p=0.5),
... ]
... ),
... ),
... ]
... )
>>> train_dataset = pytorchvideo.data.Ucf101(
... data_path=os.path.join(dataset_root_path, "train"),
... clip_sampler=pytorchvideo.data.make_clip_sampler("random", clip_duration),
... decode_audio=False,
... transform=train_transform,
... )La même séquence de flux de travail peut être appliquée aux ensembles de validation et d'évaluation :
>>> val_transform = Compose(
... [
... ApplyTransformToKey(
... key="video",
... transform=Compose(
... [
... UniformTemporalSubsample(num_frames_to_sample),
... Lambda(lambda x: x / 255.0),
... Normalize(mean, std),
... Resize(resize_to),
... ]
... ),
... ),
... ]
... )
>>> val_dataset = pytorchvideo.data.Ucf101(
... data_path=os.path.join(dataset_root_path, "val"),
... clip_sampler=pytorchvideo.data.make_clip_sampler("uniform", clip_duration),
... decode_audio=False,
... transform=val_transform,
... )
>>> test_dataset = pytorchvideo.data.Ucf101(
... data_path=os.path.join(dataset_root_path, "test"),
... clip_sampler=pytorchvideo.data.make_clip_sampler("uniform", clip_duration),
... decode_audio=False,
... transform=val_transform,
... )Note : les pipelines de jeu de données ci-dessus sont issus de l'exemple officiel de PyTorchVideo. Nous utilisons la fonction pytorchvideo.data.Ucf101() car elle est conçue spécifiquement pour le jeu de données UCF-101. En interne, elle renvoie un objet pytorchvideo.data.labeled_video_dataset.LabeledVideoDataset. La classe LabeledVideoDataset est la classe de base pour tout ce qui concerne la vidéo dans les jeux de données PyTorchVideo. Ainsi, si vous souhaitez utiliser un jeu de données personnalisé non pris en charge nativement par PyTorchVideo, vous pouvez étendre la classe LabeledVideoDataset en conséquence. Consultez la documentation de l'API data pour en savoir plus. Par ailleurs, si votre jeu de données suit une structure similaire (comme illustré ci-dessus), l'utilisation de pytorchvideo.data.Ucf101() devrait fonctionner sans problème.
Vous pouvez accéder à l’argument num_videos pour connaître le nombre de vidéos dans le jeu de données.
>>> print(train_dataset.num_videos, val_dataset.num_videos, test_dataset.num_videos)
# (300, 30, 75)Visualiser la vidéo prétraitée pour un débogage plus efficace
>>> import imageio
>>> import numpy as np
>>> from IPython.display import Image
>>> def unnormalize_img(img):
... """Un-normalizes the image pixels."""
... img = (img * std) + mean
... img = (img * 255).astype("uint8")
... return img.clip(0, 255)
>>> def create_gif(video_tensor, filename="sample.gif"):
... """Prepares a GIF from a video tensor.
...
... The video tensor is expected to have the following shape:
... (num_frames, num_channels, height, width).
... """
... frames = []
... for video_frame in video_tensor:
... frame_unnormalized = unnormalize_img(video_frame.permute(1, 2, 0).numpy())
... frames.append(frame_unnormalized)
... kargs = {"duration": 0.25}
... imageio.mimsave(filename, frames, "GIF", **kargs)
... return filename
>>> def display_gif(video_tensor, gif_name="sample.gif"):
... """Prepares and displays a GIF from a video tensor."""
... video_tensor = video_tensor.permute(1, 0, 2, 3)
... gif_filename = create_gif(video_tensor, gif_name)
... return Image(filename=gif_filename)
>>> sample_video = next(iter(train_dataset))
>>> video_tensor = sample_video["video"]
>>> display_gif(video_tensor)
Entraîner le modèle
Utilisez Trainer de 🤗 Transformers pour entraîner le modèle. Pour instancier un Trainer, vous devez définir la configuration d’entraînement et une métrique d’évaluation. L’élément le plus important est TrainingArguments, une classe qui contient tous les attributs permettant de configurer l’entraînement. Elle requiert le nom d’un dossier de sortie, qui sera utilisé pour enregistrer les points de contrôle du modèle. Elle permet également de synchroniser toutes les informations du dépôt du modèle sur 🤗 Hub.
La plupart des arguments d’entraînement sont explicites, mais l’un d’entre eux est particulièrement important ici : remove_unused_columns=False. Cet argument supprime les caractéristiques non utilisées par la fonction d’appel du modèle. Par défaut, il est défini sur True, car il est généralement idéal de supprimer les colonnes de caractéristiques inutilisées, ce qui facilite le dépaquetage des entrées dans la fonction d’appel du modèle. Cependant, dans ce cas, vous avez besoin des caractéristiques inutilisées (en particulier « video ») afin de créer pixel_values (qui est une clé obligatoire que notre modèle attend dans ses entrées).
>>> from transformers import TrainingArguments, Trainer
>>> model_name = model_ckpt.split("/")[-1]
>>> new_model_name = f"{model_name}-finetuned-ucf101-subset"
>>> num_epochs = 4
>>> args = TrainingArguments(
... new_model_name,
... remove_unused_columns=False,
... eval_strategy="epoch",
... save_strategy="epoch",
... learning_rate=5e-5,
... per_device_train_batch_size=batch_size,
... per_device_eval_batch_size=batch_size,
... warmup_steps=0.1,
... logging_steps=10,
... load_best_model_at_end=True,
... metric_for_best_model="accuracy",
... push_to_hub=True,
... max_steps=(train_dataset.num_videos // batch_size) * num_epochs,
... )Le jeu de données renvoyé par pytorchvideo.data.Ucf101() n’implémente pas la méthode __len__. Par conséquent, il est nécessaire de définir max_steps lors de l’instantiation de TrainingArguments.
Ensuite, vous devez définir une fonction pour calculer les métriques à partir des prédictions, qui utilisera le metric que vous allez charger. La seule prétraitement à effectuer consiste à prendre l'argmax de nos logits prédits :
import evaluate
metric = evaluate.load("accuracy")
def compute_metrics(eval_pred):
predictions = np.argmax(eval_pred.predictions, axis=1)
return metric.compute(predictions=predictions, references=eval_pred.label_ids)Une note sur l'évaluation :
Dans l'article VideoMAE, les auteurs utilisent la stratégie d'évaluation suivante. Ils évaluent le modèle sur plusieurs extraits de vidéos de test, appliquent différents recadrages à ces extraits et rapportent le score agrégé. Toutefois, par souci de simplicité et de concision, nous ne prenons pas en compte cet aspect dans ce tutoriel.
De plus, définissez une collate_fn, qui sera utilisée pour regrouper les exemples en lots. Chaque lot est composé de 2 clés, à savoir pixel_values et labels.
>>> def collate_fn(examples):
... # permute to (num_frames, num_channels, height, width)
... pixel_values = torch.stack(
... [example["video"].permute(1, 0, 2, 3) for example in examples]
... )
... labels = torch.tensor([example["label"] for example in examples])
... return {"pixel_values": pixel_values, "labels": labels}Ensuite, vous transmettez simplement tout cela ainsi que les jeux de données à Trainer :
>>> trainer = Trainer(
... model,
... args,
... train_dataset=train_dataset,
... eval_dataset=val_dataset,
... processing_class=image_processor,
... compute_metrics=compute_metrics,
... data_collator=collate_fn,
... )Vous vous demandez peut-être pourquoi vous avez transmis image_processor comme un tokenizer, alors que vous avez déjà prétraité les données. Cela sert uniquement à s'assurer que le fichier de configuration du processeur d'images (stocké au format JSON) sera également téléversé dans le dépôt sur le Hub.
Ajustez maintenant notre modèle en appelant la méthode train :
>>> train_results = trainer.train()Une fois l’entraînement terminé, partagez votre modèle sur le Hub à l’aide de la méthode push_to_hub() afin que chacun puisse l’utiliser :
>>> trainer.push_to_hub()Inférence
Parfait, maintenant que vous avez affiné un modèle, vous pouvez l'utiliser pour l'inférence !
Charger une vidéo pour l’inférence :
>>> sample_test_video = next(iter(test_dataset))
La manière la plus simple de tester votre modèle fine-tuné pour l’inférence est de l’utiliser dans un VideoClassificationPipeline. Instanciez un pipeline pour la classification de vidéos avec votre modèle, puis passez-lui votre vidéo :
>>> from transformers import pipeline
from accelerate import Accelerator
>>> video_cls = pipeline(model="my_awesome_video_cls_model")
>>> video_cls("https://huggingface.co/datasets/sayakpaul/ucf101-subset/resolve/main/v_BasketballDunk_g14_c06.avi")
[{'score': 0.9272987842559814, 'label': 'BasketballDunk'},
{'score': 0.017777055501937866, 'label': 'BabyCrawling'},
{'score': 0.01663011871278286, 'label': 'BalanceBeam'},
{'score': 0.009560945443809032, 'label': 'BandMarching'},
{'score': 0.0068979403004050255, 'label': 'BaseballPitch'}]Vous pouvez également reproduire manuellement les résultats de pipeline si vous le souhaitez.
>>> def run_inference(model, video):
... # (num_frames, num_channels, height, width)
... permuted_sample_test_video = video.permute(1, 0, 2, 3)
... inputs = {
... "pixel_values": perumuted_sample_test_video.unsqueeze(0),
... "labels": torch.tensor(
... [sample_test_video["label"]]
... ), # this can be skipped if you don't have labels available.
... }
... device = Accelerator().device
... inputs = {k: v.to(device) for k, v in inputs.items()}
... model = model.to(device)
... # forward pass
... with torch.no_grad():
... outputs = model(**inputs)
... logits = outputs.logits
... return logitsMaintenant, passez votre entrée au modèle et renvoyez les logits :
>>> logits = run_inference(trained_model, sample_test_video["video"])En décodant les logits, nous obtenons :
>>> predicted_class_idx = logits.argmax(-1).item()
>>> print("Predicted class:", model.config.id2label[predicted_class_idx])
# Predicted class: BasketballDunk