[Ruby]ベイズ推定(AI)で、氏名を識別する

広告

Rubyを使って、人の氏名(日本人名限定)を識別する為に、ベイズ推定を利用してみました。結果としてはサンプル数(教師ありデータ数)が少ないこともあり、微妙な結果になってしまいましたが、後々他のアプローチも試してみたいので、一旦記事にして残しておこうと思います。

【背景】仕事柄、個人情報を扱うことが多い。

当然、非常にセンシティブに個人情報を扱っているので、サーバーにあるファイル管理も大変で、個人情報が入っているファイルにパスワードをかけるのは当たり前だが、そうでないファイルまでパスワードがかかっていることもしばしば。
  結果として、社内で使いまわせる資料もパスワードの陰に隠れて利用できないという哀しい状況。

  とはいえ、氏名がファイルに含まれているかどうかを識別するというのは結構、大変で結局人がチェックしないとダメな事が多くなります。(勿論、Excelのファイルとかなら、氏名欄があるかないかみたいな簡単なチェックもあるけど)

  あまり無いが、テンプレートや文書をサンプル的に提案書に利用する際に、マスキングとか、も結構手間です。

AIでできないのか?

世の中、AIブームです。勉強をしてみたいもののイマイチ理解が進んでいません。何か試してみたくても、何か身近なテーマがあれば、実験できるのにと思っていた矢先に上記の状況が思い当たりました。同僚と「氏名と一般名詞の識別ってAIとかじゃ無理なんですかね」「キラキラネームとかあるし、むずかしいんじゃない」「そうだよね〜」

という会話をしたものの、ちょっと色々調べてみました。…負けず嫌い(笑)

形態素解析のmecabを利用すると、品詞が識別されるのだけどその中に氏名か否かという区別はある様子だが、名字になりうる文字は全部氏名としてしまうので、「張」とか「金」も氏名として識別されてしまいちょっと過剰。

もう少し、精度をよく識別したいと思い、機械学習(ベイズ推定)を試すことにしました。

ベイズ推定でアプローチをしてみる。

アプローチとして、氏名データと氏名じゃないデータ(一般名詞)をたくさん用意して、学習させて、同様の別データを使って識別率を測るという方法を取ることにした。いわゆる機械学習のやり方としては、いっぱいやり方があるわけですが、理解が簡単なものは難しい。 そんななかで、ベイズ統計については、昔本を読んだ事を思い出しました。

通読できたかどうかさえ覚えていませんし、なんとなくのイメージしか理解できなかったのですが、単なる分類をするだけならば、使えるのではないかと当たりをつけました。 本当は、Pythonのほうが、色々便利なんだと思いますが、いつも使っているのがRubyということもあり、Rubyでできるベイズ推定についての記事を検索し、以下の記事を見つけました。

他にも同様の記事はたくさんあったのですが、mecabを利用して文章を分類するという類のもので、この記事は単純に要素と分類を読み込むというコードで、単純でした。まずは、ベイズ推定を行うクラスの実装です。

# encoding: utf-8
# naivebayesの実装 http://qiita.com/mochizukikotaro/items/64247e14024c2c5534d9 を参考
include Math
require "set"

class NaiveBayes

  def initialize()
    @category = Set.new
    @num_of_train_data = 0
    @num_of_cat = Hash.new(0.0) # @num_of_cat[cat] = {catの数}
    @num_of_word = Hash.new{|h,k| h[k] = Hash.new(0.0)} # @num_of_word[cat][word] = {wordの数}

    @prob_of_category = Hash.new(0.0) # P(cat)
    @prob_of_word = Hash.new{|h,k| h[k] = Hash.new(0.0)} # P(word|cat)
  end

  # トレーニングをする(P(cat)までを求めています)
  def train(train_data)
    @num_of_train_data = train_data.count
    set_num(train_data)
    set_prob_cat
  end

  # テストデータの分類をする(P(word|cat)まで求めています)
  def classify(test_data)
    test_data.each do |v|
      doc = v[:doc]
      set_prob_word(doc)
      likelihood = {}
      @category.each do |cat|
        likelihood[cat] = log(@prob_of_category[cat]) + doc.map{|word| log(@prob_of_word[cat][word])}.inject(:+)
      end
      # 出力
      #puts @prob_of_word
      #puts likelihood
      print "テストデータ: #{v[:doc].join(',')} => 判定: #{likelihood.max{|a,b| a[1]<=>b[1]}[0]}\n\n"
    end
  end
  def classify_each(voca)
    doc=voca[:doc]
    set_prob_word(doc)
    likelihood = {}
    @category.each do |cat|
      likelihood[cat] = log(@prob_of_category[cat]) + doc.map{|word| log(@prob_of_word[cat][word])}.inject(:+)
    end
    result = likelihood.max{|a,b| a[1]<=>b[1]}[0]
    return result
  end

  private

  # トレーニングデータから、カテゴリ数やワード数を計算しておく
  def set_num(train_data)
    train_data.each do |v|
      cat = v[:cat]
      @category.add(cat)
      @num_of_cat[cat] +=1
      doc = v[:doc]
      doc.each do |word|
        #puts word
        @num_of_word[cat][word] += 1
      end
    end
  end

  # P(cat)を求める
  def set_prob_cat
    @category.each do |cat|
      @prob_of_category[cat] = @num_of_cat[cat] / @num_of_train_data
    end
  end

  # P(word|cat) を求める
  def set_prob_word(doc)
    vocabulary = Set.new
    doc.each { |word| vocabulary.add(word) }
    @prob_of_word = Hash.new{|h,k| h[k] = Hash.new(0.0)}
    @category.each do |cat|
      vocabulary.each do |word|
        @prob_of_word[cat][word] = (@num_of_word[cat][word] + 1) / (@num_of_word[cat].map{|k,v| v}.inject(:+) + vocabulary.size)
      end
    end
  end

end

実は、あまりコードの内容を理解しておりません(汗)。ただ、どう使うかだけはわかっているので、教育データ、検証データの作成を行いました。

氏名データは、演劇感想文リンクから

氏名の学習データとしては、わたしが管理している演劇感想文リンクのデータを利用することにしました。詳細は、省きますが演劇感想文リンクの中にある演出、脚本、出演の人の名前を全て抽出し、CSVデータかしました。
なお、中には外国人の名前や芸名で全部ひらがなの名前、英数字が入っている名前などが含まれる為、そういうデータを排除しました。

一般名詞のデータの方が実は相当苦労しました。結果として、青空文庫の夏目漱石「吾輩は猫である」の文章をmecabで形態素解析した上で、名詞データを洗い出したもの、Wikipedeiaの一部の記事なども同様の方法で一般名詞を抽出しました。

こっちが難しいのは、この中に人の名前も含まれており、結果として目で文章をチェックして、氏名を削除する必要があるということです。

結果として、氏名データは、36000名分集まったのですが、一般名詞は9000単語しか集まりませんでした。

wordをどのように作成するか?

学習データの構造として、複数の要素をまとめて、カテゴリー化しています。例えば、文章をカテゴライズするならば、「”スケート”,”観客”,”メダル”」という言葉が含まれる文章を「羽生結弦」、「”将棋”,”盤面”,”手”」が含まれる文章を「羽生善治」という風に分類するなどです。 羽生さんと羽生くんの分類という考え方は以下の記事によります。

 

が、今回は氏名という単語を分類するということなので、形態素解析等で分割しても無意味です(というか、分割された最小単位が対象) とりあえず、以下のような感じで、一文字ずつに分割したものをwordとして学習させることにしました。 例えば、「羽生結弦」さんの名前を氏名として学習させるためには、以下のようなデータを生成します。

@train_data << {cat:"氏名",doc: "羽","生","結”,"弦"}

検証結果1

とりあえず、一般名詞データは9000語と少ないので、氏名データをすこしずつ増やしていきながら正答率を検証していきました。(一般名詞データは、常に9000語を学習させました)

氏名学習数  氏名正答数(正答/総数)率 一般名詞正答数(正答/総数) 率  平均正答率

氏名学習数 氏名正答数(正答/総数) 氏名正答率(A) 一般名詞正答数(正答/総数) 一般名詞正答率(B)

平均正答率(A+B/2)

4000 3620/4000 0.90500 294/327 0.89908257 0.90202413
8000 3704/4000 0.92600 297/327 0.85321101 0.88960551
16000 3770/4000 0.94250 262/327 0.80122324 0.87186162
32000 3823/4000 0.95575 244/327 0.74617737 0.85096368

当たり前ですが、氏名データの学習数を増やすと氏名の正答数は上がっていきますが、一方で一般名詞が氏名に誤認される率が上がります。 単純計算で良いのかどうかわかりませんが、氏名の正答率と一般名詞の正答率を足して2で割るとトータルの正答率は着々と下がっていっています。 データを増やせばよいのですが、あるデータでもう少し正答率があげられないかと少しあがいてみることにしました。

WORDの設定方法を変える

なんらか条件を変えれば、もう少し精度はあげられないかと考えました。(氏名学習数は32000で固定)

1)文字数を追加

  長過ぎたり、短すぎたりする氏名はないので、文字数を要素に加えると精度があがるのではないかと考えました。

2)最初の二文字を要素として追加

  名字には特徴があるので、最初の二文字を要素に加えることで学習効率があがるのではと考えました。

WORDの設定追加

氏名正答数(正答/総数) 氏名正答率(A) 一般名詞正答数(正答/総数) 一般名詞正答率(B)

平均正答率(A+B/2)

文字数をWORDに追加 3855/4000 0.96375 240/327

0.73394495

0.84884748

上記+最初の2文字を追加

3861/4000

0.96525

238/327

0.72782875

0.84653938

…確かに氏名の正答率はあがるのですが、一般名詞の正答率があがりません。というか下がります。

 

思っていた以上に難しい

思っていた以上に難しいというのが、結論です。 データ作成のパワーのかかり方は大変で中々、調査するところまでたどり着きません。 ただ、機械学習には他にも色々な手法があり、試してみたいものも出てきました。データの揃え方も含め、近いうちに再チャレンジしてみたいと思います。 たまたま書店で見た、以下の雑誌でも読んでもう少し研究したいと思います。

広告

コメントを残す

メールアドレスが公開されることはありません。