01Overviewنظرة عامة
Assignment 1 is worth 100 points in two parts. Part A is regex practice on a large parallel corpus; Part B is the preprocessing pipeline that comes before any text classifier.
الواجب الأول عليه 100 درجة موزعة على جزأين. الجزء A تدريب على التعابير النمطية (regex) على مدوّنة نصية متوازية كبيرة، والجزء B هو خط المعالجة المسبقة (preprocessing pipeline) الذي يسبق أي مصنّف نصوص.
- Part Aالجزء A
- 35 points. Regular expressions with Python and UNIX tools on the UN corpus,
uncorpora_plain_20090831.tmx: 163 MB of UN General Assembly resolutions in a six-language TMX file.35 درجة. تعابير نمطية باستخدام Python وأدوات UNIX على مدوّنة الأمم المتحدة،uncorpora_plain_20090831.tmx: 163 MB من قرارات الجمعية العامة للأمم المتحدة في ملف TMX بست لغات. - Part Bالجزء B
- 65 points. Cleaning, normalizing and tokenizing
tweets_sentiment_analysis.csv, 16,363 tweets split 70/15/15 withrandom_state=42.65 درجة. تنظيف الملفtweets_sentiment_analysis.csvوتوحيد صيغته وتقطيعه إلى وحدات (tokenization)، وفيه 16,363 تغريدة مقسّمة بنسبة 70/15/15 معrandom_state=42. - Rule 1القاعدة 1
- Every answer must use Python regular expressions, not only string methods.كل إجابة يجب أن تستخدم التعابير النمطية في Python، لا دوال النصوص (string methods) وحدها.
- Rule 2القاعدة 2
- Every cell must show its output; code without output earns nothing.كل خلية يجب أن تعرض مخرجاتها، فالكود بلا مخرجات لا يحصل على أي درجة.
Each question below gives the approach, the code that produced the answer, and the result the notebook printed. A few side figures were computed separately on the same corpus and are not printed in the notebooks: the 3,668 types merged by lowercasing in Q2, the stricter Q2e count of 14,185, the second Q3 table, and the 24,783 letters-only count for a in Q4. The two executed notebooks are linked at the top of the page. The background is lecture 02: What is a word for tokens and types, Zipf and Heaps for the histogram, Regex basics and Regex in practice for the patterns, and Tokenization and BPE for the subword model.
كل سؤال أدناه يعرض طريقة الحل، والكود الذي أنتج الإجابة، والنتيجة التي طبعها الـ notebook. بعض الأرقام الجانبية حُسبت بشكل منفصل على المدوّنة نفسها ولا تظهر مطبوعة في الـ notebooks: الأنواع (types) التي يدمجها تحويل الأحرف إلى صغيرة في Q2 وعددها 3,668، والعدد الأكثر تشددًا في Q2e وهو 14,185، والجدول الثاني في Q3، وعدد a في Q4 عند اعتبار الأحرف فقط وهو 24,783. الـ notebooks المنفّذان مربوطان أعلى الصفحة. الخلفية النظرية في المحاضرة 02: ما هي الكلمة للوحدات (tokens) والأنواع، وقانونا Zipf وHeaps للمدرّج التكراري، وأساسيات regex وregex في التطبيق للأنماط، والتقطيع وBPE لنموذج الوحدات الجزئية (subword).
02Part A: regex and UNIX toolsالجزء A: التعابير النمطية (regex) وأدوات UNIX
Part A runs regular expressions over the UN corpus, uncorpora_plain_20090831.tmx, a 163 MB TMX file of UN General Assembly resolutions aligned across six languages. Every count uses Python's re module, and each cell prints its answer.
الجزء A يطبّق التعابير النمطية على مدوّنة الأمم المتحدة uncorpora_plain_20090831.tmx، وهي ملف TMX بحجم 163 MB يضم قرارات الجمعية العامة للأمم المتحدة مصفوفة بالتوازي عبر ست لغات. كل عملية عدّ تستخدم الوحدة re في Python، وكل خلية تطبع إجابتها.
Q1: lines about human rightsس1: الأسطر التي تتناول حقوق الإنسان
The question asks for lines, not matches, so the file is read line by line and each line counts once no matter how many times the phrase appears in it. re.IGNORECASE handles Human Rights, human rights and HUMAN RIGHTS in one pattern. The count runs over the raw TMX file, so every line of the file is a candidate, markup included.
السؤال يطلب عدد الأسطر لا عدد المطابقات، لذلك يُقرأ الملف سطرًا سطرًا ويُحسب كل سطر مرة واحدة مهما تكررت فيه العبارة. الخيار re.IGNORECASE يغطي Human Rights وhuman rights وHUMAN RIGHTS بنمط واحد. العدّ يجري على ملف TMX الخام، فكل سطر في الملف مرشّح، بما في ذلك أسطر الوسوم (markup).
import re
pattern = re.compile(r'human rights', re.IGNORECASE)
with open('uncorpora_plain_20090831.tmx', encoding='utf-8') as f:
count = sum(1 for line in f if pattern.search(line))
print(f'{count:,} lines of text.')- Regex
human rights- Flagsالخيارات
re.IGNORECASE- Answerالإجابة
- 5,664 lines of text.5,664 سطرًا من النص.
Q2: the English corpusس2: المدوّنة الإنجليزية
Every translation unit in the TMX file holds one <tuv> per language, and the text sits in a <seg> element on the line after the xml:lang attribute. The capture group grabs only what is between the seg tags, \s* absorbs the newline between the two tags, and the lazy .*? stops at the first closing tag instead of running to the end of the file.
كل وحدة ترجمة في ملف TMX تحتوي عنصر <tuv> واحدًا لكل لغة، والنص موجود داخل عنصر <seg> في السطر الذي يلي الخاصية xml:lang. مجموعة الالتقاط (capture group) تأخذ ما بين وسمَي seg فقط، و\s* يبتلع السطر الجديد بين الوسمين، والنمط الكسول (lazy) .*? يتوقف عند أول وسم إغلاق بدل أن يمتد حتى نهاية الملف.
with open('uncorpora_plain_20090831.tmx', encoding='utf-8') as f:
raw = f.read()
english = re.findall(r'<tuv xml:lang="EN">\s*<seg>(.*?)</seg>', raw)
with open('uncorpus.eng.txt', 'w', encoding='utf-8') as f:
f.write('\n'.join(english) + '\n')
print(f'{len(english):,} English segments written')wc -c uncorpus.eng.txtThe script finds 72,339 English segments and wc -c reports 18009005 bytes, which is exactly the required value. The hint's route, grep -a -A1 'xml:lang="EN"' followed by stripping the tags, produces the same bytes. The -a flag makes grep treat the file as text even if it detects bytes that look binary, and -A1 prints the line after each match, which is where the segment lives.
يجد السكربت 72,339 مقطعًا إنجليزيًا، ويُظهر wc -c عدد 18009005 بايت، وهي القيمة المطلوبة تمامًا. طريقة التلميح، أي grep -a -A1 'xml:lang="EN"' ثم حذف الوسوم، تنتج البايتات نفسها. الخيار -a يجعل grep يعامل الملف كنص حتى لو رصد بايتات تبدو ثنائية، و-A1 يطبع السطر الذي يلي كل مطابقة، وهو السطر الذي يوجد فيه المقطع.
For the counts, a word is any run of non-space characters, so the tokenizer is simply \S+. Punctuation stays attached to its word, which is why rights, and rights are two different types here. That definition comes straight from the assignment and is the whitespace policy from What is a word.
في العدّ، الكلمة هي أي سلسلة متصلة من المحارف غير الفراغية، لذلك أداة التقطيع هي ببساطة \S+. علامات الترقيم تبقى ملتصقة بكلمتها، ولهذا تُعدّ rights, وrights نوعين مختلفين هنا. هذا التعريف مأخوذ مباشرة من الواجب، وهو سياسة التقطيع بالفراغات من درس ما هي الكلمة.
with open('uncorpus.eng.txt', encoding='utf-8') as f:
text = f.read()
tokens = re.findall(r'\S+', text)
types = set(tokens)
types_lower = set(re.findall(r'\S+', text.lower()))
digit_tokens = re.findall(r'(?<!\S)\d+(?!\S)', text)
mixed_tokens = re.findall(r'(?<!\S)(?=\S*\d)(?=\S*[^\d\s])[^\sA-Za-z]+(?!\S)', text)| Itemالبند | RegexRegex | Countالعدد |
|---|---|---|
| a. Tokensأ. الوحدات (tokens) | \S+\S+ | 2,685,5382,685,538 |
| b. Typesب. الأنواع (types) | set of \S+set of \S+ | 37,03237,032 |
| c. Types ignoring caseج. الأنواع مع تجاهل حالة الأحرف | set of \S+ on lowercased textset of \S+ on lowercased text | 33,36433,364 |
| d. Digits onlyد. أرقام فقط | (?<!\S)\d+(?!\S)(?<!\S)\d+(?!\S) | 35,85735,857 |
| e. Digits plus other non-lettersهـ. أرقام مع رموز أخرى غير الحروف | (?<!\S)(?=\S*\d)(?=\S*[^\d\s])[^\sA-Za-z]+(?!\S)(?<!\S)(?=\S*\d)(?=\S*[^\d\s])[^\sA-Za-z]+(?!\S) | 71,33771,337 |
Lowercasing merges 3,668 types, pairs such as The and the. The two digit questions need lookarounds because \b is the wrong boundary: \b\d+\b would find the 55 inside 55/100. A whole token is bounded by whitespace, which (?<!\S) and (?!\S) express without consuming anything. Lookarounds are covered in Regex in practice.
تحويل الأحرف إلى صغيرة يدمج 3,668 نوعًا، أزواجًا مثل The وthe. سؤالا الأرقام يحتاجان إلى التحقق المسبق واللاحق (lookarounds) لأن \b حدّ غير مناسب هنا: النمط \b\d+\b سيجد 55 داخل 55/100. الوحدة الكاملة يحدّها فراغ، وهذا ما يعبّر عنه (?<!\S) و(?!\S) دون استهلاك أي محرف. التحقق المسبق واللاحق مشروح في regex في التطبيق.
Worked exampleمثال محلول
Reading the Q2e regex piece by pieceقراءة نمط Q2e جزءًا جزءًا
Start at a token boundaryابدأ عند حدّ الوحدة
(?<!\S)is a negative lookbehind: the character before this position is not a non-space character, so the match starts at the beginning of the text or right after whitespace.(?<!\S)تحقق سلبي من الخلف (negative lookbehind): المحرف الذي يسبق هذا الموضع ليس محرفًا غير فراغي، فتبدأ المطابقة عند بداية النص أو بعد فراغ مباشرة.Require a digit somewhere in the tokenاشترط وجود رقم في مكان ما من الوحدة
(?=\S*\d)looks ahead through the token without consuming it and succeeds only if a digit appears before the next space.(?=\S*\d)ينظر إلى الأمام عبر الوحدة دون أن يستهلكها، ولا ينجح إلا إذا ظهر رقم قبل الفراغ التالي.Require one other non-space characterاشترط محرفًا آخر غير فراغي
(?=\S*[^\d\s])looks ahead again for a character that is neither a digit nor whitespace. This rules out plain numbers, which already belong to Q2d.(?=\S*[^\d\s])ينظر إلى الأمام مرة أخرى بحثًا عن محرف ليس رقمًا ولا فراغًا. هذا يستبعد الأعداد المجردة، فهي تنتمي أصلًا إلى Q2d.Consume the token, letters forbiddenاستهلك الوحدة مع منع الحروف
[^\sA-Za-z]+eats the token itself, and every character must be neither whitespace nor a letter. Combined with the second lookahead, the other character must be a non-alphabetic symbol such as a comma, slash, period or bracket.[^\sA-Za-z]+يستهلك الوحدة نفسها، ويجب ألا يكون أي محرف فيها فراغًا ولا حرفًا. ومع التحقق المسبق الثاني، يجب أن يكون المحرف الآخر رمزًا غير أبجدي مثل الفاصلة أو الشرطة المائلة أو النقطة أو القوس.End at a token boundaryانتهِ عند حدّ الوحدة
(?!\S)checks that the next character is whitespace or the end of the text, so the whole token matched and not just a prefix of something like 55/100a.(?!\S)يتحقق من أن المحرف التالي فراغ أو نهاية النص، فتكون المطابقة للوحدة كاملة لا لبادئة من شيء مثل 55/100a.Resultالنتيجة
71,337 tokens, for example 55/100, 2000,, 94),, 1,, 1999, and 1.71,337 وحدة، مثل 55/100 و2000, و94), و1, و1999, و1.
Q3: war and peaceس3: الحرب والسلام (war و peace)
Q3 says to ignore case and every non-alphabetic character. The cleanest reading is to lowercase the text and take each run of letters as a word with [a-z]+, so anything that is not a letter separates words and post-war becomes post and war. Questions a to h then run over the set of unique words, and i and j run over the full word stream.
يطلب Q3 تجاهل حالة الأحرف وكل محرف غير أبجدي. أوضح قراءة لذلك هي تحويل النص إلى أحرف صغيرة واعتبار كل سلسلة متصلة من الحروف كلمة باستخدام [a-z]+، فكل ما ليس حرفًا يفصل بين الكلمات، وتصبح post-war كلمتين هما post وwar. بعد ذلك تُجرى الأسئلة من a إلى h على مجموعة الكلمات الفريدة، ويُجرى السؤالان i وj على تدفق الكلمات الكامل.
words = re.findall(r'[a-z]+', text.lower())
vocab = set(words)
def count_matching(pattern):
return sum(1 for w in vocab if re.search(pattern, w))
contains_war = count_matching(r'war')
starts_war = count_matching(r'^war')
ends_war = count_matching(r'war$')
middle_war = count_matching(r'^(?!war)[a-z]*war[a-z]*(?<!war)$')
all_words = ' '.join(words)
war_sub = len(re.findall(r'war', all_words))
war_full = len(re.findall(r'\bwar\b', all_words))| Questionالسؤال | warwar | peacepeace |
|---|---|---|
| Containsتحتوي | 4343 | 99 |
| Starts withتبدأ بـ | 2020 | 99 |
| Ends withتنتهي بـ | 11 | 11 |
| Inside onlyفي الداخل فقط | 2323 | 00 |
| Substring occurrencesمرات الظهور كجزء من كلمة | 3,4303,430 | 5,1445,144 |
| Full word occurrencesمرات الظهور ككلمة كاملة | 516516 | 3,5363,536 |
The inside-only pattern ^(?!war)[a-z]*war[a-z]*(?<!war)$ anchors both ends, refuses a word that starts with war through the lookahead, and refuses one that ends with war through the lookbehind. Its 23 matches include afterwards, award, aware, awareness and forward. The only word ending in war is war itself, and the only word ending in peace is peace. Anchors and word boundaries are the topic of Regex basics.
نمط الداخل فقط ^(?!war)[a-z]*war[a-z]*(?<!war)$ يثبّت الطرفين، ويرفض الكلمة التي تبدأ بـ war عبر التحقق المسبق، ويرفض التي تنتهي بها عبر التحقق اللاحق. من مطابقاته الـ 23: afterwards وaward وaware وawareness وforward. الكلمة الوحيدة التي تنتهي بـ war هي war نفسها، والوحيدة التي تنتهي بـ peace هي peace. المثبّتات (anchors) وحدود الكلمات موضوع درس أساسيات regex.
Peace wins both comparisons. As a substring it appears 5,144 times against 3,430, and as a full word 3,536 times against 516. The gap between substring and full word counts for war comes from words like towards, forward and awareness, which contain war without meaning it.
تفوز peace في المقارنتين. كجزء من كلمة تظهر 5,144 مرة مقابل 3,430، وككلمة كاملة 3,536 مرة مقابل 516. الفرق بين عدد الظهور كجزء وعدد الظهور ككلمة كاملة في حالة war سببه كلمات مثل towards وforward وawareness، التي تحتوي war دون أن تعنيها.
| Questionالسؤال | warwar | peacepeace |
|---|---|---|
| Containsتحتوي | 5858 | 1111 |
| Starts withتبدأ بـ | 2323 | 1111 |
| Ends withتنتهي بـ | 44 | 11 |
| Inside onlyفي الداخل فقط | 3232 | 00 |
| Full word occurrencesمرات الظهور ككلمة كاملة | 447447 | 3,4273,427 |
Q4: the word histogramس4: المدرج التكراري للكلمات
The histogram is a Counter over the same \S+ tokens as Q2, with case and punctuation kept, and most_common() returns it sorted by frequency.
المدرّج التكراري هو Counter على وحدات \S+ نفسها المستخدمة في Q2، مع الإبقاء على حالة الأحرف وعلامات الترقيم، وmost_common() يعيده مرتبًا حسب التكرار.
from collections import Counter
freq = Counter(re.findall(r'\S+', text))
ranked = freq.most_common()
hapax = sum(1 for _, n in ranked if n == 1)
top_10 = ranked[:10]
bottom_10 = ranked[-10:]| Rankالرتبة | Wordالكلمة | Countالعدد | Rank x countالرتبة × العدد |
|---|---|---|---|
| 11 | thethe | 267,940267,940 | 267,940267,940 |
| 22 | ofof | 175,497175,497 | 350,994350,994 |
| 33 | andand | 136,607136,607 | 409,821409,821 |
| 44 | toto | 99,54599,545 | 398,180398,180 |
| 55 | inin | 66,80266,802 | 334,010334,010 |
| 66 | onon | 35,91035,910 | 215,460215,460 |
| 77 | forfor | 32,32732,327 | 226,289226,289 |
| 88 | thatthat | 22,53422,534 | 180,272180,272 |
| 99 | itsits | 21,18121,181 | 190,629190,629 |
| 1010 | withwith | 20,34920,349 | 203,490203,490 |
Ten function words cover about a third of all 2,685,538 tokens. The last column is the Zipf check from Zipf and Heaps: if frequency falls as one over rank, rank times count stays roughly constant. Here it stays between about 180,000 and 410,000 across the top ten, the same order of magnitude, which is roughly what Zipf's law predicts for the head of the curve.
عشر كلمات وظيفية (function words) تغطي نحو ثلث الوحدات البالغ عددها 2,685,538. العمود الأخير هو اختبار Zipf من درس قانونا Zipf وHeaps: إذا كان التكرار يتناقص بنسبة واحد على الرتبة، فإن حاصل ضرب الرتبة في العدد يبقى ثابتًا تقريبًا. هنا يبقى بين نحو 180,000 و410,000 عبر الكلمات العشر الأولى، أي في رتبة المقدار نفسها، وهذا تقريبًا ما يتوقعه قانون Zipf لرأس المنحنى.
The bottom of the histogram is flat. Of the 37,032 distinct words, 13,527 appear exactly once, about 36.5% of the vocabulary. These hapax legomena all tie at frequency 1, so any ten of them is a valid bottom 10. The notebook prints the last ten of the ranking, which include tennis, player,, Maria, Sharapova, and post-Chernobyl.
ذيل المدرّج التكراري مسطّح. من بين 37,032 كلمة مختلفة، تظهر 13,527 كلمة مرة واحدة فقط، أي نحو 36.5% من المفردات. هذه الكلمات الفريدة (hapax legomena) كلها متساوية عند التكرار 1، لذلك أي عشر منها تصلح إجابة صحيحة لأقل 10 كلمات. يطبع الـ notebook آخر عشر كلمات في الترتيب، ومنها tennis وplayer, وMaria وSharapova, وpost-Chernobyl.
Q5: breaking the Caesar cipherس5: كسر شيفرة قيصر (Caesar cipher)
doc.crypt.txt is uppercase ciphertext that begins BPM CVQDMZAIT LMKTIZIBQWV WN PCUIV ZQOPBA. A Caesar cipher shifts every letter by the same amount, so the letter frequencies of the plaintext survive, only relabeled. The English corpus from Q2 supplies the reference frequencies.
doc.crypt.txt نص مشفّر بأحرف كبيرة يبدأ بـ BPM CVQDMZAIT LMKTIZIBQWV WN PCUIV ZQOPBA. شيفرة قيصر (Caesar cipher) تزيح كل حرف بالمقدار نفسه، فتبقى ترددات الحروف في النص الأصلي كما هي، مع تغيير التسميات فقط. المدوّنة الإنجليزية من Q2 توفّر الترددات المرجعية.
| Textالنص | Letters by frequencyالحروف حسب التكرار |
|---|---|
| Corpus (plain)المدوّنة (النص الأصلي) | E T N I O AE T N I O A |
| Ciphertextالنص المشفّر | M B V W I QM B V W I Q |
Worked exampleمثال محلول
Breaking the shift with letter frequenciesكسر الإزاحة باستخدام ترددات الحروف
Guess from the top letterخمّن من الحرف الأكثر تكرارًا
E is the most common letter in the corpus and M is the most common in the ciphertext, so M most likely encrypts E. With A as 0, M is 12 and E is 4.
E هو الحرف الأكثر تكرارًا في المدوّنة وM هو الأكثر تكرارًا في النص المشفّر، لذلك على الأرجح أن M تشفير لـ E. باعتبار A تساوي 0، فإن M تساوي 12 وE تساوي 4.
Check every shift with chi-squaredافحص كل إزاحة باختبار مربع كاي
A single letter can mislead on a short text, so the notebook scores all 26 shifts. For shift and each plaintext letter , it compares , the ciphertext count of shifted by , with , the count expected from the corpus frequency of scaled to the ciphertext length.
الحرف الواحد قد يضلّل في نص قصير، لذلك يقيّم الـ notebook جميع الإزاحات الـ 26. لكل إزاحة ولكل حرف من النص الأصلي، يقارن ، أي عدد مرات ظهور مُزاحًا بمقدار في النص المشفّر، مع ، أي العدد المتوقع من تردد في المدوّنة بعد تحجيمه إلى طول النص المشفّر.
Read the scoresاقرأ الدرجات
Shift 8 scores 230.5. The runner-up, shift 21, scores 26,875.0, more than a hundred times worse, so the top-letter guess is confirmed with a wide margin.
الإزاحة 8 تحصل على 230.5. والتي تليها، الإزاحة 21، تحصل على 26,875.0، أي أسوأ بأكثر من مئة مرة، فيتأكد تخمين الحرف الأكثر تكرارًا بفارق كبير.
Decodeفكّ التشفير
Shift every letter back by 8 with
re.suband a function replacement. B becomes T, P becomes H, and M becomes E, so the first word BPM reads THE.أزِح كل حرف إلى الخلف بمقدار 8 باستخدام
re.subمع دالة استبدال. يصبح B هو T، وP هو H، وM هو E، فتُقرأ الكلمة الأولى BPM على أنها THE.Decodedالنص بعد فكّ التشفير
THE UNIVERSAL DECLARATION OF HUMAN RIGHTS, then PREAMBLE, then WHEREAS RECOGNITION OF THE INHERENT DIGNITY ...THE UNIVERSAL DECLARATION OF HUMAN RIGHTS، ثم PREAMBLE، ثم WHEREAS RECOGNITION OF THE INHERENT DIGNITY ...
english_freq = Counter(re.findall(r'[a-z]', text.lower()))
with open('doc.crypt.txt', encoding='utf-8') as f:
cipher = f.read().upper()
cipher_freq = Counter(re.findall(r'[A-Z]', cipher))
total_eng = sum(english_freq.values())
total_cipher = sum(cipher_freq.values())
def chi_squared(s):
score = 0
for letter, n in english_freq.items():
expected = total_cipher * n / total_eng
shifted = chr((ord(letter.upper()) - ord('A') + s) % 26 + ord('A'))
observed = cipher_freq[shifted]
score += (observed - expected) ** 2 / expected
return score
scores = {s: chi_squared(s) for s in range(26)}
best = min(scores, key=scores.get)
decoded = re.sub(
r'[A-Z]',
lambda m: chr((ord(m.group()) - ord('A') - best) % 26 + ord('A')),
cipher,
)- The decoded file has a blank line between paragraphs, so the first ten lines are taken after skipping lines that contain no text.في الملف بعد فكّ التشفير سطر فارغ بين الفقرات، لذلك تُؤخذ الأسطر العشرة الأولى بعد تخطّي الأسطر التي لا تحتوي نصًا.
- Only letters are shifted;
[A-Z]leaves spaces, commas and digits untouched, which is how a Caesar cipher treats them.تُزاح الحروف فقط، فالنمط[A-Z]يترك الفراغات والفواصل والأرقام دون تغيير، وهكذا تعاملها شيفرة قيصر.
03Part B: preprocessing tweetsالجزء B: المعالجة المسبقة للتغريدات
Part B prepares tweets_sentiment_analysis.csv for a sentiment classifier. The file has 16,363 tweets with a positive or negative label, no missing values and no duplicate rows. It is split 70/15/15 into train, validation and test with two calls to train_test_split, both with random_state=42 so the split is reproducible.
الجزء B يجهّز الملف tweets_sentiment_analysis.csv لمصنّف مشاعر. يحتوي الملف 16,363 تغريدة بتصنيف إيجابي أو سلبي، دون قيم مفقودة ودون صفوف مكررة. يُقسَّم بنسبة 70/15/15 إلى تدريب وتحقق واختبار باستدعاءين لـ train_test_split، كلاهما مع random_state=42 حتى يمكن إعادة إنتاج التقسيم.
data = pd.read_csv("tweets_sentiment_analysis.csv")
data = data.drop_duplicates()
train_data, temp_data = train_test_split(data, test_size=0.3, random_state=42)
val_data, test_data = train_test_split(temp_data, test_size=0.5, random_state=42)Cleaning with regexالتنظيف باستخدام regex
clean_text is a chain of re.sub calls, and the order is the whole design. Anything that needs punctuation to be recognized, a URL, a mention, an entity, has to go before punctuation is removed. Once [^a-z\s] runs, https://t.co/x would already be the words https, t, co and x.
clean_text سلسلة من استدعاءات re.sub، والترتيب هو جوهر التصميم كله. كل ما يحتاج إلى علامات الترقيم كي يُتعرّف عليه، من رابط أو إشارة أو كيان، يجب حذفه قبل حذف علامات الترقيم. فبعد تنفيذ [^a-z\s] يكون https://t.co/x قد تحوّل بالفعل إلى الكلمات https وt وco وx.
def clean_text(text):
text = re.sub(r'https?://\S+|www\.\S+', ' ', text)
text = re.sub(r'[@#]\w+', ' ', text)
text = re.sub(r'&\w+;', ' ', text)
text = text.lower()
text = re.sub(r"['`’]", '', text)
text = re.sub(r'[^a-z\s]', ' ', text)
text = re.sub(r'([a-z])\1{2,}', r'\1', text)
text = re.sub(r'\s+', ' ', text).strip()
return text
def remove_empty_rows(df):
df = df[df['cleaned_tweet'].str.strip() != '']
return df.reset_index(drop=True)What each substitution doesما يفعله كل استبدال
- https?://\S+|www\.\S+
- URLs go first, while their slashes and dots still mark them as URLs.الروابط (URLs) تُحذف أولًا، ما دامت الشرطات المائلة والنقاط فيها تدل على أنها روابط.
- [@#]\w+
- Mentions and hashtags, the whole handle or tag with its marker.الإشارات (mentions) والوسوم (hashtags)، المعرّف أو الوسم كاملًا مع علامته.
- &\w+;
- HTML entities such as & and " left over from scraping.كيانات HTML مثل & و" المتبقية من جمع البيانات (scraping).
- text.lower()
- Lowercase before the letter class, so the class only needs a to z.التحويل إلى أحرف صغيرة قبل فئة الحروف، فلا تحتاج الفئة إلا إلى a حتى z.
- ['`’]
- Apostrophes, backticks and curly quotes are deleted, not replaced, so I'm becomes im and won't becomes wont instead of splitting in two.الفواصل العليا وعلامات backtick وعلامات الاقتباس المنحنية تُحذف ولا تُستبدل، فتصبح I'm هي im وwon't هي wont بدل أن تنقسم إلى جزأين.
- [^a-z\s]
- Every remaining digit, symbol and punctuation mark becomes a space.كل ما تبقى من أرقام ورموز وعلامات ترقيم يصبح فراغًا.
- ([a-z])\1{2,}
- A letter followed by two or more copies of itself collapses to one, so looooove becomes love while good, with only a double o, survives.الحرف المتبوع بنسختين أو أكثر من نفسه يُختصر إلى حرف واحد، فتصبح looooove هي love، بينما تبقى good كما هي لأن فيها o مكررة مرتين فقط.
- \s+ and strip()
- Runs of whitespace become one space, with none at the ends.سلاسل الفراغات المتتالية تصبح فراغًا واحدًا، دون فراغات في الطرفين.
| Originalالأصل | Cleanedبعد التنظيف |
|---|---|
| ha! I know..I`m very ashamed.ha! I know..I`m very ashamed. | ha i know im very ashamedha i know im very ashamed |
| hahahaha! i laughed my **** off just now. thankshahahaha! i laughed my **** off just now. thanks | hahahaha i laughed my off just now thankshahahaha i laughed my off just now thanks |
| Gahh ! This weather sucksss !Gahh ! This weather sucksss ! | gahh this weather sucksgahh this weather sucks |
A tweet made only of a link or a mention cleans down to the empty string, and an empty row adds nothing but a zero-length sequence later, so the rows that are empty after cleaning are dropped from each split with remove_empty_rows.
التغريدة المكوّنة من رابط أو إشارة فقط تتحول بعد التنظيف إلى نص فارغ، والصف الفارغ لا يضيف شيئًا سوى تسلسل طوله صفر لاحقًا، لذلك تُحذف الصفوف الفارغة بعد التنظيف من كل قسم باستخدام remove_empty_rows.
Stopwords, stems and lemmasكلمات التوقف (stopwords) والجذوع (stems) والأصول المعجمية (lemmas)
stopwords.words('english') returns 198 words, stored in a set so each lookup is constant time. The stemmer is PorterStemmer, a fixed cascade of suffix rules, and the lemmatizer is WordNetLemmatizer, which looks words up in WordNet and returns a dictionary form. All three functions split on spaces, transform each word, and join the result.
stopwords.words('english') تعيد 198 كلمة، تُخزّن في مجموعة (set) كي يكون كل بحث في زمن ثابت. أداة التجذيع (stemmer) هي PorterStemmer، وهي سلسلة ثابتة من قواعد اللواحق، وأداة الإرجاع إلى الأصل المعجمي هي WordNetLemmatizer، التي تبحث عن الكلمة في WordNet وتعيد صيغتها المعجمية. الدوال الثلاث تقسّم على الفراغات، وتحوّل كل كلمة، ثم تضم النتيجة.
stop_words = set(stopwords.words('english'))
stemmer = PorterStemmer()
lemmatizer = WordNetLemmatizer()
def remove_stopwords(sentence):
return ' '.join(word for word in sentence.split() if word not in stop_words)
def stem_text(sentence):
return ' '.join(stemmer.stem(word) for word in sentence.split())
def lemmatize_text(sentence):
return ' '.join(lemmatizer.lemmatize(word) for word in sentence.split())| Stepالخطوة | Textالنص |
|---|---|
| Cleanedبعد التنظيف | people in my house do not know how to close doorspeople in my house do not know how to close doors |
| Without stopwordsبدون الكلمات الشائعة | people house know close doorspeople house know close doors |
| Porter stemmedبعد تجذيع Porter | peopl in my hous do not know how to close doorpeopl in my hous do not know how to close door |
The stemmer produces peopl and hous, which are not words; a stem only has to be the same for every form of a word. The lemmatizer, called without a part of speech, treats each word as a noun, so it turns doors into door and leaves most other words alone.
أداة التجذيع تنتج peopl وhous، وهما ليستا كلمتين، فالجذع لا يلزمه إلا أن يكون واحدًا لكل صيغ الكلمة. أما أداة الإرجاع إلى الأصل المعجمي، فعند استدعائها دون تحديد نوع الكلمة (part of speech) تعامل كل كلمة على أنها اسم، فتحوّل doors إلى door وتترك معظم الكلمات الأخرى دون تغيير.
Word, character and BPE tokenizersمقطّعات (tokenizers) الكلمة والحرف و BPE
Each Keras Tokenizer is fit on the training split only and then applied to all three splits. Fitting on validation or test would leak their words into the vocabulary and make the OOV rate meaningless. With oov_token='<UNK>', any unseen word maps to that token instead of disappearing from the sequence.
كل Tokenizer من Keras يُدرَّب (fit) على قسم التدريب فقط ثم يُطبَّق على الأقسام الثلاثة. تدريبه على قسم التحقق أو الاختبار يسرّب كلماتهما إلى المفردات ويجعل نسبة الكلمات خارج المفردات (OOV) بلا معنى. ومع oov_token='<UNK>' تُربط أي كلمة لم تُرَ من قبل بهذه الوحدة بدل أن تختفي من التسلسل.
tokenizer_word = Tokenizer(oov_token='<UNK>')
tokenizer_word.fit_on_texts(train_data['cleaned_tweet'])
train_sequences_word = tokenizer_word.texts_to_sequences(train_data['cleaned_tweet'])
val_sequences_word = tokenizer_word.texts_to_sequences(val_data['cleaned_tweet'])
test_sequences_word = tokenizer_word.texts_to_sequences(test_data['cleaned_tweet'])
vocab_word_size = len(tokenizer_word.word_index)
tokenizer_char = Tokenizer(char_level=True, oov_token='<UNK>')
tokenizer_char.fit_on_texts(train_data['cleaned_tweet'])The subword model is SentencePiece in BPE mode, trained on the provided tweets_train.txt with a fixed vocabulary of 5,000 pieces. sp.encode accepts a list of strings and returns one id list per tweet. The merge procedure itself is in Tokenization and BPE.
نموذج الوحدات الجزئية هو SentencePiece في وضع BPE، مدرَّب على الملف المرفق tweets_train.txt بمفردات ثابتة من 5,000 قطعة. الدالة sp.encode تقبل قائمة من النصوص وتعيد قائمة معرّفات لكل تغريدة. أما خوارزمية الدمج نفسها ففي درس التقطيع وBPE.
spm.SentencePieceTrainer.train(
input='tweets_train.txt',
model_prefix='tweets_bpe',
model_type='bpe',
vocab_size=5000,
)
sp = spm.SentencePieceProcessor(model_file='tweets_bpe.model')
train_sequences_bpe = sp.encode(train_data['cleaned_tweet'].tolist())
vocab_size = sp.get_piece_size()| Tokenizerأداة التقطيع | Vocabularyالمفردات | Whyالسبب |
|---|---|---|
| Word, cleanedكلمات، بعد التنظيف | 13,46413,464 | Baselineخط الأساس |
| Word, without stopwordsكلمات، بدون الكلمات الشائعة | 13,32413,324 | 140 fewer; only stopwords that occur in the data disappearأقل بـ 140، فلا تختفي إلا الكلمات الشائعة الموجودة فعلًا في البيانات |
| Word, stemmedكلمات، بعد التجذيع | 10,57410,574 | Smallest word vocabulary; many forms share one stemأصغر مفردات على مستوى الكلمة، إذ تشترك صيغ كثيرة في جذع واحد |
| Word, lemmatizedكلمات، بعد الإرجاع إلى الأصل المعجمي | 12,31312,313 | Gentler than stemming, keeps dictionary wordsألطف من التجذيع، ويحافظ على كلمات معجمية |
| Characterمحارف | 2828 | 26 letters, the space and <UNK>26 حرفًا والفراغ و<UNK> |
| BPE (SentencePiece)BPE (SentencePiece) | 5,0005,000 | Fixed by vocab_sizeيحدده vocab_size |
| Tokenizerأداة التقطيع | Trainالتدريب | Validationالتحقق | Testالاختبار |
|---|---|---|---|
| Wordكلمات | 0%0% | 5.37%5.37% | 5.31%5.31% |
| Characterمحارف | 0%0% | 0%0% | 0%0% |
| BPEBPE | 0.18%0.18% | 0.18%0.18% | 0.15%0.15% |
| Tokenizerأداة التقطيع | Tokensعدد الوحدات | Piecesالقطع |
|---|---|---|
| Wordكلمات | 1111 | people | in | my | house | do | not | know | how | to | close | doorspeople | in | my | house | do | not | know | how | to | close | doors |
| Characterمحارف | 4949 | p | e | o | p | l | e | (space) | i | n | ...p | e | o | p | l | e | (space) | i | n | ... |
| BPEBPE | 1212 | ▁people | ▁in | ▁my | ▁house | ▁do | ▁not | ▁know | ▁how | ▁to | ▁close | ▁do | ors▁people | ▁in | ▁my | ▁house | ▁do | ▁not | ▁know | ▁how | ▁to | ▁close | ▁do | ors |
- The word tokenizer has zero OOV on train by construction, because every training word is in its index. About 5% of validation and test words never appeared in the training split.أداة التقطيع على مستوى الكلمة نسبة OOV فيها صفر على قسم التدريب بحكم بنائها، لأن كل كلمة تدريب موجودة في فهرسها. ونحو 5% من كلمات التحقق والاختبار لم تظهر قط في قسم التدريب.
- The character tokenizer never meets an unknown symbol, because cleaning left only 26 letters and the space. The price is length: the sample tweet is 49 tokens instead of 11.أداة التقطيع على مستوى المحارف لا تصادف رمزًا مجهولًا أبدًا، لأن التنظيف لم يُبقِ إلا 26 حرفًا والفراغ. والثمن هو الطول: التغريدة النموذجية 49 وحدة بدل 11.
- BPE keeps doors as ▁do plus ors: the word is not a single piece, but its parts are. BPE can spell any word from the characters it knows, so its small OOV rate, about the same on train as on the other splits, comes from characters missing from the model's vocabulary, not from unseen words.يحتفظ BPE بالكلمة doors على شكل ▁do زائد ors: الكلمة ليست قطعة واحدة، لكن أجزاءها كذلك. يستطيع BPE تهجئة أي كلمة من المحارف التي يعرفها، لذلك فنسبة OOV الصغيرة عنده، وهي تقريبًا نفسها على التدريب وعلى القسمين الآخرين، سببها محارف غير موجودة في مفردات النموذج، لا كلمات لم تُرَ من قبل.
04Running the notebooksتشغيل دفاتر Jupyter
Part A needs only the standard library and matplotlib. Part B imports the Keras text tokenizer, and that import decides the Python version.
الجزء A يحتاج المكتبة القياسية وmatplotlib فقط. أما الجزء B فيستورد أداة تقطيع النصوص في Keras، وهذا الاستيراد هو الذي يحدد إصدار Python.
uv venv --python 3.11 .venv
source .venv/bin/activate
uv pip install "tensorflow==2.15.*" sentencepiece nltk pandas \
scikit-learn seaborn wordcloud jupyter
python -m nltk.downloader punkt punkt_tab stopwords wordnet omw-1.4
jupyter lab- The NLTK packages are punkt and punkt_tab for tokenization, stopwords for the stopword list, and wordnet with omw-1.4 for the lemmatizer.حزم NLTK المطلوبة هي punkt وpunkt_tab للتقطيع، وstopwords لقائمة الكلمات الشائعة (stopwords)، وwordnet مع omw-1.4 لأداة الإرجاع إلى الأصل المعجمي (lemmatizer).
- Keep the data files next to the notebooks:
uncorpora_plain_20090831.tmxanddoc.crypt.txtfor Part A,tweets_sentiment_analysis.csvandtweets_train.txtfor Part B.ضع ملفات البيانات بجانب الـ notebooks:uncorpora_plain_20090831.tmxوdoc.crypt.txtللجزء A، وtweets_sentiment_analysis.csvوtweets_train.txtللجزء B.
05Sourcesالمصادر
The references behind the methods used above, for anyone who wants the original treatment.
المراجع التي تقوم عليها الطرق المستخدمة أعلاه، لمن يريد الرجوع إلى المصدر الأصلي.
Sourcesالمصادر
- Speech and Language Processing, chapter 2: regular expressions, tokenization, edit distanceBookJurafsky and Martin, 3rd edition draft(opens in a new tab)(يفتح في تبويب جديد)
- re: regular expression operationsDocsPython documentationLookarounds, flags, matching and substitution as used in every question.التحقق المسبق واللاحق (lookarounds) والخيارات (flags) والمطابقة والاستبدال كما استُخدمت في كل سؤال.(opens in a new tab)(يفتح في تبويب جديد)
- Natural Language ToolkitDocsNLTK Project(opens in a new tab)(يفتح في تبويب جديد)
- SentencePieceDocsGoogleTraining options such as the model type and the vocabulary size.خيارات التدريب مثل نوع النموذج وحجم المفردات.(opens in a new tab)(يفتح في تبويب جديد)
- Neural Machine Translation of Rare Words with Subword UnitsPaperSennrich, Haddow and Birch, ACL 2016(opens in a new tab)(يفتح في تبويب جديد)
- SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text ProcessingPaperKudo and Richardson, EMNLP 2018(opens in a new tab)(يفتح في تبويب جديد)
- The Porter Stemming AlgorithmPaperMartin Porter, 1980(opens in a new tab)(يفتح في تبويب جديد)
- United Nations General Assembly Resolutions: A Six-Language Parallel CorpusPaperRafalovitch and Dale, MT Summit 2009(opens in a new tab)(يفتح في تبويب جديد)