Bertで不自然な文を検知してみる

Bertなどの学習済みのモデルは、多くのデータから最もありえそうな単語の並びのパターンを学習しているといえる。 なので文法的に間違っている場合など不自然な位置に単語があったりすれば、その単語の出現確率は低く出るはず。

ということで簡単なスクリプトを書いて試してみたのでメモ

pytorchで自然言語系のモデルが集めてあるtransformersを用いる https://github.com/huggingface/transformers

モデルは代表的ということでBertを使う

全体的なロジックはこんな感じ

  • 各単語をに置き換えた文を生成しまとめて一つのbatchとする
  • に当てはまる単語のスコアを算出して0.0~1.0の範囲に正規化
  • 元の文に含まれていた単語のスコアがしきい値より低ければ不自然と判定
def detect(self, sentence):
    list = []

    # 各単語を<MASK>で置き換えた文を生成して一つのbatchにする
    # <CLS>, <EOS>も追加されることに注意
    input_ids = torch.tensor([self.tokenizer.encode(sentence, add_special_tokens=True)])
    n_words = input_ids.shape[1]

    for i in range(1, n_words - 1):
        ids = input_ids.clone()
        ids[0][i] = self.tokenizer.mask_token_id
        list.append(ids)

    input = torch.cat(list, dim=0)

    # 推論実行
    with torch.no_grad():
        output = self.model(input)

    all_scores = output[0]
  
    # <CLS>, <EOS>を除いたtokenについてスコアがしきい値より低いものがあれば不自然と判定
    is_strange = False
    total = 0
    for i in range(1, n_words - 1):
        scores = all_scores[i-1][i]
        topk = torch.topk(scores, 5)

        score = Score(input_ids[0][i], scores, self.tokenizer)
        top_scores = [Score(id.item(), scores, self.tokenizer) for id in topk.indices]
        is_strange = is_strange or score.value_std < self.config.threshold
        total += score.value_std

class Score:
    def __init__(self, id, scores, tokenizer):
        self.id = id
        self.value = scores[id].item()
        self.word = tokenizer.decode([id])
        # token毎にscoreのmin, maxは異なるため0.0~1.0の範囲に正規化する
        min_value = torch.min(scores)
        max_value = torch.max(scores)
        self.value_std = (self.value - min_value) / (max_value - min_value)

こちらの文を判定してみた結果
Brad came to dinner with us.

['brad', 'came', 'to', 'dinner', 'with', 'us', '.']
original word: (0.72, brad): top score: [(1.00, she), (0.99, he), (0.91, you), (0.89, they), (0.85, mom)]
original word: (1.00, came): top score: [(1.00, came), (0.93, went), (0.92, comes), (0.84, goes), (0.83, coming)]
original word: (1.00, to): top score: [(1.00, to), (0.90, for), (0.78, after), (0.78, into), (0.76, over)]
original word: (0.94, dinner): top score: [(1.00, sit), (0.94, dinner), (0.94, live), (0.93, stay), (0.91, be)]
original word: (1.00, with): top score: [(1.00, with), (0.85, for), (0.78, without), (0.77, after), (0.74, before)]
original word: (0.94, us): top score: [(1.00, me), (0.94, us), (0.92, her), (0.88, them), (0.87, brad)]
original word: (1.00, .): top score: [(1.00, .), (0.90, ;), (0.84, ?), (0.82, !), (0.66, |)]
average 0.942

top scoreの部分にはtop5のワードのスコアを参考に出力しているだけ

名前の部分以外はかなり高いスコアになっている。 名前はレアなものであればスコアは低くなると考えられるためaverageで判定する方がよいかも。

先程の文を適当に単語順序を入れ替えて判定したものがこちら

['brad', 'dinner', 'with', 'came', 'to', 'us', '.']
original word: (0.48, brad): top score: [(1.00, the), (0.98, our), (0.95, a), (0.93, my), (0.91, that)]
original word: (0.58, dinner): top score: [(1.00, along), (0.95, ,), (0.94, came), (0.92, and), (0.89, come)]
original word: (0.60, with): top score: [(1.00, never), (0.99, finally), (0.92, always), (0.90, time), (0.90, ##time)]
original word: (0.74, came): top score: [(1.00, dinner), (0.98, you), (0.97, him), (0.95, daniel), (0.94, two)]
original word: (1.00, to): top score: [(1.00, to), (0.93, for), (0.91, with), (0.91, before), (0.87, between)]
original word: (0.75, us): top score: [(1.00, mind), (0.90, me), (0.89, life), (0.86, him), (0.84, her)]
original word: (1.00, .): top score: [(1.00, .), (0.89, ;), (0.81, ?), (0.80, !), (0.65, |)]
average 0.735

brad dinner withと連なっている部分のスコアがかなり低くなった。 averageも低い値になっているためこのくらいの差があればしきい値をつけて判定できそう。

以下の場合に不自然と判定することでそれなりに変な文を見つけることはできそう

  • 一つの単語で著しくスコアが低い場合
    • もしくはn-gramをとってその平均スコアを見る
  • averageのスコアが一定より低い場合

以下は改善すべき点

  • 特に固有名詞など出現頻度が低い単語が使われいた場合にはスコアが低く出るはずのため誤検知すると考えられる
    • 今回は品詞があっている段階で一定以上のスコアになっていることを期待しているが、そうでない場合もあるはず
  • 1tokenあたりの寄与率が変わるため、文の長さによって平均スコアのしきい値を補正した方がよい

今回試したスクリプトの全体はこちら https://github.com/y-kamiya/unnatural-sentence-detector