📝 : Update and restructuring README.md
This commit is contained in:
@@ -1,31 +1,34 @@
|
||||
## TODO LIST
|
||||
|
||||
- [ ] 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
|
||||
# Projet IA5 — Étude : classification des textes par époque
|
||||
|
||||
*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.
|
||||
- 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.
|
||||
Je cherche à répondre à la question : "Est-il possible de reconnaître l'époque d'un texte grâce à ses mots et à sa construction ?".
|
||||
|
||||
**Problématique :** est‑il possible de reconnaître l'époque d'un texte grâce à ses mots et à sa construction, et jusqu'à quel degré de précision ? Peut‑on 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
|
||||
* 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)
|
||||
* 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, en‑tê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, en‑tê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 :
|
||||
|
||||
* une année : *1860*
|
||||
* un interval d'années : *1929-1931*
|
||||
* interval d'années avec valeur manquante : *1876-????* ou *????-1876*
|
||||
* une année : 1860
|
||||
* un interval d'années : 1929-1931
|
||||
* 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
|
||||
|
||||

|
||||
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.
|
||||

|
||||
|
||||
## 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,
|
||||
* élimination des caractères non alphabétiques (en conservant les lettres accentuées françaises),
|
||||
* suppression partielle des numéros de page
|
||||
Deux fonctions de nettoyage ont été testées. La première (`clean_text_old`) était une tentative de nettoyage en profondeur à base de regex.
|
||||
|
||||
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à.
|
||||
|
||||
Voici le code qui permet ce tratement :
|
||||
|
||||
```py
|
||||
```python
|
||||
# Ancienne version
|
||||
def clean_text(example):
|
||||
text = example["complete_text"]
|
||||
date = example.get("date", None)
|
||||
@@ -113,24 +111,84 @@ def clean_text(example):
|
||||
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
|
||||
reduced_ds = ds['train'].shuffle(seed=42).select(range(5000)) # Echantillon réduit
|
||||
cleaned_ds = reduced_ds.map(clean_text, remove_columns=reduced_ds.column_names)
|
||||
* suppression des sauts de ligne et des retours à la ligne, remplacement par des espaces,
|
||||
* é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à.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
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) :
|
||||
|
||||
```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', 'où',
|
||||
'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):
|
||||
"""
|
||||
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)
|
||||
```
|
||||
|
||||
## 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
|
||||
* é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.
|
||||
* 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*).
|
||||
# Entraînement
|
||||
sgd_classifier = SGDClassifier(loss='log_loss', random_state=42, max_iter=1000, tol=1e-3)
|
||||
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
|
||||
|
||||
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)}
|
||||
return sgd_classifier, tfidf_vectorizer, X_test, y_test
|
||||
```
|
||||
|
||||
### 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 TF‑IDF par groupe temporel, puis une génération d'un nuage de mots à partir des mots les plus importants (somme des scores TF‑IDF 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*.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||

|
||||
+5
-3
@@ -445,7 +445,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 35,
|
||||
"execution_count": null,
|
||||
"id": "2f38c125",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -540,8 +540,7 @@
|
||||
"\n",
|
||||
" return \" \".join(sorted_tokens)\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",
|
||||
"\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",
|
||||
@@ -557,6 +556,9 @@
|
||||
" normalize_embeddings=True\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",
|
||||
"# 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",
|
||||
|
||||
Reference in New Issue
Block a user