概要
Play Framework と Stanfordの固有表現抽出器を使って、固有表現抽出してくれる JSON の API サーバーを作りました。
コードは以下に公開しています。
https://github.com/sosuke-k/ner-play-server
動機
Python で自然言語処理をしていて、英語の固有表現抽出をする際にStanfordの固有表現抽出器を使う機会がありました。
nltkのラッパー があるのですが、文毎に java が実行されていて、おそらくその度に crf のモデルがロードがされているっぽくて、とても遅かったです。
実際に、ウィキペディアの長めの記事のものに 10分 近くかかっていました。
そこで、 java のウェブサーバーで固有表現抽出し、結果を返すことにしました。
バージョン
ルーティング
/ner
に GET
で モデルを初期化します。( classifier
をパラメータで受け取りモデルを変更できます。)
/ner
に POST
で固有表現抽出した結果を返してくれます。
GET /ner controllers.NERController.index(classifier : String ?= "english.all.3class.distsim.crf.ser.gz")
POST /ner controllers.NERController.tag
実装
依存ライブラリ
build.sbt
の libraryDependencies
で指定しています。
libraryDependencies ++= Seq(
...
"edu.stanford.nlp" % "stanford-corenlp" % "3.6.0",
"edu.stanford.nlp" % "stanford-parser" % "3.6.0"
)
モデルのロード (固有表現抽出器の初期化)
public
フォルダ以下に置くことで、URL指定で読み込んでいます。
gzip
されたものを読み込むのに、単純な InputStream
でエラーが出てしまって、つまづきました。
$ wget http://nlp.stanford.edu/software/stanford-ner-2015-12-09.zip
$ unzip stanford-ner-2015-12-09.zip
$ cp -r ./stanford-ner-2015-12-09/classifiers/ ./public/classifiers/
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
public class NERController extends Controller {
...
private boolean initialize(String classifier) {
try {
URL url = new URL("http://" + this.request().host() + "/assets/classifiers/" + classifier);
HttpURLConnection connect = (HttpURLConnection) url.openConnection();
connect.setRequestMethod("GET");
connect.setRequestProperty("Accept-Encoding", "gzip");
GZIPInputStream gzin = new GZIPInputStream(connect.getInputStream());
this.classifier = CRFClassifier.getClassifier(gzin);
ポストのデータをJSONで受け取る
ここ調べ不足なんですが、強引に文字列に直した後、 ObjectMapper
で text
の値を取得しています。
public Result tag() {
...
JsonNode json = request().body().asJson();
// String text = json.findPath("text").getTextValue();
String jsonstring = Json.stringify(json);
ObjectMapper mapper = new ObjectMapper();
Doc doc = null;
try {
doc = mapper.readValue(jsonstring, Doc.class);
} catch ( IOException e) {
System.out.println( "IOException: could not mapper.readValue" );
}
return ok(Json.toJson(this.tag_sent(doc.text)));
}
public class Doc {
public String text;
}
固有表現抽出
アンサーラベルを取得するのに、適当なメソッドが見つからず、ソースコードを見て強引に取得しています。
CoreLabel (Stanford JavaNLP API)
private Tokens tag_sent(String text) {
Tokens tokens = new Tokens();
for (List<CoreLabel> lcl : this.classifier.classify(text)) {
for (CoreLabel cl : lcl) {
tokens.add(new Token(cl.originalText(), cl.get(AnswerAnnotation.class)));
}
}
return tokens;
}
classify
するコード自体は stanford-ner-2015-12-09.zip
の NERDemo.java
をコピペしました。
JSONを返す
...
private Tokens tag_sent(String text) {
tokens tokens = new Tokens();
...
return tokens;
}
...
public Result tag() {
...
return ok(Json.toJson(this.tag_sent(doc.text)));
}
...
public class Tokens {
public List<Token> tokens;
public Tokens() {
this.tokens = new ArrayList<Token>();
}
public void add(Token token) {
this.tokens.add(token);
}
}
public class Token {
public String text;
public String label;
public Token(String text, String label) {
this.text = text;
this.label = label;
}
}
こんな感じ
初期化して(ここはすこじ時間がかかります)
$ curl http://localhost:9000/ner | jq
{
"status": "available",
"current": "english.all.3class.distsim.crf.ser.gz",
"message": ""
}
文をポスト
$ curl --header "Content-type: application/json" --request POST --data '{"text":"I live in Japan."}' http://localhost:9000/ner | jq
{
"tokens": [
{
"text": "I",
"label": "O"
},
{
"text": "live",
"label": "O"
},
{
"text": "in",
"label": "O"
},
{
"text": "Japan",
"label": "LOCATION"
},
{
"text": ".",
"label": "O"
}
]
}
jq
コマンドは linux系とかだと使いかた違うかもしれないです。
結果
python からこのサーバーを叩くようにした結果、 10分 近くかかっていた記事の固有表現抽出が、 5秒 で終わりました。
元の python のコードが良くなかったのかな?と思っていますが、とりあえず、よかったです。
※おまけ
テストなどしてないですが、ビルドしたものをあげておきました。。汗
https://github.com/sosuke-k/ner-play-server/releases
$ wget https://github.com/sosuke-k/ner-play-server/releases/download/v_1.0.0/ner-play-server-1.0-SNAPSHOT.zip
$ unzip ner-play-server-1.0-SNAPSHOT.zip
$ chmod +x ner-play-server-1.0-SNAPSHOT/bin/ner-play-server
$ ner-play-server-1.0-SNAPSHOT/bin/ner-play-server
...