تُعد Sora من OpenAI، وStable Video Diffusion من Stability AI، والعديد من نماذج تحويل النص إلى فيديو الأخرى التي ظهرت أو ستظهر في المستقبل، من بين اتجاهات الذكاء الاصطناعي الأكثر شيوعًا في عام 2024، بعد نماذج اللغات الكبيرة (LLMs). في هذه المدونة، سنقوم ببناء نموذج صغير الحجم لتحويل النص إلى فيديو من الصفر . سنقوم بإدخال مطالبة نصية، وسيقوم نموذجنا المدرب بإنشاء فيديو بناءً على تلك المطالبة. ستغطي هذه المدونة كل شيء بدءًا من فهم المفاهيم النظرية وحتى ترميز البنية بأكملها وإنشاء النتيجة النهائية.
وبما أنني لا أملك وحدة معالجة رسومات فاخرة، فقد قمت بترميز البنية صغيرة الحجم. فيما يلي مقارنة بين الوقت اللازم لتدريب النموذج على معالجات مختلفة:
فيديوهات التدريب | العصور | وحدة المعالجة المركزية | وحدة معالجة الرسومات A10 | وحدة معالجة الرسومات T4 |
---|---|---|---|---|
10 ألف | 30 | أكثر من 3 ساعات | 1 ساعة | 1 ساعة 42 م |
30 ألف | 30 | أكثر من 6 ساعات | 1 ساعة 30 | 2 ساعة 30 |
100 ألف | 30 | - | 3-4 ساعات | 5-6 ساعات |
من الواضح أن التشغيل على وحدة المعالجة المركزية سيستغرق وقتًا أطول بكثير لتدريب النموذج. إذا كنت بحاجة إلى اختبار التغييرات في التعليمات البرمجية بسرعة ورؤية النتائج، فإن وحدة المعالجة المركزية ليست الخيار الأفضل. أوصي باستخدام وحدة معالجة الرسومات T4 من Colab أو Kaggle للحصول على تدريب أكثر كفاءة وأسرع.
إليك رابط المدونة الذي يرشدك إلى كيفية إنشاء Stable Diffusion من الصفر: Coding Stable Diffusion من Scratch
سنتبع نهجًا مشابهًا للتعلم الآلي التقليدي أو نماذج التعلم العميق التي يتم تدريبها على مجموعة بيانات ثم يتم اختبارها على بيانات غير مرئية. في سياق تحويل النص إلى فيديو، لنفترض أن لدينا مجموعة بيانات تدريبية مكونة من 100 ألف مقطع فيديو للكلاب التي تجلب الكرات والقطط التي تطارد الفئران. سنقوم بتدريب نموذجنا على إنشاء مقاطع فيديو لقطة تجلب كرة أو كلبًا يطارد فأرًا.
على الرغم من أن مجموعات البيانات التدريبية هذه متاحة بسهولة على الإنترنت، إلا أن القوة الحسابية المطلوبة عالية للغاية. لذلك، سنعمل مع مجموعة بيانات فيديو للكائنات المتحركة الناتجة عن كود بايثون.
سوف نستخدم بنية GAN (شبكات الخصومة التوليدية) لإنشاء نموذجنا بدلاً من نموذج الانتشار الذي يستخدمه OpenAI Sora. لقد حاولت استخدام نموذج الانتشار، لكنه تعطل بسبب متطلبات الذاكرة، وهو ما يتجاوز قدرتي. من ناحية أخرى، تعتبر شبكات GAN أسهل وأسرع في التدريب والاختبار.
سنستخدم OOP (البرمجة الشيئية)، لذلك يجب أن يكون لديك فهم أساسي لها بالإضافة إلى الشبكات العصبية. إن معرفة شبكات GAN (شبكات الخصومة التوليدية) ليست إلزامية، حيث أننا سنغطي بنيتها هنا.
عنوان | وصلة |
---|---|
عفوًا | رابط الفيديو |
نظرية الشبكات العصبية | رابط الفيديو |
هندسة جان | رابط الفيديو |
أساسيات بايثون | رابط الفيديو |
يعد فهم بنية GAN أمرًا مهمًا لأن الكثير من بنيتنا تعتمد عليها. دعونا نستكشف ماهيته ومكوناته والمزيد.
شبكة الخصومة التوليدية (GAN) هي نموذج للتعلم العميق حيث تتنافس شبكتان عصبيتان: تقوم إحداهما بإنشاء بيانات جديدة (مثل الصور أو الموسيقى) من مجموعة بيانات معينة، والأخرى تحاول معرفة ما إذا كانت البيانات حقيقية أم مزيفة. تستمر هذه العملية حتى لا يمكن تمييز البيانات التي تم إنشاؤها عن البيانات الأصلية.
إنشاء الصور : تقوم شبكات GAN بإنشاء صور واقعية من المطالبات النصية أو تعديل الصور الموجودة، مثل تحسين الدقة أو إضافة الألوان إلى الصور بالأبيض والأسود.
تعزيز البيانات : يقومون بإنشاء بيانات تركيبية لتدريب نماذج التعلم الآلي الأخرى، مثل إنشاء بيانات المعاملات الاحتيالية لأنظمة كشف الاحتيال.
استكمال المعلومات المفقودة : يمكن لشبكات GAN ملء البيانات المفقودة، مثل إنشاء صور تحت السطح من خرائط التضاريس لتطبيقات الطاقة.
إنشاء نماذج ثلاثية الأبعاد : تقوم بتحويل الصور ثنائية الأبعاد إلى نماذج ثلاثية الأبعاد، وهي مفيدة في مجالات مثل الرعاية الصحية لإنشاء صور واقعية للأعضاء للتخطيط الجراحي.
وهو يتألف من شبكتين عصبيتين عميقتين: المولد والمميز . تتدرب هذه الشبكات معًا في بيئة عدائية، حيث تقوم إحداهما بإنشاء بيانات جديدة وتقوم الأخرى بتقييم ما إذا كانت البيانات حقيقية أم مزيفة.
فيما يلي نظرة عامة مبسطة حول كيفية عمل GAN:
تحليل مجموعة التدريب : يقوم المولد بتحليل مجموعة التدريب لتحديد سمات البيانات، بينما يقوم المُميِّز بتحليل نفس البيانات بشكل مستقل لمعرفة سماتها.
تعديل البيانات : يضيف المولد ضوضاء (تغييرات عشوائية) لبعض سمات البيانات.
تمرير البيانات : يتم بعد ذلك تمرير البيانات المعدلة إلى أداة التمييز.
حساب الاحتمالية : يحسب المُميز احتمالية أن تكون البيانات التي تم إنشاؤها من مجموعة البيانات الأصلية.
حلقة التغذية الراجعة : يقدم جهاز التمييز ردود فعل للمولد، ويوجهه لتقليل الضوضاء العشوائية في الدورة التالية.
التدريب العدائي : يحاول المولد تعظيم أخطاء التمييز، بينما يحاول التمييز تقليل أخطائه. ومن خلال العديد من تكرارات التدريب، تتحسن كلتا الشبكتين وتتطوران.
حالة التوازن : يستمر التدريب حتى لا يتمكن المُميِّز من التمييز بين البيانات الحقيقية والمركبة، مما يشير إلى أن المولد قد تعلم بنجاح إنتاج بيانات واقعية. في هذه المرحلة، تكون عملية التدريب قد اكتملت.
صورة من دليل أوس
دعونا نشرح نموذج GAN بمثال للترجمة من صورة إلى صورة، مع التركيز على تعديل الوجه البشري.
صورة الإدخال : الإدخال هو صورة حقيقية لوجه إنسان.
تعديل السمة : يقوم المولد بتعديل سمات الوجه، مثل إضافة النظارات الشمسية إلى العينين.
الصور التي تم إنشاؤها : يقوم المولد بإنشاء مجموعة من الصور مع إضافة النظارات الشمسية.
مهمة المُميِّز : يتلقى المُميِّز مزيجًا من الصور الحقيقية (الأشخاص الذين يرتدون نظارات شمسية) والصور المولدة (الوجوه التي تمت إضافة النظارات الشمسية إليها).
التقييم : يحاول المُميِّز التمييز بين الصور الحقيقية والمولدة.
حلقة التغذية الراجعة : إذا قام جهاز التمييز بتحديد الصور المزيفة بشكل صحيح، يقوم المولد بضبط معلماته لإنتاج صور أكثر إقناعًا. إذا نجح المولد في خداع أداة التمييز، يقوم أداة التمييز بتحديث معلماتها لتحسين اكتشافها.
ومن خلال هذه العملية العدائية، تتحسن كلا الشبكتين بشكل مستمر. يتحسن المولد في إنشاء صور واقعية، ويتحسن التمييز في التعرف على الصور المزيفة حتى يتم الوصول إلى التوازن، حيث لم يعد التمييز قادرًا على التمييز بين الصور الحقيقية والمولدة. عند هذه النقطة، تعلمت GAN بنجاح كيفية إنتاج تعديلات واقعية.
يعد تثبيت المكتبات المطلوبة هو الخطوة الأولى في بناء نموذج تحويل النص إلى فيديو.
pip install -r requirements.txt
سنعمل مع مجموعة من مكتبات بايثون، فلنستوردها.
# 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
الآن بعد أن استوردنا جميع مكتباتنا، فإن الخطوة التالية هي تحديد بيانات التدريب التي سنستخدمها لتدريب بنية GAN الخاصة بنا.
نحتاج إلى ما لا يقل عن 10000 مقطع فيديو كبيانات تدريبية. لماذا؟ حسنًا، لأنني اختبرت بأعداد أصغر وكانت النتائج سيئة للغاية، ولم يكن هناك أي شيء يمكن رؤيته عمليًا. السؤال الكبير التالي هو: ما موضوع مقاطع الفيديو هذه؟ تتكون مجموعة بيانات الفيديو التدريبية الخاصة بنا من دائرة تتحرك في اتجاهات مختلفة بحركات مختلفة. لذا، دعونا نقوم بتشفيرها وإنشاء 10000 مقطع فيديو لنرى كيف تبدو.
# 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
بعد ضبط بعض المعلمات الأساسية، نحتاج بعد ذلك إلى تحديد المطالبات النصية لمجموعة بيانات التدريب الخاصة بنا استنادًا إلى مقاطع الفيديو التدريبية التي سيتم إنشاؤها.
# 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
]
لقد حددنا عدة حركات لدائرتنا باستخدام هذه المطالبات. الآن، نحن بحاجة إلى ترميز بعض المعادلات الرياضية لتحريك تلك الدائرة بناءً على المطالبات.
# 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 )
يتم استخدام الوظيفة أعلاه لتحريك دائرتنا لكل إطار بناءً على الاتجاه المحدد. نحتاج فقط إلى تشغيل حلقة فوقها تصل إلى عدد مقاطع الفيديو مرات لإنشاء جميع مقاطع الفيديو.
# 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 )
بمجرد تشغيل الكود أعلاه، سيؤدي إلى إنشاء مجموعة بيانات التدريب بأكملها. إليك ما تبدو عليه بنية ملفات مجموعة بيانات التدريب لدينا.
يحتوي كل مجلد فيديو تدريبي على إطاراته بالإضافة إلى النص الموجه إليه. دعونا نلقي نظرة على عينة من مجموعة بيانات التدريب لدينا.
في مجموعة بيانات التدريب الخاصة بنا، لم نقم بتضمين حركة الدائرة التي تتحرك لأعلى ثم إلى اليمين . سوف نستخدم هذا كموجه اختباري لتقييم نموذجنا المدرّب على البيانات غير المرئية.
هناك نقطة أخرى مهمة يجب ملاحظتها وهي أن بيانات التدريب لدينا تحتوي على العديد من العينات حيث تتحرك الأشياء بعيدًا عن المشهد أو تظهر جزئيًا أمام الكاميرا، على غرار ما لاحظناه في مقاطع الفيديو التوضيحية لـ OpenAI Sora.
السبب وراء تضمين مثل هذه العينات في بيانات التدريب لدينا هو اختبار ما إذا كان نموذجنا يمكنه الحفاظ على الاتساق عندما تدخل الدائرة إلى المشهد من الزاوية ذاتها دون كسر شكلها.
الآن بعد أن تم إنشاء بيانات التدريب الخاصة بنا، نحتاج إلى تحويل مقاطع الفيديو التدريبية إلى Tensors، وهو نوع البيانات الأساسي المستخدم في أطر التعلم العميق مثل PyTorch. بالإضافة إلى ذلك، يساعد إجراء التحويلات مثل التطبيع على تحسين تقارب واستقرار بنية التدريب عن طريق توسيع نطاق البيانات إلى نطاق أصغر.
يتعين علينا ترميز فئة مجموعة بيانات لمهام تحويل النص إلى فيديو، والتي يمكنها قراءة إطارات الفيديو والمطالبات النصية المقابلة لها من دليل مجموعة بيانات التدريب، مما يجعلها متاحة للاستخدام في 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
قبل الشروع في ترميز البنية، نحتاج إلى تطبيع بيانات التدريب لدينا. سوف نستخدم حجم دفعة يبلغ 16 ونقوم بخلط البيانات لإدخال المزيد من العشوائية.
# 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 )
ربما تكون قد رأيت في بنية المحولات حيث تكون نقطة البداية هي تحويل إدخال النص الخاص بنا إلى تضمين لمزيد من المعالجة في الاهتمام متعدد الرؤوس، كما هو الحال هنا، يتعين علينا ترميز طبقة تضمين النص استنادًا إلى التدريب على بنية GAN الذي سيتم على بيانات التضمين الخاصة بنا والصور الموتر.
# 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 )
سيعتمد حجم المفردات على بيانات التدريب لدينا، والتي سنحسبها لاحقًا. سيكون حجم التضمين 10. إذا كنت تعمل مع مجموعة بيانات أكبر، يمكنك أيضًا استخدام اختيارك الخاص لنموذج التضمين المتوفر على Hugging Face.
الآن بعد أن عرفنا بالفعل ما يفعله المولد في شبكات GAN، فلنقم بتشفير هذه الطبقة ثم فهم محتوياتها.
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