[PYTHON] Séparation du nom et du prénom japonais avec BERT

En fonction de la base de données, le prénom et le nom sont stockés ensemble, et il peut y avoir un désir de les séparer mécaniquement. Il est étonnamment difficile de faire cela même avec une liste complète de noms de famille. Le BERT est un sujet brûlant en ce moment, mais je voudrais le présenter car j'ai pu séparer le prénom et le nom avec une grande précision en apprenant le prénom de la personne.

résultat

Le code source est assez long, je vais donc le montrer à partir du résultat. Nous avons pu séparer 1 200 données de vérification avec une précision de ** 99,0% **. Le contenu des données de vérification et une partie de la prédiction (nom de famille uniquement) sont les suivants.

last_name first_name full_name pred
89 Forme Mayu Mayu Katabe Forme
1114 Kumazoe Norio Norio Kumazoe Kumazoe
1068 Kimoto Souho Kimoto Soho Kimoto
55 Maison Hiroki Hiroki Iegami Maison
44 De base Shodai Kishodai De base

Les 12 cas ayant échoué sont les suivants. Même pour les humains, Toshikatsu Sabune est susceptible d'être divisé en Toshikatsu Sabane.

last_name first_name full_name pred
11 Toshi Saburi Gagner Toshikatsu Sabane Sabane
341 Brosse Kasumi Brosse Sumi Brosse fleur
345 Shinto Shinichi Shinichi Shinto Makoto Shinto
430 châtaigne Kanae Kanae Kuri Kurika
587 Keisuke Nina Kei Ryojina Kei
785 Bansho Bien Bansho Tour
786 Yutaka Wakana Kana Toyowa Toyokazu
995 Seri Yu Seriyoshi Se
1061 Alors Vraie princesse Somihime Somi
1062 Cambrure fruit Kogi Nomi Koki
1155 Hotaka Natsuho Hotaka Natsuho Hotakaho
1190 Extrêmement moyen rêver Rêve extrême très

À propos, en utilisant uniquement le dictionnaire prédéfini (ipadic) avec janome (un outil d'analyse morphologique qui est complété uniquement avec python et a les mêmes performances que mecab) comme suit Avec une simple séparation du prénom et du nom, la précision était de 34,5%.

def extract_last_name(sentence):
    for token in tokenizer.tokenize(sentence):
        if 'Nom de famille' in token.part_of_speech:
            return token.surface

df['pred'] = df['full_name'].apply(lambda full_name: extract_last_name(full_name))

Si vous obtenez le dictionnaire des noms de famille de Last Name Database et le transmettez à janome comme suit, il sera exact. S'est amélioré à 79,7%.

tokenizer2 = Tokenizer('last_name_dic.csv', udic_enc="utf8")
def extract_last_name2(sentence):
    token_arr = [token for token in tokenizer2.tokenize(sentence)]
    if 'Nom de famille' in token_arr[0].part_of_speech:
        return token_arr[0].surface

df['pred2'] = df['full_name'].apply(lambda full_name: extract_last_nam2(full_name))

Peut-être que l'ajout d'une liste de noms améliorera encore la précision, mais cela semblait assez difficile à obtenir et à traiter, alors j'aimerais la vérifier si j'ai le temps. (Je ne pense pas)

Code source

Importez d'abord ce dont vous avez besoin.

import pandas as pd
import numpy as np
from transformers import BertConfig, BertTokenizer, BertJapaneseTokenizer, BertForTokenClassification
from keras.preprocessing.sequence import pad_sequences
import torch
import MeCab
import math

En tant que préréglage, je pense qu'il existe de nombreux exemples d'utilisation de `` bert-base-japanese-whole-word-masking '', mais comme il est difficile de le diviser étrangement au moment du tokenize, cette fois un caractère à la fois Utilisez le caractère pour diviser.

tokenizer = BertJapaneseTokenizer.from_pretrained('cl-tohoku/bert-base-japanese-char-whole-word-masking')

J'ai utilisé Amazing Name Generator comme source des données de l'enseignant. C'est un site intéressant rien qu'en regardant le caractère inhabituel du nom, qui est quantifié. Cette fois, le nombre de noms était de 48 000 et les types de noms de famille étaient d'environ 22 000. Il est assez largement diffusé, y compris les noms de famille mineurs. Le seul format de csv est full_name, last_name, first_name. Tout d'abord, jetez chaque caractère avec le code suivant.

df = pd.read_csv('name_list.csv')
text1s = list(df.full_name.values)
targets = list(df.last_name.values)
text1_tokenize = [tokenizer.encode(s) for s in text1s]
target_tokenize = [[tokenizer.encode(vv)[1:-1] for vv in v]  for v in targets]

En règle générale, après avoir divisé full_name un caractère à la fois, la probabilité que chaque caractère soit 1 pour le nom et 0 pour le prénom sera donnée. Pour que BERT comprenne les données de réponse correctes, par exemple, Taro Tanaka-> ['Ta', 'Middle', 'Ta', 'Ro'] -> [1, 1, 0, 0] Je vais y arriver. attention_masks est simplement le tableau cible remplacé par 1 (peut-être inutile)

def make_tags_arr(x, token):
    start_indexes = arr_indexes(x, token)
    max_len = len(x)
    token_len = len(token)
    arr = [0] * max_len
    for i in start_indexes:
        arr[i:i+token_len] = [1] * token_len
    return arr

tags_ids = []
for i in range(len(text1_tokenize)):
    text1 = text1_tokenize[i]
    targets = target_tokenize[i]
    
    tmp = [0] * len(text1)
    for t in targets:
        # [0,0,1,1,0,0...]Créer un tableau de balises
        arr = make_tags_arr(text1, t)
        tmp = [min(x + y, 1) for (x, y) in zip(tmp, arr)]
    tags_ids.append(tmp)

attention_masks = [[float(i > 0) for i in ii] for ii in text1_tokenize]

BERT doit tous avoir la même longueur de tableau de jetons, donc le remplissage est effectué. Ensuite, divisez l'ensemble de données.

MAX_LEN = 32
input_ids = pad_sequences(text1_tokenize, maxlen=MAX_LEN, dtype="long", truncating="pre", padding="pre")
tags_ids = pad_sequences(tags_ids, maxlen=MAX_LEN, dtype="long", truncating="pre", padding="pre")
attention_masks = pad_sequences(attention_masks, maxlen=MAX_LEN, dtype="long", truncating="pre", padding="pre")

from sklearn.model_selection import train_test_split
RAN_SEED = 2020
train_inputs, validation_inputs, train_labels, validation_labels = train_test_split(input_ids, tags_ids, random_state=RAN_SEED, test_size=0.1)
train_masks, validation_masks = train_test_split(attention_masks, random_state=RAN_SEED, test_size=0.2)

train_inputs = torch.LongTensor(train_inputs)
validation_inputs = torch.LongTensor(validation_inputs)
train_labels = torch.LongTensor(train_labels)
validation_labels = torch.LongTensor(validation_labels)
train_masks = torch.LongTensor(train_masks)
validation_masks = torch.LongTensor(validation_masks)

Utilisez GPUorCPU pour charger l'ensemble de données. Chargez ensuite le modèle pré-entraîné.

if torch.cuda.is_available():    
    # Tell PyTorch to use the GPU.    
    device = torch.device("cuda")
    print('There are %d GPU(s) available.' % torch.cuda.device_count())
    print('We will use the GPU:', torch.cuda.get_device_name(0))
# If not...
else:
    print('No GPU available, using the CPU instead.')
    device = torch.device("cpu")

from torch.utils.data import TensorDataset, DataLoader, RandomSampler, SequentialSampler
batch_size = 32

# Create the DataLoader for our training set.
train_data = TensorDataset(train_inputs, train_masks, train_labels)
train_sampler = RandomSampler(train_data)
train_dataloader = DataLoader(train_data, sampler=train_sampler, batch_size=batch_size)
# Create the DataLoader for our validation set.
validation_data = TensorDataset(validation_inputs, validation_masks, validation_labels)
validation_sampler = SequentialSampler(validation_data)
validation_dataloader = DataLoader(validation_data, sampler=validation_sampler, batch_size=batch_size)

from transformers import AdamW, BertConfig
model_token_cls = BertForTokenClassification.from_pretrained('cl-tohoku/bert-base-japanese-char-whole-word-masking', num_labels=2)
model_token_cls.cuda()

Affiche un aperçu du modèle. Il n'est pas lié au traitement, vous pouvez donc l'ignorer.

# Get all of the model's parameters as a list of tuples.
params = list(model_token_cls.named_parameters())
print('The BERT model has {:} different named parameters.\n'.format(len(params)))
print('==== Embedding Layer ====\n')
for p in params[0:5]:
    print("{:<55} {:>12}".format(p[0], str(tuple(p[1].size()))))
print('\n==== First Transformer ====\n')
for p in params[5:21]:
    print("{:<55} {:>12}".format(p[0], str(tuple(p[1].size()))))
print('\n==== Output Layer ====\n')
for p in params[-4:]:
    print("{:<55} {:>12}".format(p[0], str(tuple(p[1].size()))))

Définissez la précision. Utilisé uniquement pour afficher les données de validation. Ici, c'est la valeur F1. Pour chaque personnage, si le nom ou le prénom est jugé et qu'il est élevé, il s'approche de 1.

import datetime
def flat_accuracy(pred_masks, labels, input_masks):
    tp = ((pred_masks == 1) * (labels == 1)).sum().item()
    fp = ((pred_masks == 1) * (labels == 0)).sum().item()
    fn = ((pred_masks == 0) * (labels == 1)).sum().item()
    tn = ((pred_masks == 0) * (labels == 0)).sum().item()
    precision = tp/(tp+fp)
    recall = tp/(tp+fn)
    f1 = 2*precision*recall/(precision+recall)

    return f1

def format_time(elapsed):
    # Round to the nearest second.
    elapsed_rounded = int(round((elapsed)))
    
    # Format as hh:mm:ss
    return str(datetime.timedelta(seconds=elapsed_rounded))

C'est la partie principale de la formation.

from torch.optim import Adam
from transformers import get_linear_schedule_with_warmup

param_optimizer = list(model_token_cls.named_parameters())
no_decay = ["bias", "gamma", "beta"]
optimizer_grouped_parameters = [
  {'params' : [p for n, p in param_optimizer if not any (nd in n for nd in no_decay)],
  'weight_decay_rate' : 0.01},
  {'params' : [p for n, p in param_optimizer if any(nd in n for nd in no_decay)],
  'weight_decay_rate' : 0.0}
]
optimizer = Adam(optimizer_grouped_parameters, lr=3e-5)

epochs = 3
max_grad_norm = 1.0
total_steps = len(train_dataloader) * epochs
scheduler = get_linear_schedule_with_warmup(optimizer, 
                                            num_warmup_steps = 0,
                                            num_training_steps = total_steps)

#entraînement
for epoch_i in range(epochs):
    # TRAIN loop
    model_token_cls.train()
    train_loss = 0
    nb_train_examples, nb_train_steps = 0, 0
    t0 = time.time()
    
    print('======== Epoch {:} / {:} ========'.format(epoch_i + 1, epochs))
    print('Training...')
    
    for step, batch in enumerate(train_dataloader):
        
        if step % 40 == 0 and not step == 0:
            elapsed = format_time(time.time() - t0)
            print('  Batch {:>5,}  of  {:>5,}.    Elapsed: {:}.'.format(step, len(train_dataloader), elapsed))
        
        b_input_ids = batch[0].to(device)
        b_input_mask = batch[1].to(device)
        b_labels = batch[2].to(device)
        # forward pass
        loss = model_token_cls(b_input_ids, token_type_ids = None, attention_mask = b_input_mask, labels = b_labels)
        # backward pass
        loss[0].backward()
        # track train loss
        train_loss += loss[0].item()
        nb_train_examples += b_input_ids.size(0)
        nb_train_steps += 1
        # gradient clipping
        torch.nn.utils.clip_grad_norm_(parameters = model_token_cls.parameters(), max_norm = max_grad_norm)
        # update parameters
        optimizer.step()
        scheduler.step()
        model_token_cls.zero_grad()
        
    # Calculate the average loss over the training data.
    avg_train_loss = train_loss / len(train_dataloader)
    
    print("")
    print("  Average training loss: {0:.2f}".format(avg_train_loss))
    print("  Training epcoh took: {:}".format(format_time(time.time() - t0)))
    
    # ========================================
    #               Validation
    # ========================================
    print("")
    print("Running Validation...")
    t0 = time.time()
    model_token_cls.eval()
    eval_loss, eval_accuracy = 0, 0
    nb_eval_steps, nb_eval_examples = 0, 0
    for batch in validation_dataloader:
        batch = tuple(t.to(device) for t in batch)

        b_input_ids, b_input_mask, b_labels = batch
        
        with torch.no_grad():        
            outputs = model_token_cls(b_input_ids, token_type_ids = None, attention_mask = b_input_mask, labels = b_labels)
        
        result = outputs[1].to('cpu')

        labels = b_labels.to('cpu')
        input_mask = b_input_mask.to('cpu')
        
        # Mask predicted label
        pred_masks = torch.min(torch.argmax(result, dim=2), input_mask)
        
        # Calculate the accuracy for this batch of test sentences.
        tmp_eval_accuracy = flat_accuracy(pred_masks, labels, input_mask)
        
        # Accumulate the total accuracy.
        eval_accuracy += tmp_eval_accuracy
        # Track the number of batches
        nb_eval_steps += 1
        
    # Report the final accuracy for this validation run.
    print("  Accuracy: {0:.2f}".format(eval_accuracy/nb_eval_steps))
    print("  Validation took: {:}".format(format_time(time.time() - t0)))

print("Train loss: {}".format(train_loss / nb_train_steps))

Pendant l'entraînement ... Cela a pris environ 10 minutes.

Enregistrez le modèle entraîné ici.

pd.to_pickle(model_token_cls, 'Modèle de séparation du prénom et du nom.pkl')

Sur la base des données de vérification préparées séparément, nous entrerons en fait les caractères considérés comme le nom de famille dans les mots-clés. Les caractères définis par le tokenizer ci-dessus sont de 4 000 caractères et les caractères non inclus dans la liste sont [UNK]. Il semble difficile pour le tokenizer de se souvenir de tout, y compris des variantes, j'ai donc décidé d'ajouter un traitement spécial dans de tels cas.

df = pd.read_csv('name_list_valid.csv')
keywords = []
MAX_LEN = 32
alls = list(df.full_name)
batch_size = 100

for i in range(math.ceil(len(alls)/batch_size)):
    print(i)
    s2 = list(df.full_name[i*batch_size:(i+1)*batch_size])
    d = torch.LongTensor(pad_sequences([tokenizer.encode(s) for s in s2], maxlen=MAX_LEN, dtype="long", truncating="pre", padding="pre")).cuda()
    attention_mask = (d > 0) * 1
    output = model_token_cls(d, token_type_ids = None, attention_mask = attention_mask)
    result = output[0].to('cpu')
    pred_masks = torch.min(torch.argmax(result, dim=2), attention_mask.to('cpu'))
    d = d.to('cpu')

    pred_mask_squeeze = pred_masks.nonzero().squeeze()
    b = d[pred_mask_squeeze.T.numpy()]
    pred_mask_squeeze[:,1]=b
    for j in range(len(s2)):
        tmp = pred_mask_squeeze[pred_mask_squeeze[:,0] == j]
        s = tokenizer.convert_ids_to_tokens(tmp[:,1])
        #Si le résultat de la restauration contient inconnu, obtenez le nombre de caractères du résultat depuis le début.
        if '[UNK]' in s:
            s = s2[j][0:len(s)]
        
        keywords.append(''.join(s))

Pourtant, le jugement peut être étrange si le même kanji est inclus dans le nom de famille et le nom, mais il semble que ce sera encore mieux si la condition que les noms de famille soient continus est ajoutée plus tard. J'ai trouvé que même si je ne faisais pas vraiment de liste de noms et de noms, je pourrais bien apprendre en mettant le nom complet dans BERT dans une certaine mesure. Cette fois, je suis un peu confus sur ce que je fais, mais en créant correctement les données des enseignants, la même implémentation peut être appliquée à la logique d'extraction de mots-clés dans les phrases.

Recommended Posts

Séparation du nom et du prénom japonais avec BERT
Coexistence de Fcitx et Zoom ~ Avec localisation japonaise ~
Coexistence de Python2 et 3 avec CircleCI (1.0)
Sélection de l'interface graphique PyOpenGL et séparation du dessin et de l'interface graphique
Séparation de la conception et des données dans matplotlib
J'ai essayé de comparer la précision de la classification des phrases BERT japonaises et japonaises Distil BERT avec PyTorch et introduction de la technique d'amélioration de la précision BERT
[Version japonaise] Jugement de la similitude des mots pour les mots polynomiaux utilisant ELMo et BERT
Obtenez le cours de l'action d'une entreprise japonaise avec Python et faites un graphique
Enveloppez et affichez bien les phrases japonaises avec pyglet
Obtenez le nom de la branche git et le nom de la balise avec python
Renommer la balise avec un espace de noms en lxml
Script pour tweeter avec des multiples de 3 et des nombres avec 3 !!
Implémentation de l'arbre TRIE avec Python et LOUDS
Extraire le zip avec Python (prend en charge les noms de fichiers japonais)
Conversion en ondelettes d'images avec PyWavelets et OpenCV
Poursuite du développement multi-plateforme avec Electron et Python
Exemple de lecture et d'écriture de CSV avec Python