トピック解析用にデータのパースとトピック解析までをクラス化

 前回の構想を実装するのに、「トピック解析用にデータをパース・マージする」部分と「トピック解析を実行して解析結果をまとめる」という行程が、階層ごとに再帰的に何度も使い回されると思われるので、今回、これらのメソッドをまとめたクラスを作成しました。

 このクラスがあれば、将来的にもこれを公開すれば、対応するデータを食わせれば手頃なトピック分類された結果が得られるプログラムとして使用していただけると思われるので、なるべく汎用性を持たせて実装していきます。
 まとめクラスとして、「AuthorTopicExplorer」を作成し、クラス変数として「トピック解析プログラムが出力するファイルを保存するフォルダ」、「探索トピック数の上限・下限」、「トピック解析プログラムへのパス」を持たせています。
 また、提供するメソッドは、「筆者ごとにまとめられた単語リストファイルを、全部読込んでマージしてトピック解析プログラムに渡せる形式にする」、「下限・上限トピック数でトピック解析プログラムを起動する」、「得られた各トピックごとの分類確率から、有効と判断できるトピック数を返却する」ものを考えています。

 現在3つ目のメソッドまで実装できたので、次回は残りの実装と、再帰的にトピック解析を続けるスクリプトや、Gvizを利用したグラフ出力スクリプトまで完成させていこうと思います。

階層トピック分類の構想

 前回までのポスター関連解析で扱った全ポスター数は56だったので、1枚のネットワーク図に収まりましたが、今度はもっと多くのポスターが発表される場合のポスター関連図をどのように作るか、前回までのネットワーク図の作り方をふまえて考え直してみました。

 全ポスター数が約4000にもなる場合、1つのネットワーク図にまとめても、見れないことはないですが、特定のポスターを探したり、自分の近くに配置されたポスターとの関連性の意味が希薄になります。
 また、多くのポスターを分類するためのトピック数が膨大になれば、トピックの内容を手軽に理解することができなくなり、トピック間の関連性も考えづらくなります。
 なので、あまり1枚のネットワーク図に含まれるノード数が大きくなりすぎないよう、またトピック数が多くなりすぎないよう、トピック解析を行った後のトピックに分類されたポスター集合に対し、さらにトピック解析を進めるというように、階層的なトピック解析を行うことを考えました。
 まず約4000枚のポスターから6つほどのトピックに分け、その後トピックごとのポスター集合に対し、さらに6つほどのトピックに分けていき、最終的に1トピックに分類されるポスター数が50〜60くらいになったら、そこでネットワーク図を作るようにしようと考えております。
 トピックの階層ごとに図を用意し、トピックを表すノードをクリックすれば、その階層を進み内部の図が見えるようになるような仕組みを作ろうと思います。

 そもそも、トピック内のトピック解析が上手く行くのかもまだ分からないので、次回それを行ってどのようにポスターが分類されるのか確かめてみようと思います。
 これが上手く行けば、あるポスターAがどのようなトピックに分類されるのか、そのトピックに他にどのような研究が含まれているのか、近いトピックは何か、そのトピックではどのような研究が行われているのか、などを知ることができるようになると思います。

トピック解析に用いる単語の選別

 全ポスターからMeCabを用いて取得した名詞句を使ってトピック解析を行ってきましたが、トピックに特徴的だと判定された単語リストを見ると、全体的なトピックに関連しそうな単語だったり、「目的」とか「本研究」といった一般的すぎてあまり意味の無い単語が上位にきてしまっていて、トピックの内容がよく分からない状態でした。

 なので今回、解析にかける前の単語リストから、上記のような単語を選択して除くことにしました。除く基準はほとんど恣意的ですが、全ポスターの中で出現頻度が高い単語の中で、単語を読んでみてポスターの内容とは繋がらなさそうな単語を抜いていきました。
 この「ストップワードリスト」を作成し、トピック解析にかけるフォーマットに変えるときに、ファイルを読込んで単語がマッチするなら除く、というスクリプトで単語を外していきました。
 ストップワードリストは、以下のような単語が含まれています。単語の右には全ポスターでの出現頻度が記述してあります。

データ 232
データベース 75
情報 72
DB 26
論文 19
NBDC 17
効率 13
対象 11
研究者 10
分野 9
指針 8
国内 7
他 7
成果 7
リソース 7
目的 7
DBCLS 7
プロジェクト 7
本発表 7
複数 7
取り組み 7

 このようにしてから、もう一度トピック解析を行い、前回と同様にトピックへの近さを基準にしたネットワーク図を作成しました。
 単語を選別した影響で、トピック数を5にするとトピック4と5の内容がかぶるような感じになったので、最終的にトピック数は4にしました。そして、エッジのせいでグラフが見づらくなるのをさけるため、エッジの色に透明色の設定も追加してみやすくしました。
 GraphvizではColorの設定でRGBAが設定できるので、アルファ値も追加してエッジ色を指定しました。

@gv.edges arrowhead: 'none', color: "#00000030"

 これで作成したネットワーク図で、みたところ色のグラデーションもよく分かれていていい感じになったので、これを完成版としました。作成した図は「トーゴーの日シンポジウム2013/ポスター発表」のページで公開されています。

グラフの修正

 前回作成したグラフを修正し、全てのポスターについて、各トピックへのエッジを分類確率に応じた長さでひいて、ポスターがグラフ全体でどのように配置されるのかを確認してみました。

 各トピックを示すノードに向かってエッジをひくのですが、全てのトピックへのエッジを描画するとグラフが汚くなるので、分類確率の上位2つまでのトピックに線を引き、他は線を作成しても見えないように設定しました。
 このためには、Graphvizの「style」の項目を「invis」に設定すれば良いようでした。Gvizでは、

@gv.edge :"p#{host_poster}_t#{topic_id}", len:(1.0-theta), style:"invis"

と記述して設定できました。
 これでグラフを作成しましたが、各ノードがトピックに対する位置関係のバランスを保ちながら配置されました。この後グラフの見せ方を調整し、トピックを表すノードを色線で囲ったり、各ノードの色を、分類確率が高ければ濃い色、低ければ薄い色になるように記述しました。

@color_list = ["purd6", "blues6", "greens6", "purples6", "oranges6"]

@gv.node :"t#{topic_id}", label:"T#{topic_id}", colorscheme:@color_list[topic_id-1], color:4, style:"bold", penwidth:5

color_number = (vector[0][1]*5).to_i
color_name = @color_list[vector[0][0]- 1]
@gv.node :"p#{host_poster}", label: "#{host_poster}:#{@poster_author[host_poster]}", colorscheme:color_name, color:color_number

といったように設定しました。
 以上まででグラフは作成できるようになったので、今後はトピック解析にかける単語やパラメータを調整して、グラフの出力がどう変わるかを確認していこうと思います。
 また、最終的なグラフ図として、各トピックに分類された単語のリストものせたファイルをkeynoteで作成していきました。後はこのグラフをどのように作成したのか、簡単なDocumentを下に配置しようかと思っています。

トピック分類確率から、各トピックを表すノードへのエッジをひいたグラフを作成

 前回トピックの分類確率ベクトルから、ポスター間の距離を決めてグラフを作成したのですが、そのグラフを見てもらったところ、「何で繋がっているのか分からない」「ムリに繋げているように思われる、内容が近いとは思えない」といった意見をいただきまして、今回はその原因を探してみました。

 とりあえず実装上の問題はありませんでしたが、各ポスターから「最低4本エッジをひく」という設定が甘かったようで、全然ちがうトピックのポスターとペアになっていたりすることが多かったことが分かりました。
 なので、とりあえずポスターがどのトピックに属しているのか分かりやすくしつつ、ポスター間にエッジをひくのをやめて、トピックを示すノード「T1〜Tn」を用意して、そこからエッジをひくようにしました。
 ポスターごとの分類確率を利用して、分類確率が大きいほどそのトピックを表すノードとのエッジの長さが短くなるように設定しました。今回もGvizで設定を進めていき、エッジの長さを「len」属性を使って設定していきました。

label = sprintf("%.2f", theta);
@gv.route :"p#{host_poster}" => :"t#{topic_id}"
@gv.edge :"p#{host_poster}_t#{topic_id}", label:label, len:(1.0-theta)

 また、今回は各ポスターを表すノードを、トピックごとに色分けする部分も実装しました。使用する属性は「colorscheme」と「color」です。

@gv.global layout: 'neato', overlap: false
@gv.edges arrowhead: 'none'
@gv.nodes style: "filled", colorscheme:"pastel19"

@gv.node :"t#{topic_id}", label:"T#{topic_id}", color:topic_id

 これでグラフを作成したところ、全ポスターが書くトピックの周辺に位置するようになって、ノードの配置もポスターの内容が近いものが集合するようになりました。
 グラフの見せ方の調整、特にノードの配置について、今回は上位2つまでの分類確率しか使っていなかったので、これをエッジはひかないが全ての分類確率を使ってノードを配置するようにしようと思います。
 また、このグラフがどのように作成された茅、意味するところをまとめて、グラフに添付できたら良いと思いますので、これも次回作成していこうと思います。

トピック分類確率からポスター間距離を算出&Gvizでグラフ作成

 LDAトピックモデルにより、各ポスターについてどのトピックに分類されると考えられるのか、その分類確率が算出されます。なので今回は、各ポスターごとにその分類確率をベクトルにまとめて、それらのユークリッド距離を算出することを考えました。
 ベクトルは設定したトピック数分の要素を持つので、ポスターのペアごとに残差二乗和を計算しました。分類確率θが格納されたファイルを読込んで、全ポスターペアの残差二乗和を計算してファイルに出力する、「ThetaCorr.rb」スクリプトを作成しました。
 
 次に、この距離が近いほどポスター間の内容が近いと考え、ポスター間の関連を表した図を作成することを考えました。
 ポスター間にエッジをひく基準として、算出距離が0.3よりも小さいものという設定をし、また各ノードから最低でも4本はエッジをひく、というようにしました。
 この基準をもとにネットワーク図を作りますが、去年もお世話になったGraphvizRubyのライブラリのGvizで実装していきました。使用方法はまとめたものがありますので、こちらからどうぞ。
 さて、今回は1枚の画像にネットワーク図をまとめるので、各ポスターごとに基準を満たした相手へのエッジをひいていくだけの実装です。「ThetaDistanceGraph.rb」にまとめました。

distance_hash.each_with_index do |(opp_poster, distance), i|
if distance > @upper_bound #距離が上限を超えていたら
if (i > (@rank -1)) and (distance > last_distance)
break #最低出力数を超えていて、同順位でもなければ
end
end
#エッジを実体化
@gv.route :"p#{host_poster}" => :"p#{opp_poster}"
last_distance = distance #距離を取っておく
end

といったようにエッジをひいていきます。
 また、今回もポスターペア間に2本エッジがひかれてしまい、今度はこれを1本だけにしようと思い、GraphvizDocumentationからオプションを探しました。すると、グラフ全体の設定として、「concentrate」をtrueにすればよさそうだったので、この設定を加えたところ、複数のエッジが1つにまとまるようになりました。

@gv = Gviz.new
@gv.global layout: 'neato', overlap: false, splines: true, concentrate: true

とすれば良いようです。
 以上でネットワーク図を出力できるようになりました。まだエッジをひく基準やネットワーク図の見た目などを調整したいので、次回はこれらをいじっていこうとおもいます。また、今回は「トピック」という全体的なくくりがあるので、これもネットワーク図に表現できたらと考えています。

トピックモデル解析とポスターの分類について

 前回上手く行かなかった、RubyからのMeCabの呼び出しですが、Rubyの外部コマンド呼び出しに使うライブラリの「Open3」を用いた方法を紹介されていた、hirokan55の日記様の記事から参考にして、上手く呼び出すことができるようになりました。

def mecab_exec(text)
result = Array.new
stdin, stdout, stderr = Open3.popen3("mecab")

stdin.puts(text)
stdin.close #標準入力を切断
#標準出力を回収
stdout.each do |line|
line_list = line.split("\t") #タブで出力を区切る
line_result = @node.new(line_list[0], line_list[1])
result.push line_result
end

return result
end

 上記のようなメソッドをクラスの中に用意して、文章を形態素解析するときに呼び出すようにしました。

 これで、各ポスターの内容を英語・日本語それぞれについてトピックモデル解析ができるようになったので、今度はトピックごとに分類されたポスター間の距離をどのように決めようか考えました。
 トピックモデル解析では、各ポスターについて全てのトピックへの分類確率が計算されます。これを利用して、同じような分類確率ベクトルを持つようなポスターを”近い”と判断させることを考えています。
 トピックT個に対して、大きさTのベクトルがポスターごとに計算されるので、それらの内積とベクトルの大きさから角度を求められるので、その角度の小さい順に並べるか、単純にベクトルの距離を計算して、近い順に並べるかにしようかと思っています。

 さて、来たる10月4日・5日にトーゴーの日が開催されるので、そこでのポスターの要旨一覧をいただきました。今回はこれから筆者名とポスタータイトルと要旨をパースし、形態素解析してトピックモデル解析を試すところまで実装しました。
 全体的な分布をみると、トピックは4つか5つにするのが良さそうでした。次回は早速ポスター間の距離を調べてネットワーク図を作ってみようと思います。