日本語NLPライブラリ GiNZA で遊ぶ

つい最近、リクルートと国立国語研究所の共同研究のアウトプットとしNLPライブラリ GiNZA が公開されました。今回はこのGiNZAをお試ししてみます。

www.recruit.co.jp

GiNZA

GiNZAは日本語の自然言語処理の統合ライブラリです。

megagonlabs.github.io

spaCyのフレームワークに乗っかっており、また、形態素解析でSudachi / SudachiPyを使っていることが特徴です。 (これらのライブラリは過去の投稿でも触れてきました。個人的にもspaCyの日本語への適用を期待していましたので、嬉しい限りです。)

インストール

最新リリースは以下でインストールできます。

$ pip install "https://github.com/megagonlabs/ginza/releases/download/v1.0.2/ja_ginza_nopn-1.0.2.tgz"

コンソールから使う

コンソールから使う場合 python -m spacy.lang.ja_ginza.cli で起動します。"友人・我孫子とスカイツリーで美味いスパゲティを食った。" を入力すると、CoNLL-U形式で解析結果が出力されます。

品詞や標準表現 (3列目, "スパゲティ" -> "スパゲッティ") 、係り受け (7列目のIDが関係を表し、"友人"-> "我孫子"、"美味い" -> "スパゲッティ"となっている) などが正しく解析できてますね。 (CoNLL-Uの解析結果の詳細はこちら) 。Sudachiが使われていることもあって、洗練されていそうです。

$ python -m spacy.lang.ja_ginza.cli
Loading model 'ja_ginza_nopn'
mode is C
disabling sentence separator
友人・我孫子とスカイツリーで美味いスパゲティを食った。
# text = 友人・我孫子とスカイツリーで美味いスパゲティを食った。
1       友人    友人    NOUN    名詞-普通名詞-一般      _       3       compound        _       SpaceAfter=No|NP_B
2       ・      ・      PUNCT   補助記号-一般   _       3       punct   _       SpaceAfter=No
3       我孫子  我孫子  PROPN   名詞-固有名詞-地名-一般 _       5       nmod    _       SpaceAfter=No|NP_B|NE=LOC_B
4       と      と      ADP     助詞-格助詞     _       3       case    _       SpaceAfter=No
5       スカイツリー    スカイツリー    PROPN   名詞-固有名詞-一般      _       10      nmod    _       SpaceAfter=No|NP_B
6       で      で      ADP     助詞-格助詞     _       5       case    _       SpaceAfter=No
7       美味い  旨い    ADJ     形容詞-一般     _       8       amod    _       SpaceAfter=No
8       スパゲティ      スパゲッティ    NOUN    名詞-普通名詞-一般      _       10      obj     _       SpaceAfter=No|NP_B
9       を      を      ADP     助詞-格助詞     _       8       case    _       SpaceAfter=No
10      食っ    食う    VERB    動詞-一般       _       0       root    _       SpaceAfter=No
11      た      た      AUX     助動詞  _       10      aux     _       SpaceAfter=No
12      。      。      PUNCT   補助記号-句点   _       10      punct   _       SpaceAfter=No

Pythonから使う

Pythonから使う場合は spacy.load('ja_ginza_nopn') とすることで、GiNZAのモデルを選択できます。あとはspaCyで英文を扱うのと同じように解析できます。

import spacy

# モデルをロード
nlp = spacy.load('ja_ginza_nopn')

# 解析
doc = nlp("""友人・我孫子とスカイツリーで美味いスパゲティを食った。""")

for sent in doc.sents:
    for token in sent:
        print(token.i, token.orth_, token.lemma_, token.pos_, token.dep_, token.head.i)

# 0 友人 友人 NOUN compound 2
# 1 ・ ・ PUNCT punct 2
# 2 我孫子 我孫子 PROPN nmod 4
# 3 と と ADP case 2
# 4 スカイツリー スカイツリー PROPN nmod 9
# 5 で で ADP case 4
# 6 美味い 旨い ADJ amod 7
# 7 スパゲティ スパゲッティ NOUN obj 9
# 8 を を ADP case 7
# 9 食っ 食う VERB root 9
# 10 た た AUX aux 9
# 11 。 。 PUNCT punct 9

spaCyのインタフェースが実装されていますので、今度は固有表現を抽出してみます (以前の投稿も参考にしてみてください) 。

結果としては、"我孫子"が場所 (LOC) として認識されていたり、"スカイツリー"が抽出できていなかったり、もうちょっと、かもしれません。"東京スカイツリータワー"に変えると、ちゃんと場所として抽出されました。

# 固有表現
for ent in doc.ents:
    print(ent.text, ent.label_, ent.start_char, ent.end_char)

# 我孫子 LOC 3 6

まとめ

今回は最近登場したGiNZAを触ってみました。

Python: Redisで複数の値を操作する (mset, mget, scan, delete)

小ネタです。

以前、PythonからRedisを操作するredis-pyについて紹介しましたが、今回はこのredis-pyで複数の値を一括して値を設定・削除する方法についてです。

準備

こちら↓の記事を参考にRedisコンテナの追加・redis-pyのインストールを行ってください。

ohke.hateblo.jp

で、接続します。

import redis

conn = redis.StrictRedis(host='localhost', port=6379)

複数の値を更新 (mset)

msetメソッドで、引数に辞書を渡すことで一括更新できます。成功すると"True"が返ります。

ret = con.mset({'key1': 'value1', 'key2': 'value2'})
print(ret)  # True

複数の値を取得 (mget, scan)

msetと対になるmgetメソッドにキーのリストを渡すと、値を一括取得できます。

values = con.mget(['key1', 'key2'])
print(values)  # [b'value1', b'value2']

あるパターンのキー (例えば"key*") の値を取得する場合、最初にキーをscanしてからmgetします。
setが返ってきますが、2つ目がキーのリストとなります。1つ目の値は次に読み込むべきインデックスです (count引数が設定されていない場合は0) 。

keys = con.scan(match='key*')
print(keys)  # (0, [b'key2', b'key1'])
# keys = con.scan(match='key*', count=1)
# print(keys)  # (1, [b'key2'])

values = con.mget(keys[1])
print(values)  # [b'value2', b'value1']

複数の値を削除 (delete)

値の削除はdeleteメソッドで、引数にキーのリストを渡すことで削除できます。

_, keys = con.scan(match='key*')

ret = con.delete(*keys)
print(ret)  # 2
print(con.mget(keys))  # [None, None]

Python: more-itertoolsで複雑なイテレーションを簡単に実装する

開発や分析で「タプルのリストからタプルの1つ目の要素だけを取り出したい」「リストの要素を3つずつ処理したい」といったことがちょくちょく起こります。Pythonでこうしたケースに便利なライブラリ mote_itertools を紹介します。

github.com

インストール

今回は7.0.0を使います。

$ pip install -U more-itertools
Successfully installed more-itertools-7.0.0

実装

今回紹介するメソッドをインポートしておきます。APIの詳細はこちらです。

from more_itertools import chunked, flatten, distribute, divide, split_at, split_before, split_after, unzip, windowed

リストからx個ずつ取り出したい (chunked)

chunked_iter = chunked([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 4)
print(list(chunked_iter))  # [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9]]

リストをx個へ均等に分割したい (divide, distributed)

iter1, iter2, iter3 = list(divide(3, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]))
print(list(iter1), list(iter2), list(iter3))  # [0, 1, 2, 3] [4, 5, 6] [7, 8, 9]

x個置きに分割する場合は、distributedを使うと良いです。

iter1, iter2, iter3 = list(distribute(3, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]))
print(list(iter1), list(iter2), list(iter3))  # [0, 3, 6, 9] [1, 4, 7] [2, 5, 8]

リストのリスト・タプルのリストを1つのリストに展開したい (flatten)

flatten_iter = flatten([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9]])  # [(0, 1, 2, 3), (4, 5, 6, 7), (8, 9)] でもOK
print(list(flatten_iter)) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

リスト内の特定の要素で分割したい (split_at, split_before, split_after)

split_atを使い、4で割り切れる要素で分割しています。分割に使った要素 (0, 4, 8) は除去されます。

splitted_iters = split_at([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], lambda e: e%4 == 0)
print(list(splitted_iters))  # [[], [1, 2, 3], [5, 6, 7], [9]]

分割に使った要素を残す場合、split_beforeまたはsplit_afterを使います。分割した要素が、split_beforeでは先頭、split_afterでは末尾になるように分割されます。

splitted_iters = split_before([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], lambda e: e%4 == 0)
print(list(splitted_iters))  # [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9]]

splitted_iters = split_after([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], lambda e: e%4 == 0)
print(list(splitted_iters))  # [[0], [1, 2, 3, 4], [5, 6, 7, 8], [9]]

タプルのリストからタプル要素ごとのリストにしたい (unzip)

要素が無い場合、以降の要素が取得できなくなりますので、注意です。

name_iter, age_iter = unzip([('tanaka', 28), ('suzuki', 33), ('yamada', 23), ('sato', )])
print(list(name_iter))  # ['tanaka', 'suzuki', 'yamada', 'sato']
print(list(age_iter))  # [28, 33, 23]

リストから範囲をずらしながら取り出したい (windowed)

4個ずつずらしながらイテレーションしてます。

windowed_iter = windowed([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 4)
print(list(windowed_iter))  # [(0, 1, 2, 3), (1, 2, 3, 4), (2, 3, 4, 5), (3, 4, 5, 6), (4, 5, 6, 7), (5, 6, 7, 8), (6, 7, 8, 9)]

ずらし幅はstepで指定できます。

windowed_iter = windowed([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 4, step=2)
print(list(windowed_iter))  # [(0, 1, 2, 3), (2, 3, 4, 5), (4, 5, 6, 7), (6, 7, 8, 9)]

まとめ

同じことは二重forループやlambdaを組み合わせれば実現できるのですが、コードの見通しが悪くなってしまってなんだかなあ、という問題を解決できるので、おすすめのライブラリです。