Sora d'OpenAI, Stable Video Diffusion de Stability AI et de nombreux autres modèles texte-vidéo qui sont sortis ou apparaîtront à l'avenir font partie des tendances d'IA les plus populaires en 2024, à la suite des grands modèles de langage (LLM). Dans ce blog, nous allons créer un modèle texte-vidéo à petite échelle à partir de zéro . Nous saisirons une invite textuelle et notre modèle entraîné générera une vidéo basée sur cette invite. Ce blog couvrira tout, de la compréhension des concepts théoriques au codage de l'architecture entière et à la génération du résultat final.
Comme je n'ai pas de GPU sophistiqué, j'ai codé l'architecture à petite échelle. Voici une comparaison du temps nécessaire pour entraîner le modèle sur différents processeurs :
Vidéos de formation | Époques | Processeur | GPU A10 | GPU T4 |
---|---|---|---|---|
10K | 30 | plus de 3 heures | 1 heure | 1 h 42 min |
30K | 30 | plus de 6 heures | 1h30 | 2h30 |
100K | 30 | - | 3-4 heures | 5-6 heures |
L'exécution sur un processeur prendra évidemment beaucoup plus de temps pour entraîner le modèle. Si vous avez besoin de tester rapidement les modifications apportées au code et de voir les résultats, le processeur n'est pas le meilleur choix. Je recommande d'utiliser un GPU T4 de Colab ou Kaggle pour une formation plus efficace et plus rapide.
Voici le lien du blog qui vous guide sur la façon de créer une diffusion stable à partir de zéro : Coding Stable Diffusion from Scratch
Nous suivrons une approche similaire aux modèles traditionnels d'apprentissage automatique ou d'apprentissage profond qui s'entraînent sur un ensemble de données et sont ensuite testés sur des données invisibles. Dans le contexte de la conversion texte-vidéo, disons que nous disposons d'un ensemble de données d'entraînement de 100 000 vidéos de chiens récupérant des balles et de chats chassant des souris. Nous entraînerons notre modèle à générer des vidéos d'un chat récupérant une balle ou d'un chien poursuivant une souris.
Bien que de tels ensembles de données de formation soient facilement disponibles sur Internet, la puissance de calcul requise est extrêmement élevée. Par conséquent, nous travaillerons avec un ensemble de données vidéo d'objets en mouvement générés à partir du code Python.
Nous utiliserons l'architecture GAN (Generative Adversarial Networks) pour créer notre modèle au lieu du modèle de diffusion utilisé par OpenAI Sora. J'ai essayé d'utiliser le modèle de diffusion, mais il a planté en raison des besoins en mémoire, ce qui dépasse mes capacités. Les GAN, en revanche, sont plus faciles et plus rapides à former et à tester.
Nous utiliserons la POO (programmation orientée objet), vous devez donc en avoir une compréhension de base ainsi que les réseaux de neurones. La connaissance des GAN (Generative Adversarial Networks) n’est pas obligatoire, car nous aborderons ici leur architecture.
Sujet | Lien |
---|---|
POO | Lien vidéo |
Théorie des réseaux de neurones | Lien vidéo |
Architecture GAN | Lien vidéo |
Les bases de Python | Lien vidéo |
Comprendre l'architecture GAN est important car une grande partie de notre architecture en dépend. Explorons ce que c'est, ses composants et plus encore.
Le Generative Adversarial Network (GAN) est un modèle d'apprentissage profond dans lequel deux réseaux de neurones s'affrontent : l'un crée de nouvelles données (comme des images ou de la musique) à partir d'un ensemble de données donné, et l'autre essaie de déterminer si les données sont réelles ou fausses. Ce processus se poursuit jusqu'à ce que les données générées soient impossibles à distinguer de l'original.
Générer des images : les GAN créent des images réalistes à partir d'invites de texte ou modifient des images existantes, par exemple en améliorant la résolution ou en ajoutant de la couleur aux photos en noir et blanc.
Augmentation des données : ils génèrent des données synthétiques pour former d'autres modèles d'apprentissage automatique, tels que la création de données de transactions frauduleuses pour les systèmes de détection de fraude.
Informations manquantes complètes : les GAN peuvent compléter les données manquantes, comme générer des images souterraines à partir de cartes de terrain pour des applications énergétiques.
Générer des modèles 3D : ils convertissent des images 2D en modèles 3D, utiles dans des domaines tels que la santé pour créer des images d'organes réalistes pour la planification chirurgicale.
Il se compose de deux réseaux de neurones profonds : le générateur et le discriminateur . Ces réseaux s'entraînent ensemble dans une configuration contradictoire, où l'un génère de nouvelles données et l'autre évalue si les données sont réelles ou fausses.
Voici un aperçu simplifié du fonctionnement du GAN :
Analyse de l'ensemble de formation : le générateur analyse l'ensemble de formation pour identifier les attributs des données, tandis que le discriminateur analyse indépendamment les mêmes données pour connaître ses attributs.
Modification des données : Le générateur ajoute du bruit (changements aléatoires) à certains attributs des données.
Passage de données : Les données modifiées sont ensuite transmises au discriminateur.
Calcul de probabilité : le discriminateur calcule la probabilité que les données générées proviennent de l'ensemble de données d'origine.
Boucle de rétroaction : le discriminateur fournit une rétroaction au générateur, le guidant pour réduire le bruit aléatoire lors du cycle suivant.
Entraînement contradictoire : Le générateur essaie de maximiser les erreurs du discriminateur, tandis que le discriminateur essaie de minimiser ses propres erreurs. Grâce à de nombreuses itérations de formation, les deux réseaux s’améliorent et évoluent.
État d'équilibre : la formation se poursuit jusqu'à ce que le discriminateur ne puisse plus faire la distinction entre les données réelles et synthétisées, indiquant que le générateur a appris avec succès à produire des données réalistes. À ce stade, le processus de formation est terminé.
image du guide AWS
Expliquons le modèle GAN avec un exemple de traduction d'image à image, en nous concentrant sur la modification d'un visage humain.
Image d'entrée : L'entrée est une image réelle d'un visage humain.
Modification d'attributs : Le générateur modifie les attributs du visage, comme l'ajout de lunettes de soleil aux yeux.
Images générées : Le générateur crée un ensemble d'images avec des lunettes de soleil ajoutées.
Tâche du discriminateur : Le discriminateur reçoit un mélange d'images réelles (personnes portant des lunettes de soleil) et d'images générées (visages sur lesquels des lunettes de soleil ont été ajoutées).
Évaluation : Le discriminateur tente de différencier les images réelles et générées.
Boucle de rétroaction : Si le discriminateur identifie correctement les fausses images, le générateur ajuste ses paramètres pour produire des images plus convaincantes. Si le générateur réussit à tromper le discriminateur, celui-ci met à jour ses paramètres pour améliorer sa détection.
Grâce à ce processus contradictoire, les deux réseaux s’améliorent continuellement. Le générateur s'améliore dans la création d'images réalistes, et le discriminateur s'améliore dans l'identification des contrefaçons jusqu'à ce que l'équilibre soit atteint, où le discriminateur ne peut plus faire la différence entre les images réelles et générées. À ce stade, le GAN a appris avec succès à produire des modifications réalistes.
L'installation des bibliothèques requises est la première étape de la création de notre modèle texte-vidéo.
pip install -r requirements.txt
Nous allons travailler avec une gamme de bibliothèques Python, importons-les.
# Operating System module for interacting with the operating system
import os
# Module for generating random numbers
import random
# Module for numerical operations
import numpy as np
# OpenCV library for image processing
import cv2
# Python Imaging Library for image processing
from PIL import Image , ImageDraw , ImageFont
# PyTorch library for deep learning
import torch
# Dataset class for creating custom datasets in PyTorch
from torch . utils . data import Dataset
# Module for image transformations
import torchvision . transforms as transforms
# Neural network module in PyTorch
import torch . nn as nn
# Optimization algorithms in PyTorch
import torch . optim as optim
# Function for padding sequences in PyTorch
from torch . nn . utils . rnn import pad_sequence
# Function for saving images in PyTorch
from torchvision . utils import save_image
# Module for plotting graphs and images
import matplotlib . pyplot as plt
# Module for displaying rich content in IPython environments
from IPython . display import clear_output , display , HTML
# Module for encoding and decoding binary data to text
import base64
Maintenant que nous avons importé toutes nos bibliothèques, l'étape suivante consiste à définir nos données de formation que nous utiliserons pour former notre architecture GAN.
Nous devons disposer d'au moins 10 000 vidéos comme données de formation. Pourquoi? Eh bien, parce que j'ai testé avec des chiffres plus petits et les résultats étaient très mauvais, pratiquement rien à voir. La prochaine grande question est : de quoi parlent ces vidéos ? Notre ensemble de données vidéo de formation consiste en un cercle se déplaçant dans différentes directions avec différents mouvements. Alors codons-le et générons 10 000 vidéos pour voir à quoi cela ressemble.
# Create a directory named 'training_dataset'
os . makedirs ( 'training_dataset' , exist_ok = True )
# Define the number of videos to generate for the dataset
num_videos = 10000
# Define the number of frames per video (1 Second Video)
frames_per_video = 10
# Define the size of each image in the dataset
img_size = ( 64 , 64 )
# Define the size of the shapes (Circle)
shape_size = 10
après avoir défini certains paramètres de base, nous devons ensuite définir les invites textuelles de notre ensemble de données de formation en fonction des vidéos de formation qui seront générées.
# Define text prompts and corresponding movements for circles
prompts_and_movements = [
( "circle moving down" , "circle" , "down" ), # Move circle downward
( "circle moving left" , "circle" , "left" ), # Move circle leftward
( "circle moving right" , "circle" , "right" ), # Move circle rightward
( "circle moving diagonally up-right" , "circle" , "diagonal_up_right" ), # Move circle diagonally up-right
( "circle moving diagonally down-left" , "circle" , "diagonal_down_left" ), # Move circle diagonally down-left
( "circle moving diagonally up-left" , "circle" , "diagonal_up_left" ), # Move circle diagonally up-left
( "circle moving diagonally down-right" , "circle" , "diagonal_down_right" ), # Move circle diagonally down-right
( "circle rotating clockwise" , "circle" , "rotate_clockwise" ), # Rotate circle clockwise
( "circle rotating counter-clockwise" , "circle" , "rotate_counter_clockwise" ), # Rotate circle counter-clockwise
( "circle shrinking" , "circle" , "shrink" ), # Shrink circle
( "circle expanding" , "circle" , "expand" ), # Expand circle
( "circle bouncing vertically" , "circle" , "bounce_vertical" ), # Bounce circle vertically
( "circle bouncing horizontally" , "circle" , "bounce_horizontal" ), # Bounce circle horizontally
( "circle zigzagging vertically" , "circle" , "zigzag_vertical" ), # Zigzag circle vertically
( "circle zigzagging horizontally" , "circle" , "zigzag_horizontal" ), # Zigzag circle horizontally
( "circle moving up-left" , "circle" , "up_left" ), # Move circle up-left
( "circle moving down-right" , "circle" , "down_right" ), # Move circle down-right
( "circle moving down-left" , "circle" , "down_left" ), # Move circle down-left
]
Nous avons défini plusieurs mouvements de notre cercle à l'aide de ces invites. Maintenant, nous devons coder quelques équations mathématiques pour déplacer ce cercle en fonction des invites.
# defining function to create image with moving shape
def create_image_with_moving_shape ( size , frame_num , shape , direction ):
# Create a new RGB image with specified size and white background
img = Image . new ( 'RGB' , size , color = ( 255 , 255 , 255 ))
# Create a drawing context for the image
draw = ImageDraw . Draw ( img )
# Calculate the center coordinates of the image
center_x , center_y = size [ 0 ] // 2 , size [ 1 ] // 2
# Initialize position with center for all movements
position = ( center_x , center_y )
# Define a dictionary mapping directions to their respective position adjustments or image transformations
direction_map = {
# Adjust position downwards based on frame number
"down" : ( 0 , frame_num * 5 % size [ 1 ]),
# Adjust position to the left based on frame number
"left" : ( - frame_num * 5 % size [ 0 ], 0 ),
# Adjust position to the right based on frame number
"right" : ( frame_num * 5 % size [ 0 ], 0 ),
# Adjust position diagonally up and to the right
"diagonal_up_right" : ( frame_num * 5 % size [ 0 ], - frame_num * 5 % size [ 1 ]),
# Adjust position diagonally down and to the left
"diagonal_down_left" : ( - frame_num * 5 % size [ 0 ], frame_num * 5 % size [ 1 ]),
# Adjust position diagonally up and to the left
"diagonal_up_left" : ( - frame_num * 5 % size [ 0 ], - frame_num * 5 % size [ 1 ]),
# Adjust position diagonally down and to the right
"diagonal_down_right" : ( frame_num * 5 % size [ 0 ], frame_num * 5 % size [ 1 ]),
# Rotate the image clockwise based on frame number
"rotate_clockwise" : img . rotate ( frame_num * 10 % 360 , center = ( center_x , center_y ), fillcolor = ( 255 , 255 , 255 )),
# Rotate the image counter-clockwise based on frame number
"rotate_counter_clockwise" : img . rotate ( - frame_num * 10 % 360 , center = ( center_x , center_y ), fillcolor = ( 255 , 255 , 255 )),
# Adjust position for a bouncing effect vertically
"bounce_vertical" : ( 0 , center_y - abs ( frame_num * 5 % size [ 1 ] - center_y )),
# Adjust position for a bouncing effect horizontally
"bounce_horizontal" : ( center_x - abs ( frame_num * 5 % size [ 0 ] - center_x ), 0 ),
# Adjust position for a zigzag effect vertically
"zigzag_vertical" : ( 0 , center_y - frame_num * 5 % size [ 1 ]) if frame_num % 2 == 0 else ( 0 , center_y + frame_num * 5 % size [ 1 ]),
# Adjust position for a zigzag effect horizontally
"zigzag_horizontal" : ( center_x - frame_num * 5 % size [ 0 ], center_y ) if frame_num % 2 == 0 else ( center_x + frame_num * 5 % size [ 0 ], center_y ),
# Adjust position upwards and to the right based on frame number
"up_right" : ( frame_num * 5 % size [ 0 ], - frame_num * 5 % size [ 1 ]),
# Adjust position upwards and to the left based on frame number
"up_left" : ( - frame_num * 5 % size [ 0 ], - frame_num * 5 % size [ 1 ]),
# Adjust position downwards and to the right based on frame number
"down_right" : ( frame_num * 5 % size [ 0 ], frame_num * 5 % size [ 1 ]),
# Adjust position downwards and to the left based on frame number
"down_left" : ( - frame_num * 5 % size [ 0 ], frame_num * 5 % size [ 1 ])
}
# Check if direction is in the direction map
if direction in direction_map :
# Check if the direction maps to a position adjustment
if isinstance ( direction_map [ direction ], tuple ):
# Update position based on the adjustment
position = tuple ( np . add ( position , direction_map [ direction ]))
else : # If the direction maps to an image transformation
# Update the image based on the transformation
img = direction_map [ direction ]
# Return the image as a numpy array
return np . array ( img )
La fonction ci-dessus est utilisée pour déplacer notre cercle pour chaque image en fonction de la direction sélectionnée. Il nous suffit d'exécuter une boucle dessus jusqu'au nombre de vidéos pour générer toutes les vidéos.
# Iterate over the number of videos to generate
for i in range ( num_videos ):
# Randomly choose a prompt and movement from the predefined list
prompt , shape , direction = random . choice ( prompts_and_movements )
# Create a directory for the current video
video_dir = f'training_dataset/video_ { i } '
os . makedirs ( video_dir , exist_ok = True )
# Write the chosen prompt to a text file in the video directory
with open ( f' { video_dir } /prompt.txt' , 'w' ) as f :
f . write ( prompt )
# Generate frames for the current video
for frame_num in range ( frames_per_video ):
# Create an image with a moving shape based on the current frame number, shape, and direction
img = create_image_with_moving_shape ( img_size , frame_num , shape , direction )
# Save the generated image as a PNG file in the video directory
cv2 . imwrite ( f' { video_dir } /frame_ { frame_num } .png' , img )
Une fois que vous aurez exécuté le code ci-dessus, il générera l’intégralité de notre ensemble de données de formation. Voici à quoi ressemble la structure de nos fichiers de jeux de données de formation.
Chaque dossier de vidéo de formation contient ses images ainsi que son invite de texte. Jetons un coup d'œil à l'échantillon de notre ensemble de données de formation.
Dans notre ensemble de données d'entraînement, nous n'avons pas inclus le mouvement du cercle se déplaçant vers le haut puis vers la droite . Nous l'utiliserons comme invite de test pour évaluer notre modèle entraîné sur des données invisibles.
Un autre point important à noter est que nos données d'entraînement contiennent de nombreux échantillons dans lesquels des objets s'éloignent de la scène ou apparaissent partiellement devant la caméra, similaire à ce que nous avons observé dans les vidéos de démonstration d'OpenAI Sora.
La raison pour laquelle de tels échantillons sont inclus dans nos données d'entraînement est de tester si notre modèle peut maintenir la cohérence lorsque le cercle entre dans la scène depuis le coin même sans casser sa forme.
Maintenant que nos données de formation ont été générées, nous devons convertir les vidéos de formation en tenseurs, qui sont le principal type de données utilisé dans les frameworks d'apprentissage en profondeur comme PyTorch. De plus, effectuer des transformations telles que la normalisation contribue à améliorer la convergence et la stabilité de l'architecture de formation en adaptant les données à une plage plus petite.
Nous devons coder une classe d'ensemble de données pour les tâches de conversion texte-vidéo, qui peut lire les images vidéo et leurs invites textuelles correspondantes à partir du répertoire de l'ensemble de données d'entraînement, les rendant ainsi disponibles pour une utilisation dans PyTorch.
# Define a dataset class inheriting from torch.utils.data.Dataset
class TextToVideoDataset ( Dataset ):
def __init__ ( self , root_dir , transform = None ):
# Initialize the dataset with root directory and optional transform
self . root_dir = root_dir
self . transform = transform
# List all subdirectories in the root directory
self . video_dirs = [ os . path . join ( root_dir , d ) for d in os . listdir ( root_dir ) if os . path . isdir ( os . path . join ( root_dir , d ))]
# Initialize lists to store frame paths and corresponding prompts
self . frame_paths = []
self . prompts = []
# Loop through each video directory
for video_dir in self . video_dirs :
# List all PNG files in the video directory and store their paths
frames = [ os . path . join ( video_dir , f ) for f in os . listdir ( video_dir ) if f . endswith ( '.png' )]
self . frame_paths . extend ( frames )
# Read the prompt text file in the video directory and store its content
with open ( os . path . join ( video_dir , 'prompt.txt' ), 'r' ) as f :
prompt = f . read (). strip ()
# Repeat the prompt for each frame in the video and store in prompts list
self . prompts . extend ([ prompt ] * len ( frames ))
# Return the total number of samples in the dataset
def __len__ ( self ):
return len ( self . frame_paths )
# Retrieve a sample from the dataset given an index
def __getitem__ ( self , idx ):
# Get the path of the frame corresponding to the given index
frame_path = self . frame_paths [ idx ]
# Open the image using PIL (Python Imaging Library)
image = Image . open ( frame_path )
# Get the prompt corresponding to the given index
prompt = self . prompts [ idx ]
# Apply transformation if specified
if self . transform :
image = self . transform ( image )
# Return the transformed image and the prompt
return image , prompt
Avant de procéder au codage de l'architecture, nous devons normaliser nos données de formation. Nous utiliserons une taille de lot de 16 et mélangerons les données pour introduire plus de caractère aléatoire.
# Define a set of transformations to be applied to the data
transform = transforms . Compose ([
transforms . ToTensor (), # Convert PIL Image or numpy.ndarray to tensor
transforms . Normalize (( 0.5 ,), ( 0.5 ,)) # Normalize image with mean and standard deviation
])
# Load the dataset using the defined transform
dataset = TextToVideoDataset ( root_dir = 'training_dataset' , transform = transform )
# Create a dataloader to iterate over the dataset
dataloader = torch . utils . data . DataLoader ( dataset , batch_size = 16 , shuffle = True )
Vous avez peut-être vu dans l'architecture du transformateur où le point de départ est de convertir notre saisie de texte en intégration pour un traitement ultérieur dans une attention multi-têtes. De la même manière, ici, nous devons coder une couche d'intégration de texte sur la base de laquelle la formation de l'architecture GAN aura lieu sur nos données d'intégration. et le tenseur des images.
# Define a class for text embedding
class TextEmbedding ( nn . Module ):
# Constructor method with vocab_size and embed_size parameters
def __init__ ( self , vocab_size , embed_size ):
# Call the superclass constructor
super ( TextEmbedding , self ). __init__ ()
# Initialize embedding layer
self . embedding = nn . Embedding ( vocab_size , embed_size )
# Define the forward pass method
def forward ( self , x ):
# Return embedded representation of input
return self . embedding ( x )
La taille du vocabulaire sera basée sur nos données d'entraînement, que nous calculerons plus tard. La taille d'intégration sera de 10. Si vous travaillez avec un ensemble de données plus grand, vous pouvez également utiliser votre propre choix de modèle d'intégration disponible sur Hugging Face.
Maintenant que nous savons déjà ce que fait le générateur dans les GAN, codons cette couche puis comprenons son contenu.
class Generator ( nn . Module ):
def __init__ ( self , text_embed_size ):
super ( Generator , self ). __init__ ()
# Fully connected layer that takes noise and text embedding as input
self . fc1 = nn . Linear ( 100 + text_embed_size , 256 * 8 * 8 )
# Transposed convolutional layers to upsample the input
self . deconv1 = nn . ConvTranspose2d ( 256 , 128 , 4 , 2 , 1 )
self . deconv2 = nn . ConvTranspose2d ( 128 , 64 , 4 , 2 , 1 )
self . deconv3 = nn . ConvTranspose2d ( 64 , 3 , 4 , 2 , 1 ) # Output has 3 channels for RGB images
# Activation functions
self . relu = nn . ReLU ( True ) # ReLU activation function
self . tanh = nn . Tanh () # Tanh activation function for final output
def forward ( self , noise , text_embed ):
# Concatenate noise and text embedding along the channel dimension
x = torch . cat (( noise , text_embed ), dim = 1 )
# Fully connected layer followed by reshaping to 4D tensor
x = self . fc1 ( x ). view ( - 1 , 256 , 8 , 8 )
# Upsampling through transposed convolution layers with ReLU activation
x = self . relu ( self . deconv1 ( x ))
x = self . relu ( self . deconv2 ( x ))
# Final layer with Tanh activation to ensure output values are between -1 and 1 (for images)
x