Découvrir Bertopic et le pouvoir des encodeurs
2026-07-05
Une réflexion initiée autour d’un tutorial The General Inquirer in the time of LLMs: a BERTopic tutorial
Qui a déjà fait du topic modeling ? Qui a de l’expérience avec les méthodes de NLP ?
Qui a des notions en Python ?
Qui est à l’aise avec un notebook ?
Avez-vous déjà en tête des objectifs ?
Cas d’étude de la séance:
Identifier les principaux thèmes de recherche à partir des résumés de thèses défendues en France.
Une interface quanti (corpus) / quali (thèmes)
« BERTopic was ranked first by 8 out of 12 participants (67%), LDA by 3 out of 12 participants (25%), and NMF by 1 out of 12 participants (8%). » (Kaur et Wallace, 2024, p. 11) (pdf)
Tip
Exemple de représentation de textes par sac de mots:
| cat | cutest | offer | premium | food | |
|---|---|---|---|---|---|
| doc 1 | 1 | 1 | 0 | 0 | 0 |
| doc 2 | 1 | 0 | 1 | 1 | 1 |
A Visual Guide to Using BERT for the First Time - Jay Alammar, source
Différence majeures:
On ne se contente plus d’observer la présence de mots, on cherche à identifier leur sens dans leur contexte.

Un projet qui facilite la manipulation de ces distances sémantiques pour extraire des thèmes (et faire plein d’autres choses : visualiser, etc.). Ce projet rassemble :
sentence-transformers)Un pipeline de méthodes de machine learning
Et donc de s’adapter aux enjeux spécifiques du traitement
« they valued BERTopic’s ability to uncover hidden connections, emphasizing the need for meaningful, comprehensive analysis tools that support their research objectives and enhance data interpretation » (Kaur et Wallace, 2024, p. 2) (pdf)
embeddings.zipRetrouver les disciplines explorées par les thèses défendues en France grâce au topic modelling.
import pandas as pd
from bertopic import BERTopic
df = pd.read_csv("./theses-soutenues-curated-stratified.csv")
docs = df["resumes.fr"].sample(1000).to_list()
topic_model = BERTopic(language="french")
topic_model.fit(documents=docs)
On cherche à afficher les thèmes identifiés, et les représenter avec des mots clefs.
| Topic | Count | Representation |
|---|---|---|
| -1 | 302 | [‘de’, ‘la’, ‘des’, ‘et’, ‘les’, ‘le’, ‘une’, ‘en’, ‘dans’, ‘un’] |
| 0 | 178 | [‘de’, ‘la’, ‘des’, ‘et’, ‘les’, ‘le’, ‘dans’, ‘une’, ‘un’, ‘en’] |
| 1 | 117 | [‘de’, ‘la’, ‘et’, ‘les’, ‘des’, ‘le’, ‘du’, ‘en’, ‘un’, ‘une’] |
| 2 | 108 | [‘de’, ‘des’, ‘la’, ‘les’, ‘et’, ‘une’, ‘le’, ‘un’, ‘pour’, ‘en’] |
| 3 | 100 | [‘de’, ‘des’, ‘la’, ‘et’, ‘en’, ‘les’, ‘été’, ‘dans’, ‘le’, ‘une’] |
| 4 | 49 | [‘de’, ‘des’, ‘et’, ‘les’, ‘la’, ‘en’, ‘du’, ‘une’, ‘le’, ‘sur’] |
| 5 | 48 | [‘de’, ‘et’, ‘la’, ‘les’, ‘des’, ‘une’, ‘en’, ‘le’, ‘dans’, ‘du’] |
| 6 | 41 | [‘la’, ‘de’, ‘et’, ‘le’, ‘du’, ‘les’, ‘une’, ‘des’, ‘en’, ‘dans’] |
| 7 | 31 | [‘la’, ‘de’, ‘et’, ‘les’, ‘le’, ‘des’, ‘en’, ‘du’, ‘langue’, ‘dans’] |
| 8 | 13 | [‘de’, ‘la’, ‘des’, ‘et’, ‘les’, ‘en’, ‘une’, ‘du’, ‘le’, ‘au’] |
| 9 | 13 | [‘de’, ‘la’, ‘et’, ‘un’, ‘les’, ‘pression’, ‘le’, ‘une’, ‘en’, ‘des’] |
On cherche à afficher les documents dans un espace 2D pour identifier la taille des clusters et leurs position relative.
On cherche à afficher les mots clefs de chaque thème pour facilement les comparer
On cherche à identifier la structure interne au modèle et les proximités entre les thèmes et comment ils s’emboitent.
Cette visualisation ouvre la question de la fusion de thèmes qui est rendu possible par BERTopic.
On en parle au cas pas cas.
# code_oai = "ddc:300" # "Sciences sociales, sociologie, anthropologie",
# code_oai = "ddc:340" # "Droit",
# code_oai = "ddc:004" # "Informatique",
# code_oai = "ddc:570" # "Sciences de la vie, biologie, biochimie",
# code_oai = "ddc:540" # "Chimie, minéralogie, cristallographie",
# code_oai = "ddc:620" # "Sciences de l'ingénieur",
# code_oai = "ddc:550" # "Sciences de la terre",
code_oai = "ddc:530" # "Physique",
# code_oai = "ddc:510" # "Mathématiques",
# code_oai = "ddc:610" # "Médecine et santé"
doc_contains_code_oai = df_sample["oai_set_specs"].apply(lambda s: code_oai in s.split("||"))
topics_per_class = topic_model.topics_per_class(docs, classes=doc_contains_code_oai)
topic_model.visualize_topics_per_class(topics_per_class)Après affinage et vérification, on obtient les résultats suivants :
Le nerf de la guerre: la représentation sémantique des textes.
Avec les mains: un modèle est une fonction qui accepte des textes et renvoie une représentation numérique qui encapsule la sémantique du texte.
Les détails d’importance
Nouveau notebook avance: https://shorturl.at/3lko9
On tire avantage du pré-calcul des plongements et on les fournit au topic model:
from datasets import load_from_disk
import numpy as np
ds = load_from_disk(f"./embeddings/all-MiniLM-L6-v2-fr-SBERT")
docs = np.array(ds[f"resumes.fr"]) # 6500 rows
embeddings = np.array(ds["embedding"]) # Shape: 6500 x 768
topic_model = BERTopic(language = "french")
topic_model.fit(documents=docs, embeddings=embeddings)Les résultats sont les mêmes, mais le temps de calcul se voit fortement réduit!






Pour la suite on va utiliser gte-multilingual-base-fr-SBERT.
n_neighors et n_components (conserver min_dist=0.).UMAP Dimension Reduction, Main Ideas!!! - StatQuest with Josh Starmer source
Ressource pour jouer avec les paramètres: Understanding UMAP
from umap import UMAP
ds = load_from_disk(f"./embeddings/all-MiniLM-L6-v2-fr-SBERT")
docs = np.array(ds[f"resumes.fr"]) # 6500 rows
embeddings = np.array(ds["embedding"]) # Shape: 6500 x 768
umap_model = UMAP(
n_neighbors = 50,
# Default parameters
metric = "cosine",
n_components = 5,
min_dist=0.0,
low_memory = False
)
topic_model = BERTopic(language = "french", umap_model= umap_model)
topic_model.fit(documents=docs, embeddings=embeddings)n_neighbors
n_neighbors = 5
n_neighbors = 15 (default)
n_neighbors = 300Warning
Attention, en fonction de la taille du corpus, une valeur “raisonnable” de n_neighbors va changer !
🚨Attention🚨 ici la représentation visuelle ne change pas, malgré qu’on ai changé n_neighbors. C’est normal, en réalité il y a 2 UMAP:
n_neighbors choisie, et dont les vecteurs résultants (en 5D), et sur lequel on applique l’algorithme de clustering.topic_model.visualize_documents, qui réduit les embeddings à 2 dimensions, et utilise n_neighbors=10. D’où le fait que la visualisation ne bouge pas.n_components
min_cluster_size.HDBSCAN, Fast Density Based Clustering, the How and the Why - John Healy source
Ressource pour comprendre HDBSCAN: Documentation HDBSCAN - How HDBSCAN Works
from hdbscan import HDBSCAN
ds = load_from_disk(f"./embeddings/all-MiniLM-L6-v2-fr-SBERT")
docs = np.array(ds[f"resumes.fr"]) # 6500 rows
embeddings = np.array(ds["embedding"]) # Shape: 6500 x 768
hdbscan_model = HDBSCAN(
min_cluster_size=50,
# Default parameters
prediction_data=True
)
topic_model = BERTopic(language = "french", hdbscan_model=hdbscan_model)
topic_model.fit(documents=docs, embeddings=embeddings)min_cluster_size
min_cluster_size = 5
min_cluster_size = 10 (default)
min_cluster_size = 50Warning
Attention, en fonction de la taille du corpus, une valeur “raisonnable” de min_cluster_size va changer !
n_gram_range=(1, 1) ou (1,2)).from sklearn.feature_extraction.text import CountVectorizer
from stopwordsiso import stopwords
ds = load_from_disk(f"./embeddings/all-MiniLM-L6-v2-fr-SBERT")
docs = np.array(ds[f"resumes.fr"]) # 6500 rows
embeddings = np.array(ds["embedding"]) # Shape: 6500 x 768
vectorizer_model = CountVectorizer(stop_words = list(stopwords("fr")))
topic_model = BERTopic(language = "french", vectorizer_model = vectorizer_model)
topic_model.fit(documents=docs, embeddings=embeddings)

Une histoire compliquée « standard metrics may not reliably reflect human perception of coherence in specialized fields » (Prouteau et al., 2026, p. 8) (pdf)
Comment découper son corpus ?