[PYTHON] Trennung von japanischem Nachnamen und Vornamen mit BERT

Abhängig von der Datenbank werden Vor- und Nachname zusammen gespeichert, und es besteht möglicherweise der Wunsch, sie mechanisch zu trennen. Es ist überraschend schwierig, dies selbst mit einer vollständigen Liste von Nachnamen zu tun. BERT ist momentan ein heißes Thema, aber ich möchte es gerne vorstellen, da ich den Nachnamen und den Vornamen mit hoher Genauigkeit trennen konnte, indem ich den Namen der Person lernte.

Ergebnis

Der Quellcode ist ziemlich lang, daher werde ich ihn anhand des Ergebnisses anzeigen. Wir konnten 1.200 Verifizierungsdaten mit einer Genauigkeit von ** 99,0% ** trennen. Der Inhalt der Verifizierungsdaten und ein Teil der Vorhersage (nur Nachname) sind wie folgt.

last_name first_name full_name pred
89 Gestalten Mayu Mayu Katabe Gestalten
1114 Kumazoe Norio Norio Kumazoe Kumazoe
1068 Kimoto Souho Kimoto Soho Kimoto
55 Haus Hiroki Hiroki Iegami Haus
44 Basic Shodai Kishodai Basic

Die 12 fehlgeschlagenen Fälle sind wie folgt. Selbst für Menschen wird Toshikatsu Sabune wahrscheinlich in Toshikatsu Sabane unterteilt.

last_name first_name full_name pred
11 Toshi Saburi Sieg Toshikatsu Sabane Sabane
341 Bürste Kasumi Sumi-Pinsel Pinsel Blume
345 Schintoismus Shinichi Shinichi Shinto Makoto Shinto
430 Kastanie Kanae Kanae Kuri Kurika
587 Keisuke Nina Kei Ryojina Kei
785 Bansho Gut Bansho Wende
786 Yutaka Wakana Kana Toyowa Toyokazu
995 Seri Yu Seriyoshi Se
1061 Damit Echte Prinzessin Irgendwann Somi
1062 Spann Obst Kogi Nomi Koki
1155 Hotaka Natsuho Hotaka Natsuho Hotakaho
1190 Extrem durchschnittlich Traum Extremer Traum sehr

Übrigens: Verwenden Sie nur das voreingestellte Wörterbuch (ipadic) mit janome (ein morphologisches Analysetool, das nur mit Python ausgeführt wird und die gleiche Leistung wie Mecab aufweist) Bei einer einfachen Trennung von Vor- und Nachnamen betrug die Genauigkeit 34,5%.

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

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

Wenn Sie das Nachnamen-Wörterbuch von Last Name Database erhalten und es wie folgt an janome weiterleiten, ist es korrekt. Hat sich auf 79,7% verbessert.

tokenizer2 = Tokenizer('last_name_dic.csv', udic_enc="utf8")
def extract_last_name2(sentence):
    token_arr = [token for token in tokenizer2.tokenize(sentence)]
    if 'Nachname' 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))

Vielleicht wird das Hinzufügen einer Namensliste die Genauigkeit weiter verbessern, aber es schien ziemlich schwierig zu sein, sie zu erhalten und zu verarbeiten, daher würde ich sie gerne überprüfen, wenn ich Zeit habe. (Ich glaube nicht)

Quellcode

Importieren Sie zuerst das, was Sie brauchen.

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

Als Voreinstellung denke ich, dass es viele Beispiele für die Verwendung von "Bert-Base-Japanisch-Ganzwort-Maskierung" gibt, aber da es schwierig ist, sie zum Zeitpunkt der Tokenisierung seltsam zu teilen, diesmal jeweils ein Zeichen nach dem anderen Verwenden Sie das Zeichen, um zu teilen.

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

Ich habe Amazing Name Generator als Quelle für die Lehrerdaten verwendet. Es ist eine interessante Seite, wenn man nur die Ungewöhnlichkeit des Namens betrachtet, der quantifiziert wird. Diesmal betrug die Anzahl der Namen 48.000 und die Arten der Nachnamen etwa 22.000. Es ist ziemlich weit verbreitet, einschließlich kleinerer Nachnamen. Das einzige Format von csv ist full_name, last_name, first_name. Tokenisieren Sie zunächst jedes Zeichen mit dem folgenden Code.

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]

Als Fluss wird nach dem Teilen von full_name durch jeweils ein Zeichen die Wahrscheinlichkeit angegeben, dass jedes Zeichen 1 für den Nachnamen und 0 für den Vornamen ist. Damit BERT die richtigen Antwortdaten versteht, z. B. Taro Tanaka-> ['Ta', 'Mitte', 'Ta', 'Ro'] -> [1, 1, 0, 0] Ich werde es schaffen. Aufmerksamkeitsmasken ist einfach das Zielarray, das durch 1 ersetzt wird (möglicherweise nicht erforderlich).

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...]Erstellen Sie ein Tag-Array
        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 muss alle dieselbe Token-Array-Länge haben, damit das Auffüllen durchgeführt wird. Teilen Sie dann den Datensatz auf.

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)

Verwenden Sie GPUorCPU, um das Dataset zu laden. Laden Sie dann das vorgefertigte Modell.

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()

Zeigt eine Übersicht über das Modell. Es hat nichts mit der Verarbeitung zu tun, daher können Sie es überspringen.

# 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()))))

Genauigkeit definieren. Wird nur zum Anzeigen von Validierungsdaten verwendet. Hier ist es der F1-Wert. Wenn für jedes Zeichen der Nachname oder Vorname beurteilt wird und er hoch ist, nähert er sich 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))

Dies ist der Hauptteil des Trainings.

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)

#Ausbildung
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))

Während des Trainings ... Es dauerte ungefähr 10 Minuten.

Speichern Sie das trainierte Modell hier.

pd.to_pickle(model_token_cls, 'Vor- und Nachname Trennungsmodell.pkl')

Basierend auf den separat vorbereiteten Verifizierungsdaten geben wir die als Nachname bewerteten Zeichen in Schlüsselwörter ein. Die vom obigen Tokenizer definierten Zeichen sind 4.000 Zeichen, und die nicht in der Liste enthaltenen Zeichen sind [UNK]. Es scheint für den Tokenizer schwierig zu sein, sich an alles zu erinnern, einschließlich der Varianten. Deshalb habe ich beschlossen, in solchen Fällen eine spezielle Verarbeitung hinzuzufügen.

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])
        #Wenn das Wiederherstellungsergebnis unbekannt ist, ermitteln Sie von Anfang an die Anzahl der Zeichen im Ergebnis.
        if '[UNK]' in s:
            s = s2[j][0:len(s)]
        
        keywords.append(''.join(s))

Trotzdem mag das Urteil seltsam sein, wenn das gleiche Kanji im Nachnamen und im Namen enthalten ist, aber es scheint noch besser zu sein, wenn die Bedingung, dass die Nachnamen fortlaufend sind, später hinzugefügt wird. Ich fand heraus, dass ich, selbst wenn ich keine Liste mit Nachnamen und Namen erstellt hätte, gut lernen könnte, indem ich den vollständigen Namen bis zu einem gewissen Grad in BERT einfügte. Dieses Mal bin ich mir nicht sicher, was ich tue, aber durch die ordnungsgemäße Erstellung von Lehrerdaten kann dieselbe Implementierung auf die Schlüsselwortextraktionslogik in Sätzen angewendet werden.

Recommended Posts

Trennung von japanischem Nachnamen und Vornamen mit BERT
Koexistenz von Fcitx und Zoom ~ Mit japanischer Lokalisierung ~
Koexistenz von Python2 und 3 mit CircleCI (1.0)
PyOpenGL GUI Auswahl und Trennung von Zeichnung und GUI
Trennung von Design und Daten in matplotlib
Ich habe versucht, die Genauigkeit der japanischen BERT- und der japanischen Distil-BERT-Satzklassifizierung mit PyTorch & Einführung der BERT-Technik zur Verbesserung der Genauigkeit zu vergleichen
[Japanische Version] Beurteilung der Wortähnlichkeit für Polynomwörter mit ELMo und BERT
Holen Sie sich mit Python den Aktienkurs eines japanischen Unternehmens und erstellen Sie eine Grafik
Wickeln Sie japanische Sätze gut ein und zeigen Sie sie mit Pyglet an
Holen Sie sich den Git-Zweignamen und den Tag-Namen mit Python
Schreiben Sie den Namen des Tags mit dem Namespace in lxml neu
Skript zum Twittern mit Vielfachen von 3 und Zahlen mit 3 !!
TRIE-Baumimplementierung mit Python und LOUDS
Zip mit Python extrahieren (unterstützt japanische Dateinamen)
Wavelet-Konvertierung von Bildern mit PyWavelets und OpenCV
Fortsetzung der Multi-Plattform-Entwicklung mit Electron und Python
Beispiel für das Lesen und Schreiben von CSV mit Python