Goalist Developers Blog

Googleのbertを利用してみました〜!

こんにちは、チナパです!

先日、Word2vecを利用して、単語から数字のための辞書を作成してみました。その続きで、Googleが最近リリースした「bert」(Bidirectional Encoder Representations from Transformers)を利用してみましょう。

f:id:c-pattamada:20190329191856p:plain
人間よりできる!

bertとは?

まずはそもそもこれは何なのか、なんですごいのかを説明します。

現在までの自然言語処理の技術が文章を読みながら、今までの言葉からコンテキストを理解して、順番での次の言葉の意味を今までの言葉によってのコンテキストで影響されるような技術が代表的でした。

それはRNNの構築を利用して行ってました。

去年、ELMoとbertでは「Attention」を利用し、より精度の高い結果を出せるようになってる。

例えば、チャットボットを作成する時に、Attentionの構築では前のメッセージのキーワードを覚えながらお返事を作成するようなイメージです。

Bertはそれをより強化的に使っています。

“Multi-headed attention”と説明されていますが、複数な箇所をバラバラに注目しながら解析しているような巨大モデルです。

英語の自然言語処理のベンチマークの一つのSQuADでは人間より良い結果が簡単に出せるようです。

bertが事前に単語を学習されたままで利用もできますので、試してみましょう!

素晴らしい!では、どうやって使える??

Bertは割と重いので、今回はColabでやってみました。

!pip install bert-tensorflow

from google.colab import drive
drive.mount('/content/gdrive')

driveの許可を与えて、bertの語彙データをtensorflow_hub_からアクセスしましょう。

from bert.tokenization import FullTokenizer
import pandas as pd
import tensorflow_hub as hub

bert_path = "https://tfhub.dev/google/bert_multi_cased_L-12_H-768_A-12/1" # 日本語用のモデルがこちら

sess = tf.Session()

def create_tokenizer_from_hub_module():
    """Get the vocab file and casing info from the Hub module."""
    bert_module =  hub.Module(bert_path)
    tokenization_info = bert_module(signature="tokenization_info", as_dict=True)
    vocab_file, do_lower_case = sess.run(
        [
            tokenization_info["vocab_file"],
            tokenization_info["do_lower_case"],
        ]
    )

    return FullTokenizer(vocab_file=vocab_file, do_lower_case=do_lower_case)
  
tokenizer = create_tokenizer_from_hub_module()

これで、bertのtokenizerのインスタンスを作りました。MeCabみたいに、文字列を言葉に分けるためのものです。bertでは、漢字が全部一文字ずつのトークンに変換されます。

tokenizer.tokenize('こんにちは、今日の天気はいかがでしょうか?') すると、

['こ',
 '##ん',
 '##に',
 '##ち',
 '##は',
 '、',
 '今',
 '日',
 'の',
 '天',
 '気',
 'は',
 '##い',
 '##か',
 '##が',
 '##で',
 '##し',
 '##ょう',
 '##か',
 '?']

みたいなとんでもない結果が出ます。

"##" が続いているような風に見える。トークンから文字列に戻そうとするとこのあたりは気をつけないと… 後は、変な分からない文字がありましたら、このように出てきます。

tokenizer.tokenize('゛')

# アウトプット=> ['[UNK]']

さて、ベクトル化しましょう。

bertでトークンをベクトル変えるために、3つのインプットが必要です。

tokenizer.convert_tokens_to_ids(...)

で作成できるid以外にsegment idとinput maskが必要です。

input_idがbertで素早くベクトルに変換するためのものです。segment_idは文の番号です、これでメッセージとその返事が分けられたりできます。padding系のインプットを無視できるように、input_mask_を利用します。

f:id:c-pattamada:20190329200016p:plain
そう、これがいい!

一旦は、一つの文章だけの処理をしましょう。上記の3つのベクトルがこのようなメソッドで作られます。

def convert_string_to_bert_input(tokenizer, input_string, max_length=128):

    tokens = []
    tokens.append("[CLS]")
    tokens.extend(tokenizer.tokenize(input_string))
    if len(tokens) > max_seq_length - 2:
        tokens = tokens[0 : (max_seq_length - 2)]
    tokens.append("[SEP]")
    
    segment_ids = [0] len(tokens)
    input_ids = tokenizer.convert_tokens_to_ids(tokens)
    # これから加えるpaddingが無視できるように
    input_mask = [1] * len(tokens)
    
    while len(input_ids) < max_seq_length:
        input_ids.append(0)
        input_mask.append(0)
        segment_ids.append(0)

    return np.array(input_ids),
            np.array(input_mask),
            np.array(segment_ids)

上記のメソッドで"[CLS]"と"[SEP]"を追加しているのが見えます。これはbertで利用する「開始文字」と「文を分ける文字」になってます。複数の文章がある場合に、間に"[SEP]"を入れます。

Pandasでベクトル化したいデータを読み込み、上記のメソッドで変換しましょう。

data_path = ...
input_column = ...
df = pd.read_csv(data_path)

features = df[input_column].map(
                       lambda my_string:
                           convert_string_to_bert_input(tokenizer, my_string)
                       )

bertをkerasで利用

よっし、これからbertをkerasのモデルに利用する魔法を使いましょう。

https://github.com/strongio/keras-bert/blob/master/keras-bert.ipynb

こちらはかなり参考になりました。

class BertLayer(tf.layers.Layer):
    def __init__(self, n_fine_tune_layers=10, **kwargs):
        self.n_fine_tune_layers = n_fine_tune_layers
        self.trainable = False
        self.output_size = 768
        super(BertLayer, self).__init__(**kwargs)

    def build(self, input_shape):
        self.bert = hub.Module(
            bert_path,
            trainable=self.trainable,
            name="{}_module".format(self.name)
        )

        trainable_vars = self.bert.variables

        # Remove unused layers
        trainable_vars = [var for var in trainable_vars if not "/cls/" in var.name]

        # Select how many layers to fine tune
        trainable_vars = trainable_vars[-self.n_fine_tune_layers :]

        # Add to trainable weights
        for var in trainable_vars:
            self._trainable_weights.append(var)
            
        for var in self.bert.variables:
            if var not in self._trainable_weights:
                self._non_trainable_weights.append(var)

        super(BertLayer, self).build(input_shape)

    def call(self, inputs):
        inputs = [K.cast(x, dtype="int32") for x in inputs]
        input_ids, input_mask, segment_ids = inputs
        bert_inputs = dict(
            input_ids=input_ids, input_mask=input_mask, segment_ids=segment_ids
        )
        result = self.bert(inputs=bert_inputs, signature="tokens", as_dict=True)[
            "pooled_output"
        ]
        return result

    def compute_output_shape(self, input_shape):
        return (input_shape[0], self.output_size)

これはkerasのカスタムLayerを作成し、その中にbertを利用するようなコードです。 自分のカスタムLayerを書くときにこちらもとても便利です https://keras.io/layers/writing-your-own-keras-layers/

重要な部分は3箇所あります。

1. build()

def build(self, input_shape):
    self.bert = hub.Module(
            bert_path,
            trainable=self.trainable,
            name="{}_module".format(self.name)
        )

ここでtensor flow hubを使って、bert_path_で定義されたモデルを利用します。

2. call()

input_ids, input_mask, segment_ids = inputs
        bert_inputs = dict(
            input_ids=input_ids, input_mask=input_mask, segment_ids=segment_ids
        )
        result = self.bert(inputs=bert_inputs, signature="tokens", as_dict=True)[
            "pooled_output"
        ]

ここでは、先ほど用意したinputをdictに変え、bertのモデルの結果を求める。 現在はsignature="tokens"以外のサポートがありません(29/03/2019)、"pooled_output"と別に"sequence_output"もあります。pooledの方が単語ベクトルを作成する時に使いますので、そちらで行きます。

3. output size

self.output_size = 768

こちらは使ってるモデルに応じて設定する数字です。普通のモデルのそれぞれのレイヤーが768次元で帰ってきます。Largeのモデルでは1024次元です。

これで先ほど作成した3つの変数をdictに入れ、self.bertで結果を得ます。 上記のlayerを普通のkerasのモデルで使えます。

例:

def get_model(max_length=128, num_classes=5):
    input_ids = tf.keras.layers.Input(shape=(max_seq_length,), name="input_ids")
    in_mask = tf.keras.layers.Input(shape=(max_seq_length,), name="input_masks")
    in_segment = tf.keras.layers.Input(shape=(max_seq_length,), name="segment_ids")
    bert_inputs = [input_ids, in_mask, in_segment]
    
    bert_output = BertLayer(n_fine_tune_layers=1)(bert_inputs)
    dense = tf.keras.layers.Dense(128, activation='relu')(bert_output)
    pred = tf.keras.layers.Dense(num_classes=5, activation='sigmoid')(dense)
    
    model = tf.keras.models.Model(inputs=bert_inputs, outputs=pred)
    model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
    model.summary()

ぜひ試してみましょう!

まとめ

今回、 今までの自然言語処理のやり方、とbertの簡単な説明をしました。それから、文字列をbertに適切な形に変換して、モデルで利用できるような形にしました。

今回は、自分も理解し切れてない部分もありますが、ぜひフィードバックを聞きたいです〜