spaCyがver 2.3になって、デフォルトで日本語に対応したようです。
ということで、何日か前に、GiNZAスゲーとなった感謝の気持ちは忘れないながらも、フリーライダーな私としては、裸のspaCy2.3に乗り換えて、まずは雰囲気を掴んでみようということで使ってみることにしました。
なお、この記事を書き始めた時点では、以前、次の記事で試してみたGiNZAは、spaCyのver2.2までの対応でした。
よって、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を使ってみるモチベーション/ユースケースのようなもの
- spaCy2.3インストール
- spaCy 前提知識・予備知識
- 類似度
- nlp.pipeで複数ドキュメントを一括解析
- ホットワード集計の元ネタ抽出(やってみた!の例)
- spaCy参考URL
spaCyを使ってみるモチベーション/ユースケースのようなもの
言い忘れましたが(またこの後の文章を見れば見る人は分かると思いますが)、筆者は特に自然言語処理のエキスパートでもなんでもありません。謙遜でもありません。
しかし、ちょっとこの分野の応用のビジネスの場で、形を問わずブサイクでもなんらか成果を出したい世界に所属しているので、(真理の探究というよりは)手間をかけずになんちゃってでも成果が出ると嬉しいという方向でのある意味真面目なモチベーションを持ち合わせています。
具体的には、(実際の案件からは多少デフォルメ&ぼやかしているものの)次のようなものです。
モチベーション
あるドキュメントやドキュメント群に頻出する、あるいはこれらのドキュメント群のある程度の塊全体の特徴をよく表したフレーズを効率的に抜き出したいです。
つぶやきのホットワードがイメージに近いでしょうか。
なお、実際のホットワードのように、ここでいうフレーズは、TwitterやInstagramのハッシュタグやトレンドでみられるような、単語以上、2、3程度の文節からなる複合ワードをイメージしています。
spaCyでやりたいこと
キーセンテンスや要約とはちょっと違いますので、文書要約のための有名ライブラリは応用はできなくないかもしれませんが、守備範囲ど真ん中ではなさそうです。
ディープラーニングで(なんらか仕組みを借用するならまだしも、自分でモデル開発してまで)というのは少し目的に対してヘビーそうです。
一方、対象とするドキュメント群が、構造化こそされていないもののある程度の基準・秩序でメンテされていることがあてにできる場合があります。
例えば、ブログは難しいですが、ニュースだとライティングや用語、センテンスなどに一定の暗黙のルールが潜んでいると思います。
このようにある程度ドキュメントの性質があてにできる場合、ひとつの仮説として、次のようなややアドホックな手法でもある程度良い結果が得られるのではないかと考えることにしました。
- ストップワードを取り除く
- また、(文学的味わいのようなものは薄れてしまいますが)過剰な修飾に該当する単語を取り除く → ここの加減が難しいのではと思う一方、深層学習などで頑張るよりも、単語の依存関係や品詞が分かるなら今回の目的の範囲でもぼちぼち都合が良い結果が得られるのではないか。
- 残った単語、単語と単語を意味の繋がりをある程度担保したフレーズを形成、これらの長すぎず短すぎずの集合にした、元のドキュメントの純度・濃度*1を上げたデータに変換。
ある程度純度が高いデータであれば、出現数ランキングやなんらか相対的に出現数が多い((ややハンマー釘脳かもしれませんが、筆者はElasticsearchのSignificant_termsを気に入っており、これの利用が念頭にあります。)といった、比較的平易な集計でいい感じのホットワード一覧が得られたら良いな。
ということで、うまくいくかどうかはともかく、spaCyで可能になる、日本語の品詞分類や文中の単語間の依存関係をよろしく参照しつつ、まずはドキュメント全体の性質をなんらか反映しているような単語やフレーズをヒューリスティックに抜き出すスクリプトを試して勉強してみようというのがモチベーションです。
なお、一瞬だけGiNZAの話に戻るのですが、この記事のモチベーションとなった上記の試行例相当は、GiNZAのver4で導入された、文節を意識したAPIでよりスマートに実装できるのでは?という気がしております。
もし、たまたまこの拙記事を検索で訪れた方で、てめー(私)のオレオレ方式はいいから、もっと由緒正しいアプローチをしりたいというかたは、GiNZAのver4をご検討されるのが良いかもしれない...ということを素直に申しておきます。
※ 注目: 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をつまみ食いしたい人向けのちょっとしたチュートリアルになっています。
公式ドキュメント
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(日本語関連)
日本語を対象にした例だと、ググって出てくる、こちらのサーベイを参考にさせていただいています。
spaCy公式でのUniversal Dependencies 説明
※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について(今回は使ってませんが...)
公式のチュートリアルがわかりやすい。
独自のパイプ処理を追加できるし、ある要件の場合は不要かな〜というパイプを外すことができます。
例えば、nlp.remove_pipe でdoc.entsで取得できる固有値表現を取り除くと、手元の環境だと30%ぐらい高速化されました。
また、今回の例では、今思えば、後述の内容をpipelineの一つとして実現するのが、spaCyのイケてる利用方法だったかもしれないと今気づきました。
Matcher、PhraseMatcherについて(今回は使ってませんが...)
実は、spaCyでは、(ループやイテレータで回すような逐次的な記述ではなく)宣言的な記述により、品詞の組み合わせや特定の意味合いの語の並びといったルールベースの検索/テキスト抽出が可能です。
後述の例では、対象とするドキュメント例はアドホックな分析の方がしっくりくると、(個人的な)経験則に従うことにしたので、用いていませんが、spaCyではこのようなことも可能ということで触れておきます。
ここでは、ルールベース(という言い方が正確かは自身がありませんが)、通常のgrepや分かち書き考慮の全文検索型との対比でいうと、次のようなものです。
全文検索型:
「東京都」でその文書における出現位置を(「京都」はヒットさせずに)検索
vs
ルールベースの例:
- 「地名」を含むようなセンテンスを検索
- 「東京都の...」のように、「地名」を所有格としたセンテンスを検索
- 文書中の「地名」「人」「動詞」という構文をとるセンテンスを検索
- 「地名」が主にになっているセンテンスを検索
(もちろん、「東京都」という単語を含むという検索も可能です。)
今回は(私の理解不足を差し引いても)考えがあってMatcherを使わなかったのですが、やっぱり使っておけばよかったかなとも、この記事を仕上げるにあたって思うところもあります。
ホットワード集計の元ネタ抽出(やってみた!の例)
随分、前置きが長くなりましたが、ホットワード集計の元ネタ抽出の「やってみた!」例です。
具体例を出せませんがある程度秩序のあるドキュメント群を念頭においてあるのと、どちらにせよ後で統計的集計を行うので、できるだけ好みのワードやフレーズが残るようにするものの、「不自然なものが絶対混在しないようにする」というところは目指していません。
(1) 以下の4カテゴリごとに抽出
- 名詞: 桜, 花
- 動詞/形容詞: 美しい
- 名詞フレーズ(装飾語あり): 美しい桜,美しい桜の花
- 動詞・形容詞フレーズ(前方の装飾あり、語尾の自然な活用): 桜が美しい, 美しい桜が咲く
品詞の違いや単語か複合語を意識せず素直にカウントする案もありますが、ある文書の特徴を表す表現が名詞や固有名詞など特定の品詞に強く出る場合がある一方で、逆に全体から見ると数は少ないものの特定の品詞やフレーズの中では件数が多く、それがドキュメント全体の傾向を示すということもままあると感じています。
感覚的には、名詞はその文書が対象としているドメインを象徴することが多く、動詞や形容詞はその文書の中で発生しているなんらかの動きやストーリーを浮き彫りにする傾向があるといえるのではないでしょうか。
というのが、この4つのカテゴリごとに単語・フレーズ抽出を行うこととした理由です。
(2)名詞フレーズと動詞・形容詞フレーズの抽出
それぞれ、下図のような考え方としました。(ちょっと図中の文例があまりリアルでないかも...頑張って行間を読んでください。)
ポイントは次のとおり。
- 名詞フレーズ:名詞を見つけたらその前方の修飾の語句のもっともらしい部分から続けてフレーズを作る。
- 動詞・形容詞フレーズ:動詞あるいは形容詞を見つけたらその前方にいる目的語などと繋げて、何をしようとしているか、何の様子かをシンプルなワンフレーズとして抜き出す。
名詞フレーズは名詞の近くほどその名詞の意味を深める語が配置される傾向があるのでそれを意識したフレーズを抜き出す一方、動詞や形容詞は近くに副詞が配置されることが多く、必ずしも近くに配置される単語がその動詞や形容詞の主題を表さな い、加えてその分、該当の動詞や形容詞との「依存関係(dep)」が強い単語に絞ってやるいった味付けをしています。
また、実際のソースコードではもう少し味付け((いくつかベンチマークとなる文書を読み込ませてみて、紛らわしいと感じた例の例外処理をしている部分もあります。
なお、名詞フレーズと動詞・形容詞フレーズに分けた理由は、前述の名詞と動詞・形容詞に大きく分けたのと同じ理由です。
どちらかと言えば、おおきな方針のもと、おおよそ「フレーズらしい形になっているかな」というチャンクを抜き出しておき、最後にもういちどだけ、目についたパターンを具体的に指定して除外する。それでも残るような微妙なフレーズ例はいずれにせよ、のちの統計処理で自ずとふるいにかけられるだろうというスタンスです。
spaCyの品詞分類やUDを活用したオレオレホットワード集計用クレンジングデータ出力サンプル
実行結果
入力テキスト
抽出結果
アドホックな方法の割に、意外にいいところまで来たような気もします。
一方、簡単に済まそうとして、逆にステップ数が多くなったのと、後で見た時に自分でも分からなそう...というところはありますが、自分の中ではなかなか勉強になったので満足しています。
spaCy参考URL
spaCyに慣れるのに参考にさせていただいたページ