اپنی زبان کے لیے مخصوص ایل ایل ایم کیسے بنائیں [Full Handbook]

کیا ہوگا اگر آپ اپنی مادری زبان کا استعمال کرتے ہوئے شروع سے اپنا LLM بنا سکتے ہیں؟ بالکل یہی ہے جو ہم اس ٹیوٹوریل میں کریں گے۔ یہ سمجھنے کا بہترین طریقہ ہے کہ ایل ایل ایم کیسے کام کرتا ہے دراصل اسے بنانا ہے۔

آئیے ایک مخصوص زبان میں اپنا ایل ایل ایم بنانے کے ہر مرحلے سے گزرتے ہیں (اس معاملے میں اردو)۔ اس سے آپ کو یہ سمجھنے میں مدد ملے گی کہ LLM کے اندر کیا ہوتا ہے۔

جدید LLM تحقیقی مقالے سے تعلق رکھتا ہے جس نے سب کچھ بدل دیا۔ "یہ سب کچھ ہے جس پر توجہ دینے کی ضرورت ہے۔”. لیکن ریاضی میں کھو جانے کے بجائے (میں ریاضی میں اچھا نہیں ہوں)، میں اسے شروع سے بنا کر سیکھنے جا رہا ہوں۔

یہ کتابچہ کس کے لیے ہے؟

سافٹ ویئر انجینئرز، پروڈکٹ کے مالکان یا کوئی بھی اس بارے میں دلچسپی رکھتا ہے کہ LLM کیسے کام کرتے ہیں۔ اگر آپ کے پاس مشین لرننگ کا کچھ علم ہے تو یہ اچھا ہے، لیکن اگر آپ کے پاس نہیں ہے تو فکر نہ کریں۔ میں نے یہ اس لیے لکھا ہے کہ آپ کو اس ٹیوٹوریل سے باہر جانے کی ضرورت نہیں ہے۔

آخر میں، آپ اردو ایل ایل ایم چیٹ بوٹ نوکریاں تعینات اور چل رہا ہے. ذیل میں بیان کردہ مراحل پر عمل کرکے آپ اسے اپنی زبان میں بھی بنا سکتے ہیں۔

توقعات پر نوٹس:

یہاں کا مقصد اپنے آپ کو تعلیم دینا ہے کہ LLM تمام مراحل سے گزر کر کیسے کام کرتا ہے۔

مقصد یہ ہے۔ ~ نہیں LLM ChatGPT کی طرح کام کرتا ہے۔ اس میں کئی رکاوٹیں ہیں جیسے بڑے ڈیٹا سیٹس، مہینوں کی تربیت، اور انسانی تاثرات کے ساتھ کمک سیکھنا (RLHF)، ان سب کو اس ٹیوٹوریل کے ذریعے بہتر طور پر سمجھا جا سکتا ہے۔

کوڈ کے بارے میں نوٹس:

اس ٹیوٹوریل کا کوڈ بنیادی طور پر Claude Opus 4 کا استعمال کرتے ہوئے تیار کیا گیا تھا۔ یہ نمایاں کرنے کے قابل ہے کیونکہ یہ ظاہر کرتا ہے کہ LLM صرف ایک کوڈنگ مددگار نہیں ہے جو آپ کو خصوصیات کو تیزی سے فراہم کرنے میں مدد کرے گا۔ یہ سیکھنے کا ایک طاقتور ٹول بھی ہو سکتا ہے۔

Claude کے ہر ایک جزو کے ذریعے تخلیق، وضاحت اور اعادہ کرنے سے مجھے صرف دستاویزات کو پڑھنے کے بجائے LLM ٹریننگ کے اندرونی معاملات کے بارے میں بہت گہرا سمجھ حاصل ہوئی۔

اگر آپ پیروی کر رہے ہیں، تو میں آپ کو بھی ایسا کرنے کی ترغیب دیتا ہوں۔ سیکھنے کے لیے ایل ایل ایم کا استعمال کریں۔

ہم کیا احاطہ کریں گے:

ایل ایل ایم تعلیم کے اجزاء

اس ٹیوٹوریل میں، ہم بہتر تفہیم کے لیے کوڈ کی مثالوں کے ساتھ ایک ایک کرکے درج ذیل اجزاء کا احاطہ کریں گے۔

  1. ڈیٹا کی تیاری

  2. ٹوکنائزیشن

  3. پیشگی تربیت

  4. زیر نگرانی فائن ٹیوننگ (SFT)

  5. تعیناتی

ٹیکنالوجی اسٹیک درکار ہے۔

اقدامات شروع کرنے سے پہلے، یہ ٹیکنالوجی اسٹیک ہے جس کی آپ کو ضرورت ہوگی:

  1. Python 3.9+

  2. پائی ٹارچ

  3. Tokenizer/SentencePiece

  4. ہگنگ فیسس ڈیٹاسیٹ اور حب

  5. باقاعدہ اظہار، خوبصورت سوپ4، درخواستیں (ڈیٹا کی صفائی کے لیے)

  6. tqdm، matplotlib (تربیتی افادیت کے لیے)

  7. گریڈیو (چیٹ UI تعیناتی کے لیے)

  8. Google Colab (تربیت کے لیے مفت T4 GPU)

میمو: یقینی بنائیں کہ آپ نے ذیل میں درج تمام انحصارات کو انسٹال کر لیا ہے: requirements.txt شروع کرنے سے پہلے، اپنے ذخیرہ میں موجود فائلوں کو چیک کریں۔

1. ڈیٹا کی تیاری

ڈیٹا کی تیاری میں سب سے پہلے کام کرنا ہے۔ ڈیٹا اکٹھا کرنا. LLMs کو ٹیکسٹ ڈیٹا کی بڑی مقدار پر تربیت کی ضرورت ہوتی ہے۔ یہ ڈیٹا حاصل کرنے کے لیے کوئی ایک جگہ نہیں ہے۔ آپ جس ماڈل کو بنانا چاہتے ہیں اس پر منحصر ہے، آپ متنوع ذرائع سے متن جمع کر سکتے ہیں۔

  • ڈیجیٹل لائبریریاں اور آرکائیوز: انٹرنیٹ آرکائیو یا ویکیپیڈیا ڈمپ

  • کوڈ ذخیرہ: GitHub، GitLab (اگر آپ کے ماڈل کو کوڈ کو سمجھنے کی ضرورت ہو تو مفید ہے)

  • ویب سکریپنگ: خودکار اسکرپٹس کا استعمال کرتے ہوئے ویب سائٹس، بلاگز اور فورمز کو کرال کریں۔

  • تعلیمی ڈیٹاسیٹس: تحقیقی مقالے، اوپن رسائی جرنلز

  • پہلے سے تیار کردہ ڈیٹاسیٹس: Hugging Face Datasets اور Kaggle جیسے پلیٹ فارم ہزاروں استعمال کے لیے تیار ڈیٹا سیٹس کی میزبانی کرتے ہیں۔

درحقیقت، بڑے پیمانے پر LLMs جیسے GPT اور LLaMA خودکار پائپ لائنوں کا استعمال کرتے ہوئے مختلف ذرائع سے ویب سکریپنگ پر بہت زیادہ انحصار کرتے ہیں۔ تاہم، پیروی کرنے کے لئے ایک اہم اصول ہے. صرف عوامی طور پر دستیاب، اوپن سورس ڈیٹا استعمال کریں۔ ذاتی معلومات یا نجی صارف کی معلومات کو نہ کھرچیں۔ اس ڈیٹا پر قائم رہیں جو عوامی استعمال کے لیے واضح طور پر شیئر کیا گیا ہو یا جو اجازت نامہ کے تحت آتا ہو۔

بھی، ان اصولوں کو ذہن میں رکھیں: کچرا اندر، کچرا باہر. صرف ڈیٹا حاصل کرنا کافی نہیں ہے۔ یہ درست، صاف اور شور سے پاک ہونا چاہیے۔

عملی طور پر، ڈیٹا مختلف ذرائع سے جمع کیا جا سکتا ہے. میرے معاملے میں مجھے کافی ڈیٹا ملا ہے: گلے لگنے والا چہرہ. گلے ملتے چہرے پر کلچر ایکس میرے پاس کثیر لسانی ڈیٹاسیٹ ہے۔ ڈیٹا سیٹ بہت بڑا تھا، لہذا میں نے پوری چیز کے بجائے اس کا صرف ایک حصہ ڈاؤن لوڈ کیا۔

اس ٹیوٹوریل میں گلے لگنے والا چہرہ میرے ڈیٹا سورس تک۔ میں نے اسے چند وجوہات کی بنا پر منتخب کیا۔

سب سے پہلے، چونکہ میرا مقصد یہ جاننا تھا کہ ایل ایل ایم کیسے کام کرتا ہے، میں اپنا وقت ویب سکریپر لکھنے کے بجائے ماڈلز پر گزارنا چاہتا تھا۔ Hugging Face پہلے سے ہی ایک صاف ستھرا، سٹرکچرڈ فارمیٹ میں ڈیٹا سیٹس کا ایک بڑا مجموعہ ہے، جس سے آپ کو ابتدائی کام کی کافی بچت ہوتی ہے۔

دوسرا، ہگنگ فیس زبان سے متعلق ڈیٹا سیٹ فراہم کرتا ہے۔ میں اردو ایل ایل ایم بنا رہا تھا اس لیے مجھے خاص طور پر اردو ٹیکسٹ اور ہیگنگ فیس کی ضرورت تھی۔ کلچر ایکس یہ اردو اور بہت سی دوسری زبانوں سمیت کثیر لسانی ڈیٹا سیٹس پیش کرتا ہے۔ ڈیٹاسیٹ بہت بڑا تھا، اس لیے میں نے یہ سب ڈاؤن لوڈ نہیں کیا، لیکن اس کا صرف ایک حصہ۔

اہم: اس سے پہلے کہ آپ Hugging Face سے ڈیٹا سیٹ ڈاؤن لوڈ کرنا شروع کر سکیں، آپ کو ایک اکاؤنٹ بنانا ہوگا۔ اس کے بعد آپ CLI میں لاگ ان کرکے ڈیٹاسیٹ ڈاؤن لوڈ کرسکتے ہیں۔

نیچے دی گئی اسکرپٹ Hugging Face ڈیٹاسیٹ کو لوڈ کرتی ہے اور سلسلہ بندی شروع کر دیتی ہے۔ True. ایسا کرنے کا مقصد یہ ہے کہ تمام اعداد و شمار کو ڈاؤن لوڈ کرنے کی ضرورت نہیں ہے، صرف نمونے کے حصے ذیل میں بیان کیے گئے ہیں۔ NUM_SAMPLES.

# ============================================================
# Option A: Download from CulturaX (recommended, high quality)
# ============================================================
# CulturaX is a cleaned version of mC4 + OSCAR
# We stream it to avoid downloading the entire dataset

NUM_SAMPLES = 100_000  # Start with 100K samples (~50-100MB text)

print("Loading CulturaX Urdu dataset (streaming)...")
dataset = load_dataset(
    "uonlp/CulturaX",
    "ur",                    # Urdu language code
    split="train",
    streaming=True,          # Don't download everything
    trust_remote_code=True
)

# Collect samples
raw_texts = []
for i, sample in enumerate(tqdm(dataset, total=NUM_SAMPLES, desc="Downloading")):
    if i >= NUM_SAMPLES:
        break
    raw_texts.append(sample["text"])

print(f"\nDownloaded {len(raw_texts)} samples")
print(f"Total characters: {sum(len
print(f"\nSample text (first 500 chars):")
print(raw_texts[0][:500])

ڈیٹا کی صفائی

ماڈل کی تربیت شروع کرنے کے لیے صرف ڈیٹا ہونا کافی نہیں ہے۔ اگلا مرحلہ شاید سب سے اہم ہے۔ ڈیٹا کی صفائی. مقصد ڈیٹا کو ہر ممکن حد تک خالص بنانا ہے۔

زبان کے لیے مخصوص اردو ایل ایل ایم بناتے وقت، مجھے غیر اردو متن، ایچ ٹی ایم ایل لنکس، خصوصی حروف، ڈپلیکیٹ مواد، اور ضرورت سے زیادہ خالی جگہ کو ہٹانے کے لیے کلین اپ منطق لکھنا پڑی۔ یہ تمام عوامل تربیتی ڈیٹا کو آلودہ کر سکتے ہیں اور تربیت کے دوران مسائل پیدا کر سکتے ہیں۔

ڈیٹا سیٹ کی قسم پر منحصر ہے، کچھ مخصوص زبان یا استعمال کے معاملے کی تنظیم کی ضرورت ہوگی۔

آپ کے لیے نئی چیزوں میں سے ایک NFKC یونیکوڈ نارملائزیشن قدم یہ متن کو معمول بناتا ہے جو ایک جیسا نظر آتا ہے لیکن ایک معیاری فارمیٹ کو برقرار رکھنے کے لیے مختلف یونیکوڈ فارمیٹس میں موجود ہے۔

آپ کچھ ریگولر ایکسپریشن پیٹرن بھی دیکھ سکتے ہیں جو صرف اردو ٹیکسٹ رکھنے کے لیے استعمال ہوتے ہیں۔ اردو رسم الخط عربی پر مبنی ہے اس لیے عربی یونیکوڈ رینج استعمال کرتا ہے۔ میں نے درج ذیل نمونے بھی ہٹا دیے ہیں: //, --اور خام ڈیٹا میں موجود اضافی خالی جگہ۔

اس صفائی کے لیے کئی تکرار کی ضرورت تھی۔ ہر بار، ہم نے نتائج کا دستی طور پر جائزہ لیا اور متضاد وقفہ کاری، لمبی ڈیشز، اور غلط اوقاف جیسے مسائل کی نشاندہی کی۔ یہ سب اگلے مراحل پر منفی اثر ڈال سکتے ہیں، اس لیے اسے اچھی طرح صاف کرنا ضروری ہے۔

اس سے آپ کو اندازہ ہوتا ہے کہ ڈیٹا کا حصہ اب بھی کتنا اہم ہے اور LLM ڈیٹا پر کتنا منحصر ہے۔

یہاں صفائی کی خصوصیات ہیں جو میں نے استعمال کی ہیں:

def clean_urdu_text(text: str) -> str:
    """
    Clean a single Urdu text document.
    
    Steps:
    1. Remove URLs
    2. Remove HTML tags and entities
    3. Remove email addresses
    4. Normalize Unicode (NFKC normalization)
    5. Remove non-Urdu characters (keep Urdu + punctuation + digits)
    6. Normalize repeated punctuation (۔۔۔, ..., - -, etc.)
    7. Normalize whitespace
    """
    import unicodedata
    
    # Step 1: Remove URLs
    text = re.sub(r'https?://\S+|www\.\S+', '', text)
    
    # Step 2: Remove HTML tags
    text = re.sub(r'<[^>]+>', '', text)
    # Remove HTML entities
    text = re.sub(r'&[a-zA-Z]+;', ' ', text)
    text = re.sub(r'\d+;', ' ', text)
    
    # Step 3: Remove email addresses
    text = re.sub(r'\S+@\S+', '', text)
    
    # Step 4: Unicode normalization (NFKC)
    # This normalizes different representations of the same character
    text = unicodedata.normalize('NFKC', text)
    
    # Step 5: Keep only Urdu characters, basic punctuation, digits, and whitespace
    # Urdu Unicode ranges + Arabic punctuation + Western digits + basic punctuation
    urdu_pattern = regex.compile(
        r'[^'
        r'\u0600-\u06FF'    # Arabic (includes Urdu)
        r'\u0750-\u077F'    # Arabic Supplement
        r'\u08A0-\u08FF'    # Arabic Extended-A
        r'\uFB50-\uFDFF'    # Arabic Presentation Forms-A
        r'\uFE70-\uFEFF'    # Arabic Presentation Forms-B
        r'0-9۰-۹'           # Western and Eastern Arabic-Indic digits
        r'\s'               # Whitespace
        r'۔،؟!٪'           # Urdu punctuation (full stop, comma, question mark, etc.)
        r'.,:;!?\-\(\)"\']'  # Basic Latin punctuation
    )
    text = urdu_pattern.sub(' ', text)
    
    # Step 6: Normalize repeated punctuation
    text = re.sub(r'۔{2,}', '۔', text)
    text = re.sub(r'\.{2,}', '.', text)
    text = re.sub(r'-\s*-+', '-', text)
    text = re.sub(r'-{2,}', '-', text)
    text = re.sub(r'،{2,}', '،', text)
    text = re.sub(r',{2,}', ',', text)
    text = re.sub(r'\s+[۔\.\-,،]\s+', ' ', text)
    
    # Step 7: Normalize whitespace
    text = re.sub(r'\n{3,}', '\n\n', text)  # Max 2 newlines
    text = re.sub(r'[^\S\n]+', ' ', text)    # Collapse spaces (but keep newlines)
    text = text.strip()
    
    return text


def is_mostly_urdu(text: str, threshold: float = 0.5) -> bool:
    """
    Check if text is mostly Urdu characters.
    This filters out documents that are primarily English/other languages.
    
    threshold: minimum fraction of characters that must be Urdu
    """
    if len(text) == 0:
        return False
    urdu_chars = len(regex.findall(r'[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]', text))
    return (urdu_chars / len(text)) > threshold


# Test the cleaning function
sample = raw_texts[0]
print("=== BEFORE CLEANING ===")
print(sample[:300])
print("\n=== AFTER CLEANING ===")
cleaned = clean_urdu_text(sample)
print(cleaned[:300])
print(f"\nIs mostly Urdu: {is_mostly_urdu(cleaned)}")

صفائی کے بعد ڈیٹا کو دو فارمیٹس میں محفوظ کیا گیا۔ ٹیکسٹ فائل (ٹوکنائزر کی تربیت کے لیے استعمال کیا جاتا ہے) اور JSONL فائل (پری ٹریننگ میں استعمال کیا جاتا ہے) اگلے مرحلے میں ہر فارمیٹ کو ایک خاص مقصد کے لیے استعمال کیا جاتا ہے۔

2. ٹوکنائزیشن

صفائی کے بعد اگلا مرحلہ ہے۔ ٹوکنائزیشن. ٹوکنائزیشن متن کو نمبروں میں تبدیل کرنے اور ان نمبروں کو واپس متن میں تبدیل کرنے کا ایک طریقہ فراہم کرتا ہے۔

یہ ضروری ہے کیونکہ عصبی نیٹ ورک متن کو نہیں سمجھ سکتے، صرف اعداد۔ لہذا ٹوکنائزیشن بنیادی طور پر انسانی زبان کے درمیان ترجمہ کی پرت ہے اور جو ماڈل سنبھال سکتا ہے۔

مثال کے طور پر:

"hello world"  →  ["hel", "lo", " world"]  →  [1245, 532, 995]
"اردو زبان"   ←  ["ار", "دو", "زب", "ان"]  ←  [412, 87, 953, 201]

ٹوکنائزیشن کا طریقہ

ٹوکنائزیشن کے تین اہم طریقے ہیں:

نقطہ نظر 1: کردار کی سطح

یہ نقطہ نظر آپ کو متن کو انفرادی حروف میں تقسیم کرنے کی اجازت دیتا ہے۔

  • hello -> ['h', 'e', 'l', 'l', 'o']

  • اردو -> ['ا', 'ر', 'د', 'و']

مسئلہ یہ ہے کہ سلسلہ بہت طویل ہو جاتا ہے۔ 1000 الفاظ کی دستاویز میں 5000 سے زیادہ ٹوکن ہو سکتے ہیں۔ ماڈل کو حروف کو الفاظ میں جوڑنے کا طریقہ سیکھنے کی ضرورت ہے، جو کہ بہت مشکل ہے۔

نقطہ نظر 2: لفظ کی سطح

یہ نقطہ نظر الفاظ کے درمیان جگہ کی بنیاد پر تقسیم ہوتا ہے۔

  • hello how are you -> ['hello', 'how', 'are', 'you']

  • اردو بہت اچھی زبان ہے -> ['اردو', 'بہت', 'اچھی', 'زبان', 'ہے']

اس کے ساتھ مسئلہ یہ ہے کہ زبانوں کی ذخیرہ الفاظ بہت زیادہ ہیں (اردو میں 100,000 سے زیادہ منفرد الفاظ ہیں؛ انگریزی میں 170,000 منفرد الفاظ ہیں)۔ ماڈل نئے یا نایاب الفاظ کو نہیں سنبھال سکتا (لفظ سے باہر کا مسئلہ)۔

نقطہ نظر 3: بائٹ پیئر انکوڈنگ (BPE) کا استعمال کرتے ہوئے ذیلی الفاظ

اس نقطہ نظر کے ساتھ، ماڈل ڈیٹا سے عام کردار کی ترتیب سیکھتا ہے۔

  • unhappiness اسے اس طرح تقسیم کیا جا سکتا ہے: ['un', 'happi', 'ness']

  • مکمل اسے اس طرح تقسیم کیا جا سکتا ہے: ['مکم', 'ل'] یا، اگر یہ کافی عام ہے، تو اسے مکمل رکھیں۔

یہ ایک چھوٹی ذخیرہ الفاظ ہے (32K ٹوکنز کا استعمال کرتے ہوئے) اور تمام الفاظ، یہاں تک کہ نئے الفاظ بھی سنبھال سکتی ہے۔ عام الفاظ کو سنگل ٹوکن کے طور پر رکھا جاتا ہے۔

BPE ایک صنعتی معیار ہے جسے GPT، LLaMA، اور جدید ترین LLMs استعمال کرتے ہیں۔ یہاں یہ ہے کہ یہ قدم بہ قدم کیسے کام کرتا ہے:

  1. ایک خط کے ساتھ شروع ہوتا ہے: الفاظ = تمام انفرادی حروف

  2. جوڑی کی گنتی: اکثر ملحقہ ٹوکن جوڑے تلاش کریں۔

  3. جذب: جوڑے کو ایک نئے ٹوکن میں جوڑتا ہے۔

  4. دہرائیں: جب تک کہ الفاظ مطلوبہ سائز تک نہ پہنچ جائیں۔

مثالوں میں شامل ہیں:

Start:  ا ر د و   ز ب ا ن
Merge 1: 'ا ر' -> 'ار'    (most common pair)
Result: ار د و   ز ب ا ن
Merge 2: 'ز ب' -> 'زب'    (next most common)
Result: ار د و   زب ا ن
...and so on for 32,000 merges

یہ وہ طریقہ ہے جسے میں اپنے اردو ایل ایل ایم کے لیے استعمال کروں گا۔ میں نے تیار کردہ اردو کارپس پر 32K ٹوکن کے الفاظ کے سائز کے ساتھ ایک BPE ٹوکنائزر کو تربیت دی۔

خصوصی ٹوکن

BPE کے ساتھ آپ کو یہ بھی شامل کرنا چاہئے: خصوصی ٹوکن. یہ ٹوکن تربیت اور تخمینہ کے دوران درکار ماڈل ڈھانچے کی معلومات فراہم کرتے ہیں۔

ٹوکن مقصد آپ کو اس کی ضرورت کیوں ہے۔
مساوی لمبائی کے سلسلے کے لیے پیڈنگ بیچ پروسیسنگ کے لیے، تمام ترتیبوں کی لمبائی ایک جیسی ہونی چاہیے۔ مختصر سلسلے اس سے بھرے ہوئے ہیں: ٹوکن
نامعلوم لفظ کو تبدیل کریں۔ جب ماڈل کا سامنا کسی ایسے ٹوکن سے ہوتا ہے جو اس کے ذخیرہ الفاظ میں نہیں ہے، تو اسے اس طرح نقشہ بنایا جاتا ہے: ناکام ہونے کے بجائے۔
ایک ترتیب کے آغاز کو نشان زد کرتا ہے۔ یہ ماڈل کو بتاتا ہے کہ ان پٹ کہاں سے شروع ہوتا ہے، جس سے زیادہ قابل اعتماد نسل پیدا ہوتی ہے۔
ایک ترتیب کے اختتام کو نشان زد کرتا ہے۔ ماڈل کو بتاتا ہے کہ کب پیدا کرنا بند کرنا ہے۔ بصورت دیگر، آؤٹ پٹ ہمیشہ کے لیے چل سکتا ہے یا بے ترتیب طور پر رک سکتا ہے۔
الگ الگ طبقات چیٹ فارمیٹ سسٹم کے اشارے، صارف کے پیغامات، اور معاون جوابات کو الگ کرتا ہے تاکہ ماڈل کو معلوم ہو کہ اس کا کیا کردار ہے۔
` صارف >`
` اسسٹنٹ >`
` نظام >`

BPE ٹوکنائزر کنفیگریشن

میں نے الفاظ کا سائز اس پر سیٹ کیا: 32K. اس کا کیا مطلب ہے؟ اس کا مطلب ہے کہ ماڈل کے الفاظ کی تلاش کے جدول میں 32K ٹوکنز ہیں۔

یہ زبان کی کوریج اور ماڈل سائز کے درمیان ایک اچھا توازن ہے۔ الفاظ کے سائز کو بڑھانے سے سرایت کرنے والی پرت اور آؤٹ پٹ پرت دونوں میں اضافہ ہوتا ہے، جس کے نتیجے میں سیکھنے کے لیے مزید پیرامیٹرز ہوتے ہیں۔ سیکھنے کے منصوبوں کے لیے، 32K ہر چیز کو قابل انتظام رکھتا ہے۔

MIN_FREQUENCY یہ 2 پر سیٹ کیا گیا ہے۔ اس کا مطلب ہے کہ شامل کیے جانے کے لیے ایک ٹوکن کم از کم دو بار ظاہر ہونا چاہیے۔ یہ ون آف شور ٹوکنز کو فلٹر کرتا ہے جو الفاظ کی سلاٹس کو ضائع کرتے ہیں۔

حوالہ کے لیے: GPT-2 50K ٹوکنز کا ذخیرہ استعمال کرتا ہے، جبکہ LLaMA 32K ٹوکن استعمال کرتا ہے۔ ہم نے جو 32K منتخب کیا ہے وہ پروڈکشن ماڈل سے میل کھاتا ہے۔

VOCAB_SIZE = 32_000  # Number of tokens in our vocabulary
MIN_FREQUENCY = 2    # Token must appear at least twice (filters noise)

# Special tokens - these have reserved IDs
SPECIAL_TOKENS = [
    "",    # ID 0: padding
    "",    # ID 1: unknown
    "",    # ID 2: beginning of sequence 
    "",    # ID 3: end of sequence
    "",    # ID 4: separator (for chat format)
    "<|user|>",     # ID 5: user turn marker (for chat)
    "<|assistant|>", # ID 6: assistant turn marker (for chat)
    "<|system|>",    # ID 7: system prompt marker (for chat)
]

ٹوکنائزر بنانا

اگلا، ہم پہلے بنائی گئی کلین اپ ٹیکسٹ فائل کا استعمال کرتے ہوئے ٹوکنائزر بنائیں گے۔ سب سے پہلے، مطلوبہ لائبریریوں کو درآمد کریں اور فائل کا راستہ سیٹ کریں۔

import os
from pathlib import Path
from tokenizers import (
    Tokenizer,
    models,
    trainers,
    pre_tokenizers,
    decoders,
    processors,
    normalizers,
)

PROJECT_ROOT = Path(".").resolve().parent
CLEANED_DIR = PROJECT_ROOT / "data" / "cleaned"
TOKENIZER_DIR = PROJECT_ROOT / "tokenizer" / "urdu_tokenizer"
TOKENIZER_DIR.mkdir(parents=True, exist_ok=True)

CORPUS_FILE = str(CLEANED_DIR / "urdu_corpus.txt")
print(f"Corpus file: {CORPUS_FILE}")
print(f"Tokenizer output: {TOKENIZER_DIR}")

# Verify corpus exists
assert os.path.exists(CORPUS_FILE), f"Corpus not found at {CORPUS_FILE}. Run notebook 01 first!"
file_size_mb = os.path.getsize(CORPUS_FILE) / 1024 / 1024
print(f"Corpus size: {file_size_mb:.1f} MB")

اب ٹوکنائزر جزو کو کنفیگر کرتے ہیں۔

# ============================================================
# Build the tokenizer
# ============================================================

# Step 1: Create a BPE model (the core algorithm)
tokenizer = Tokenizer(models.BPE(unk_token=""))

# Step 2: Add normalizer (text cleaning before tokenization)
# NFKC normalizes Unicode (e.g., different forms of the same Arabic letter)
tokenizer.normalizer = normalizers.NFKC()

# Step 3: Pre-tokenizer (how to split text before BPE)
# We use Metaspace which replaces spaces with ▁ and splits on them
# This preserves space information so we can reconstruct the original text
tokenizer.pre_tokenizer = pre_tokenizers.Metaspace()

# Step 4: Decoder (how to convert tokens back to text)
# Metaspace decoder converts ▁ back to spaces
tokenizer.decoder = decoders.Metaspace()

# Step 5: Configure the trainer
trainer = trainers.BpeTrainer(
    vocab_size=VOCAB_SIZE,
    min_frequency=MIN_FREQUENCY,
    special_tokens=SPECIAL_TOKENS,
    show_progress=True,
    initial_alphabet=[]  # Learn alphabet from data
)

print("Tokenizer configured. Ready to train!")

ٹوکنائزر کی تربیت

ٹوکنائزر کنفیگر ہونے کے بعد، اگلا مرحلہ اسے چلانا ہے۔ آپ کے آلے پر منحصر ہے، اس میں تقریباً 5 سے 10 منٹ لگیں گے۔

print("Training tokenizer... (this may take a few minutes)")
tokenizer.train([CORPUS_FILE], trainer)

print(f"\n Tokenizer trained!")
print(f"  Vocabulary size: {tokenizer.get_vocab_size():,}")

پوسٹ پروسیسنگ کنفیگریشن (BOS/EOS کے ساتھ خودکار ریپنگ)

اگلا، ہم پوسٹ پروسیسنگ کو ترتیب دیتے ہیں تاکہ ٹوکنائزر خود بخود تمام ترتیب کو سمیٹ لے۔ اور ٹوکن اس کا مطلب ہے کہ جب بھی آپ متن کو انکوڈ کرتے ہیں آپ کو اسے دستی طور پر شامل کرنے کی ضرورت نہیں ہے۔

bos_id = tokenizer.token_to_id("")
eos_id = tokenizer.token_to_id("")

tokenizer.post_processor = processors.TemplateProcessing(
    single=f":0 $A:0 :0",
    pair=f":0 \(A:0 :0 \)B:1 :1",
    special_tokens=[
        ("", bos_id),
        ("", eos_id),
        ("", tokenizer.token_to_id("")),
    ],
)

print("Post-processor configured (auto-adds  and )")

میمو: آپ سوچ رہے ہوں گے کہ جب آپ پہلے ہی اس کی وضاحت کر چکے ہیں تو آپ کو اس قدم کی ضرورت کیوں ہے۔ اور کو SPECIAL_TOKENS. کہ SPECIAL_TOKENS صرف فہرست ذخیرہ الفاظ کے سلاٹ ان ٹوکنز (ID تفویض) کے لیے پوسٹ پروسیسنگ ٹوکنائزر کو ہدایت کرتی ہے کہ: خود کار طریقے سے داخل کریں تمام انکوڈ شدہ ترتیبوں میں شامل ہے۔

اس قدم کے بغیر، ٹوکن آپ کے ذخیرہ الفاظ میں موجود رہے گا، لیکن آپ کے ڈیٹا میں ظاہر نہیں ہوگا جب تک کہ آپ اسے ہر بار دستی طور پر شامل نہ کریں۔

ٹوکنائزر ٹیسٹ

ٹوکنائزیشن کا آخری مرحلہ جانچ ہے۔ یہ ٹیسٹ اردو جملوں کو ٹوکن آئی ڈیز میں انکوڈ کرتا ہے اور پھر ان آئی ڈیز کو دوبارہ ٹیکسٹ میں ڈی کوڈ کرتا ہے۔ اگر ڈی کوڈ شدہ متن اصل ان پٹ سے میل کھاتا ہے تو ٹوکنائزر صحیح طریقے سے کام کر رہا ہے۔ یہ راؤنڈ ٹرپ ٹیسٹ اس بات کو یقینی بناتا ہے کہ انکوڈنگ اور ڈی کوڈنگ کے دوران کوئی بھی معلومات ضائع نہ ہو۔

test_sentences = [
    "اردو ایک بہت خوبصورت زبان ہے",           # "Urdu is a very beautiful language"
    "پاکستان کا دارالحکومت اسلام آباد ہے",      # "The capital of Pakistan is Islamabad"
    "آج موسم بہت اچھا ہے",                     # "The weather is very nice today"
    "مصنوعی ذہانت مستقبل کی ٹیکنالوجی ہے",     # "AI is the technology of the future"
    "السلام علیکم! آپ کیسے ہیں؟",               # "Peace be upon you! How are you?"
]

print("=" * 70)
print("TOKENIZER TEST RESULTS")
print("=" * 70)

for sentence in test_sentences:
    encoded = tokenizer.encode(sentence)
    decoded = tokenizer.decode(encoded.ids)
    
    print(f"\n Input:    {sentence}")
    print(f" Token IDs: {encoded.ids}")
    print(f"  Tokens:   {encoded.tokens}")
    print(f" Decoded:  {decoded}")
    print(f"   Num tokens: {len(encoded.ids)}")
    print(f"   Roundtrip OK: {sentence in decoded}")
    print("-" * 70)

آؤٹ پٹ ہے:

======================================================================
TOKENIZER TEST RESULTS
======================================================================

 Input:    اردو ایک بہت خوبصورت زبان ہے
 Token IDs: [2, 1418, 324, 431, 2965, 1430, 276, 3]
 Tokens:   ['', '▁اردو', '▁ایک', '▁بہت', '▁خوبصورت', '▁زبان', '▁ہے', '']
 Decoded:  اردو ایک بہت خوبصورت زبان ہے
   Num tokens: 8
   Roundtrip OK: True
----------------------------------------------------------------------

 Input:    پاکستان کا دارالحکومت اسلام آباد ہے
 Token IDs: [2, 474, 289, 3699, 616, 1004, 276, 3]
 Tokens:   ['', '▁پاکستان', '▁کا', '▁دارالحکومت', '▁اسلام', '▁آباد', '▁ہے', '']
 Decoded:  پاکستان کا دارالحکومت اسلام آباد ہے
   Num tokens: 8
   Roundtrip OK: True

کس طرح توجہ دینا اور (پوسٹ پروسیسنگ سٹیپ کا شکریہ) درج ذیل عام اردو الفاظ خود بخود شامل ہو جاتے ہیں: پاکستان ایک واحد ٹوکن کے طور پر برقرار رکھا میٹا اسپیس لغت ٹوکنائزر کے لیے سابقہ ​​الفاظ کی حدود کو نشان زد کرتا ہے۔ سب سے اہم بات یہ ہے کہ تمام راؤنڈ ٹرپ کامیاب ہوتے ہیں۔ اس کا مطلب ہے کہ ڈی کوڈ شدہ متن اصل ان پٹ سے بالکل میل کھاتا ہے۔

زرخیزی سکور

زرخیزی فی لفظ ٹوکن کی اوسط تعداد ہے۔

  • 1 کی افادیت کا مطلب ہے کہ ہر لفظ ایک ٹوکن پر نقشہ بناتا ہے (جدید سب ورڈ ٹوکنائزرز میں مثالی لیکن غیر حقیقی)۔

  • جدید LLMs میں، زبان کے لحاظ سے شرح پیدائش عام طور پر 1.3-2.5 کے لگ بھگ ہوتی ہے۔

  • زیادہ پیداواری صلاحیت کا مطلب ہے زیادہ ٹوکن اسپلٹ، جو لاگت کو بڑھاتا ہے اور کارکردگی کو کم کرتا ہے، لیکن زبان کی پیچیدگی کے ساتھ ساتھ ٹوکنائزر کے معیار سے بھی متاثر ہوتا ہے۔

# ============================================================
# Calculate fertility score on training corpus
# ============================================================
import json

jsonl_file = CLEANED_DIR / "urdu_corpus.jsonl"
corpus_words = 0
corpus_tokens = 0
sample_size = 10000  # Sample 10K documents for speed

print(f"Calculating fertility on {sample_size:,} documents from corpus...")

with open(jsonl_file, "r", encoding="utf-8") as f:
    for i, line in enumerate(f):
        if i >= sample_size:
            break
        doc = json.loads(line)
        text = doc["text"]
        
        words = text.split()
        tokens = tokenizer.encode(text).tokens
        n_tokens = len(tokens) - 2  # Remove  and 
        
        corpus_words += len(words)
        corpus_tokens += n_tokens

corpus_fertility = corpus_tokens / corpus_words
print(f"\n📊 Fertility Score (corpus): {corpus_fertility:.2f} tokens/word")
print(f"   (Total: {corpus_words:,} words → {corpus_tokens:,} tokens)")
print(f"   Documents sampled: {min(i+1, sample_size):,}")

if corpus_fertility < 2.0:
    print("   ✅ Excellent! Tokenizer is well-optimized for Urdu.")
elif corpus_fertility < 3.0:
    print("   ⚠ Good, but could be better. Consider larger vocab.")
else:
    print("   ❌ High fertility. The tokenizer needs improvement.")

ہمیں یہاں حاصل ہونے والا زرخیزی سکور 1.04 ہے، جو بہت اچھا ہے۔ تاہم، ذہن میں رکھیں کہ یہ تعداد مصنوعی طور پر کم ہے کیونکہ ٹوکنائزر کو اسی چھوٹے کارپس پر تربیت دی گئی تھی جس پر اس کا جائزہ لیا گیا تھا۔ بڑے یا پوشیدہ ڈیٹا سیٹس کے استعمال کے نتیجے میں زرخیزی کی شرح زیادہ ہونے کا امکان ہے (پروڈکشن ٹوکنائزرز کے لیے عام 1.3-2.5 رینج کے قریب)۔

ٹوکنائزر کو محفوظ کریں۔

آخری مرحلہ ٹوکنائزر کو JSON فارمیٹ میں محفوظ کرنا اور اس بات کو یقینی بنانا ہے کہ یہ صحیح طریقے سے لوڈ ہو۔

# ============================================================
# Save the tokenizer
# ============================================================

tokenizer_path = str(TOKENIZER_DIR / "urdu_bpe_tokenizer.json")
tokenizer.save(tokenizer_path)

print(f" Tokenizer saved to: {tokenizer_path}")
print(f"   File size: {os.path.getsize(tokenizer_path) / 1024:.0f} KB")

# Verify we can load it back
loaded_tokenizer = Tokenizer.from_file(tokenizer_path)
test = loaded_tokenizer.encode("اردو ایک خوبصورت زبان ہے")
print(f"\n   Verification: {test.tokens}")
print(f"    Tokenizer loads correctly!")

ایک بار محفوظ ہونے کے بعد، آپ کے پاس تلاش کرنے کی میز ہوگی۔ آپ اسے اپنے ڈیٹا اکٹھا کرنے کے ساتھ درج ذیل اہم اقدامات کرنے کے لیے استعمال کر سکتے ہیں۔ پیشگی تربیت.

3. پری ٹریننگ

اس حصے میں، ماڈل زبان، گرامر، پیٹرن، اور الفاظ سیکھتا ہے۔ تربیت مکمل ہونے کے بعد، ماڈل ترتیب میں اگلے لفظ کی پیشین گوئی کر سکتا ہے، اور یہیں سے ہم خام ڈیٹا کو LLM میں تبدیل ہوتے دیکھنا شروع کر دیتے ہیں۔

LLM دراصل اگلا لفظ پیشین گو ہے۔ الفاظ کے ایک سیٹ کو دیکھتے ہوئے، یہ ممکنہ طور پر اگلے لفظ کی پیش گوئی کرتا ہے۔

تربیت کے ذریعے، ماڈل سیکھتا ہے:

  • زبان کی ترکیب

  • سیمنٹکس، سیاق و سباق کے معنی

  • کثرت سے استعمال ہونے والے تاثرات

  • تربیتی ڈیٹا سیٹ سے حقائق

تربیت کے لیے کئی اختیارات ہیں۔ چونکہ ماڈل چھوٹا ہے، آپ اسے اپنی مقامی مشین پر تربیت دے سکتے ہیں۔ یہ سست ہے، لیکن یہ کام ہو جاتا ہے.

دوسرا آپشن گوگل کولاب استعمال کرنا ہے۔ یہ وہی ہے جو میں نے استعمال کیا: مفت ورژن T4 GPU کا استعمال کرتے ہوئے میری تربیت کی ضروریات کے لئے کافی تھا۔

پری ٹریننگ انجام دینے کے اقدامات

  1. ڈیٹا سیٹ JSONL فائل اور ٹوکنائزر کو گوگل ڈرائیو پر اپ لوڈ کریں۔

  2. ماڈل کنفیگریشن سیٹ کریں (الفاظ کا سائز، پرتیں، سر، وغیرہ)۔

  3. کنورٹر فن تعمیر کی وضاحت کریں (توجہ، فیڈ فارورڈ، بلاک)۔

  4. کارپس کو تربیت/توثیق کی تقسیم میں لوڈ کریں اور اسے ٹوکنائز کریں۔

  5. آپٹیمائزر، LR شیڈول اور چیک پوائنٹس کا استعمال کرتے ہوئے ٹریننگ لوپ چلائیں۔

ماڈل کی ترتیب

from dataclasses import dataclass

@dataclass
class UrduLLMConfig:
    # Vocabulary
    vocab_size: int = 32_000
    pad_token_id: int = 0
    bos_token_id: int = 2
    eos_token_id: int = 3

    # Model Architecture
    d_model: int = 384
    n_layers: int = 6
    n_heads: int = 6
    d_ff: int = 1536  # 4 * d_model
    dropout: float = 0.1
    max_seq_len: int = 256

    # Training
    batch_size: int = 32
    learning_rate: float = 3e-4
    weight_decay: float = 0.1
    max_epochs: int = 10
    warmup_steps: int = 500
    grad_clip: float = 1.0

کنفیگریشن پیرامیٹر کی تفصیل:

لغوی پیرامیٹرز (vocab_size, pad_token_id, bos_token_id, eos_token_id) بس اسے اس ٹوکنائزر سے جوڑیں جو آپ نے پہلے بنایا تھا۔ vocab_size 32K (BPE ذخیرہ الفاظ) ہے اور خصوصی ٹوکن IDs (0, 2, 3) ٹوکنائزر کی تربیت کے دوران تفویض کردہ عہدوں کے مطابق ہیں۔

ماڈل فن تعمیر کے پیرامیٹرز:

متغیر اس کا کیا مطلب ہے ہاں اقدار کا اثر
d_model ایمبیڈنگ/ویکٹر سائز فی ٹوکن 384 اعلی: بہتر فہم، لیکن سست رفتار اور زیادہ میموری۔ لو: تیز، لیکن کم اظہار خیال۔
n_layers ٹرانسفارمر کی تہوں کی تعداد 6 مزید پرتیں بہتر تفہیم فراہم کرتی ہیں، لیکن طویل تاخیر۔ کم: تیز لیکن کم طاقتور
n_heads دھیان سر فی پرت 6 مزید سر: بہتر سیاق و سباق کی گرفت۔ بہت کم: ریاستی تنوع محدود ہے۔
d_ff فیڈ فارورڈ پرت کا سائز 1536 بڑا: زیادہ کمپیوٹیشنل پاور۔ چھوٹا: تیز لیکن کمزور تبدیلی
dropout تربیت کے دوران % نیوران حذف ہو گئے۔ 0.1 زیادہ: زیادہ فٹنگ کو روکتا ہے، لیکن اس کے نتیجے میں کم فٹنگ ہو سکتی ہے۔ کم: تربیت کے لیے اچھا، لیکن زیادہ فٹنگ کا خطرہ
max_seq_len زیادہ سے زیادہ ٹوکن فی ان پٹ 256 اعلی: زیادہ سیاق و سباق، لیکن سست اور زیادہ مہنگا۔ کم: تیز، لیکن سیاق و سباق محدود

ٹریننگ ہائپرپیرامیٹر:

متغیر اس کا کیا مطلب ہے ہاں اقدار کا اثر
batch_size نمونے فی تربیتی قدم 32 بڑا: تربیت تیز ہوتی ہے لیکن اس کے لیے زیادہ میموری کی ضرورت ہوتی ہے۔ چھوٹا: مستحکم لیکن سست
learning_rate قدم کے سائز کو اپ ڈیٹ کریں۔ 0.0003 اگر یہ بہت زیادہ ہے، تو آپ کی تربیت غیر مستحکم ہوگی۔ بہت کم: سیکھنے کی شرح بہت سست ہے۔
weight_decay معمول کی طاقت 0.1 اعلی: اوور فٹنگ کو کم کرتا ہے۔ کم: اوور فٹنگ کا خطرہ
max_epochs مکمل ڈیٹا سیٹ پاس 10 مزید: سیکھنے کی صلاحیت کو بہتر بناتا ہے، لیکن زیادہ فٹ ہونے کا خطرہ ہوتا ہے۔ چند: زیر تربیت ماڈل
warmup_steps بتدریج LR میں اضافہ کا مرحلہ 500 مزید: نرم شروعات، محفوظ تربیت۔ کم: ابتدائی عدم استحکام کا خطرہ
grad_clip زیادہ سے زیادہ میلان قدر 1.0 کم: مستحکم، لیکن سیکھنے میں سست۔ زیادہ: ڈھلوان دھماکے کا خطرہ

ٹرانسفارمر فن تعمیر

یہاں تربیت کے اہم حصے ہیں: ٹرانسفارمر فن تعمیر. کوڈ شروع کرنے سے پہلے، یہ جاننا ضروری ہے کہ مترجم کا فن تعمیر کیا ہے۔

اس بارے میں مزید جاننے کے لیے کہ ٹرانسفارمر کیا ہے اور یہ RNNs اور CNNs سے کیسے مختلف ہے، ہم اس مضمون کو پڑھنے کی تجویز کرتے ہیں: AWS: مصنوعی ذہانت میں ٹرانسفارمرز کیا ہیں؟

لیکن مختصر میں:

"ایک ٹرانسفارمر نیورل نیٹ ورک فن تعمیر کی ایک قسم ہے جو کسی ان پٹ کی ترتیب کو آؤٹ پٹ کی ترتیب میں تبدیل یا تبدیل کرتا ہے۔”

اصل ٹرانسفارمر مضمون میں دونوں کو شامل کیا گیا تھا۔ انکوڈر (ان پٹ پڑھیں) اور ڈیکوڈر (جنریٹنگ آؤٹ پٹ)۔ تاہم، ہمارے جیسے جی پی ٹی طرز کے ماڈل صرف ڈیکوڈر کا حصہ استعمال کرتے ہیں۔ یہ ہے صرف ڈیکوڈر فن تعمیر

ڈیکوڈر ٹوکنز کی ایک سیریز لیتا ہے اور ان کا اطلاق کرتا ہے۔ ذاتی مفاد ان کے درمیان تعلقات کو سمجھیں اور اگلے ٹوکن کی پیشن گوئی کریں۔

خود دھیان ہی وہ ہے جو ٹرانسڈیوسر کو طاقتور بناتا ہے۔ RNN کی طرح ایک ایک کر کے ٹوکن پر کارروائی کرنے کے بجائے، ماڈل تمام پچھلے ٹوکنز کو بیک وقت دیکھتا ہے اور اس بات کا تعین کرتا ہے کہ کون سا ٹوکن موجودہ پیشین گوئی سے زیادہ متعلقہ ہے۔

یہاں مکمل کنورٹر کوڈ ہے: ہر جزو کا تفصیلی تجزیہ درج ذیل ہے:

import math
import torch
import torch.nn as nn
import torch.nn.functional as F


class MultiHeadSelfAttention(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.n_heads = config.n_heads
        self.d_model = config.d_model
        self.head_dim = config.d_model // config.n_heads

        self.qkv_proj = nn.Linear(config.d_model, 3 * config.d_model)
        self.out_proj = nn.Linear(config.d_model, config.d_model)
        self.dropout = nn.Dropout(config.dropout)

    def forward(self, x, mask=None):
        B, T, C = x.shape

        qkv = self.qkv_proj(x)
        qkv = qkv.reshape(B, T, 3, self.n_heads, self.head_dim)
        qkv = qkv.permute(2, 0, 3, 1, 4)
        q, k, v = qkv[0], qkv[1], qkv[2]

        attn = (q @ k.transpose(-2, -1)) * (self.head_dim ** -0.5)

        if mask is not None:
            attn = attn.masked_fill(mask == 0, float('-inf'))

        attn = F.softmax(attn, dim=-1)
        attn = self.dropout(attn)

        out = attn @ v
        out = out.transpose(1, 2).reshape(B, T, C)
        out = self.out_proj(out)
        return out


class FeedForward(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.fc1 = nn.Linear(config.d_model, config.d_ff)
        self.fc2 = nn.Linear(config.d_ff, config.d_model)
        self.dropout = nn.Dropout(config.dropout)

    def forward(self, x):
        x = F.gelu(self.fc1(x))
        x = self.dropout(x)
        x = self.fc2(x)
        return x


class TransformerBlock(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.ln1 = nn.LayerNorm(config.d_model)
        self.attn = MultiHeadSelfAttention(config)
        self.ln2 = nn.LayerNorm(config.d_model)
        self.ff = FeedForward(config)
        self.dropout = nn.Dropout(config.dropout)

    def forward(self, x, mask=None):
        x = x + self.dropout(self.attn(self.ln1(x), mask))
        x = x + self.dropout(self.ff(self.ln2(x)))
        return x


class UrduGPT(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.config = config

        self.token_emb = nn.Embedding(config.vocab_size, config.d_model)
        self.pos_emb = nn.Embedding(config.max_seq_len, config.d_model)
        self.dropout = nn.Dropout(config.dropout)

        self.blocks = nn.ModuleList([
            TransformerBlock(config) for _ in range(config.n_layers)
        ])

        self.ln_f = nn.LayerNorm(config.d_model)
        self.head = nn.Linear(config.d_model, config.vocab_size, bias=False)

        # Weight tying
        self.head.weight = self.token_emb.weight

        self.apply(self._init_weights)

    def _init_weights(self, module):
        if isinstance(module, nn.Linear):
            torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
            if module.bias is not None:
                torch.nn.init.zeros_(module.bias)
        elif isinstance(module, nn.Embedding):
            torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)

    def forward(self, input_ids, targets=None):
        B, T = input_ids.shape
        device = input_ids.device

        tok_emb = self.token_emb(input_ids)
        pos = torch.arange(0, T, dtype=torch.long, device=device)
        pos_emb = self.pos_emb(pos)

        x = self.dropout(tok_emb + pos_emb)

        # Causal mask
        mask = torch.tril(torch.ones(T, T, device=device)).unsqueeze(0).unsqueeze(0)

        for block in self.blocks:
            x = block(x, mask)

        x = self.ln_f(x)
        logits = self.head(x)

        loss = None
        if targets is not None:
            loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))

        return {'logits': logits, 'loss': loss}

    @torch.no_grad()
    def generate(self, input_ids, max_new_tokens=100, temperature=0.8,
                 top_k=50, top_p=0.9, eos_token_id=None):
        """
        Generate text autoregressively.

        Sampling strategies:
        - temperature: Controls randomness (low = deterministic, high = creative)
        - top_k: Only consider the top K most likely tokens
        - top_p (nucleus): Only consider tokens whose cumulative probability <= p
        - eos_token_id: Stop generating when this token is produced
        """
        self.eval()
        eos_token_id = eos_token_id or getattr(self.config, 'eos_token_id', None)

        for _ in range(max_new_tokens):
            idx_cond = input_ids if input_ids.size(1) <= self.config.max_seq_len \
                       else input_ids[:, -self.config.max_seq_len:]

            outputs = self.forward(idx_cond)
            logits = outputs["logits"][:, -1, :] / temperature

            # Top-K filtering
            if top_k > 0:
                v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
                logits[logits < v[:, [-1]]] = float('-inf')

            # Top-P (nucleus) filtering
            if top_p < 1.0:
                sorted_logits, sorted_indices = torch.sort(logits, descending=True)
                cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
                sorted_indices_to_remove = cumulative_probs > top_p
                sorted_indices_to_remove[:, 1:] = sorted_indices_to_remove[:, :-1].clone()
                sorted_indices_to_remove[:, 0] = 0
                indices_to_remove = sorted_indices_to_remove.scatter(
                    1, sorted_indices, sorted_indices_to_remove
                )
                logits[indices_to_remove] = float('-inf')

            probs = F.softmax(logits, dim=-1)
            next_token = torch.multinomial(probs, num_samples=1)
            input_ids = torch.cat([input_ids, next_token], dim=1)

            if eos_token_id is not None and next_token.item() == eos_token_id:
                break

        return input_ids

یہ کوڈ ٹیکسٹ پیشن گوئی مشین بناتا ہے۔ اسے اردو کے چند الفاظ دیں اور یہ اگلے لفظ کا اندازہ لگاتا رہے گا جب تک کہ یہ جملہ نہ بن جائے۔ لفظی طور پر جس طرح سے ChatGPT کام کرتا ہے وہ بھی بہت بڑا ہے۔

کنورٹر کوڈ کا تجزیہ

1. MultiHeadSelfAttention: "لُک بیک سسٹم”

ایک جملہ پڑھنے کا تصور کریں۔ جب آپ لفظ "اس” (یہ) دیکھتے ہیں، تو آپ کا دماغ یہ جاننے کے لیے پیچھے مڑ کر دیکھتا ہے کہ "اس” سے کیا مراد ہے۔ یہ دلچسپی کی بات ہے۔

کیو، کے، وی: اسے ایک لائبریری کی طرح سوچیں۔

  • سوال (س): "میں X کے بارے میں معلومات تلاش کر رہا ہوں”

  • کلید (K): ہر پچھلے لفظ کو "Y کے بارے میں معلومات ہے” کی علامت سے نشان زد کیا گیا ہے۔

  • قدر (V): اصل معلومات جو الفاظ بیان کرتے ہیں۔

6 سر = چھ مختلف "قارئین” ایک ہی وقت میں جملے کو دیکھ رہے ہیں۔ کچھ لوگ گرامر پر توجہ مرکوز کر سکتے ہیں، کچھ لوگ معنی پر، دوسرے قریبی الفاظ وغیرہ پر۔

causal ماسک = یہ قاعدہ کہ "آپ صرف وہی الفاظ دیکھ سکتے ہیں جو پہلے آتے ہیں نہ کہ بعد میں آنے والے الفاظ کو۔” (کیونکہ تخلیق کے وقت مستقبل کا لفظ ابھی موجود نہیں ہے!)

ریاضی: "ہر لفظ کتنا متعلقہ ہے؟” حاصل کرنے کے لیے Q×K کو ضرب دیں۔ اور پھر اس سکور کا استعمال V سے سب سے مفید معلومات حاصل کرنے کے لیے کریں۔

2. فیڈ فارورڈ: "سوچ کے اقدامات”

یہ وہ مرحلہ ہے جہاں آپ یہ جاننے پر توجہ دیتے ہیں کہ کون سے الفاظ اہم ہیں، اور پھر ماڈل دراصل سوچتا ہے کہ ان کا کیا مطلب ہے۔

یہ صرف دو پرتیں ہیں:

  • توسیع (384 → 1536): اپنے ماڈل کو سوچنے کے لیے مزید "دماغ کی جگہ” دیں۔

  • سکڑیں (1536 → 384): اپنے خیالات کو دوبارہ کمپریس کریں۔

  • GELU کو چالو کریں: یہ فیصلہ کرنے کے لیے فلٹر کریں (ہموار، سخت نہیں)

3. ٹرانسفارمر بلاک: "1 بار پڑھیں”

یہ کسی جملے کو پڑھنے اور سوچنے کا عمل ہے۔

  • مرحلہ 1: دوسرے الفاظ دیکھیں (نوٹ)

  • مرحلہ 2: آپ جو دیکھتے ہیں اس کے بارے میں سوچیں (فیڈ فارورڈ)

  • پرت کا معیار: یہ اپنے دماغ کو قدموں کے درمیان ری سیٹ کرنے جیسا ہے تاکہ اس بات کو یقینی بنایا جا سکے کہ نمبر بہت زیادہ یا بہت چھوٹے نہ ہوں۔

  • باقی کنکشن (x + ...): ماڈل اصل خیال کو برقرار رکھتا ہے اور نئی بصیرت کا اضافہ کرتا ہے۔ یہ نوٹ لینے جیسا ہے۔ یہ پرانے نوٹ کو حذف کرنے کے بجائے ایک نیا نوٹ شامل کرتا ہے۔

ماڈل یہ 6 بار (6 بلاکس) کرتا ہے۔ ہر دور کے ساتھ، آپ متن کو قدرے گہرائی سے سمجھتے ہیں۔

4. اردو جی پی ٹی: "پرفیکٹ مشین”

ترتیب (__init__):

  • ٹوکن ایمبیڈنگ: بڑی تلاش کی میز۔ اردو کے 32,000 الفاظ/ ذیلی الفاظ میں سے ہر ایک کو 384 نمبروں کی فہرست ملتی ہے جو اس کے "معنی” کو ظاہر کرتی ہے۔

  • مقام داخل کریں: یہ ایک اور تلاش کی میز ہے جو ماڈل کو بتاتی ہے کہ "یہ لفظ 1st ہے، یہ 2nd ہے، یہ 3rd ہے…” (بصورت دیگر آپ لفظ کی ترتیب کو نہیں جانتے)۔

  • چھ ٹرانسفارمر بلاکس: یہ اوپر بیان کیے گئے چھ پڑھنے کے چکر ہیں۔

  • ایل ایم سر: آخر میں، ہم ماڈل کے اندرونی "خیالات” (384 نمبرز) کو 32,000 ممکنہ اگلے الفاظ میں سے ہر ایک کے لیے اسکور میں تبدیل کرتے ہیں۔

  • پابند وزن: ان پٹ لوک اپ ٹیبل اور آؤٹ پٹ سکور ٹیبل ایک ہی ڈیٹا کا اشتراک کرتے ہیں۔ میموری کو بچاتا ہے اور حقیقت میں بہتر کام کرتا ہے!

پروسیسنگ (forward):

  1. ہر لفظ کے معنی تلاش کریں (ایمبیڈنگ)

  2. مقام کی معلومات شامل کریں۔

  3. احتیاط + سوچ کے 6 مراحل پر عمل کریں۔

  4. مندرجہ ذیل تمام ممکنہ الفاظ کو اسکور کریں۔

  5. اگر آپ کو جواب معلوم ہے تو حساب لگائیں کہ آپ کتنے غلط تھے (نقصان)

متن بنائیں (generate): سادہ لوپ:

  1. اب تک کے الفاظ درج کریں۔

  2. اگلے لفظ کے لیے پوائنٹس حاصل کریں۔

  3. درجہ حرارت: اپنی تخلیقی صلاحیتوں پر قابو پالیں۔ کم = محفوظ/پیش گوئی، اعلی = جنگلی/تخلیقی۔

  4. اوپر K: صرف K کے بہترین اختیارات پر غور کریں (31,950 غیر متوقع الفاظ کو نظر انداز کرتے ہوئے)۔

  5. ٹاپ پی (ایٹمی): متحرک طور پر ٹوکنز کا سب سے چھوٹا سیٹ منتخب کرتا ہے جس کا مجموعی امکان حد تک پہنچ جاتا ہے۔

  6. بقیہ اختیارات میں سے بے ترتیب ایک لفظ کا انتخاب کریں۔

  7. اسے اپنے جملے میں شامل کریں اور مرحلہ 1 پر واپس جائیں۔

  8. روکیں اگر: پیدا کیا یا max_new_tokens پہنچ گئے

ڈیٹاسیٹ اور تربیت لوڈ کریں۔

سب سے پہلے، ہم JSONL کارپس کو لوڈ کرتے ہیں اور تمام دستاویزات کو ٹوکن IDs کی ایک طویل ترتیب میں ٹوکنائز کرتے ہیں۔ اس کے بعد ہم اس 90/10 کو تربیت اور توثیق کے سیٹوں میں تقسیم کرتے ہیں اور اسے ایک PyTorch ڈیٹاسیٹ میں لپیٹ دیتے ہیں جو اگلی ٹوکن پیشین گوئی کے لیے مقررہ لمبائی کے ٹکڑے تیار کرتا ہے۔

import json
from tokenizers import Tokenizer
from torch.utils.data import Dataset, DataLoader
from tqdm import tqdm

# Device
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using: {device}")

# Load tokenizer
tokenizer = Tokenizer.from_file(TOKENIZER_PATH)
print(f"Tokenizer loaded. Vocab: {tokenizer.get_vocab_size():,}")

# Load and tokenize corpus
print("Loading corpus...")
all_token_ids = []
with open(DATA_PATH, "r", encoding="utf-8") as f:
    for line in tqdm(f, desc="Tokenizing"):
        doc = json.loads(line)
        encoded = tokenizer.encode(doc["text"])
        all_token_ids.extend(encoded.ids)

all_token_ids = torch.tensor(all_token_ids, dtype=torch.long)
print(f"Total tokens: {len(all_token_ids):,}")
class UrduTextDataset(Dataset):
    def __init__(self, token_ids, seq_len):
        self.token_ids = token_ids
        self.seq_len = seq_len
        self.n_chunks = (len(token_ids) - 1) // seq_len

    def __len__(self):
        return self.n_chunks

    def __getitem__(self, idx):
        start = idx * self.seq_len
        chunk = self.token_ids[start:start + self.seq_len + 1]
        return chunk[:-1], chunk[1:]  # input, target (shifted by 1)

config = UrduLLMConfig()

# Split 90/10
split_idx = int(len(all_token_ids) * 0.9)
train_dataset = UrduTextDataset(all_token_ids[:split_idx], config.max_seq_len)
val_dataset = UrduTextDataset(all_token_ids[split_idx:], config.max_seq_len)

train_loader = DataLoader(train_dataset, batch_size=config.batch_size, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=config.batch_size)

print(f"Train: {len(train_dataset):,} chunks")
print(f"Val: {len(val_dataset):,} chunks")

ہر حصہ 256 ٹوکن لمبا ہے۔ __getitem__ رپورٹ (input, target) یہاں ہدف ایک پوزیشن سے منتقل ہونے والا ان پٹ ہے، جو بالکل وہی ہے جو اگلے ٹوکن کی پیشن گوئی کرنے کے لیے درکار ہے۔

میرے لیے تربیت میں تقریباً 3 گھنٹے لگے اور میں نے 3 دور مکمل کر لیے۔ بنیادی طور پر، مجھے 10 دور کرنے تھے، لیکن 3 کے بعد میں نے Google Colab کی مفت حد کو مارا۔ چونکہ تربیت کا مقصد سیکھنا ہے، اس لیے میں نے بنایا ہوا ماڈل استعمال کیا اور اسے ڈرائیو میں محفوظ کر لیا۔

مکمل تربیتی کوڈ ذیل میں ہے:

# Optimizer
optimizer = torch.optim.AdamW(model.parameters(), lr=config.learning_rate, weight_decay=config.weight_decay)

# LR Schedule
total_steps = len(train_loader) * config.max_epochs
def get_lr(step):
    if step < config.warmup_steps:
        return config.learning_rate * step / config.warmup_steps
    progress = (step - config.warmup_steps) / (total_steps - config.warmup_steps)
    return config.learning_rate * 0.5 * (1 + math.cos(math.pi * progress))

# Training
history = {'train_loss': [], 'val_loss': []}
global_step = 0
best_val_loss = float('inf')

for epoch in range(config.max_epochs):
    model.train()
    epoch_loss = 0
    pbar = tqdm(train_loader, desc=f"Epoch {epoch+1}")

    for input_ids, targets in pbar:
        input_ids, targets = input_ids.to(device), targets.to(device)

        lr = get_lr(global_step)
        for g in optimizer.param_groups:
            g['lr'] = lr

        outputs = model(input_ids, targets)
        loss = outputs['loss']

        optimizer.zero_grad()
        loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), config.grad_clip)
        optimizer.step()

        epoch_loss += loss.item()
        global_step += 1
        pbar.set_postfix({'loss': f'{loss.item():.4f}'})

    # Validation
    model.eval()
    val_loss = 0
    with torch.no_grad():
        for input_ids, targets in val_loader:
            input_ids, targets = input_ids.to(device), targets.to(device)
            val_loss += model(input_ids, targets)['loss'].item()
    val_loss /= len(val_loader)

    train_loss = epoch_loss / len(train_loader)
    history['train_loss'].append(train_loss)
    history['val_loss'].append(val_loss)

    print(f"Epoch {epoch+1}: Train={train_loss:.4f}, Val={val_loss:.4f}")

    # Save best
    if val_loss < best_val_loss:
        best_val_loss = val_loss
        torch.save(model.state_dict(), f"{DRIVE_PATH}/best_model.pt")
        print(f"Best model saved!")

print(f"\nDone! Best val loss: {best_val_loss:.4f}")

اب آئیے تجزیہ کرتے ہیں کہ تربیتی کوڈ کا ہر حصہ کیا کرتا ہے۔

سیکھنے کے کوڈ کی وضاحت: لائن بہ لائن

1. آپٹیمائزر کی ترتیبات

optimizer = torch.optim.AdamW(model.parameters(), lr=config.learning_rate, weight_decay=config.weight_decay)

AdamW فی پیرامیٹر پر عملدرآمد کے دو اعدادوشمار کو برقرار رکھتا ہے (23M × 2 = 46M اضافی قدر میموری میں)۔

  • پہلا لمحہ (مومینٹم): ڈھلوان کی ایک تیز رفتار حرکت اوسط۔ شور مچانے والی اپ ڈیٹس کو ہموار کریں تاکہ آپٹمائزر ٹیڑھا نہ ہو۔

  • دوسرا لمحہ: مربع ڈھلوان کا ایک کفایتی حرکت پذیری اوسط۔ ہر پیرامیٹر کے لیے ایک منفرد انکولی سیکھنے کی شرح فراہم کرتا ہے (کثرت سے اپ ڈیٹ ہونے والے پیرامیٹرز کے لیے چھوٹے اقدامات اور کبھی کبھار پیرامیٹرز کے لیے بڑے اقدامات)۔

  • وزن میں کمی (0.1): ہر قدم پر وزن کو ضرب دیا جاتا ہے۔ (1 - lr × 0.1)تھوڑا سا زوم آؤٹ کریں۔ یہ ہے L2 ریگولرائزیشن. ایک وزن کو بہت زیادہ ہونے سے روک کر اوور فٹنگ کو کم کرتا ہے۔ ایڈم ڈبلیو میں "ڈبلیو" کا مطلب ہے کہ اس کشی کو گریڈینٹ اپ ڈیٹ سے ملایا جاتا ہے (ونیلا ایڈم کی طرح میلان میں ملانے کے بجائے براہ راست وزن پر لاگو ہوتا ہے)۔

2. سیکھنے کی شرح کا شیڈول

total_steps = len(train_loader) * config.max_epochs  # e.g., 500 batches × 10 epochs = 5000 steps

def get_lr(step):
    if step < config.warmup_steps:                                      # Phase 1: steps 0–499
        return config.learning_rate * step / config.warmup_steps        # Linear ramp: 0 → 3e-4
    progress = (step - config.warmup_steps) / (total_steps - config.warmup_steps)  # 0.0 → 1.0
    return config.learning_rate * 0.5 * (1 + math.cos(math.pi * progress))        # 3e-4 → ~0
  • وارم اپ (پہلے 500 مراحل): مرحلے 0 میں، وزن بے ترتیب ہوتے ہیں اور گریڈیئنٹس نیم بے ترتیب سمتوں کی طرف اشارہ کرتے ہیں، لہذا ایک بڑا LR تباہ کن پیرامیٹر اپ ڈیٹس کا نتیجہ ہوگا۔ 0 سے 3e-4 تک لکیری طور پر اضافہ کرکے، ہم جارحانہ اپڈیٹس کرنے سے پہلے نقصان کے ماحول کو "مستحکم" کرتے ہیں۔

  • کوزائن کا خاتمہ (باقی مراحل): سرکاری 0.5 × (1 + cos(π × progress)) جیسے جیسے 0 سے 1 تک ترقی ہوتی ہے، یہ 1.0 سے 0.0 تک ایک ہموار S-کرو کا پتہ لگاتا ہے۔ چوٹی LR کو ضرب دینے سے ملتا ہے:

LR:  0 ──ramp──▶ peak ──smooth curve──▶ ~0
     |  warmup  |     cosine decay      |

3. متغیرات کو ٹریک کرنا

history = {'train_loss': [], 'val_loss': []}   # For plotting curves later
global_step = 0                                 # Counts total batches across all epochs (for LR schedule)
best_val_loss = float('inf')                    # Tracks best validation; starts at infinity so any real loss beats it

4. ٹریننگ لوپ

بیرونی لوپ: عہد

for epoch in range(config.max_epochs):
    model.train()     # Enables dropout (randomly zeros 10% of activations for regularization)

ہر دور = تمام تربیتی ڈیٹا سے ایک مکمل پاس۔ ہم دہراتے ہیں max_epochs گول

اندرونی لوپ: جگہ کا تعین

1. GPU پر جائیں:

input_ids, targets = input_ids.to(device), targets.to(device)

ٹینسر ڈیٹا کو CPU RAM سے GPU VRAM میں منتقل کریں۔ ٹرانسفارمر میں میٹرکس ضرب (توجہ، FFN) بڑے پیمانے پر متوازی ہونے کی وجہ سے GPUs پر 50 سے 100 گنا تیز چلتی ہے۔

2. دستی LR اپ ڈیٹ:

lr = get_lr(global_step)
for g in optimizer.param_groups:
    g['lr'] = lr

PyTorch کا AdamW مقامی طور پر صارف کے طے شدہ نظام الاوقات کی حمایت نہیں کرتا ہے، اس لیے ہم ہر قدم پر ایل آر کو دستی طور پر اوور رائیڈ کرتے ہیں۔ param_groups ایک فہرست ہے (یہاں ایک گروپ) اور ہر گروپ کا اپنا LR/وزن میں کمی ہو سکتی ہے۔

3. فارورڈ پاس:

outputs = model(input_ids, targets)
loss = outputs['loss']

ان پٹ ٹوکن ایمبیڈنگ → چھ ٹرانسڈیوسر بلاکس → LM ہیڈ → لاگٹ سے گزرتے ہیں۔ کراس اینٹروپی نقصان کا حساب لاگٹس (شکلوں) کے درمیان کیا جاتا ہے۔ [batch, seq_len, 32000]) اور ہدف ٹوکن ID۔ یہ نقصان اس منفی لاگ امکان کی پیمائش کرتا ہے جو ماڈل اگلے درست ٹوکن کو تفویض کرتا ہے، جس کا اوسط تمام پوزیشن اور پلیسمنٹ کے عوامل پر ہوتا ہے۔

4. ریورس پاس + اپ ڈیٹ:

optimizer.zero_grad()          # Reset all parameter gradients to zero (they accumulate by default)
loss.backward()                # Backpropagation: compute ∂loss/∂θ for all 23M parameters via chain rule
torch.nn.utils.clip_grad_norm_(model.parameters(), config.grad_clip)  # If ||gradient||₂ > 1.0, scale it down
optimizer.step()               # θ_new = θ_old - lr × adam_adjusted_gradient - lr × weight_decay × θ_old
  • zero_grad(): PyTorch پہلے سے طے شدہ طور پر گریڈینٹ جمع کرتا ہے (مائیکرو بیچوں میں گریڈینٹ جمع کرنے کے لیے مفید)۔ ہر نئے پسماندہ پاس سے پہلے اسے دستی طور پر صاف کرنا ضروری ہے۔

  • loss.backward(): بیک پروپیگیشن کمپیوٹیشنل گراف کو پیچھے کی طرف لے جاتا ہے اور تمام پیرامیٹرز کے لیے ∂loss/∂θ کی گنتی کرنے کے لیے چین کے اصول کا استعمال کرتا ہے۔ یہ، فارورڈ فارورڈنگ کے ساتھ، سب سے زیادہ کمپیوٹ کرنے والا قدم ہے۔

  • تدریجی تراشنا: ایک ویکٹر کے ذریعے جڑے ہوئے تمام پیرامیٹر گریڈینٹ پر L2 معمول کی گنتی کریں۔ اگر Norm 1.0 سے زیادہ ہو تو، تمام گریڈیئنٹس کو اس سے ضرب دیا جاتا ہے: 1.0/normسمت کو برقرار رکھتا ہے لیکن سائز کو محدود کرتا ہے۔ یہ اسپارس پلیسمنٹ (غیر معمولی ٹوکن کی تقسیم) کو تباہ کن بڑے اپ ڈیٹس سے روکتا ہے جو تربیت کو غیر مستحکم کرتے ہیں۔

  • optimizer.step(): ایڈم ڈبلیو مومینٹم، پیرامیٹر کے لیے مخصوص انڈیپٹیو ایل آر، اور ڈیکپلڈ ویٹ ڈے کا استعمال کرتے ہوئے اپ ڈیٹ کے قوانین کا اطلاق کرتا ہے۔

5. بک کیپنگ:

epoch_loss += loss.item()      # .item() extracts the Python float from the CUDA tensor (avoids GPU memory leak)
global_step += 1               # Increment for LR schedule
pbar.set_postfix({'loss': ...})  # Update the tqdm progress bar display

6. تصدیق

model.eval()                   # Disables dropout so we use full model capacity for honest evaluation
val_loss = 0
with torch.no_grad():          # Disables gradient tracking, saves ~50% memory and runs faster
    for input_ids, targets in val_loader:
        input_ids, targets = input_ids.to(device), targets.to(device)
        val_loss += model(input_ids, targets)['loss'].item()
val_loss /= len(val_loader)    # Average loss per batch

یہ آپ کے پاس موجود ڈیٹا کی جانچ کرتا ہے جس پر ماڈل کو کبھی تربیت نہیں دی گئی ہے۔ ٹرین کے نقصانات اور قیمت کے نقصانات کا موازنہ کرنے سے درج ذیل پتہ چلتا ہے:

پیٹرن معنی
دونوں کمی ماڈل عام کرنے کے قابل نمونوں کو سیکھ رہا ہے۔
ٹرین ↓، فٹ اسٹال/↑ اوور فٹنگ: حفظ کرنا، سیکھنا نہیں۔
اعلی اور فلیٹ انڈر فٹنگ: ماڈل کو زیادہ صلاحیت یا ڈیٹا کی ضرورت ہے۔

model.eval() چونکہ ڈراپ آؤٹ آف ہے، اس لیے پورے ماڈل کا جائزہ لیا جاتا ہے۔ torch.no_grad() چونکہ ہم صرف پیمائش کرتے ہیں، سیکھتے نہیں، اس لیے ہم تدریجی حساب کو چھوڑ دیتے ہیں۔

7. چیک پوائنٹ

if val_loss < best_val_loss:
    best_val_loss = val_loss
    torch.save(model.state_dict(), f"{DRIVE_PATH}/best_model.pt")

model.state_dict() واپسی OrderedDict پیرامیٹر کے نام ٹینسر کے لیے نقشہ بنائیں۔ torch.save ہم اسے ڈسک میں سیریلائز کرنے کے لیے Python's pickle + zip استعمال کرتے ہیں۔ صرف اس صورت میں بچت کریں جب قدر کا نقصان بہتر ہو۔

یہ ہے ابتدائی خاتمے ذہنی طور پر: ہم ان چیک پوائنٹس کو برقرار رکھتے ہیں جو سب سے بہتر عام بناتے ہیں، قطع نظر اس کے کہ بعد کے ادوار میں کیا ہوتا ہے۔

خلاصہ: 6 مراحل میں ایک ہی بیچ

  1. ماڈل کے ذریعے 32 اردو ترتیبیں فیڈ کریں → پیشین گوئی کے امکانات حاصل کریں۔

  2. کراس اینٹروپی بمقابلہ اصل اگلا ٹوکن → اسکیلر نقصان (یہ کتنا غلط ہے؟)

  3. 23M پیرامیٹرز کے ذریعے بیک پروپیگیشن → گریڈینٹ فی پیرامیٹر (کیا ترمیم کرنا ہے؟)

  4. 1.0 سے نیچے کلپ گریڈینٹ معیار → عدم استحکام کو روکیں۔

  5. ایڈم ڈبلیو پیرامیٹرز کو مومینٹم + ڈے → اصل سیکھنے کے طور پر اپ ڈیٹ کرتا ہے۔

  6. 5000 بار تک دہرائیں، بہترین چیک پوائنٹ محفوظ کریں → مکمل

اہم اشارے

کراس اینٹروپی نقصان یہ پیمائش کرتا ہے کہ پیش گوئی شدہ امکانی تقسیم اصل اگلے ٹوکن سے کتنی دور ہے۔ 32,000 سے زیادہ الفاظ کے بے ترتیب ماڈل کو نقصان ہوتا ہے ≒ ln(32000) ≒ 10.4۔

شرمندگی = نقصاناس کی تشریح اس طرح کی جا سکتی ہے کہ "ماڈل N یکساں ممکنہ ٹوکنز میں سے منتخب کرتا ہے"

  • پی پی ایل 32,000 = بے ترتیب اندازہ

  • PPL 100 = ~100 امیدواروں تک محدود

  • PPL 10 = بہت پراعتماد پیشین گوئی

ایک بار جب ٹریننگ مکمل ہو جائے اور ماڈل آپ کی ڈرائیو میں محفوظ ہو جائے، اگلا مرحلہ یہ ہے کہ ماڈل کو اپنے لوکل سسٹم میں ڈاؤن لوڈ کریں تاکہ درج ذیل اقدامات کیے جا سکیں:

اب آپ کے پاس ایک تیار ماڈل ہے، لیکن ایک سوال پیدا ہوتا ہے. کیا آپ اپنے ماڈل کے ساتھ چیٹ کرنے کے لیے تیار ہیں جیسا کہ آپ AI ٹول جیسے ChatGPT، Claude، یا Copilot کے ساتھ کرتے ہیں؟ جواب ہے نہیںمیں ابھی تک تیار نہیں ہوں۔ کیوں

تربیت کا حصہ مکمل ہو چکا ہے، لیکن میں نہیں جانتا کہ اس کی ساخت کیسے بنائی جائے یا اسے گفتگو کے انداز میں لکھنا، جیسے صارف کے سوالات کا جواب دینا۔ یہ وہ قدم ہے جسے ہم کہتے ہیں۔ زیر نگرانی فائن ٹیوننگ (SFT).

4. زیر نگرانی فائن ٹیوننگ (SFT)

بہت اعلیٰ سطح پر، SFT ایک ماڈل کو سکھاتا ہے کہ سوالات کا جواب کیسے دیا جائے۔ یہ ایک مثال دینے کی طرح ہے جس سے آپ سیکھ سکتے ہیں کہ کس طرح جواب دینا ہے۔ ہمارے پاس جتنے زیادہ کیسز ہوں گے، جواب اتنا ہی بہتر ہوگا۔ لہذا جوہر میں، زیر نگرانی فائن ٹیوننگ ماڈل کو ایک انٹرایکٹو ایجنٹ میں بدل دیتی ہے۔

اس کو حاصل کرنے کے لیے، ہم درج ذیل کلیدی جوڑے اور فارمیٹ کا استعمال کرتے ہوئے ایک مثال ڈیٹاسیٹ بناتے ہیں:

{
  "conversations": [
    {"role": "system", "content": "آپ ایک مددگار اردو اسسٹنٹ ہیں۔"},
    {"role": "user", "content": "سوال..."},
    {"role": "assistant", "content": "جواب..."}
  ]
}

تقریباً 79 مثالیں۔ اسے سسٹم میں فیڈ کیا جاتا ہے اور JSONL فارمیٹ میں اسٹور کیا جاتا ہے۔ ہم حقیقی دنیا کے معاملات سے مزید مثالیں استعمال کریں گے۔ جیسا کہ پہلے ہی ذکر کیا گیا ہے، آپ کے پاس جتنی زیادہ مثالیں ہوں گی، آپ کو اتنے ہی بہتر نتائج ملیں گے۔

تربیت کے لیے گفتگو کو فارمیٹ کرنا

اگلا مرحلہ تربیت کے لیے اوپر محفوظ کی گئی گفتگو کو فارمیٹ کرنا ہے۔ یہ SFT گفتگو کی فارمیٹنگ کا مرحلہ ہے۔ خام گفتگو JSON کو استعمال کرتے ہوئے ٹوکن ID ترتیب میں تبدیل کریں: نقصان ماسکنگلہذا، ماڈل صرف یہ سیکھتا ہے کہ کس طرح معاون ردعمل پیدا کرنا ہے۔

نقصان کی ماسکنگ کا مطلب ہے جان بوجھ کر تربیت کے نقصان سے ان پٹ کے کچھ حصوں کو چھپانا۔ اس صورت میں، ہم سسٹم پرامپٹس اور صارف کے پیغامات کو ماسک کرتے ہیں تاکہ ماڈل کو انہیں یاد رکھنے یا دوبارہ بنانے کی تربیت نہ دی جائے۔ تربیت کے اشارے صرف اسسٹنٹ کے جوابات سے آتے ہیں، جو ماڈل کو سکھانے کا ایک مفید حصہ ہے کہ کیا پیدا کرنا ہے اور کب روکنا ہے۔

حصہ 1: آٹو فارمیٹ کو غیر فعال کریں اور ایک خصوصی ٹوکن ID حاصل کریں۔

tokenizer.no_padding()

BOS_ID = tokenizer.token_to_id("")       # 2
EOS_ID = tokenizer.token_to_id("")       # 3
SEP_ID = tokenizer.token_to_id("")       # 4
PAD_ID = tokenizer.token_to_id("")       # 0
USER_ID = tokenizer.token_to_id("<|user|>")          # 5
ASSISTANT_ID = tokenizer.token_to_id("<|assistant|>") # 6
SYSTEM_ID = tokenizer.token_to_id("<|system|>")       # 7

IGNORE_INDEX = -100
  • no_padding(): ٹوکنائزر سے کہو، "پیڈنگ خودکار طور پر شامل نہ کریں۔ میں خود کروں گا۔" ٹوکن کی ترتیب پر مکمل کنٹرول کی ضرورت ہے۔

  • آپ ہر خصوصی ٹوکن کی عددی ID حاصل کر سکتے ہیں اور اسے دستی طور پر صحیح جگہ پر داخل کر سکتے ہیں۔

  • IGNORE_INDEX = -100: پائی ٹارچ cross_entropy اس میں بلٹ ان خصوصیات ہیں۔ -100 پر سیٹ کیے گئے کسی بھی لیبل کو نقصان کے حساب کتاب میں چھوڑ دیا جاتا ہے۔ اس طرح نقصان دہ ماسکنگ کو لاگو کیا جاتا ہے۔

حصہ 2: format_conversation(): بنیادی خصوصیات

اس سے مکالمے کے ذریعے دو متوازی انتظامات پیدا ہوتے ہیں۔

input_ids: [BOS, SYSTEM, آپ, ایک, مددگار, ..., SEP, USER, پاکستان, کا, ..., SEP, ASST, اسلام, آباد, ہے, EOS, PAD, PAD, ...]
labels:    [-100, -100, -100, -100, -100, ..., -100, -100, -100,    -100,..., -100, -100, اسلام, آباد, ہے, EOS, -100, -100, ...]

فنکشن کے اندر مرحلہ وار وضاحت:

1. BOS کے ساتھ شروع کریں:

input_ids = [BOS_ID]
labels = [IGNORE_INDEX]    # Don't learn to predict BOS

2. ہر موڑ پر مواد کو انکوڈ کریں اور خود بخود شامل کردہ BOS/EOS کو ہٹا دیں۔

content_ids = tokenizer.encode(content).ids
if content_ids[0] == BOS_ID: content_ids = content_ids[1:]     # Remove if tokenizer auto-added
if content_ids[-1] == EOS_ID: content_ids = content_ids[:-1]

چونکہ ہم دستی طور پر خاص ٹوکنز کو صحیح جگہ پر رکھتے ہیں، ہمیں ڈپلیکیٹس نہیں چاہیے، اس لیے ہم انہیں ہٹا دیتے ہیں۔

3. ہر کردار کے لیے ٹوکن کی ترتیب بنائیں۔

کردار ٹوکن کی ترتیب لیبل
نظام [SYSTEM_ID] + content + [SEP_ID] تمام -100 (نقاب پوش)
صارف [USER_ID] + content + [SEP_ID] تمام -100 (نقاب پوش)
اسسٹنٹ [ASST_ID] + content + [EOS_ID] [-100] + content + [EOS_ID]

اسسٹنٹ کے رول ٹوکن (<|assistant|>) خود نقاب پوش ہے کیونکہ ہم نہیں چاہتے کہ ماڈل یہ سیکھے کہ پیشن گوئی کیسے کی جائے۔ تاہم، اصل جوابی مواد اور چونکہ ہمارے پاس لیبل ہیں، ماڈل سیکھتا ہے:

4. کاٹ کر بھریں۔

input_ids = input_ids[:max_len]          # Cut to 256 tokens max
pad_len = max_len - len(input_ids)
input_ids = input_ids + [PAD_ID] * pad_len
labels = labels + [IGNORE_INDEX] * pad_len   # Don't learn from padding either

بیچ سیکھنے کے لیے، تمام ترتیبوں کی لمبائی ایک ہی ہونی چاہیے۔ پیڈنگ لیبل -100 ہے، لہذا نقصان کی صورت میں اسے نظر انداز کر دیا جاتا ہے۔

یہاں مکمل ایک ہے۔ format_conversation() فنکشن:

def format_conversation(conversation: dict, max_len: int = 256) -> dict:
    """
    Convert a conversation dict into token IDs + labels for SFT.

    Format: <|system|>...<|user|>...<|assistant|>...
    Labels: -100 for system/user tokens (masked), actual IDs for assistant tokens.
    """
    input_ids = [BOS_ID]
    labels = [IGNORE_INDEX]

    for turn in conversation["conversations"]:
        role = turn["role"]
        content = turn["content"]

        content_ids = tokenizer.encode(content).ids
        if content_ids and content_ids[0] == BOS_ID:
            content_ids = content_ids[1:]
        if content_ids and content_ids[-1] == EOS_ID:
            content_ids = content_ids[:-1]

        if role == "system":
            role_ids = [SYSTEM_ID] + content_ids + [SEP_ID]
            role_labels = [IGNORE_INDEX] * len(role_ids)
        elif role == "user":
            role_ids = [USER_ID] + content_ids + [SEP_ID]
            role_labels = [IGNORE_INDEX] * len(role_ids)
        elif role == "assistant":
            role_ids = [ASSISTANT_ID] + content_ids + [EOS_ID]
            role_labels = [IGNORE_INDEX] + content_ids + [EOS_ID]

        input_ids.extend(role_ids)
        labels.extend(role_labels)

    # Truncate and pad to max_len
    input_ids = input_ids[:max_len]
    labels = labels[:max_len]
    pad_len = max_len - len(input_ids)
    input_ids = input_ids + [PAD_ID] * pad_len
    labels = labels + [IGNORE_INDEX] * pad_len

    return {"input_ids": input_ids, "labels": labels}

حصہ 3: تصدیق

n_loss_tokens = sum(1 for l in test_formatted['labels'] if l != IGNORE_INDEX)
print(f"  Tokens with loss: {n_loss_tokens} / 256")

یہ اس بات کی تصدیق کرتا ہے کہ ٹوکن کا صرف ایک چھوٹا سا حصہ (اسسٹنٹ اسپیچ + EOS) نقصان میں حصہ ڈالتا ہے۔ ایک عام مثال کے طور پر، آپ کو کچھ اس طرح نظر آ سکتا ہے: Tokens with loss: 18 / 256اس کا مطلب ہے کہ صرف ~7% سیکوینسز گریڈینٹ اپ ڈیٹس چلاتے ہیں۔ باقی (سسٹم پرامپٹس، صارف کے سوالات، خصوصی ٹوکن، پیڈنگ) مندرجہ ذیل طور پر نقاب پوش ہیں: -100.

یہ SFT کو بہت موثر بناتا ہے۔ 100% سیکھنے کے سگنل اسسٹنٹ کے اصل جواب کی پیشین گوئی کرنے اور یہ جاننے سے آتے ہیں کہ کب رکنا ہے ()۔ یہ کارکردگی خاص طور پر اہم ہے جب صرف 79 تربیتی مثالیں ہوں۔

فارم کا خلاصہ

عنصر مقصد
no_padding() ٹوکن پلیسمنٹ کو دستی طور پر کنٹرول کریں۔
خصوصی ٹوکن ID عین مطابق مقامات پر چیٹ ڈھانچہ مارکر داخل کریں۔
IGNORE_INDEX = -100 کھوئی ہوئی پوزیشنوں کو چھوڑنے کے لیے PyTorch کا بلٹ ان میکانزم
سسٹم/یوزر لیبل → -100 اس سے مت سیکھو (صرف سیاق و سباق)
ثانوی لیبل → اصلی ID جانیں کہ کیسے جوابات پیدا کیے جائیں + کب روکنا ہے۔
256 تک کاٹ دیا گیا۔ ماڈل کی سیاق و سباق کی کھڑکی سے میچ کریں۔
-100 لیبل کے ساتھ پیڈنگ آلودگی کے نقصانات کے بغیر بیچ چھانٹنا

SFT ڈیٹاسیٹ اور ڈیٹا لوڈر

class SFTDataset(Dataset):
    def __init__(self, conversations: list, max_len: int = 256):
        self.examples = []
        for conv in conversations:
            formatted = format_conversation(conv, max_len)
            self.examples.append(formatted)

    def __len__(self):
        return len(self.examples)

    def __getitem__(self, idx):
        return (
            torch.tensor(self.examples[idx]['input_ids'], dtype=torch.long),
            torch.tensor(self.examples[idx]['labels'], dtype=torch.long),
        )

یہ تمام 79 فارمیٹ شدہ گفتگو کو PyTorch ڈیٹاسیٹ میں سمیٹتا ہے۔ شروع کرنے پر، تمام بات چیت کو پہلے سے فارمیٹ کیا جاتا ہے اس کا استعمال کرتے ہوئے: format_conversation() اور نتیجہ محفوظ کریں۔ جب ڈیٹا لوڈر کسی آئٹم کی درخواست کرتا ہے۔ idxیہ واپس آتا ہے (input_ids, labels) ٹینسر کے ساتھ۔

ڈیٹا لوڈر:

sft_loader = DataLoader(sft_dataset, batch_size=4, shuffle=True)
  • batch_size=4: صرف 79 مثالیں ہیں، لہذا یہ ایک چھوٹا سا بیچ ہے۔ بڑے بیچوں کو فی زمانہ کم گریڈینٹ اپ ڈیٹس کی ضرورت ہوتی ہے۔

  • shuffle=True: ہم ہر دور کے لیے ترتیب کو بے ترتیب بناتے ہیں تاکہ ماڈل کو ایک مقررہ مثال کی ترتیب یاد نہ رہے۔

پہلے سے تربیت یافتہ ماڈل لوڈ کریں۔

model = UrduGPT(config).to(device)
checkpoint = torch.load("best_model.pt", map_location=device)
state_dict = checkpoint['model_state_dict']

# Name mapping (Colab → local)
name_mapping = {
    'token_emb.weight': 'token_embedding.weight',
    'pos_emb.weight': 'position_embedding.weight',
    'ln_f.weight': 'ln_final.weight',
    'ln_f.bias': 'ln_final.bias',
    'head.weight': 'lm_head.weight',
}

اس کے بعد ایک نیا اردو جی پی ٹی ماڈل تیار کیا جاتا ہے اور مرحلہ 3 سے پہلے سے تربیت یافتہ وزن کے ساتھ لوڈ کیا جاتا ہے۔

آپ شاید سوچ رہے ہوں گے کہ آپ کو نام کی نقشہ سازی کی ضرورت کیوں ہے۔ ماڈل کو Google Colab میں قدرے مختلف متغیر ناموں کا استعمال کرتے ہوئے تربیت دی گئی تھی، جیسے token_emb بڑا token_embedding)۔ میپنگ آپ کے مقامی کوڈ میں Colab کے نام کے کنونشنز کو کنونشنز میں ترجمہ کرتی ہے۔ strict=False کو load_state_dict لوڈنگ ممکن ہے یہاں تک کہ اگر کچھ چابیاں بالکل مماثل نہ ہوں۔

اس کے علاوہ، کیوں پری ٹریننگ کے ساتھ شروع کریں؟ ٹھیک ہے، SFT پری ٹریننگ پر بنایا گیا ہے۔ ماڈل اردو گرامر، الفاظ اور حقائق کو پہلے سے جانتا ہے۔ SFT گفتگو کے فارمیٹس سکھاتا ہے۔ بے ترتیب وزن کے ساتھ شروع کرنے کے لیے بہت زیادہ ڈیٹا اور تربیت کی ضرورت ہوتی ہے۔

SFT ٹریننگ لوپ

مکمل SFT ٹریننگ لوپ مندرجہ ذیل ہے:

SFT_LR = 2e-5
SFT_EPOCHS = 50
optimizer = torch.optim.AdamW(model.parameters(), lr=SFT_LR, weight_decay=0.01)

sft_history = {'loss': []}
best_loss = float('inf')

for epoch in range(SFT_EPOCHS):
    model.train()
    epoch_loss = 0
    n_batches = 0

    for input_ids, labels in sft_loader:
        input_ids = input_ids.to(device)
        labels = labels.to(device)

        outputs = model(input_ids)
        logits = outputs['logits']

        shift_logits = logits[:, :-1, :].contiguous()
        shift_labels = labels[:, 1:].contiguous()

        loss = F.cross_entropy(
            shift_logits.view(-1, shift_logits.size(-1)),
            shift_labels.view(-1),
            ignore_index=IGNORE_INDEX,
        )

        optimizer.zero_grad(set_to_none=True)
        loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
        optimizer.step()

        epoch_loss += loss.item()
        n_batches += 1

    avg_loss = epoch_loss / n_batches
    sft_history['loss'].append(avg_loss)

    if avg_loss < best_loss:
        best_loss = avg_loss
        torch.save({
            'model_state_dict': model.state_dict(),
            'config': config.__dict__,
            'epoch': epoch + 1,
            'loss': avg_loss,
        }, "sft_model.pt")

    if (epoch + 1) % 10 == 0 or epoch == 0:
        print(f"Epoch {epoch+1}/{SFT_EPOCHS} | Loss: {avg_loss:.4f}")

print(f"SFT complete! Best loss: {best_loss:.4f}")

یہاں یہ ہے کہ یہ ہائپرپیرامیٹر پری ٹریننگ سے مختلف کیوں ہیں:

پیرامیٹر پیشگی تربیت ایس ایف ٹی یہ مختلف کیوں ہے؟
سیکھنے کی شرح 3e-4 2e-5 کم LR تباہ کن بھولنے سے روکتا ہے۔ اہم اپ ڈیٹ آپ نے پری ٹریننگ کے دوران سیکھا ہوا اردو علم ختم کر دے گا۔
اوقات 3 50 لاکھوں ٹوکنز کے مقابلے میں صرف 79 مثالیں ہیں۔ ماڈل کو گفتگو کے نمونے سیکھنے میں بہت سے گزرنے لگتے ہیں۔
وزن میں کمی 0.1 0.01 چونکہ ہم چاہتے ہیں کہ ماڈل ان مخصوص مثالوں کو قریب سے فٹ کرے، اس لیے کم ریگولرائزیشن کی ضرورت ہے۔
LR شیڈول کوسائن وارم اپ لامتناہی چھوٹے ڈیٹا کو ٹھیک کرنے کے لیے آسان اور موثر۔

تربیت کے مراحل درج ذیل ہیں (فی بیچ):

# Forward pass with no targets; we compute loss manually
outputs = model(input_ids)
logits = outputs['logits']

# Shift for next-token prediction
shift_logits = logits[:, :-1, :].contiguous()    # Predictions at positions 0..254
shift_labels = labels[:, 1:].contiguous()         # Targets at positions 1..255

# Loss with masking
loss = F.cross_entropy(
    shift_logits.view(-1, shift_logits.size(-1)),
    shift_labels.view(-1),
    ignore_index=IGNORE_INDEX,  # Skip -100 positions
)

پری ٹریننگ سے بنیادی فرق یہ ہے کہ پری ٹریننگ میں ہدف براہ راست پہنچایا جاتا تھا۔ model(input_ids, targets) ہم نے ہر ٹوکن کے لیے اندرونی طور پر نقصان کا حساب لگایا ہے۔ یہاں آپ دستی طور پر نقصان کا حساب لگا سکتے ہیں اور استعمال کر سکتے ہیں: ignore_index=-100 غیر معاون مقامات کو ماسک کرنے کے لیے۔

شفٹ: logits[:, :-1] اور labels[:, 1:] درج ذیل ٹوکن پیشین گوئی کو لاگو کریں: مقام پر ماڈل کی پیشین گوئیاں i اس کا موازنہ مقام پر موجود اصل ٹوکن سے کیا جاتا ہے۔ i+1.

ریورس پاس + اپ ڈیٹ:

optimizer.zero_grad(set_to_none=True)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()

یہ پری ٹریننگ جیسا ہی ہے۔ عدم استحکام کو روکنے کے لیے گریڈیئنٹس → بیک پروپیگیشن → کلپ → اپ ڈیٹ پیرامیٹرز کو صاف کریں۔ 1.0 پر گراڈینٹ کلپنگ یہاں خاص طور پر اہم ہے کیونکہ ماڈل ٹھیک ہے اور کچھ گراڈینٹ چھوٹے ڈیٹا پر بڑے ہو سکتے ہیں۔

چوکی:

if avg_loss < best_loss:
    torch.save({'model_state_dict': model.state_dict(), ...}, "sft_model.pt")

جب بھی آپ کے تربیتی نقصان میں بہتری آئے تو بچت کریں۔ پری ٹریننگ کے برعکس، کوئی الگ توثیق سیٹ نہیں ہے (79 مثالیں تقسیم کرنے کے لیے بہت کم ہیں)، اس لیے ہم تربیت کے نقصان کو چیک کرتے ہیں۔

چیٹ کی خصوصیات: اندازہ

مکمل چیٹ کی خصوصیات میں شامل ہیں:

def chat(model, tokenizer, user_message: str, system_prompt: str = None,
         max_tokens: int = 100, temperature: float = 0.7) -> str:
    """Generate a chat response."""
    model.eval()

    if system_prompt is None:
        system_prompt = SYSTEM_PROMPT

    # Build the prompt
    prompt_ids = [BOS_ID, SYSTEM_ID]

    sys_ids = tokenizer.encode(system_prompt).ids
    if sys_ids and sys_ids[0] == BOS_ID: sys_ids = sys_ids[1:]
    if sys_ids and sys_ids[-1] == EOS_ID: sys_ids = sys_ids[:-1]
    prompt_ids.extend(sys_ids)
    prompt_ids.append(SEP_ID)

    prompt_ids.append(USER_ID)
    user_ids = tokenizer.encode(user_message).ids
    if user_ids and user_ids[0] == BOS_ID: user_ids = user_ids[1:]
    if user_ids and user_ids[-1] == EOS_ID: user_ids = user_ids[:-1]
    prompt_ids.extend(user_ids)
    prompt_ids.append(SEP_ID)

    prompt_ids.append(ASSISTANT_ID)

    # Generate
    input_tensor = torch.tensor([prompt_ids], dtype=torch.long).to(device)
    with torch.no_grad():
        output_ids = model.generate(
            input_tensor,
            max_new_tokens=max_tokens,
            temperature=temperature,
            top_k=50,
            top_p=0.9,
            eos_token_id=EOS_ID,
        )

    # Decode only the generated part
    generated_ids = output_ids[0][len(prompt_ids):].tolist()
    if EOS_ID in generated_ids:
        generated_ids = generated_ids[:generated_ids.index(EOS_ID)]

    return tokenizer.decode(generated_ids)

یہاں مرحلہ وار تجزیہ ہے:

1. پرامپٹ مکمل کریں:

prompt_ids = [BOS_ID, SYSTEM_ID]
prompt_ids.extend(sys_ids)          # System prompt content
prompt_ids.append(SEP_ID)
prompt_ids.append(USER_ID)
prompt_ids.extend(user_ids)          # User message content
prompt_ids.append(SEP_ID)
prompt_ids.append(ASSISTANT_ID)      # "Now respond..."

یہ بالکل وہی شکل تشکیل دیتا ہے جو ماڈل نے SFT ٹریننگ کے دوران دیکھا تھا۔

<|system|>آپ ایک مددگار...<|user|>پاکستان کا دارالحکومت؟<|assistant|>

ماڈل لگ رہا ہے۔ <|assistant|> اور میں جانتا ہوں کہ SFT کے دوران مجھے "اب ایک جواب پیدا کرنے کی ضرورت ہے"۔ <|assistant|> یہ وہی ہے جو اسے پیدا کرنا ہے.

2. خود بخود پیدا کریں:

with torch.no_grad():
    output_ids = model.generate(
        input_tensor,
        max_new_tokens=max_tokens,
        temperature=temperature,
        top_k=50,
        top_p=0.9,
        eos_token_id=EOS_ID,
    )
  • torch.no_grad(): انفرنس کے لیے گریڈیئنٹس کی ضرورت نہیں ہے، میموری اور رفتار کو بچانا۔

  • temperature=0.7: مسلسل، لیکن قدرے تیز تقسیم، نہ کہ روبوٹک آؤٹ پٹ۔

  • top_k=50: کم امکان شور سے بچنے کے لیے ہم صرف ٹاپ 50 ٹوکنز سے نمونہ لیتے ہیں۔

  • top_p=0.9: نیوکلئس سیمپلنگ، جو متحرک طور پر 0.9 یا اس سے زیادہ کے مجموعی امکان کے ساتھ ٹوکن کے سب سے چھوٹے سیٹ کو منتخب کرتا ہے۔

  • eos_token_id: جنریشن رک جاتی ہے اگر: پیدا کیا جاتا ہے

3. نکالنا اور ضابطہ کشائی کرنا:

generated_ids = output_ids[0][len(prompt_ids):].tolist()    # Only the new tokens
if EOS_ID in generated_ids:
    generated_ids = generated_ids[:generated_ids.index(EOS_ID)]  # Trim at EOS
return tokenizer.decode(generated_ids)

پرامپٹ کو کاٹ دیں (میں دوبارہ سسٹم پرامپٹس اور صارف کے پیغامات واپس نہیں کرنا چاہتا) ٹوکن آئی ڈی کو دوبارہ اردو ٹیکسٹ میں ڈی کوڈ کریں۔

5. تقسیم

اس وقت، آپ کا اپنا ایل ایل ایم ہے۔ یہ ایک بہت بڑا سنگ میل ہے۔ لیکن اب بھی ایک کلاسک مسئلہ ہے۔ "یہ میرے کمپیوٹر پر کام کرتا ہے۔"

اپنے ماڈل کو دوسروں کے لیے دستیاب کرنے کے لیے، آپ کو اسے تقسیم کرنے اور صارفین کو اس کے ساتھ تعامل کرنے کے لیے ایک انٹرفیس فراہم کرنے کی ضرورت ہے۔

تعیناتی کے اختیارات کی تلاش کے دوران، میں نے Gradio کو دریافت کیا، جو مشین لرننگ ماڈلز اور ایپلیکیشنز کو تعینات کرنے کے لیے ایک سادہ اور صاف انٹرفیس فراہم کرتا ہے۔ Gradio کم سے کم سیٹ اپ کے ساتھ مفت ہوسٹنگ فراہم کرتے ہوئے، Hugging Face Spaces کے ساتھ براہ راست ضم ہوتا ہے۔

گریڈیو ویب انٹرفیس (app.py)

کہ app.py فائلیں ہر چیز کو جوڑ دیتی ہیں۔ ٹوکنائزر اور ماڈل لوڈ کریں۔ chat() فنکشن کو انجام دیتا ہے اور Gradio UI لانچ کرتا ہے۔ ماڈل لوڈنگ اور chat() منطق وہی ہے جس کا احاطہ SFT سیکشن میں کیا گیا ہے، لہذا یہاں صرف Gradio کے مخصوص حصے دکھائے گئے ہیں۔

import gradio as gr

def respond(message, history):
    if not message.strip():
        return "براہ کرم کچھ لکھیں۔"
    return chat(message)

demo = gr.ChatInterface(
    fn=respond,
    title="🇵🇰 اردو LLM چیٹ بوٹ",
    description="""
    ### ایک چھوٹا اردو زبان ماڈل جو شروع سے تیار کیا گیا ہے
    **A small Urdu language model built from scratch (~23M parameters)**
    """,
    examples=[
        "السلام علیکم",
        "پاکستان کا دارالحکومت کیا ہے؟",
        "لاہور کے بارے میں بتائیں۔",
        "بریانی کیسے بنتی ہے؟",
        "کرکٹ کیسے کھیلی جاتی ہے؟",
        "چاند کیسے چمکتا ہے؟",
        "رمضان کیا ہے؟",
        "علامہ اقبال کون تھے؟",
        "خوش کیسے رہیں؟",
        "آپ کون ہیں؟",
    ],
    theme=gr.themes.Soft(),
)

if __name__ == "__main__":
    demo.launch()
  • respond() لیب chat() خالی ان پٹ گارڈ کا استعمال کرتے ہوئے Gradio کے دستخط سے میل کھاتا ہے۔ ChatInterface میں اس کا منتظر ہوں۔

  • gr.ChatInterface یہ میسج ہسٹری، ان پٹ ونڈو، سینڈ بٹن وغیرہ کے ساتھ ایک ریڈی میڈ چیٹ UI فراہم کرتا ہے۔

  • examples یہ ایک پہلے سے آباد پیغام ہے جسے صارف آزمانے کے لیے کلک کر سکتے ہیں۔

  • theme=gr.themes.Soft() یہ ایک صاف اور جدید بصری تھیم پیش کرتا ہے۔

میمو: گلے لگانا چہرے کی جگہ چلائیں۔ app.py چونکہ یہ ایک اسٹینڈ اسکرپٹ ہے، مکمل app.py ذخیرہ ہر چیز کو ایک فائل میں ان لائن کرتا ہے: ماڈل کنفیگریشن، پورا مترجم فن تعمیر، اور ماڈل لوڈنگ۔ gc.collect() میموری کی اصلاح کے لیے chat() خصوصیات اور اوپر گریڈیو انٹرفیس۔

ہم اسے پہلے ہی پری ٹریننگ اور SFT سیکشنز میں کور کر چکے ہیں، اس لیے میں یہاں یہ سب نہیں دہراؤں گا۔

مقامی طور پر چلائیں:

python app.py
# Opens at http://127.0.0.1:7860

تعیناتی کے اختیارات

Hugging Face Spaces Gradio ایپس کے لیے مفت CPU ہوسٹنگ پیش کرتا ہے۔

کیا اپ لوڈ کرنا ہے:

urdu-llm-chat/
├── app.py                          # Gradio web interface
├── requirements.txt                # torch, tokenizers, gradio
├── README.md                       # Space metadata (sdk: gradio)
├── model/
│   ├── __init__.py
│   ├── config.py
│   ├── transformer.py
│   └── checkpoints/sft_model.pt    # ~90MB trained model weights
└── tokenizer/
    └── urdu_tokenizer/
        └── urdu_bpe_tokenizer.json

یہ کیسے کام کرتا ہے:

  1. Huggingface.co پر ایک مفت اکاؤنٹ بنائیں

  2. ایک نئی جگہ بنائیں (SDK: Gradio، Hardware: CPU Basic)

  3. Git کے ذریعے فائلوں کو پش کریں۔ git clone https://huggingface.co/spaces/USERNAME/urdu-llm-chat

  4. پروجیکٹ فائلوں کو کاپی کریں اور کلون شدہ ذخیرہ میں دھکیلیں۔

  5. ہگنگ فیس خود بخود اس کی انحصار کو انسٹال اور چلاتا ہے۔ app.py

  6. آپ کا ماڈل یہاں شائع ہوا ہے: https://huggingface.co/spaces/USERNAME/urdu-llm-chat

کیوں CPUs اچھے ہیں: ہمارے ماڈل میں صرف 23M پیرامیٹرز (~90MB) ہیں۔ CPU پر اندازہ لگانے میں 1 سیکنڈ سے بھی کم وقت لگتا ہے۔ ڈیلیوری کے لیے GPU کی ضرورت نہیں ہے۔

آپشن B: مقامی طور پر چلائیں۔

cd your-project-directory
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python app.py

یہ اگلی بار کھلتا ہے۔ http://127.0.0.1:7860. یہ Python 3.9 یا اس سے اوپر والے کسی بھی کمپیوٹر پر کام کرتا ہے۔

اختیار C: ٹرمینل چیٹ (کوئی UI نہیں)

ایک ہلکا پھلکا متبادل جس میں کوئی گریڈیو انحصار نہیں، صرف ٹرمینل ان پٹ/آؤٹ پٹ۔ ماڈل لوڈ کریں اور ایک انٹرایکٹو لوپ داخل کریں۔

"""
Standalone Chat Inference Script for Urdu LLM

Usage:
    python inference/chat.py
"""

import sys
import torch
from pathlib import Path
from tokenizers import Tokenizer

# Add project root to path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT))

from model.config import UrduLLMConfig
from model.transformer import UrduGPT


def load_model(checkpoint_path: str, device: str = None):
    """Load the fine-tuned model."""
    if device is None:
        if torch.cuda.is_available():
            device = "cuda"
        elif torch.backends.mps.is_available():
            device = "mps"
        else:
            device = "cpu"

    device = torch.device(device)

    config = UrduLLMConfig()
    model = UrduGPT(config).to(device)

    checkpoint = torch.load(checkpoint_path, map_location=device)
    model.load_state_dict(checkpoint['model_state_dict'])
    model.eval()

    return model, config, device


def chat_response(model, tokenizer, config, device, user_message,
                  system_prompt="آپ ایک مددگار اردو اسسٹنٹ ہیں۔",
                  max_tokens=100, temperature=0.7):
    """Generate a chat response."""
    BOS_ID = tokenizer.token_to_id("")
    EOS_ID = tokenizer.token_to_id("")
    SEP_ID = tokenizer.token_to_id("")
    USER_ID = tokenizer.token_to_id("<|user|>")
    ASSISTANT_ID = tokenizer.token_to_id("<|assistant|>")
    SYSTEM_ID = tokenizer.token_to_id("<|system|>")

    # Build prompt
    prompt_ids = [BOS_ID, SYSTEM_ID]

    sys_ids = tokenizer.encode(system_prompt).ids
    if sys_ids and sys_ids[0] == BOS_ID: sys_ids = sys_ids[1:]
    if sys_ids and sys_ids[-1] == EOS_ID: sys_ids = sys_ids[:-1]
    prompt_ids.extend(sys_ids)
    prompt_ids.append(SEP_ID)

    prompt_ids.append(USER_ID)
    user_ids = tokenizer.encode(user_message).ids
    if user_ids and user_ids[0] == BOS_ID: user_ids = user_ids[1:]
    if user_ids and user_ids[-1] == EOS_ID: user_ids = user_ids[:-1]
    prompt_ids.extend(user_ids)
    prompt_ids.append(SEP_ID)

    prompt_ids.append(ASSISTANT_ID)

    # Generate
    input_tensor = torch.tensor([prompt_ids], dtype=torch.long).to(device)
    output_ids = model.generate(
        input_tensor,
        max_new_tokens=max_tokens,
        temperature=temperature,
        top_k=50,
        top_p=0.9,
        eos_token_id=EOS_ID,
    )

    generated_ids = output_ids[0][len(prompt_ids):].tolist()
    if EOS_ID in generated_ids:
        generated_ids = generated_ids[:generated_ids.index(EOS_ID)]

    return tokenizer.decode(generated_ids)


def main():
    print("=" * 60)
    print("🇵🇰  اردو LLM چیٹ بوٹ  🇵🇰")
    print("    Urdu LLM ChatBot")
    print("=" * 60)

    # Load model
    tokenizer_path = PROJECT_ROOT / "tokenizer" / "urdu_tokenizer" / "urdu_bpe_tokenizer.json"

    # Try SFT model first, fall back to pre-trained
    sft_path = PROJECT_ROOT / "model" / "checkpoints" / "sft_model.pt"
    pretrained_path = PROJECT_ROOT / "model" / "checkpoints" / "best_model.pt"

    if sft_path.exists():
        checkpoint_path = sft_path
        print("Loading SFT (conversational) model...")
    elif pretrained_path.exists():
        checkpoint_path = pretrained_path
        print("Loading pre-trained model (not fine-tuned for chat)...")
    else:
        print("❌ No model checkpoint found!")
        print("   Run notebooks 03 and 04 first to train the model.")
        sys.exit(1)

    model, config, device = load_model(str(checkpoint_path))
    tokenizer = Tokenizer.from_file(str(tokenizer_path))

    print(f"Model loaded on {device}")
    print("\nType your message in Urdu. Type 'quit' to exit.\n")
    print("-" * 60)

    while True:
        try:
            user_input = input("\n👤 آپ: ").strip()
        except (EOFError, KeyboardInterrupt):
            print("\nخدا حافظ! 👋")
            break

        if user_input.lower() in ['quit', 'exit', 'q']:
            print("خدا حافظ! 👋")
            break

        if not user_input:
            continue

        response = chat_response(model, tokenizer, config, device, user_input)
        print(f"🤖 بوٹ: {response}")


if __name__ == "__main__":
    main()

اسے استعمال کرتے ہوئے چلائیں:

python inference/chat.py
👤 آپ: السلام علیکم
🤖 بوٹ: وعلیکم السلام! میں آپ کی کیا مدد کر سکتا ہوں؟

مکمل پائپ لائن کا خلاصہ

قدم وضاحت
مرحلہ 1 خام اردو متن → صاف کارپس
مرحلہ 2 کارپس → BPE ٹوکنائزر (32K ذخیرہ الفاظ)
مرحلہ 3 ٹوکنائزڈ حصہ → GPT پری ٹریننگ (23M پیرامیٹرز، اگلی ٹوکن پیشن گوئی)
مرحلہ 4 بات چیت → نقصان کی ماسکنگ کے ساتھ SFT → چیٹ ماڈل
تقسیم app.py + ماڈل وزن → گلے لگائیں چہرے کی جگہ/مقامی

آپ تعینات کردہ ماڈل کو یہاں دیکھ سکتے ہیں: https://huggingface.co/spaces/Wisamul/urdu_llm

نتیجہ

نتائج کافی دلچسپ تھے۔ چونکہ ہمارے پاس ایک چھوٹا سا تربیتی نمونہ تھا، اس لیے فریب کاری کا امکان کافی زیادہ تھا، اور آپ فریب کے تفریحی حصے کی تعریف کریں گے، خاص طور پر اگر آپ اردو سمجھ سکتے ہوں۔ لیکن مجموعی طور پر اس نے اچھا کام کیا۔

یہاں تعینات ماڈل کے ساتھ کچھ نمونے کے تعاملات ہیں:

👤 User: السلام علیکم
🤖 Assistant: وعلیکم السلام! میں آپ کی کیا مدد کر سکتا ہوں؟

👤 User: پاکستان کا دارالحکومت کیا ہے؟
🤖 Assistant: پاکستان کا دارالحکومت اسلام آباد ہے۔ یہ شہر 1960 کی دہائی میں تعمیر کیا گیا تھا۔

👤 User: تعلیم کی اہمیت کیا ہے؟
🤖 Assistant: تعلیم انسانی ترقی کی بنیاد ہے۔ یہ نہ صرف علم حاصل کرنے کا ذریعہ ہے بلکہ معاشرے کی تعمیر میں اہم کردار ادا کرتی ہے۔

ایسے سوالات کے لیے جو تربیتی ڈیٹا کے قریب ہیں، ماڈل درست اور روانی سے جواب دیتا ہے۔ تقسیم سے باہر کے سوالات کے لیے، وہ ٹکڑوں کو فریب دینے یا دہرانے کا رجحان رکھتے ہیں۔ چھوٹے SFT ڈیٹاسیٹ (79 مثالیں) اور ماڈل سائز (23M پیرامیٹرز) کے پیش نظر یہ متوقع ہے۔

نتیجہ

اس سب کا مقصد ایل ایل ایم بنانے کے عمل میں شامل تمام اقدامات پر بحث اور وضاحت کرنا تھا۔ لاگت، تربیت، ڈیٹا اکٹھا کرنے وغیرہ جیسے عوامل کے ساتھ، ہم امید کرتے ہیں کہ اس سے یہ واضح ہو جائے گا کہ ہر کوئی اپنا LLM کیوں نہیں بناتا۔ ساتھ ہی، مجھے امید ہے کہ اس عمل سے گزرنے سے آپ کو یہ سمجھنے میں مدد ملے گی کہ آپ نے پہلے کیا کیا ہے اور آپ نے یہاں کیا حاصل کیا ہے۔

ہم خام اردو متن سے ایک تعینات چیٹ بوٹ پر چلے گئے۔ ڈیٹا کی صفائی، بی پی ای ٹوکنائزیشن، جی پی ٹی اسٹائل کنورٹر پری ٹریننگ، نقصان کی ماسکنگ کے ساتھ فائن ٹیوننگ نگرانی، اور آخر میں گریڈیو ویب انٹرفیس تھا۔

ماڈل چھوٹے ہیں اور ڈیٹا سیٹ چھوٹے ہیں، لیکن یہاں کے تمام تصورات (توجہ، اگلی ٹوکن پیشن گوئی، SFT، چیٹ فارمیٹ) کا مقصد GPT-4 اور Llama جیسے پروڈکشن LLMs کو بہت بڑے پیمانے پر سپورٹ کرنا ہے۔

اگر آپ اسے بہتر بنانا چاہتے ہیں، تو اگلے اقدامات جن پر سب سے زیادہ اثر پڑے گا وہ ہیں:

  1. مزید SFT ڈیٹا (79 کی بجائے ہزاروں مثالیں)؛

  2. بڑے ماڈل (100 ملین سے زیادہ پیرامیٹرز)

  3. RLHF/DPO الائنمنٹ۔

لیکن اس پیمانے پر بھی، اب ہمارے پاس پوری ایل ایل ایم پائپ لائن کی ٹھوس سمجھ ہے۔

Scroll to Top