2012年1月10日火曜日

Rubyでコーパス言語学(4)

複数のファイルをまとめて処理

複数のXMLファイルをまとめて処理したい場合は、次のようにする。

files = Dir.glob('C:¥KyotoCorpus4.0¥xml¥syn¥*.xml')
files.each do |file|
  STDERR.puts file
  doc = open(file){|f| Nokogiri::XML(f){|config| config.noblanks}}
  (ここに処理を記述)
end

Dir.globはワイルドカードを含むファイル名を展開してファイル名のリスト(Array)で返す。STDERR.putsは標準エラー出力(普通はコマンドプロンプト)に文字列を出力する命令で、現在処理中のファイルの名前を画面に表示する。複数のファイルをまとめて処理する場合、処理が完了するまでに時間がかかって、ちゃんと動いているのか不安になることがあるので動作確認のために入れてある。上記のコードを関数として定義しておいて次のように使うこともできる。

def xml_enum(path)
  files = Dir.glob(path)
  files.each do |file|
    STDERR.puts file
    doc = open(file){|f| Nokogiri::XML(f){|config| config.noblanks}}
    yield doc
  end
end

path = 'C:¥KyotoCorpus4.0¥xml¥syn¥*.xml'
xml_enum(path) do |doc|
  (ここに処理を記述)
end

さらに応用してクラスとして定義する場合。

class XMLFiles
  include enumerable
  def initialize(path)
    @path = path
    @files = Dir.glob(path)
  end
  def each
    @files.each do |file|
      doc = open(file){|f| Nokogiri::XML(f){|config| config.noblanks}}
      yield doc
    end
  end
end

path = 'C:¥KyotoCorpus4.0¥xml¥syn¥*.xml'
corpora = XMLFiles.new(path)
corpora.each do |doc|
  (ここに処理を記述)
end

このクラスはeachメソッドが定義されており、enumerableモジュールをインクルードしてあるが、to_aとかすると全てのXMLファイルを一度にメモリに読み込むことになるのでエラーに注意。mapとかで必要な情報だけを拾い集める分には問題ないと思われる。さらにファイルが複数あることを意識せずにxpath検索するメソッドを追加してみる。

class XMLFiles
  def xpath(xpath)
    self.each do |doc|
      doc.xpath(xpath).each{|node| yield node}
    end
  end
end

corpora.xpath(xpath) do |node|
  (ここに処理を記述)
end

Nokogiri::XML::Documentなどに対するxpathメソッドがNodeSetを返すのに対して、上記のxpathメソッドはeachのように動作する。複数のXML文書から集めたノードを集めて、それらのリストを返すようなメソッドとして定義してしまうと、メモリエラーになる可能性があるのでこのような方法を取っている。ノードセットに似たオブジェクトを返すようにしたいのであれば、次のようにEnumeratorを使う方法が考えられる。

class XMLFiles
  def xpath(xpath)
    enum = Enumerator.new do |y|
      self.each do |doc|
        doc.xpath(xpath).each{|node| y << node}
      end
    end
    return enum
  end
end

corpora.xpath(xpath).each do |node|
  (ここに処理を記述)
end

この方法であれば、each以外のmapやcountなどのメソッドも利用することが可能になる。to_aするとメモリエラーになる可能性があるのは同じなので注意すること。

Rubyでコーパス言語学(3)

ノードや属性の一覧を取得する

XMLファイルに含まれるノードや属性の一覧を取得するには次のようにする。

data = Hash.new{|h, k| h[k] = Hash.new(0)}
doc.xpath('//*').each do |node|
  node.attributes.keys.each do |attr|
    data[node.node_name][attr] += 1
  end
end
data.each do |k1, v1|
  puts k1
  v1.each do |k2, v2|
    puts "  #{k2}: #{v2}"
  end
end

このコードでは、ノードや属性のリストを保存するためにハッシュを使用している。RubyのHashは、Hash.newの後に{|h,k| h[k] = ...}や()をつけることによって初期値を...に指定することができる。xpathの*はワイルドカードで、全てのノードを意味する。attributesはノードの属性をハッシュで返す。keysはvaluesと対をなすHashのメソッドで、キー名のリスト(Array)を返す。node_nameはノード名を返す。950101.xmlに対してこのプログラムを実行した結果は次の通りである。

sentence
  S-ID: 1134
  KNP: 1134
  MOD: 1109
  MEMO: 14
chunk
  id: 10268
  link: 10268
  rel: 10268
tok
  id: 26739
  read: 26739
  base: 26739
  pos: 26739
  ctype: 26739
  cform: 26739

属性の値の一覧を取得する

ある属性の値の一覧を取得するには、次のようにする。例えば、pos属性の場合。

puts doc.xpath('//@pos').map{|attr| attr.value}.uniq.sort

950101.xmlに対する結果は以下の通りである。

判定詞
副詞
助動詞
助詞-副助詞
助詞-接続助詞
助詞-格助詞
助詞-終助詞
(以下略)

基本形から活用形のリストを取得する

例えば、判定詞の「だ」の活用形リストを取得する場合は次のようにする。

data = Hash.new(0)
words = doc.xpath('//tok[@base="だ" and @pos="判定詞"]')
words.each do |word|
  items = [word.text, word.attr('ctype'), word.attr('cform')]
  data[items] += 1
end
puts data.map{|rec| rec.join("¥t")}.sort

結果は以下の通り。

じゃ 判定詞 ダ列タ系連用ジャ形 1
だった 判定詞 ダ列タ形 37
だろう 判定詞 ダ列基本推量形 1
で 判定詞 ダ列タ系連用テ形 53
であった 判定詞 デアル列タ形 3
であって 判定詞 デアル列タ系連用テ形 1
であり 判定詞 デアル列基本連用形 10
である 判定詞 デアル列基本形 14
であろう 判定詞 デアル列基本推量形 1
でした 判定詞 デス列タ形 4
でしょう 判定詞 デス列基本推量形 2
です 判定詞 デス列基本形 15
な 判定詞 ダ列基本連体形 7
の 判定詞 ダ列特殊連体形 10

無理やり正規表現検索

検索したい表現の語の切れ目とか分からないときに、とりあえず正規表現で検索してマッチする語の組み合わせをリストアップする方法。ここでは「とは」の例を示す。もっとうまいやり方があるかも。

re = /とは/
list = []
doc.xpath('//sentence').each do |sen|
  pos1 = []
  sen.text.scan(re){|s| pos1 << [$~.begin(0), $~.end(0)]}
  next if pos1 == []
  tokens = sen.xpath('.//tok')
  pos2 = [0]
  tokens.each{|tok| pos2 << pos2[-1] += tok.text.size}
  items = pos1.map{|b1,e1|
    b2 = pos2.find_index{|n| n > b1}
    e2 = pos2.find_index{|n| n >= e1}
    tokens[b2..e2]
  }
  list += items
end
data = Hash.new(0)
list.each do |items|
  key = items.map{|item| "#{item.text}(#{item.attr('pos').split('-').last})"}
  data[key] += 1
end
puts data.map{|rec| rec.join("¥t")}

pos1とpos2はpart_of_speechとは全く関係なく、正規表現とマッチした文字列の位置(position)を記録しておくための変数。xpathで全てのsentence要素のリストを取得し、それの文のtextに順番に正規表現検索をかけていって、マッチしたらその部分のノードのリストを取得する。正規表現のマッチはscanメソッドで行い、マッチング結果を格納する$~という特別な変数を使ってマッチした部分の開始位置と終了位置を取得する。次にxpathでそのsentenceの中のtok要素を全て取得し、それらを順番に数えていって正規表現とマッチした部分と一文字でも重複するノードがあれば拾って行く。結果は以下の通り。

あと(副詞) は(副助詞) 2
えと(普通名詞) は(副助詞) 1
こと(形式名詞) は(副助詞) 12
と(格助詞) は(副助詞) 10
もともと(副詞) は(副助詞) 1

Rubyでコーパス言語学(2)

XMLコーパスを読み込む

前回のエントリで作成したXMLコーパスに対して、いろいろな操作を試みる。まずはnokogiriを用いてXMLコーパスをロードする。

# -*- coding: utf-8 -*-
require 'nokogiri'
f = open('c:¥KyotoCorpus4.0¥xml¥syn¥950101.xml')
doc = Nokogiri::XML(f){|config| config.noblanks}
f.close

XMLファイルのロードはNokogiri::XML(f)で行う。このとき、後ろにブロックをつけて読み込み時の設定を行うことができる。noblanksは空白だけのテキスト要素を無視せよというもので、これがないとXMLファイル中のインデントとかがいちいちテキスト要素として読み込まれるのでうざい。ちなみに最後の三行は次のように書くこともできる。

file = 'c:¥KyotoCorpus4.0¥xml¥syn¥950101.xml'
doc = open(file){|f| Nokogiri::XML(f){|config| config.noblanks}}

文、文節、語の数を数える

今回使用するXMLコーパスの基本的な構造は次の通りである。

<document>文書全体
<sentence>
<chunk>文節
<tok>

特定の種類のノードのリスト(ノードセット)を取得するには、xpathメソッドを使う。xpathはxmlにおける正規表現みたいなもので、詳しくはぐぐれ。

sentences = doc.xpath('//sentence')
chunks = doc.xpath('//chunk')
tokens = doc.xpath('//tok')

ノードセットの数を取得するにはsizeメソッドを使う。

puts <<"END"
sentences: #{sentences.size}
chunks: #{chunks.size}
tokens: #{tokens.size}
END

形態素情報で検索する

特定の語のノードセットを取得したい場合はtok要素の属性を利用する。例えば判定詞の「で」だけを検索したい場合は次のようにする。

de = doc.xpath('//tok[text()="で" and @pos="判定詞"]')

xpath中のtext()はテキスト要素を取得する関数で、@posのように@で始まる名前は属性を表す。この例では、<tok pos="判定詞">で</tok>のようなパターンのtok要素のセットを取得する。


手抜きKWIC

文ID、語ID、前文脈、検索語、後文脈をタブ文字で区切ったKWICもどきを出力する。

# -*- coding: utf-8 -*-
require 'nokogiri'
file = '(中略)950101.xml'
xpath = '//tok[@pos="判定詞"]'

doc = open(file){|f| Nokogiri::XML(f){|config| config.noblanks}}
tokens = doc.xpath(xpath)
tokens.each do |tok|
  sen = tok.at_xpath('ancestor::sentence')
  senid = sen.attr('S-ID')
  tokid = tok.attr('id')
  prev = sen.xpath('.//tok[@id < %i]' % tokid)
  foll = sen.xpath('.//tok[@id > %i]' % tokid)
  puts [senid, tokid, prev.text, tok.text, foll.text].join("¥t")
end

at_xpathはノードセットではなく最初にマッチしたノードだけを返すメソッド。最初から該当する要素が一つしか無いと予想される場合によく使う。xpath中のancestorは軸と呼ばれるもので、現在のノードの祖先(親ノードや祖父ノードなど)を検索せよという指示を与える。attr('S-ID')はS-IDという名前の属性の値を返せというメソッドで、戻り値は多分文字列(String)。ちなみに類似するattributeというメソッドはNokogiri::XML::Attrオブジェクトだったかを返したはず。textはテキスト要素を文字列として返すメソッドで、ノードにもノードセットにも使える。ノードセットの場合は、各ノードのtextを連結した文字列を返す(スペースなどは挟まない)。

このプログラムは、kwic.rbとか適当な名前で保存して次のように使う。

ruby kwic.rb > result.txt

出力結果のresult.txtは、Excelとかで開くと閲覧しやすい。コマンドラインでXMLファイルや検索文字列(xpath)を指定できるようにするとか改造はお好みで。

Rubyでコーパス言語学(1)

京都大学テキストコーパスをXML形式に変換する

京都大学テキストコーパスは毎日新聞95年度版のデータに構文情報や形態素情報をタグ付けしたものであり、おおよそKNPの出力形式になっている。このコーパスをRubyなどのスクリプトで処理しやすいようにXML形式に変換する。XML形式は、CaboChaのXML形式に似せたものを用いる。

[kc2xml.rb]
# -*- coding: utf-8 -*-

# kc2xml.rb ver 0.20 [2011.8.26]

require 'nokogiri'

module KC2XML

def KC2XML.parse(file)
  xml = Nokogiri::XML::Document.new
  xml.encoding = 'UTF-8'
  xml << doc = xml.create_element('document')
  sen = chu = tag = tok = tokid = nil
  lines = open(file, 'r:eucjp-ms:utf-8'){|f| f.readlines}
  lines.each do |line|
    items = line.split(' ')
    case items[0]
    when '#'
      attr = Hash.new
      items.each do |item|
        k, v = item.split(':')
        attr[k] = v
      end
      sen = xml.create_element('sentence', attr)
      doc << sen
      tokid = 0
    when '*'
      attr = Hash.new
      attr['id'] = items[1]
      attr['link'] = items[2][0 .. -2]
      attr['rel'] = items[2][-1]
      chu = xml.create_element('chunk', attr)
      sen << chu
    when '+'
      attr = Hash.new
      attr['id'] = items[1]
      attr['link'] = items[2][0 .. -2]
      attr['rel'] = items[2][-1]
      tag = xml.create_element('tag', attr)
      tag.add_child items[3] if items[3]
      chu << tag
    when 'EOS'; # nothing to do
    else
      attr = Hash.new
      attr['id'] = tokid
      attr['read'] = items[1]
      attr['base'] = items[2]
      attr['pos'] = items[3 .. 4].delete_if{|s| s == '*'}.join('-')
      attr['ctype'] = items[5]
      attr['cform'] = items[6]
      text = items[0]
      tok = xml.create_element('tok', text, attr)
      (tag or chu) << tok
      tokid += 1
    end
  end
  return doc.to_xml
end

def KC2XML.auto_conv(dir = '.', code = 'eucjp-ms')
  Dir.mkdir('./xml') unless FileTest.directory?('./xml')
  Dir.mkdir("./xml/syn") unless FileTest.directory?("./xml/syn")
  Dir.mkdir("./xml/rel") unless FileTest.directory?("./xml/rel")
  for subdir in ['syn', 'rel']
    files = Dir.glob(File.join(dir, "dat/#{subdir}/*.KNP"))
    files.each do |file|
      STDERR.print file
      des = "./xml/#{subdir}/#{File.basename(file, '.KNP')}.xml"
      STDERR.puts ' -> ' + des
      open(des, 'w', :encoding => code){|f| f.puts parse(file)}
    end
  end
end

end

KC2XML.auto_conv *ARGV[0, 2] if $0 == __FILE__


# 修正履歴
# 2011.08.26 ver 0.20
#  - nokogiriを使って全面的に書き直し
#  - sentenceタグの属性は特に調整していない(今後はnokogiri使うつもりなので)
#  - 全角ダッシュの問題はeucjp-ms使ったら解決した。
# 2010.08.30 ver 0.10
#  - Ruby1.9で動くように全面的に書き直し
# 2008.06.02 ver 0.03
#  - MSXMLでparse Errorしないように調整
#    (sentenceタグの属性をS-ID、KNP、MOD、MEMOに限定)
#  - posが「判定詞-*」とかなってたのを「判定詞」に修正
#  - kconvをiconvに差し替え(全角ダッシュがうまく変換されてなかったので)
# 2008.02.18 ver 0.02 tokの属性がずれていたのを修正
# 2008.02.13 ver 0.01

この程度の変換処理であればnokogiriとか使わない方が多分速いのだが、コード量を少なくするためにnokogiriを使用している。このスクリプトは以下のように使用する。

ruby kc2xml.rb c:¥KyotoCorpus4.0 utf-8

第一引数は京大コーパスが置いてあるフォルダ(C:¥KyotoCorpus4.0など)、第二引数は出力するXMLファイルの文字コードである。kc2xml.rbをあらかじめKyotoCorpus4.0フォルダに置いておけば、第一引数は省略できる(デフォルトはカレントフォルダ)。第二引数を省略した場合は、eucjp-msで出力する。なお、出力するXMLファイルはカレントフォルダ以下にxmlという名前のフォルダを作成し、その中に作成される。

2011年11月18日金曜日

LaTeX for HPSG

HPSG関係で使いそうな記号とかその他。

パッケージ

avm.sty
AVM(Attribute-Value Matrix)を書くためによく使われるマクロらしい。
木構造の中にAVMを埋め込みたいが、木構造マクロと組み合わせるとうまく動かない、という場合は\setboxとか\boxとか使うとうまく行くかも知れない。例えばtreeprintと組み合わせる場合。
¥setbox0=¥hbox{
  ¥begin{avm}[¥asort{word}HEAD&¥ajKaku1¥¥VAL&itr]¥end{avm}
}
¥tree{¥box0}
  ¥leaf{fish}
¥endtree

演算子・括弧など

\preceq
\succeq
\wedge
\vee
\Rightarrow
¬\neg
\top
\bot
〈〉\langle \rangle
{}\{ \}

角囲み数字

プリアンブルで\usepackage{otf}として、本文中で\ajKaku{1}のようにする(0〜100)。

二重角括弧

二重角括弧[[]]は、\usepackage{txfonts}(またはpxfonts)して\llbracket \rrbracketか、\usepackage{textcomp}して\textlbrackdbl \textrbrackdblで書ける。

二重山括弧

二重山括弧《》は、\usepackage{MnSymbol}して\llangle \rrangleで書ける。だがどうもtxfontsやpxfontsとの相性が悪いらしい。全角の 《》をそのまま使ってもいいが、$\left《 ... \right》$みたいな使い方はできない。

2011年11月1日火曜日

nokogiriのインストール on Lion

Lionでruby1.9とnokogiriを使うメモ。

1. ruby1.9をインストールする。

以前のエントリとかよそのサイトとか参照。

2. nokogiriをインストールする。

gem install nokogiri

rubyのインストールさえちゃんとできてればnokogiriのインストールはあっさり終わると思われる。

京都大学テキストコーパス on Lion

Lionで京都大学テキストコーパス4.0をセットアップするメモ。

1. 京都大学テキストコーパスをセットアップ

以前、別の場所で書いたWindows環境でのセットアップと大体同じ方法でいけた。MacなのでCygwinのインストールは不要。perlスクリプトの修正は必要。フォルダは$HOME/KyotoCorpus4.0とか適当に。

http://www.mizimada.net/notes/kyotocorpus/install.html

Last Update: 2011.08.11
インストールには、perl、京大コーパス、毎日新聞データが必要。
Windows環境&最新のperlだとインストール用スクリプトの一部が正しく動かない場合があるので修正が必要。
  1. Cygwinをインストールする
    ダウンロードサイトの選択(choose a download site)
    ftp://ring.aist.go.jp とか適当に
    パッケージの選択(choose packages)
    perlを追加する
    インストールが完了したら、Cygwin Bash Shellを一度起動しておく(初期設定のため)
  2. 京都大学テキストコーパスの準備
    kyotocorpus4.0.tar.gzを解凍して、kyotocorpus4.0フォルダを
    c:\cygwin\home\(username)\KyotoCorpus4.0
    あたりに置く
  3. 毎日新聞データの準備
    mai95.txtを
    c:\cygwin\home\(username)\KyotoCorpus4.0\mai95.txt
    あたりに置く。
    ファイル名がmai1995.txt(最近の版?)になっている場合はmai95.txtにリネームする。
  4. インストール用スクリプトの修正(重要)
    c:\cygwin\home\(username)\KyotoCorpus4.0\src
    にある
    format.pl 
    num2KNP.pl
    の二つのファイルをテキストエディタで開いて
    use open IO => ':encoding(euc-jp)';
    の次の行に
    use open ":std"; 
    と書き加える。
  5. インストールの実行
    Cygwin Bash Shellを起動し、以下のように入力。
    $ cd KyotoCorpus4.0
    $ ./auto_conv -d .


2. XMLファイルに変換

以前作ったXML変換スクリプトで行けた。ここからkc2xml.rbを取得してKyotoCorpus4.0フォルダに置き、ターミナルでKyotoCorpus4.0フォルダに移動して以下のように実行。要ruby1.9。

ruby kc2xml.rb -s ./dat/syn -t ./xml/syn -u
ruby kc2xml.rb -s ./dat/rel -t ./xml/rel -u

ただし、あらかじめ出力先フォルダ(KyotoCorpus4.0/xml/synとKyotoCorpus4.0/xml/rel)を作っておかないとうまく動かない。無ければ勝手に作るように書いたつもりだったのだが、いろいろいじってるうちにミスったっぽい。そのうち直そう。