<i> Tokenizer </i> rapide dans le pipeline de QA
Nous allons maintenant nous plonger dans le pipeline de question-answering
et voir comment exploiter les offsets pour extraire d’u ncontexte la réponse à la question posée. Nous verrons ensuite comment gérer les contextes très longs qui finissent par être tronqués. Vous pouvez sauter cette section si vous n’êtes pas intéressé par la tâche de réponse aux questions.
Utilisation du pipeline de question-answering
Comme nous l’avons vu dans le chapitre 1, nous pouvons utiliser le pipeline de question-answering
comme ceci pour obtenir une réponse à une question :
from transformers import pipeline
question_answerer = pipeline("question-answering")
context = """
🤗 Transformers is backed by the three most popular deep learning libraries
— Jax, PyTorch, and TensorFlow — with a seamless integration between them.
It's straightforward to train your models with one before loading them for inference with the other.
"""
# 🤗 Transformers s'appuie sur les trois bibliothèques d'apprentissage profond les plus populaires
# (Jax, PyTorch et TensorFlow) avec une intégration transparente entre elles.
# C'est simple d'entraîner vos modèles avec l'une avant de les charger pour l'inférence avec l'autre.
question = "Which deep learning libraries back 🤗 Transformers?"
# Quelles bibliothèques d'apprentissage profond derrière 🤗 Transformers ?
question_answerer(question=question, context=context)
{'score': 0.97773,
'start': 78,
'end': 105,
'answer': 'Jax, PyTorch and TensorFlow'}
Contrairement aux autres pipelines, qui ne peuvent pas tronquer et diviser les textes dont la longueur est supérieure à la longueur maximale acceptée par le modèle (et qui peuvent donc manquer des informations à la fin d’un document), ce pipeline peut traiter des contextes très longs et retournera la réponse à la question même si elle se trouve à la fin :
long_context = """
🤗 Transformers: State of the Art NLP
🤗 Transformers provides thousands of pretrained models to perform tasks on texts such as classification, information extraction,
question answering, summarization, translation, text generation and more in over 100 languages.
Its aim is to make cutting-edge NLP easier to use for everyone.
🤗 Transformers provides APIs to quickly download and use those pretrained models on a given text, fine-tune them on your own datasets and
then share them with the community on our model hub. At the same time, each python module defining an architecture is fully standalone and
can be modified to enable quick research experiments.
Why should I use transformers?
1. Easy-to-use state-of-the-art models:
- High performance on NLU and NLG tasks.
- Low barrier to entry for educators and practitioners.
- Few user-facing abstractions with just three classes to learn.
- A unified API for using all our pretrained models.
- Lower compute costs, smaller carbon footprint:
2. Researchers can share trained models instead of always retraining.
- Practitioners can reduce compute time and production costs.
- Dozens of architectures with over 10,000 pretrained models, some in more than 100 languages.
3. Choose the right framework for every part of a model's lifetime:
- Train state-of-the-art models in 3 lines of code.
- Move a single model between TF2.0/PyTorch frameworks at will.
- Seamlessly pick the right framework for training, evaluation and production.
4. Easily customize a model or an example to your needs:
- We provide examples for each architecture to reproduce the results published by its original authors.
- Model internals are exposed as consistently as possible.
- Model files can be used independently of the library for quick experiments.
🤗 Transformers is backed by the three most popular deep learning libraries — Jax, PyTorch and TensorFlow — with a seamless integration
between them. It's straightforward to train your models with one before loading them for inference with the other.
"""
long_context - fr = """
🤗 Transformers : l'état de l'art du NLP
🤗 Transformers fournit des milliers de modèles pré-entraînés pour effectuer des tâches sur des textes telles que la classification,
l'extraction d'informations, la réponse à des questions, le résumé de textes, la traduction, la génération de texte et plus encore dans plus de 100 langues.
Son objectif est de rendre le traitement automatique des langues de pointe plus facile à utiliser pour tout le monde.
🤗 Transformers fournit des API permettant de télécharger et d'utiliser rapidement ces modèles pré-entraînés sur un texte donné, de les affiner sur vos propres ensembles de données et de les partager avec la communauté sur notre site Web.
puis de les partager avec la communauté sur notre hub de modèles. En même temps, chaque module python définissant une architecture est entièrement autonome et peut être modifié pour permettre des expériences de recherche rapides.
peut être modifié pour permettre des expériences de recherche rapides.
Pourquoi devrais-je utiliser des transformateurs ?
1. Des modèles de pointe faciles à utiliser :
- Haute performance sur les tâches NLU et NLG.
- Faible barrière à l'entrée pour les éducateurs et les praticiens.
- Peu d'abstractions pour l'utilisateur avec seulement trois classes à apprendre.
- Une API unifiée pour utiliser tous nos modèles pré-entraînés.
- Des coûts de calcul plus faibles, une empreinte carbone réduite :
2. Les chercheurs peuvent partager les modèles formés au lieu de toujours les reformer.
- Les praticiens peuvent réduire le temps de calcul et les coûts de production.
- Des dizaines d'architectures avec plus de 10 000 modèles pré-formés, certains dans plus de 100 langues.
3. Choisissez le cadre approprié pour chaque étape de la vie d'un modèle :
- Entraînez des modèles de pointe en 3 lignes de code.
- Déplacez un seul modèle entre les frameworks TF2.0/PyTorch à volonté.
- Choisissez de manière transparente le bon framework pour l'entraînement, l'évaluation et la production.
4. Adaptez facilement un modèle ou un exemple à vos besoins :
- Nous fournissons des exemples pour chaque architecture afin de reproduire les résultats publiés par ses auteurs originaux.
- Les éléments internes des modèles sont exposés de manière aussi cohérente que possible.
- Les fichiers de modèles peuvent être utilisés indépendamment de la bibliothèque pour des expériences rapides.
🤗 Transformers s'appuie sur les trois bibliothèques d'apprentissage profond les plus populaires (Jax, PyTorch et TensorFlow) avec une intégration parfaite
entre elles. Il est simple d'entraîner vos modèles avec l'une avant de les charger pour l'inférence avec l'autre.
"""
question_answerer(question=question, context=long_context)
{'score': 0.97149,
'start': 1892,
'end': 1919,
'answer': 'Jax, PyTorch and TensorFlow'}
Voyons comment il fait tout cela !
Utilisation d’un modèle pour répondre à des questions
Comme avec n’importe quel autre pipeline, nous commençons par tokeniser notre entrée et l’envoyons ensuite à travers le modèle. Le checkpoint utilisé par défaut pour le pipeline de question-answering
est distilbert-base-cased-distilled-squad
(le « squad » dans le nom vient du jeu de données sur lequel le modèle a été finetuné, nous parlerons davantage de ce jeu de données dans le chapitre 7) :
from transformers import AutoTokenizer, AutoModelForQuestionAnswering
model_checkpoint = "distilbert-base-cased-distilled-squad"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
model = AutoModelForQuestionAnswering.from_pretrained(model_checkpoint)
inputs = tokenizer(question, context, return_tensors="pt")
outputs = model(**inputs)
Notez que nous tokenizons la question et le contexte comme une paire, la question en premier.
Les modèles de réponse aux questions fonctionnent un peu différemment des modèles que nous avons vus jusqu’à présent. En utilisant l’image ci-dessus comme exemple, le modèle a été entraîné à prédire l’index du token de début de la réponse (ici 21) et l’index du token où la réponse se termine (ici 24). C’est pourquoi ces modèles ne retournent pas un tenseur de logits mais deux : un pour les logits correspondant au token de début de la réponse, et un pour les logits correspondant au token de fin de la réponse. Puisque dans ce cas nous n’avons qu’une seule entrée contenant 66 tokens, nous obtenons :
start_logits = outputs.start_logits
end_logits = outputs.end_logits
print(start_logits.shape, end_logits.shape)
torch.Size([1, 66]) torch.Size([1, 66])
Pour convertir ces logits en probabilités, nous allons appliquer une fonction softmax. Mais avant cela, nous devons nous assurer de masquer les indices qui ne font pas partie du contexte. Notre entrée est [CLS] question [SEP] contexte [SEP]
donc nous devons masquer les tokens de la question ainsi que le token [SEP]
. Nous garderons cependant le token [CLS]
car certains modèles l’utilisent pour indiquer que la réponse n’est pas dans le contexte.
Puisque nous appliquerons une fonction softmax par la suite, il nous suffit de remplacer les logits que nous voulons masquer par un grand nombre négatif. Ici, nous utilisons -10000
:
import torch
sequence_ids = inputs.sequence_ids()
# Masque tout, sauf les tokens du contexte
mask = [i != 1 for i in sequence_ids]
# Démasquer le token [CLS]
mask[0] = False
mask = torch.tensor(mask)[None]
start_logits[mask] = -10000
end_logits[mask] = -10000
Maintenant que nous avons correctement masqué les logits correspondant aux positions que nous ne voulons pas prédire, nous pouvons appliquer la softmax :
start_probabilities = torch.nn.functional.softmax(start_logits, dim=-1)[0]
end_probabilities = torch.nn.functional.softmax(end_logits, dim=-1)[0]
A ce stade, nous pourrions prendre l’argmax des probabilités de début et de fin mais nous pourrions nous retrouver avec un indice de début supérieur à l’indice de fin. Nous devons donc prendre quelques précautions supplémentaires. Nous allons calculer les probabilités de chaque start_index
et end_index
possible où start_index<=end_index
, puis nous prendrons le tuple (start_index, end_index)
avec la plus grande probabilité.
En supposant que les événements « La réponse commence à start_index
» et « La réponse se termine à end_index
» sont indépendants, la probabilité que la réponse commence à end_index
et se termine à end_index
est :
Ainsi, pour calculer tous les scores, il suffit de calculer tous les produits où start_index <= end_index
.
Calculons d’abord tous les produits possibles :
scores = start_probabilities[:, None] * end_probabilities[None, :]
Ensuite, nous masquerons les valeurs où start_index > end_index
en les mettant à 0
(les autres probabilités sont toutes des nombres positifs). La fonction torch.triu()
renvoie la partie triangulaire supérieure du tenseur 2D passé en argument, elle fera donc ce masquage pour nous :
scores = torch.triu(scores)
Il ne nous reste plus qu’à obtenir l’indice du maximum. Puisque PyTorch retourne l’index dans le tenseur aplati, nous devons utiliser les opérations division //
et modulo %
pour obtenir le start_index
et le end_index
:
max_index = scores.argmax().item()
start_index = max_index // scores.shape[1]
end_index = max_index % scores.shape[1]
print(scores[start_index, end_index])
Nous n’avons pas encore tout à fait terminé, mais au moins nous avons déjà le score correct pour la réponse (vous pouvez le vérifier en le comparant au premier résultat de la section précédente) :
0.97773
✏️ Essayez ! Calculez les indices de début et de fin pour les cinq réponses les plus probables.
Nous avons les start_index
et end_index
de la réponse en termes de tokens. Maintenant nous devons juste convertir en indices de caractères dans le contexte. C’est là que les offsets seront super utiles. Nous pouvons les saisir et les utiliser comme nous l’avons fait dans la tâche de classification des tokens :
inputs_with_offsets = tokenizer(question, context, return_offsets_mapping=True)
offsets = inputs_with_offsets["offset_mapping"]
start_char, _ = offsets[start_index]
_, end_char = offsets[end_index]
answer = context[start_char:end_char]
Il ne nous reste plus qu’à tout formater pour obtenir notre résultat :
result = {
"answer": answer,
"start": start_char,
"end": end_char,
"score": scores[start_index, end_index],
}
print(result)
{'answer': 'Jax, PyTorch and TensorFlow',
'start': 78,
'end': 105,
'score': 0.97773}
Super ! C’est la même chose que dans notre premier exemple !
✏️ Essayez ! Utilisez les meilleurs scores que vous avez calculés précédemment pour afficher les cinq réponses les plus probables. Pour vérifier vos résultats, retournez au premier pipeline et passez dans top_k=5
lorsque vous l’appelez.
Gestion des contextes longs
Si nous essayons de tokeniser la question et le long contexte que nous avons utilisé dans l’exemple précédemment, nous obtenons un nombre de tokens supérieur à la longueur maximale utilisée dans le pipeline question-answering
(qui est de 384) :
inputs = tokenizer(question, long_context)
print(len(inputs["input_ids"]))
461
Nous devrons donc tronquer nos entrées à cette longueur maximale. Il y a plusieurs façons de le faire mais nous ne voulons pas tronquer la question, seulement le contexte. Puisque le contexte est la deuxième phrase, nous utilisons la stratégie de troncature "only_second"
. Le problème qui se pose alors est que la réponse à la question peut ne pas se trouver dans le contexte tronqué. Ici, par exemple, nous avons choisi une question dont la réponse se trouve vers la fin du contexte, et lorsque nous la tronquons, cette réponse n’est pas présente :
inputs = tokenizer(question, long_context, max_length=384, truncation="only_second")
print(tokenizer.decode(inputs["input_ids"]))
"""
[CLS] Which deep learning libraries back [UNK] Transformers? [SEP] [UNK] Transformers : State of the Art NLP
[UNK] Transformers provides thousands of pretrained models to perform tasks on texts such as classification, information extraction,
question answering, summarization, translation, text generation and more in over 100 languages.
Its aim is to make cutting-edge NLP easier to use for everyone.
[UNK] Transformers provides APIs to quickly download and use those pretrained models on a given text, fine-tune them on your own datasets and
then share them with the community on our model hub. At the same time, each python module defining an architecture is fully standalone and
can be modified to enable quick research experiments.
Why should I use transformers?
1. Easy-to-use state-of-the-art models:
- High performance on NLU and NLG tasks.
- Low barrier to entry for educators and practitioners.
- Few user-facing abstractions with just three classes to learn.
- A unified API for using all our pretrained models.
- Lower compute costs, smaller carbon footprint:
2. Researchers can share trained models instead of always retraining.
- Practitioners can reduce compute time and production costs.
- Dozens of architectures with over 10,000 pretrained models, some in more than 100 languages.
3. Choose the right framework for every part of a model's lifetime:
- Train state-of-the-art models in 3 lines of code.
- Move a single model between TF2.0/PyTorch frameworks at will.
- Seamlessly pick the right framework for training, evaluation and production.
4. Easily customize a model or an example to your needs:
- We provide examples for each architecture to reproduce the results published by its original authors.
- Model internal [SEP]
"""
"""
[CLS] Quelles sont les bibliothèques d'apprentissage profond qui soutiennent [UNK] Transformers ? [SEP] [UNK] Transformers : l'état de l'art du NLP
[UNK] Transformers fournit des milliers de modèles pré-entraînés pour effectuer des tâches sur des textes telles que la classification, l'extraction d'informations, la réponse à des questions, le résumé, la traduction, la génération de textes, etc,
la réponse à des questions, le résumé, la traduction, la génération de texte et plus encore dans plus de 100 langues.
Son objectif est de rendre le traitement automatique des langues de pointe plus facile à utiliser pour tous.
Transformers [UNK] fournit des API permettant de télécharger et d'utiliser rapidement ces modèles pré-entraînés sur un texte donné, de les affiner sur vos propres ensembles de données et de les partager avec la communauté sur notre site Web.
puis de les partager avec la communauté sur notre hub de modèles. En même temps, chaque module python définissant une architecture est entièrement autonome et peut être modifié pour permettre des expériences de recherche rapides.
peut être modifié pour permettre des expériences de recherche rapides.
Pourquoi devrais-je utiliser des transformateurs ?
1. Des modèles de pointe faciles à utiliser :
- Haute performance sur les tâches NLU et NLG.
- Faible barrière à l'entrée pour les éducateurs et les praticiens.
- Peu d'abstractions pour l'utilisateur avec seulement trois classes à apprendre.
- Une API unifiée pour utiliser tous nos modèles pré-entraînés.
- Des coûts de calcul plus faibles, une empreinte carbone réduite :
2. Les chercheurs peuvent partager les modèles formés au lieu de toujours les reformer.
- Les praticiens peuvent réduire le temps de calcul et les coûts de production.
- Des dizaines d'architectures avec plus de 10 000 modèles pré-formés, certains dans plus de 100 langues.
3. Choisissez le cadre approprié pour chaque étape de la vie d'un modèle :
- Entraînez des modèles de pointe en 3 lignes de code.
- Déplacez un seul modèle entre les frameworks TF2.0/PyTorch à volonté.
- Choisissez de manière transparente le bon framework pour l'entraînement, l'évaluation et la production.
4. Adaptez facilement un modèle ou un exemple à vos besoins :
- Nous fournissons des exemples pour chaque architecture afin de reproduire les résultats publiés par ses auteurs originaux.
- Modèle interne [SEP]
"""
Cela signifie que le modèle a du mal à trouver la bonne réponse. Pour résoudre ce problème, le pipeline de question-answering
nous permet de diviser le contexte en morceaux plus petits, en spécifiant la longueur maximale. Pour s’assurer que nous ne divisons pas le contexte exactement au mauvais endroit pour permettre de trouver la réponse, il inclut également un certain chevauchement entre les morceaux.
Nous pouvons demander au tokenizer (rapide ou lent) de le faire pour nous en ajoutant return_overflowing_tokens=True
, et nous pouvons spécifier le chevauchement que nous voulons avec l’argument stride
. Voici un exemple, en utilisant une phrase plus petite :
sentence = "This sentence is not too long but we are going to split it anyway."
# "Cette phrase n'est pas trop longue mais nous allons la diviser quand même."
inputs = tokenizer(
sentence, truncation=True, return_overflowing_tokens=True, max_length=6, stride=2
)
for ids in inputs["input_ids"]:
print(tokenizer.decode(ids))
'[CLS] This sentence is not [SEP]'
'[CLS] is not too long [SEP]'
'[CLS] too long but we [SEP]'
'[CLS] but we are going [SEP]'
'[CLS] are going to split [SEP]'
'[CLS] to split it anyway [SEP]'
'[CLS] it anyway. [SEP]'
Comme on peut le voir, la phrase a été découpée en morceaux de telle sorte que chaque entrée dans inputs["input_ids"]
a au maximum 6 tokens (il faudrait ajouter du padding pour que la dernière entrée ait la même taille que les autres) et il y a un chevauchement de 2 tokens entre chacune des entrées.
Regardons de plus près le résultat de la tokénisation :
print(inputs.keys())
dict_keys(['input_ids', 'attention_mask', 'overflow_to_sample_mapping'])
Comme prévu, nous obtenons les identifiants d’entrée et un masque d’attention. La dernière clé, overflow_to_sample_mapping
, est une carte qui nous indique à quelle phrase correspond chacun des résultats. Ici nous avons 7 résultats qui proviennent tous de la (seule) phrase que nous avons passée au tokenizer :
print(inputs["overflow_to_sample_mapping"])
[0, 0, 0, 0, 0, 0, 0]
C’est plus utile lorsque nous tokenisons plusieurs phrases ensemble. Par exemple :
sentences = [
"This sentence is not too long but we are going to split it anyway.",
# Cette phrase n'est pas trop longue mais nous allons la diviser quand même.
"This sentence is shorter but will still get split.",
# Cette phrase est plus courte mais sera quand même divisée.
]
inputs = tokenizer(
sentences, truncation=True, return_overflowing_tokens=True, max_length=6, stride=2
)
print(inputs["overflow_to_sample_mapping"])
nous donne :
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1]
ce qui signifie que la première phrase est divisée en 7 morceaux comme précédemment et que les 4 morceaux suivants proviennent de la deuxième phrase.
Revenons maintenant à notre contexte long. Par défaut, le pipeline question-answering
utilise une longueur maximale de 384 et un stride de 128, qui correspondent à la façon dont le modèle a été finetuné (vous pouvez ajuster ces paramètres en passant les arguments max_seq_len
et stride
lorsque vous appelez le pipeline). Nous utiliserons donc ces paramètres lors de la tokenisation. Nous ajouterons aussi du padding (pour avoir des échantillons de même longueur afin de pouvoir construire des tenseurs) ainsi que demander les offsets :
inputs = tokenizer(
question,
long_context,
stride=128,
max_length=384,
padding="longest",
truncation="only_second",
return_overflowing_tokens=True,
return_offsets_mapping=True,
)
Ces inputs
contiendront les identifiants d’entrée, les masques d’attention que le modèle attend, ainsi que les offsets et le overflow_to_sample_mapping
dont on vient de parler. Puisque ces deux éléments ne sont pas des paramètres utilisés par le modèle, nous allons les sortir des inputs
(et nous ne stockerons pas la correspondance puisqu’elle n’est pas utile ici) avant de le convertir en tenseur :
_ = inputs.pop("overflow_to_sample_mapping")
offsets = inputs.pop("offset_mapping")
inputs = inputs.convert_to_tensors("pt")
print(inputs["input_ids"].shape)
torch.Size([2, 384])
Notre contexte long a été divisé en deux, ce qui signifie qu’après avoir traversé notre modèle, nous aurons deux ensembles de logits de début et de fin :
outputs = model(**inputs)
start_logits = outputs.start_logits
end_logits = outputs.end_logits
print(start_logits.shape, end_logits.shape)
torch.Size([2, 384]) torch.Size([2, 384])
Comme précédemment, nous masquons d’abord les tokens qui ne font pas partie du contexte avant de prendre le softmax. Nous masquons également tous les tokens de padding (tels que signalés par le masque d’attention) :
sequence_ids = inputs.sequence_ids()
# Masque tout, sauf les tokens du contexte
mask = [i != 1 for i in sequence_ids]
# Démasquer le jeton [CLS]
mask[0] = False
# Masquer tous les tokens [PAD]
mask = torch.logical_or(torch.tensor(mask)[None], (inputs["attention_mask"] == 0))
start_logits[mask] = -10000
end_logits[mask] = -10000
Ensuite, nous pouvons utiliser la fonction softmax pour convertir nos logits en probabilités :
start_probabilities = torch.nn.functional.softmax(start_logits, dim=-1)
end_probabilities = torch.nn.functional.softmax(end_logits, dim=-1)
L’étape suivante est similaire à ce que nous avons fait pour le petit contexte mais nous la répétons pour chacun de nos deux morceaux. Nous attribuons un score à tous les espaces de réponse possibles puis nous prenons l’espace ayant le meilleur score :
candidates = []
for start_probs, end_probs in zip(start_probabilities, end_probabilities):
scores = start_probs[:, None] * end_probs[None, :]
idx = torch.triu(scores).argmax().item()
start_idx = idx // scores.shape[1]
end_idx = idx % scores.shape[1]
score = scores[start_idx, end_idx].item()
candidates.append((start_idx, end_idx, score))
print(candidates)
[(0, 18, 0.33867), (173, 184, 0.97149)]
Ces deux candidats correspondent aux meilleures réponses que le modèle a pu trouver dans chaque morceau. Le modèle est beaucoup plus confiant dans le fait que la bonne réponse se trouve dans la deuxième partie (ce qui est bon signe !). Il ne nous reste plus qu’à faire correspondre ces deux espaces de tokens à des espaces de caractères dans le contexte (nous n’avons besoin de faire correspondre que le second pour avoir notre réponse, mais il est intéressant de voir ce que le modèle a choisi dans le premier morceau).
✏️ Essayez ! Adaptez le code ci-dessus pour renvoyer les scores et les étendues des cinq réponses les plus probables (au total, pas par morceau).
Le offsets
que nous avons saisi plus tôt est en fait une liste d’offsets avec une liste par morceau de texte :
for candidate, offset in zip(candidates, offsets):
start_token, end_token, score = candidate
start_char, _ = offset[start_token]
_, end_char = offset[end_token]
answer = long_context[start_char:end_char]
result = {"answer": answer, "start": start_char, "end": end_char, "score": score}
print(result)
{'answer': '\n🤗 Transformers: State of the Art NLP', 'start': 0, 'end': 37, 'score': 0.33867}
{'answer': 'Jax, PyTorch and TensorFlow', 'start': 1892, 'end': 1919, 'score': 0.97149}
Si nous ignorons le premier résultat, nous obtenons le même résultat que notre pipeline pour ce long contexte !
✏️ Essayez ! Utilisez les meilleurs scores que vous avez calculés auparavant pour montrer les cinq réponses les plus probables (pour l’ensemble du contexte, pas pour chaque morceau). Pour vérifier vos résultats, retournez au premier pipeline et spécifiez top_k=5
en argument en l’appelant.
Ceci conclut notre plongée en profondeur dans les capacités du tokenizer. Nous mettrons à nouveau tout cela en pratique dans le prochain chapitre, lorsque nous vous montrerons comment finetuner un modèle sur une série de tâches NLP courantes.