Sora von OpenAI, Stable Video Diffusion von Stability AI und viele andere Text-zu-Video-Modelle, die herausgekommen sind oder in Zukunft erscheinen werden, gehören nach großen Sprachmodellen (LLMs) zu den beliebtesten KI-Trends im Jahr 2024. In diesem Blog werden wir von Grund auf ein kleines Text-zu-Video-Modell erstellen. Wir geben eine Textaufforderung ein und unser trainiertes Modell generiert auf Grundlage dieser Aufforderung ein Video. In diesem Blog wird alles behandelt, vom Verständnis der theoretischen Konzepte über die Codierung der gesamten Architektur bis hin zur Generierung des Endergebnisses.
Da ich keine ausgefallene GPU habe, habe ich die kleine Architektur codiert. Hier ist ein Vergleich der Zeit, die zum Trainieren des Modells auf verschiedenen Prozessoren erforderlich ist:
Schulungsvideos | Epochen | CPU | GPU A10 | GPU T4 |
---|---|---|---|---|
10K | 30 | mehr als 3 Std | 1 Std | 1 Stunde und 42 Minuten |
30.000 | 30 | mehr als 6 Std | 1 Std. 30 | 2 Std. 30 |
100.000 | 30 | - | 3-4 Std | 5-6 Std |
Die Ausführung auf einer CPU dauert offensichtlich viel länger, um das Modell zu trainieren. Wenn Sie Änderungen im Code schnell testen und Ergebnisse sehen müssen, ist CPU nicht die beste Wahl. Für ein effizienteres und schnelleres Training empfehle ich die Verwendung einer T4-GPU von Colab oder Kaggle.
Hier ist der Blog-Link, der Sie dabei unterstützt, wie Sie eine stabile Diffusion von Grund auf erstellen: Coding Stable Diffusion from Scratch
Wir werden einen ähnlichen Ansatz wie bei herkömmlichen Modellen für maschinelles Lernen oder Deep Learning verfolgen, die anhand eines Datensatzes trainieren und dann anhand unsichtbarer Daten getestet werden. Nehmen wir im Kontext von Text-to-Video an, wir haben einen Trainingsdatensatz mit 100.000 Videos von Hunden, die Bälle apportieren, und Katzen, die Mäuse jagen. Wir werden unser Modell trainieren, um Videos von einer Katze zu erzeugen, die einen Ball holt, oder von einem Hund, der eine Maus jagt.
Obwohl solche Trainingsdatensätze leicht im Internet verfügbar sind, ist die erforderliche Rechenleistung extrem hoch. Daher werden wir mit einem Videodatensatz von sich bewegenden Objekten arbeiten, der aus Python-Code generiert wurde.
Wir werden die GAN-Architektur (Generative Adversarial Networks) verwenden, um unser Modell zu erstellen, anstelle des Diffusionsmodells, das OpenAI Sora verwendet. Ich habe versucht, das Diffusionsmodell zu verwenden, aber es stürzte aufgrund von Speicheranforderungen ab, die meine Kapazitäten übersteigen. GANs hingegen sind einfacher und schneller zu trainieren und zu testen.
Wir werden OOP (objektorientierte Programmierung) verwenden, Sie müssen also über grundlegende Kenntnisse davon und neuronaler Netze verfügen. Kenntnisse über GANs (Generative Adversarial Networks) sind nicht zwingend erforderlich, da wir uns hier mit deren Architektur befassen.
Thema | Link |
---|---|
OOP | Videolink |
Theorie neuronaler Netze | Videolink |
GAN-Architektur | Videolink |
Python-Grundlagen | Videolink |
Es ist wichtig, die GAN-Architektur zu verstehen, da ein Großteil unserer Architektur davon abhängt. Lassen Sie uns untersuchen, was es ist, welche Komponenten es enthält und vieles mehr.
Generative Adversarial Network (GAN) ist ein Deep-Learning-Modell, bei dem zwei neuronale Netze miteinander konkurrieren: eines erstellt neue Daten (wie Bilder oder Musik) aus einem bestimmten Datensatz und das andere versucht herauszufinden, ob die Daten echt oder gefälscht sind. Dieser Prozess wird so lange fortgesetzt, bis die generierten Daten nicht mehr vom Original zu unterscheiden sind.
Bilder generieren : GANs erstellen realistische Bilder aus Textaufforderungen oder ändern vorhandene Bilder, indem sie beispielsweise die Auflösung erhöhen oder Schwarzweißfotos Farbe hinzufügen.
Datenerweiterung : Sie generieren synthetische Daten, um andere Modelle des maschinellen Lernens zu trainieren, beispielsweise um betrügerische Transaktionsdaten für Betrugserkennungssysteme zu erstellen.
Fehlende Informationen vervollständigen : GANs können fehlende Daten ergänzen, beispielsweise durch die Erstellung von Untergrundbildern aus Geländekarten für Energieanwendungen.
Generieren Sie 3D-Modelle : Sie konvertieren 2D-Bilder in 3D-Modelle, die in Bereichen wie dem Gesundheitswesen nützlich sind, um realistische Organbilder für die Operationsplanung zu erstellen.
Es besteht aus zwei tiefen neuronalen Netzen: dem Generator und dem Diskriminator . Diese Netzwerke trainieren gemeinsam in einem kontradiktorischen Setup, wobei eines neue Daten generiert und das andere bewertet, ob die Daten echt oder gefälscht sind.
Hier ist eine vereinfachte Übersicht über die Funktionsweise von GAN:
Analyse des Trainingssatzes : Der Generator analysiert den Trainingssatz, um Datenattribute zu identifizieren, während der Diskriminator unabhängig dieselben Daten analysiert, um ihre Attribute zu lernen.
Datenänderung : Der Generator fügt einigen Attributen der Daten Rauschen (zufällige Änderungen) hinzu.
Datenübergabe : Die geänderten Daten werden dann an den Diskriminator übergeben.
Wahrscheinlichkeitsberechnung : Der Diskriminator berechnet die Wahrscheinlichkeit, dass die generierten Daten aus dem Originaldatensatz stammen.
Rückkopplungsschleife : Der Diskriminator liefert eine Rückmeldung an den Generator und leitet ihn an, um zufälliges Rauschen im nächsten Zyklus zu reduzieren.
Gegnerisches Training : Der Generator versucht, die Fehler des Diskriminators zu maximieren, während der Diskriminator versucht, seine eigenen Fehler zu minimieren. Durch viele Trainingsiterationen verbessern und entwickeln sich beide Netzwerke.
Gleichgewichtszustand : Das Training wird fortgesetzt, bis der Diskriminator nicht mehr zwischen realen und synthetisierten Daten unterscheiden kann, was darauf hinweist, dass der Generator erfolgreich gelernt hat, realistische Daten zu erzeugen. An diesem Punkt ist der Trainingsprozess abgeschlossen.
Bild aus dem AWS-Guide
Lassen Sie uns das GAN-Modell anhand eines Beispiels für die Bild-zu-Bild-Übersetzung erläutern, wobei der Schwerpunkt auf der Modifizierung eines menschlichen Gesichts liegt.
Eingabebild : Die Eingabe ist ein echtes Bild eines menschlichen Gesichts.
Attributmodifikation : Der Generator modifiziert Attribute des Gesichts, z. B. das Hinzufügen einer Sonnenbrille zu den Augen.
Erzeugte Bilder : Der Generator erstellt eine Reihe von Bildern mit hinzugefügten Sonnenbrillen.
Aufgabe des Diskriminators : Der Diskriminator erhält eine Mischung aus realen Bildern (Personen mit Sonnenbrille) und generierten Bildern (Gesichter mit Sonnenbrille).
Auswertung : Der Diskriminator versucht zwischen realen und generierten Bildern zu unterscheiden.
Rückkopplungsschleife : Wenn der Diskriminator gefälschte Bilder korrekt identifiziert, passt der Generator seine Parameter an, um überzeugendere Bilder zu erzeugen. Wenn der Generator den Diskriminator erfolgreich täuscht, aktualisiert der Diskriminator seine Parameter, um seine Erkennung zu verbessern.
Durch diesen kontradiktorischen Prozess verbessern sich beide Netzwerke kontinuierlich. Der Generator wird besser darin, realistische Bilder zu erstellen, und der Diskriminator wird besser darin, Fälschungen zu identifizieren, bis ein Gleichgewicht erreicht ist, bei dem der Diskriminator den Unterschied zwischen echten und generierten Bildern nicht mehr erkennen kann. Zu diesem Zeitpunkt hat das GAN erfolgreich gelernt, realistische Modifikationen zu erzeugen.
Die Installation der erforderlichen Bibliotheken ist der erste Schritt beim Aufbau unseres Text-zu-Video-Modells.
pip install -r requirements.txt
Wir werden mit einer Reihe von Python-Bibliotheken arbeiten. Importieren wir sie.
# 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
Nachdem wir nun alle unsere Bibliotheken importiert haben, besteht der nächste Schritt darin, unsere Trainingsdaten zu definieren, die wir zum Training unserer GAN-Architektur verwenden werden.
Wir benötigen mindestens 10.000 Videos als Trainingsdaten. Warum? Nun, weil ich mit kleineren Zahlen getestet habe und die Ergebnisse sehr schlecht waren, praktisch nichts zu sehen. Die nächste große Frage ist: Worum geht es in diesen Videos? Unser Trainingsvideodatensatz besteht aus einem Kreis, der sich mit unterschiedlichen Bewegungen in verschiedene Richtungen bewegt. Also programmieren wir es und generieren 10.000 Videos, um zu sehen, wie es aussieht.
# 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
Nachdem wir einige grundlegende Parameter festgelegt haben, müssen wir als Nächstes die Textaufforderungen unseres Trainingsdatensatzes definieren, auf deren Grundlage Trainingsvideos generiert werden.
# 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
]
Mithilfe dieser Eingabeaufforderungen haben wir mehrere Bewegungen unseres Kreises definiert. Jetzt müssen wir einige mathematische Gleichungen codieren, um diesen Kreis basierend auf den Eingabeaufforderungen zu verschieben.
# 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 )
Die obige Funktion wird verwendet, um unseren Kreis für jeden Frame basierend auf der ausgewählten Richtung zu verschieben. Wir müssen nur eine Schleife darüber laufen lassen, bis die Anzahl der Videos erreicht ist, um alle Videos zu generieren.
# 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 )
Sobald Sie den obigen Code ausführen, wird unser gesamter Trainingsdatensatz generiert. So sieht die Struktur unserer Trainingsdatensatzdateien aus.
Jeder Schulungsvideoordner enthält seine Frames zusammen mit seiner Textaufforderung. Werfen wir einen Blick auf das Beispiel unseres Trainingsdatensatzes.
In unserem Trainingsdatensatz haben wir die Bewegung des Kreises, der sich nach oben und dann nach rechts bewegt, nicht berücksichtigt. Wir werden dies als Testaufforderung verwenden, um unser trainiertes Modell anhand unsichtbarer Daten zu bewerten.
Ein weiterer wichtiger Punkt ist, dass unsere Trainingsdaten viele Beispiele enthalten, bei denen sich Objekte von der Szene entfernen oder teilweise vor der Kamera erscheinen, ähnlich wie wir es in den OpenAI Sora-Demovideos beobachtet haben.
Der Grund für die Aufnahme solcher Proben in unsere Trainingsdaten besteht darin, zu testen, ob unser Modell die Konsistenz beibehalten kann, wenn der Kreis von der äußersten Ecke aus in die Szene eintritt, ohne seine Form zu zerstören.
Nachdem unsere Trainingsdaten nun generiert wurden, müssen wir die Trainingsvideos in Tensoren konvertieren, den primären Datentyp, der in Deep-Learning-Frameworks wie PyTorch verwendet wird. Darüber hinaus trägt die Durchführung von Transformationen wie der Normalisierung dazu bei, die Konvergenz und Stabilität der Trainingsarchitektur zu verbessern, indem die Daten auf einen kleineren Bereich skaliert werden.
Wir müssen eine Datensatzklasse für Text-zu-Video-Aufgaben codieren, die Videobilder und die entsprechenden Textaufforderungen aus dem Trainingsdatensatzverzeichnis lesen und sie für die Verwendung in PyTorch verfügbar machen kann.
# 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
Bevor wir mit dem Codieren der Architektur fortfahren, müssen wir unsere Trainingsdaten normalisieren. Wir werden eine Stapelgröße von 16 verwenden und die Daten mischen, um mehr Zufälligkeit einzuführen.
# 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 )
Möglicherweise haben Sie in der Transformer-Architektur gesehen, dass der Ausgangspunkt darin besteht, unsere Texteingabe in Einbettung für die weitere Verarbeitung in Multi-Head-Aufmerksamkeit umzuwandeln. Ähnlich müssen wir hier eine Texteinbettungsschicht codieren, auf deren Grundlage das GAN-Architekturtraining für unsere Einbettungsdaten stattfindet und Bildertensor.
# 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 )
Die Größe des Wortschatzes basiert auf unseren Trainingsdaten, die wir später berechnen werden. Die Einbettungsgröße beträgt 10. Wenn Sie mit einem größeren Datensatz arbeiten, können Sie auch Ihr eigenes Einbettungsmodell verwenden, das auf Hugging Face verfügbar ist.
Da wir nun bereits wissen, was der Generator in GANs tut, wollen wir diese Ebene codieren und dann ihren Inhalt verstehen.
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