{ "cells": [ { "cell_type": "markdown", "source": [ "# Classification de commentaires avec Camembert sans prise de tête : les fondamentaux 🇫🇷\n", "\n", "[Xiaoou WANG](https://scholar.google.fr/citations?user=vKAMMpwAAAAJ&hl=en)" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "## Motivation\n", "\n", "Camembert a été publié en juin 2020. Cependant force est de constater que l'emploi de Bert en français (Il s'agit plutôt de Roberta pour Camembert, voir [10 questions rapides sur Bert](01_theorie)) n'est pas encore une tendance. Nous pensons que cela est en partie dû au manque de tutoriels sur l'emploi des modèles pré-entraînés.\n", "\n", "Ceci est le deuxième d'une série de 10 tutoriels sur Camembert. Dans ce tuto nous allons voir notamment comment utiliser Camembert sans fine-tuning, ce dernier présupposant des connaissances relativement plus poussées.\n", "\n", "Nous commençons par installer `transformers` et `sentencepiece`, deux packages nécessaires à l'usage de Camembert. Quelques autres packages courants en machine learning ont aussi été importés." ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": 47, "metadata": { "collapsed": true }, "outputs": [], "source": [ "# !pip install transformers\n", "# !pip install sentencepiece\n", "\n", "import numpy as np\n", "import pandas as pd\n", "from sklearn.model_selection import train_test_split\n", "from sklearn.linear_model import LogisticRegression\n", "from sklearn.model_selection import GridSearchCV\n", "from sklearn.model_selection import cross_val_score\n", "import torch\n", "import transformers as ppb\n", "import warnings\n", "warnings.filterwarnings('ignore')" ] }, { "cell_type": "markdown", "source": [ "## Données\n", "\n", "Ensuite nous importons les données. Il s'agit d'un petit jeu de données que j'ai trouvé [ici](https://medium.com/@vitalshchutski/french-nlp-entamez-le-camembert-avec-les-librairies-fast-bert-et-transformers-14e65f84c148). Cela vous permettra notamment de répliquer ce tutoriel sur votre propre ordinateur, tant l'usage de GPU est peu nécessaire.\n", "\n", "La structure du dataframe est simple. Il y a une colonne commentaire avec quelques autres colonnes annotant la classe de ce commentaire. J'ai filtré les autres classes en ne gardant que `temps` pour utiliser une simple régression logistique sur les données. 1 signifie que le commentaire est lié à des problèmes de temps d'attente et 0 non." ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": 48, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Number of sentences: 322\n", "\n" ] }, { "data": { "text/plain": " review temps\n10 accueil moyen... 0\n273 .trop d'attente.insupportable trop de stress 1\n84 la boutique 0\n81 pas trop de monde contrairement à d'autres bou... 0\n276 1h30 d’attente pour un service irrespectueux. 1", "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
reviewtemps
10accueil moyen...0
273.trop d'attente.insupportable trop de stress1
84la boutique0
81pas trop de monde contrairement Ă  d'autres bou...0
2761h30 d’attente pour un service irrespectueux.1
\n
" }, "execution_count": 48, "metadata": {}, "output_type": "execute_result" } ], "source": [ "url=\"https://raw.githubusercontent.com/nlpinfrench/nlpinfrench.github.io/master/source/labeled_data.csv\"\n", "df = pd.read_csv(url,header=1,names = ['a','review','b','c','temps','e'])\n", "# Report the number of sentences.\n", "print('Number of sentences: {:,}\\n'.format(df.shape[0]))\n", "# remove unuseful columns\n", "df = df[[\"review\",\"temps\"]]\n", "# Display 5 random rows from the data.\n", "df.sample(5)" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "## Modèle et Tokenizer\n", "\n", "Ensuite nous allons importer le modèle, le tokenizer et les weights pré-entraînés. Les weights sont comme les word embeddings. Deux choses à noter :\n", "\n", "1. Bert a son propre tokenizer avec un vocabulaire fixe. Il est donc inutile de tokénisez vous-même.\n", "\n", "2. Ces weights sont issus du modèle à l'état \"brut\".\n", "\n", "En pratique vous devez fine-tuner Camembert sur des données à vous afin d'avoir des weights propres à chaque tâche et corpus.\n", "\n", "Par contre, le but de ce tuto n'est pas de fine-tuner le modèle mais de vous montrer que vous pouvez utiliser Bert avec les choses que vous connaissez déjà en word embeddings. Il s'agit d'une séance de \"désintimidation\" :D" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": 9, "outputs": [], "source": [ "# load model, tokenizer and weights\n", "camembert, tokenizer, weights = (ppb.CamembertModel, ppb.CamembertTokenizer, 'camembert-base')\n", "\n", "# Load pretrained model/tokenizer\n", "tokenizer = tokenizer.from_pretrained(weights)\n", "model = camembert.from_pretrained(weights)" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "Bert ne sait que tokéniser des phrases de longueur maximale de 512 tokens. Ici nous allons simplement enlever les commentaires trop longs." ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": 51, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "annoying review at 314 with length 556\n", "Max sentence length: 556\n" ] } ], "source": [ "# see if there are length > 512\n", "max_len = 0\n", "for i,sent in enumerate(df[\"review\"]):\n", " # Tokenize the text and add `[CLS]` and `[SEP]` tokens.\n", " input_ids = tokenizer.encode(sent, add_special_tokens=True)\n", " if len(input_ids) > 512:\n", " print(\"annoying review at\", i,\"with length\",\n", " len(input_ids))\n", " # Update the maximum sentence length.\n", " max_len = max(max_len, len(input_ids))\n", "\n", "print('Max sentence length: ', max_len)" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "code", "execution_count": 52, "outputs": [], "source": [ "# remove > 512 sentence\n", "df.drop([314],inplace=True)" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "## Padding\n", "\n", "Maintenant que la phrase la plus longue enlevée, nous faisons un padding de 472 tokens pour homogénéiser la longueur de phrases. Cela rendra l'entraînement plus simple. Nous indiquons aussi où se trouve les paddings avec `np.where` pour que Bert sache traiter les tokens de padding." ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": 35, "outputs": [ { "data": { "text/plain": "(321, 472)" }, "execution_count": 35, "metadata": {}, "output_type": "execute_result" } ], "source": [ "tokenized = df['review'].apply((lambda x: tokenizer.encode(x, add_special_tokens=True)))\n", "max_len = 0\n", "for i in tokenized.values:\n", " if len(i) > max_len:\n", " max_len = len(i)\n", "\n", "padded = np.array([i + [0]*(max_len-len(i)) for i in tokenized.values])\n", "np.array(padded).shape" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "code", "execution_count": 36, "outputs": [ { "data": { "text/plain": "(321, 472)" }, "execution_count": 36, "metadata": {}, "output_type": "execute_result" } ], "source": [ "attention_mask = np.where(padded != 0, 1, 0)\n", "attention_mask.shape" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "## Utiliser l'encodeur (encoder)\n", "\n", "Enfin nous transformer les tokens en tensor pour les passer dans le fameux transformer. Seule la dernière couche est conservée pour faire la classification." ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": 38, "outputs": [], "source": [ "input_ids = torch.tensor(padded)\n", "attention_mask = torch.tensor(attention_mask)\n", "\n", "with torch.no_grad():\n", " last_hidden_states = model(input_ids, attention_mask=attention_mask)" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "## Entraîner un modèle logistique\n", "\n", "Comme nous avons seulement besoin du premier token (CLS qui signifie classification) pour le modèle logistique, nous faisons en slice avec `[:,0,:]`.\n" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 39, "outputs": [], "source": [ "features = last_hidden_states[0][:,0,:].numpy()\n", "labels = df.temps\n", "labels\n" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "Que l'entraînement commence ! Nous commençons par faire un split train/test avec Scikit-Learn. Ensuite nous utilisons Grid Search pour essayer de trouver le meilleur paramètre. Finalement on entraîne le modèle." ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": 41, "outputs": [], "source": [ "train_features, test_features, train_labels, test_labels = train_test_split(features, labels)" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "code", "execution_count": 42, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "best parameters: {'C': 5.263252631578947}\n", "best scrores: 0.8625\n" ] } ], "source": [ "# Grid search\n", "parameters = {'C': np.linspace(0.0001, 100, 20)}\n", "grid_search = GridSearchCV(LogisticRegression(), parameters)\n", "grid_search.fit(train_features, train_labels)\n", "\n", "print('best parameters: ', grid_search.best_params_)\n", "print('best scrores: ', grid_search.best_score_)" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "code", "execution_count": 44, "outputs": [ { "data": { "text/plain": "LogisticRegression(C=5.263252631578947, class_weight=None, dual=False,\n fit_intercept=True, intercept_scaling=1, l1_ratio=None,\n max_iter=100, multi_class='auto', n_jobs=None, penalty='l2',\n random_state=None, solver='lbfgs', tol=0.0001, verbose=0,\n warm_start=False)" }, "execution_count": 44, "metadata": {}, "output_type": "execute_result" } ], "source": [ "lr_clf = LogisticRegression(C=grid_search.best_params_['C'])\n", "lr_clf.fit(train_features, train_labels)\n" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "## Résultats et conclusions\n", "\n", "Nous arrivons donc à une précision de 91.36%. Si nous avions utilisé un classifieur aléatoire la précision aurait été autour de 60%.\n", "\n", "Voilà ! Bravo d'avoir lu jusqu'ici. Nous espérons que vous avez vu que finalement Bert n'était pas si difficile à comprendre. Il s'agit juste d'un encoder auquel on ajoute un algorithme de classification.\n", "\n", "La vraie force de Bert réside dans ses possibilités de fine-tuning. A bientôt donc pour un cas pratique en classification de documents :D" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": 45, "outputs": [ { "data": { "text/plain": "0.9135802469135802" }, "execution_count": 45, "metadata": {}, "output_type": "execute_result" } ], "source": [ "lr_clf.score(test_features, test_labels)" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "code", "execution_count": 54, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Dummy classifier score: 0.621 (+/- 0.15)\n" ] } ], "source": [ "from sklearn.dummy import DummyClassifier\n", "clf = DummyClassifier()\n", "\n", "scores = cross_val_score(clf, train_features, train_labels)\n", "print(\"Dummy classifier score: %0.3f (+/- %0.2f)\" % (scores.mean(), scores.std() * 2))" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "## Références principales :\n", "\n", "https://jalammar.github.io/a-visual-guide-to-using-bert-for-the-first-time/\n", "\n", "https://camembert-model.fr/" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 2 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython2", "version": "2.7.6" } }, "nbformat": 4, "nbformat_minor": 0 }