📝 : Update and restructuring README.md

This commit is contained in:
2025-10-28 20:38:07 +01:00
parent 3f5639ac4d
commit 098c5e3738
2 changed files with 132 additions and 146 deletions
+134 -150
View File
@@ -1,31 +1,34 @@
## TODO LIST # Projet IA5 — Étude : classification des textes par époque
- [ ] Introduction du rapport
- [ ] Toute la première partie d'essaies (*Méthodes de classification essayées* & *Observations initiales*)
- [ ] Comparaison de perf entre 10 ans et 50 ans (dans *Recalibrage et optimisations*)
- [x] Mettre le code de la méthode clean_text initiale
- [x] Mentionner la réduction des données parce que prétraitément très long
*Mathéo GUILBERT* *Mathéo GUILBERT*
# Projet IA5 — Étude : classification des textes par époque
TODO: INTRODUCTION A REVOIR
--- ---
## Contexte et problématique ## 1. Contexte et problématique
L'objectif principal de cette étude est double : L'objectif principal de cette étude est d'évaluer si l'on peut reconnaître, de façon fiable et robuste, l'époque d'un texte uniquement à partir de son vocabulaire et de sa sémantique.
- Évaluer si l'on peut reconnaître, de façon fiable et robuste, l'époque d'un texte uniquement à partir de son vocabulaire et de sa sémantique. Je cherche à répondre à la question : "Est-il possible de reconnaître l'époque d'un texte grâce à ses mots et à sa construction ?".
- Explorer s'il est possible, conceptuellement et techniquement, de « traduire » un texte d'une époque à une autre — c'est‑à‑dire adapter le style et les choix lexicaux pour rendre un texte contemporain semblable à un texte d'une époque donnée.
**Problématique :** estil possible de reconnaître l'époque d'un texte grâce à ses mots et à sa construction, et jusqu'à quel degré de précision ? Peuton ensuite transformer le style pour le rapprocher d'une époque cible ? Dans un second temps, j'explore la faisabilité de "traduire" un texte d'une époque à une autre.
## Les données ## 2. Démarche
Le jeu de données utilisé est `PleIAs/French-PD-Books` ([voir sur Hugging Face](https://huggingface.co/datasets/PleIAs/French-PD-Books)). Il contient environ 289 000 livres complets en français (soit environ 16,407,292,362 mots) avec, pour chaque ouvrage, les données suivantes : Pour répondre à cette problématique, ma démarche a été la suivante :
1. **Exploration et Prétraitement :** Analyse du jeu de données, nettoyage des textes et création d'étiquettes de "périodes" (par tranches de 10 et 50 ans).
2. **Premier essaie :** Une première expérimentation utilisant TF-IDF avec SGDClassifier.
3. **Second essaie :** Une seconde approche utilisant des *embeddings* de phrases pour capturer la sémantique du texte.
4. **Analyse et Comparaison :** Comparaison des performances des deux modèles.
## 3. Les données et Prétraitement
### 3.1. Source
Le jeu de données utilisé est `PleIAs/French-PD-Books` ([voir sur Hugging Face](https://huggingface.co/datasets/PleIAs/French-PD-Books)). Il contient 289 000 livres environ, avec, pour chaque ouvrage, les données suivantes :
* file_id : id du fichier * file_id : id du fichier
* ocr : * ocr :
@@ -37,35 +40,30 @@ Le jeu de données utilisé est `PleIAs/French-PD-Books` ([voir sur Hugging Face
* character_count : nombre de personnages mentionnés dans le libre (fictifs ou réels) * character_count : nombre de personnages mentionnés dans le libre (fictifs ou réels)
* complete_text : texte entier du livre * complete_text : texte entier du livre
**A noter** : les textes proviennent de scans OCR. Ils contiennent donc beaucoup de bruit : éléments de pagination (numéros de page, entêtes, pieds de page), sauts de ligne et retours à la ligne, coupures de mots au passage de ligne, caractères d'échappement, et parfois des erreurs d'OCR. Une étape de prétraitement robuste est donc indispensable avant toute modélisation. **A noter :** les textes proviennent de scans OCR. Ils contiennent donc beaucoup de bruit : éléments de pagination (numéros de page, entêtes, pieds de page), sauts de ligne et retours à la ligne, coupures de mots au passage de ligne, caractères d'échappement, et parfois des erreurs d'OCR. Une étape de prétraitement robuste est donc indispensable avant toute modélisation..
### Dates #### Les dates
Les dates ne sont pas toutes homogènes, on retrouve les formats suivants : Les dates ne sont pas toutes homogènes, on retrouve les formats suivants :
* une année : *1860* * une année : 1860
* un interval d'années : *1929-1931* * un interval d'années : 1929-1931
* interval d'années avec valeur manquante : *1876-????* ou *????-1876* * interval d'années avec valeur manquante : 1876-???? ou ????-1876
Puisque l'objectif est de classifier les livres par date de parution, la donnée importante en plus du texte lui même est la date. Voici un graph illustrant la **répartition des exemples dans le temps**. #### La taille du jeu de données
![Répartition des exemples dans le temps](images/repartition-exemples-dans-le-temps.png) Le jeu de données est très volumineux. On voit sur le graphique ci-dessous une forte concentration de textes sur certaines périodes.
On constate que ce n'est pas très bien répartis, cela pourrait altérer les performamces. Dans un premier temps je n'en tiens pas compte, j'y reviendrais plutard. ![Répartition des textes dans le temps](images/repartition-exemples-dans-le-temps.png)
## Prétraitement des textes Compte tenu du temps de prétraitement et d'entraînement, j'ai effectué mes expérimentations sur un **sous-ensemble de 5000 textes**.
Avant de construire des représentations et d'entraîner des modèles, j'ai appliqué plusieurs étapes de nettoyage : ### 3.2. Prétraitement des textes
* suppression des sauts de ligne et des retours à la ligne, remplacement par des espaces, Deux fonctions de nettoyage ont été testées. La première (`clean_text_old`) était une tentative de nettoyage en profondeur à base de regex.
* élimination des caractères non alphabétiques (en conservant les lettres accentuées françaises),
* suppression partielle des numéros de page
La suppression des numéros de pages est une étape délicate car leur mise en forme dépends de l'ouvrage et de l'éditeur le plus souvent. J'ai quand même repéré un écriture récurente : *-- [NUMERO DE PAGE] --*. J'ai supprimé ces cas là. ```python
# Ancienne version
Voici le code qui permet ce tratement :
```py
def clean_text(example): def clean_text(example):
text = example["complete_text"] text = example["complete_text"]
date = example.get("date", None) date = example.get("date", None)
@@ -113,24 +111,84 @@ def clean_text(example):
return {"text": text, "date": str(date)} return {"text": text, "date": str(date)}
``` ```
Soit dit en passant, cette étape est très longue et pour réduire ce temps de traitement et aller plus vite sur la classification elle même, j'ai travaillé sur un echantillon du jeu de données. Ici, les traitements appliqués sont :
```py * suppression des sauts de ligne et des retours à la ligne, remplacement par des espaces,
reduced_ds = ds['train'].shuffle(seed=42).select(range(5000)) # Echantillon réduit * élimination des caractères non alphabétiques (en conservant les lettres accentuées françaises),
cleaned_ds = reduced_ds.map(clean_text, remove_columns=reduced_ds.column_names) * suppression partielle des numéros de page
La suppression des numéros de pages est une étape délicate car leur mise en forme dépends de l'ouvrage et de l'éditeur le plus souvent. J'ai quand même repéré un écriture récurente : -- [NUMERO DE PAGE] --. J'ai supprimé ces cas là.
Cette fonction s'est avérée complexe et pas nécessairement plus performante.
Pour l'approche par embeddings, j'ai opté pour une version simplifiée, se concentrant sur la miniscule, la suppression de la ponctuation et des stopwords.
```python
# Version actuelle (depuis work.ipynb)
def clean_text(example):
"""
Nettoie le texte d'entrée
"""
text = example["complete_text"]
date = example.get("date", None)
# --- Nettoyage de la date
if "-" in str(date) and date is not None:
parts = str(date).split("-")
if (parts[1].isdigit() and len(parts[1]) == 4) and parts[0] == "????"
date = str(parts[1])
else:
date = str(parts[0])
# --- Nettoyage de texte
text = (text.replace("\\\\n", " ")
.replace("\\\\r", " ")
.replace("\\\\t", " "))
text = text.lower()
text = re.sub(r"[^a-zàâäæçéèêëïîôùûüœ\\s]", " ", text)
text = re.sub(r"\\s+", " ", text).strip()
words = text.split()
filtered_words = [word for word in words if word not in french_stopwords]
text = " ".join(filtered_words)
return {"text": text, "date": str(date)}
``` ```
## Méthodes de classification essayées Les stopwords utilisés ont été générés par Gemini après lui avoir donné les nuages de mots qui viendrons [plutard dans le rapport](#62-analyse-qualitative-nuages-de-mots) :
J'ai essayé les méthodes par TF-IDF et par embeddings avec un modèle CamemBERT-base (environ 100M de paramètres).
Mais d'abord il y a une étape de préparation des données.
Pour grouper les données facilement, j'ai fais une méthode `create_period_label`.
J'ai groupé les exemples par période pour faire l'entrainement dessus.
```py ```py
french_stopwords = set([
'a', 'ai', 'aie', 'aient', 'aies', 'ait', 'alors', 'as', 'au', 'aucun', 'aura', 'aurai', 'auraient', 'aurais', 'aurait', 'auras', 'aurez', 'auriez', 'aurions', 'aurons', 'auront', 'aussi', 'autre', 'aux', 'avaient', 'avais', 'avait', 'avant', 'avec', 'avez', 'aviez', 'avions', 'avoir', 'avons', 'ayant', 'ayez', 'ayons',
'bon',
'c', 'ce', 'ceci', 'cela', 'ces', 'cet', 'cette', 'chaque', 'comme', 'comment',
'd', 'dans', 'de', 'des', 'deux', 'donc', 'dont', 'du',
'elle', 'en', 'encore', 'es', 'est', 'et', 'etaient', 'etais', 'etait', 'etant', 'ete', 'etes', 'etiez', 'etions', 'etre', 'eu', 'eue', 'eues', 'eurent', 'eus', 'eusse', 'eussent', 'eusses', 'eussiez', 'eussions', 'eut', 'eux', 'eûmes', 'eût', 'eûtes',
'fait', 'fais', 'faisaient', 'faisais', 'faisait', 'faisant', 'faire', 'faites', 'fasse', 'fassent', 'fasses', 'fassiez', 'fassions', 'faut', 'fi', 'font', 'force', 'furent', 'fus', 'fusse', 'fussent', 'fusses', 'fussiez', 'fussions', 'fut', 'fûmes', 'fût', 'fûtes',
'hors',
'i', 'ici', 'il', 'ils',
'j', 'je',
'l', 'la', 'le', 'les', 'leur', 'leurs', 'lui',
'm', 'ma', 'mais', 'me', 'mes', 'moi', 'mon',
'n', 'ne', 'ni', 'nos', 'notre', 'nous',
'on', 'ont', 'ou', '',
'par', 'pas', 'pendant', 'peu', 'peut', 'peux', 'plus', 'point', 'pour', 'pourquoi',
'qu', 'quand', 'que', 'quel', 'quelle', 'quelles', 'quels', 'qui',
's', 'sa', 'sans', 'se', 'sera', 'serai', 'seraient', 'serais', 'serait', 'seras', 'serez', 'seriez', 'serions', 'serons', 'seront', 'ses', 'soi', 'soient', 'sois', 'soit', 'sommes', 'son', 'sont', 'soyez', 'soyons', 'suis', 'sur',
't', 'ta', 'te', 'tes', 'toi', 'ton', 'tous', 'tout', 'tu', 'un', 'une',
'va', 'vers', 'voici', 'voilà', 'vos', 'votre', 'vous',
'y', 'à'
])
```
### 3.3. Création des labels
Pour la classification, j'ai groupé les textes par périodes de 10 ans et 50 ans.
```python
def create_period_label(example, period_length=50): def create_period_label(example, period_length=50):
""" """
Crée une étiquette de période basée sur l'année de publication. Crée une étiquette de période basée sur l'année de publication.
@@ -147,115 +205,41 @@ def create_period_label(example, period_length=50):
dataset_with_labels = cleaned_ds.map(create_period_label) dataset_with_labels = cleaned_ds.map(create_period_label)
``` ```
## 4\. Méthodes de classification (Première approche)
Ma première approche a servi de base. J'ai utilisé un `TfidfVectorizer` pour transformer le texte en vecteurs de fréquence de mots, puis entraîné un `SGDClassifier`.
### Observations initiales ```python
def train_and_evaluate_tfidf(dataset, period_length_value):
# Séparer en ensembles d'entraînement (80%) et de test (20%)
train_test_split = dataset_with_labels.train_test_split(test_size=0.2, seed=42)
train_dataset = train_test_split['train']
test_dataset = train_test_split['test']
## Recalibrage et optimisations # Extraire les textes et les labels pour une utilisation plus simple
train_texts = [ex['text'] for ex in train_dataset]
test_texts = [ex['text'] for ex in test_dataset]
train_labels = [ex['decade'] for ex in train_dataset]
test_labels = [ex['decade'] for ex in test_dataset]
Après ce premier aperçu, plusieurs choix d'optimisation ont été faits : # Vectorisation
# Dans cette premiere etape le pretraitement n'exclus pas les stopwords
tfidf_vectorizer = TfidfVectorizer(max_features=5000, stop_words=None, min_df=3, max_df=0.85)
X_train_tfidf = tfidf_vectorizer.fit_transform(X_train)
X_test_tfidf = tfidf_vectorizer.transform(X_test)
* augmentation de la qualité des données par radicalisation du prétraitement des données # Entraînement
* élargissement de la granularité temporelle : plutôt que prévoir par décénies, j'ai augmenté en périodes de 50 ans. Ce choix vise à capter des tendances lexicale et stylistiques plutôt que des variations annuelles insignifiantes. sgd_classifier = SGDClassifier(loss='log_loss', random_state=42, max_iter=1000, tol=1e-3)
* changement de la métrique d'évaluation : j'ai utilisé la différence moyenne entre la date réelle et la date prédite (MAE temporelle en années) — c'est plus pertinent que la précision (*accuracy*). sgd_classifier.fit(X_train_tfidf, y_train)
**En effet, je cherche à m'approcher de la date réelle et non d'être exacte.** # Évaluation
y_pred_sgd = sgd_classifier.predict(X_test_tfidf)
accuracy = accuracy_score(y_test, y_pred_sgd)
* recherche de la plage d'années la plus pertinente pour la clarification. Avec cela j'espère pourvoir améliorer les performances et retrouver des périodes qui s'approchent des mouvements littéraires connus. print(f"Accuracy: {accuracy:.4f}")
print(classification_report(y_test, y_pred_sgd, zero_division=0))
### Pré traitement plus radical return sgd_classifier, tfidf_vectorizer, X_test, y_test
Malgrés le prétraitement déja appliqué aux données, il reste beaucoup d'impuretés. En plus de cela le prétraitement était de loins l'étape la plus longue du processus de classification car je cherchais des expressions dans les textes pour les enlever.
Pour optimiser ce temps et l'efficacité de l'entrainement, j'ia simplifié les critères de prétraitement :
* traitement des dates identiques au précedant
* suppression de tous les sauts de lignes et tabulations
* normalisation des caractères en minuscule
* suppression de tous les caractère non alphabetique (comme précédemment)
**Résultat :** tous les textes sont réduit à une suite de mots spérés par des espaces.
```py
def clean_text(example):
"""
Nettoie le texte d'entrée
"""
text = example["complete_text"]
date = example.get("date", None)
# --- Nettoyage de la date
if "-" in str(date) and date is not None:
parts = str(date).split("-")
if (parts[1].isdigit() and len(parts[1]) == 4) and parts[0] == "????":
date = str(parts[1])
else:
date = str(parts[0])
# --- Nettoyage de texte
text = (text.replace("\\n", " ")
.replace("\\r", " ")
.replace("\\t", " "))
text = text.lower()
text = re.sub(r"[^a-zàâäæçéèêëïîôùûüœ\s]", " ", text)
text = re.sub(r"\s+", " ", text).strip()
words = text.split()
filtered_words = [word for word in words if word not in french_stopwords]
text = " ".join(filtered_words)
return {"text": text, "date": str(date)}
``` ```
### Impacte de la granularité temporelle
### Visualisations des différences entre périodes
Pour comprendre quelles caractéristiques distinguent les périodes, j'ai généré des nuages de mots par période. J'ai effectué un calcul de TFIDF par groupe temporel, puis une génération d'un nuage de mots à partir des mots les plus importants (somme des scores TFIDF sur la période).
```py
# Paramètres pour TF-IDF et Word Cloud
max_features_tfidf = 5000
min_df_tfidf = 3 # Ignorer les mots trop rares
max_df_tfidf = 0.85 # Ignorer les mots trop fréquents
top_n_words = 100
# Calculer le TF-IDF et générer les nuages pour chaque période
for period, texts in grouped_texts.items():
try:
tfidf_vectorizer_period = TfidfVectorizer(
max_features=max_features_tfidf,
min_df=min_df_tfidf,
max_df=max_df_tfidf,
stop_words=None
)
tfidf_matrix = tfidf_vectorizer_period.fit_transform(texts)
# Calculer le score TF-IDF total pour chaque mot sur l'ensemble des textes de la période
# Somme des scores TF-IDF pour chaque terme sur tous les documents de la période
sum_tfidf = tfidf_matrix.sum(axis=0)
tfidf_scores = [(feature_names[col], sum_tfidf[0, col]) for col in range(sum_tfidf.shape[1])]
# Trier les mots par score TF-IDF décroissant
tfidf_scores.sort(key=lambda x: x[1], reverse=True)
top_words_scores = dict(tfidf_scores[:top_n_words])
wordcloud = WordCloud(width=800, height=400, background_color='white').generate_from_frequencies(top_words_scores)
```
Voici le résultat par période de *50 ans*.
![Nuages de mots par période de 50 ans](images/nuages-mots-periode-50-ans.png)
![Prédiction des période (50 ans)](images/prediction-periode-50-ans.png)
+5 -3
View File
@@ -445,7 +445,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 35, "execution_count": null,
"id": "2f38c125", "id": "2f38c125",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -540,8 +540,7 @@
"\n", "\n",
" return \" \".join(sorted_tokens)\n", " return \" \".join(sorted_tokens)\n",
"\n", "\n",
"# Charger un modèle CamemBERT léger, spécialisé dans la création d'embeddings\n", "\n",
"embedding_model = SentenceTransformer('dangvantuan/sentence-camembert-base')\n",
"\n", "\n",
"# Fonction pour créer les embeddings par lots (pour optimiser l'usage de la RAM)\n", "# Fonction pour créer les embeddings par lots (pour optimiser l'usage de la RAM)\n",
"def create_embeddings(texts, selection_method=\"start_tokens\", batch_size=32):\n", "def create_embeddings(texts, selection_method=\"start_tokens\", batch_size=32):\n",
@@ -557,6 +556,9 @@
" normalize_embeddings=True\n", " normalize_embeddings=True\n",
" )\n", " )\n",
"\n", "\n",
"# Charger un modèle CamemBERT léger, spécialisé dans la création d'embeddings\n",
"embedding_model = SentenceTransformer('dangvantuan/sentence-camembert-base')\n",
"\n",
"# Recréer les listes de textes car elles ont été supprimées\n", "# Recréer les listes de textes car elles ont été supprimées\n",
"# Assurez-vous que les variables `train_dataset` et `test_dataset` sont bien définies en exécutant les cellules précédentes.\n", "# Assurez-vous que les variables `train_dataset` et `test_dataset` sont bien définies en exécutant les cellules précédentes.\n",
"train_texts = [ex['text'] for ex in train_dataset]\n", "train_texts = [ex['text'] for ex in train_dataset]\n",