関口宏司のLuceneブログ

OSS検索ライブラリのLuceneおよびそのサブプロジェクト(Solr/Tika/Mahoutなど)について
<< CaboChaでKNBコーパスを使う | main | Lucene 4.0で実施されたIndexReaderの主なリファクタリング(Uwe氏のブログより) >>
CaboChaとKNBコーパスで固有表現抽出を行う
さらにCaboChaの評価は続く。今回はKNBコーパスを使って固有表現抽出の交差検定(5分割)を行った。

今回はprecisionやrecallをカウントする評価ツールも自作した。実際にどのようにカウントするかをフィクションのデータを例にした下表を使って説明する:

説明番号正解CaboCha出力カウント
形態素NE形態素NE
1自民党B-ORGANIZATION自民党B-ORGANIZATIONtp++
OO
2谷垣B-PERSON谷垣B-PERSONtp++
総裁O総裁O
OO
3B-DATEB-MONEYfn++, fp++
4I-DATEOfn++
夕方O夕方O
遅いO遅いO
昼食O昼食O
OO
5民主党食堂O民主党B-ORGANIZATIONfp++
食堂O
OO
とったOとったO
OO


  1. 「自民党」のNEタグが一致
  2. 「谷垣」のNEタグが一致
  3. 「6」のNEタグが不一致
  4. 「日」のNEタグが出力されず
  5. 「民主党」のNEタグが出力されたが不正解


以上から、このフィクションのデータの例では:

precision = tp / (tp + fp) = 2 / (2 + 2) = 0.5

recall = tp / (tp + fn) = 2 / (2 + 2) = 0.5

F = 2PR / (P + R) = 2 * 0.5 * 0.5 / (0.5 + 0.5) = 0.5 (ここで、P:precision, R:recall)

結果

結果は予想していた通りF値で40から60ポイント程度と低い値となった。原因としては、KNBコーパスはデータ量が多くないがその中でも固有表現タグはさらに数が限定されるためと考えられる。また、モデルファイルのとりかたによって固有表現タグの出現に濃淡があり、そのため正解率にばらつきがある。固有表現タグの種類(ORGANIZATIONやLOCATIONなど)にも数の多い少ないがあり(詳細はKNBコーパスの論文を参照)、これらも影響している可能性があるだろう。さらに、学習データを作成する際に、複数のNEタグ候補があるOPTIONALタグをそのままOPTIONALタグとして出力した。複数候補をCaboChaに学習させる方法がわからなかったためだが、これも学習データの量を減少させ、正解率を落とした原因といえる(このため、評価ツールでは正解データがOPTIONALの場合、fnをカウントしないこととした)。いずれにしてもデータ量が少ないのが残念だ。

モデルファイル名precisionrecallF
ne.juman.10.4596770.3630570.405694
ne.juman.20.5468750.5844160.565022
ne.juman.30.6043580.6085450.606444
ne.juman.40.5444920.5444920.544492
ne.juman.50.5540900.4988120.525000


今回作成したプログラム

KNBコーパスをNEタグ付きのCaboCha形式に変更するプログラム knbc2cabocha_ne.rb と 評価プログラム eval_ne.rb を作成したので掲載する:

knbc2cabocha_ne.rb

このプログラムでは、OPTIONALタグはそのままOPTIONALタグとして出力した。しかし、(おそらくは)CaboChaではOPTIONALタグはスキップしており(?)、OPTIONALの実際の候補であるLOCATIONやORGANIZATIONなどを出力すべきであると考えられる。そのため、次の評価プログラムでは正解データがOPTIONALの場合は、fnはカウントしないようにした。

KNBC_DIR = ARGV[0]

CATEGORIES = ["Keitai", "Kyoto", "Gourmet", "Sports"]

def ne(terms)
  ne_m = //, 1)
  b = ne_m.post_match.slice(/(¥w+):(¥w+)>/, 2)
  b = b == "head" || b == "single" ? "B" : "I"
  return "#{b}-#{type}"
end

CATEGORIES.each{|category|
  File.open("#{category}.dat", "w"){|ofile|
    Dir.glob(KNBC_DIR+"/KN???_#{category}_*").sort!.each{|article_dir_path|
      File.open("#{article_dir_path}/fileinfos"){|info_file|
        while info_line = info_file.gets
          File.open("#{article_dir_path}/#{info_line.split[1].split(/:/)[1]}"){|ifile|
            chunk_num = 0
            while line = ifile.gets
              next if /¥A# S-ID:/ =~ line # skip the comment line
              terms = line.split
              if terms[0] == "*"
#                ofile.puts "* #{chunk_num} #{terms[1]}"
                ofile.puts "* #{chunk_num} -1D"
                chunk_num += 1
              elsif terms[0] == "+"
                next
              elsif terms[0] == "EOS"
                ofile.puts "EOS"
              else
                ne_info = ne(terms)
                ofile.puts "#{terms[0]}¥t#{terms[3]},#{terms[5]},*,*,#{terms[2]},#{terms[1]},*¥t#{ne_info}"
              end
            end
          }
        end
      }
    }
  }
}


eval_ne.rb

def next_term_ne(file)
  while line = file.gets
    next if /¥A¥*/ =~ line
    return "EOS", nil if /¥AEOS/ =~ line
    terms = line.split
    return terms[0], terms[-1]
  end
  return nil, nil
end

def no_ne(ne_a)
  return true if ne_a.length == 1 && ne_a[0] == nil
  ne_a.each{ |ne|
    return false unless ne == "O"
  }
  true
end

def print_terms(ans_str_a, ans_ne_a, out_str_a, out_ne_a)
  if ans_str_a != out_str_a || ans_ne_a != out_ne_a
    puts "#{ans_str_a.join("/")}(#{ans_ne_a.join(",")}) #{out_str_a.join("/")}(#{out_ne_a.join(",")})"
  end
end

def num_ne(ne_a)
  return 0 if ne_a.length == 1 && ne_a[0] == nil
  num = 0
  ne_a.each{ |ne|
    num += 1 unless ne == "O"
  }
  num
end

def check(ans_str_a, ans_ne_a, out_str_a, out_ne_a, tp, fp, fn)
  if !no_ne(ans_ne_a)
    print_terms(ans_str_a, ans_ne_a, out_str_a, out_ne_a)
    if !no_ne(out_ne_a)
      i = 0
      while true
        ans_ne_c = i < ans_ne_a.length ? ans_ne_a[i] : ""
        out_ne_c = i < out_ne_a.length ? out_ne_a[i] : ""
        break if ans_ne_c == "" && out_ne_c == ""
        if ans_ne_c != "O" && out_ne_c != "O"
          if ans_ne_c == out_ne_c
            tp += 1
          else
            fn += 1 if ans_ne_c != "O" && ans_ne_c != "" && ans_ne_c.index("OPTIONAL") != nil
            fp += 1 if out_ne_c != "O" && out_ne_c != ""
          end
        end
        i += 1
      end
    else
      fn += num_ne(ans_ne_a)
    end
  elsif !no_ne(out_ne_a)
    fp += num_ne(out_ne_a)
    print_terms(ans_str_a, ans_ne_a, out_str_a, out_ne_a)
  end
  return tp, fp, fn
end

ans_file = File.open(ARGV[0])
out_file = File.open(ARGV[1])

tp = fp = fn = 0

while true
  ans_str_a = []
  ans_ne_a = []
  out_str_a = []
  out_ne_a = []
  ans_term, ans_ne = next_term_ne(ans_file)
  ans_str_a << ans_term
  ans_ne_a << ans_ne
  out_term, out_ne = next_term_ne(out_file)
  out_str_a << out_term
  out_ne_a << out_ne

  break if ans_term == nil || out_term == nil

  while out_str_a.join("").length != ans_str_a.join("").length
    if out_str_a.join("").length > ans_str_a.join("").length
      ans_term, ans_ne = next_term_ne(ans_file)
      ans_str_a << ans_term
      ans_ne_a << ans_ne
    elsif out_str_a.join("").length < ans_str_a.join("").length
      out_term, out_ne = next_term_ne(out_file)
      out_str_a << out_term
      out_ne_a << out_ne
    end
  end
  tp, fp, fn = check(ans_str_a, ans_ne_a, out_str_a, out_ne_a, tp, fp, fn)
end

ans_file.close
out_file.close

printf("tp = %d, fp = %d, fn = %d¥n", tp, fp, fn)

precision = tp.to_f / (tp.to_f + fp.to_f)
recall = tp.to_f / (tp.to_f + fn.to_f)
f = 2.0 * precision * recall / (precision + recall)

printf("precision = %f, recall = %f, F = %f¥n", precision, recall, f)


オペレーションメモ

# KNBコーパスをNEタグ付きCaboCha形式に変更。
$ ruby knbc2cabocha_ne.rb ../KNBC_v1.0_090925/corpus1

# NEタグ付きCaboCha形式のデータをマージしながら同程度の5つに分割。
$ ruby rearrange.rb 4 837 Keitai.dat Kyoto.dat Gourmet.dat Sports.dat

# モデルのソースを生成。
$  cat G0402.dat G0403.dat G0404.dat G0405.dat > model.source.ne.1
$  cat G0401.dat G0403.dat G0404.dat G0405.dat > model.source.ne.2
$  cat G0401.dat G0402.dat G0404.dat G0405.dat > model.source.ne.3
$  cat G0401.dat G0402.dat G0403.dat G0405.dat > model.source.ne.4
$  cat G0401.dat G0402.dat G0403.dat G0404.dat > model.source.ne.5

# NEを学習。
$ /usr/local/libexec/cabocha/cabocha-learn -e ne -P JUMAN model.source.ne.1 ne.juman.1
$ /usr/local/libexec/cabocha/cabocha-learn -e ne -P JUMAN model.source.ne.2 ne.juman.2
$ /usr/local/libexec/cabocha/cabocha-learn -e ne -P JUMAN model.source.ne.3 ne.juman.3
$ /usr/local/libexec/cabocha/cabocha-learn -e ne -P JUMAN model.source.ne.4 ne.juman.4
$ /usr/local/libexec/cabocha/cabocha-learn -e ne -P JUMAN model.source.ne.5 ne.juman.5

# CaboCha形式のデータから原文テキストを逆再生。
$ ruby make_source.rb 0401
$ ruby make_source.rb 0402
$ ruby make_source.rb 0403
$ ruby make_source.rb 0404
$ ruby make_source.rb 0405

# cabocha -e ne で固有表現抽出。
$ cat S0401-01.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.1 -O1 -f1 -n1 > O0401.txt
$ cat S0401-02.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.1 -O1 -f1 -n1 >> O0401.txt
$ cat S0401-03.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.1 -O1 -f1 -n1 >> O0401.txt
$ cat S0402-01.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.2 -O1 -f1 -n1 > O0402.txt
$ cat S0402-02.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.2 -O1 -f1 -n1 >> O0402.txt
$ cat S0402-03.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.2 -O1 -f1 -n1 >> O0402.txt
$ cat S0403-01.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.3 -O1 -f1 -n1 > O0403.txt
$ cat S0403-02.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.3 -O1 -f1 -n1 >> O0403.txt
$ cat S0403-03.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.3 -O1 -f1 -n1 >> O0403.txt
$ cat S0404-01.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.4 -O1 -f1 -n1 > O0404.txt
$ cat S0404-02.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.4 -O1 -f1 -n1 >> O0404.txt
$ cat S0404-03.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.4 -O1 -f1 -n1 >> O0404.txt
$ cat S0405-01.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.5 -O1 -f1 -n1 > O0405.txt
$ cat S0405-02.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.5 -O1 -f1 -n1 >> O0405.txt
$ cat S0405-03.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.5 -O1 -f1 -n1 >> O0405.txt

# 正解データと比較。
$ ruby eval_ne.rb G0401.dat O0401.txt
$ ruby eval_ne.rb G0402.dat O0402.txt
$ ruby eval_ne.rb G0403.dat O0403.txt
$ ruby eval_ne.rb G0404.dat O0404.txt
$ ruby eval_ne.rb G0405.dat O0405.txt


| 関口宏司 | NLP | 15:50 | comments(0) | trackbacks(0) |









http://lucene.jugem.jp/trackback/471
+ Solrによるブログ内検索
+ PROFILE
  12345
6789101112
13141516171819
20212223242526
2728293031  
<< August 2017 >>
+ LINKS
検索エンジン製品 - 比較のポイント
商用検索エンジンを購入した企業担当者は読まないでください。ショックを受けますから・・・
>>製品比較 10のポイント
+ Lucene&Solrデモ
+ ThinkIT記事
+ RECOMMEND
Apache Solr入門 ―オープンソース全文検索エンジン
Apache Solr入門 ―オープンソース全文検索エンジン (JUGEMレビュー »)
関口 宏司,三部 靖夫,武田 光平,中野 猛,大谷 純
+ RECOMMEND
Lucene in Action
Lucene in Action (JUGEMレビュー »)
Erik Hatcher,Otis Gospodnetic,Mike McCandless
FastVectorHighlighterについて解説記事を寄稿しました。
+ RECOMMEND
+ SELECTED ENTRIES
+ RECENT COMMENTS
+ RECENT TRACKBACK
+ CATEGORIES
+ ARCHIVES
+ MOBILE
qrcode
+ SPONSORED LINKS