読者です 読者をやめる 読者になる 読者になる

データの持ち方を工夫→SVMを使って専門用語に分類

自然言語処理 DBCLS

文字列にある文字が登場したら1、そうでなかったら0というような特徴ベクトルを束ねた行列を作ろうとしていたんだけど、巨大になりすぎた(扱うテキストの量は大分少なくしたんだけど)。つまり、こんな行列がでかくなりすぎたということだ。

グリコーゲン 分枝 酵素 肝内 合成
グリコーゲン分枝酵素 1 1 1 0 0
肝内グリコーゲン 1 0 0 1 0
グリコーゲン合成酵素 1 0 1 0 1

完全に疎行列です、本当にありがとうございました。ということで行列の形を変更する(RubyのHashっぽく表現)。

  • グリコーゲン => 1
  • 分枝 => 2
  • 酵素 => 3
  • 肝内 => 4
  • 合成 => 5

という感じでidを付けてあげると

  • グリコーゲン分枝酵素 => [1,2,3]
  • 肝内グリコーゲン => [4,1]
  • グリコーゲン合成酵素 => [1,5,3]

という具合に持っておく情報を大幅に削減できる。まあ、グラフとかではよくある表現ですが。

Rでどうやって扱おう?

上に書いた最初の行列での持ち方をA、次のやり方をBとする。このとき、Rの分類を行えるglm(一般化線形モデル(generalized liner model)の略。ロジステック回帰などを扱える)などはAのタイプだ。が、umlsのデータと青空文庫のデータを教師データとすると、Aのやり方では教師データを吐くのに一日かかってしまいそうな勢いである(どっかやり方が悪いかもしれない)。吐いたとしてもRに載るかが結構あやしいところな気もするし。

むー、Bのやり方で分類問題を扱えるRの関数とか知らないぞー(知ってる人がいたら教えてください!!)ということで、Rを使わないでどうにかする方法を模索することにする。Bの方式で扱えそうなのって、そういえばSVM-Lightとかがあったなーということを思い出した。ということでSVMが中で何をやっているかよく知らないけど、とりあえずSVMでやってみることにする。

SVM-Lightはmakeだけすれば使える感じ。これは簡単なんだが、それ用のデータを整備するのが結構面倒であった。というわけで、umlsを正例、青空文庫を負例、pneを学習したいデータとして吐き出す用のプログラムを書いた。

#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <map>
#include <vector>
#include <algorithm>
#include <math.h>
#include <mecab.h>
#include <boost/algorithm/string.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/filesystem/fstream.hpp>
#include "wrapper.cpp"

using namespace std;
using namespace boost;
using namespace boost::filesystem;

string itos(int i) {
  // convert int into string
  ostringstream s;
  s << i;
  return s.str();
}

vector<string> str2vec(string& str) {
  vector<string> result;
  MeCab::Tagger *tagger = MeCab::createTagger("");
  const MeCab::Node *node = tagger->parseToNode( str.c_str() );
  for( node=node->next; node->next; node=node->next ){ 
	// 形態素解析されて出てきた文字列(品詞の情報ではなく)
	char *s = new char [node->length + 1];
	strncpy(s, node->surface, node->length);
	s[node->length] = '\0';
	result.push_back(s);
  } 	
  delete tagger;
  return result;
}

void add_substr_comb(vector<string>& strvec, vector<vector<string> >& result) {
  unsigned int c = 1; // いくつの大きさで見ていくか	
  //  cout << c << " : " << strvec.size() << endl;
  while (c <= strvec.size()) {
	for (size_t i = 0; i < strvec.size() - c + 1; ++i) { 
	  vector<string> tmp;
	  vector<string>::iterator it = strvec.begin();
	  advance(it, i);
	  vector<string>::iterator start = it;
	  advance(it, c);
	  vector<string>::iterator end = it;
	  tmp.assign(start, end);
	  result.push_back(tmp);
	}
	c++;
  }
}

struct map_for_feature_vector {
  map<string, int> str2id; 
  // &#12098;字列から素性番号への連想配列 
  vector<string> id2str; 
  // 素性番号から&#12098;字列への逆引き 
  int getID(const string& str){ 
	map<string, int>::const_iterator it = str2id.find(str); 
	if (it != str2id.end()){ 
	  return it->second;   // 登録済みのIDを返す 
	} else {               // 登録されてない 
	  const int newID= str2id.size() + 1; 
	  str2id[str] = newID; // 登録 
	  id2str.push_back(str); 
	  return newID;        // 新しいiDを返す 
	} 
  }
  int size () {
	return str2id.size();
  }
};

int main (int argc, char **argv) {
  map_for_feature_vector m;
  vector<vector<string> > positive_example;
  vector<vector<string> > negative_example;

  // 教師データの作成(正例)
  std::ifstream fis("/Users/syou6162/dbcls/umls/umls2ja.tab");
  string str;
  while (getline(fis, str)) {
	vector<string> strvec;
	boost::algorithm::split(strvec, str, boost::algorithm::is_any_of("\t"));
	vector<string> tmp = str2vec(strvec[2]);
	positive_example.push_back(tmp);
	for (vector<string>::iterator it = tmp.begin(); it != tmp.end(); ++it) {
	  m.getID(*it);
	}
  }

  // 教師データの作成(負例)
  string dir = "/Users/syou6162/dbcls/aozora/";
  path fullPath = complete(path(dir, native));
  directory_iterator end;
  int i = 0;
  for (directory_iterator it(fullPath); it !=end; ++it){
	std::ifstream fis((dir + it->leaf()).c_str());
	string str;
	char c;
	while (fis.get(c)) {
	  str.push_back(c);
	}
	MeCab::Tagger *tagger = MeCab::createTagger("-O wakati");
	MeCabWrapper mw(tagger);
	mw.addText(str);
	delete tagger;
	for (vector<vector<string> >::iterator vecitr = mw.extraction.begin(); vecitr != mw.extraction.end(); ++vecitr) {
	  negative_example.push_back(*vecitr);
	  for (vector<string>::iterator stritr = (*vecitr).begin(); stritr != (*vecitr).end(); ++stritr) {
		m.getID(*stritr);
	  }
	}
	i++;
	if (i > 10) {
	  break;
	}
  }

  // データの表示(正例)
//   for (vector<vector<string> >::iterator vecitr = positive_example.begin(); vecitr != positive_example.end(); ++vecitr) {
// 	cout << 1 << " ";
// 	vector<int> feature_vector;
// 	for (vector<string>::iterator strit = (*vecitr).begin(); strit != (*vecitr).end(); ++strit) {
// 	  feature_vector.push_back(m.getID(*strit));
// 	}
// 	sort(feature_vector.begin(), feature_vector.end());
// 	feature_vector.erase(unique(feature_vector.begin(), feature_vector.end()), feature_vector.end());
// 	for (vector<int>::iterator strit = feature_vector.begin(); strit != feature_vector.end(); ++strit) {
// 	  cout << *strit << ":" << 1 << " ";
// 	}
// 	cout << endl;
//   }

  // データの表示(負例)
//   for (vector<vector<string> >::iterator vecitr = negative_example.begin(); vecitr != negative_example.end(); ++vecitr) {
// 	cout << -1 << " ";
// 	vector<int> feature_vector;
// 	for (vector<string>::iterator strit = (*vecitr).begin(); strit != (*vecitr).end(); ++strit) {
// 	  feature_vector.push_back(m.getID(*strit));
// 	}
// 	sort(feature_vector.begin(), feature_vector.end());
// 	feature_vector.erase(unique(feature_vector.begin(), feature_vector.end()), feature_vector.end());
// 	for (vector<int>::iterator strit = feature_vector.begin(); strit != feature_vector.end(); ++strit) {
// 	  cout << *strit << ":" << 1 << " ";
// 	}
// 	cout << endl;
//   }

  // 学習したいデータ
  vector<vector<string> > test_data;  

  for (int year = 1985; year < 1986; ++year) {
	string dir = "/Users/syou6162/dbcls/pne/" + itos(year);
	path fullPath = complete(path(dir, native));
	directory_iterator end;
	for (directory_iterator it(fullPath); it !=end; ++it){
	  std::ifstream fis((dir + "/" + it->leaf()).c_str());
	  string str;
	  char c;
	  while (fis.get(c)) {
		str.push_back(c);
	  }
	  MeCab::Tagger *tagger = MeCab::createTagger("-O wakati");
	  MeCabWrapper mw(tagger);
	  mw.addText(str);
	  delete tagger;
	  for (vector<vector<string> >::iterator vecitr = mw.extraction.begin(); vecitr != mw.extraction.end(); ++vecitr) {
		add_substr_comb(*vecitr, test_data);
	  }
	}
  }

  for (vector<vector<string> >::iterator vecitr = test_data.begin(); vecitr != test_data.end(); ++vecitr) {  
	string s;
	s = accumulate((*vecitr).begin(), (*vecitr).end(), s);
	cout << s << endl;
}


  // データの表示(テストデータ)
//   for (vector<vector<string> >::iterator vecitr = test_data.begin(); vecitr != test_data.end(); ++vecitr) {
// 	cout << 0 << " ";
// 	vector<int> feature_vector;
// 	for (vector<string>::iterator strit = (*vecitr).begin(); strit != (*vecitr).end(); ++strit) {
// 	  feature_vector.push_back(m.getID(*strit));
// 	}
// 	sort(feature_vector.begin(), feature_vector.end());
// 	feature_vector.erase(unique(feature_vector.begin(), feature_vector.end()), feature_vector.end());
// 	for (vector<int>::iterator strit = feature_vector.begin(); strit != feature_vector.end(); ++strit) {
// 	  cout << *strit << ":" << 1 << " ";
// 	}
// 	cout << endl;
//   }
  return 0;
}

いくつかはまったところがあって

  • 特徴のidは0以上にしろよ!!
  • 特徴はソートしてから書いておけよ!!
  • 学習したいやつのラベルは0にしておけよ!!

などなどというところにつまづきつつ、とりあえずできた。で、SVM-Lightを走らせる。

/Users/syou6162/dbcls/src/cpp% ../../../Downloads/svm_light/svm_learn hoge.txt model              
/Users/syou6162/dbcls/src/cpp% ../../../Downloads/svm_light/svm_classify test.txt model prediction

SVMが内部でどういうことをやっているとか、パラメータの推定に何使っているとかは今勉強している暇ないので、とりあえず全部ディフォルト。

で、predictionっていうファイルに予測結果が出てくる。こんなの。このファイルをCとしよう。

/Users/syou6162/dbcls/src/cpp% head prediction 
0.96817413
1.7556166
1.7873854
0.93640533
0.93640533
1.0745405
1.8900646
0.93640533
-0.17596597
0.93640533

0以上だったら、(ここでは)専門用語と判定されたもの、そうでなかったら普通の単語として判定されたものである。で、元の単語が何だったかがこれだと分からないので、吐き出すスクリプトも用意(さっきの上のやつに組み込んでいたんだった)。

/Users/syou6162/dbcls/src/cpp% head name.txt 
貯蔵
蛋白
貯蔵蛋白
セプター
エクダイソン
活性
化
センチニクバエ
三
令

吐き出された名詞が入っているファイルをDとしよう。で、CとDのファイルをマージすればいいんだけど、ワンライナーでできそうなやり方が分からん。まあ、とりあえずRubyで書くかーということで書いた。どうせだから、スコア(というか距離?)順にソートするようにしておいた。zip関数とかid:Hash書いてたなーと思って初めて使ってみた。

require "pp"

name = []

f = File.open("../cpp/name.txt", "r")
f.each{|line|
  name.push line.chomp
}
f.close

prediction = []

f = File.open("../cpp/prediction", "r")
f.each{|line|
  prediction.push line.chomp.to_f
}
f.close

name.zip(prediction).sort{|a, b|
  (b[1] <=> a[1]) * 2 + (a[0] <=> b[0])
}.each{|x|
  puts "#{x[0]} : #{x[1]}"
}

で、スコアの高いほう(専門用語であろうと判定されたもの)を見ていくとこんな感じになっている。お、わりと専門用語っぽい単語が抽出されているし、専門用語じゃないやつははじけてるっぽい!!

性蛋白質(AdcodedDNAbindingprotein;AdDBP : 7.4176569
-5'-リン酸依存性酵素 : 7.4066807
ピリドキサル-5'-リン酸依存性酵素 : 7.4066807
DNA,蛋白質合成,アミノ酸透過,コレステロール合成,グルコース6- : 7.4048364
メルカプツール酸(N-アセチルシステイン抱合体) : 7.3970092
酸(N-アセチルシステイン抱合体) : 7.3970092
固定化コレステロール酸化酵素(COD)膜 : 7.3960023
酢酸緩衝液(pH6.0)(5)Somogyi-Nelson試薬 : 7.393121

逆にスコアの低いほう(専門用語ではなかろうと判定されたもの)を見てみる。今度は専門用語っぽいものは入ってなくて、一般の名詞が多いということが分かる。

1例以来,19854 : -5.6792571
1例以来,1985 : -5.6807515
以来,1985430 : -5.6824445
例以来,19854 : -5.7144712
以来,19854 : -5.7147269
例以来,1985 : -5.7159655
,1985430 : -6.2655624
1985726 : -6.3123316
1985430 : -6.3741314
1例以来,1985430 : -7.5829741
1例以来,19854 : -7.6152565
以来,1985430 : -7.6179166
例以来,1985430 : -7.6181882
例以来,19854 : -7.6504705
1例以来,1985430 : -9.5184462
例以来,1985430 : -9.5536603

現在使っている特徴はものすごく単純というか、SVMできる最低限のものという感じだからもちろんうまくいっていない部分もある。例えばこういうものだ。

性口内炎ウイルス感染BHK細胞 : 10.727399
活性(DNA,蛋白質合成,アミノ酸透過,コレステロール合成,グルコース6-リン酸 : 10.515023
(DNA,蛋白質合成,アミノ酸透過,コレステロール合成,グルコース6-リン酸 : 10.376888
,(1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性 : 9.84303
,(1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素 : 9.84303
,(1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素- : 9.84303
,(1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素-電極 : 9.84303
(1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性 : 9.734461
(1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素 : 9.734461
(1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素- : 9.734461
(1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素-電極 : 9.734461
1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性 : 9.734461
1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素 : 9.734461
1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素- : 9.734461
1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素-電極 : 9.734461
)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性 : 9.699247
)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素 : 9.699247
)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素- : 9.699247
)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素-電極 : 9.699247

分類したいデータのほうでは、「,(1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素-電極」みたいな一番長い名詞から、MeCabで区切らせたのを要素として、全部の部分文字列の組み合わせのようなものを考えている(この辺を参考)。この中でよいの(「固定化酵素カラム-電極」とか)を選んでくれればいいんだけど、その付近ができていなくて全部のスコアが同じくらい、ということになっている(ということで、スコアの高いものはこういう同じような単語が出現しまくっている状況)。前後の文脈とかまだ入れてないのもあるし、その付近かなあ。複合名詞にある単語が出現したかどうかの0、1だけではまあこんなもんか。

というわけで今週は単語自身の素性だけじゃなくって文脈の素性を使ってどうにかできるようにしようと思います。