may24c/insertion-paragraphe
0
1<!-- web/templates/documentation.html -->2{% extends "base.html" %}3 4{% block title %}Documentation - Scripts du projet{% endblock %}5{% block page_theme %}docs-theme{% endblock %}6 7{% block content %}8<div class="page-header">9 <h1> Scripts du projet</h1>10 <p>L'ensemble des scripts utilisés pour la collecte, le traitement et l'entraînement ainsi que l'evaluation des modèles</p>11</div>12 13<div class="content-section">14 <h2> Scripts de collecte des données</h2>15 16 <!-- Script 1 : Téléchargement arXiv -->17 <details class="script-details">18 <summary>19 <span class="script-icon"></span>20 <span class="script-title">download_arxiv_pdfs.py</span>21 <span class="script-badge">arXiv</span>22 </summary>23 <div class="script-content">24 <p><strong>Objectif :</strong> Télécharger des articles scientifiques depuis arXiv (catégorie cs.CL)</p>25 <pre><code class="language-python">import requests26import os27import feedparser28 29# Dossier où on va sauvegarder les PDFs30SAVE_DIR = "arxiv_pdfs"31os.makedirs(SAVE_DIR, exist_ok=True) # Crée le dossier s'il n'existe pas32 33# Requête pour récupérer des articles en informatique (cs.CL = Computation and Language)34QUERY = "cat:cs.CL"35MAX_RESULTS = 650 # Nombre max d'articles qu'on veut36 37# Construction de l'URL pour interroger l'API arXiv38url = (39 "http://export.arxiv.org/api/query?"40 f"search_query={QUERY}&start=0&max_results={MAX_RESULTS}"41)42 43# On parse le résultat (format Atom/RSS)44feed = feedparser.parse(url)45 46# Pour chaque article trouvé47for entry in feed.entries:48 49 # On récupère l'ID de l'article (ex: 2401.12345)50 arxiv_id = entry.id.split("/")[-1]51 52 # Le deuxième lien est celui du PDF53 pdf_url = entry.links[1].href54 55 # Chemin où sauvegarder le fichier56 out_path = os.path.join(SAVE_DIR, arxiv_id + ".pdf")57 58 # Si le fichier existe déjà, on passe (évite de retélécharger)59 if os.path.exists(out_path):60 continue61 62 print("Téléchargement :", arxiv_id)63 64 # Téléchargement du PDF65 r = requests.get(pdf_url)66 67 # Sauvegarde en binaire (mode 'wb')68 with open(out_path, "wb") as f:69 f.write(r.content)</code></pre>70 </div>71 </details>72 73 <!-- Script 2 : Extraction texte arXiv -->74 <details class="script-details">75 <summary>76 <span class="script-icon"></span>77 <span class="script-title">extract_arxiv_texts.py</span>78 <span class="script-badge">arXiv</span>79 </summary>80 <div class="script-content">81 <p><strong>Objectif :</strong> Convertir les PDF arXiv en fichiers texte exploitables</p>82 <pre><code class="language-python">import os83import fitz # PyMuPDF - bibliothèque pour lire les PDFs84import sys85 86# Dossiers d'entrée et de sortie87PDF_DIR = "arxiv_pdfs"88OUT_DIR = "arxiv_texts"89 90# Création du dossier de sortie91os.makedirs(OUT_DIR, exist_ok=True)92 93 94def extract_text(pdf_path):95 """Extrait le texte d'un PDF"""96 97 try:98 # On ouvre le PDF99 doc = fitz.open(pdf_path)100 101 # Vérification que le PDF est valide102 if doc.is_closed or doc.needs_pass:103 print(f" ⚠ Document invalide ou protégé: {pdf_path}")104 return None105 106 full_text = []107 108 # On parcourt chaque page du PDF109 for page_num in range(len(doc)):110 try:111 page = doc[page_num]112 text = page.get_text() # Extraction du texte de la page113 114 # On ne garde que les pages non vides115 if text and text.strip():116 full_text.append(text)117 118 except Exception as e:119 print(f" ⚠ Erreur sur la page {page_num} de {os.path.basename(pdf_path)}: {str(e)}")120 continue121 122 # On ferme le document123 doc.close()124 125 # Si on n'a rien extrait, on retourne None126 if not full_text:127 print(f" Aucun texte extrait de: {os.path.basename(pdf_path)}")128 return None129 130 # On retourne tout le texte en une seule chaîne131 return "\n".join(full_text)132 133 except Exception as e:134 print(f" Erreur lors de l'extraction de {os.path.basename(pdf_path)}: {str(e)}")135 return None136 137 138def extract_text_alternative(pdf_path):139 """Méthode alternative si la première échoue"""140 141 try:142 doc = fitz.open(pdf_path)143 full_text = []144 145 for page_num in range(len(doc)):146 try:147 page = doc[page_num]148 149 # Première méthode : extraction en texte brut150 text = page.get_text("text")151 152 # Si ça ne marche pas, on essaye mot par mot153 if not text or not text.strip():154 text = page.get_text("words") # Extraction mot par mot155 if text:156 # On reconstruit le texte à partir des mots157 text = " ".join([word[4] for word in text if len(word) > 4])158 159 if text and text.strip():160 full_text.append(text)161 162 except Exception as e:163 print(f" Erreur méthode alternative page {page_num}: {str(e)}")164 continue165 166 doc.close()167 168 if full_text:169 return "\n".join(full_text)170 return None171 172 except Exception as e:173 print(f" Erreur méthode alternative: {str(e)}")174 return None175 176 177def process_pdfs():178 """Traite tous les PDFs du répertoire"""179 180 # Compteurs pour les statistiques181 total_pdfs = 0182 success_count = 0183 failed_count = 0184 corrupted_count = 0185 186 # On liste tous les fichiers PDF187 pdf_files = [f for f in os.listdir(PDF_DIR) if f.endswith(".pdf")]188 total_pdfs = len(pdf_files)189 190 print(f"\n {total_pdfs} fichiers PDF trouvés dans {PDF_DIR}\n")191 192 # On traite chaque fichier un par un193 for i, file in enumerate(pdf_files, 1):194 pdf_path = os.path.join(PDF_DIR, file)195 196 print(f" [{i}/{total_pdfs}] Traitement: {file}")197 198 # Essai de la méthode standard199 txt = extract_text(pdf_path)200 201 # Si ça échoue, on essaye la méthode alternative202 if txt is None:203 print(f" Tentative avec méthode alternative...")204 txt = extract_text_alternative(pdf_path)205 if txt is not None:206 print(f" Réussi avec méthode alternative!")207 208 # Si on a réussi à extraire du texte209 if txt is not None and txt.strip():210 out_path = os.path.join(OUT_DIR, file.replace(".pdf", ".txt"))211 212 try:213 # Sauvegarde du texte dans un fichier .txt214 with open(out_path, "w", encoding="utf8") as f:215 f.write(txt)216 217 print(f" Sauvegardé: {os.path.basename(out_path)} ({len(txt)} caractères)")218 success_count += 1219 220 except Exception as e:221 print(f" Erreur sauvegarde: {str(e)}")222 failed_count += 1223 else:224 # Si l'extraction a échoué225 try:226 file_size = os.path.getsize(pdf_path)227 if file_size < 1000:228 print(f" Fichier suspect (taille: {file_size} octets)")229 else:230 print(f" Échec extraction")231 corrupted_count += 1232 except:233 corrupted_count += 1234 235 # Rapport final236 print(f"\n" + "="*50)237 print(f" RAPPORT FINAL")238 print(f"="*50)239 print(f"Total PDFs trouvés: {total_pdfs}")240 print(f"Extractions réussies: {success_count}")241 print(f"Échecs extraction: {failed_count}")242 print(f"Fichiers corrompus: {corrupted_count}")243 print(f"Textes sauvegardés dans: {OUT_DIR}")244 print(f"="*50)245 246 247if __name__ == "__main__":248 249 # Vérification que le dossier source existe250 if not os.path.exists(PDF_DIR):251 print(f" Erreur: Le répertoire {PDF_DIR} n'existe pas!")252 sys.exit(1)253 254 print(" Début du traitement des PDFs arXiv")255 print(f" Source: {PDF_DIR}")256 print(f" Destination: {OUT_DIR}")257 258 # On lance le traitement259 process_pdfs()260 261 print("\n Traitement terminé!")</code></pre>262 </div>263 </details>264 265 <!-- Script 3 : Téléchargement PMC -->266 <details class="script-details">267 <summary>268 <span class="script-icon"></span>269 <span class="script-title">download_pmc_xml.py</span>270 <span class="script-badge">PMC</span>271 </summary>272 <div class="script-content">273 <p><strong>Objectif :</strong> Télécharger des articles biomédicaux depuis PubMed Central</p>274 <pre><code class="language-python">import requests275import os276import time277 278# Dossier où on va sauvegarder les fichiers XML279SAVE_DIR = "pmc_xml"280os.makedirs(SAVE_DIR, exist_ok=True)281 282# ==========================================283# ÉTAPE 1 : Recherche des IDs des articles284# ==========================================285 286# URL de recherche des articles PMC287search_url = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi"288 289# Paramètres de la requête290params = {291 "db": "pmc", # Base de données : PubMed Central292 "term": "open access[filter]", # On veut uniquement les articles en libre accès293 "retmax": 650 # Nombre max d'articles294}295 296print("Recherche des IDs PMC...")297 298# On envoie la requête299r = requests.get(search_url, params=params, timeout=30)300r.raise_for_status() # Lève une erreur si la requête a échoué301 302# On extrait les IDs de la réponse XML303ids = []304for line in r.text.split("<Id>")[1:]:305 ids.append(line.split("</Id>")[0])306 307print(len(ids), "IDs trouvés")308 309# ==========================================310# ÉTAPE 2 : Téléchargement des fichiers XML311# ==========================================312 313# Pour chaque ID trouvé314for pmc_id in ids:315 316 # URL pour récupérer le contenu de l'article317 fetch_url = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi"318 319 params = {320 "db": "pmc",321 "id": pmc_id,322 "retmode": "xml" # On veut le format XML323 }324 325 out_path = os.path.join(SAVE_DIR, pmc_id + ".xml")326 327 # Si le fichier existe déjà, on passe (évite de retélécharger)328 if os.path.exists(out_path):329 print("Déjà présent :", pmc_id)330 continue331 332 print("Téléchargement PMC", pmc_id)333 334 try:335 # Téléchargement du fichier XML336 r = requests.get(fetch_url, params=params, timeout=30)337 338 if r.status_code != 200:339 print(" -> erreur HTTP", r.status_code)340 continue341 342 # Sauvegarde du fichier343 with open(out_path, "w", encoding="utf-8") as f:344 f.write(r.text)345 346 except requests.exceptions.RequestException as e:347 print(" -> erreur réseau pour", pmc_id)348 print(" ", e)349 continue350 351 # Pause pour éviter de surcharger le serveur352 time.sleep(1)</code></pre>353 </div>354 </details>355</div>356 357<div class="content-section">358 <h2> Scripts de génération des datasets</h2>359 360 <!-- Script 4 : Génération dataset Wikipédia -->361 <details class="script-details">362 <summary>363 <span class="script-icon"></span>364 <span class="script-title">generate_wiki.py</span>365 <span class="script-badge">Wikipédia</span>366 </summary>367 <div class="script-content">368 <p><strong>Objectif :</strong> Générer des exemples d'insertion à partir des articles Wikipédia</p>369 <pre><code class="language-python">import os370import json371import random372from tqdm import tqdm373 374# Configuration375INPUT_DIR = "extracted" # Dossier où sont les articles Wikipédia extraits376OUTPUT_FILE = "wiki_dataset.jsonl" # Fichier de sortie377 378MIN_PARAGRAPHS = 5 # Minimum de paragraphes pour garder un article379MAX_SAMPLES_PER_ARTICLE = 1 # Nombre d'exemples par article380 381 382def clean_paragraphs(text):383 """Nettoie le texte en gardant uniquement les paragraphes valides"""384 385 # On découpe par sauts de ligne386 paragraphs = text.split("\n")387 388 # On ne garde que les paragraphes non vides et assez longs389 paragraphs = [390 p.strip()391 for p in paragraphs392 if len(p.strip()) > 80 # On ignore les paragraphes trop courts393 ]394 395 return paragraphs396 397 398def generate_examples_from_text(title, text):399 """400 À partir d'un article, génère des exemples où on retire un paragraphe401 et on demande au modèle de le réinsérer402 """403 404 # On nettoie le texte405 paragraphs = clean_paragraphs(text)406 407 # Si l'article n'a pas assez de paragraphes, on l'ignore408 if len(paragraphs) < MIN_PARAGRAPHS:409 return []410 411 examples = []412 413 # On génère un exemple par article (ou plusieurs selon config)414 for _ in range(min(MAX_SAMPLES_PER_ARTICLE, len(paragraphs))):415 416 # On choisit un paragraphe au hasard à retirer417 idx = random.randint(0, len(paragraphs) - 1)418 419 # Le paragraphe qu'on va retirer420 removed = paragraphs[idx]421 422 # L'article sans ce paragraphe423 truncated = paragraphs[:idx] + paragraphs[idx + 1:]424 425 # On crée l'exemple426 example = {427 "source": "wikipedia", # Source des données428 "title": title, # Titre de l'article429 "article_truncated": truncated, # Article sans le paragraphe430 "paragraph_to_insert": removed, # Paragraphe à insérer431 "true_position": idx # Position correcte (0-based)432 }433 434 examples.append(example)435 436 return examples437 438 439def process_wiki_dump():440 """Traitement principal"""441 442 # On récupère tous les fichiers d'articles Wikipédia443 all_files = []444 445 for root, _, files in os.walk(INPUT_DIR):446 for file in files:447 all_files.append(os.path.join(root, file))448 449 print(len(all_files), "fichiers trouvés")450 451 # On mélange pour avoir des articles variés452 random.shuffle(all_files)453 454 article_count = 0 # Compteur d'articles traités455 456 with open(OUTPUT_FILE, "w", encoding="utf-8") as out:457 458 # On parcourt tous les fichiers459 for file_path in tqdm(all_files):460 461 # On s'arrête quand on a assez d'articles462 if article_count >= MAX_ARTICLES:463 break464 465 # On ouvre le fichier466 with open(file_path, "r", encoding="utf-8", errors="ignore") as f:467 468 # Chaque ligne est un article au format JSON469 for line in f:470 471 if article_count >= MAX_ARTICLES:472 break473 474 line = line.strip()475 if not line:476 continue477 478 try:479 article = json.loads(line)480 except json.JSONDecodeError:481 continue482 483 # On génère les exemples pour cet article484 examples = generate_examples_from_text(485 article.get("title", ""),486 article.get("text", "")487 )488 489 # Si l'article a donné des exemples, on le compte490 if examples:491 article_count += 1492 493 # On écrit les exemples dans le fichier de sortie494 for ex in examples:495 out.write(json.dumps(ex, ensure_ascii=False) + "\n")496 497 print("Articles utilisés :", article_count)498 499 500if __name__ == "__main__":501 process_wiki_dump()</code></pre>502 </div>503 </details>504 505 <!-- Script 5 : Génération dataset arXiv -->506 <details class="script-details">507 <summary>508 <span class="script-icon"></span>509 <span class="script-title">generate_arxiv.py</span>510 <span class="script-badge">arXiv</span>511 </summary>512 <div class="script-content">513 <p><strong>Objectif :</strong> Générer des exemples d'insertion à partir des articles arXiv</p>514 <pre><code class="language-python">import os515import json516import random517 518# Configuration519INPUT_DIR = "arxiv_texts" # Dossier avec les fichiers .txt extraits520OUTPUT_FILE = "arxiv_dataset.jsonl" # Fichier de sortie521 522MIN_PARAGRAPHS = 5 # Minimum de paragraphes par article523MAX_SAMPLES_PER_ARTICLE = 1 # Nombre d'exemples par article524 525 526def clean_paragraphs(text):527 """Nettoie le texte en paragraphes valides"""528 529 # On découpe par sauts de ligne530 paragraphs = text.split("\n")531 532 # On nettoie et on filtre533 paragraphs = [534 p.strip()535 for p in paragraphs536 if len(p.strip()) > 80 # On garde les paragraphes suffisamment longs537 ]538 539 return paragraphs540 541 542def generate_examples_from_text(title, text):543 """544 Génère des exemples d'insertion à partir d'un article545 Principe : on retire un paragraphe au hasard, et on garde l'article sans ce paragraphe546 """547 548 # On nettoie le texte549 paragraphs = clean_paragraphs(text)550 551 # Si l'article n'a pas assez de paragraphes, on l'ignore552 if len(paragraphs) < MIN_PARAGRAPHS:553 return []554 555 examples = []556 557 # On génère un exemple par article (ou plus selon config)558 for _ in range(min(MAX_SAMPLES_PER_ARTICLE, len(paragraphs))):559 560 # On choisit un paragraphe au hasard561 idx = random.randint(0, len(paragraphs) - 1)562 563 # Le paragraphe qu'on va retirer564 removed = paragraphs[idx]565 566 # L'article sans ce paragraphe567 truncated = paragraphs[:idx] + paragraphs[idx + 1:]568 569 # Construction de l'exemple570 example = {571 "source": "arxiv", # Source des données572 "title": title, # Titre de l'article573 "article_truncated": truncated, # Article modifié574 "paragraph_to_insert": removed, # Paragraphe retiré575 "true_position": idx # Position originale576 }577 578 examples.append(example)579 580 return examples581 582 583# On récupère tous les fichiers .txt584all_files = [f for f in os.listdir(INPUT_DIR) if f.endswith(".txt")]585 586print(len(all_files), "fichiers trouvés")587 588# On mélange pour avoir une bonne diversité589random.shuffle(all_files)590 591article_count = 0 # Compteur592 593with open(OUTPUT_FILE, "w", encoding="utf8") as out:594 595 # On parcourt chaque fichier596 for file in all_files:597 598 # On s'arrête quand on a assez d'articles599 if article_count >= MAX_ARTICLES:600 break601 602 path = os.path.join(INPUT_DIR, file)603 604 # Lecture du texte de l'article605 with open(path, encoding="utf8", errors="ignore") as f:606 text = f.read()607 608 # Le titre est le nom du fichier sans l'extension609 title = file.replace(".txt", "")610 611 # Génération des exemples612 examples = generate_examples_from_text(title, text)613 614 # Si l'article a donné des exemples, on le compte615 if examples:616 article_count += 1617 618 # Écriture dans le fichier de sortie619 for ex in examples:620 out.write(json.dumps(ex, ensure_ascii=False) + "\n")621 622print("Articles utilisés :", article_count)</code></pre>623 </div>624 </details>625 626 <!-- Script 6 : Génération dataset PMC -->627 <details class="script-details">628 <summary>629 <span class="script-icon"></span>630 <span class="script-title">generate_pmc.py</span>631 <span class="script-badge">PMC</span>632 </summary>633 <div class="script-content">634 <p><strong>Objectif :</strong> Générer des exemples d'insertion à partir des articles PMC</p>635 <pre><code class="language-python">import os636import json637import random638import xml.etree.ElementTree as ET639 640# Configuration641INPUT_DIR = "pmc_xml" # Dossier avec les fichiers XML de PMC642OUTPUT_FILE = "pmc_dataset.jsonl" # Fichier de sortie643 644MIN_PARAGRAPHS = 5 # Minimum de paragraphes par article645MAX_SAMPLES_PER_ARTICLE = 1 # Nombre d'exemples par article646 647 648def extract_paragraphs(xml_path):649 """650 Extrait les paragraphes d'un fichier XML PMC651 Les paragraphes sont dans les balises <p>652 """653 654 # On parse le fichier XML655 tree = ET.parse(xml_path)656 root = tree.getroot()657 658 paragraphs = []659 660 # On cherche toutes les balises <p> (paragraphe)661 for p in root.findall(".//p"):662 # On récupère le texte complet de la balise663 txt = "".join(p.itertext()).strip()664 665 # On garde les paragraphes suffisamment longs666 if len(txt) > 80:667 paragraphs.append(txt)668 669 return paragraphs670 671 672# On récupère tous les fichiers XML673all_files = [f for f in os.listdir(INPUT_DIR) if f.endswith(".xml")]674 675print(len(all_files), "fichiers trouvés")676 677# On mélange pour avoir des articles variés678random.shuffle(all_files)679 680article_count = 0681 682with open(OUTPUT_FILE, "w", encoding="utf8") as out:683 684 # On parcourt chaque fichier685 for file in all_files:686 687 # On s'arrête quand on a assez d'articles688 if article_count >= MAX_ARTICLES:689 break690 691 path = os.path.join(INPUT_DIR, file)692 693 try:694 # Extraction des paragraphes695 paragraphs = extract_paragraphs(path)696 except Exception as e:697 print(f"Erreur avec {file}: {e}")698 continue699 700 # On ignore les articles trop courts701 if len(paragraphs) < MIN_PARAGRAPHS:702 continue703 704 # Le titre est le nom du fichier sans extension705 title = file.replace(".xml", "")706 707 examples = []708 709 # Génération des exemples710 for _ in range(min(MAX_SAMPLES_PER_ARTICLE, len(paragraphs))):711 712 # On choisit un paragraphe au hasard713 idx = random.randint(0, len(paragraphs) - 1)714 715 # Le paragraphe qu'on retire716 removed = paragraphs[idx]717 718 # L'article sans ce paragraphe719 truncated = paragraphs[:idx] + paragraphs[idx + 1:]720 721 # Construction de l'exemple722 example = {723 "source": "pmc", # Source724 "title": title, # Titre725 "article_truncated": truncated, # Article modifié726 "paragraph_to_insert": removed, # Paragraphe à insérer727 "true_position": idx # Position originale728 }729 730 examples.append(example)731 732 # Si l'article a donné des exemples, on le compte733 if examples:734 article_count += 1735 736 # Écriture dans le fichier de sortie737 for ex in examples:738 out.write(json.dumps(ex, ensure_ascii=False) + "\n")739 740print("Articles utilisés :", article_count)</code></pre>741 </div>742 </details>743</div>744 745<div class="content-section">746 <h2> Scripts de traitement</h2>747 748 <!-- Script 7 : Shuffle dataset -->749 <details class="script-details">750 <summary>751 <span class="script-icon"></span>752 <span class="script-title">shuffle_dataset.py</span>753 <span class="script-badge">Traitement</span>754 </summary>755 <div class="script-content">756 <p><strong>Objectif :</strong> Mélanger aléatoirement les exemples du dataset</p>757 <pre><code class="language-python">import json758import random759 760# Fichier d'entrée (dataset fusionné) et fichier de sortie761INPUT_FILE = "dataset_total.jsonl"762OUTPUT_FILE = "dataset_shuffled.jsonl"763 764# ==========================================765# Pourquoi mélanger ?766# ==========================================767# Le mélange aléatoire (shuffling) est crucial pour l'apprentissage :768# - Évite que le modèle apprenne des patterns liés à l'ordre des données769# - Évite les biais liés à la source (Wikipédia, arXiv, PMC)770# - Permet une meilleure généralisation du modèle771 772data = []773 774# Lecture du dataset775with open(INPUT_FILE, encoding="utf8") as f:776 for line in f:777 data.append(json.loads(line))778 779print("Nombre d'exemples :", len(data))780 781# Mélange aléatoire des exemples782random.shuffle(data)783 784# Écriture du dataset mélangé785with open(OUTPUT_FILE, "w", encoding="utf8") as f:786 for example in data:787 f.write(json.dumps(example, ensure_ascii=False) + "\n")788 789print("Dataset mélangé sauvegardé dans :", OUTPUT_FILE)</code></pre>790 </div>791 </details>792 793 <!-- Script 8 : Split dataset -->794 <details class="script-details">795 <summary>796 <span class="script-icon"></span>797 <span class="script-title">split_dataset.py</span>798 <span class="script-badge">Traitement</span>799 </summary>800 <div class="script-content">801 <p><strong>Objectif :</strong> Diviser le dataset en train/dev/test</p>802 <pre><code class="language-python">import json803import random804 805# Fichier d'entrée806INPUT_FILE = "dataset_shuffled.jsonl"807 808# Fichiers de sortie809TRAIN_FILE = "train.jsonl"810DEV_FILE = "dev.jsonl"811TEST_FILE = "test.jsonl"812 813# Lecture du dataset814with open(INPUT_FILE, encoding="utf8") as f:815 data = [json.loads(line) for line in f]816 817# On mélange à nouveau pour garantir un split aléatoire818random.shuffle(data)819 820n = len(data)821print("Total d'exemples :", n)822 823# Répartition : 80% train, 10% dev, 10% test824train = data[: int(0.8 * n)]825dev = data[int(0.8 * n): int(0.9 * n)]826test = data[int(0.9 * n):]827 828# Sauvegarde des trois fichiers829for name, dataset in [830 (TRAIN_FILE, train),831 (DEV_FILE, dev),832 (TEST_FILE, test)833]:834 with open(name, "w", encoding="utf8") as f:835 for ex in dataset:836 f.write(json.dumps(ex, ensure_ascii=False) + "\n")837 838print("Train:", len(train))839print("Dev:", len(dev))840print("Test:", len(test))</code></pre>841 </div>842 </details>843 844 <!-- Script 9 : Build pairs -->845 <details class="script-details">846 <summary>847 <span class="script-icon"></span>848 <span class="script-title">build_pairs.py</span>849 <span class="script-badge">Traitement</span>850 </summary>851 <div class="script-content">852 <p><strong>Objectif :</strong> Transformer les articles en paires (prev, insert, next) pour l'entraînement</p>853 <pre><code class="language-python">import json854import argparse855 856def convert_dataset(input_file, output_file):857 """858 Transforme un dataset d'articles en paires d'insertion859 860 Pour chaque article, on génère des paires (prev, next, insert, label)861 où :862 - prev = paragraphe avant l'insertion863 - next = paragraphe après l'insertion864 - insert = paragraphe à insérer865 - label = 1 si c'est la bonne position, 0 sinon866 """867 868 # On ouvre le fichier d'entrée (dataset brut)869 # et le fichier de sortie où on va écrire les paires870 with open(input_file, encoding="utf8") as f, open(output_file, "w", encoding="utf8") as out:871 872 # On lit le fichier ligne par ligne (format JSONL)873 for line in f:874 875 # On convertit la ligne JSON en dictionnaire Python876 ex = json.loads(line)877 878 # On récupère les éléments utiles879 article = ex["article_truncated"] # liste des paragraphes880 insert = ex["paragraph_to_insert"] # paragraphe à insérer881 gold = ex["true_position"] # position correcte882 883 # Si l'article est trop court, on ne peut pas créer de paires884 if len(article) < 2:885 continue886 887 # On parcourt toutes les positions possibles dans l'article888 for i in range(len(article) - 1):889 890 # On récupère le paragraphe avant et après891 prev_p = article[i]892 next_p = article[i + 1]893 894 # Label = 1 si c'est la bonne position895 # Attention : gold correspond à la position dans l'article original896 # On compare avec i+1 car les positions sont décalées897 label = 1 if i == gold - 1 else 0898 899 # On construit la paire pour le modèle900 pair = {901 "prev": prev_p,902 "next": next_p,903 "insert": insert,904 "label": label905 }906 907 # On écrit la paire dans le fichier de sortie (format JSONL)908 out.write(json.dumps(pair, ensure_ascii=False) + "\n")909 910 911if __name__ == "__main__":912 913 # On utilise argparse pour pouvoir lancer le script en ligne de commande914 parser = argparse.ArgumentParser()915 916 # Argument : chemin du fichier d'entrée917 parser.add_argument("--input")918 919 # Argument : chemin du fichier de sortie920 parser.add_argument("--output")921 922 # On récupère les arguments donnés par l'utilisateur923 args = parser.parse_args()924 925 # On lance la conversion du dataset926 convert_dataset(args.input, args.output)</code></pre>927 <p><strong>Exécution :</strong></p>928 <pre><code class="language-bash">python build_pairs.py --input train.jsonl --output train_pairs.jsonl929python build_pairs.py --input dev.jsonl --output dev_pairs.jsonl930python build_pairs.py --input test.jsonl --output test_pairs.jsonl</code></pre>931 </div>932 </details>933</div>934 935<div class="content-section">936 <h2> Rétrécissement du dataset</h2>937 <p>Face aux contraintes de mémoire GPU et pour accélérer les phases d'expérimentation, nous avons dû réduire la taille de notre dataset tout en préservant son équilibre entre les classes. Cette étape, appelée <strong>sous-échantillonnage (downsampling)</strong>, nous permet de travailler avec un jeu de données plus petit mais toujours représentatif.</p>938 939 <p>Nous avons utilisé la méthode du <strong>réservoir sampling (échantillonnage par réservoir)</strong> qui garantit un tirage aléatoire uniforme même sur des fichiers trop volumineux pour être chargés entièrement en mémoire.</p>940 941 <p>L'objectif était de créer des versions réduites de nos datasets :</p>942 <ul>943 <li><strong>Train :</strong> 20 000 exemples par classe (40 000 au total)</li>944 <li><strong>Dev (validation) :</strong> 2 000 exemples par classe (4 000 au total)</li>945 <li><strong>Test :</strong> 5 000 exemples par classe (10 000 au total)</li>946 </ul>947 948 <p>Cette réduction nous a permis de :</p>949 <ul>950 <li>Réduire drastiquement le temps d'entraînement (de plusieurs heures à 1 seule heure)</li>951 <li>Adapter la consommation mémoire aux limitations de nos GPUs </li>952 </ul>953 954 <!-- Script : Réservoir sampling -->955 <details class="script-details">956 <summary>957 <span class="script-icon"></span>958 <span class="script-title">reservoir_sampling.py</span>959 <span class="script-badge">Traitement</span>960 </summary>961 <div class="script-content">962 <p><strong>Objectif :</strong> Réduire la taille du dataset tout en préservant l'équilibre entre les classes (label 0 et label 1).</p>963 964 <pre><code class="language-python">import json965import random966from pathlib import Path967 968def reservoir_sample_by_label(input_path, output_path, target_counts, seed=42):969 """970 Échantillonne un dataset JSONL en préservant un nombre cible d'exemples par label.971 972 Args:973 input_path: chemin du fichier d'entrée974 output_path: chemin du fichier de sortie975 target_counts: dictionnaire {label: nombre_cible}976 seed: graine aléatoire pour la reproductibilité977 """978 979 # On fixe la graine aléatoire pour la reproductibilité980 random.seed(seed)981 982 # Création des réservoirs pour chaque label983 reservoirs = {label: [] for label in target_counts}984 seen_counts = {label: 0 for label in target_counts}985 986 print(f"Lecture du fichier: {input_path}")987 988 with open(input_path, "r", encoding="utf-8") as f:989 for line in f:990 obj = json.loads(line)991 label = obj["label"]992 993 # On ignore les labels non ciblés994 if label not in target_counts:995 continue996 997 seen_counts[label] += 1998 k = target_counts[label]999 1000 # Algorithme du réservoir sampling1001 if len(reservoirs[label]) < k:1002 # Réservoir pas encore plein → on ajoute directement1003 reservoirs[label].append(line)1004 else:1005 # Réservoir plein → remplacement aléatoire avec probabilité k/n1006 j = random.randint(1, seen_counts[label])1007 if j <= k:1008 reservoirs[label][j - 1] = line1009 1010 # Vérification que tous les réservoirs sont remplis1011 for label, target in target_counts.items():1012 actual = len(reservoirs[label])1013 if actual < target:1014 print(f" Attention: label {label} n'a que {actual} exemples (objectif: {target})")1015 1016 # Mélange des exemples de tous les labels1017 all_lines = []1018 for label in target_counts:1019 all_lines.extend(reservoirs[label])1020 random.shuffle(all_lines)1021 1022 # Création du dossier de sortie si nécessaire1023 Path(output_path).parent.mkdir(parents=True, exist_ok=True)1024 1025 # Écriture du fichier de sortie1026 with open(output_path, "w", encoding="utf-8") as f:1027 for line in all_lines:1028 f.write(line)1029 1030 print(f" Dataset sauvegardé: {output_path}")1031 print(" Répartition finale :")1032 for label in target_counts:1033 print(f" label {label}: {len(reservoirs[label])} exemples")1034 1035 1036if __name__ == "__main__":1037 # Chemins des fichiers originaux1038 TRAIN_IN = "dataset/train_pairs.jsonl"1039 DEV_IN = "dataset/dev_pairs.jsonl"1040 TEST_IN = "dataset/test_pairs.jsonl"1041 1042 # Chemins des fichiers réduits1043 TRAIN_OUT = "dataset_small/train_small_balanced.jsonl"1044 DEV_OUT = "dataset_small/dev_small_balanced.jsonl"1045 TEST_OUT = "dataset_small/test_small_balanced.jsonl"1046 1047 print("=" * 60)1048 print(" RÉTRÉCISSEMENT DES DATASETS")1049 print("=" * 60)1050 1051 # Sous-échantillonnage du train : 20k par classe1052 reservoir_sample_by_label(1053 TRAIN_IN,1054 TRAIN_OUT,1055 target_counts={0: 20000, 1: 20000},1056 seed=421057 )1058 1059 # Sous-échantillonnage du dev : 2k par classe1060 reservoir_sample_by_label(1061 DEV_IN,1062 DEV_OUT,1063 target_counts={0: 2000, 1: 2000},1064 seed=421065 )1066 1067 # Sous-échantillonnage du test : 5k par classe1068 reservoir_sample_by_label(1069 TEST_IN,1070 TEST_OUT,1071 target_counts={0: 5000, 1: 5000},1072 seed=421073 )1074 1075 print(" Tous les datasets ont été réduits avec succès!")</code></pre>1076 1077 <h3> Résultat du rétrécissement</h3>1078 1079 <table class="results-table">1080 <thead>1081 <tr>1082 <th>Dataset</th>1083 <th>Label 0 (original)</th>1084 <th>Label 1 (original)</th>1085 <th>Label 0 (réduit)</th>1086 <th>Label 1 (réduit)</th>1087 <th>Total réduit</th>1088 </tr>1089 </thead>1090 <tbody>1091 <tr>1092 <td><strong>Train</strong></td>1093 <td>~5.6M</td>1094 <td>~5.6M</td>1095 <td>20 000</td>1096 <td>20 000</td>1097 <td>40 000</td>1098 </tr>1099 <tr>1100 <td><strong>Dev</strong></td>1101 <td>~700k</td>1102 <td>~700k</td>1103 <td>2 000</td>1104 <td>2 000</td>1105 <td>4 000</td>1106 </tr>1107 <tr>1108 <td><strong>Test</strong></td>1109 <td>~700k</td>1110 <td>~700k</td>1111 <td>5 000</td>1112 <td>5 000</td>1113 <td>10 000</td>1114 </tr>1115 </tbody>1116 </table>1117 1118 <div class="note">1119 <strong> Pourquoi l'échantillonnage par réservoir ?</strong><br>1120 Cette méthode présente plusieurs avantages :<br>1121 - <strong>Économie de mémoire</strong> : on ne charge jamais tout le dataset en mémoire<br>1122 - <strong>Échantillon aléatoire uniforme</strong> : chaque ligne a la même probabilité d'être sélectionnée<br>1123 - <strong>Reproductibilité</strong> : la graine aléatoire (seed=42) garantit des résultats identiques à chaque exécution<br>1124 - <strong>Équilibre préservé</strong> : on conserve exactement le nombre d'exemples souhaité par classe1125 </div>1126 </div>1127 </details>1128</div>1129 1130<div class="content-section">1131 <h2> Scripts d'entraînement et d'évaluation</h2>1132 1133 <!-- Script 10 : XLM-RoBERTa -->1134 <details class="script-details">1135 <summary>1136 <span class="script-icon"></span>1137 <span class="script-title">XLM-RoBERTa</span>1138 <span class="script-badge">Entraînement</span>1139 </summary>1140 <div class="script-content">1141 <p><strong>Objectif :</strong> Entraîner le modèle XLM-RoBERTa (modèle multilingue performant) sur notre tâche d'insertion de paragraphe.</p>1142 <p>XLM-RoBERTa est un modèle multilingue pré-entraîné sur 100 langues, ce qui le rend particulièrement adapté pour traiter des textes en français comme en anglais. Contrairement à l'approche de ranking utilisée pour BERT Multilingual, ici nous abordons le problème comme une <strong>classification binaire classique</strong> : pour chaque position possible, le modèle doit prédire si c'est la bonne position (label 1) ou non (label 0).</p>1143 <p>Le script commence par charger les données au format JSONL, où chaque exemple contient les champs <code>prev</code>, <code>insert</code>, <code>next</code> et <code>label</code>. La fonction <code>preprocess()</code> prépare chaque exemple en concaténant les trois paragraphes sous la forme <code>insert [SEP] prev [SEP] next</code> (l'ordre est différent de l'approche ranking pour mieux correspondre à l'architecture du modèle). La tokenisation utilise <code>longest_first</code> pour gérer les séquences de longueur variable.</p>1144 <p>Le modèle XLM-RoBERTa est configuré avec <code>num_labels=2</code> pour effectuer une classification binaire. L'entraînement utilise le <code>Trainer</code> de Hugging Face, qui gère automatiquement la boucle d'entraînement, l'évaluation et la sauvegarde des checkpoints. Les métriques calculées sont l'accuracy, le F1-score, la précision et le rappel.</p>1145 <p>Les hyperparamètres sont les suivants : batch size de 16, learning rate de 2e-5, 4 epochs, warmup ratio de 0.1, et une évaluation à chaque epoch. Le modèle sauvegarde automatiquement le meilleur checkpoint en fonction de la loss de validation.</p>1146 <p>À la fin de l'entraînement, le script évalue le modèle sur les datasets de validation et de test, puis sauvegarde le modèle final dans <code>my_xlm_roberta_model</code>. Une fonction <code>predict()</code> est également fournie pour faire des prédictions sur de nouveaux exemples.</p>1147 1148 <pre><code class="language-python"># ==========================================1149# XLM-ROBERTA - ENTRAÎNEMENT CLASSIFICATION1150# ==========================================1151 1152# Imports1153import json1154import torch1155from datasets import Dataset1156from transformers import (1157 AutoTokenizer,1158 AutoModelForSequenceClassification,1159 Trainer,1160 TrainingArguments1161)1162from sklearn.metrics import accuracy_score, precision_recall_fscore_support1163 1164# ==========================================1165# CHARGEMENT DES DONNÉES1166# ==========================================1167 1168def load_jsonl(path):1169 \"\"\"Charge un fichier JSONL et retourne une liste de dictionnaires\"\"\"1170 data = []1171 with open(path, "r", encoding="utf-8") as f:1172 for line in f:1173 try:1174 data.append(json.loads(line))1175 except json.JSONDecodeError:1176 pass1177 return data1178 1179# Chargement des datasets1180train_data = load_jsonl("dataset_small/train_small_balanced.jsonl")1181dev_data = load_jsonl("dataset_small/dev_small_balanced.jsonl")1182test_data = load_jsonl("dataset_small/test_small_balanced.jsonl")1183 1184# Conversion en Dataset Hugging Face1185train_dataset = Dataset.from_list(train_data)1186dev_dataset = Dataset.from_list(dev_data)1187test_dataset = Dataset.from_list(test_data)1188 1189# ==========================================1190# TOKENIZER ET MODÈLE1191# ==========================================1192 1193model_name = "xlm-roberta-base"1194tokenizer = AutoTokenizer.from_pretrained(model_name)1195 1196# ==========================================1197# PRÉTRAITEMENT1198# ==========================================1199 1200def preprocess(example):