{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# الشبكات التوليدية\n", "\n", "توفر الشبكات العصبية المتكررة (RNNs) ونسخها ذات الخلايا البوابية مثل خلايا الذاكرة طويلة المدى (LSTMs) ووحدات التكرار البوابية (GRUs) آلية لنمذجة اللغة، أي أنها تستطيع تعلم ترتيب الكلمات وتقديم توقعات للكلمة التالية في تسلسل معين. هذا يسمح لنا باستخدام الشبكات العصبية المتكررة في **المهام التوليدية**، مثل توليد النصوص العادية، الترجمة الآلية، وحتى وصف الصور.\n", "\n", "في بنية الشبكات العصبية المتكررة التي ناقشناها في الوحدة السابقة، كانت كل وحدة متكررة تنتج الحالة المخفية التالية كمخرج. ومع ذلك، يمكننا أيضًا إضافة مخرج آخر لكل وحدة متكررة، مما يسمح لنا بإنتاج **تسلسل** (يكون طوله مساويًا لطول التسلسل الأصلي). علاوة على ذلك، يمكننا استخدام وحدات متكررة لا تقبل مدخلًا في كل خطوة، بل تأخذ فقط متجه حالة أولية، ثم تنتج تسلسلًا من المخرجات.\n", "\n", "في هذا الدفتر، سنركز على نماذج توليدية بسيطة تساعدنا في توليد النصوص. وللتبسيط، دعونا نبني **شبكة على مستوى الحروف**، والتي تولد النص حرفًا بحرف. أثناء التدريب، نحتاج إلى أخذ نص معين وتقسيمه إلى تسلسلات من الحروف.\n" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Loading dataset...\n", "Building vocab...\n" ] } ], "source": [ "import torch\n", "import torchtext\n", "import numpy as np\n", "from torchnlp import *\n", "train_dataset,test_dataset,classes,vocab = load_dataset()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## بناء مفردات الحروف\n", "\n", "لإنشاء شبكة توليد على مستوى الحروف، نحتاج إلى تقسيم النص إلى حروف فردية بدلاً من كلمات. يمكن تحقيق ذلك من خلال تعريف محلل نصي مختلف:\n" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Vocabulary size = 82\n", "Encoding of 'a' is 1\n", "Character with code 13 is c\n" ] } ], "source": [ "def char_tokenizer(words):\n", " return list(words) #[word for word in words]\n", "\n", "counter = collections.Counter()\n", "for (label, line) in train_dataset:\n", " counter.update(char_tokenizer(line))\n", "vocab = torchtext.vocab.vocab(counter)\n", "\n", "vocab_size = len(vocab)\n", "print(f\"Vocabulary size = {vocab_size}\")\n", "print(f\"Encoding of 'a' is {vocab.get_stoi()['a']}\")\n", "print(f\"Character with code 13 is {vocab.get_itos()[13]}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "لنرى مثالاً عن كيفية ترميز النص من مجموعة البيانات الخاصة بنا:\n" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "tensor([ 0, 1, 2, 2, 3, 4, 5, 6, 3, 7, 8, 1, 9, 10, 3, 11, 2, 1,\n", " 12, 3, 7, 1, 13, 14, 3, 15, 16, 5, 17, 3, 5, 18, 8, 3, 7, 2,\n", " 1, 13, 14, 3, 19, 20, 8, 21, 5, 8, 9, 10, 22, 3, 20, 8, 21, 5,\n", " 8, 9, 10, 3, 23, 3, 4, 18, 17, 9, 5, 23, 10, 8, 2, 2, 8, 9,\n", " 10, 24, 3, 0, 1, 2, 2, 3, 4, 5, 9, 8, 8, 5, 25, 10, 3, 26,\n", " 12, 27, 16, 26, 2, 27, 16, 28, 29, 30, 1, 16, 26, 3, 17, 31, 3, 21,\n", " 2, 5, 9, 1, 23, 13, 32, 16, 27, 13, 10, 24, 3, 1, 9, 8, 3, 10,\n", " 8, 8, 27, 16, 28, 3, 28, 9, 8, 8, 16, 3, 1, 28, 1, 27, 16, 6])" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def enc(x):\n", " return torch.LongTensor(encode(x,voc=vocab,tokenizer=char_tokenizer))\n", "\n", "enc(train_dataset[0][1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## تدريب شبكة RNN التوليدية\n", "\n", "الطريقة التي سنقوم بها بتدريب شبكة RNN لتوليد النصوص هي كالتالي. في كل خطوة، سنأخذ سلسلة من الأحرف بطول `nchars`، ونطلب من الشبكة توليد الحرف التالي لكل حرف مدخل:\n", "\n", "![صورة توضح مثالًا على توليد شبكة RNN لكلمة \"HELLO\".](../../../../../translated_images/ar/rnn-generate.56c54afb52f9781d.webp)\n", "\n", "اعتمادًا على السيناريو الفعلي، قد نرغب أيضًا في تضمين بعض الأحرف الخاصة، مثل *نهاية السلسلة* ``. في حالتنا، نريد فقط تدريب الشبكة على توليد نصوص بلا نهاية، وبالتالي سنقوم بتثبيت حجم كل سلسلة ليكون مساويًا لعدد الرموز `nchars`. وبناءً على ذلك، كل مثال تدريبي سيتكون من `nchars` مدخلات و`nchars` مخرجات (وهي سلسلة المدخلات مزاحة بمقدار رمز واحد إلى اليسار). وستتكون الدفعة الصغيرة (minibatch) من عدة سلاسل من هذا النوع.\n", "\n", "الطريقة التي سنقوم بها بتوليد الدفعات الصغيرة هي أخذ كل نص إخباري بطول `l`، وتوليد جميع التوليفات الممكنة من المدخلات والمخرجات منه (سيكون هناك `l-nchars` من هذه التوليفات). ستشكل هذه التوليفات دفعة صغيرة واحدة، وسيكون حجم الدفعات الصغيرة مختلفًا في كل خطوة تدريب.\n" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(tensor([[ 0, 1, 2, ..., 28, 29, 30],\n", " [ 1, 2, 2, ..., 29, 30, 1],\n", " [ 2, 2, 3, ..., 30, 1, 16],\n", " ...,\n", " [20, 8, 21, ..., 1, 28, 1],\n", " [ 8, 21, 5, ..., 28, 1, 27],\n", " [21, 5, 8, ..., 1, 27, 16]]),\n", " tensor([[ 1, 2, 2, ..., 29, 30, 1],\n", " [ 2, 2, 3, ..., 30, 1, 16],\n", " [ 2, 3, 4, ..., 1, 16, 26],\n", " ...,\n", " [ 8, 21, 5, ..., 28, 1, 27],\n", " [21, 5, 8, ..., 1, 27, 16],\n", " [ 5, 8, 9, ..., 27, 16, 6]]))" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "nchars = 100\n", "\n", "def get_batch(s,nchars=nchars):\n", " ins = torch.zeros(len(s)-nchars,nchars,dtype=torch.long,device=device)\n", " outs = torch.zeros(len(s)-nchars,nchars,dtype=torch.long,device=device)\n", " for i in range(len(s)-nchars):\n", " ins[i] = enc(s[i:i+nchars])\n", " outs[i] = enc(s[i+1:i+nchars+1])\n", " return ins,outs\n", "\n", "get_batch(train_dataset[0][1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "الآن دعونا نعرّف شبكة التوليد. يمكن أن تستند إلى أي خلية متكررة ناقشناها في الوحدة السابقة (بسيطة، LSTM أو GRU). في مثالنا، سنستخدم LSTM.\n", "\n", "نظرًا لأن الشبكة تأخذ الأحرف كمدخلات، وحجم المفردات صغير جدًا، فلا نحتاج إلى طبقة تضمين، حيث يمكن إدخال الترميز الواحد مباشرة إلى خلية LSTM. ومع ذلك، لأننا نمرر أرقام الأحرف كمدخلات، نحتاج إلى ترميزها بطريقة الترميز الواحد قبل تمريرها إلى LSTM. يتم ذلك عن طريق استدعاء دالة `one_hot` أثناء تمرير `forward`. مشفر المخرجات سيكون طبقة خطية تقوم بتحويل الحالة المخفية إلى مخرجات مرمزة بطريقة الترميز الواحد.\n" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "class LSTMGenerator(torch.nn.Module):\n", " def __init__(self, vocab_size, hidden_dim):\n", " super().__init__()\n", " self.rnn = torch.nn.LSTM(vocab_size,hidden_dim,batch_first=True)\n", " self.fc = torch.nn.Linear(hidden_dim, vocab_size)\n", "\n", " def forward(self, x, s=None):\n", " x = torch.nn.functional.one_hot(x,vocab_size).to(torch.float32)\n", " x,s = self.rnn(x,s)\n", " return self.fc(x),s" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "أثناء التدريب، نريد أن نتمكن من أخذ عينات من النص المُنتج. لتحقيق ذلك، سنقوم بتعريف دالة `generate` التي ستنتج سلسلة نصية بطول `size`، تبدأ من السلسلة الأولية `start`.\n", "\n", "طريقة عملها كالتالي. أولاً، سنمرر السلسلة الأولية بالكامل عبر الشبكة، ونأخذ حالة الإخراج `s` والحرف المتوقع التالي `out`. بما أن `out` مشفر بطريقة one-hot، سنستخدم `argmax` للحصول على فهرس الحرف `nc` في المفردات، ثم نستخدم `itos` لتحديد الحرف الفعلي وإضافته إلى قائمة الأحرف الناتجة `chars`. يتم تكرار هذه العملية لإنتاج حرف واحد `size` مرة للحصول على العدد المطلوب من الأحرف.\n" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "def generate(net,size=100,start='today '):\n", " chars = list(start)\n", " out, s = net(enc(chars).view(1,-1).to(device))\n", " for i in range(size):\n", " nc = torch.argmax(out[0][-1])\n", " chars.append(vocab.get_itos()[nc])\n", " out, s = net(nc.view(1,-1),s)\n", " return ''.join(chars)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "الآن لنبدأ التدريب! حلقة التدريب تشبه إلى حد كبير جميع الأمثلة السابقة لدينا، ولكن بدلاً من دقة النموذج، نقوم بطباعة النص المُولد كل 1000 دورة.\n", "\n", "يجب الانتباه بشكل خاص للطريقة التي نحسب بها الخسارة. نحتاج إلى حساب الخسارة بناءً على الإخراج المشفر بطريقة \"one-hot\" `out`، والنص المتوقع `text_out`، وهو قائمة مؤشرات الأحرف. لحسن الحظ، وظيفة `cross_entropy` تتطلب الإخراج غير المُطبع للشبكة كأول وسيط، ورقم الفئة كالثاني، وهو بالضبط ما لدينا. كما أنها تقوم بعملية المتوسط التلقائي لحجم الدفعة المصغرة.\n", "\n", "نقوم أيضًا بتحديد التدريب بعدد معين من العينات `samples_to_train`، حتى لا ننتظر لفترة طويلة. نشجعك على التجربة ومحاولة التدريب لفترة أطول، ربما لعدة دورات (وفي هذه الحالة ستحتاج إلى إنشاء حلقة إضافية حول هذا الكود).\n" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Current loss = 4.398899078369141\n", "today sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr s\n", "Current loss = 2.161320447921753\n", "today and to the tor to to the tor to to the tor to to the tor to to the tor to to the tor to to the tor t\n", "Current loss = 1.6722588539123535\n", "today and the court to the could to the could to the could to the could to the could to the could to the c\n", "Current loss = 2.423795223236084\n", "today and a second to the conternation of the conternation of the conternation of the conternation of the \n", "Current loss = 1.702607274055481\n", "today and the company to the company to the company to the company to the company to the company to the co\n", "Current loss = 1.692358136177063\n", "today and the company to the company to the company to the company to the company to the company to the co\n", "Current loss = 1.9722288846969604\n", "today and the control the control the control the control the control the control the control the control \n", "Current loss = 1.8705692291259766\n", "today and the second to the second to the second to the second to the second to the second to the second t\n", "Current loss = 1.7626899480819702\n", "today and a security and a security and a security and a security and a security and a security and a secu\n", "Current loss = 1.5574463605880737\n", "today and the company and the company and the company and the company and the company and the company and \n", "Current loss = 1.5620026588439941\n", "today and the be that the be the be that the be the be that the be the be that the be the be that the be t\n" ] } ], "source": [ "net = LSTMGenerator(vocab_size,64).to(device)\n", "\n", "samples_to_train = 10000\n", "optimizer = torch.optim.Adam(net.parameters(),0.01)\n", "loss_fn = torch.nn.CrossEntropyLoss()\n", "net.train()\n", "for i,x in enumerate(train_dataset):\n", " # x[0] is class label, x[1] is text\n", " if len(x[1])-nchars<10:\n", " continue\n", " samples_to_train-=1\n", " if not samples_to_train: break\n", " text_in, text_out = get_batch(x[1])\n", " optimizer.zero_grad()\n", " out,s = net(text_in)\n", " loss = torch.nn.functional.cross_entropy(out.view(-1,vocab_size),text_out.flatten()) #cross_entropy(out,labels)\n", " loss.backward()\n", " optimizer.step()\n", " if i%1000==0:\n", " print(f\"Current loss = {loss.item()}\")\n", " print(generate(net))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "هذا المثال بالفعل يولد نصًا جيدًا إلى حد كبير، ولكن يمكن تحسينه بطرق عدة:\n", "\n", "* **تحسين توليد الدُفعات الصغيرة (minibatch)**. الطريقة التي قمنا بها بإعداد البيانات للتدريب كانت تعتمد على توليد دفعة صغيرة واحدة من عينة واحدة. هذه الطريقة ليست مثالية، لأن الدُفعات الصغيرة تكون بأحجام مختلفة، وبعضها قد لا يمكن توليده لأن النص أصغر من `nchars`. بالإضافة إلى ذلك، الدُفعات الصغيرة لا تستغل قدرة وحدة معالجة الرسومات (GPU) بشكل كافٍ. سيكون من الأفضل أخذ جزء كبير من النص من جميع العينات، ثم توليد جميع أزواج الإدخال والإخراج، خلطها، وتوليد دُفعات صغيرة ذات حجم متساوٍ.\n", "\n", "* **LSTM متعدد الطبقات**. من المنطقي تجربة 2 أو 3 طبقات من خلايا LSTM. كما ذكرنا في الوحدة السابقة، كل طبقة من LSTM تستخرج أنماطًا معينة من النص، وفي حالة مولد النصوص على مستوى الحروف يمكننا توقع أن تكون الطبقة الأدنى من LSTM مسؤولة عن استخراج المقاطع الصوتية، والطبقات الأعلى - عن الكلمات وتركيبات الكلمات. يمكن تنفيذ ذلك ببساطة عن طريق تمرير معلمة عدد الطبقات إلى منشئ LSTM.\n", "\n", "* قد ترغب أيضًا في تجربة **وحدات GRU** لمعرفة أيها يقدم أداءً أفضل، وكذلك تجربة **أحجام مختلفة للطبقات المخفية**. الطبقة المخفية الكبيرة جدًا قد تؤدي إلى الإفراط في التكيف (على سبيل المثال، الشبكة ستتعلم النص بشكل حرفي)، بينما الحجم الأصغر قد لا ينتج نتائج جيدة.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## توليد النص الناعم ودرجة الحرارة\n", "\n", "في التعريف السابق لـ `generate`، كنا دائمًا نأخذ الحرف ذو أعلى احتمال ليكون الحرف التالي في النص المُولد. أدى ذلك إلى أن النص غالبًا ما \"يدور\" بين نفس تسلسلات الأحرف مرارًا وتكرارًا، كما في هذا المثال:\n", "```\n", "today of the second the company and a second the company ...\n", "```\n", "\n", "ومع ذلك، إذا نظرنا إلى توزيع الاحتمالات للحرف التالي، قد نجد أن الفرق بين أعلى الاحتمالات ليس كبيرًا، على سبيل المثال، يمكن أن يكون احتمال أحد الأحرف 0.2، وآخر 0.19، وهكذا. على سبيل المثال، عند البحث عن الحرف التالي في التسلسل '*play*'، يمكن أن يكون الحرف التالي بنفس الاحتمال إما مسافة أو **e** (كما في كلمة *player*).\n", "\n", "هذا يقودنا إلى استنتاج أنه ليس من \"العادل\" دائمًا اختيار الحرف ذو الاحتمال الأعلى، لأن اختيار الحرف ذو الاحتمال الثاني قد يؤدي أيضًا إلى نص ذو معنى. من الأكثر حكمة أن نقوم بـ **أخذ عينات** من الأحرف بناءً على توزيع الاحتمالات الناتج عن الشبكة.\n", "\n", "يمكن تنفيذ هذه العملية باستخدام وظيفة `multinomial` التي تطبق ما يُعرف بـ **التوزيع متعدد الحدود**. يتم تعريف وظيفة تقوم بتطبيق هذا التوليد النصي **الناعم** أدناه:\n" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "--- Temperature = 0.3\n", "Today and a company and complete an all the land the restrational the as a security and has provers the pay to and a report and the computer in the stand has filities and working the law the stations for a company and with the company and the final the first company and refight of the state and and workin\n", "\n", "--- Temperature = 0.8\n", "Today he oniis its first to Aus bomblaties the marmation a to manan boogot that pirate assaid a relaid their that goverfin the the Cappets Ecrotional Assonia Cition targets it annight the w scyments Blamity #39;s TVeer Diercheg Reserals fran envyuil that of ster said access what succers of Dour-provelith\n", "\n", "--- Temperature = 1.0\n", "Today holy they a 11 will meda a toket subsuaties, engins for Chanos, they's has stainger past to opening orital his thempting new Nattona was al innerforder advan-than #36;s night year his religuled talitatian what the but with Wednesday to Justment will wemen of Mark CCC Camp as Timed Nae wome a leaders\n", "\n", "--- Temperature = 1.3\n", "Today gpone 2.5 fech atcusion poor cocles toparsdorM.cht Line Pamage put 43 his calt lowed to the book, that has authh-the silia rruch ailing to'ory andhes beutirsimi- Aefffive heading offil an auf eacklets is charged evis, Gunymy oy) Mony has it after-sloythyor loveId out filme, the Natabl -Najuntaxiggs \n", "\n", "--- Temperature = 1.8\n", "Today plary, P.slan chly\\401 mardregationly #39;t 8.1Mide) closes ,filtcon alfly playin roven!\\grea.-QFBEP: Iss onfarchQ/itilia CCf Zivesigntwasta orce.-Peul-aw.uicrin of fuglinfsut aftaningwo, MIEX awayew Aice Woiduar Corvagiugge oppo esig ThusBratourid canthly-RyI.co lagitems\\eexciaishes.conBabntusmor I\n", "\n" ] } ], "source": [ "def generate_soft(net,size=100,start='today ',temperature=1.0):\n", " chars = list(start)\n", " out, s = net(enc(chars).view(1,-1).to(device))\n", " for i in range(size):\n", " #nc = torch.argmax(out[0][-1])\n", " out_dist = out[0][-1].div(temperature).exp()\n", " nc = torch.multinomial(out_dist,1)[0]\n", " chars.append(vocab.get_itos()[nc])\n", " out, s = net(nc.view(1,-1),s)\n", " return ''.join(chars)\n", " \n", "for i in [0.3,0.8,1.0,1.3,1.8]:\n", " print(f\"--- Temperature = {i}\\n{generate_soft(net,size=300,start='Today ',temperature=i)}\\n\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "لقد قدمنا ​​معلمة إضافية تسمى **درجة الحرارة**، والتي تُستخدم للإشارة إلى مدى التزامنا بأعلى احتمال. إذا كانت درجة الحرارة 1.0، نقوم بأخذ عينات متعددة بشكل عادل، وعندما تقترب درجة الحرارة من اللانهاية - تصبح جميع الاحتمالات متساوية، ونختار الحرف التالي بشكل عشوائي. في المثال أدناه يمكننا ملاحظة أن النص يصبح بلا معنى عندما نزيد درجة الحرارة كثيرًا، ويشبه النص \"المتكرر\" الذي يتم إنشاؤه بصعوبة عندما تقترب من 0.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n---\n\n**إخلاء المسؤولية**: \nتم ترجمة هذا المستند باستخدام خدمة الترجمة بالذكاء الاصطناعي [Co-op Translator](https://github.com/Azure/co-op-translator). بينما نسعى لتحقيق الدقة، يرجى العلم أن الترجمات الآلية قد تحتوي على أخطاء أو معلومات غير دقيقة. يجب اعتبار المستند الأصلي بلغته الأصلية المصدر الرسمي. للحصول على معلومات حاسمة، يُوصى بالاستعانة بترجمة بشرية احترافية. نحن غير مسؤولين عن أي سوء فهم أو تفسيرات خاطئة تنشأ عن استخدام هذه الترجمة.\n" ] } ], "metadata": { "interpreter": { "hash": "16af2a8bbb083ea23e5e41c7f5787656b2ce26968575d8763f2c4b17f9cd711f" }, "kernelspec": { "display_name": "Python 3.8.12 ('py38')", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.12" }, "coopTranslator": { "original_hash": "7673cd150d96c74c6d6011460094efb4", "translation_date": "2025-08-28T04:08:49+00:00", "source_file": "lessons/5-NLP/17-GenerativeNetworks/GenerativePyTorch.ipynb", "language_code": "ar" } }, "nbformat": 4, "nbformat_minor": 4 }