astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

DB3分クッキング Neo4jではじめるグラフデータベース入門

f:id:astamuse:20180123182120j:plain

こんにちは、福田 a.k.a. FDKです。

バルトークのアレグロ・バルバロを聴きながらバルクロードを実行しています。

点と点をこねくり回していると、イノヴェイションが生まれることがあります。

そう、まさに “Connecting the Dots” の瞬間です。

はじめに

グラフは、点と点を線で繋いだ構造で、この世界の至るところに見られます。 点同士を結ぶ線に向きや重みを持たせて関係性を表現することで、様々な探索ができるようになります。

例えば、SNS。人を点、友達関係を線で表し、友達関係の連鎖を辿り新たな関係を知るといったことはイメージしやすいと思います。

ボキャブラリ。わたしたちがものごとを考えるとき、言葉は欠かせません。キーワード同士にも何らかの関係があり、豊かな語彙は文脈とともに思索の飛び石となります。

ここでは、特許文書に現れるキーワードとその近接関係、そしてキーワードが現れる文脈との関係データをグラフ化して探索をしてみます。

Neo4jとは

グラフデータベースは、グラフ構造のデータの格納と検索に特化したデータベースです。商用、オープンソース、様々なプロダクトがあり、広く使われています。

Neo4jは、Neo Technology社により開発されたデータベースで、2010年にバージョン1.0がリリースされました。2018年1月23日時点での最新の安定バージョンは3.3.2です。

特徴として以下があげられます。

  • Cypherと呼ばれる宣言型グラフ向けクエリ言語
  • ACIDトランザクションのサポート
  • プロパティグラフ(点と線それぞれにkey-valueの形式で複数の属性を持たせられる)
  • Javaで実装されている
  • Boltというバイナリプロトコル(3.0以降)
  • 豊富なドライバ(.NET、Java、JavaScript、Python、Ruby、 PHP、R、Go、Erlang、C/C++、Clojure、Perl, Haskellなど)
  • 内部的にLuceneを使用
  • コミュニティ版とエンタープライズ版
  • GPLv3 及び AGPLv3 / 商用

*1

レシピ編

データ

  • キーワードのデータ(4,722,057行)
  • キーワード同士の関連データ(136,802,075行)
  • 課題文脈のデータ(22,444行)
  • キーワードと課題の関連データ(114,832,650行)

手元の実験用テストデータ。日本国内特許公報データをもとに、Hadoop、Word2vecなどを使って生成したデータになります。アスタミューゼにはデータがたくさんあり、データからデータを生成できます。

環境・ミドルウェア

  • GCP n1-highmem-8 preemptible*2 (8 vCPU, 52GB RAM, SSD storage 100 GB)
  • Linux (Ubuntu 16.04.3 LTS)
  • Neo4j 3.4.0 alpha06*3

設計

Neo4jで扱うプロパティグラフの形式に落とし込みます。

まず、キーワードと、課題文脈のデータ*4をNodeに対応させます。

そして、Node同士の関係をRelationshipに対応させます。関係の向きと重み、ラベルを定義します。

角丸の四角がNode(点)、矢印がRelation(線)になります。

f:id:astamuse:20180123193603p:plain

準備編

マニュアル*5を参照しながらセットアップします。

ここではUbuntu 16.04.3 LTSにNeo4j 3.4.0 alpha06をインストールします。

インストール

$ wget -O - https://debian.neo4j.org/neotechnology.gpg.key | sudo apt-key add -
$ echo 'deb http://debian.neo4j.org/repo testing/' | sudo tee -a /etc/apt/sources.list.d/neo4j.list
$ sudo apt-get update
$ sudo apt-get install neo4j=3.4.0.alpha06

*6

設定

設定ファイル/etc/neo4j/neo4j.confの編集

35,36c35,36
< #dbms.memory.heap.initial_size=512m
< #dbms.memory.heap.max_size=512m
---
> dbms.memory.heap.initial_size=16G
> dbms.memory.heap.max_size=16G
46c46
< #dbms.memory.pagecache.size=10g
---
> dbms.memory.pagecache.size=32g
54c54
< #dbms.connectors.default_listen_address=0.0.0.0
---
> dbms.connectors.default_listen_address=0.0.0.0
< #dbms.threads.worker_count=
---
> dbms.threads.worker_count=8

調理編

CSVの作成

Neo4jには大量のデータをCSVから効率良くロードするためのneo4j-admin importコマンド*7があります。このコマンドはデータの初期ロード専用で、データベースディレクトリが空でないと実行できませんが、高速にデータをロードできます。既存のデータベースへのデータのバルクロードには別途LOAD CSVコマンド*8を使用します。

Node、RelationshipのCSVファイルを複数用意し、一括でロードします。それぞれのCSVファイルのヘッダに、ロード先のスキーマを表現します。詳しくはマニュアル*9にあります。

ここでは、前述の設計に従って、CSVファイルを用意します。

keyword.csv:KeywordのNodeデータ

id:ID(Keyword-ID),name:string,:LABEL
12852111,プリプリ感,Keyword
115168,プリプレグ,Keyword
10515289,プリプレグFRPシート,Keyword
10515208,プリプレグアセンブリ,Keyword
12852112,プリプレグカセット,Keyword
10515209,プリプレグクロス,Keyword
10515210,プリプレグシート,Keyword

issue_fterm_context.csv:FTermのNodeデータ

id:ID(FTerm-ID),code,context,:LABEL
1,2B022EA00,植物の栽培/化学物質の処理目的,FTerm
2,2B022EA01,植物の栽培/化学物質の処理目的/生長調節,FTerm
3,2B022EA02,植物の栽培/化学物質の処理目的/生長調節/種なし果実の作成,FTerm
4,2B022EA03,植物の栽培/化学物質の処理目的/果実、野菜の食味、色付きの向上,FTerm
5,2B022EA10,植物の栽培/化学物質の処理目的/その他,FTerm
6,2B026BB00,海藻の栽培/支柱の目的、機能,FTerm
7,2B026BB01,海藻の栽培/支柱の目的、機能/強度を増す,FTerm
8,2B026BB02,海藻の栽培/支柱の目的、機能/打ち込み引きぬき,FTerm
9,2B026BB03,海藻の栽培/支柱の目的、機能/倒伏防止,FTerm

Nodeデータのフォーマットは、(属性[:型],)+:LABELのような形になります。:IDは識別子であることを表し、Node毎に必ず指定します。:IDの後ろの(Keyword-ID)は、IDの名前空間を指定しています。デフォルトではNodeの名前空間は一つになります。リレーショナル・データベースでよくみられるように数値型のIDを持つエンティティが複数ある場合には、それぞれのIDの名前空間を分ける必要があります。

similarity.csv:SIMILAR_TOのRelationsipデータ

:START_ID(Keyword-ID),score:double,:END_ID(Keyword-ID),:TYPE
14384010,0.846703052521,14251003,SIMILAR_TO
14164776,0.840930700302,14251003,SIMILAR_TO
11690273,0.817037463188,14251003,SIMILAR_TO
10211878,0.808667063713,14251003,SIMILAR_TO
10446057,0.798547029495,14251003,SIMILAR_TO
13343895,0.795734345913,14251003,SIMILAR_TO
14335569,0.789334237576,14251003,SIMILAR_TO
12207055,0.788067042828,14251003,SIMILAR_TO
12698152,0.784716844559,14251003,SIMILAR_TO

keyword_issue.csv:RESOLVEのRelationshipデータ

:START_ID(Keyword-ID),n:int,:END_ID(FTerm-ID),:TYPE
12338970,1,2,RESOLVE
42045,1,2,RESOLVE
10181692,1,2,RESOLVE
12637528,1,2,RESOLVE
12676513,1,2,RESOLVE
109171,1,2,RESOLVE
10799840,1,2,RESOLVE
13335687,1,2,RESOLVE
11029079,8,2,RESOLVE

データロード

そして、以下のような簡易ロードスクリプトを用意しました。

#!/bin/bash

systemctl stop neo4j

rm -rf /var/lib/neo4j/data/databases/graph.db/*

NEO4J_DEBUG=true HEAP_SIZE=16g neo4j-admin import --nodes issue_fterm_context.csv --nodes keyword.csv --relation
ships similarity.csv --relationships keyword_issue.csv --ignore-missing-nodes=true --ignore-duplicate-nodes=true
-id-type=INTEGER

chown -R neo4j:neo4j /var/lib/neo4j/data/databases/graph.db/

systemctl start neo4j

実行すると、進捗とサマリーが表示されます。

Neo4j version: 3.4.0-alpha06
Importing the contents of these files into /var/lib/neo4j/data/databases/graph.db:
Nodes:
 /var/tmp/blog/issue_fterm_context.csv

 /var/tmp/blog/keyword.csv
Relationships:
 /var/tmp/blog/similarity.csv

 /var/tmp/blog/keyword_issue.csv

Available resources:
 Total machine memory: 51.10 GB
 Free machine memory: 49.98 GB
 Max heap memory : 15.33 GB
 Processors: 8
 Configured max memory: 31.18 GB

Import starting 2018-01-23 04:05:24.396+0000
 Estimated number of nodes: 7.29 M
 Estimated number of node properties: 14.63 M
 Estimated number of relationships: 145.48 M
 Estimated number of relationship properties: 145.48 M
 Estimated disk space usage: 10.86 GB
 Estimated required memory usage: 569.89 MB

Interactive command list (end with ENTER):
 c: Print more detailed information about current stage
 i: Print more detailed information

(1/4) Node import 2018-01-23 04:05:24.418+0000
 Estimated number of nodes: 7.29 M
 Estimated disk space usage: 710.50 MB
 Estimated required memory usage: 569.89 MB
.......... .......... .......... .......... .......... 5%
.......... .......... .......... .......... .......... 10%
.......... .......... .......... .......... .......... 15%
.......... .......... .......... .......... .......... 20%
.......... .......... .......... .......... .......... 25%
.......... .......... .......... .......... .......... 30%
.......... .......... .......... .......... .......... 35%
.......... .......... .......... .......... .......... 40%
.......... .......... .......... .......... .......... 45%
.......... .......... .......... .......... .......... 50%
.......... .......... .......... .......... .......... 55%
.......... .......... .......... .......... .......... 60%
.......... .......... .......... .......... .......... 65%
.......... .......... .......... .......... .......... 70%
.......... .......... .......... .......... .......... 75%
.......... .......... .......... .......... .......... 80%
.......... .......... .......... .......... .......... 85%
.......... .......... .......... .......... .......... 90%
.......... .......... .......... .......... .......... 95%
.......... .......... .......... .......... .......... 100%

(2/4) Relationship import 2018-01-23 04:05:29.298+0000
 Estimated number of relationships: 145.48 M
 Estimated disk space usage: 10.16 GB
 Estimated required memory usage: 534.38 MB
.......... .......... .......... .......... .......... 5%
.......... .......... .......... .......... .......... 10%
.......... .......... .......... .......... .......... 15%
.......... .......... .......... .......... .......... 20%
.......... .......... .......... .......... .......... 25%
.......... .......... .......... .......... .......... 30%
.......... .......... .......... .......... .......... 35%
.......... .......... .......... .......... .......... 40%
.......... .......... .......... .......... .......... 45%
.......... .......... .......... .......... .......... 50%
.......... .......... .......... .......... .......... 55%
.......... .......... .......... .......... .......... 60%
.......... .......... .......... .......... .......... 65%
.......... .......... .......... .......... .......... 70%
.......... .......... .......... .......... .......... 75%
.......... .......... .......... .......... .......... 80%
.......... .......... .......... .......... .......... 85%
.......... .......... .......... .......... .......... 90%
.......... .......... .......... .......... .......... 95%
.......... .......... .......... .......... .........(3/4) Relationship linking 2018-01-23 04:08:40.284+0000
 Estimated required memory usage: 527.90 MB
.......... .......... .......... .......... .......... 5%
.......... .......... .......... .......... .......... 10%
.......... .......... .......... .......... .......... 15%
.......... .......... .......... .......... .......... 20%
.......... .......... .......... .......... .......... 25%
.......... .......... .......... .......... .......... 30%
.......... .......... .......... .......... .......... 35%
.......... .......... .......... .......... .......... 40%
.......... .......... .......... .......... .......... 45%
.......... .......... .......... .......... .......... 50%
.......... .......... .......... .......... .......... 55%
.......... .......... .......... .......... .......... 60%
.......... .......... .......... .......... .......... 65%
.......... .......... .......... .......... .......... 70%
.......... .......... .......... .......... .......... 75%
.......... .......... .......... .......... .......... 80%
.......... .......... .......... .......... .......... 85%
.......... .......... .......... .......... .......... 90%
.......... .......... .......... .......... .......... 95%
.......... .......... .......... .......... .......... 100%

(4/4) Post processing 2018-01-23 04:09:46.174+0000
 Estimated required memory usage: 478.13 MB
.......... .......... .......... .......... .......... 5%
.......... .......... .......... .......... .......... 10%
.......... .......... .......... .......... .......... 15%
.......... .......... .......... .......... .......... 20%
.......... .......... .......... .......... .......... 25%
.......... .......... .......... .......... .......... 30%
.......... .......... .......... .......... .......... 35%
.......... .......... .......... .......... .......... 40%
.......... .......... .......... .......... .......... 45%
.......... .......... .......... .......... .......... 50%
.......... .......... .......... .......... .......... 55%
.......... .......... .......... .......... .......... 60%
.......... .......... .......... .......... .......... 65%
.......... .......... .......... .......... .......... 70%
.......... .......... .......... .......... .......... 75%
.......... .......... .......... .......... .......... 80%
.......... .......... .......... .......... .......... 85%
.......... .......... .......... .......... .......... 90%
.......... .......... .......... .......... .......... 95%
.......... .......... .......... .......... .......... 100%


IMPORT DONE in 4m 35s 487ms.  
Imported:
 4744499 nodes
 145399772 relationships
 154911213 properties
Peak memory usage: 632.11 MB

4分半程でロードが完了しました。単純計算で約93万行/秒のスループットです。

また、以前のバージョンよりも表示がわかりやすく、進捗の視認性がよくなっています。

インデックスの作成

neo4j-admin importで初期ロードしたデータには、インデックスは作成されていないため、個別に作成します。

cypher-shellというインタラクティブなシェルを使います。*10

$ cypher-shell  
username: neo4j
password: ********
Connected to Neo4j 3.4.0-alpha06 at bolt://localhost:7687 as user neo4j.
Type :help for a list of available commands or :exit to exit the shell.
Note that Cypher queries must end with a semicolon.
neo4j> MATCH (k: Keyword {name: "プリプレグ"}) RETURN k;
+----------------------------------------+
| k                                      |
+----------------------------------------+
| (:Keyword {name: "プリプレグ", id: 115168}) |
+----------------------------------------+

1 row available after 113 ms, consumed after another 4509 ms
neo4j> MATCH (k: Keyword {name: "プリプレグ"}) RETURN k;
+----------------------------------------+
| k                                      |
+----------------------------------------+
| (:Keyword {name: "プリプレグ", id: 115168}) |
+----------------------------------------+

1 row available after 5 ms, consumed after another 3888 ms
neo4j> CREATE INDEX ON :Keyword(name);
0 rows available after 54 ms, consumed after another 0 ms
Added 1 indexes

CREATE INDEXを実行すると、インデックスは非同期で作成されます。

ステータスはログに出力されます。

2018-01-23 04:13:24.092+0000 INFO [o.n.k.i.a.i.IndexPopulationJob] Index population started: [:Keyword(name) [pr
ovider: {key=lucene+native, version=1.0}]]
2018-01-23 04:13:40.480+0000 INFO [o.n.k.i.a.i.IndexPopulationJob] Completed node store scan. Flushing all pendi
ng updates.
BatchingMultipleIndexPopulator{activeTasks=5, executor=java.util.concurrent.ThreadPoolExecutor@2b76dd88[Running,
pool size = 7, active threads = 5, queued tasks = 0, completed tasks = 468], batchedUpdates = [0 updates], queu
edUpdates = 0}
2018-01-23 04:13:40.481+0000 INFO [o.n.k.i.a.i.IndexPopulationJob] Shutting down executor.
BatchingMultipleIndexPopulator{activeTasks=5, executor=java.util.concurrent.ThreadPoolExecutor@2b76dd88[Running,
pool size = 7, active threads = 5, queued tasks = 0, completed tasks = 469], batchedUpdates = [0 updates], queu
edUpdates = 0}
2018-01-23 04:13:46.163+0000 INFO [o.n.k.i.a.i.IndexPopulationJob] Index population completed. Index is now onli
ne: [:Keyword(name) [provider: {key=lucene+native, version=1.0}]]

インデックスの作成が完了したところで、再度検索してみます。

neo4j> MATCH (k: Keyword {name: "プリプレグ"}) RETURN k;
+----------------------------------------+
| k                                      |
+----------------------------------------+
| (:Keyword {name: "プリプレグ", id: 115168}) |
+----------------------------------------+

1 row available after 62 ms, consumed after another 1 ms
neo4j> MATCH (k: Keyword {name: "プリプレグ"}) RETURN k;
+----------------------------------------+
| k                                      |
+----------------------------------------+
| (:Keyword {name: "プリプレグ", id: 115168}) |
+----------------------------------------+

1 row available after 2 ms, consumed after another 1 ms
neo4j>

試食編

プリプレグ

ところで、「プリプレグ」とは何でしょう。食べ物ではなさそうです。あまり耳馴染みがない方も多いかもしれません。私もその一人です。

初めて聞くキーワードを、周辺のキーワードと、キーワードが寄与している文脈から、おおよその見当をつけられるかもしれません。

早速、WebUI*11にアクセスし、クエリを実行してみます。

MATCH (k:Keyword {name: "プリプレグ"})-[s:SIMILAR_TO *1..3]-(l) RETURN DISTINCT s,l,k LIMIT 10

f:id:astamuse:20180123191853p:plain

朧げながら、プリプレグの人となりが見えてきます。わりと硬派な印象です。

今度は、周辺のキーワードに手を伸ばしつつ、関連する課題を探索してみます。

MATCH (k:Keyword {name: "プリプレグ"})-[s:SIMILAR_TO]-(l:Keyword)
WITH k, s, l ORDER BY s.score DESC LIMIT 100
MATCH (k)-[s]-(l)-[r:RESOLVE]->(f:FTerm)
WITH DISTINCT k, s, l, r, f ORDER BY r.n DESC LIMIT 20
RETURN k, s, l, r, f

グラフ表示

f:id:astamuse:20180123183837p:plain

テキスト表示

╒════════════════════════════╤════════════════════════╤═══════════════════════════════╤════════╤══════════════════════════════════════════════════════════════════════╕
│"k"                         │"s"                     │"l"                            │"r"     │"f"                                                                   │
╞════════════════════════════╪════════════════════════╪═══════════════════════════════╪════════╪══════════════════════════════════════════════════════════════════════╡
│{"name":"プリプレグ","id":115168}│{"score":0.641988456249}│{"name":"熱硬化性樹脂","id":13947024}│{"n":50}│{"context":"接着剤、接着方法/物理(化学)的性質又は目的、効果/機械的特性","code":"4J040LA06","id":1│
│                            │                        │                               │        │4260}                                                                 │
├────────────────────────────┼────────────────────────┼───────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"プリプレグ","id":115168}│{"score":0.641988456249}│{"name":"熱硬化性樹脂","id":13947024}│{"n":45}│{"context":"接着剤、接着方法/物理(化学)的性質又は目的、効果/電気磁気特性","code":"4J040LA09","id":│
│                            │                        │                               │        │14263}                                                                │
├────────────────────────────┼────────────────────────┼───────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"プリプレグ","id":115168}│{"score":0.641988456249}│{"name":"熱硬化性樹脂","id":13947024}│{"n":45}│{"context":"接着剤、接着方法/物理(化学)的性質又は目的、効果/温度特性又は熱特性","code":"4J040LA08","i│
│                            │                        │                               │        │d":14262}                                                             │
├────────────────────────────┼────────────────────────┼───────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"プリプレグ","id":115168}│{"score":0.641988456249}│{"name":"熱硬化性樹脂","id":13947024}│{"n":43}│{"context":"多層プリント配線板の製造/目的課題効果/機械的特性に関するもの","code":"5E346HH11","id":1│
│                            │                        │                               │        │7583}                                                                 │
├────────────────────────────┼────────────────────────┼───────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"プリプレグ","id":115168}│{"score":0.641988456249}│{"name":"熱硬化性樹脂","id":13947024}│{"n":30}│{"context":"接着剤、接着方法/物理(化学)的性質又は目的、効果/分子量又はその関連特性","code":"4J040LA01",│
│                            │                        │                               │        │"id":14255}                                                           │
├────────────────────────────┼────────────────────────┼───────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"プリプレグ","id":115168}│{"score":0.641988456249}│{"name":"熱硬化性樹脂","id":13947024}│{"n":29}│{"context":"電場又は磁場に対する装置又は部品の遮蔽/目的/電(磁)波シールド電磁シールド","code":"5E321GG05"│
│                            │                        │                               │        │,"id":17466}                                                          │
├────────────────────────────┼────────────────────────┼───────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"プリプレグ","id":115168}│{"score":0.641988456249}│{"name":"熱硬化性樹脂","id":13947024}│{"n":29}│{"context":"印刷回路の非金属質の保護被覆/目的効果/その他","code":"5E314GG26","id":17427}   │
├────────────────────────────┼────────────────────────┼───────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"プリプレグ","id":115168}│{"score":0.584715306759}│{"name":"ワニス","id":145746}     │{"n":29}│{"context":"電動機、発電機の製造/目的/製造,組立","code":"5H615AA01","id":20027}       │
├────────────────────────────┼────────────────────────┼───────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"プリプレグ","id":115168}│{"score":0.641988456249}│{"name":"熱硬化性樹脂","id":13947024}│{"n":28}│{"context":"印刷回路に対する電気部品等の電気的接続/目的効果/はんだ付け性の改良","code":"5E319GG03","id│
│                            │                        │                               │        │":17455}                                                              │
├────────────────────────────┼────────────────────────┼───────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"プリプレグ","id":115168}│{"score":0.641988456249}│{"name":"熱硬化性樹脂","id":13947024}│{"n":27}│{"context":"多層プリント配線板の製造/目的課題効果/製造生産に関するもの/歩留生産性の向上","code":"5E346HH33│
│                            │                        │                               │        │","id":17596}                                                         │
├────────────────────────────┼────────────────────────┼───────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"プリプレグ","id":115168}│{"score":0.641988456249}│{"name":"熱硬化性樹脂","id":13947024}│{"n":26}│{"context":"接着剤、接着方法/物理(化学)的性質又は目的、効果/結晶関連特性","code":"4J040LA02","id":│
│                            │                        │                               │        │14256}                                                                │
├────────────────────────────┼────────────────────────┼───────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"プリプレグ","id":115168}│{"score":0.791278362274}│{"name":"積層板","id":348376}     │{"n":25}│{"context":"プリント基板への印刷部品(厚膜薄膜部品)/目的,効果/その他","code":"4E351GG20","id":13│
│                            │                        │                               │        │245}                                                                  │
├────────────────────────────┼────────────────────────┼───────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"プリプレグ","id":115168}│{"score":0.641988456249}│{"name":"熱硬化性樹脂","id":13947024}│{"n":25}│{"context":"電動機、発電機の製造/目的/製造,組立","code":"5H615AA01","id":20027}       │
├────────────────────────────┼────────────────────────┼───────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"プリプレグ","id":115168}│{"score":0.791278362274}│{"name":"積層板","id":348376}     │{"n":23}│{"context":"プリント配線の製造(2)/目的効果/密着性の改良/導体とプリント板の基板","code":"5E343GG02","│
│                            │                        │                               │        │id":17545}                                                            │
├────────────────────────────┼────────────────────────┼───────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"プリプレグ","id":115168}│{"score":0.641988456249}│{"name":"熱硬化性樹脂","id":13947024}│{"n":23}│{"context":"多層プリント配線板の製造/目的課題効果/電気的特性に関するもの/電気的接続性","code":"5E346HH07"│
│                            │                        │                               │        │,"id":17581}                                                          │
├────────────────────────────┼────────────────────────┼───────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"プリプレグ","id":115168}│{"score":0.641988456249}│{"name":"熱硬化性樹脂","id":13947024}│{"n":20}│{"context":"繊維製品への有機化合物の付着処理/目的,効果/接着","code":"4L033AC11","id":14648} │
├────────────────────────────┼────────────────────────┼───────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"プリプレグ","id":115168}│{"score":0.641988456249}│{"name":"熱硬化性樹脂","id":13947024}│{"n":19}│{"context":"多層プリント配線板の製造/目的課題効果/その他","code":"5E346HH40","id":17597}   │
├────────────────────────────┼────────────────────────┼───────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"プリプレグ","id":115168}│{"score":0.791278362274}│{"name":"積層板","id":348376}     │{"n":17}│{"context":"多層プリント配線板の製造/目的課題効果/機械的特性に関するもの","code":"5E346HH11","id":1│
│                            │                        │                               │        │7583}                                                                 │
├────────────────────────────┼────────────────────────┼───────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"プリプレグ","id":115168}│{"score":0.641988456249}│{"name":"熱硬化性樹脂","id":13947024}│{"n":17}│{"context":"印刷回路の非金属質の保護被覆/目的効果/生産性向上","code":"5E314GG24","id":17426} │
├────────────────────────────┼────────────────────────┼───────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"プリプレグ","id":115168}│{"score":0.641988456249}│{"name":"熱硬化性樹脂","id":13947024}│{"n":15}│{"context":"半導体又は固体装置の封緘,被覆構造と材料/用途又は特殊目的/光半導体用","code":"4M109GA01","i│
│                            │                        │                               │        │d":14812}                                                             │
└────────────────────────────┴────────────────────────┴───────────────────────────────┴────────┴──────────────────────────────────────────────────────────────────────┘

クエリの結果を解釈して評価するためには、別途何らかの判断基準が必要そうです。

門外漢の私には少々荷が重いので、いささか心得のある、ラーメンというキーワードに換えてクエリを実行してみることにします。

ラーメン

MATCH (k:Keyword {name: "ラーメン"})-[s:SIMILAR_TO]-(l:Keyword)
WITH k, s, l ORDER BY s.score DESC LIMIT 100
MATCH (k)-[s]-(l)-[r:RESOLVE]->(f:FTerm)
WITH DISTINCT k, s, l, r, f ORDER BY r.n DESC LIMIT 20
RETURN k, s, l, r, f

グラフ表示 f:id:astamuse:20180123183933p:plain

テキスト表示

╒═══════════════════════════╤════════════════════════╤═══════════════════════════════════╤═══════╤══════════════════════════════════════════════════════════════════════╕
│"k"                        │"s"                     │"l"                                │"r"    │"f"                                                                   │
╞═══════════════════════════╪════════════════════════╪═══════════════════════════════════╪═══════╪══════════════════════════════════════════════════════════════════════╡
│{"name":"ラーメン","id":137294}│{"score":0.617753505707}│{"name":"包装食品","id":10958102}      │{"n":3}│{"context":"包装体/目的,機能(1)特定の環境の維持/その他/包装体/その他/包装体/その他/包装体/その他","code":"│
│                           │                        │                                   │       │3E067GD10","id":5974}                                                 │
├───────────────────────────┼────────────────────────┼───────────────────────────────────┼───────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"ラーメン","id":137294}│{"score":0.336226522923}│{"name":"喫食用容器入り冷凍麺","id":13427756}│{"n":3}│{"context":"穀類誘導製品3(麺類)/目的/即席化","code":"4B046LC11","id":12085}        │
├───────────────────────────┼────────────────────────┼───────────────────────────────────┼───────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"ラーメン","id":137294}│{"score":0.617753505707}│{"name":"包装食品","id":10958102}      │{"n":2}│{"context":"食品の保存(凍結・冷却・乾燥を除く)/使用目的/抗微生物","code":"4B021MC01","id":1180│
│                           │                        │                                   │       │3}                                                                    │
├───────────────────────────┼────────────────────────┼───────────────────────────────────┼───────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"ラーメン","id":137294}│{"score":0.492829829454}│{"name":"ゲル状調味料","id":10169798}    │{"n":2}│{"context":"ゼリ−、ジャム、シロップ/目的/テクスチャーの改善","code":"4B041LC03","id":12012} │
├───────────────────────────┼────────────────────────┼───────────────────────────────────┼───────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"ラーメン","id":137294}│{"score":0.492829829454}│{"name":"ゲル状調味料","id":10169798}    │{"n":2}│{"context":"種実、スープ、その他の食品/目的/味,香りの改善","code":"4B036LC01","id":11983}  │
├───────────────────────────┼────────────────────────┼───────────────────────────────────┼───────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"ラーメン","id":137294}│{"score":0.617753505707}│{"name":"包装食品","id":10958102}      │{"n":2}│{"context":"袋/目的機能/内容物の保存性向上","code":"3E064EA18","id":5883}           │
├───────────────────────────┼────────────────────────┼───────────────────────────────────┼───────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"ラーメン","id":137294}│{"score":0.617753505707}│{"name":"包装食品","id":10958102}      │{"n":1}│{"context":"肉類、卵、魚製品/目的/呈味の改善","code":"4B042AC03","id":12021}         │
├───────────────────────────┼────────────────────────┼───────────────────────────────────┼───────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"ラーメン","id":137294}│{"score":0.617753505707}│{"name":"包装食品","id":10958102}      │{"n":1}│{"context":"高周波加熱[構造]/目的/電磁波の漏洩防止","code":"3K090LA04","id":10994}     │
├───────────────────────────┼────────────────────────┼───────────────────────────────────┼───────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"ラーメン","id":137294}│{"score":0.617753505707}│{"name":"包装食品","id":10958102}      │{"n":1}│{"context":"肉類、卵、魚製品/目的/保存性の改善","code":"4B042AC06","id":12024}        │
├───────────────────────────┼────────────────────────┼───────────────────────────────────┼───────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"ラーメン","id":137294}│{"score":0.617753505707}│{"name":"包装食品","id":10958102}      │{"n":1}│{"context":"基本的包装技術VII(真空包装)/目的/その他","code":"3E053JA10","id":5838}    │
├───────────────────────────┼────────────────────────┼───────────────────────────────────┼───────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"ラーメン","id":137294}│{"score":0.617753505707}│{"name":"包装食品","id":10958102}      │{"n":1}│{"context":"高周波加熱[構造]/目的/加熱効率向上又は省電力","code":"3K090AA02","id":10977}  │
├───────────────────────────┼────────────────────────┼───────────────────────────────────┼───────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"ラーメン","id":137294}│{"score":0.617753505707}│{"name":"包装食品","id":10958102}      │{"n":1}│{"context":"基本的包装技術VII(真空包装)/目的/高速化","code":"3E053JA01","id":5829}    │
├───────────────────────────┼────────────────────────┼───────────────────────────────────┼───────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"ラーメン","id":137294}│{"score":0.617753505707}│{"name":"包装食品","id":10958102}      │{"n":1}│{"context":"袋/目的機能/その他","code":"3E064EA30","id":5888}                 │
├───────────────────────────┼────────────────────────┼───────────────────────────────────┼───────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"ラーメン","id":137294}│{"score":0.617753505707}│{"name":"包装食品","id":10958102}      │{"n":1}│{"context":"基本的包装技術VII(真空包装)/目的/ガス置換率の向上","code":"3E053JA03","id":5831│
│                           │                        │                                   │       │}                                                                     │
├───────────────────────────┼────────────────────────┼───────────────────────────────────┼───────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"ラーメン","id":137294}│{"score":0.617753505707}│{"name":"包装食品","id":10958102}      │{"n":1}│{"context":"袋/目的機能/排出容易性","code":"3E064EA12","id":5877}               │
├───────────────────────────┼────────────────────────┼───────────────────────────────────┼───────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"ラーメン","id":137294}│{"score":0.617753505707}│{"name":"包装食品","id":10958102}      │{"n":1}│{"context":"袋/目的機能/経済性","code":"3E064EA01","id":5867}                 │
├───────────────────────────┼────────────────────────┼───────────────────────────────────┼───────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"ラーメン","id":137294}│{"score":0.617753505707}│{"name":"包装食品","id":10958102}      │{"n":1}│{"context":"加熱調理器/目的、効果/衛生性、外観","code":"4B055BA51","id":12144}        │
├───────────────────────────┼────────────────────────┼───────────────────────────────────┼───────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"ラーメン","id":137294}│{"score":0.617753505707}│{"name":"包装食品","id":10958102}      │{"n":1}│{"context":"誘導加熱調理器/目的、効果〔回路装置〕/回路素子/インバータ/表示,監視装置/誘導加熱調理器/非物理量※/基準値/構│
│                           │                        │                                   │       │造対象の処理態様※/使い勝手,判り易さ","code":"3K051AD39","id":10504}                   │
├───────────────────────────┼────────────────────────┼───────────────────────────────────┼───────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"ラーメン","id":137294}│{"score":0.617753505707}│{"name":"包装食品","id":10958102}      │{"n":1}│{"context":"包装体/目的,機能(1)特定の環境の維持/その他/包装体/その他/包装体/その他/包装体/風味,香気保持","cod│
│                           │                        │                                   │       │e":"3E067GD02","id":5966}                                             │
├───────────────────────────┼────────────────────────┼───────────────────────────────────┼───────┼──────────────────────────────────────────────────────────────────────┤
│{"name":"ラーメン","id":137294}│{"score":0.617753505707}│{"name":"包装食品","id":10958102}      │{"n":1}│{"context":"食品の調整及び処理一般/目的/その他","code":"4B035LC16","id":11981}        │
└───────────────────────────┴────────────────────────┴───────────────────────────────────┴───────┴──────────────────────────────────────────────────────────────────────┘

インスタントラーメンの製造に関する課題であることがわかります。

以上、簡単ですが、グラフ構造に対してクエリを投げ、周辺の語彙と文脈を照らすことができました。

まとめ

最新のNeo4jをセットアップし、インポートツールを使用してデータの初期ロードを行いました。

特許文書から抽出したキーワード同士の関連と、キーワードが現れる文脈としての分類データのグラフを、あるキーワードを起点として探索しました。

グラフ構造は複雑で、データの量が多くなると全てを把握し見通すことは困難となり、合理的なコストで探索できることがなにより重要になってきます。その点でNeo4jは扱いやすく、一定の規模のデータに対して探索的なクエリ実行ができるので、強力な武器になりそうです。

スマートスピーカー向けに、ブレインストーミングの相手をしてくれるようなアプリケーションを開発してみても面白いかもしれません。

*1:https://neo4j.com

Neo4jをより詳しく知りたい方向けの参考書籍

グラフ型データベース入門 - Neo4jを使う Neo4jユーザーグループ

グラフデータベース ―Neo4jによるグラフデータモデルとグラフデータベース入門 Ian Robinson

O’Reilly’s Graph Databases(無料ダウンロード・英語)

*2:プリエンプティブ仮想マシンを指定することで少し余裕のあるインスタンスをリーズナブルなコストで使用できます

*3:Neo4jの2018年1月23日時点で最新のα版。安定版の最新は3.3.2です

*4:国内の特許公報文書には、案件毎に複数の分類体系のコードが付与され、特許の先行技術調査を行う際に利用されます。Fタームという分類体系もその一つです。

*5:https://neo4j.com/docs/operations-manual/current/installation/linux/

*6:安定版をインストールする場合はリポジトリのtestingをstableにします。

*7:https://neo4j.com/docs/operations-manual/current/tools/import/

*8:http://neo4j.com/docs/developer-manual/current/cypher/clauses/load-csv/

*9:https://neo4j.com/docs/operations-manual/current/tools/import/file-header-format/

*10:以前のバージョンではneo4j-shellというコマンドでした。こちらはまだ存在はしますが、cypher-shellが後継となります

*11:Web UI(デフォルトで7474ポート)にアクセスしてクエリを実行すると、ビジュアライゼーションされたグラフがきれいに表示されます。結果はテーブル形式、テキスト形式、JSONコード形式に切替えられます。また、グラフ表示でノードをダブルクリックすると周辺のノードが展開されます。クエリを投げながら探索の方向性を探るといった使い方ができます。

マーケティングとエンジニアとの関わりについて

はじめまして。 アスタミューゼで事業開発・マーケティングを担当しているボーダー☆キングと申します。開発部長の並河さんより、当ブログの執筆という大役を頂きました。緊張しています(笑)。

参考)この人形が執筆者に廻ってきます…。(通称:白い悪魔) f:id:astamuse:20180117185726j:plain

このブログはエンジニアとデザイナーのブログなので技術的なお話が多いと思うのですが、今回は趣向を変えて、企画からリリースまでのマーケティングとエンジニアの関わりや、ベンチャーならではの関わり方だなー、と思ったことについて記してみたいと思います。

1.簡単な略歴

その前に、私の経歴について触れておいた方が判りやすいですかね。
私はアスタミューゼが3社目の会社になります。

1社目:リクルート
リクルートでは、WEB商品を中心に新規商品の策定などを行っていました。割とヒットしたのは「街の不動産屋さんでも扱える、簡単CMSツール」というコンセプトで企画・開発した「ホームページサービス」でしょうか。担当した1年で年間売上1.5億の商品になりました。

例)ホームページサービス
https://business.suumo.jp/chintai/lineup/hpservice.html f:id:astamuse:20180117185921j:plain

【企画にあたり、主にやったこと】
・競合調査と商品優位性の規定
・Information Architectureとワイヤーフレーム作成
・営業とエンジニアの間に立っての仕様調整
・既存物件DBとの繋ぎこみ設計

2社目:カカクコム
カカクコムでは、商品比較サイト「価格.com」のコミュニティ担当としてサイト改善やリニューアルを担当していました。大きい仕事では「ランキングページの8年ぶりのリニューアル」ですかね。リリース後、ランキングページのUUを1.2倍まで持って行くことに成功しました。

例)ランキングページ
http://kakaku.com/kaden/lcd-tv/ranking_2041/ f:id:astamuse:20180117190035j:plain

【企画にあたり、主にやったこと】
・ユーザーヒアリングに基づいたコンセプト設計
・ワイヤーフレーム作成
・各画面の詳細仕様設計と策定
・エンジニアとの仕様調整

アスタミューゼでは「転職ナビ」というサービスを主に担当しています。
過去に企画で行ってきた作業をベースにしながら、顧客対応や各種有料集客施策といったフロントの部分から売り上げを最大化するための運用面まで幅広く見渡しながら日々過ごしています。

2.転職ナビとは

「転職ナビ」とは、様々な特許情報や技術情報を持っているアスタミューゼが、そこに携わる人材の支援もトータルで行いたい、という思想で生まれた転職サイトです。2010年のサービスを開始より、今では年間あたり3万人以上の登録を頂くサイトに成長しています。

indeedやDODAといった大手の総合型のサイトと異なる強みは何か、というと特定の技術分野に特化していることがひとつとして挙げられます。

例えばですが
 ・有機EL転職ナビ
 ・核融合転職ナビ
 ・Scala転職ナビ

f:id:astamuse:20180117192848j:plain
一例)Scala転職ナビ
といった、かなりニッチな(笑)サイトが400近くあります。
「転職ナビ」はScalaを開発言語で活用していますが、Scala転職ナビ経由で登録された方は確実にその言語に対する技術を持っていらっしゃるため、総合型サイトと比較しても格段のマッチング精度で募集することが出来る、いうことがプロダクトとしての強みだと思います。

3.転職ナビのビジネスモデル上の課題

次は、転職ナビのビジネスモデルとその課題についてご説明します。
転職ナビはサイト毎にパーソルキャリアやジェイエイシーリクルートメントなどの大手人材紹介会社と提携をしており、基本的にサイト毎の提携先に登録者を紹介させて頂いています。
ただし、紹介した方が既に他のルートで人材紹介会社に登録済みの場合、他の提携先に送客させて頂く場合があり、それは登録者全体の3割程度が該当します。
年間にすると1万人程度の人数となり、その方たちには他の提携先の求人状況やサービス範囲などの条件と照らし合わせて、最適な提携先を紹介する必要があります。
非常に重要な業務なのですが、多岐にわたる提携先の特徴を視野に入れながら、提携先も求職者も喜んでいただける紹介選定は職人芸に依るところが大きく、ナレッジの共有化と継続性、労働工数という観点で大きな課題となっていました。

イメージ)再送客先選定は一子相伝の職人芸 f:id:astamuse:20180117190413j:plain

4.機械学習を活用した判定結果の導入

「こんなことで困っているんですよねー」
ある日飲み会の席で開発部長の並河さんに軽く相談したところ、「毎年3万人以上のデータが整っているのであれば、機械学習を活用すれば、ある程度判定を自動化することもできるんじゃないですかねー」と提案を頂きました。

イメージ)仕事のことも気軽に相談できる懇親会 f:id:astamuse:20180117190454j:plain

後日、提携先ごとに数千から数万存在する過去の判定結果(この人はカウンセリングの対象かどうか)を並河さんにお渡しし、数日経ったシミュレーションの結果がこちらになります。

参考)ある提携先のシミュレーション結果 f:id:astamuse:20180117190528j:plain

登録者の個人情報は「年齢」や「職種」など様々な要素で構成されていますが、ある提携先がカウンセリングしたくなる人は、どの項目と相関性が強いかを表しています。
(クリーム色に網掛けしてある部分が相関性の強い構成要素の組み合わせになります)

こうした試行錯誤をしていく中で、企画とエンジニアの「こうした要素の関連が強いのではないか」というやり取りを頻繁に行い、結果として「年齢」「都道府県」「職種小分類」を活用したものを採択しました。

実際の運用としては、既存登録者の個人情報を機械学習の判定プログラムにかけると、それぞれの人にスコアと判定がついて戻ってきます。

例えば
Aさん…スコア:0.6 判定:○
Bさん…スコア:0.3 判定:×
というような形です。

このプログラムのスコア0.6で「○」が付いたAさんは99.6%以上の確率でカウンセリングへと繋がることが分かっているため、この提携先に紹介すれば良いですし、同様にBさんは98.6%の確率でカウンセリングに繋がらないので、他に適した提携先を紹介すれば良いことになります。

参考)スコアと○×の正解率一覧表
f:id:astamuse:20180117190558j:plain

こうして確実に、スピーディーに新しい提携先に登録者を紹介することが可能となりました。
以前はこの運用に毎日150分という時間をかけていましたが、30分で終了できるようになり、提携先の信頼を担保しながら、大幅な運用工数の削減をすることができました。

また、現在はこの考え方を活用して、過去に登録した方に対して再登録を促すメール送信する対象者を選定するなど、様々な施策への応用を行っています。

5.おわりに

ある案件を参考に、マーケティングと開発の関わりを簡単ですが記してみました。
過去に経験した2社はいずれも大手企業に属する会社だと思いますが、最後にそれらとアスタミューゼの違いみたいなものを書いておきたいと思います。

a.コミュニケーションコストが低くて済む
私がいたころのリクルートは、開発といえば丸投げで、開発会社が膨大なコミュニケーションコストをかけて内容をキャッチアップする、という構図でした。営業サイドもITに明るい人が少なかったため、ものすごくコミュニケーションに時間を割いていました。アスタミューゼは基本自社開発ですので、プロダクトのことを熟知した方が多く、阿吽の呼吸で物事を進めることができるのは心地良いと思います。

b.まずやってみる、という気軽さ
カカクコムは一つのサイトが巨大すぎて影響範囲を確認するだけでも大変で、企画から承認まで6カ月くらいかかることがザラでした。今回採り上げたお話しもそうですが、エンジニアの課題解決力と新しい技術への好奇心から「まずやってみる」、というフットワークの軽さがあるのはベンチャー企業ならではではないでしょうか。

8年以上続く成熟した自社プロダクトを持ちながら、ベンチャーならではの足回りの速さを持ち合わせている開発現場だからこそ、マーケティングやエンジニアやデザイナーも日々様々な挑戦が出来ているのではないかなー、と思います。

最後になりましたが、 アスタミューゼでは現在、エンジニア・デザイナーに限らず幅広い職種を募集中です。 興味のある方はぜひ採用サイトからご応募ください。

採用情報|アスタミューゼ株式会社

ではまた。

CoreNLPを使ってみる(2) API編

山縣です。

新年明けましておめでとうございます。

弊社の年末年始休暇は例年になく長く11連休となりました。おかげでかなりリフレッシュできました。 まだちょっと休みボケも残っていますが頑張っていきたいと思います。

本年も弊社と当ブログをよろしくお願いします。

今回も CoreNLP について書きたいと思います。 前回 CoreNLP を CLI から使う方法やサーバとして起動してAPI経由で使う方法について書きました。 今回は API の方を見ていきたいと思います。 CoreNLP では Java の API が提供されているので、これを Scala から使ってみます。

1. Stanford CoreNLP API

サンプルコードとその実行

CoreNLP のサイトでAPI についてはこちらで説明されています。 このサイトを参考に Scala から API を使ってみます。

Scala の環境ということでビルドツールに SBT (1.0.4) を使います。

$ cat project/build.properties
sbt.version=1.0.4

build.sbt は以下のとおりです。

lazy val root = (project in file(".")).
  settings(
    organization := "example",
    scalaVersion := "2.11.12",
    version      := "0.1.0-SNAPSHOT",
    name := "Example1",
    libraryDependencies ++= Seq(
      "edu.stanford.nlp" % "stanford-corenlp" % "3.8.0",
      "edu.stanford.nlp" % "stanford-corenlp" % "3.8.0" classifier "models-english",
      "org.slf4j" % "slf4j-simple" % "1.7.12"
    ),
    fork in run := true,
    outputStrategy := Some(StdoutOutput),
    javaOptions in run += "-Xmx8G"
  )

corenlp は maven 上にパッケージがあるのでそれを利用します。 英語のモデルデータを取得するため、追加で classifier "models-english" を指定した依存関係も記述します。 また slf4j-simple を追加しています。このパッケージがないとcorenlp のログメッセージが出力されません。

サンプルのプログラムをサイトのサンプルを元に書いてみました。

package example1
import java.util

import scala.collection.JavaConverters._
import edu.stanford.nlp.ling.CoreAnnotations._
import edu.stanford.nlp.ling.CoreLabel
import edu.stanford.nlp.pipeline._
import edu.stanford.nlp.util.{CoreMap, PropertiesUtils}

import scala.collection.mutable


object Example1 {
  def main(args: Array[String]): Unit = {
    val props = PropertiesUtils.asProperties(
      "annotators", "tokenize, ssplit, pos, lemma, ner",
      "tokenize.language", "en"
    )
    val pipeline = new StanfordCoreNLP(props) //定義したプロパティ propsで Annotator である StanfordCoreNLP を生成

    val text = "Stanford University is located in California. It is a great university."
    val document: Annotation = new Annotation(text) //サンプルテキスト(String) で Annotation を生成

    pipeline.annotate(document) // アノテートする

    printResult(document)
  }

  def printResult(document:Annotation):Unit = {
    val sentences: mutable.Seq[CoreMap] = document.get(classOf[SentencesAnnotation]).asScala
    val tokens: mutable.Seq[CoreLabel] = sentences.flatMap(s => s.get(classOf[TokensAnnotation]).asScala)
    tokens.foreach{ t =>
      val txt: String = t.get(classOf[TextAnnotation])
      val pos: String = t.get(classOf[PartOfSpeechAnnotation])
      val lemma: String = t.get(classOf[LemmaAnnotation])
      val ner: String = t.get(classOf[NamedEntityTagAnnotation])
      println((txt,pos,lemma, ner))
    }
  }

実行すると以下のような出力が表示されます。

sbt:Example1> run
...
[info] Running (fork) example1.Example1
[main] INFO edu.stanford.nlp.pipeline.StanfordCoreNLP - Adding annotator tokenize
[main] INFO edu.stanford.nlp.pipeline.StanfordCoreNLP - Adding annotator ssplit
[main] INFO edu.stanford.nlp.pipeline.StanfordCoreNLP - Adding annotator pos
[main] INFO edu.stanford.nlp.tagger.maxent.MaxentTagger - Loading POS tagger from edu/stanford/nlp/models/pos-tagger/english-left3words/english-left3words-distsim.tagger ... done [0.8 sec].
...
(Stanford,NNP,Stanford,ORGANIZATION)
(University,NNP,University,ORGANIZATION)
(is,VBZ,be,O)
(located,JJ,located,O)
(in,IN,in,O)
(California,NNP,California,LOCATION)
(.,.,.,O)
(It,PRP,it,O)
(is,VBZ,be,O)
(a,DT,a,O)
(great,JJ,great,O)
(university,NN,university,O)
(.,.,.,O)
[success] Total time: 18 s, completed Dec 26, 2017 3:59:03 AM```
アノテーションの実行

上記のサンプルでトークナイズなどの一通りの処理(アノテーシ ョン)を実行する部分は以下になります。

    val props = PropertiesUtils.asProperties(
      "annotators", "tokenize, ssplit, pos, lemma, ner, parse, dcoref",
      "tokenize.language", "en"
    )
    val pipeline = new StanfordCoreNLP(props)

    val text = "Stanford University is located in California. It is a great university."
    val document: Annotation = new Annotation(text)

    pipeline.annotate(document)

コードを少し細かく見ていきます。

    val props = PropertiesUtils.asProperties(
      "annotators", "tokenize, ssplit, pos, lemma, ner, parse, dcoref",
      "tokenize.language", "en"
    )
    val pipeline = new StanfordCoreNLP(props) 

上記の部分では props に処理内容を記述しアノテータである StanfordCoreNLP を生成しています。

    val text = "Stanford University is located in California. It is a great university."
    val document: Annotation = new Annotation(text) 

次に処理したいテキストを用意します。 Annotation クラスのインスタンスdocumentとして定義します。

以上で準備が整ったので実際にアノテーションを実行します。

    pipeline.annotate(document)

annotate を呼ぶことでアノテーションの処理が実行され処理結果が document に保存されます。

処理結果の取得

次に処理された結果の表示について見ていきます。表示は printResult(..) メソッドにまとめています。

  def printResult(document:Annotation):Unit = {
    val sentences: mutable.Seq[CoreMap] = document.get(classOf[SentencesAnnotation]).asScala
    val tokens: mutable.Seq[CoreLabel] = sentences.flatMap(s => s.get(classOf[TokensAnnotation]).asScala)
    tokens.foreach{ t =>
      val txt: String = t.get(classOf[TextAnnotation])
      val pos: String = t.get(classOf[PartOfSpeechAnnotation])
      val lemma: String = t.get(classOf[LemmaAnnotation])
      val ner: String = t.get(classOf[NamedEntityTagAnnotation])
      println((txt,pos,lemma, ner))
    }
  }

まず下記のコードでセンテンスのリストを取得しています。

    val sentences: mutable.Seq[CoreMap] = document.get(classOf[SentencesAnnotation]).asScala

document.get() でアノテーションクラスのClass型インスタンスを渡すことで対応するデータを取得しています。 SentencesAnnotation を渡すと List[CoreMap] が返されます。(上記の場合 asScala で Scala のコレクション mutable.Seq に変換されています。)

各センテンスの情報は CoreMap インタフェースの実装クラスに保存されています。CoreMapはCoreNLP ライブラリの独自の Map インタフェースを定義しています。

次にセンテンスからトークンを取得します。

    val tokens: mutable.Seq[CoreLabel] = sentences.flatMap(s => s.get(classOf[TokensAnnotation]).asScala)

センテンスからトークンのリストを取得しているのは "s.get(classOf[TokensAnnotation])" です。 こちらも get() でトークンのアノテーションクラスのClass型インスタンスを渡すことで結果を取得しています。 get() で返ってくるのは List[CoreLabel] です。(例によって asScala で Scala のコレクション mutable.Seq に変換されています。) flatMap 使うことで Seq のネストを解消して mutable.Seq[CoreLabel] にしています。

各トークンに対してトークンのテキスト、品詞、レンマ、固有表現抽出の結果を出力しているのが下記の部分になります。

    tokens.foreach{ t =>
      val txt: String = t.get(classOf[TextAnnotation])
      val pos: String = t.get(classOf[PartOfSpeechAnnotation])
      val lemma: String = t.get(classOf[LemmaAnnotation])
      val ner: String = t.get(classOf[NamedEntityTagAnnotation])
      println((txt,pos,lemma, ner))
    }

CoreLabel は一つのトークンおよびアノテータにより付加されたアノテーション情報を保存するクラスです。 CoreLabel.get() において引数で指定されたClass型インスタンスに対応した値を返します。上記では各 トークン(CoreLabel) に対して TextAnnotation, PartOfSpeechAnnotation, LemmaAnnotation, NamedEntityTagAnnotation のそれぞれのアノテーションの情報(この場合はいずれもString型)を取得して表示しています。 以上のようにデータを取得するには各段階で get(classOf[...] ) というメソッドを呼び出しますが、コードが見づらい印象を受けます。 CoreLabel では代わりにそれぞれのアノテーションを簡易に取得するメソッドも提供されており上記の部分は、下記のように書くこともできます。

    tokens.foreach{ t =>
      val txt: String = t.word()
      val pos: String = t.tag()
      val lemma:String = t.lemma()
      val ner: String = t.ner()
      println((txt,pos,lemma, ner))
    }

こちらのほうが簡潔に書けて良いですね。

2. Simple CoreNLP API

以上のように Stanford CoreNLP API の使用方法を見てきましたが、少々まどろっこしい感じも受けますね。CoreNLP の API には前述の API とは別に Simple CoreNLP API というものも提供されており、より簡潔な記述で CoreNLP を利用する方法もありますので、ここではそれを見ていきたいと思います。

package example2

import scala.collection.JavaConversions._
import edu.stanford.nlp.simple._

import scala.collection.mutable


object Example2{
  def main(args: Array[String]): Unit = {
    val text = "Stanford University is located in California. It is a great university."
    val doc = new Document(text)
    val tokens: mutable.Seq[Token] = doc.sentences.flatMap(_.tokens())
    tokens.foreach{ t =>
      val txt: String = t.word()
      val pos: String = t.tag()
      val lemma:String = t.lemma()
      val ner: String = t.ner()
      println((txt,pos,lemma, ner))
    }
  }
}

上記は先程の Example1 と同じ内容の処理になります。 実際に実行して確認してみます。

sbt:Example1> runMain example2.Example2
[warn] Multiple main classes detected.  Run 'show discoveredMainClasses' to see the list
[info] Running example2.Example2
[run-main-0] INFO edu.stanford.nlp.tagger.maxent.MaxentTagger - Loading POS tagger from edu/stanford/nlp/models/pos-tagger/english-left3words/english-left3words-distsim.tagger ... done [1.0 sec].
...
(Stanford,NNP,Stanford,ORGANIZATION)
(University,NNP,University,ORGANIZATION)
(is,VBZ,be,O)
(located,JJ,located,O)
(in,IN,in,O)
(California,NNP,California,LOCATION)
(.,.,.,O)
(It,PRP,it,O)
(is,VBZ,be,O)
(a,DT,a,O)
(great,JJ,great,O)
(university,NN,university,O)
(.,.,.,O)
[success] Total time: 17 s, completed Dec 25, 2017 3:34:25 AM

同じ結果が返ってきています。

Simple API を使用するには "edu.stanford.nlp.simple._" を import します。

コードの書き方について Stanford API と比較すると、一番大きな違いは Stanford API で行っていた、アノテータ(StanfordCoreNLP) の生成と annotate() の実行が無いことです。これらの処理は隠蔽されています。

    val doc = new Document(text)
    val tokens: mutable.Seq[Token] = doc.sentences.flatMap(_.tokens())

処理したいテキストで クラスDocument を生成します。上記の "doc.sentences はメソッドでこのメソッドの中で tokenize, ssplit に対応するアノテータが実行されています。

    tokens.foreach{ t =>
      val txt: String = t.word()
      val pos: String = t.tag()
      val lemma:String = t.lemma()
      val ner: String = t.ner()
      println((txt,pos,lemma, ner))
    }

こちらのコードでは、各アノテーションを取得しています。 例えば POS(PartOfSpeach) 情報を取得する tag() メソッドでは内部で pos に対応するAnnotator を実行します。このように Simple API ではデータが実際に必要になったときにはじめて処理が呼び出される遅延実行をすることで無駄な処理が実行されないようにしています。

ドキュメントでは Simple API のメリットとして以下が上げられています。

  • 直感的なシンタックス
  • Lazy computation (必要になるまで処理が実行されない)
  • ヌルポが起きない(nullを返さない)
  • 高速で頑健なシリアライゼーション(protocol buffers を使用)
  • スレッドセーフ

一方で、カスタマイズがしづらいこと、処理が決定的でない(呼ばれる処理の順番によって使用されるアルゴリズムが異なったりすることで結果が常に同じにはならない)などをデメリットとして上げています。

3. Stanford CoreNLP API と Simple CoreNLP APIの性能比較

Stanford API と Simple API について性能面で違いがあるのか比較してみました。 基本的には上記までの Example1, Example2 と同じ処理(pos, lemma, ner) を大きなテキストデータについて実行し、実行時間を計測してみました。 データは社内にあるデータ (英文)1000件、約2MBのデータになります。 環境は 私の作業用のノートPC(Win10, WSL) 上です。

sbt:Example1> runMain example3.Example3 simple
...
[success] Total time: 154 s, completed Dec 26, 2017 7:35:20 AM

上記のように引数 simple をつけた場合 Simple API をそうでない場合は Stanford API を実行するようにし、3回実行して平均を取りました。

Stanford API Simple API
1回目  168 160
2回目  154 154
3回目  155 150
平均  159.0 154.7

結果を見ると Simple API のほうが少し速いようですが、あまり厳密なテストでもないですし、ほとんど変わらないと考えて良いのでは無いかと思います。

次に少し処理を変えてみます。

Stanford API/ Simple API それぞれのデータを取得するメソッドは以下のようになっています。

Standford API:

  def getResult(document:Annotation):Seq[Result] = {
    val sentences: mutable.Seq[CoreMap] = document.get(classOf[SentencesAnnotation]).asScala
    val tokens: mutable.Seq[CoreLabel] = sentences.flatMap(s => s.get(classOf[TokensAnnotation]).asScala)
    tokens.map(t => Result(t.word(), t.tag(), t.lemma(), t.ner())).toSeq
  }

Simple API:

  def getResult(document:Document):Seq[Result] = {
    val tokens = document.sentences.asScala.flatMap(x => x.tokens().asScala)
    tokens.map(t => Result(t.word(), t.tag(), t.lemma(), t.ner())).toSeq
  }

Result はアノテーションの結果を保存する case class で、以下のように定義されています。

case class Result(txt:String, pos:String, lemma:String = "", ner:String = "")

先程までは pos, lemma, ner という3つのアノテーション結果を取得していましたが、lemmaとnerは使わなかったので pos だけを取得しようとコードを変えたとします。 Stanford API/ Simple API それぞれ

    tokens.map(t => Result(t.word(), t.tag(), t.lemma(), t.ner())).toSeq

となっている部分を

    tokens.map(t => Result(t.word(), t.tag())).toSeq

として 品詞の結果だけを取得するように変更します。修正点はここだけになります。 このように変更したコードを使って再度処理を実行してみます。

Stanford API Simple API
1回目  155 17
2回目  160 16
3回目  157 16
平均  157.3 16.3

今回は結果が大きく異なり Simple API は大幅に速くなりました。 一方 Stanford API は、前とほとんど変わりません。

これは Stanford API の場合 プロパティ "annotators" で処理内容を決めており、データを取得する、取得しないにかかわらず annotate() を実行した時点でこれらの処理がすべて実行されてしまうからです。一方で Simple API ではアノテーション取得時に対応するアノテータがオンデマンドで実行されるので呼び出さなければその処理が実行されないため大幅に処理時間を短縮できています。

もちろん Stanford API でも必要としていない lemma, ner を annotators から抜けば同じような処理時間で処理をすることが可能です。ですが、ついうっかり忘れてしまうと無駄に処理時間がかかってしまいます。

おわりに

以上、Stanford API と Simple API について見てみました。 どちらを使うのかは好みの問題とは思いますが、CLI と同じように使いたい、細かいカスタマイズがしたいなら Stanford API を、あまり細かいところは良いのでとにかく手軽に処理がしたいのなら Simple API という感じでしょうか。また NLP の処理は重いので不必要な処理は実行しないようにしないと無駄に処理時間がかかってしまうので気をつけたほうが良いと思います。

ちょっと時間がなくなってしまったので Spark での CoreNLP の使用については次回に書きたいと思います。

Copyright © astamuse company, ltd. all rights reserved.