アプリとサービスのすすめ

アプリやIT系のサービスを中心に書いていきます。たまに副業やビジネス関係の情報なども気ままにつづります

日本語版keras BERTのfine tuningでlivedoorニュースのトピック分類

今回は日本語版keras BERTで、自然言語処理用の公開データセット" livedoorニュースコーパス "のトピック分類をしてみた。

前回の記事で、英語版のkeras BERTでネガポジ判定をしたが、日本語版はやったことなかった。
テキストの前処理→日本語版keras BERT読み込み→訓練までのやった過程とそのメインポイントをまとめてく。


目次
・日本語テキストの前処理
1.テキストデータ読み込み(and クリーニング)
2.単語の正規化
3.mecabで単語の分割
4.stopwordsの除去& 頻度の低い単語除去
5.単語を分散表現でidベクトル化

・日本語版keras BERTで学習
1.BERTの読み込み
2.整形済みテキストの読み込み
3.訓練と正解率



日本語テキストの前処理
今回は公開データセット" livedoorニュースコーパス "のテキストを、トピック別に分類するタスク。


今回は、トピックの「Sports、トピックニュース」をBERTを使い、どのくらいの精度で分類できるかの2値分類を試した。

・訓練データ 1300

・テストデータ 360

BERTのネットワーク構造
f:id:trafalbad:20190803082446j:plain



1.テキストデータ読み込み(and クリーニング)

livedoorのテキストデータは多いので、google driveに保存してから、google colaboratoryとマウントして読み込んだ。

読み込む処理は行ごとにリストに入れて、list[2:]とかのスライス表記で、1行ずつ削除できるよう工夫。

htmlタグとかはないので、特にhtmlパースとかはなし。

c=0
L={}
# テキストののN行目を削除できるよう読み込み
text_path =  ["drive/My Drive/sports-watch", "drive/My Drive/topic-news"]
for i, path in enumerate(os.listdir(text_path[c]), len(L)):
  _list =[]
  with open(text_path[c]+'/'+path, "r") as f:
  #text_list = text_data.readlines()
     for _l in f:
        _l = _l.rstrip()
        if _l:
           _list.append(_l)
  L[i] = _list
c+=1


2.単語の正規化

大体やったことは、

・単語の統一 (半角から全角へ変換)

import mojimoji

S=[]
for i in range(0, len(L)):
  # 半角=>全角
  sen = mojimoji.han_to_zen(sen) 
  S.append(sen)

・数字の置き換え (連続した数字を0に置換)

def normalize_number(text):
    # 連続した数字を0で置換
    replaced_text = re.sub(r'\d+', '0', text)
    return replaced_text

単語の統一はタスクによりけりなので、特にしなかった。





3.mecabで単語の分割

名詞、形容詞、動詞のみ使用するという、よくある精度を上げるテク。

def mecab(document):
  mecab = MeCab.Tagger("-Ochasen")
  lines = mecab.parse(document).splitlines()
  words = []
  for line in lines:
      chunks = line.split('\t')
      if len(chunks) > 3 and (chunks[3].startswith('動詞') or chunks[3].startswith('形容詞') or (chunks[3].startswith('名詞') and not chunks[3].startswith('名詞-数'))):
          words.append(chunks[0])
  return words

4.stopwordsの除去& 頻度の低い単語除去




stopwordsの除去



def delete_stopwords(doc):
  sentence_words = []
  for word in doc:
    # ストップワードに含まれるものは除外
    if word in stopwords: 
      continue
    #  特定の文字列(chr(92)=="\")を含む文字列削除
    if chr(92) in word or '″' in word:    
      continue
    sentence_words.append(word)        
  return sentence_words




頻度の低い単語の除去


tf-idfはクラスタリングで分類して、外れ値的なテキストそのものを削除しまう。そうするとデータセット数が減るのでやめた。

他にも、collectionメソッドで、頻度の少ないwordsのリスト(few_wordlist)を作って、頻度が一回以下の単語を削除してみたけど、文章がほとんど原型を留めてなかったのでそれもNG。

結果的に、頻度の低い単語は削除しせずとも、精度は高かった。(多分、attentionのおかげ)。頻度の低い単語を削除するのは、適材適所で使った方がいい。


使うと逆に精度を下げることがあるので注意。



・tf-idfで削除

import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.cluster import KMeans
 
vectorizer = TfidfVectorizer(use_idf=True, token_pattern=u'(?u)\\b\\w+\\b')
vecs = vectorizer.fit_transform(docs)

# クラスタリング
clusters = KMeans(n_clusters=5, random_state=0).fit_predict(vecs)
docs = [doc for doc in docs]

参考サイト:TF-IDF で文書をベクトル化。python の TfidfVectorizer を使ってみる



・頻度の低い単語の削除

import collections

def delete_sewwords(doc):
  sentence_words = []
  for word in doc:
# few_wordlist 含まれるものは除外
    if word in few_wordlist:
      continue
    sentence_words.append(word)        
  return sentence_words


#  few_wordslistを作る
count = collections.Counter(docs)
few_wordlist=[]
for word, amount in count.items():
  if amount >1:
    continue
  few_wordlist.append(word)

# 頻度の少ない単語を除外
train_x=[]
for doc in docs:
  doc = delete_sewwords(doc)
  train_x.append(doc)

5.単語を分散表現でidベクトル化

sentencepieceを使って、2次元のidベクトル化する例が多くなってるけど、日本語だと上手くいってないケースが散見されるので、今回は純粋に正攻法でidベクトル化した。

# 単語のidx辞書
words = {}
for sentence in SS:
  for word in sentence:
    if word not in words:
#  '+1' がゼロパディングのキーポイント
      words[word] = len(words)+1


# 文章を単語ID配列にする
data_x_vec = []
for sentence in SS:
  sentence_ids = []
  for word in sentence:
    sentence_ids.append(words[word])
  data_x_vec.append(sentence_ids)

# 文章の長さを揃えるため、0でパディングする
max_sentence_size = 0
for sentence_vec in data_x_vec:
    if max_sentence_size < len(sentence_vec):
        max_sentence_size = len(sentence_vec)
print(max_sentence_size)   # ==len(sentence_ids)

for sentence_ids in data_x_vec:
    while len(sentence_ids) < max_sentence_size:
         # 末尾に0を追加
        sentence_ids.append(0)

# arrayに変換
data_x_vecs = np.array(data_x_vec, dtype="int32")

# 保存
np.save('train_label', train_sy)
np.save('test_label', test_sy)
# テキストも保存
np.save('all_text', np.array(docs))


日本語版keras BERTで学習

日本語版のkeras bert は下のサイトからに関連ファイルをダウンロード。
www.inoue-kobo.com



BERTの内部構造
f:id:trafalbad:20190803082605j:plain

1.BERTの読み込み

BERT関連ファイルは容量がでかいので、google driveにアップロードして、google colaboratoryとマウントして読み込んだ。

# tensorflowのバージョンを1.13.1に変更
!pip uninstall tensorflow && pip install tensorflow==1.13.1

# colabratory上でgoogle driveとマウント
from google.colab import drive
drive.mount('/content/drive')


keras BERTでは、ネットワーク構造の関係でinput sizeは512がmaxらしいので、bert_config.jsonの"max_position_embeddings"と
"max_seq_length"の値をそれに合わせた。

bert_config.json

{ "attention_probs_dropout_prob": 0.1,
    "hidden_act": "gelu",
    "hidden_dropout_prob": 0.1,
    "hidden_size": 768,
    "initializer_range": 0.02,
    "intermediate_size": 3072,
    "max_position_embeddings": 512,
    "max_seq_length": 512,
    "num_attention_heads": 12,
    "num_hidden_layers": 12,
    "type_vocab_size": 2,
    "vocab_size": 32000}

もともとinput size=(batch, 619)だった(619はindexのmax(seq_lenth)、つまりidのmax)。

なので、各テキストを512に削って、input size=(batch, 512)に整形

train_x = [text[:512] for text in train_x]
train_y = train_y[:512]


あとは前回同様、BERTを読み込んで、分類問題用に、モデルを作り変える。

# BERTの読み込み
from keras_bert import AdamWarmup, calc_train_steps
from keras_bert import get_custom_objects
from keras_bert import load_trained_model_from_checkpoint

pretrained_path = "drive/My Drive/bert-wiki-ja"
config_path = 'bert_config.json'
checkpoint_path = os.path.join(pretrained_path, 'model.ckpt-1400000')

bert = load_trained_model_from_checkpoint(config_path,
    checkpoint_path, training=True, trainable=True, seq_len=SEQ_LEN)
bert.summary()


# 分類問題用にモデルの再構築
inputs = bert.inputs[:2]
dense = bert.get_layer('NSP-Dense').output
outputs = keras.layers.Dense(units=2, activation='softmax')(dense)

decay_steps, warmup_steps = calc_train_steps(train_y.shape[0],
    batch_size=BATCH_SIZE, epochs=EPOCHS)

model = keras.models.Model(inputs, outputs)
model.compile(AdamWarmup(decay_steps=decay_steps, warmup_steps=warmup_steps, lr=LR),
    loss='sparse_categorical_crossentropy', metrics=['sparse_categorical_accuracy'])
model.summary()




2.整形済みテキストの読み込み

訓練用とテスト用のテキストとラベルを読み込む。

BERTではマスク処理をするので、テキストをそれに合わせて整形。

# 読み込み
train_x = np.load('train_xs.npy')

# BERTに合わせて整形
train_x = [train_x, np.zeros_like(train_x)]
test_x= [test_x, np.zeros_like(test_x)]



3.訓練と正解率

sess = K.get_session()
uninitialized_variables = set([i.decode('ascii') for i in sess.run(tf.report_uninitialized_variables())])
init_op = tf.variables_initializer([v for v in tf.global_variables() if v.name.split(':')[0] in uninitialized_variables])
sess.run(init_op)

#  TPU Modelに変換
tpu_address = 'grpc://' + os.environ['COLAB_TPU_ADDR']
strategy = tf.contrib.tpu.TPUDistributionStrategy(tf.contrib.cluster_resolver.TPUClusterResolver(tpu=tpu_address))

with tf.keras.utils.custom_object_scope(get_custom_objects()):
    tpu_model = tf.contrib.tpu.keras_to_tpu_model(model, strategy=strategy)

# train
with tf.keras.utils.custom_object_scope(get_custom_objects()):
    tpu_model.fit(train_x, train_y, epochs=EPOCHS, batch_size=BATCH_SIZE)

# 予測
with tf.keras.utils.custom_object_scope(get_custom_objects()):
    predicts = tpu_model.predict(test_x, verbose=True).argmax(axis=-1)
print(np.sum(test_y == predicts) / test_y.shape[0]) 

前処理をしっかりやったおかげでもあるけど、精度はわずか2エポックで94%という驚異的な精度。
さすがBERT。



まとめ

テキストが日本語の場合、テキスト前処理をsentencepieceとかでお手軽にやってる例があるけど、やっぱりきちんと特徴量エンジニアリング的にしっかり前処理した方が、精度がかなり上がるね。


参考サイト

Keras BERTでファインチューニングしてみる

sentencepieceチュートリアル