J'ai essayé d'utiliser le japonais BERT avec huggingface / transformers dans article précédent, mais si vous utilisez huggingface / transformers, d'autres pré-appris Le modèle de BERT est également facile à manipuler.
Dans Liste des modèles utilisables, il semble y avoir d'autres modèles tels que Distil BERT et ALBERT qui semblent être japonais. Les deux sont positionnés comme des BERT légers.
Cette fois, tout en présentant brièvement DistilBERT fourni par Namco Bandai qui peut également être utilisé en se serrant le visage , J'ai essayé de comparer la précision avec BERT normal. Enfin, je présenterai l'une des techniques pour améliorer la précision lors de la classification des phrases avec BERT.
J'emprunterai le README du Github de Namco Bandai tel quel.
DistilBERT est un modèle présenté par Huggingface à NeurIPS 2019, et son nom signifie "Distilated-BERT". Veuillez vous référer ici pour les articles soumis.
Distil BERT est un petit modèle de transformateur rapide et léger basé sur l'architecture BERT. On dit que DistilBERT a 40% de paramètres en moins que BERT-base, fonctionne 60% plus vite et maintient 97% des performances de BERT telles que mesurées par le GLUE Benchmark.
DistilBERT est formé à l'aide de la distillation des connaissances, une technique qui comprime un grand modèle appelé enseignant en un modèle plus petit appelé étudiant. En distillant BERT, vous pouvez obtenir un modèle Transformer plus léger et plus rapide, tout en ayant de nombreuses similitudes avec le modèle BERT original.
En bref, cela ressemble à une version légère de la base BERT qui atteint une vitesse élevée (d'où la précision est légèrement inférieure à celle de la base BERT). Utilisons-le réellement pour voir à quelle vitesse il est et à quel point il est inexact.
[Github officiel](https://github.com/BandaiNamcoResearchInc/DistilBERT-base-jp/blob/master/docs/GUIDE.md#transformers-%E3%83%A9%E3%82%A4%E3%83% 96% E3% 83% A9% E3% 83% AA% E3% 83% BC% E3% 81% 8B% E3% 82% 89% E8% AA% AD% E8% BE% BC% E3% 81% BF) Vous pouvez facilement l'appeler en serrant le visage / les transformateurs dans la rue.
-Est-ce que le tokenizer provoque une erreur à moins que vous n'ajoutiez cl-tohoku /
au nom comme indiqué ci-dessous?
from transformers import AutoModel, AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("cl-tohoku/bert-base-japanese-whole-word-masking")
distil_model = AutoModel.from_pretrained("bandainamco-mirai/distilbert-base-japanese")
Fondamentalement, il peut être utilisé de la même manière que la base japonaise BERT introduite dans Dernier article, mais en raison de la différence dans la structure interne du réseau, le réglage fin est un peu. Il semble que cela doit être changé.
Pour le moment, vérifions la structure du contenu de Distil BERT.
print(distil_model)
C'est long, alors gardez-le fermé.
<détails> La différence avec BERT-base est qu'il y a 12 blocs Par conséquent, lors du réglage fin, écrivez comme suit. Un exemple de déclaration de modèle de classification est également décrit. Cette fois, il n'y a pas d'effet particulier, mais vérifions également la référence DistilBERT. Le modèle de retour est également légèrement différent. (Comme pour l'article précédent, la classification du titre du corpus d'actualités de livingoor est supposée.) Similaire à Dernière fois, il gère la tâche de classification des titres du corpus de nouvelles de livingoor. BERT-base Le temps d'apprentissage de 10 époques était d'environ 102 secondes et la précision était de 0,83 (score F). DistilBERT ――Le temps d'apprentissage de 10 époques était d'environ 60 secondes et la précision était de 0,87 (score F).
C'est bien que le temps d'apprentissage soit plus rapide, mais la précision s'est améliorée.
«À l'origine, il était censé être un peu moins précis que BERT-base, mais il semble que ce soit plus élevé.
――De toute façon, la tâche de classer les titres du corpus d'actualités de livingoor, que j'essaie toujours à titre expérimental, n'est peut-être pas très bonne ... À partir de là, je présenterai une technique pour améliorer la précision lors de la classification des phrases en japonais BERT, pas en comparaison avec Distil BERT. (À l'origine, vous devriez d'abord considérer soigneusement le prétraitement en fonction de la tâche, mais cela semble être une technique d'amélioration de la précision qui ne dépend pas beaucoup de la tâche, je vais donc l'introduire ici.) Bien que ce soit une technique, elle est introduite dans 5.3 Feature-based Approach with BERT de BERT's paper, et dans le concours NLP précédemment organisé à kaggle Cela semble être la 1ère méthode de Jigsaw Unintended Bias in Toxicity Classification. Veuillez vous référer à l'article suivant pour plus de détails sur la technique. Le fait est que sur les 12 couches Encoder de la base BERT, il est préférable de combiner les vecteurs des jetons CLS des 4 couches finales plutôt que d'utiliser uniquement les vecteurs des jetons CLS de la couche finale. Il semble. (Je ne sais pas pourquoi ...) L'idée est si simple que je vais l'essayer dans cette tâche de classification des titres du corpus d'actualités de Liveoor. fin
Recommended Posts
DistilBertModel(
(embeddings): Embeddings(
(word_embeddings): Embedding(32000, 768, padding_idx=0)
(position_embeddings): Embedding(512, 768)
(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
(dropout): Dropout(p=0.1, inplace=False)
)
(transformer): Transformer(
(layer): ModuleList(
(0): TransformerBlock(
(attention): MultiHeadSelfAttention(
(dropout): Dropout(p=0.1, inplace=False)
(q_lin): Linear(in_features=768, out_features=768, bias=True)
(k_lin): Linear(in_features=768, out_features=768, bias=True)
(v_lin): Linear(in_features=768, out_features=768, bias=True)
(out_lin): Linear(in_features=768, out_features=768, bias=True)
)
(sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
(ffn): FFN(
(dropout): Dropout(p=0.1, inplace=False)
(lin1): Linear(in_features=768, out_features=3072, bias=True)
(lin2): Linear(in_features=3072, out_features=768, bias=True)
)
(output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
)
(1): TransformerBlock(
(attention): MultiHeadSelfAttention(
(dropout): Dropout(p=0.1, inplace=False)
(q_lin): Linear(in_features=768, out_features=768, bias=True)
(k_lin): Linear(in_features=768, out_features=768, bias=True)
(v_lin): Linear(in_features=768, out_features=768, bias=True)
(out_lin): Linear(in_features=768, out_features=768, bias=True)
)
(sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
(ffn): FFN(
(dropout): Dropout(p=0.1, inplace=False)
(lin1): Linear(in_features=768, out_features=3072, bias=True)
(lin2): Linear(in_features=3072, out_features=768, bias=True)
)
(output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
)
(2): TransformerBlock(
(attention): MultiHeadSelfAttention(
(dropout): Dropout(p=0.1, inplace=False)
(q_lin): Linear(in_features=768, out_features=768, bias=True)
(k_lin): Linear(in_features=768, out_features=768, bias=True)
(v_lin): Linear(in_features=768, out_features=768, bias=True)
(out_lin): Linear(in_features=768, out_features=768, bias=True)
)
(sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
(ffn): FFN(
(dropout): Dropout(p=0.1, inplace=False)
(lin1): Linear(in_features=768, out_features=3072, bias=True)
(lin2): Linear(in_features=3072, out_features=768, bias=True)
)
(output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
)
(3): TransformerBlock(
(attention): MultiHeadSelfAttention(
(dropout): Dropout(p=0.1, inplace=False)
(q_lin): Linear(in_features=768, out_features=768, bias=True)
(k_lin): Linear(in_features=768, out_features=768, bias=True)
(v_lin): Linear(in_features=768, out_features=768, bias=True)
(out_lin): Linear(in_features=768, out_features=768, bias=True)
)
(sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
(ffn): FFN(
(dropout): Dropout(p=0.1, inplace=False)
(lin1): Linear(in_features=768, out_features=3072, bias=True)
(lin2): Linear(in_features=3072, out_features=768, bias=True)
)
(output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
)
(4): TransformerBlock(
(attention): MultiHeadSelfAttention(
(dropout): Dropout(p=0.1, inplace=False)
(q_lin): Linear(in_features=768, out_features=768, bias=True)
(k_lin): Linear(in_features=768, out_features=768, bias=True)
(v_lin): Linear(in_features=768, out_features=768, bias=True)
(out_lin): Linear(in_features=768, out_features=768, bias=True)
)
(sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
(ffn): FFN(
(dropout): Dropout(p=0.1, inplace=False)
(lin1): Linear(in_features=768, out_features=3072, bias=True)
(lin2): Linear(in_features=3072, out_features=768, bias=True)
)
(output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
)
(5): TransformerBlock(
(attention): MultiHeadSelfAttention(
(dropout): Dropout(p=0.1, inplace=False)
(q_lin): Linear(in_features=768, out_features=768, bias=True)
(k_lin): Linear(in_features=768, out_features=768, bias=True)
(v_lin): Linear(in_features=768, out_features=768, bias=True)
(out_lin): Linear(in_features=768, out_features=768, bias=True)
)
(sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
(ffn): FFN(
(dropout): Dropout(p=0.1, inplace=False)
(lin1): Linear(in_features=768, out_features=3072, bias=True)
(lin2): Linear(in_features=3072, out_features=768, bias=True)
)
(output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
)
)
)
)
transformer
dans BERT-base, mais seulement 6 dans Distil BERT. Vous pouvez également voir que la dénomination de la couche de contenu est un peu différente de BERT-base.
Modèle de déclaration
import torch
from torch import nn
import torch.nn.functional as F
from transformers import *
class DistilBertClassifier(nn.Module):
def __init__(self):
super(DistilBertClassifier, self).__init__()
# BERT-C'est le seul endroit différent de la base.
self.distil_bert = AutoModel.from_pretrained("bandainamco-mirai/distilbert-base-japanese")
#Le nombre de dimensions de la couche cachée de DistilBERT est de 768,9 catégories d'actualités Liveoor
self.linear = nn.Linear(768, 9)
#Traitement d'initialisation du poids
nn.init.normal_(self.linear.weight, std=0.02)
nn.init.normal_(self.linear.bias, 0)
def forward(self, input_ids):
vec, _ = self.distil_bert(input_ids)
#Obtenez uniquement le vecteur du premier jeton cls
vec = vec[:,0,:]
vec = vec.view(-1, 768)
#Convertir les dimensions pour la classification en couches entièrement connectées
out = self.linear(vec)
return F.log_softmax(out)
#Instance de modèle de classification
distil_classifier = DistilBertClassifier()
Réglage fin
#Tout d'abord OFF
for param in distil_classifier.parameters():
param.requires_grad = False
#Mettre à jour uniquement la dernière couche de DistilBERT ON
# BERT-la base est.encoder.layer[-1]C'était,
#Dans le cas de DistilBERT, comme confirmé la structure ci-dessus, comme suit.transfomer.layer[-1]Ce sera.
for param in distil_classifier.distil_bert.transformer.layer[-1].parameters():
param.requires_grad = True
#La classification de classe est également activée
for param in distil_classifier.linear.parameters():
param.requires_grad = True
import torch.optim as optim
#Le taux d'apprentissage est faible pour la partie pré-apprise et élevé pour la dernière couche entièrement connectée.
#N'oubliez pas de changer cela pour Distil BERT
optimizer = optim.Adam([
{'params': distil_classifier.distil_bert.transformer.layer[-1].parameters(), 'lr': 5e-5},
{'params': distil_classifier.linear.parameters(), 'lr': 1e-4}
])
Comparaison de BERT-base et Distil BERT
Définition du modèle et mise au point
class BertClassifier(nn.Module):
def __init__(self):
super(BertClassifier, self).__init__()
self.bert = BertModel.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')
#Le calque caché de BERT a 768 dimensions,9 catégories d'actualités Liveoor
self.linear = nn.Linear(768, 9)
#Traitement d'initialisation du poids
nn.init.normal_(self.linear.weight, std=0.02)
nn.init.normal_(self.linear.bias, 0)
def forward(self, input_ids):
# last_Recevoir uniquement caché
vec, _ = self.bert(input_ids)
#Obtenez uniquement le vecteur du premier jeton cls
vec = vec[:,0,:]
vec = vec.view(-1, 768)
#Convertir les dimensions pour la classification en couches entièrement connectées
out = self.linear(vec)
return F.log_softmax(out)
#Déclaration d'instance de modèle de classification
bert_classifier = BertClassifier()
#Paramètres de réglage précis
#Tout d'abord OFF
for param in bert_classifier.parameters():
param.requires_grad = False
#Mettre à jour uniquement la dernière couche de BERT ON
for param in bert_classifier.bert.encoder.layer[-1].parameters():
param.requires_grad = True
#La classification de classe est également activée
for param in bert_classifier.linear.parameters():
param.requires_grad = True
import torch.optim as optim
#Le taux d'apprentissage est faible pour la partie pré-apprise et élevé pour la dernière couche entièrement connectée.
optimizer = optim.Adam([
{'params': bert_classifier.bert.encoder.layer[-1].parameters(), 'lr': 5e-5},
{'params': bert_classifier.linear.parameters(), 'lr': 1e-4}
])
#Paramètres de la fonction de perte
loss_function = nn.NLLLoss()
Apprentissage et raisonnement
#Mesurez le temps d'étude.
import time
start = time.time()
#Paramètres GPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
#Envoyer le réseau au GPU
bert_classifier.to(device)
losses = []
#Le nombre d'époques est de 10
for epoch in range(10):
all_loss = 0
for idx, batch in enumerate(train_iter):
batch_loss = 0
bert_classifier.zero_grad()
input_ids = batch.Text[0].to(device)
label_ids = batch.Label.to(device)
out = bert_classifier(input_ids)
batch_loss = loss_function(out, label_ids)
batch_loss.backward()
optimizer.step()
all_loss += batch_loss.item()
print("epoch", epoch, "\t" , "loss", all_loss)
end = time.time()
print ("time : ", end - start)
#epoch 0 loss 251.19750046730042
#epoch 1 loss 110.7038831859827
#epoch 2 loss 82.88570280373096
#epoch 3 loss 67.0771074667573
#epoch 4 loss 56.24497305601835
#epoch 5 loss 42.61423560976982
#epoch 6 loss 35.98485875874758
#epoch 7 loss 25.728398952633142
#epoch 8 loss 20.40780107676983
#epoch 9 loss 16.567239843308926
#time : 101.97362518310547
#inférence
answer = []
prediction = []
with torch.no_grad():
for batch in test_iter:
text_tensor = batch.Text[0].to(device)
label_tensor = batch.Label.to(device)
score = bert_classifier(text_tensor)
_, pred = torch.max(score, 1)
prediction += list(pred.cpu().numpy())
answer += list(label_tensor.cpu().numpy())
print(classification_report(prediction, answer, target_names=categories))
# precision recall f1-score support
# kaden-channel 0.94 0.92 0.93 172
#dokujo-tsushin 0.75 0.86 0.80 156
# peachy 0.81 0.68 0.74 211
# movie-enter 0.78 0.81 0.80 171
# smax 0.98 0.91 0.94 176
#livedoor-homme 0.68 0.83 0.75 83
# it-life-hack 0.79 0.94 0.86 150
# topic-news 0.81 0.76 0.78 172
# sports-watch 0.89 0.82 0.85 185
# accuracy 0.83 1476
# macro avg 0.83 0.84 0.83 1476
# weighted avg 0.84 0.83 0.83 1476
Apprentissage et raisonnement
import time
#Paramètres GPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
#Envoyer le réseau au GPU
distil_classifier.to(device)
losses = []
start = time.time()
#Le nombre d'époques est de 10
for epoch in range(10):
all_loss = 0
for idx, batch in enumerate(train_iter):
batch_loss = 0
distil_classifier.zero_grad()
input_ids = batch.Text[0].to(device)
label_ids = batch.Label.to(device)
out = distil_classifier(input_ids)
batch_loss = loss_function(out, label_ids)
batch_loss.backward()
optimizer.step()
all_loss += batch_loss.item()
print("epoch", epoch, "\t" , "loss", all_loss)
end = time.time()
print ("time : ", end - start)
#/usr/local/lib/python3.6/dist-packages/ipykernel_launcher.py:26: UserWarning: Implicit dimension choice for log_softmax has been deprecated. Change the call to include dim=X as an argument.
#epoch 0 loss 450.1027842760086
#epoch 1 loss 317.39041769504547
#epoch 2 loss 211.34138756990433
#epoch 3 loss 144.4813650548458
#epoch 4 loss 106.24609130620956
#epoch 5 loss 83.87273170053959
#epoch 6 loss 68.9661111086607
#epoch 7 loss 59.31868125498295
#epoch 8 loss 49.874382212758064
#epoch 9 loss 41.56027300283313
#time : 60.22182369232178
from sklearn.metrics import classification_report
answer = []
prediction = []
with torch.no_grad():
for batch in test_iter:
text_tensor = batch.Text[0].to(device)
label_tensor = batch.Label.to(device)
score = distil_classifier(text_tensor)
_, pred = torch.max(score, 1)
prediction += list(pred.cpu().numpy())
answer += list(label_tensor.cpu().numpy())
print(classification_report(prediction, answer, target_names=categories))
#/usr/local/lib/python3.6/dist-packages/ipykernel_launcher.py:26: UserWarning: Implicit dimension choice for log_softmax has been deprecated. Change the call to include dim=X as an argument.
# precision recall f1-score support
# kaden-channel 0.93 0.96 0.95 163
#dokujo-tsushin 0.88 0.88 0.88 178
# peachy 0.86 0.75 0.80 202
# movie-enter 0.86 0.84 0.85 183
# smax 0.96 0.95 0.95 165
#livedoor-homme 0.67 0.71 0.69 96
# it-life-hack 0.91 0.91 0.91 178
# topic-news 0.80 0.86 0.83 148
# sports-watch 0.88 0.91 0.89 163
# accuracy 0.87 1476
# macro avg 0.86 0.86 0.86 1476
# weighted avg 0.87 0.87 0.87 1476
Essayez d'améliorer la précision de la base BERT
la mise en oeuvre
Modèle de déclaration
class BertClassifierRevised(nn.Module):
def __init__(self):
super(BertClassifierRevised, self).__init__()
self.bert = BertModel.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')
#Le nombre de dimensions de la couche cachée de BERT est de 768, mais comme il traite de la combinaison des vecteurs des quatre dernières couches, il est fixé à 768 x 4 dimensions.
self.linear = nn.Linear(768*4, 9)
#Traitement d'initialisation du poids
nn.init.normal_(self.linear.weight, std=0.02)
nn.init.normal_(self.linear.bias, 0)
#Préparer une fonction pour obtenir le vecteur du jeton cls
def _get_cls_vec(self, vec):
return vec[:,0,:].view(-1, 768)
def forward(self, input_ids):
#Dernière valeur de retour_hidden_Dans l'état, seule la couche finale peut être obtenue, donc
# output_hidden_states=Déclarez True pour obtenir tous les vecteurs de calque cachés,
#Troisième valeur de retour(État de toutes les couches cachées)Obtenir.
_, _, hidden_states = self.bert(input_ids, output_hidden_states=True)
#Obtenez le vecteur de jeton cls de chacune des 4 dernières couches cachées
vec1 = self._get_cls_vec(hidden_states[-1])
vec2 = self._get_cls_vec(hidden_states[-2])
vec3 = self._get_cls_vec(hidden_states[-3])
vec4 = self._get_cls_vec(hidden_states[-4])
#Combinez quatre jetons cls en un seul vecteur.
vec = torch.cat([vec1, vec2, vec3, vec4], dim=1)
#Convertir les dimensions pour la classification en couches entièrement connectées
out = self.linear(vec)
return F.log_softmax(out)
#Déclaration d'instance
bert_classifier_revised = BertClassifierRevised()
Réglage fin
#Tout d'abord OFF
for param in bert_classifier_revised.parameters():
param.requires_grad = False
#Allumez les 4 dernières couches de BERT
for param in bert_classifier_revised.bert.encoder.layer[-1].parameters():
param.requires_grad = True
for param in bert_classifier_revised.bert.encoder.layer[-2].parameters():
param.requires_grad = True
for param in bert_classifier_revised.bert.encoder.layer[-3].parameters():
param.requires_grad = True
for param in bert_classifier_revised.bert.encoder.layer[-4].parameters():
param.requires_grad = True
#La classification de classe est également activée
for param in bert_classifier_revised.linear.parameters():
param.requires_grad = True
import torch.optim as optim
#Le taux d'apprentissage est faible pour la partie pré-apprise et élevé pour la dernière couche entièrement connectée.
optimizer = optim.Adam([
{'params': bert_classifier_revised.bert.encoder.layer[-1].parameters(), 'lr': 5e-5},
{'params': bert_classifier_revised.bert.encoder.layer[-2].parameters(), 'lr': 5e-5},
{'params': bert_classifier_revised.bert.encoder.layer[-3].parameters(), 'lr': 5e-5},
{'params': bert_classifier_revised.bert.encoder.layer[-4].parameters(), 'lr': 5e-5},
{'params': bert_classifier_revised.linear.parameters(), 'lr': 1e-4}
])
#Paramètres de la fonction de perte
loss_function = nn.NLLLoss()
Apprentissage et raisonnement
import time
start = time.time()
#Paramètres GPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
#Envoyer le réseau au GPU
bert_classifier_revised.to(device)
losses = []
#Le nombre d'époques est de 5
for epoch in range(10):
all_loss = 0
for idx, batch in enumerate(train_iter):
batch_loss = 0
bert_classifier_revised.zero_grad()
input_ids = batch.Text[0].to(device)
label_ids = batch.Label.to(device)
out = bert_classifier_revised(input_ids)
batch_loss = loss_function(out, label_ids)
batch_loss.backward()
optimizer.step()
all_loss += batch_loss.item()
print("epoch", epoch, "\t" , "loss", all_loss)
end = time.time()
print ("time : ", end - start)
#epoch 0 loss 196.0047192275524
#epoch 1 loss 75.8067753687501
#epoch 2 loss 42.30751228891313
#epoch 3 loss 16.470114511903375
#epoch 4 loss 7.427484432584606
#epoch 5 loss 2.9392087209271267
#epoch 6 loss 1.5984382012393326
#epoch 7 loss 1.7370687873335555
#epoch 8 loss 0.9278695838729618
#epoch 9 loss 1.499190401067608
#time : 149.01919651031494
#inférence
answer = []
prediction = []
with torch.no_grad():
for batch in test_iter:
text_tensor = batch.Text[0].to(device)
label_tensor = batch.Label.to(device)
score = bert_classifier_revised(text_tensor)
_, pred = torch.max(score, 1)
prediction += list(pred.cpu().numpy())
answer += list(label_tensor.cpu().numpy())
print(classification_report(prediction, answer, target_names=categories))
# precision recall f1-score support
# kaden-channel 0.80 0.99 0.89 137
#dokujo-tsushin 0.89 0.86 0.88 183
# peachy 0.78 0.82 0.80 168
# movie-enter 0.87 0.88 0.87 176
# smax 0.95 0.93 0.94 168
#livedoor-homme 0.72 0.83 0.77 88
# it-life-hack 0.95 0.79 0.86 215
# topic-news 0.83 0.84 0.83 159
# sports-watch 0.92 0.86 0.89 182
# accuracy 0.86 1476
# macro avg 0.86 0.87 0.86 1476
# weighted avg 0.87 0.86 0.86 1476
en conclusion