はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

spaCy2.3の日本語標準対応にのっかってホットワード抽出の真似事(ただし候補ワード・フレーズ抜き出しまで)

spaCyがver 2.3になって、デフォルトで日本語に対応したようです。

spacy.io

ということで、何日か前に、GiNZAスゲーとなった感謝の気持ちは忘れないながらも、フリーライダーな私としては、裸のspaCy2.3に乗り換えて、まずは雰囲気を掴んでみようということで使ってみることにしました。

なお、この記事を書き始めた時点では、以前、次の記事で試してみたGiNZAは、spaCyのver2.2までの対応でした。

itdepends.hateblo.jp

よって、spaCyをver2.3にアップグレードすると、(なんとなく動作している雰囲気はありますが、)それまで動作していた古いバージョンのGiNZAは公式には動作対象外となるのでご注意ください。

(ちなみに、この記事を公開した時点ですでに、GiNZAはver4となり、ベースとなるspaCyは2.3に対応したようです。また、上記では、spaCy2.3のデフォルトの日本語まわりはGiNZAと無関係かのような言い方をしていますが、公式でダウンロードできるモデルはGiNZA同梱のモデルと同様のものに見えなくないですので、なんらか繋がりはあるのかもしれません。単に私の探索力の問題で正式な情報源は未確認ですが...)

https://github.com/megagonlabs/ginza/blob/develop/docs/index.md#breaking-changes-in-v40 https://www.recruit.co.jp/newsroom/2020/0817_18783.html

■目次

spaCyを使ってみるモチベーション/ユースケースのようなもの

言い忘れましたが(またこの後の文章を見れば見る人は分かると思いますが)、筆者は特に自然言語処理のエキスパートでもなんでもありません。謙遜でもありません。

しかし、ちょっとこの分野の応用のビジネスの場で、形を問わずブサイクでもなんらか成果を出したい世界に所属しているので、(真理の探究というよりは)手間をかけずになんちゃってでも成果が出ると嬉しいという方向でのある意味真面目なモチベーションを持ち合わせています。

具体的には、(実際の案件からは多少デフォルメ&ぼやかしているものの)次のようなものです。

モチベーション

あるドキュメントやドキュメント群に頻出する、あるいはこれらのドキュメント群のある程度の塊全体の特徴をよく表したフレーズを効率的に抜き出したいです。

つぶやきのホットワードがイメージに近いでしょうか。

なお、実際のホットワードのように、ここでいうフレーズは、TwitterInstagramハッシュタグやトレンドでみられるような、単語以上、2、3程度の文節からなる複合ワードをイメージしています。

spaCyでやりたいこと

キーセンテンスや要約とはちょっと違いますので、文書要約のための有名ライブラリは応用はできなくないかもしれませんが、守備範囲ど真ん中ではなさそうです。

ディープラーニングで(なんらか仕組みを借用するならまだしも、自分でモデル開発してまで)というのは少し目的に対してヘビーそうです。

一方、対象とするドキュメント群が、構造化こそされていないもののある程度の基準・秩序でメンテされていることがあてにできる場合があります。

例えば、ブログは難しいですが、ニュースだとライティングや用語、センテンスなどに一定の暗黙のルールが潜んでいると思います。

このようにある程度ドキュメントの性質があてにできる場合、ひとつの仮説として、次のようなややアドホックな手法でもある程度良い結果が得られるのではないかと考えることにしました。

  1. ストップワードを取り除く
  2. また、(文学的味わいのようなものは薄れてしまいますが)過剰な修飾に該当する単語を取り除く → ここの加減が難しいのではと思う一方、深層学習などで頑張るよりも、単語の依存関係や品詞が分かるなら今回の目的の範囲でもぼちぼち都合が良い結果が得られるのではないか。
  3. 残った単語、単語と単語を意味の繋がりをある程度担保したフレーズを形成、これらの長すぎず短すぎずの集合にした、元のドキュメントの純度・濃度*1を上げたデータに変換。

ある程度純度が高いデータであれば、出現数ランキングやなんらか相対的に出現数が多い((ややハンマー釘脳かもしれませんが、筆者はElasticsearchのSignificant_termsを気に入っており、これの利用が念頭にあります。)といった、比較的平易な集計でいい感じのホットワード一覧が得られたら良いな。

ということで、うまくいくかどうかはともかく、spaCyで可能になる、日本語の品詞分類や文中の単語間の依存関係をよろしく参照しつつ、まずはドキュメント全体の性質をなんらか反映しているような単語やフレーズをヒューリスティックに抜き出すスクリプトを試して勉強してみようというのがモチベーションです。

なお、一瞬だけGiNZAの話に戻るのですが、この記事のモチベーションとなった上記の試行例相当は、GiNZAのver4で導入された、文節を意識したAPIでよりスマートに実装できるのでは?という気がしております。

もし、たまたまこの拙記事を検索で訪れた方で、てめー(私)のオレオレ方式はいいから、もっと由緒正しいアプローチをしりたいというかたは、GiNZAのver4をご検討されるのが良いかもしれない...ということを素直に申しておきます。

github.com

※ 注目: bunsetu_XXXXやphrase他のAPIs

spaCy2.3インストール

公式を見てください... というところですが、手元のmacですとこんな感じでした。 今回は、サイズが37MBの大中小でいうところの「中(_md)」のモデルにしています。

pip3 install spacy

python3 -m spacy download ja_core_news_md

https://spacy.io/usage#quickstart https://spacy.io/models/ja#ja_core_news_md

インポート方法の例

import spacy
nlp = spacy.load('ja_core_news_md')

spaCy 前提知識・予備知識

いきなりオレオレスクリプトをベタ張りするのも気が引けたので、私の感じている、spaCyの特徴的な使い方の例などを少し述べます。

体系的、網羅的ではありませんが、spaCyをつまみ食いしたい人向けのちょっとしたチュートリアルになっています。

公式ドキュメント

spacy.io

APIのトップページと、左メニューのDoc、Tokenのセクションを最初に見るのが良いかと思われます。

spaCyがわかった気になるスクリプト

import spacy
nlp = spacy.load('ja_core_news_md')

doc = nlp('昔々、あるところにおじいさんとおばあさんが住んでいました。ある日おじいさんは山に芝刈りに、おばあさんは川に選択に行きました。')

sentences = [s for s in doc.sents] #センテンス(1文ずつ)のリストが得られる。
ents = [e for e in doc.ents] # 固有表現のリストが得られる。 https://spacy.io/api/annotation#named-entities
tokens = [t for t in doc] # トークンの一覧 (私自身は、一度この形式にして取り捌くのが体に馴染む気がしています。)

for i in tokens:
   print(
       '',
       i.text,
       i._.reading,
       i.i,
       #Universal Part-of-speech  https://spacy.io/api/annotation#pos-tagging    https://spacy.io/usage/adding-languages#tag-map
       i.pos_, 
       #Sudachi/SudachiPyが利用されているとのことで、どこかに品詞の分類の公式一覧ドキュメントがあると思われるのだがよく分からず。→ 歴史的なところから想像すると、「IPA品詞体系、UniDic品詞体系、
       i.tag_, 
       i.lemma_,
       i.orth_,
       i._.inf,
       # Universal Dependencies:  日本語を対象にした例だと、ググって出てくる、こちらのサーベイを参考にさせていただいています→ https://www.jstage.jst.go.jp/article/jnlp/26/1/26_3/_pdf
       # spaCy公式での説明→ https://spacy.io/api/annotation#dependency-parsing  ※Universal Dependency Labelsというところのアコーディオンをクリックすると一覧が表示される。
       i.dep_, 
       i.head.text,
       '',
       sep='|'
   )

出力例

i.text i._.reading i.i i.pos_ i.tag_ i.lemma_ i.orth_ i._.inf i.dep_ i.head.text
昔々 ムカシムカシ 0 NOUN 名詞-普通名詞-副詞可能 昔々 昔々 , obl 住ん
1 PUNCT 補助記号-読点 , punct 昔々
ある アル 2 DET 連体詞 或る ある , nmod ところ
ところ トコロ 3 NOUN 名詞-普通名詞-副詞可能 ところ , obl 住ん
4 ADP 助詞-格助詞 , case ところ
5 NOUN 接頭辞 , compound さん
じい ジイ 6 NOUN 名詞-普通名詞-一般 じい , compound さん
さん サン 7 NOUN 接尾辞-名詞的-一般 さん さん , nmod さん
8 ADP 助詞-格助詞 , case さん
9 NOUN 接頭辞 , compound さん
ばあ バア 10 NOUN 名詞-普通名詞-一般 ばあ , compound さん
さん サン 11 NOUN 接尾辞-名詞的-一般 さん さん , nsubj 住ん
12 ADP 助詞-格助詞 , case さん
住ん スン 13 VERB 動詞-一般 住む 住ん 五段-マ行,連用形-撥音便 ROOT 住ん
14 CCONJ 助詞-接続助詞 , mark 住ん
15 VERB 動詞-非自立可能 居る 上一段-ア行,連用形-一般 aux 住ん
まし マシ 16 AUX 助動詞 ます まし 助動詞-マス,連用形-一般 aux 住ん
17 AUX 助動詞 助動詞-タ,終止形-一般 aux 住ん
18 PUNCT 補助記号-句点 , punct 住ん
ある アル 19 DET 連体詞 或る ある , nmod
20 NOUN 名詞-普通名詞-副詞可能 , nmod さん
21 NOUN 接頭辞 , compound さん
じい ジイ 22 NOUN 名詞-普通名詞-一般 じい , compound さん
さん サン 23 NOUN 接尾辞-名詞的-一般 さん さん , nsubj 行き
24 ADP 助詞-係助詞 , case さん
ヤマ 25 NOUN 名詞-普通名詞-一般 , obl 芝刈り
26 ADP 助詞-格助詞 , case
芝刈り シバカリ 27 NOUN 名詞-普通名詞-一般 芝刈り 芝刈り , advcl 行き
28 ADP 助詞-格助詞 , case 芝刈り
29 PUNCT 補助記号-読点 , punct 芝刈り
30 NOUN 接頭辞 , compound さん
ばあ バア 31 NOUN 名詞-普通名詞-一般 ばあ , compound さん
さん サン 32 NOUN 接尾辞-名詞的-一般 さん さん , nsubj 行き
33 ADP 助詞-係助詞 , case さん
カワ 34 NOUN 名詞-普通名詞-一般 , obl 行き
35 ADP 助詞-格助詞 , case
選択 センタク 36 NOUN 名詞-普通名詞-サ変可能 選択 選択 , obl 行き
37 ADP 助詞-格助詞 , case 選択
行き イキ 38 VERB 動詞-非自立可能 行く 行き 五段-カ行,連用形-一般 ROOT 行き
まし マシ 39 AUX 助動詞 ます まし 助動詞-マス,連用形-一般 aux 行き
40 AUX 助動詞 助動詞-タ,終止形-一般 aux 行き
41 PUNCT 補助記号-句点 , punct 行き

Universal Dependenciesについて

pos(Part Of Speech)で示される品詞や、tagで示される日本語の慣例を加味した品詞のタグ付けはなんとなくみてみると分かるところがあります。

一方、上記でいうところの「dep(Token.dep_)」はなんなんでしょうか。

これは単語と単語の関係、ある単語に対してどういう役割を担っているかといったものを表すもののようで、「係り受け」や「Universal Dependencies」と言われるもの(もしくはそれらに関するもの)のようです。

Universal Dependenciesについての、世界の御本尊、日本語に関するものの本丸がどちらに御座すのかは不勉強で分かっていませんが、

例えば、「Token.dep_」が

acl であれば → clausal modifier of noun (adjectival clause) ということで

美しい花」でいうと、「美しい」は形容詞であると同時に、Token.dep_については「acl」で、名詞を形容する(修飾する)役割であると分析されるとともに、「Token.head」は、修飾先の「花」のTokenを指し示すということになります。

Universal Dependencies(日本語関連)

日本語を対象にした例だと、ググって出てくる、こちらのサーベイを参考にさせていただいています。

https://www.jstage.jst.go.jp/article/jnlp/26/1/26_3/_pdf

spaCy公式でのUniversal Dependencies 説明

spacy.io

※Universal Dependency Labelsというところのアコーディオンをクリックすると一覧が表示される。

類似度

後述の例では特に用いていませんが、単語ベクトルなどの数値も保持されており、2つの単語の類似判定もできるようです。

(私は使いこなせていませんが)spaCyはもっといろいろできるよという可能性をお伝えするのに少しだけふれておきます。

gf = None
for i in tokens:
   if  i.text == 'じい':
       gf = i
       break

from operator import itemgetter
sorted([[i.text,i.similarity(gf)] for i in tokens],key=itemgetter(1),reverse=True)

↓ 出力例 (文例の中の単語では、「じい」に似ている(と評価された)のは「ばあ」ということが分かる)

[['じい', 1.0],
 ['じい', 1.0],
 ['ばあ', 0.4911714],
 ['ばあ', 0.4911714],
 ['と', 0.30002618],
 ['、', 0.28635746],
 ['、', 0.28635746],
 ['は', 0.25570428],
 ['は', 0.25570428],
 ['い', 0.23832183],
 ['に', 0.21961989],
 ['に', 0.21961989],
 ['に', 0.21961989],
 ['に', 0.21961989],
 ['に', 0.21961989],
 ['で', 0.21929],
 ['た', 0.20998874],
 ['た', 0.20998874],
 ['が', 0.19864015],
 ['。', 0.17501514],
 ['。', 0.17501514],
...

nlp.pipeで複数ドキュメントを一括解析

上の方の例でしめしたように、nlp('テキスト')で、解析後のDocオブジェクトが得られるので、ググると、これを使ったサンプルスクリプト例が多く見られます。

しかし、複数ドキュメントを扱う場合は、nlp.pipeで並列化するのがベストプラクティスのようです。

docs = nlp.pipe([
    '昔々、あるところにおじいさんとおばあさんが住んでいました。ある日おじいさんは山に芝刈りに、おばあさんは川に洗濯に行きました。',
    'むかしむかし、足柄山の山奥に、金太郎という名前の男の子が住んでいました。',
    '海の側の村に、浦島太郎という男の若者が暮らしていました。'
])


doc1 = nlp('昔々、あるところにおじいさんとおばあさんが住んでいました。ある日おじいさんは山に芝刈りに、おばあさんは川に洗濯に行きました。')
doc2 = nlp('むかしむかし、足柄山の山奥に、金太郎という名前の男の子が住んでいました。')
とするより高速
(実際にやってみた例としては、2倍ほど高速でした。)

nlp.pipelineについて(今回は使ってませんが...)

公式のチュートリアルがわかりやすい。

course.spacy.io

独自のパイプ処理を追加できるし、ある要件の場合は不要かな〜というパイプを外すことができます。

例えば、nlp.remove_pipe でdoc.entsで取得できる固有値表現を取り除くと、手元の環境だと30%ぐらい高速化されました。

また、今回の例では、今思えば、後述の内容をpipelineの一つとして実現するのが、spaCyのイケてる利用方法だったかもしれないと今気づきました。

Matcher、PhraseMatcherについて(今回は使ってませんが...)

実は、spaCyでは、(ループやイテレータで回すような逐次的な記述ではなく)宣言的な記述により、品詞の組み合わせや特定の意味合いの語の並びといったルールベースの検索/テキスト抽出が可能です。

後述の例では、対象とするドキュメント例はアドホックな分析の方がしっくりくると、(個人的な)経験則に従うことにしたので、用いていませんが、spaCyではこのようなことも可能ということで触れておきます。

spacy.io

ここでは、ルールベース(という言い方が正確かは自身がありませんが)、通常のgrep分かち書き考慮の全文検索型との対比でいうと、次のようなものです。

全文検索型:

「東京都」でその文書における出現位置を(「京都」はヒットさせずに)検索

vs

ルールベースの例:

  • 「地名」を含むようなセンテンスを検索
  • 「東京都の...」のように、「地名」を所有格としたセンテンスを検索
  • 文書中の「地名」「人」「動詞」という構文をとるセンテンスを検索
  • 「地名」が主にになっているセンテンスを検索

(もちろん、「東京都」という単語を含むという検索も可能です。)

今回は(私の理解不足を差し引いても)考えがあってMatcherを使わなかったのですが、やっぱり使っておけばよかったかなとも、この記事を仕上げるにあたって思うところもあります。

ホットワード集計の元ネタ抽出(やってみた!の例)

随分、前置きが長くなりましたが、ホットワード集計の元ネタ抽出の「やってみた!」例です。

具体例を出せませんがある程度秩序のあるドキュメント群を念頭においてあるのと、どちらにせよ後で統計的集計を行うので、できるだけ好みのワードやフレーズが残るようにするものの、「不自然なものが絶対混在しないようにする」というところは目指していません。

(1) 以下の4カテゴリごとに抽出

  1. 名詞: 桜, 花
  2. 動詞/形容詞: 美しい
  3. 名詞フレーズ(装飾語あり): 美しい桜,美しい桜の花
  4. 動詞・形容詞フレーズ(前方の装飾あり、語尾の自然な活用): 桜が美しい, 美しい桜が咲く

品詞の違いや単語か複合語を意識せず素直にカウントする案もありますが、ある文書の特徴を表す表現が名詞や固有名詞など特定の品詞に強く出る場合がある一方で、逆に全体から見ると数は少ないものの特定の品詞やフレーズの中では件数が多く、それがドキュメント全体の傾向を示すということもままあると感じています。

感覚的には、名詞はその文書が対象としているドメインを象徴することが多く、動詞や形容詞はその文書の中で発生しているなんらかの動きやストーリーを浮き彫りにする傾向があるといえるのではないでしょうか。

というのが、この4つのカテゴリごとに単語・フレーズ抽出を行うこととした理由です。

(2)名詞フレーズと動詞・形容詞フレーズの抽出

それぞれ、下図のような考え方としました。(ちょっと図中の文例があまりリアルでないかも...頑張って行間を読んでください。)

f:id:azotar:20200819232803p:plain

ポイントは次のとおり。

  1. 名詞フレーズ:名詞を見つけたらその前方の修飾の語句のもっともらしい部分から続けてフレーズを作る。
  2. 動詞・形容詞フレーズ:動詞あるいは形容詞を見つけたらその前方にいる目的語などと繋げて、何をしようとしているか、何の様子かをシンプルなワンフレーズとして抜き出す。

名詞フレーズは名詞の近くほどその名詞の意味を深める語が配置される傾向があるのでそれを意識したフレーズを抜き出す一方、動詞や形容詞は近くに副詞が配置されることが多く、必ずしも近くに配置される単語がその動詞や形容詞の主題を表さな い、加えてその分、該当の動詞や形容詞との「依存関係(dep)」が強い単語に絞ってやるいった味付けをしています。

また、実際のソースコードではもう少し味付け((いくつかベンチマークとなる文書を読み込ませてみて、紛らわしいと感じた例の例外処理をしている部分もあります。

なお、名詞フレーズと動詞・形容詞フレーズに分けた理由は、前述の名詞と動詞・形容詞に大きく分けたのと同じ理由です。

どちらかと言えば、おおきな方針のもと、おおよそ「フレーズらしい形になっているかな」というチャンクを抜き出しておき、最後にもういちどだけ、目についたパターンを具体的に指定して除外する。それでも残るような微妙なフレーズ例はいずれにせよ、のちの統計処理で自ずとふるいにかけられるだろうというスタンスです。

spaCyの品詞分類やUDを活用したオレオレホットワード集計用クレンジングデータ出力サンプル

spaCyの練習

実行結果

入力テキスト

f:id:azotar:20200821223504p:plain

抽出結果

f:id:azotar:20200821223521p:plain

アドホックな方法の割に、意外にいいところまで来たような気もします。

一方、簡単に済まそうとして、逆にステップ数が多くなったのと、後で見た時に自分でも分からなそう...というところはありますが、自分の中ではなかなか勉強になったので満足しています。

spaCy参考URL

spaCyに慣れるのに参考にさせていただいたページ

qiita.com

blog.imind.jp

*1:自然言語関係の情報処理でより適切な用語があるかもしれませんが、よく知らないのでこのような表現としました。