アメリカ最新AI情報満載!セミナーや講演情報など交えて毎週水曜配信 無料ニュースレター 下記へメールアドレスを入力し無料で登録
CLOSE
パロアルトインサイト/ PALO ALTO INSIGHT, LLC.

ブログBLOG

パロアルトインサイト/PALO ALTO INSIGHT, LLC. > ブログ > 最先端自然言語モデルB E R Tとは②

最先端自然言語モデルB E R Tとは②

2020/03/27 ブログ 
by 辻 智範 

こんにちは。パロアルトインサイト、データサイエンティストの辻です。前回の「最先端自然言語モデルBERTとは①」ではモデルの概要について説明しました。今回はデータの前処理も含めたBERTのチュートリアルをします。BERTは既にたくさんブログで紹介されていますが、簡素化されたAPIの使い方やニューラルネットワークそのものを構築したものが多く、データ作成手順にについてのブログが少ないと感じたため、BERT日本語Pretrainedモデルを使い、固有表現抽出モデルのデータの作り方とその学習プロセスを紹介します。これから紹介するステップを順に追うことで、BERTをよりよく理解することが目的です。

構築環境 (MacOS Catalina)

python = 3.6
pytorch-transformers == 1.2.0
Juman++ = 1.0.2
pyknp = 0.4.1
tensorflow = 1.15

ステップ1:BERTのリポジトリーをクローンする

GoogleのBERTレポジトリーをクローンします。

git clone https://github.com/google-research/bert

ステップ2:日本語版BERTモデルのダウンロード

BERT日本語版Pretrainedモデルサイトからpytorch transformer用ファイルをダウンロードしてください。このチュートリアルではBASE 通常版を使用します。ダウンロードしたファイルは、サイトの注意書きにあるよう「bert」ディレクトリー内に置いてください

ステップ3:Juman++対応

初期の状態では日本語形態素解析システムJuman++に対応していないため、以下の変更をtokenization.pyに加えることが必要です。[参考URL:BERT導入手順おさらいメモ] 1から3のステップでプログラムを始める準備が整いました。

1.  tokenization.pyの末尾にclass JumanPPTokenizerの追加する

class JumanPPTokenizer(BasicTokenizer):

   def __init__(self):
      """Constructs a BasicTokenizer.
      """
      from pyknp import Juman

      self.do_lower_case = False
      self._jumanpp = Juman()
      self.never_split = ("[UNK]", "[SEP]", "[PAD]", "[CLS]", "[MASK]")

   def tokenize(self, text):
      """Tokenizes a piece of text."""
      text = self.convert_to_unicode(text.replace(' ', ''))
      text = self._clean_text(text)

      juman_result = self._jumanpp.analysis(text)
      split_tokens = []
      for mrph in juman_result.mrph_list():
         split_tokens.extend(self._run_split_on_punc(mrph.midasi))

      output_tokens = whitespace_tokenize(" ".join(split_tokens))
      return output_tokens

   def convert_to_unicode(self, text):
      """Converts `text` to Unicode (if it's not already), assuming utf-8 input."""
      if six.PY3:
         if isinstance(text, str):
            return text
         elif isinstance(text, bytes):
            return text.decode("utf-8", "ignore")
         else:
            raise ValueError("Unsupported string type: %s" % (type(text)))
      elif six.PY2:
         if isinstance(text, str):
            return text.decode("utf-8", "ignore")
         elif isinstance(text, unicode):
            return text
         else:
            raise ValueError("Unsupported string type: %s" % (type(text)))
      else:
         raise ValueError("Not running on Python2 or Python 3?")

2. class FullTokenizerでBasicToknizerをJumannPPTokenizerに変更

class FullTokenizer(object):
   """Runs end-to-end tokenziation."""

   def __init__(self, vocab_file, do_lower_case=True):
      self.vocab = load_vocab(vocab_file)
      self.inv_vocab = {v: k for k, v in self.vocab.items()}

      # JUMAN++を用いた日本語対応の場合、BasicTokenizerを使わない。
      # self.basic_tokenizer = BasicTokenizer(do_lower_case=do_lower_case)
      self.jumanpp_tokenizer = JumanPPTokenizer()
      self.wordpiece_tokenizer = WordpieceTokenizer(vocab=self.vocab)

   def tokenize(self, text):
      split_tokens = []
      # JUMAN++を用いた日本語対応用。
      #for token in self.basic_tokenizer.tokenize(text):
      for token in self.jumanpp_tokenizer.tokenize(text):
         for sub_token in self.wordpiece_tokenizer.tokenize(token):
            split_tokens.append(sub_token)

      return split_tokens

   def convert_tokens_to_ids(self, tokens):
      return convert_by_vocab(self.vocab, tokens)

   def convert_ids_to_tokens(self, ids):
      return convert_by_vocab(self.inv_vocab, ids)

ステップ4:BERTトークン化

ここでは、モデルをよりよく理解するためpythonのスクリプト状でモデルを動かしてみたいと思います。先ほどダウンロードしたフォルダーからtokenizerをロードします。

from bert.tokenization import FullTokenizer
bert_dir = os.path.join(os.getcwd(), "bert/Japanese_L-12_H-768_A-12_E-30_BPE_transformers")
tokenizer = FullTokenizer(os.path.join(bert_dir, "vocab.txt"),do_lower_case=False)

次に以下の5文をトークン化してみます。BERTトークンはJuman++に加え、「##」トークンが追加されることが特徴で、前の語彙との繋がりを表しします。固有表現抽出の場合これにより語彙数が変わってしまうので、後ほど調整を行います。

# Make sample text
sampletxts = ["こんにちはいい天気ですね。",
        "明日は雨になるそうです。",
        "昨日からお腹が痛いです。",
        "今年の冬は暖冬だそうです。",
        "体調が良くなってきました。"]

result = [tokenizer.tokenize(txt) for txt in sampletxts]

result
[['こん', '##にち', '##は', '、', 'いい', '天気', 'です', 'ね', '。'],
['明日', 'は', '雨', 'に', 'なる', 'そう', '##です', '。'],
['昨', '##日', 'から', 'お', '##腹', 'が', '痛', '##い', 'です', '。'],
['今年', 'の', '冬', 'は', '暖', '##冬', 'だ', 'そう', '##です', '。'],
['体調', 'が', '良く', 'なって', 'き', 'ました', '。']]

オリジナルjuman++のトークン化
[['こんにち', 'は', 'いい', '天気', 'です', 'ね', '。']
['明日', 'は', '雨', 'に', 'なる', 'そうです', '。']
['昨日', 'から', 'お腹', 'が', '痛い', 'です', '。']
['今年', 'の', '冬', 'は', '暖冬', 'だ', 'そうです', '。']
['体調', 'が', '良く', 'なって', 'き', 'ました', '。']]
ステップ5:トレーニングデータ作成

学習データを文章ID,語彙以下のフォーマットに落とし込みます。日時、天気、健康の3種類のラベル付けを上の文章に行ったサンプルです。(最初の3文章。残りはURLからダウンロードしてください)タグを付ける初めの語彙はBで始まり、連続するタグはIでタグ付けをするのが特徴です。また関係のない語彙は「O」でタグ付けを行います。

Sentence_id Word Tag
0 こんにち O
0 O
0 いい O
0 天気 B-Weather
0 です O
0 O
0 O
1 明日 B-DayTime
1 O
1 B-Weather
1 O
1 なる O
1 そうです O
1 O
2 昨日 B-DayTime
2 から O
2 お腹 B-Health
2 I-Health
2 痛い I-Health
2 です O
2 O

BERTのモデルをトレーニングするためには、データをBERTが読み込めるフォーマットに変換することが必要です。そのために以下のしょりを行います。

(1) 文章の始めと終わりに[CLS], [SEP]を追加をする。(文章分類、固有表現抽出の場合スタートと終わりを見分けるため)
(2) subword ##を使用したトークン。語彙が細かく分解されるためタグを調整する

# Make sentence generator
class SentenceGetter():

   def __init__(self, data, tokenizer):
      self.n_sent = 1
      self.data = data
      self.empty = False
      agg_func = lambda s: [(w, t) for w, t in zip(s["Word"].values.tolist(), s["Tag"].values.tolist())]
      self.grouped = self.data.groupby("Sentence_id").apply(agg_func)
      self.sentences = [s for s in self.grouped]
      self.tokenizer = tokenizer

   def get_adjustedTags(self):
      sentences, tags = [], []
      for sent in getter.sentences:
         newsent = ['[CLS]']
         newtag = ['O']
         for (word, tag) in sent:
            subword = tokenizer.tokenize(word)
            if len(subword) >= 1:
               newtag = newtag + [tag]
               tag = tag.replace('B-', 'I-') if 'B-' in tag else tag
               newtag = newtag + ([tag] * (len(subword) - 1))
            else:
               newtag.append(tag)
            newsent = newsent + subword
         newsent.append('[SEP]')
         newtag.append('O')
         sentences.append(newsent)
         tags.append(newtag)
      return sentences, tags

dfNER = pd.read_csv(os.path.join(data_dir, "dftrain_labeled.csv"), encoding = 'utf-8', sep = ',')
getter = SentenceGetter(dfNER, tokenizer)
sentences, tags = getter.get_adjustedTags()
tags_vals = set([l for tag in tags for l in tag])
tag2idx = {t: i for i, t in enumerate(tags_vals)}
tdx2tag = {i: t for i, t in enumerate(tags_vals)}

この時点で文章とタグは以下の通りになります

sentences
[['[CLS]', 'こん', '##にち', 'は', 'いい', '天気', 'です', 'ね', '。', '[SEP]'],
['[CLS]', '明日', 'は', '雨', 'に', 'なる', 'そう', 'です', '。', '[SEP]'],
['[CLS]', '昨', '##日', 'から', 'お', '##腹', 'が', '痛', '##い', 'です', '。', '[SEP]'],
['[CLS]', '今年', 'の', '冬', 'は', '暖', '##冬', 'だ', 'そう', 'です', '。', '[SEP]'],
['[CLS]', '体調', 'が', '良く', 'なって', 'き', 'ました', '。', '[SEP]']]

tags
[['O', 'O', 'O', 'O', 'O', 'B-Weather', 'O', 'O', 'O', 'O'],
['O', 'B-DayTime', 'O', 'B-Weather', 'O', 'O', 'O', 'O', 'O', 'O'],
['O', 'B-DayTime', 'I-DayTime','O','B-Health','I-Health','I-Health','I-Health','I-Health','O','O','O'],
['O','B-DayTime','O','O','O','B-Weather','I-Weather','O','O','O','O','O'],
['O', 'B-Health', 'I-Health', 'I-Health', 'O', 'O', 'O', 'O', 'O']]

tag2idx
{'O' : 0, B-Health': 1, 'I-Weather': 2, 'B-Weather': 3, 'I-DayTime': 4, 'B-DayTime': 5, 'I-Health': 6}

さらにpaddingと呼ばれる入力値の長さをそろえる処理とデータを数値に変換する処理を行います

from keras.preprocessing.sequence import pad_sequences

MAX_LEN = 15
input_ids = pad_sequences([tokenizer.convert_tokens_to_ids(txt) for txt in sentences],
maxlen=MAX_LEN, dtype="long", truncating="post", padding="post")

tags_ids = pad_sequences([[tag2idx.get(l) for l in tag] for tag in tags],
maxlen = MAX_LEN, value = tag2idx["O"], padding = "post",
dtype = "long", truncating = "post")

入力値は以下のようにすべての語彙とタグを数値に変換します。これで入力IDs(X)とタグ(Y)のペアが揃いました。すべての語彙はvocab.txtにある語彙順の番号が割り当てられ、タグも定義した番号に振り分けられました。

input_ids
[[ 2 8185 21115 9 1635 9292 3338 2382 7 3 0 0 0 0 0]
[ 2 12072 9 2899 8 66 2208 3338 7 3 0 0 0 0 0]
[ 2 20753 2581 27 273 11209 11 5743 574 3338 7 3 0 0 0]
[ 2 19884 5 2141 9 11147 12029 234 2208 3338 7 3 0 0 0]
[ 2 9125 11 4161 78 1237 4561 7 3 0 0 0 0 0 0]]

tags_ids
[[0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 5, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 5, 4, 0, 1, 6, 6, 6, 6, 0, 0, 0, 0, 0, 0],
[0, 5, 0, 0, 0, 3, 2, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]

以上のプロセスで一般的な学習の準備が揃ったのですが、BERTには「attension mask」と言う文章の語彙を隠しその言葉を予測する教師なし学習プロセスが含まれています。そのため、X、Yペアの他に「attention mask」を作成します。語彙の部分のみをマスクの対象とし1を割り当てます。これでトレーニングデータが完成です。

attention_masks = [[float(i > 0) for i in ii] for ii in input_ids]
attention_masks
[[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0],
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0],
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]]
ステップ6:モデル学習

今回は5サンプルのみのため、トレーニングデータをバリデーションデータと分けることはやめ、モデルチューニングのプロセスのみを説明します。学習データを作成した後は、pytorchの一般的なトレーニング方法で学習が可能です。

import torch
from torch.optim import Adam
from torch.utils.data import TensorDataset, DataLoader, RandomSampler
from pytorch_transformers import BertTokenizer, BertConfig, BertModel
from pytorch_transformers import BertForTokenClassification

# モデルの読み込み
config = BertConfig.from_json_file(os.path.join(bert_dir, "config.json"))
model = BertModel.from_pretrained(os.path.join(bert_dir, "pytorch_model.bin"), config = config)

# データをtensorに変換
train_inputs = torch.tensor(input_ids)
train_tags = torch.tensor(tags_ids)
train_masks = torch.tensor(attention_masks)

train_data = TensorDataset(train_inputs, train_masks, train_tags)
train_sampler = RandomSampler(train_data)
train_dataloader = DataLoader(train_data, sampler=train_sampler, batch_size = batch_size)

# 全ての重みを学習し直す場合は、パラメターの設定を行う。Opimizerの設定
FULL_FINETUNING = True
if FULL_FINETUNING:
   param_optimizer = list(model.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}
   ]
else:
   param_optimizer = list(model.classifier.named_parameters())
   optimizer_grouped_parameters = [{"params": [p for n, p in param_optimizer]}]
optimizer = Adam(optimizer_grouped_parameters, lr=3e-5)

# トレーニング
epochs = 5
max_grad_norm = 1.0

for _ in trange(epochs, desc="Epoch"):
   # TRAIN loop
   model.train()
   train_loss = 0
   nb_train_examples, nb_train_steps = 0, 0

   for step, batch in enumerate(train_dataloader):
      b_input_ids, b_input_mask, b_labels = batch
      # forward pass
      loss = model(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.parameters(), max_norm = max_grad_norm)
      # update parameters
      optimizer.step()
      model.zero_grad()

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

トレーニング結果の出力。モデルの動作確認のため、精度の検証は割愛します。

Epoch: 100%|██████████| 5/5 [00:52<00:00, 10.57s/it]
Train loss: 1.490822696685791
Train loss: 0.6730928838253021
Train loss: 0.2201131671667099
Train loss: 0.07963154837489128
Train loss: 0.027780962735414506
ステップ7:予測

最後にトレーニングをしたモデルから予測しを出力します。非常に少ないデータでトレーニングを行なったので、以下の2文章の予測を比べます。

1. 明日はお腹の痛みが引くといいですね
2. 明日までにお腹の痛みが引くといいですね

1の文はトレーニングデータに全く同じ文脈があるため、うまくタグ付けがされています。2の文は明日の後の部分がトレーニングデータにないため、うまくタグ付けができてません。一方でお腹の痛みの周辺の語彙は違うにもかかわらず、タグ付けがうまくできています。「明日」は文脈の先頭部分に当たり、他の語彙と比べ周りの文脈の情報が少ないことと、「お腹の痛み」は語彙数5の長さですが、「明日」は語彙数1のため文脈が読みづらいことなどが考えられます。実際のプロジェクトでは多くのデータで学習を行い、様々な文脈を読めるモデルを構築します。

# Test with a sentence
test_sents = ["明日までにお腹の痛みが引くといいですね。",
              "明日はお腹の痛みが引くといいですね。"]
test_sents = [['[CLS]'] + tokenizer.tokenize(sent) + ['[SEP]'] for sent in test_sents]

sent_ids = pad_sequences([tokenizer.convert_tokens_to_ids(sent) for sent in test_sents],
maxlen = MAX_LEN, dtype = 'long', truncating='post', padding='post')

result = model((torch.tensor(sent_ids)))
values, indexes = result[0].max(2)

preds = []
for index in indexes.tolist():
   preds.append([idx2tag[i] for i in index])
test_sents
[['[CLS]', '明日', 'まで', 'に', 'お', '##腹', 'の', '痛み', 'が', '引く', 'と', 'いい', 'です', 'ね', '。', '[SEP]'],
['[CLS]', '明日', 'は', 'お', '##腹', 'の', '痛み', 'が', '引く', 'と', 'いい', 'です', 'ね', '。', '[SEP]']]

preds
[['O', 'O', 'O', 'O', 'B-Health', 'I-Health', 'I-Health', 'I-Health', 'I-Health', 'I-Health', 'O', 'O', 'O', 'O', 'O'],
['O', 'B-DayTime', 'O', 'B-Health', 'I-Health', 'I-Health', 'I-Health', 'I-Health', 'O', 'O', 'O', 'O', 'O', 'O', 'O']]
まとめ

今回のブログでは日本語BERTのデータの作り方を紹介しました。オープンソースのAPIなどを使用すれば前処理の必要もなく予測できるものもありますが、予測の結果がうまくいかない時などは、学習データが正しく変換されているかやパラメータが適切なのかなどの分析が必要となる場合もあります。ご興味のある方は一度試してください。

実際に導入をご検討の方は、AI導入/DX推進プロジェクトを成功させる3つアドバイスもぜひご参考ください。

BERT の学習時間を削減する研究論文「Early BERT」について、The Insight の記事「BERT(自然言語処理)の学習時間を削減する「モデル圧縮」とは」としても取り上げました。こちらもぜひご確認ください。

パロアルトインサイトについて

AIの活用提案から、ビジネスモデルの構築、AI開発と導入まで一貫した支援を日本企業へ提供する、石角友愛氏(CEO)が2017年に創業したシリコンバレー発のAI企業。

社名 :パロアルトインサイトLLC
設立 :2017年
所在 :米国カリフォルニア州 (シリコンバレー)
メンバー数:17名(2021年9月現在)

石角友愛
<CEO 石角友愛(いしずみともえ)>

2010年にハーバードビジネススクールでMBAを取得したのち、シリコンバレーのグーグル本社で多数のAI関連プロジェクトをシニアストラテジストとしてリード。その後HRテック・流通系AIベンチャーを経てパロアルトインサイトをシリコンバレーで起業。データサイエンティストのネットワークを構築し、日本企業に対して最新のAI戦略提案からAI開発まで一貫したAI支援を提供。AI人材育成のためのコンテンツ開発なども手掛け、順天堂大学大学院医学研究科データサイエンス学科客員教授(AI企業戦略)及び東京大学工学部アドバイザリー・ボードをはじめとして、京都府アート&テクノロジー・ヴィレッジ事業クリエイターを務めるなど幅広く活動している。また、毎日新聞「石角友愛のシリコンバレー通信」、ITメディア「石角友愛とめぐる、米国リテール最前線」など大手メディアでの寄稿連載を多く持ち、最新のIT業界に関する情報を発信している。「報道ステーション」「NHKクローズアップ現代+」などTV出演も多数。

著書に『いまこそ知りたいDX戦略』『いまこそ知りたいAIビジネス』(ディスカヴァー・トゥエンティワン)、『経験ゼロから始めるAI時代の新キャリアデザイン』(KADOKAWA)、『才能の見つけ方 天才の育て方』(文藝春秋)など多数。

パロアルトインサイトHP:www.paloaltoinsight.com
お問い合わせ、ご質問などはこちらまで:info@paloaltoinsight.com
※石角友愛の著書一覧

NEWSLETTERパロアルトインサイトの
無料ニュースレター

毎週水曜日、アメリカの最新AI情報が満載の
ニュースレターを無料でお届け!
その他講演情報やAI導入事例紹介、
ニュースレター登録者対象の
無料オンラインセミナーのご案内などを送ります。

BACK TO BLOG
« »
PAGE TOP