ナード戦隊データマン

データサイエンスを用いて悪と戦うぞ

words2textのためのアノテーション

文の自然さを評価するための2値分類モデルがあれば、文の生成タスクにおいて、いくつかの候補文の中から不自然な文を除外したり、ランク付けすることに使えます。ここでは、そのような目的で使う「文の自然さ」のためのアノテーション方法を考えます。

Words2Text

私が行いたいタスクは、「Words2Text」という種類のタスクです。ググってもこのタスクは見つからないと思いますが、これは複数の単語を与えた時、その単語を使って文を生成します。

例えば、

入力: 家, ゴキブリ, 殺虫剤 期待される出力の例: 家でゴキブリが出たので殺虫剤で殺した

私は単純な文の生成のために、述語項構造シソーラスを用いることを考えています。事前に「述語」だけがわかっていると仮定して、述語項構造シソーラスからその述語に対するテンプレートを取得し、テンプレートに入力語の順列を当てはめる、というタスクを考えています。

文の自然さの自動アノテーション

BCCWJのようなコーパスを持っていると仮定します。このようなコーパスを、文単位で区切って取得した時、これらの文は「すべて自然な文である」と仮定します。

述語項構造シソーラスでは、

動く(x, y, z)

のような形式で、動詞と動詞の項からなるテンプレートがある状態です。私が解きたいタスクでは、述語は事前に判明しているという仮定なので、求めたいのは「そこに当てはめる項(とその順番)」ということになります。

そこで、BCCWJの自然な文のリストから不自然な文を生成するために、以下の方法を使います。

方法: 各文の名詞をすべての順列として入れ替える。入れ替えてないものをTrue, それ以外をFalseにする。

これにより、distant supervision的に自動アノテーションが可能です。

コード

# coding: utf-8
import MeCab
from tqdm import tqdm
from itertools import permutations

def split_custom(sentence, tagger):
    try:
        lines = [x.split("\t") for x in tagger.parse(sentence).split("\n")]
        lines = [(x[0], x[1].split(",")[0]) for x in lines if len(x) == 2]
        return lines
    except Exception as e:
        print(e)
        return []


def has_noun(ss):
    for s in ss:
        if s[1] == "名詞":
            return True
    return False


def noun_perm(ss):
    temp = ""
    ns = []
    for s in ss:
        if s[1] == "名詞":
            temp += "{}"
            ns.append(s[0])
        else:
            temp += s[0]
    return temp, ns


def run(out, outfile="annotated.txt"):
    with open(outfile, "w") as f:
        for ss in tqdm(out):
            temp, ns = noun_perm(ss)
            if len(ns) > 5:
                continue
            for i, x in enumerate(permutations(ns)):
                if i == 0:
                    label = "1"
                else:
                    label = "0"
                f.write("{}\t{}\n".format(temp.format(*x),label))



if __name__ == "__main__":
    tagger = MeCab.Tagger("-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd")

    with open("data.txt") as f:
        out = []
        for line in tqdm(f):
            line = line.strip()
            tmp = split_custom(line, tagger)
            if tmp:
                out.append(tmp)


    out = [ss for ss in tqdm(out) if has_noun(ss)]
    run(out)

補足

ちなみに、このような特殊なタスクではなく、より一般的に「文の自然さ」を評価したい場合は、名詞を入れ替えるだけではなく、すべての形態素を入れ替えることによって不自然な文が生成できます。

確かに、一定の確率で「入れ替えた文が自然になる」ようなケースもありますが、そのようなケースは稀です。

リンク

述語項構造シソーラス - http://pth.cl.cs.okayama-u.ac.jp/ BCCWJ - https://pj.ninjal.ac.jp/corpus_center/bccwj/

wordnetを使った特徴量設計

wordnetとは、概念の関係を表すシソーラス(辞書)です。概念の階層はネットワークとして表せるため、networkxへ読み込むことができます。ここでは、日本語wordnetを使い、ルートノードから指定した日本語の単語までの概念パスを求めます。その概念パスを使った特徴量設計を考えます。

日本語wordnetのダウンロード

#!/bin/bash
mkdir data
pushd data
if [ -f wnjpn.db ]; then
    echo "wnjpn.db exists"
else
    wget http://compling.hss.ntu.edu.sg/wnja/data/1.1/wnjpn.db.gz
    gunzip *.gz
fi
popd

コード

# coding: utf-8
import sqlite3
import networkx as nx
import pickle
import pandas as pd

    
def get_synset_from_lemma(conn, lemma):
    cur = conn.cursor()
    cur.execute("select wordid from word where lemma=?",(lemma, ))
    wordid = cur.fetchone()[0]
    cur.execute("select synset from sense where wordid=?",(wordid,))
    return cur.fetchone()[0]


def calc_synset_path(conn, G, lemma):
    root_node = get_synset_from_lemma(conn, "entity")
    target_node = get_synset_from_lemma(conn, lemma)
    path = nx.shortest_path(G, root_node, target_node)
    out = []
    cur = conn.cursor()
    for node in path:
        cur.execute("select * from synset where synset=?",(node,))
        tmp = cur.fetchone()[2]
        out.append(tmp)
    return out

if __name__ == "__main__":
    import sys
    conn = sqlite3.connect("../data/wnjpn.db")
    cur = conn.cursor()
    cur.execute("select * from synlink")
    links = cur.fetchall()
    links = [(x[0], x[1]) for x in links]

    G = nx.DiGraph()
    G.add_edges_from(links)

    #with open("synlink_nx.pkl", "wb") as f:
    #    pickle.dump(G, f)

    print(calc_synset_path(conn, G, sys.argv[1]))

実行

python wn.py パン

出力

['entity', 'physical_entity', 'matter', 'solid', 'food', 'baked_goods', 'bread']

使いみち

概念パスの深さや、最短パス内の深さnの位置の概念IDを、自然言語処理における特徴量として使えます。概念パスの深さは「概念の抽象性」を表し、概念パス内の深さnの位置の概念IDは、概念の分類を表します。

特に、概念の分類を特徴量にする場合、深さnをどのように指定するかに依存します。深さが浅い場合はより抽象的な分類になり、分類カテゴリーの数が減ります。深さが深い場合は具体性が増しますが、分類カテゴリーが増えてしまい、未知のデータに対して未知のカテゴリーが出てきてしまう可能性が高まります。

ただし、wordnet特徴量は、wordnet内に存在する語にのみ対応できます。

リンク

http://compling.hss.ntu.edu.sg/wnja/

BERT苦行録3 - OpenIEをBERTで解く

OpenIEとは関係抽出タスクの一種で、文から主語、動詞、目的語のトリプルを抽出するタスクです。ナレッジグラフの自動構築などに使われます。今回はBERTをファインチューニングしてOpenIEに適用してみましょう。

イデア

BERTのrun_squad.pyでは、ソース文、質問文、回答のソース内位置(開始・終了)を訓練データとして入力し、位置を予測するタスクとして解いています。

これを、S,V,Oのそれぞれの位置を予測するよう変更できないか、というのが私のアイデアです。

コード

実際のコードは以下で公開しました。 https://github.com/sugiyamath/bert_openie/blob/master/run_openie.py

モデルの部分のコードだけ載せます。

def create_model(bert_config, is_training, input_ids, input_mask, segment_ids,
                 use_one_hot_embeddings):
  """Creates a classification model."""
  model = modeling.BertModel(
      config=bert_config,
      is_training=is_training,
      input_ids=input_ids,
      input_mask=input_mask,
      token_type_ids=segment_ids,
      use_one_hot_embeddings=use_one_hot_embeddings)

  final_hidden = model.get_sequence_output()

  final_hidden_shape = modeling.get_shape_list(final_hidden, expected_rank=3)
  batch_size = final_hidden_shape[0]
  seq_length = final_hidden_shape[1]
  hidden_size = final_hidden_shape[2]

  output_weights = tf.get_variable(
      "cls/openie/output_weights", [6, hidden_size],
      initializer=tf.truncated_normal_initializer(stddev=0.02))

  output_bias = tf.get_variable(
      "cls/openie/output_bias", [6], initializer=tf.zeros_initializer())

  final_hidden_matrix = tf.reshape(final_hidden,
                                   [batch_size * seq_length, hidden_size])
  logits = tf.matmul(final_hidden_matrix, output_weights, transpose_b=True)
  logits = tf.nn.bias_add(logits, output_bias)

  logits = tf.reshape(logits, [batch_size, seq_length, 6])
  logits = tf.transpose(logits, [2, 0, 1])

  unstacked_logits = tf.unstack(logits, axis=0)

  (S_start_logits, S_end_logits) = (unstacked_logits[0], unstacked_logits[1])
  (V_start_logits, V_end_logits) = (unstacked_logits[2], unstacked_logits[3])
  (O_start_logits, O_end_logits) = (unstacked_logits[4], unstacked_logits[5])

  return (S_start_logits, S_end_logits, V_start_logits, V_end_logits, O_start_logits, O_end_logits)

要するに、各サブトークンの位置で開始位置・終了位置である確率を予測します。これらの位置は、サブトークンの位置からドキュメントトークンの位置へと変換され、さらに文字の位置へと変換されるようです。

そのため、日本語で実行する場合は、ドキュメントトークンの分割方法を検討する必要があります。(英語は空白文字で区切っている)

予測結果

テストデータに対する予測結果は以下で公開しています。 https://raw.githubusercontent.com/sugiyamath/bert_openie/master/predictions.json

ちなみに、データはTupleInfの一部を使っています。 https://github.com/sugiyamath/bert_openie/tree/master/TupleInfKB

予測結果の一部を見てみます。

    "10213": [
        "Burned or dried leaf edges and wilted plants",
        "are",
        "a sign of excess fertilizer application."
    ],
    "10214": [
        "Burning",
        "changes",
        "the physical and chemical factors of the ecosystem."
    ],
    "10215": [
        "Burning and the rusting of iron",
        "are",
        "examples of chemical change."
    ],

これらの例は比較的マシですが、以下のようなひどい結果もあります。

    "10209": [
        "Bureau",
        "Bureau \u00e0 gradin",
        "Bureau \u00e0 gradin"
    ],

実行コマンド

実行コマンドの例は以下です。

python run_openie.py --vocab_file=/root/work/multilingual_L-12_H-768_A-12/vocab.txt --bert_config_file=/root/work/multilingual_L-12_H-768_A-12/bert_config.json --init_checkpoint=/root/work/multilingual_L-12_H-768_A-12/bert_model.ckpt --do_train=True --train_file=/root/work/bert/bert_openie/TupleInfKB/train.json --do_predict=True --predict_file=/root/work/bert/bert_openie/TupleInfKB/dev.json --train_batch_size=24 --learning_rate=3e-5 --num_train_epochs=2.0 --max_seq_length=128 --doc_stride=128 --output_dir=/root/work/output --use_tpu=False --version_2_with_negative=True

--version_2_with_negativeは、予測が不可能な場合を考慮する場合に使います。このオプションは有効化してください。

補足

  • このモデルは1文に複数のSVOが存在するケースが考慮されていません。もっともスコアの高いものだけが出力されます。
  • 疑問文が接続詞として勘違いされ、目的語扱いされてしまう場合があります。
  • 精度の評価をしていません。

 リンク

https://github.com/sugiyamath/bert_openie/

参考

[0] https://github.com/google-research/bert/ [1] http://data.allenai.org/tuple-ie/

poem_detector: Qiitaのクズ記事を判定する機械学習モデル

記事の良し悪しを判定するのは、文書分類の典型的なタスクです。イイネが記事の良し悪しを意味しないなら、アノテーション基準を制定する方法でモデルを構築すればそれなりのものができそうなので、試してみましょう。

パイプライン

  1. 良い記事と悪い記事の基準を考える。
  2. その基準を元に、良い記事URLと悪い記事URLを手動で収集。
  3. URLから本文テキストのみを抽出。
  4. 本文テキストだけを用いてモデリング
  5. モデルを他の記事へ適用。

注意: スクレイピングでQiitaに負荷をかけないに気をつけましょう。

パイプラインをモジュール化したgithub上のプロジェクト

上記パイプラインを誰でも簡単に実行できる形式にしたので、以下のgithubプロジェクトで公開しておきます。

https://github.com/sugiyamath/poem_detector

実行方法

訓練済みモデルを使いたい場合はプロジェクト内のREADME.mdを参考にしてください。 イチから自分のモデルを作ってみたい方は以下。

1. 良い記事と悪い記事のURLリストを手動で定義

プロジェクト内に bad_articles.txtgood_articles.txt があります。それぞれに悪い記事と良い記事のURLを貼ってください。URLは多いほどよいです。

2. コンテンツ抽出

python get_content.py を実行すると、content.csv というファイルが出力されます。

3. 前処理 & 訓練

python model.py を実行すると、content.csv を使って訓練が行われます。訓練によって、vec.pklmodel.pkl が出力されます。これがモデルです。

4. 交差検証

モデルをAUCで交差検証をするために、python eval.py を実行してください。最初に出力される配列はRandomForestの交差検証結果、次のものはLogisticRegressionです。目安として、0.8以上の値が出れば汎化性能を持っている可能性があります。汎化性能は、前処理・特徴量・モデル・データなどに依存します。URLリストの定義次第で性能が変わります。

5. モデルを利用する

predictor.py を使えばモデルを実際の予測に使えます。predictor.pyの中身は以下です。

from model import predict, load
from get_content import get_content

class Predictor:

    def __init__(self):
        self.clf, self.vec = load()

    def predict(self, path, is_url=True):
        if is_url:
            return predict(self.clf, self.vec, [get_content(path)])
        else:
            with open(path) as f:
                return predict(self.clf, self.vec, [f.read()])

    def predict_list(self, path_list, is_url=True):
        if is_url:
            return predict(self.clf, self.vec, [get_content(path) for path in path_list])
        else:
            for path in path_list:
                data = []
                with open(path) as f:
                    data.append(f.read())
                return predict(self.clf, self.vec, data)


if __name__ == "__main__":
    predictor = Predictor()
    print(predictor.predict("https://qiita.com/taku910/items/7e52f1e58d0ea6e7859c"))
    print(predictor.predict("https://qiita.com/Yuta_Yamamoto/items/fbeb7b31173b3e787fc2"))
    print(predictor.predict("https://qiita.com/riversun/items/29d5264480dd06c7b9fb"))
    print(predictor.predict("https://qiita.com/_EnumHack/items/3d7d50c43523c71ab307"))
    print(predictor.predict("https://qiita.com/tani_AI_Academy/items/3edc5effeb386ae3caa9"))
    print(predictor.predict("https://qiita.com/charmston/items/df31a419a4e57ebe86ba"))
    print(predictor.predict("https://qiita.com/rana_kualu/items/afe544b0f5680e81fabc"))
    print(predictor.predict("https://qiita.com/qiitadaisuki/items/2160a390ce91283707a1"))
    print(predictor.predict("https://qiita.com/Kosuke-Szk/items/4b74b5cce84f423b7125"))
    print(predictor.predict("https://qiita.com/Y_F_Acoustics/items/6742a0a8ad6b37a0f4f5"))
    print(predictor.predict("https://qiita.com/ozikot/items/f7e5c346e631de067efb"))
    print(predictor.predict("https://qiita.com/kurogelee/items/1c081a5a7e209e81921c"))

使い方はコードを見てのとおりです。要するに、Predictorクラスのpredictを呼べば、指定したURLを判定します。sklearnのpredict_probaを使っているので、出力は以下のようになります。

[[0.1, 0.9]]

0.1は「悪い記事である確率」で、0.9は「良い記事である確率」です。

モデルの詳細

このモデルは文字ベースBowを使ったランダムフォレストです。

なぜこのモデルを使ったかというと、

  1. ユーザー属性を使いたくない。
  2. いいね数を使いたくない。

の二点です。要するに、バイアスを避けるためにコンテンツだけを評価するモデルが欲しかったわけです。

単語ベースBoWとLogisticRegressionを使ったモデルはよく見かけますが、これが機能する理由は「単語の出現の有無を重みで評価する」ようなモデルだからです。

これに対し、文字ベースBoW + RandomForestは、「文字の出現の組合せを条件分岐的に評価する」ようなモデルとなります。

なぜ文字ベースを採用するのかというと、単語ベースの場合は特徴量ベクトルが大きくなりすぎてしまうのと、データ数が多くないと精度が上がらない可能性があったためです。文字ベースならば、文字の組合せであるため、遥かに少ない特徴量ベクトルになります。

訓練済みモデルの交差検証の結果を確かめるため、以下を実行します。

git clone https://github.com/sugiyamath/poem_detector/
cd poem_detector
python eval.py
[0.8625     0.80972222 0.86111111 0.73611111 0.87777778]
[0.675      0.46666667 0.82777778 0.63055556 0.63055556]

RandomForestはLogisticRegressionよりも高い精度が出ており、文字ベースに適していることが推測できます。しかし、評価データが少ないこともあり、文字ベースBoW+RandomForestという手法に確証が持ててはいません。

組合せを評価するのであれば、むしろLSTMやCNNのほうがよいかもしれません。試してみてはどうでしょう。

教師データが重要

結局、モデリングがどうであれ、記事の良し悪しの分類の本質は教師データの用意です。一貫したアノテーション基準のもとでURLを用意し、URLを大量に用意すれば精度は高まります。

これは、「いいね数」「リツイート数」「contribute数」を使った方法とは本質的に異なっています。いいね数を使ったモデルは、結局「いいね数の予測」でしかありませんから、記事の良し悪しについて何も言及していることにはなりません。

一方、一貫したアノテーション基準を元に教師データが用意できれば、「良い記事とは○○である」と定義された上で教師データを用意できるため、定義を柔軟に変更することが可能です。これは、良い記事の定義方法について柔軟性を持っていると言えます。

なにより、本文だけを使ってバイアスを極力避ければ、出力の良し悪しは教師ラベルに依存することになるため、「良い教師ラベルを準備すること」に専念できます。End2Endの良さはこういったところにもあるのかもしれません。

リンク

https://github.com/sugiyamath/poem_detector/

BERT苦行録2 - sentencepieceを使って事前訓練

BERTを用いて日本語ツイートの感情分析を試すという記事では、BERTについてファインチューニングと事前訓練を行いました。今回は事前訓練を行う上での注意点を書きます。

1. 語彙数とトーカナイザの問題

MeCabボキャブラリにあわせてBERTを訓練をしようと試みましたが、それは愚かです。そのような方法を使った場合、「語彙数を頻度等で切り捨てる」ようなことをすることになり、カバーできる文が少なくなってしまいます。

@taku910 さんは、sentencepieceの記事で以下のようにおっしゃっています。

単語をそのまま扱うのは実用上の問題点があります。RNNによるテキスト生成では、語彙サイズに依存した計算量が必要となるめ、大規模な語彙を扱えません。高頻度語彙のみに限定することで計算量の問題は回避できますが、低頻度語が捨てられてしまいます。この問題を解決する手法の一つがSentencepieceの土台ともなったサブワードです。

つまり、語彙数に依存したモデルは語彙を圧縮する方法を使ったほうが良さそうです。BERTでは、MeCabを使うよりも、むしろsentencepieceを使ったほうがよいと思います。

sentencepieceを試しに使ったモデルの設定は以下で公開しました。 https://github.com/sugiyamath/bert/tree/master/jamodel

jawiki.model, jawiki.vocabはどちらもsentencepieceのモデルです。

BERTのトーカナイザは、正規化を無効化し、中国文字判定を無効化しておきます。その上で、BERTのモデルに渡す入力データは、毎回事前にsentencepieceでトーカナイズしておきます。これでBERTはsentencepieceに対応できます。

2. 巨大なデータの前処理はデータを分割せよ

jawiki全体を前処理したらOOMが起こりました。この問題をbertのissueで残したら以下の返信がありました:

You should shard the input data (text.txt_00000, text.txt_00001), run the script for each shard (tf_examples.tfrecord_00000, tf_examples.tf_record_00001), and then pass in a file glob (e.g., tf_examples.tfrecord*) to run_pretraining.py. データをtext.txt_00000, text.txt_00001のように分割してそれぞれtf_examples.tfrecord_00000,...でスクリプトを回し、run_pretraining.pyに対してtf_examples.tfrecord*というglobを渡してください。

大体以下のような感じになりました。(マルチプロセスとマルチスレッドを組合せている部分は気休めです。)

from subprocess import check_output
import os
from tqdm import tqdm
from functools import partial
from multiprocessing.pool import ThreadPool

def build_command(input_file, output_file, vocab_file="/root/work/bert/jamodel/vocab.txt"):
    cmd = ["python","/root/work/bert/create_pretraining_data.py",
           "--input_file={}".format(input_file),
           "--output_file={}".format(output_file),
           "--vocab_file={}".format(vocab_file),
       "--do_lower_case=False","--max_seq_length=128","--max_predictions_per_seq=20",
       "--masked_lm_prob=0.15","--random_seed=12345","--dupe_factor=2"]
    result = check_output(cmd)
    return result


def execute(i, input_dir, output_dir, vocab_file):
    try:
        datanum = str(i).rjust(6, '0')
        input_file = os.path.join(input_dir, "text.txt_{}".format(datanum))
        output_file = os.path.join(output_dir, "tf_examples.tf_record_{}".format(datanum))
        if os.path.exists(output_file):
            return None
        else:
            build_command(input_file, output_file, vocab_file)
            print(str(i), end=' ', flush=True)
    except:
        print("Error:"+str(i), end=' ', flush=True)


def execute_them(ds, input_dir, output_dir, vocab_file, poolsize=10):
    try:
        pool = ThreadPool(poolsize)
        func = partial(execute,
            input_dir=input_dir,
            output_dir=output_dir,
            vocab_file=vocab_file)

        pool.map(func, ds)
    except Exception as e:
        print(e)
        print("Error in pool")
        

def main(input_dir, output_dir, vocab_file, datasize=999425):
    from multiprocessing import Pool
    import numpy as np

    poolsize = 5
    targets = np.split(np.array(list(range(datasize))), 5)
    
    func = partial(execute_them,
            input_dir=input_dir,
            output_dir=output_dir,
            vocab_file=vocab_file)
    pool = Pool(5)
    pool.map(func, targets)


if __name__ == "__main__":
    main("/root/work/data/txt_data/", "/root/work/data/records", "/root/work/bert/jamodel/vocab.txt")

ということで、OOMが発生するので分割してください。

BERT苦行録 - ファインチューニングして日本語ツイートを分類

BERTとは言語表現モデルで、GLUEデータセットなどに対して最近SOTAを達成したものです。今回は、日本語ツイートの感情分析を試し、その後四苦八苦して色々試したことを書きます。

BERTのダウンロード

git clone https://github.com/google-research/bert
wget https://storage.googleapis.com/bert_models/2018_11_03/multilingual_L-12_H-768_A-12.zip
unzip multilingual*

ツイート感情分析データの用意

以下の記事で用いた方法でデータを取得します。 https://qiita.com/sugiyamath/items/7cabef39390c4a07e4d8

取得済みのデータは以下。 https://github.com/sugiyamath/bert/tree/master/JAS

run_classifier.pyを修正

以下を追加します。

class JasProcessor(DataProcessor):

  def read_tsv(self, path):
    df = pd.read_csv(path, sep="\t")
    return [(str(text), str(label)) for text,label in zip(df['text'], df['label'])]

  
  def get_train_examples(self, data_dir):
    """See base class."""
    return self._create_examples(
        self.read_tsv(os.path.join(data_dir, "train.tsv")), "train")

  def get_dev_examples(self, data_dir):
    """See base class."""
    return self._create_examples(
        self.read_tsv(os.path.join(data_dir, "dev.tsv")), "dev")

  def get_test_examples(self, data_dir):
    """See base class."""
    return self._create_examples(
      self.read_tsv(os.path.join(data_dir, "test.tsv")), "test")

  def get_labels(self):
    """See base class."""
    return ["0", "1", "2", "3", "4", "5"]

  def _create_examples(self, lines, set_type):
    """Creates examples for the training and dev sets."""
    examples = []
    for (i, line) in enumerate(lines):
      guid = "%s-%s" % (set_type, i)
      text_a = tokenization.convert_to_unicode(line[0])
      label = tokenization.convert_to_unicode(line[1])
      examples.append(
          InputExample(guid=guid, text_a=text_a, text_b=None, label=label))
    return examples

さらにmain関数内の以下部分を修正します。

  processors = {
      "cola": ColaProcessor,
      "mnli": MnliProcessor,
      "mrpc": MrpcProcessor,
      "xnli": XnliProcessor,
      "jas": JasProcessor
  }

実行

JASをGLUEデータセットディレクトリの中につっこみます。

mv JAS glue_data

ついで、以下コマンドを実行します。(exportの部分は修正してください。)

export BERT_BASE_DIR=/path/to/bert/multilingual_L-12_H-768_A-12
export GLUE_DIR=/path/to/glue_data

python run_classifier.py \
  --task_name=JAS \
  --do_train=true \
  --do_eval=true \
  --data_dir=$GLUE_DIR/JAS \
  --vocab_file=$BERT_BASE_DIR/vocab.txt \
  --bert_config_file=$BERT_BASE_DIR/bert_config.json \
  --init_checkpoint=$BERT_BASE_DIR/bert_model.ckpt \
  --max_seq_length=128 \
  --train_batch_size=32 \
  --learning_rate=2e-5 \
  --num_train_epochs=3.0 \
  --output_dir=/tmp/jas_output/

結果は以下です。

eval_accuracy = 0.363
eval_loss = 1.5512451
global_step = 937
loss = 1.5512451

あまり良い結果ではないですね。文字ベース1次元CNNの方が良い結果でした。

その後の四苦八苦

日本語のボキャブラリが少ないのが原因かと思いましたが、WordPieceという手法をわざわざ使っているので、語の基本要素のようなものをネットワーク内で組み合せることがキモの一つなのかなーとも思います。

ともあれ、MeCabボキャブラリーに合わせてコードを修正したらどうなるか試したわけです。

修正したtokenizerは以下です。 https://github.com/sugiyamath/bert/blob/master/tokenization.py

修正部分は

  1. テキストの正規化をしないようにする。(濁点が消えないように)
  2. 中国文字を認識しないようにする。(文字ベースにならないように)

で、wikipediaから作成したvocab.txtは以下です。 https://github.com/sugiyamath/bert/blob/master/jamodel/vocab.txt

最後に、以下スクリプトを実行。

#!/bin/bash

export BERT_BASE_DIR=/root/work/bert/jamodel
FILE=/root/work/tf_examples.tfrecord


if [ -f $FILE ]; then
    echo "FILE EXISTS"
else
    python create_pretraining_data.py \
       --input_file=/root/work/data/splitted_1.txt \
       --output_file=/root/work/tf_examples.tfrecord \
       --vocab_file=$BERT_BASE_DIR/vocab.txt \
       --do_lower_case=True \
       --max_seq_length=128 \
       --max_predictions_per_seq=20 \
       --masked_lm_prob=0.15 \
       --random_seed=12345 \
       --dupe_factor=5
fi


python run_pretraining.py \
       --input_file=/root/work/tf_examples.tfrecord \
       --output_dir=/root/work/pretraining_output \
       --do_train=True \
       --do_eval=True \
       --bert_config_file=$BERT_BASE_DIR/bert_config.json \
       --train_batch_size=32 \
       --max_seq_length=128 \
       --max_predictions_per_seq=20 \
       --num_train_steps=20 \
       --num_warmup_steps=10 \
       --learning_rate=2e-5

結果は... run_pretraining.pyでOOMエラー。理由は、ボキャブラリーのサイズがでかすぎてネットワークが巨大になるので。

ボキャブラリー自体はEmbedding等で低次元に圧縮されるのかと思っていたのですが、どういうことなのかちょっとわかりません。<- tensorflowのコードをよくわかってない。

ちなみに、bert_configは以下です。

{
  "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,
  "num_attention_heads": 6,
  "num_hidden_layers": 6,
  "type_vocab_size": 2,
  "vocab_size": 1710509
}

うーん、どうやらGoogleのオリジナルのコードを使った方法だと、WordPieceに依存しているので、ボキャブラリ等もそういうものに合わせないとダメなんでしょうか。

詳しい方、教えてください m( )m

ちなみに、splitted_1.txtはこちらを参考にしていますが、create_pretraining_data.pyの実行は正常に完了しています。

参考

[0] https://qiita.com/Kosuke-Szk/items/4b74b5cce84f423b7125 [1] https://github.com/google-research/bert