#
Entraîner un nouveau <i> tokenizer </i> à partir d’un ancien

Si un modèle de langue n’est pas disponible dans la langue qui vous intéresse ou si votre corpus est très différent de celui sur lequel votre modèle de langue a été entraîné, vous voudrez très probablement réentraîner le modèle à partir de zéro en utilisant un tokenizer adapté à vos données. Pour ce faire, vous devrez entraîner un nouveau tokenizer sur votre jeu de données. Mais qu’est-ce que cela signifie exactement ? Lorsque nous avons examiné pour la première fois les tokenizers dans le chapitre 2, nous avons vu que la plupart des transformers utilisent un algorithme de tokenisation en sous-mots. Pour identifier les sous-mots qui sont intéressants et qui apparaissent le plus fréquemment dans un corpus donné, le tokenizer doit examiner attentivement tous les textes du corpus. C’est un processus que nous appelons entraînement. Les règles exactes qui régissent cet apprentissage dépendent du type de tokenizer utilisé. Nous passerons en revue les trois principaux algorithmes plus loin dans ce chapitre.

> ⚠️ Entraîner un tokenizer n’est pas la même chose qu’entraîner un modèle ! L’entraînement du modèle utilise la descente de gradient stochastique pour réduire un peu plus la perte à chaque batch. Il est par nature aléatoire (ce qui signifie que vous devez définir des graines pour obtenir les mêmes résultats lorsque vous effectuez deux fois le même entraînement). Entraîner un tokenizer est un processus statistique qui identifie les meilleurs sous-mots à choisir pour un corpus donné. Les règles exactes utilisées pour les choisir dépendent de l’algorithme de tokénisation. Le processus est déterministe, ce qui signifie que vous obtenez toujours les mêmes résultats lorsque vous vous entraînez avec le même algorithme sur le même corpus.

##
Assemblage d’un corpus

Il y a une API très simple dans 🤗 Transformers que vous pouvez utiliser pour entraîner un nouveau tokenizer avec les mêmes caractéristiques qu’un déjà existant : `AutoTokenizer.train_new_from_iterator()`. Pour illustrer cela, disons que nous voulons entraîner GPT-2 à partir de zéro mais dans une langue autre que l’anglais. Notre première tâche est de rassembler des batchs de données dans cette langue dans un corpus d’entraînement. Pour avoir des exemples que tout le monde puisse comprendre, nous n’utiliserons pas ici une langue comme le russe ou le chinois mais plutôt une langue anglaise spécialisée : le langage Python.

La bibliothèque 🤗 Datasets peut nous aider à assembler un corpus de code source Python. Nous allons utiliser la fonction habituelle `load_dataset()` pour télécharger et mettre en cache le jeu de données CodeSearchNet. Ce jeu de données a été créé pour le CodeSearchNet challenge et contient des millions de fonctions provenant de bibliothèques open source sur GitHub dans plusieurs langages de programmation. Ici, nous allons charger la partie Python de ce jeu de données :

    from datasets import load_dataset

    # Cela peut prendre quelques minutes alors prenez un thé ou un café pendant que vous patientez !
    raw_datasets = load_dataset("code_search_net", "python")

Nous pouvons jeter un coup d’œil au jeu d’entraînement pour voir quelles sont les colonnes auxquelles nous avons accès :

    raw_datasets["train"]

    Dataset({
        features: ['repository_name', 'func_path_in_repository', 'func_name', 'whole_func_string', 'language',
          'func_code_string', 'func_code_tokens', 'func_documentation_string', 'func_documentation_tokens', 'split_name',
          'func_code_url'
        ],
        num_rows: 412178
    })

Nous pouvons voir que le jeu de données sépare les chaînes de documents du code et suggère une tokenization des deux. Ici, nous utiliserons simplement la colonne `whole_func_string` pour entraîner notre tokenizer. Nous pouvons regarder un exemple de la façon suivante :

    print(raw_datasets["train"][123456]["whole_func_string"])

qui nous affiche ce qui suit :

    def handle_simple_responses(
          self, timeout_ms=None, info_cb=DEFAULT_MESSAGE_CALLBACK):
        """Accepts normal responses from the device.

        Args:
          timeout_ms: Timeout in milliseconds to wait for each response.
          info_cb: Optional callback for text sent from the bootloader.

        Returns:
          OKAY packet's message.
        """
        return self._accept_responses('OKAY', info_cb, timeout_ms=timeout_ms)

La première chose à faire est de transformer le jeu de données en un itérateur de listes de textes. Par exemple, une liste de listes de textes. L’utilisation de listes de textes permet à notre tokenizer d’aller plus vite (l’entraînement a alors lieu sur des batchs de textes au lieu de traiter des textes un par un). Et le fait que ce soit un itérateur permet d’éviter d’avoir tout en mémoire en même temps. Si votre corpus est énorme, vous voudrez profiter du fait que 🤗 Datasets ne charge pas tout en RAM mais stocke les éléments du jeu de données sur le disque.

Faire ce qui suit créerait une liste de listes de 1 000 textes chacune mais chargerait tout en mémoire :

    # Ne décommentez pas la ligne suivante à moins que votre jeu de données soit petit !
    # training_corpus = [raw_datasets["train"][i: i + 1000]["whole_func_string"] for i in range(0, len(raw_datasets["train"]), 1000)]

En utilisant un générateur, nous pouvons éviter que Python ne charge quoi que ce soit en mémoire à moins que cela soit réellement nécessaire. Pour créer un tel générateur, il suffit de remplacer les crochets par des parenthèses :

    training_corpus = (
        raw_datasets["train"][i : i + 1000]["whole_func_string"]
        for i in range(0, len(raw_datasets["train"]), 1000)
    )

Cette ligne de code ne récupère aucun élément du jeu de données. Elle crée simplement un objet que vous pouvez utiliser dans une boucle `for` Python. Les textes ne seront chargés que lorsque vous en aurez besoin (c’est-à-dire lorsque vous serez à l’étape de la boucle `for` qui les requiert) et seulement 1 000 textes à la fois. De cette façon, vous n’épuiserez pas toute votre mémoire, même si vous traitez un énorme jeu de données.

Le problème avec un objet générateur est qu’il ne peut être utilisé qu’une seule fois. Ainsi, au lieu que cet objet nous donne deux fois la liste des 10 premiers chiffres :

    gen = (i for i in range(10))
    print(list(gen))
    print(list(gen))

on les reçoit une fois et ensuite une liste vide :

    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    []

C’est pourquoi nous définissons une fonction qui renvoie un générateur à la place :

    def get_training_corpus():
        return (
            raw_datasets["train"][i : i + 1000]["whole_func_string"]
            for i in range(0, len(raw_datasets["train"]), 1000)
        )

    training_corpus = get_training_corpus()

Vous pouvez également définir votre générateur à l’intérieur d’une boucle `for` en utilisant l’instruction `yield` :

    def get_training_corpus():
        dataset = raw_datasets["train"]
        for start_idx in range(0, len(dataset), 1000):
            samples = dataset[start_idx : start_idx + 1000]
            yield samples["whole_func_string"]

qui produit exactement le même générateur que précédemment mais permet d’utiliser une logique plus complexe que celle que vous pouvez utiliser dans une compréhension de liste.

##
Entraînement d’un nouveau <i> tokenizer </i>

Maintenant que nous avons notre corpus sous la forme d’un itérateur de batchs de textes, nous sommes prêts à entraîner un nouveau tokenizer. Pour ce faire, nous devons d’abord charger le tokenizer que nous voulons coupler avec notre modèle (ici, le GPT-2) :

    from transformers import AutoTokenizer

    old_tokenizer = AutoTokenizer.from_pretrained("gpt2")

Même si nous allons entraîner un nouveau tokenizer, c’est une bonne idée de faire ça pour éviter de partir entièrement de zéro. De cette façon, nous n’aurons pas à spécifier l’algorithme de tokénisation ou les jetons spéciaux que nous voulons utiliser. Notre nouveau tokenizer sera exactement le même que celui du GPT-2. La seule chose qui changera sera le vocabulaire qui sera déterminé lors de l’entraînement sur notre corpus.

Voyons d’abord comment ce tokenizer traiterait un exemple de fonction :

    example = '''def add_numbers(a, b):
        """Add the two numbers `a` and `b`."""
        return a + b'''

    tokens = old_tokenizer.tokenize(example)
    tokens

    ['def', 'Ġadd', '_', 'n', 'umbers', '(', 'a', ',', 'Ġb', '):', 'Ċ', 'Ġ', 'Ġ', 'Ġ', 'Ġ"""', 'Add', 'Ġthe', 'Ġtwo',
     'Ġnumbers', 'Ġ`', 'a', '`', 'Ġand', 'Ġ`', 'b', '`', '."', '""', 'Ċ', 'Ġ', 'Ġ', 'Ġ', 'Ġreturn', 'Ġa', 'Ġ+', 'Ġb']

Ce tokenizer possède quelques symboles spéciaux, comme `Ġ` et `Ċ`, qui désignent respectivement les espaces et les retours à la ligne. Comme on peut le voir, ce n’est pas très efficace. Le tokenizer renvoie des jetons individuels pour chaque espace alors qu’il pourrait regrouper ceux des indentations (puisqu’avoir des ensembles de quatre ou huit espaces est très courant dans du code). Il divise également le nom de la fonction de façon un peu bizarre car pas habitué à voir des mots avec le caractère `_`.

Entraînons un nouveau tokenizer et voyons s’il résout ces problèmes. Pour cela, nous allons utiliser la méthode `train_new_from_iterator()` :

    tokenizer = old_tokenizer.train_new_from_iterator(training_corpus, 52000)

Cette commande peut prendre un peu de temps si votre corpus est très grand. Pour ce jeu de données de 1,6 Go de textes, elle est très rapide (1 minute 16 secondes sur un CPU AMD Ryzen 9 3900X avec 12 cœurs).

Notez que `AutoTokenizer.train_new_from_iterator()` ne fonctionne que si le tokenizer que vous utilisez est un tokenizer « rapide ». Comme vous le verrez dans la section suivante, la bibliothèque 🤗 Transformers contient deux types de tokenizers : certains sont écrits en pur Python et d’autres (les rapides) sont soutenus par la bibliothèque 🤗 Tokenizers qui est écrite dans le langage Rust. Python est le langage le plus souvent utilisé pour les applications de science des données et d’apprentissage profond, mais lorsque quelque chose doit être parallélisé pour être rapide, il faut que cela soit écrit dans un autre langage. Par exemple, les multiplications matricielles qui sont au cœur du calcul du modèle sont écrites en CUDA, une bibliothèque en C optimisée pour les GPUs.

Entraîner un tout nouveau tokenizer en Python pur est atrocement lent, c’est pourquoi nous avons développé la bibliothèque 🤗 Tokenizers. Notez que, tout comme vous n’avez pas eu à apprendre le langage CUDA pour pouvoir exécuter votre modèle sur un batch d’entrées sur un GPU, vous n’aurez pas besoin d’apprendre Rust pour utiliser un tokenizer rapide. La bibliothèque 🤗 Tokenizers fournit des liaisons Python pour de nombreuses méthodes qui appellent en interne un morceau de code en Rust. Par exemple, pour paralléliser l’entraînement de votre nouveau tokenizer ou, comme nous l’avons vu dans le chapitre 3, la tokenisation d’un lot d’entrées.

La plupart des transformers ont un tokenizer rapide de disponible. Il y a quelques exceptions que vous pouvez vérifier ici. S’il est disponible, l’API `AutoTokenizer` sélectionne toujours pour vous le tokenizer rapide. Dans la prochaine section, nous allons jeter un coup d’oeil à certaines des autres caractéristiques spéciales des tokenizers rapides, qui seront très utiles pour des tâches comme la classification de tokens et la réponse aux questions. Mais avant cela, essayons notre tout nouveau tokenizer sur l’exemple précédent :

    tokens = tokenizer.tokenize(example)
    tokens

    ['def', 'Ġadd', '_', 'numbers', '(', 'a', ',', 'Ġb', '):', 'ĊĠĠĠ', 'Ġ"""', 'Add', 'Ġthe', 'Ġtwo', 'Ġnumbers', 'Ġ`',
     'a', '`', 'Ġand', 'Ġ`', 'b', '`."""', 'ĊĠĠĠ', 'Ġreturn', 'Ġa', 'Ġ+', 'Ġb']

Ici, nous voyons à nouveau les symboles spéciaux `Ġ` et `Ċ` qui indiquent les espaces et les retours à la ligne. Nous pouvons également voir que notre tokenizer a appris certains tokens qui sont très spécifiques à un corpus de fonctions Python. Par exemple, il y a un token `ĊĠĠĠ` qui représente une indentation et un token `Ġ"""` qui représente les trois guillemets qui commencent une docstring. Le tokenizer divise également correctement le nom de la fonction sur `_`. Il s’agit d’une représentation assez compacte. En comparaison, l’utilisation du tokenizer en anglais « simple » sur le même exemple nous donnera une phrase plus longue :

    print(len(tokens))
    print(len(old_tokenizer.tokenize(example)))

    27
    36

Prenons un autre exemple :

    example = """class LinearLayer():
        def __init__(self, input_size, output_size):
            self.weight = torch.randn(input_size, output_size)
            self.bias = torch.zeros(output_size)

        def __call__(self, x):
            return x @ self.weights + self.bias
        """
    tokenizer.tokenize(example)

    ['class', 'ĠLinear', 'Layer', '():', 'ĊĠĠĠ', 'Ġdef', 'Ġ__', 'init', '__(', 'self', ',', 'Ġinput', '_', 'size', ',',
     'Ġoutput', '_', 'size', '):', 'ĊĠĠĠĠĠĠĠ', 'Ġself', '.', 'weight', 'Ġ=', 'Ġtorch', '.', 'randn', '(', 'input', '_',
     'size', ',', 'Ġoutput', '_', 'size', ')', 'ĊĠĠĠĠĠĠĠ', 'Ġself', '.', 'bias', 'Ġ=', 'Ġtorch', '.', 'zeros', '(',
     'output', '_', 'size', ')', 'ĊĊĠĠĠ', 'Ġdef', 'Ġ__', 'call', '__(', 'self', ',', 'Ġx', '):', 'ĊĠĠĠĠĠĠĠ',
     'Ġreturn', 'Ġx', 'Ġ@', 'Ġself', '.', 'weights', 'Ġ+', 'Ġself', '.', 'bias', 'ĊĠĠĠĠ']

En plus du token correspondant à une indentation, on peut également voir ici un token pour une double indentation : `ĊĠĠĠĠĠĠĠĠĠ`. Les mots spéciaux de Python comme `class`, `init`, `call`, `self`, et `return` sont tous tokenizés comme un seul token. Nous pouvons voir qu’en plus de séparer sur `_` et `.` le tokenizer sépare correctement même les noms en minuscules. Par exemple `LinearLayer` est tokenisé comme `["ĠLinear", "Layer"]`.

##
Sauvegarde du <i> tokenizer </i>

Pour être sûr de pouvoir l’utiliser plus tard, nous devons sauvegarder notre nouveau tokenizer. Comme pour les modèles, ceci est fait avec la méthode `save_pretrained()` :

    tokenizer.save_pretrained("code-search-net-tokenizer")

Cela créera un nouveau dossier nommé code-search-net-tokenizer contenant tous les fichiers dont le tokenizer a besoin pour être rechargé. Si vous souhaitez partager ce tokenizer avec vos collègues et amis, vous pouvez le télécharger sur le Hub en vous connectant à votre compte. Si vous travaillez dans un notebook, il existe une fonction pratique pour vous aider à le faire :

    from huggingface_hub import notebook_login

    notebook_login()

Cela affichera un widget où vous pourrez entrer vos identifiants de connexion à Hugging Face. Si vous ne travaillez pas sur un ordinateur portable, tapez simplement la ligne suivante dans votre terminal :

    huggingface-cli login

Une fois connecté, vous pouvez pousser votre tokenizer en exécutant la commande suivante :

    tokenizer.push_to_hub("code-search-net-tokenizer")

Cela créera un nouveau dépôt dans votre espace avec le nom `code-search-net-tokenizer` contenant le fichier tokenizer. Vous pouvez ensuite charger le tokenizer de n’importe où avec la méthode `from_pretrained()` :

    # Remplacez "huggingface-course" ci-dessous par votre espace réel pour utiliser votre propre tokenizer
    tokenizer = AutoTokenizer.from_pretrained("huggingface-course/code-search-net-tokenizer")

Vous êtes maintenant prêt à entraîner un modèle de langue à partir de zéro et à le finetuner sur votre tâche ! Nous verrons cela dans le chapitre 7, mais d’abord, dans le reste de ce chapitre, nous allons examiner de plus près les tokenizers rapides et explorer en détail ce qui se passe lorsque nous appelons la méthode `train_new_from_iterator()`.

Update on GitHub