Le volume en constante augmentation des publications de recherche nécessite des méthodes efficaces pour structurer les connaissances académiques. Cette tâche implique généralement de développer un schéma sous-jacent sous-jacent supervisé et d'allorer les publications à la classe la plus pertinente. Dans cet article, nous mettons en œuvre une solution automatisée de bout en bout à l'aide de la quantification d'intégration et un pipeline de modèles de langage grand (LLM). Notre étude de cas commence par un ensemble de données de 25 000 publications ArXIV à partir de la linguistique informatique (CS.CL), publiée avant juillet 2024, que nous organisons dans un nouveau schéma de classes.
Notre approche se concentre sur trois tâches clés: (i) clustering non supervisé de l'ensemble de données ArXIV dans des collections connexes, (ii) découvrir les structures thématiques latentes au sein de chaque cluster, et (iii) créer un système de taxonomie candidate basé sur lesdites structures thématiques.
À la base, la tâche de clustering nécessite d'identifier un nombre suffisant d'exemples similaires dans un ensemble de données non étiqueté . Il s'agit d'une tâche naturelle pour les intérêts, car ils capturent les relations sémantiques dans un corpus et peuvent être fournies en tant que caractéristiques d'entrée à un algorithme de clustering pour établir des liens de similitude entre les exemples. Nous commençons par transformer les paires ( Titre : Résumé ) de notre ensemble de données en une représentation d'intégration à l'aide de Jina-Embeddings-V2, un modèle d'attention basé sur Bert-Alibi. Et l'application de la quantification scalaire à l'aide des transformateurs de phrases et d'une implémentation personnalisée.
Pour le clustering, nous exécutons HDBSCAN dans un espace dimensionnel réduit, en comparant les résultats en utilisant des méthodes de clustering eom et leaf . De plus, nous examinons si l'utilisation de la quantification (u)int8 au lieu des représentations float32 affecte ce processus.
Pour découvrir des sujets latents au sein de chaque groupe de publications ArXIV, nous combinons Langchain et Pydontic avec Mistral-7B-Instruct-V0.3 (et GPT-4O, inclus pour comparaison) dans un LLM-Pipeline. La sortie est ensuite incorporée dans un modèle invite raffiné qui guide Claude Sonnet 3.5 pour générer une taxonomie hiérarchique.
Les résultats font allusion à 35 sujets de recherche émergents, dans lesquels chaque sujet comprend au moins 100 publications. Ceux-ci sont organisés dans les 7 classes parentales dans le domaine de la linguistique informatique (CS.CL). Cette approche peut servir de référence pour générer automatiquement des schémas de candidats hiérarchiques dans les catégories ArXIV de haut niveau et compléter efficacement les taxonomies, relevant le défi posé par le volume croissant de la littérature scolaire.
Taxonomie Achèvement de la littérature académique avec quantification d'intégration et un LLM-Pipeline
Les intégres sont des représentations numériques d'objets du monde réel comme le texte, les images et l'audio qui résument les informations sémantiques des données qu'elles représentent. Ils sont utilisés par les modèles d'IA pour comprendre les domaines de connaissances complexes dans des applications en aval telles que le clustering, la récupération de l'information et les tâches de compréhension sémantique, entre autres.
Nous cartographierons ( Titre : Résumé ) les paires des publications ARXIV à un espace 768 dimensionnel utilisant Jina-Embeddings-V2 [1], un modèle d'intégration de texte open source capable d'accueillir jusqu'à 8192 jetons. Cela fournit une longueur de séquence suffisamment grande pour les titres, les résumés et autres sections de documents qui pourraient être pertinentes. Pour surmonter la limite conventionnelle de 512-token présents dans d'autres modèles, Jina-Embeddings-V2 intègre l'alibi bidirectionnel [2] dans le cadre Bert. L'alibi (attention avec les biais linéaires) permet l'extrapolation de la longueur d'entrée (c'est-à-dire, des séquences dépassant 2048 jetons) en codant pour les informations de position directement dans la couche d'auto-addition, au lieu d'introduire des incorporations de position. Dans la pratique, il biaise les scores d'attention de la cure de requête avec une pénalité proportionnelle à leur distance, favorisant une attention mutuelle plus forte entre les jetons proximes.
La première étape de l'utilisation du modèle Jina-Embeddings-V2 consiste à le charger via des transformateurs de phrases, un cadre pour accéder aux modèles de pointe qui sont disponibles sur le centre de face étreint:
from sentence_transformers import SentenceTransformer
model = SentenceTransformer ( 'jinaai/jina-embeddings-v2-base-en' , trust_remote_code = True ) Nous codant maintenant ( Titre : Résumé ) des paires de notre ensemble de données à l'aide de batch_size = 64 . Cela permet un calcul parallèle sur des accélérateurs matériels comme les GPU (bien qu'au prix de nécessiter plus de mémoire):
from datasets import load_dataset
ds = load_dataset ( "dcarpintero/arxiv.cs.CL.25k" , split = "train" )
corpus = [ title + ':' + abstract for title , abstract in zip ( ds [ 'title' ], ds [ 'abstract' ])]
f32_embeddings = model . encode ( corpus ,
batch_size = 64 ,
show_progress_bar = True )La similitude sémantique entre les corpus peut désormais être calculée trivialement comme le produit intérieur des intérêts. Dans la carte thermique suivante, chaque entrée [x, y] est colorée en fonction de ladite phrase intégrée pour les phrases « Title » exemplaires [x] et [y].
Simant similaire dans les titres arxiv CS.Cl en utilisant des intégres
La mise à l'échelle des intérêts peut être difficile. Actuellement, les modèles de pointe représentent chaque intégration comme float32 , qui nécessite 4 octets de mémoire. Étant donné que Jina-Embeddings-V2 mappe le texte à un espace à 768 dimensions, les exigences de mémoire pour notre ensemble de données seraient d'environ 73 Mo, sans index et autres métadonnées liées aux dossiers de publication:
25 , 000 embeddings * 768 dimensions / embedding * 4 bytes / dimension = 76 , 800 , 000 bytes
76 , 800 , 000 bytes / ( 1024 ^ 2 ) ≈ 73.24 MBCependant, travailler avec un ensemble de données plus important pourrait augmenter considérablement les exigences de mémoire et les coûts associés:
| Intégration Dimension | Intégration Modèle | 2,5 m Abstracts Arxiv | 60,9 m Pages wikipedia | 100m Incorporer |
|---|---|---|---|---|
| 384 | All-Minilm-L12-V2 | 3,57 Go | 85,26 Go | 142.88 Go |
| 768 | All-Mpnet-Base-V2 | 7,15 Go | 170,52 Go | 285,76 Go |
| 768 | Jina-Embeddings-V2 | 7,15 Go | 170,52 Go | 285,76 Go |
| 1536 | Openai-Text-Embedding-3-Small | 14,31 Go | 341.04 Go | 571,53 Go |
| 3072 | Openai-Text-Embedding-3-Garg | 28,61 Go | 682.08 Go | 1,143 TB |
Une technique utilisée pour réaliser la mémoire est la quantification . L'intuition derrière cette approche est que nous pouvons discrétiser les valeurs à virgule flottante en mappant leur plage [ f_max , f_min ] en une plus petite gamme de nombres à point fixe [ q_max , q_min ], et distribuant linéairement toutes les valeurs entre ces plages. En pratique, cela réduit généralement la précision d'un point flottant 32 bits à des largeurs de bit inférieurs comme 8 bits (quantification scalaire) ou des valeurs 1 bits (quantification binaire).
Quantification d'intégration scalaire - De Float32 à (U) INT8
En traçant la distribution de fréquence des intérêts générés par la jina , nous observons que les valeurs sont en effet concentrées autour d'une plage relativement étroite [-2.0, +2,0]. Cela signifie que nous pouvons mapper efficacement les valeurs float32 à 256 (u)int8 sans perte significative d'informations:
import matplotlib . pyplot as plt
plt . hist ( f32_embeddings . flatten (), bins = 250 , edgecolor = 'C0' )
plt . xlabel ( 'float-32 jina-embeddings-v2' )
plt . title ( 'distribution' )
plt . show ()Distribution d'origine Float32 Jina-Embeddings-V2
Nous pouvons calculer les valeurs exactes [min, max] de la distribution:
> >> np . min ( f32_embeddings ), np . max ( f32_embeddings )
( - 2.0162134 , 2.074683 ) La première étape pour implémenter la quantification scalaire consiste à définir un ensemble d'étalonnage d'incorporation. Un point de départ typique est un sous-ensemble de 10k intégrés, qui dans notre cas couvrira près de 99,98% des valeurs d'incorporation d'origine float32 . L'utilisation de l'étalonnage est destinée à obtenir des valeurs représentatives f_min et f_max le long de chaque dimension pour réduire les frais généraux de calcul et les problèmes potentiels causés par des valeurs aberrantes qui peuvent apparaître dans des ensembles de données plus importants.
def calibration_accuracy ( embeddings : np . ndarray , k : int = 10000 ) -> float :
calibration_embeddings = embeddings [: k ]
f_min = np . min ( calibration_embeddings , axis = 0 )
f_max = np . max ( calibration_embeddings , axis = 0 )
# Calculate percentage in range for each dimension
size = embeddings . shape [ 0 ]
avg = []
for i in range ( embeddings . shape [ 1 ]):
in_range = np . sum (( embeddings [:, i ] >= f_min [ i ]) & ( embeddings [:, i ] <= f_max [ i ]))
dim_percentage = ( in_range / size ) * 100
avg . append ( dim_percentage )
return np . mean ( avg )
acc = calibration_accuracy ( f32_embeddings , k = 10000 )
print ( f"Average percentage of embeddings within [f_min, f_max] calibration: { acc :.5f } %" )
> >> Average percentage of embeddings within [ f_min , f_max ] calibration : 99.98636 % Les deuxième et troisième étapes de la quantification scalaire - échelles informatiques et point zéro et codage - peuvent être facilement appliquées avec des transformateurs de phrase, ce qui entraîne une enregistrement de mémoire 4x par rapport à la représentation float32 d'origine. De plus, nous bénéficierons également d'opérations arithmétiques plus rapides, car la multiplication matricielle peut être effectuée plus rapidement avec l'arithmétique entier.
from sentence_transformers . quantization import quantize_embeddings
# quantization is applied in a post-processing step
int8_embeddings = quantize_embeddings (
np . array ( f32_embeddings ),
precision = "int8" ,
calibration_embeddings = np . array ( f32_embeddings [: 10000 ]),
) f32_embeddings . dtype , f32_embeddings . shape , f32_embeddings . nbytes
>> > ( dtype ( 'float32' ), ( 25107 , 768 ), 77128704 ) # 73.5 MB
int8_embeddings . dtype , int8_embeddings . shape , int8_embeddings . nbytes
>> > ( dtype ( 'int8' ), ( 25107 , 768 ), 19282176 ) # 18.3 MB
# calculate compression
( f32_embeddings . nbytes - int8_embeddings . nbytes ) / f32_embeddings . nbytes * 100
>> > 75.0Pour l'exhaustivité, nous mettons en œuvre une méthode de quantification scalaire pour illustrer ces trois étapes:
def scalar_quantize_embeddings ( embeddings : np . ndarray ,
calibration_embeddings : np . ndarray ) -> np . ndarray :
# Step 1: Calculate [f_min, f_max] per dimension from the calibration set
f_min = np . min ( calibration_embeddings , axis = 0 )
f_max = np . max ( calibration_embeddings , axis = 0 )
# Step 2: Map [f_min, f_max] to [q_min, q_max] => (scaling factors, zero point)
q_min = 0
q_max = 255
scales = ( f_max - f_min ) / ( q_max - q_min )
zero_point = 0 # uint8 quantization maps inherently min_values to zero
# Step 3: encode (scale, round)
quantized_embeddings = (( embeddings - f_min ) / scales ). astype ( np . uint8 )
return quantized_embeddings calibration_embeddings = f32_embeddings [: 10000 ]
beta_uint8_embeddings = scalar_quantize_embeddings ( f32_embeddings , calibration_embeddings ) beta_uint8_embeddings [ 5000 ][ 64 : 128 ]. reshape ( 8 , 8 )
array ([[ 187 , 111 , 96 , 128 , 116 , 129 , 130 , 122 ],
[ 132 , 153 , 72 , 136 , 94 , 120 , 112 , 93 ],
[ 143 , 121 , 137 , 143 , 195 , 159 , 90 , 93 ],
[ 178 , 189 , 143 , 99 , 99 , 151 , 93 , 102 ],
[ 179 , 104 , 146 , 150 , 176 , 94 , 148 , 118 ],
[ 161 , 138 , 90 , 122 , 93 , 146 , 140 , 129 ],
[ 121 , 115 , 153 , 118 , 107 , 45 , 70 , 171 ],
[ 207 , 53 , 67 , 115 , 223 , 105 , 124 , 158 ]], dtype = uint8 )Nous continuerons avec la version des intérêts qui ont été quantifiés à l'aide de transformateurs de phrases (notre implémentation personnalisée est également incluse dans l'analyse des résultats):
# `f32_embeddings` => if you prefer to not use quantization
# `beta_uint8_embeddings` => to check our custom implemention
embeddings = int8_embeddings Dans cette section, nous effectuons une projection en deux étapes de paires d'intégration ( Titre : Résumé ) de leur espace d'origine à haute dimension (768) à des dimensions plus basses, à savoir:
5 dimensions pour réduire la complexité de calcul pendant le regroupement, et2 dimensions pour activer la représentation visuelle dans les coordonnées (x, y) .Pour les deux projections, nous utilisons UMAP [3], une technique de réduction de la dimensionnalité populaire connue pour son efficacité dans la préservation des structures de données locales et globales. En pratique, cela en fait un choix préféré pour gérer des ensembles de données complexes avec des intégres à haute dimension:
import umap
embedding_5d = umap . UMAP ( n_neighbors = 100 , # consider 100 nearest neighbors for each point
n_components = 5 , # reduce embedding space from 768 to 5 dimensions
min_dist = 0.1 , # maintain local and global balance
metric = 'cosine' ). fit_transform ( embeddings )
embedding_2d = umap . UMAP ( n_neighbors = 100 ,
n_components = 2 ,
min_dist = 0.1 ,
metric = 'cosine' ). fit_transform ( embeddings ) Notez que lorsque nous appliquons le regroupement HDBSCAN à l'étape suivante, les grappes trouvées seront influencées par la façon dont UMAP a conservé les structures locales. Une valeur n_neighbors plus petite signifie que UMAP se concentrera davantage sur les structures locales, tandis qu'une valeur plus grande permet de capturer plus de représentations globales, ce qui pourrait être bénéfique pour comprendre les modèles globaux dans les données.
Les intégres réduits ( titre : abstrait ) peuvent désormais être utilisés comme caractéristiques d'entrée d'un algorithme de clustering, permettant l'identification de catégories connexes en fonction des distances d'intégration.
Nous avons opté pour HDBSCAN (regroupement spatial basé sur la densité hiérarchique des applications avec du bruit) [4], un algorithme de clustering avancé qui étend DBSCAN en s'adaptant à des grappes de densité variables. Contrairement à K-Means qui nécessite la pré-spécification du nombre de clusters, HDBSCAN n'a qu'un seul hyperparamètre important, n , qui établit le nombre minimum d'exemples à inclure dans un cluster.
HDBSCAN fonctionne en transformant d'abord l'espace de données en fonction de la densité des points de données, ce qui rend les régions plus denses (zones où les points de données sont rapprochés en nombre élevé) plus attrayants pour la formation de cluster. L'algorithme construit ensuite une hiérarchie de clusters basée sur la taille minimale du cluster établie par l'hyperparamètre n . Cela lui permet de faire la distinction entre le bruit (zones clairsemé) et les régions denses (grappes potentielles). Enfin, HDBSCAN condense cette hiérarchie pour dériver les grappes les plus persistantes, identifiant des grappes de densités et de formes différentes. En tant que méthode basée sur la densité, il peut également détecter les valeurs aberrantes.
import hdbscan
hdbs = hdbscan . HDBSCAN ( min_cluster_size = 100 , # conservative clusters' size
metric = 'euclidean' , # points distance metric
cluster_selection_method = 'leaf' ) # favour fine grained clustering
clusters = hdbs . fit_predict ( embedding_5d ) # apply HDBSCAN on reduced UMAP Le cluster_selection_method détermine comment HDBSCAN sélectionne les clusters plats dans la hiérarchie des arbres. Dans notre cas, l'utilisation de la méthode de sélection des cluster eom (excès de masse) en combinaison avec la quantification d'intégration avait tendance à créer quelques grappes plus grandes et moins spécifiques. Ces clusters auraient nécessité un autre processus de reclassement pour extraire des sujets latents significatifs. Au lieu de cela, en passant à la méthode de sélection leaf , nous avons guidé l'algorithme pour sélectionner les nœuds de feuilles de la hiérarchie du cluster, qui a produit un clustering à grain plus fin par rapport à l'excès de méthode de masse:
Comparaison de la méthode de clustering HDBSCAN EOM et des feuilles à l'aide de Quantitation INT8-Embedding
Après avoir effectué l'étape de clustering, nous illustrons maintenant comment déduire le sujet latent de chaque cluster en combinant un LLM tel que Mistral-7B-Istruct [5] avec Pydontic et Langchain pour créer un pipeline LLM qui génère la sortie dans un format structuré composable.
Les modèles pydontiques sont des classes qui dérivent de pydantic.BaseModel , définissant les champs comme des attributs annotés de type. Ils sont similaires aux classes de données Python . Cependant, ils ont été conçus avec des différences subtiles mais significatives qui optimisent diverses opérations telles que la validation, la sérialisation et la génération de schéma JSON . Notre classe Topic définit un champ nommé label . Cela générera une sortie LLM dans un format structuré, plutôt qu'un bloc de texte de forme libre, facilitant le traitement et l'analyse plus faciles.
from pydantic import BaseModel , Field
class Topic ( BaseModel ):
"""
Pydantic Model to generate an structured Topic Model
"""
label : str = Field (..., description = "Identified topic" )Les modèles d'invite Langchain sont des recettes prédéfinies pour traduire l'entrée utilisateur et les paramètres en instructions pour un modèle de langue. Nous définissons ici l'invite pour notre tâche prévue:
from langchain_core . prompts import PromptTemplate
topic_prompt = """
You are a helpful research assistant. Your task is to analyze a set of research paper
titles related to Natural Language Processing, and determine the overarching topic.
INSTRUCTIONS:
1. Based on the titles provided, identify the most relevant topic:
- Ensure the topic is concise and clear.
2. Format Respose:
- Ensure the title response is in JSON as in the 'OUTPUT OUTPUT' section below.
- No follow up questions are needed.
OUTPUT FORMAT:
{{"label": "Topic Name"}}
TITLES:
{titles}
""" Composons maintenant un pipeline de modélisation de sujets à l'aide de Langchain Expression Language (LCEL) pour rendre notre modèle d'invite dans l'entrée LLM et analyser la sortie d'inférence en tant que JSON :
from langchain . chains import LLMChain
from langchain_huggingface import HuggingFaceEndpoint
from langchain_core . output_parsers import PydanticOutputParser
from typing import List
def TopicModeling ( titles : List [ str ]) -> str :
"""
Infer the common topic of the given titles w/ LangChain, Pydantic, OpenAI
"""
repo_id = "mistralai/Mistral-7B-Instruct-v0.3"
llm = HuggingFaceEndpoint (
repo_id = repo_id ,
temperature = 0.2 ,
huggingfacehub_api_token = os . environ [ "HUGGINGFACEHUB_API_TOKEN" ]
)
prompt = PromptTemplate . from_template ( topic_prompt )
parser = PydanticOutputParser ( pydantic_object = Topic )
topic_chain = prompt | llm | parser
return topic_chain . invoke ({ "titles" : titles })Pour permettre au modèle de déduire le sujet de chaque cluster, nous incluons un sous-ensemble de 25 titres de papier de chaque cluster dans le cadre de l'entrée LLM:
topics = []
for i , cluster in df . groupby ( 'cluster' ):
titles = cluster [ 'title' ]. sample ( 25 ). tolist ()
topic = TopicModeling ( titles )
topics . append ( topic . label )Attribuons chaque publication ArXIV à son cluster correspondant:
n_clusters = len ( df [ 'cluster' ]. unique ())
topic_map = dict ( zip ( range ( n_clusters ), topics ))
df [ 'topic' ] = df [ 'cluster' ]. map ( topic_map )Pour créer une taxonomie hiérarchique, nous élaborons une invite pour guider Claude Sonnet 3.5 dans l'organisation des sujets de recherche identifiés correspondant à chaque cluster dans un schéma hiérarchique:
from langchain_core . prompts import PromptTemplate
taxonomy_prompt = """
Create a comprehensive and well-structured taxonomy
for the ArXiv cs.CL (Computational Linguistics) category.
This taxonomy should organize subtopics in a logical manner.
INSTRUCTIONS:
1. Review and Refine Subtopics:
- Examine the provided list of subtopics in computational linguistics.
- Ensure each subtopic is clearly defined and distinct from others.
2. Create Definitions:
- For each subtopic, provide a concise definition (1-2 sentences).
3. Develop a Hierarchical Structure:
- Group related subtopics into broader categories.
- Create a multi-level hierarchy, with top-level categories and nested subcategories.
- Ensure that the structure is logical and intuitive for researchers in the field.
4. Validate and Refine:
- Review the entire taxonomy for consistency, completeness, and clarity.
OUTPUT FORMAT:
- Present the final taxonomy in a clear, hierarchical format, with:
. Main categories
.. Subcategories
... Individual topics with their definitions
SUBTOPICS:
{taxonomy_subtopics}
""" Créons un tracé de dispersion interactif:
chart = alt . Chart ( df ). mark_circle ( size = 5 ). encode (
x = 'x' ,
y = 'y' ,
color = 'topic:N' ,
tooltip = [ 'title' , 'topic' ]
). interactive (). properties (
title = 'Clustering and Topic Modeling | 25k arXiv cs.CL publications)' ,
width = 600 ,
height = 400 ,
)
chart . display () Et comparer les résultats de clustering en utilisant des représentations d'intégration float32 et la quantification des transformateurs de phrases int8 :
HDBScan Leaf-Clustering Utilisation de Float32 et intégres quantifiés-INT8 (Transformateurs de phrase-Quantitation)
Nous effectuons maintenant la même comparaison avec notre implémentation de quantification personnalisée:
HDBScan Leaf-Clustering Utilisation de Float32 et intégration de quantification-UInt8 (mise en œuvre sur le Quantisation personnalisée)
Les résultats de clustering utilisant des intégres quantifiés float32 et (u)int8 montrent une disposition générale similaire de clusters bien définis, ce qui indique que (i) l'algorithme de clustering HDBSCAN était efficace dans les deux cas, et (ii) les relations principales dans les données ont été maintenues après la quantification (à l'aide de transformateurs de phrases et de notre mise en œuvre personnalisée).
Il peut notamment être observé que l'utilisation de la quantification d'intégration a entraîné les deux cas dans un regroupement légèrement plus granulaire (35 grappes contre 31) qui semblent sémantiquement cohérentes. Notre hypothèse provisoire pour cette différence est que la quantification scalaire pourrait paradoxalement guider l'algorithme de clustering HDBSCAN pour séparer les points qui étaient précédemment regroupés.
Cela pourrait être dû à (i) le bruit (la quantification peut créer de petites variations bruyantes dans les données, qui pourraient avoir une sorte d'effet de régularisation et conduire à des décisions de regroupement plus sensibles), ou en raison de (ii) la différence de précision numérique et d'altération des calculs de distance (cela pourrait amplifier certaines différences entre les points qui étaient moins prononcés dans la représentation float32 ). Une enquête plus approfondie serait nécessaire pour bien comprendre les implications de la quantification sur le clustering.
L'ensemble du programme est disponible sur CS.CL.Taxonomy. Cette approche peut servir de référence pour identifier automatiquement les programmes de candidats de classes dans les catégories ARXIV de haut niveau:
. Foundations of Language Models
.. Model Architectures and Mechanisms
... Transformer Models and Attention Mechanisms
... Large Language Models (LLMs)
.. Model Optimization and Efficiency
... Compression and Quantization
... Parameter-Efficient Fine-Tuning
... Knowledge Distillation
.. Learning Paradigms
... In-Context Learning
... Instruction Tuning
. AI Ethics, Safety, and Societal Impact
.. Ethical Considerations
... Bias and Fairness in Models
... Alignment and Preference Optimization
.. Safety and Security
... Hallucination in LLMs
... Adversarial Attacks and Robustness
... Detection of AI-Generated Text
.. Social Impact
... Hate Speech and Offensive Language Detection
... Fake News Detection
[...]
@article{carpintero2024
author = { Diego Carpintero},
title = {Taxonomy Completion with Embedding Quantization and an LLM-Pipeline: A Case Study in Computational Linguistics},
journal = {Hugging Face Blog},
year = {2024},
note = {https://huggingface.co/blog/dcarpintero/taxonomy-completion},
}