Keras 2 : examples : NLP – Transformers による固有表現認識 (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 05/30/2022 (keras 2.9.0)
* 本ページは、Keras の以下のドキュメントを翻訳した上で適宜、補足説明したものです:
- Code examples : Natural Language Processing : Named Entity Recognition using Transformers (Author: Varun Singh)
* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
- 人工知能研究開発支援
- 人工知能研修サービス(経営者層向けオンサイト研修)
- テクニカルコンサルティングサービス
- 実証実験(プロトタイプ構築)
- アプリケーションへの実装
- 人工知能研修サービス
- PoC(概念実証)を失敗させないための支援
- お住まいの地域に関係なく Web ブラウザからご参加頂けます。事前登録 が必要ですのでご注意ください。
◆ お問合せ : 本件に関するお問い合わせ先は下記までお願いいたします。
- 株式会社クラスキャット セールス・マーケティング本部 セールス・インフォメーション
- sales-info@classcat.com ; Web: www.classcat.com ; ClassCatJP
Keras 2 : examples : 自然言語処理 – Transformers による固有表現認識
Description : Transformers と CoNLL 2003 共有タスクからのデータを使用した NER。
イントロダクション
固有表現認識 (NER) はテキストの固有表現を識別するプロセスです。固有表現の例は : “Person”, “Location”, “Organization”, “Dates” 等です。NER は本質的にはトークン分類タスクで、総てのトークンは一つまたはそれ以上の事前定義されたカテゴリーに分類されます。
この課題では、NER を遂行するために単純な Transformer ベースのモデルを訓練します。CoNLL 2003 共有タスクからのデータを使用していきます。データセットの詳細は、データセット web サイト にアクセスしてください。しかしながら、このデータの取得はフリーライセンスを得る追加のステップを必要としますので、HuggingFace のデータセット・ライブラリを使用していきます、これはこのデータセットの加工バージョンを含みます。
HuggingFace のオープンソース・データセット・ライブラリのインストール
また NER モデルを評価するために使用されるスクリプトもダウンロードします。
!pip3 install datasets
!wget https://raw.githubusercontent.com/sighsmile/conlleval/master/conlleval.py
import os
import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
from datasets import load_dataset
from collections import Counter
from conlleval import evaluate
この素晴らしい サンプル からの transformer 実装を使用しています。
TransformerBlock 層の定義から始めましょう :
class TransformerBlock(layers.Layer):
def __init__(self, embed_dim, num_heads, ff_dim, rate=0.1):
super(TransformerBlock, self).__init__()
self.att = keras.layers.MultiHeadAttention(
num_heads=num_heads, key_dim=embed_dim
)
self.ffn = keras.Sequential(
[
keras.layers.Dense(ff_dim, activation="relu"),
keras.layers.Dense(embed_dim),
]
)
self.layernorm1 = keras.layers.LayerNormalization(epsilon=1e-6)
self.layernorm2 = keras.layers.LayerNormalization(epsilon=1e-6)
self.dropout1 = keras.layers.Dropout(rate)
self.dropout2 = keras.layers.Dropout(rate)
def call(self, inputs, training=False):
attn_output = self.att(inputs, inputs)
attn_output = self.dropout1(attn_output, training=training)
out1 = self.layernorm1(inputs + attn_output)
ffn_output = self.ffn(out1)
ffn_output = self.dropout2(ffn_output, training=training)
return self.layernorm2(out1 + ffn_output)
次に、TokenAndPositionEmbedding 層を定義しましょう :
class TokenAndPositionEmbedding(layers.Layer):
def __init__(self, maxlen, vocab_size, embed_dim):
super(TokenAndPositionEmbedding, self).__init__()
self.token_emb = keras.layers.Embedding(
input_dim=vocab_size, output_dim=embed_dim
)
self.pos_emb = keras.layers.Embedding(input_dim=maxlen, output_dim=embed_dim)
def call(self, inputs):
maxlen = tf.shape(inputs)[-1]
positions = tf.range(start=0, limit=maxlen, delta=1)
position_embeddings = self.pos_emb(positions)
token_embeddings = self.token_emb(inputs)
return token_embeddings + position_embeddings
keras.Model サブクラスとして NER モデルクラスを構築する
class NERModel(keras.Model):
def __init__(
self, num_tags, vocab_size, maxlen=128, embed_dim=32, num_heads=2, ff_dim=32
):
super(NERModel, self).__init__()
self.embedding_layer = TokenAndPositionEmbedding(maxlen, vocab_size, embed_dim)
self.transformer_block = TransformerBlock(embed_dim, num_heads, ff_dim)
self.dropout1 = layers.Dropout(0.1)
self.ff = layers.Dense(ff_dim, activation="relu")
self.dropout2 = layers.Dropout(0.1)
self.ff_final = layers.Dense(num_tags, activation="softmax")
def call(self, inputs, training=False):
x = self.embedding_layer(inputs)
x = self.transformer_block(x)
x = self.dropout1(x, training=training)
x = self.ff(x)
x = self.dropout2(x, training=training)
x = self.ff_final(x)
return x
データセット・ライブラリから CoNLL 2003 データセットをロードして加工する
conll_data = load_dataset("conll2003")
このデータをタブ区切りファイル形式にエクスポートします、これは tf.data.Dataset オブジェクトとして容易に読めます。
def export_to_file(export_file_path, data):
with open(export_file_path, "w") as f:
for record in data:
ner_tags = record["ner_tags"]
tokens = record["tokens"]
if len(tokens) > 0:
f.write(
str(len(tokens))
+ "\t"
+ "\t".join(tokens)
+ "\t"
+ "\t".join(map(str, ner_tags))
+ "\n"
)
os.mkdir("data")
export_to_file("./data/conll_train.txt", conll_data["train"])
export_to_file("./data/conll_val.txt", conll_data["validation"])
NER ラベル検索テーブルの作成
NER ラベルは通常は IOB, IOB2 or IOBES 形式で提供されます。詳細はこのリンクを確認してください : Wikipedia
0 はパデングのために予約されますので、ラベルの番号付けは 1 から始まることに注意してください。全部で 10 個のラベルを持ちます : NER データセットから 9 つ、パディングのために 1 つです。
def make_tag_lookup_table():
iob_labels = ["B", "I"]
ner_labels = ["PER", "ORG", "LOC", "MISC"]
all_labels = [(label1, label2) for label2 in ner_labels for label1 in iob_labels]
all_labels = ["-".join([a, b]) for a, b in all_labels]
all_labels = ["[PAD]", "O"] + all_labels
return dict(zip(range(0, len(all_labels) + 1), all_labels))
mapping = make_tag_lookup_table()
print(mapping)
{0: '[PAD]', 1: 'O', 2: 'B-PER', 3: 'I-PER', 4: 'B-ORG', 5: 'I-ORG', 6: 'B-LOC', 7: 'I-LOC', 8: 'B-MISC', 9: 'I-MISC'}
訓練データセットの総てのトークンのリストを取得します。これは語彙を作成するために使用されます。
all_tokens = sum(conll_data["train"]["tokens"], [])
all_tokens_array = np.array(list(map(str.lower, all_tokens)))
counter = Counter(all_tokens_array)
print(len(counter))
num_tags = len(mapping)
vocab_size = 20000
# We only take (vocab_size - 2) most commons words from the training data since
# the `StringLookup` class uses 2 additional tokens - one denoting an unknown
# token and another one denoting a masking token
vocabulary = [token for token, count in counter.most_common(vocab_size - 2)]
# The StringLook class will convert tokens to token IDs
lookup_layer = keras.layers.StringLookup(
vocabulary=vocabulary
)
21009
訓練と検証データから 2 つの新しい Dataset オブジェクトを作成します。
train_data = tf.data.TextLineDataset("./data/conll_train.txt")
val_data = tf.data.TextLineDataset("./data/conll_val.txt")
1 行を出力してそれが上手く見えるか確認します。行の最初のレコードはトークン数です。その後、総てのトークン、続いて総ての ner タグを持ちます。
print(list(train_data.take(1).as_numpy_iterator()))
[b'9\tEU\trejects\tGerman\tcall\tto\tboycott\tBritish\tlamb\t.\t3\t0\t7\t0\t0\t0\t7\t0\t0']
データセットのデータを変換するために次の map 関数を使用していきます :
def map_record_to_training_data(record):
record = tf.strings.split(record, sep="\t")
length = tf.strings.to_number(record[0], out_type=tf.int32)
tokens = record[1 : length + 1]
tags = record[length + 1 :]
tags = tf.strings.to_number(tags, out_type=tf.int64)
tags += 1
return tokens, tags
def lowercase_and_convert_to_ids(tokens):
tokens = tf.strings.lower(tokens)
return lookup_layer(tokens)
# We use `padded_batch` here because each record in the dataset has a
# different length.
batch_size = 32
train_dataset = (
train_data.map(map_record_to_training_data)
.map(lambda x, y: (lowercase_and_convert_to_ids(x), y))
.padded_batch(batch_size)
)
val_dataset = (
val_data.map(map_record_to_training_data)
.map(lambda x, y: (lowercase_and_convert_to_ids(x), y))
.padded_batch(batch_size)
)
ner_model = NERModel(num_tags, vocab_size, embed_dim=32, num_heads=4, ff_dim=64)
パディングされたトークンからの損失を無視するカスタム損失関数を使用していきます。
class CustomNonPaddingTokenLoss(keras.losses.Loss):
def __init__(self, name="custom_ner_loss"):
super().__init__(name=name)
def call(self, y_true, y_pred):
loss_fn = keras.losses.SparseCategoricalCrossentropy(
from_logits=True, reduction=keras.losses.Reduction.NONE
)
loss = loss_fn(y_true, y_pred)
mask = tf.cast((y_true > 0), dtype=tf.float32)
loss = loss * mask
return tf.reduce_sum(loss) / tf.reduce_sum(mask)
loss = CustomNonPaddingTokenLoss()
モデルをコンパイルして適合させる
ner_model.compile(optimizer="adam", loss=loss)
ner_model.fit(train_dataset, epochs=10)
def tokenize_and_convert_to_ids(text):
tokens = text.split()
return lowercase_and_convert_to_ids(tokens)
# Sample inference using the trained model
sample_input = tokenize_and_convert_to_ids(
"eu rejects german call to boycott british lamb"
)
sample_input = tf.reshape(sample_input, shape=[1, -1])
print(sample_input)
output = ner_model.predict(sample_input)
prediction = np.argmax(output, axis=-1)[0]
prediction = [mapping[i] for i in prediction]
# eu -> B-ORG, german -> B-MISC, british -> B-MISC
print(prediction)
Epoch 1/10 439/439 [==============================] - 13s 26ms/step - loss: 0.9300 Epoch 2/10 439/439 [==============================] - 11s 24ms/step - loss: 0.2997 Epoch 3/10 439/439 [==============================] - 11s 24ms/step - loss: 0.1544 Epoch 4/10 439/439 [==============================] - 11s 25ms/step - loss: 0.1129 Epoch 5/10 439/439 [==============================] - 11s 25ms/step - loss: 0.0875 Epoch 6/10 439/439 [==============================] - 11s 25ms/step - loss: 0.0696 Epoch 7/10 439/439 [==============================] - 11s 25ms/step - loss: 0.0597 Epoch 8/10 439/439 [==============================] - 11s 25ms/step - loss: 0.0509 Epoch 9/10 439/439 [==============================] - 11s 25ms/step - loss: 0.0461 Epoch 10/10 439/439 [==============================] - 11s 25ms/step - loss: 0.0408 tf.Tensor([[ 989 10951 205 629 7 3939 216 5774]], shape=(1, 8), dtype=int64) ['B-ORG', 'O', 'B-MISC', 'O', 'O', 'O', 'B-MISC', 'O']
メトリクス計算
ここにメトリクスを計算する関数があります。この関数は NER データセット全体に対する F1 スコアと各 NER タグに対する個別のスコアを計算します。
def calculate_metrics(dataset):
all_true_tag_ids, all_predicted_tag_ids = [], []
for x, y in dataset:
output = ner_model.predict(x)
predictions = np.argmax(output, axis=-1)
predictions = np.reshape(predictions, [-1])
true_tag_ids = np.reshape(y, [-1])
mask = (true_tag_ids > 0) & (predictions > 0)
true_tag_ids = true_tag_ids[mask]
predicted_tag_ids = predictions[mask]
all_true_tag_ids.append(true_tag_ids)
all_predicted_tag_ids.append(predicted_tag_ids)
all_true_tag_ids = np.concatenate(all_true_tag_ids)
all_predicted_tag_ids = np.concatenate(all_predicted_tag_ids)
predicted_tags = [mapping[tag] for tag in all_predicted_tag_ids]
real_tags = [mapping[tag] for tag in all_true_tag_ids]
evaluate(real_tags, predicted_tags)
calculate_metrics(val_dataset)
processed 51362 tokens with 5942 phrases; found: 5504 phrases; correct: 3855. accuracy: 63.28%; (non-O) accuracy: 93.22%; precision: 70.04%; recall: 64.88%; FB1: 67.36 LOC: precision: 85.67%; recall: 78.12%; FB1: 81.72 1675 MISC: precision: 73.15%; recall: 65.29%; FB1: 69.00 823 ORG: precision: 56.05%; recall: 63.53%; FB1: 59.56 1520 PER: precision: 65.01%; recall: 52.44%; FB1: 58.05 1486
終わりに
この課題では、単純な transformer ベースの固有表現認識モデルを作成しました。CoNLL 2003 共有タスクデータでそれを訓練しておよそ 70% の全体的な F1 スコアを得ました。BERT or ELECTRA のような事前訓練済みモデルで最調整された最先端の NER モデルは、事前訓練プロセスの一部としての単語の固有知識とサブワードトークン化の使用により、このデータセットで 90-95% の間の遥かに高い F1 スコアを簡単に得ることができます。
Hugging Face Hub にホストされている訓練済みモデルを使用して Hugging Face Spaces でデモを試すことができます。
以上