Help us understand the problem. What is going on with this article?

Play Framework と Stanfordの固有表現抽出器を用いた NER API サーバー

More than 3 years have passed since last update.

概要

Play FrameworkStanfordの固有表現抽出器を使って、固有表現抽出してくれる JSON の API サーバーを作りました。

コードは以下に公開しています。
https://github.com/sosuke-k/ner-play-server

動機

Python で自然言語処理をしていて、英語の固有表現抽出をする際にStanfordの固有表現抽出器を使う機会がありました。

nltkのラッパー があるのですが、文毎に java が実行されていて、おそらくその度に crf のモデルがロードがされているっぽくて、とても遅かったです。
実際に、ウィキペディアの長めの記事のものに 10分 近くかかっていました。

そこで、 java のウェブサーバーで固有表現抽出し、結果を返すことにしました。

バージョン

You’re using Play 2.5.4

ルーティング

/nerGET で モデルを初期化します。( classifier をパラメータで受け取りモデルを変更できます。)
/nerPOST で固有表現抽出した結果を返してくれます。

routes
GET     /ner                        controllers.NERController.index(classifier : String ?= "english.all.3class.distsim.crf.ser.gz")
POST    /ner                        controllers.NERController.tag

実装

依存ライブラリ

build.sbtlibraryDependencies で指定しています。

http://mvnrepository.com/artifact/edu.stanford.nlp

build.sbt
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/
routes
GET     /assets/*file               controllers.Assets.versioned(path="/public", file: Asset)
NERController.java
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で受け取る

ここ調べ不足なんですが、強引に文字列に直した後、 ObjectMappertext の値を取得しています。

NERController.java
  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)));
  }
Doc.java
public class Doc {
  public String text;
}

固有表現抽出

アンサーラベルを取得するのに、適当なメソッドが見つからず、ソースコードを見て強引に取得しています。
CoreLabel (Stanford JavaNLP API)

NERController.java
  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.zipNERDemo.java をコピペしました。

JSONを返す

NERController.java
...
  private Tokens tag_sent(String text) {
    tokens tokens = new Tokens();
    ...
    return tokens;
  }
  ...
  public Result tag() {
    ...
    return ok(Json.toJson(this.tag_sent(doc.text)));
  }
...
Tokens.java
public class Tokens {
  public List<Token> tokens;

  public Tokens() {
    this.tokens = new ArrayList<Token>();
  }

  public void add(Token token) {
    this.tokens.add(token);
  }
}
Token.java
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
...
sosuke
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away