前回は spaCy と GiNZA についてその概要と使い方を紹介しました。今回はキーフレーズ抽出の手法について解説し、spaCy ベースのキーフレーズ抽出処理ライブラリである pke を用い日本語データセットで実験した結果を紹介します。
1. はじめに
本記事ではキーフレーズ抽出について、その概要といくつかの抽出手法について説明します。記事の後半ではキーフレーズ抽出処理ライブラリである pke を用い、記事の前半で説明した各手法を日本語のデータセットに対して適用した精度比較試験を行った結果を紹介します。
2. キーフレーズ抽出
キーフレーズ抽出処理について簡単に説明すると、「文章からその主題を良く表現している句を抽出する技術」と言えるでしょう。日本語では「キーワード」のほうが一般的で通りのよい表現になりますが、処理としては「大統領|選挙」のように複数単語の連続を抽出するので、単語を意味する「ワード」ではなく、句を意味する「フレーズ」が使われます。 キーフレーズ抽出の手法は統計ベース、グラフベース、機械学習ベースの手法に大別されます。機械学習ベースの手法の難点は学習データを用意する必要があるところで、特にキーフレーズ抽出をはじめとする自然言語処理では正解が明確に定まらないことが多い為、教師データを作成しづらいという悩みがあります。 本記事では学習データが不要で手軽に試すことができるグラフベースの手法を紹介することにします。
3. グラフベースの手法
グラフベースのキーフレーズ抽出手法の根幹をなすアルゴリズムが PageRank 1 です。PageRank は Google検索におけるWebページの順位付けに用いられたことで有名ですので聞き覚えのある方も多いのではないでしょうか。直感的には、沢山のページからリンクされているページは価値が高い、価値が高いページからリンクされているページは価値が高い、という理解でよいでしょう。
後述する各手法はすべて内部的に PageRank の計算を利用するので、ここで PageRank の計算やその意味合いを簡単に押さえておきましょう。まず、Webページをノード、ページ間のリンクをエッジとして図のような有向グラフを作成します。
ここでページ A の PageRank を以下のように計算します。あとは値が収束するまで、この計算を繰り返します。
上式中の各項の意味合いは以下のとおりです。
- PR(A) : ページ A の PageRank。
- C(Ti) : ページ Ti から出ていくリンク数。
- Ti : ページ A にリンクを張っているページ。総数 n 。
- d : ダンピングファクタ。0 ≦ d ≦ 1 の値を取る。
意味合い的には、「あるWebページを起点にランダムにリンクをたどり、そうこうするうち、また別のWebページを起点に同じことを繰り返すユーザ(ランダムサーファー)を仮定したとき、そのユーザがあるページを閲覧している確率」であると言えます。ですので、全てのページの PageRank を合計すると 1 になります。
ダンピングファクタ d は前述の「そうこうするうち別のWebページを起点に」する確率で、ページ内のリンクをクリックせずにアドレスバーに URL を直打ちするイメージです。 ページ内のリンクをたどるだけでは有向グラフに閉路(上図で黄緑と濃橙)が形成されていると脱出できなくなります。確率 1 - d でページ内のリンクを無視してテレポートさせることで、閉路がある場合でも PageRank の計算が可能になります。
Webページのランク付けではWebページをノード、リンクをエッジとしてグラフを形成したわけですが、以降で紹介するキーフレーズ抽出の手法では何をノード、エッジにしてグラフを形成するか?というところがキモになってきますので、そのあたりを踏まえつつ見ていきましょう。
3.1 TextRank
TextRank 2 は「相互に関連度の高い語句は重要度が高い」という仮定に基づき PageRank を用いて語句を選択する手法です。
TextRank では文章に含まれる単語をノードとし、単語と単語が指定した窓幅内で共起した場合にエッジを張り無向グラフを形成します。また、グラフを形成する際にはグラフが肥大化し過ぎないよう単語を品詞で絞り込みます。名詞+形容詞で絞り込みをかけることが多いようです。 以下は例文と例文から形成されたグラフです。例文に名詞、形容詞のみで絞り込みを掛けた上で窓幅1でエッジを構成しています。
形成されたグラフはWebページの場合と異なり無向グラフですので、ノードへの入力エッジ数=ノードからの出力エッジ数として PageRank をかけ、PageRank の高い順に単語を抽出します。最後に連続した抽出単語を単一のキーフレーズにまとめます。
3.2 SingleRank
SingleRank 3 は基本的に TextRank の拡張です。SingleRank が提案された論文では CollabRank という手法が提案されており、これは予め用意したコーパスをクラスタリングし、キーフレーズの抽出対象文書と同一クラスタに分類された文書群の情報を利用します。SingleRank は CollabRank の手順を一部省略して単一ドキュメントに適用できるようにしたものです。 SingleRank では単語のスコアを以下のように計算します。
上式中の各項の意味合いは以下のとおりです。 PageRank で出力リンク数 C(Ti)で割っていた部分が重み M~j,iに置き換わった以外は大体同じであるところが見て取れるかと思います。
- WordScore(vi) : 単語 vi のスコア。
- |V| : キーフレーズを抽出する文書の語彙数。
- M~ : |V| x |V| の重み行列。M~i,jの値は単語iと単語jの共起回数を M~ の各行を合計1になるよう正規化したもの。
- μ : ダンピングファクタ。0 ≦ μ ≦ 1 の値を取る。
単語のスコアを計算した後、グラフに追加した単語(名詞+形容詞等)の連続をフレーズ候補とし、フレーズ候補に含まれる単語のスコアを合計することで、フレーズ候補のスコアを導出、高スコアのフレーズ候補をキーフレーズとして抽出します。
TextRank との相違点をまとめると以下になります。
- グラフのエッジを共起回数で重みづけします( TextRank はエッジの重みなし)。
- フレーズ候補を構成する単語のスコアの合計をフレーズ候補のスコアとします( TextRank は高スコアの単語を抽出してから連続した抽出単語を結合)。
せっかくなので、 CollabRank と SingleRank の違いにも触れておきましょう。
CollabRank では事前に用意したコーパスをクラスタリングし、分類したクラスタ単位で WordScore を計算します。文書からキーフレーズを抽出する際は、文書が属するクラスタの WordScore を用いてフレーズ候補のスコアを求めます。類似した文書を多数集めることでより良い精度の単語スコアが得られることを期待するものです。
3.3 PositionRank
PositionRank 4 は SingleRank を拡張したもので、学術論文からキーフレーズを抽出することを目的とする手法です。
無向グラフ形成の手順やフレーズ候補のスコア算出は SingleRank と同じで、以下のように単語のスコアを計算します。S や p がベクトルなので、計算式から Σ が消えて見た目すっきりしていますが、意味合い的には大体同じです。
- S : 単語スコアの列ベクトル。次元数は文書中の単語を品詞で絞り込んだ後の語彙数 |V|。
- M~ : |V| x |V| の重み行列。SingleRank の M~と同じ。
- p~ : エッジを無視してテレポートする際の飛び先の重みを保持した列ベクトル。次元数は |V|。
- α : ダンピングファクタ。0 ≦ α ≦ 1 の値を取る。
PositionRank が SingleRank と異なる箇所は p~ の部分です。ここまでの手法はダンピングファクタによりエッジを無視して別ノードにテレポートする際、飛び先の確率は全ノードで一定でしたが、 PositionRank ではノードによって遷移確率が異なります。 p~ がテレポート先の遷移確率となっており、 p~ の i 番目の要素 p~i の値は単語 i の文書中での全ての出現位置の逆数の総和を p~ の全要素の合計が 1 になるように正規化したものが入ります。例えば単語 “criteria” が文書中で先頭から5番目、19番目、37番目の位置に出現した場合、pcriteria は以下のように計算します。
つまり、 PostionRank では単語の出現位置が文書の先頭に近ければ近いほどテレポート先になる確率が高くなり最終的なスコアでも有利になります。「学術論文では重要な単語は先頭近くに存在する」と仮定してそれを SingleRank に組み込んだのが PositionRank というわけです。
3.4 TopicRank
ここまで TextRank, SingleRank, PositionRank という一連の改善の流れで来ていましたが、 TopicRank 5 は少し毛色が異なります。ここまでグラフのノードは単語でしたが、 TopicRank ではその名の通りトピックをノードにします。
では、トピックとは何か?という話になるのですが、平たく言えば SingleRank, PositionRank 同様に連続するキーフレーズ対象品詞の単語をフレーズ候補として抽出し、フレーズ候補をクラスタリングで似た者同士の集団に分類した一つ一つの集団がトピックになります。
ここで何故トピックをノードにするのか説明しておきます。前述した単語をノードとする手法の難点は「乳児用液体ミルク」、「液体ミルク」のように意味合い的に重複するキーフレーズが複数抽出されてしまうことでした。 TopickRank は以下のようなステップを踏むことで、この課題を回避するものです。
- 似たフレーズ候補をトピックにまとめる
- トピックレベルで重要度を評価
- 重要なトピックからキーフレーズを一つづつ抽出
話をトピックの形成方法に戻します。フレーズ候補のクラスタリングのアルゴリズムは、HAC(Hierarchical Agglomerative Clusterring)を用います。以下のように最初は距離の近いトピック同士を結合し要素数が2のクラスタを形成、次に距離の近いクラスタ同士を結合して…という具合に階層的に大きなクラスタにしていきます。
この際、クラスタ間の距離はクラスタ間の全要素同士の距離の平均6を用い、要素(フレーズ候補)間の距離は Jaccard 係数です。Jaccard係数は以下のように算出し、二つのフレーズ候補に共通する単語が多いほど大きくなります。
フレーズ候補のクラスタリングによりトピックの集合が準備できたら、トピックをノード、エッジの重みをトピック間の意味的関係性とした完全無向グラフを形成します。
トピック間の意味的な関係性とは何ぞや?という話になりますが、言葉で説明すると「二つのトピックに属する全フレーズ候補の全出現位置のレシプロカル距離(出現位置の差分の絶対値の逆数)の総和」になります。式で記述すると以下のとおりですが、はやい話が「トピックが二つあり、各トピックに属するフレーズ候補が近い位置で出現する場合、その二つのトピックの関係性は強い(=重みが大きくなる)」と理解してください。
- ti, tj : トピック
- ci, cj : トピック中のフレーズ候補
- pos(ci) : フレーズ候補 ci の全出現位置
このようにしてグラフを形成したら、いままでと同様に PageRank でトピックの重要度を評価します。あとは前述のとおり重要度の高いトピックから代表フレーズを抽出する訳です。選択方法としては以下の3つの戦略が提案されていますが、5の論文によると先頭に最も近いフレーズ候補を選ぶのが最良の結果であったようです。
- 文章の先頭に最も近いフレーズ候補
- 出現回数が最も多いフレーズ候補
- トピックを構成するクラスタの重心となるフレーズ候補
3.5 MultipartieRank
MultipartieRank 7 は TopicRank を改良したものになります。TopicRank の課題はトピックレベルで順位付けされるため、同一トピックに属するフレーズ候補の集合は同じ重要度となり、代表フレーズがヒューリスティックに抽出されることでした。 MultipartieRank ではグラフ形成の仕方を工夫することで、この課題を回避します。
フレーズ候補の形成とトピック分類するところまでは TopicRank と同じです。 TopicRank ではトピックをノードとして完全無向グラフを形成しましたが、 MultipartieRank のグラフはフレーズ候補をノードとした完全有向グラフを形成した後、同一トピックに属するノード間の接続を切ったものになります。エッジの重みは TopicRank エッジの重みに利用した dist(ci, cj) を用います。
言葉にすると分かりにくいですが、 以下が TopicRank と MultipartieRank のグラフ形成の違いを示したものです。
トピック内の接続を切ることの意味合いですが、 PageRank におけるランダムサーファーの比喩で考えてみましょう。
フレーズ候補が Web ページ、トピックは Webサイトです。ここで「ランダムサーファーは同一サイト内へのリンクをクリックしてはいけない」というルールにします。当然、ランダムサーファーは PageRank の繰り返し計算の過程でより多くの Webサイトを訪問することになり、結果的に重要度が高いとされる Webページ(=フレーズ候補)はより多くの Webサイト(トピック)に分散することになります。つまり、同じ数のキーフレーズでより多くのトピックを網羅できるようになるわけです。
また MultipartieRank は各トピック毎で最初に出現したフレーズ候補を以下の要領で優遇します。
各トピック毎に文書中で最初に出現したフレーズ候補 cj (③)への全ての入力エッジの重みを cj と同一トピックの他のフレーズ候補 ck(④、⑤)と cj への入力元フレーズ候補 ci (①)間の重みの総和および ci の最初の出現位置に基づいて加算します。
- cj : 各トピック毎の最初に出現したフレーズ候補
- τ(cj) : cj と同一トピックに属するフレーズ候補集合
- ci : cj から入力を受けるフレーズ候補(※全フレーズ候補集合から τ(cj) を除いた数存在する)
- pi : フレーズ候補 ci の最初の出現位置
- 𝛼 : ハイパーパラメータ
ようするに、各トピック毎に文書中で最初に出現したフレーズ候補は自身が属するトピックとの関連性が強く文書先頭に近い位置に出現する他のトピックのフレーズ候補からより多くの優遇を受けることになります。
ここまでくれば、あとは PageRank の計算です。 MultipartieRank はフレーズ候補がグラフのノードですから、 PageRank のスコア順でフレーズ候補を抽出すればOKです。
さて、手法の説明が長くなってしまいましたが、ここからは紹介した手法を実装したライブラリと日本語の扱い方について説明します。
4. キーフレーズ抽出の実装
今回はキーフレーズ抽出手法を実装したライブラリとして pke 8 を用います。 pke は MultipartieRank の論文の著者である Florian Boudin 氏が公開しており、 GPL 3.0 ですので(気を使うところはありますが)商用利用も可能です。 pke には今回紹介した全ての手法に加え、統計ベース、教師ありなどの手法も実装されているので、様々な手法を手軽に比較することができます。
さて、この pke ですが、サポートしている言語は英語・フランス語等の欧米の言語のみ、分かち書きや品詞推定には前回紹介した spaCy が用いられています。これまでは「うーん、 spaCy かぁ。」と諦めるか、 MeCab などで当該部分を再実装する必要があったのですが、 GiNZA の登場で状況が変わります。 spaCy の API で日本語を処理できるようになった為、比較的小さな手直しで日本語対応させることが可能になりました。
spaCy は欧米で人気のフレームワークで spaCy をベースとした自然言語処理ライブラリは多数存在します。それらを日本語対応させる敷居が大幅に下がったと考えると GiNZA という Universal Dependencies体系に準拠した日本語処理系の意義は大きく非常に有り難い限りです。
では、実際に MultipartieRank で日本語文書からキーフレーズを抽出してみましょう。 他の手法も使用法は大体同じです。詳しくは pke のドキュメントを参照してください。また GiNZA は前回の記事の要領でインストール済みとします。
まず、 pke を以下のように pip でインストールします。
$ pip install git+https://github.com/boudinfl/pke.git
次に pke は内部的に NLTK のストップワードを参照するのでダウンロードしておきます。ただし NLTK のストップワードに日本語は含まれていないので、この部分が後に細工が必要になる部分です。
$ python -m nltk.downloader stopwords
pke は pke.base.ISO_to_language
という dict で使用可能な言語を管理しているので、日本語のエントリを足してやります。この dict のキーは spacy.load()
の引数に合わせる必要があるので、ja_ginza
で登録します。値の japanese
の方は NLTK が nltk.corpus.stopwords.words
で保持しているストップワードを管理する dict を引く為のキーに使われます。
import pke pke.base.ISO_to_language['ja_ginza'] = 'japanese'
最後に pke が japanese
でストップワードをルックアップした際に有向な値を見つけられるよう、nltk.corpus.stopwords.words
を差し替えます。日本語のストップワードですが、今回はとりあえず GiNZA が内部的に保持しているものを使用しました。
import ginza import nltk stopwords = list(ginza.STOP_WORDS) nltk.corpus.stopwords.words_org = nltk.corpus.stopwords.words nltk.corpus.stopwords.words = lambda lang : stopwords if lang == 'japanese' else nltk.corpus.stopwords.words_org(lang)
これで pke を用いて日本語の文章からキーフレーズを抽出する準備が整いました。では、さっそく試してみましょう。以下のテキストからキーフレーズを抽出します。
# 引用元:「東京ディズニーランド」『フリー百科事典 ウィキペディア日本語版』。 # 最終更新 2019年9月29日 (日) 04:02 UTC、URL: https://ja.wikipedia.org text = "東京ディズニーランド、英称:Tokyo Disneyland、略称:TDL)は、" +\ "千葉県浦安市舞浜にあるディズニーリゾートを形成する日本のディズニーパーク。" +\ "年間来場者数は日本最大の約1,600万人で、世界のテーマパーク・アミューズメントパークの中でも、" +\ "フロリダ州のウォルト・ディズニー・ワールド・リゾートのマジック・キングダム、カリフォルニア州の" +\ "ディズニーランド・リゾートのディズニーランド・パークに次いで世界3位の規模を誇る[1]。オリエンタルランド" +\ "がザ・ウォルト・ディズニー・カンパニーとのライセンス契約のもと運営している[3]。"
まずはキーフレーズ抽出の extractor を作ります。
extractor = pke.unsupervised.MultipartiteRank()
次に extractor
にテキストをロードします。language
はそのまま spacy.load()
に渡されるので ja_ginza
を指定します。
normalization
が未指定だと NLTK のステミング処理9がかかって日本語未対応でエラーになるので None
を指定しておきます。
extractor.load_document(input=text, language='ja_ginza', normalization=None)
次にフレーズ候補を形成する品詞を spaCy の定数で指定します。以下は名詞、固有名詞、形容詞、数を指定しています。
extractor.candidate_selection(pos={'NOUN', 'PROPN', 'ADJ', 'NUM'})
トピック分類する際のクラスタリングの結合閾値と距離の計算方法、重み調整のハイパーパラメータの指定(全てデフォルト)です。
extractor.candidate_weighting(threshold=0.74, method='average', alpha=1.1)
ここまで準備できたら抽出するキーフレーズの数を指定して実行です。
extractor.get_n_best(n=10) # [('東京 ディズニーランド', 0.1574627398196762), # ('ディズニー リゾート', 0.08566621365949743), # ('ディズニー パーク', 0.07594994471471744), # ('テーマ パーク', 0.05666289782040913), # ('tokyo disneyland', 0.05518756519311371), # ('千葉県 浦安市 舞浜', 0.05150314589561125), # ('tdl', 0.046633582717173494), # ('ウォルト', 0.04604439241086669), # ('リゾート', 0.04582719149065351), # ('ディズニー', 0.04410279186778268)]
日本語の文章に対しても、それっぽいフレーズが抽出できるようですね。
では、評価用のデータセットを使って紹介した手法の精度比較をしていきたいと思います。
5. 精度比較試験
ここからは日本語のキーフレーズ抽出データセットを使った精度比較試験の結果を紹介します。
5.1 使用したデータセット
まず使用したデータセットです。キーフレーズ抽出向けの公開日本語データセットがあれば良かったのですが見つけることができませんでした。仕方ないので、 Yahoo ニュースから記事を50本ほどスクレイピングし、手作業でアノテーションしたものを用いました。
- アノテーション作業はアルゴリズムの詳細を知らない第三者による実施。
- ただし、キーフレーズは「連続する名詞、形容詞、数字」とする条件を設定。
ちなみに、アノテーション作業には chakki-works の doccano 10 を使用させて頂きました。やはりマウスの範囲選択でアノテーションできるとラクですね(というか、そうでないと人に頼みづらいですし)。
5.2 実験結果
実験ですがパラメータ調整に関してはダンピングファクタはデフォルトで固定、単語共起をカウントする窓幅やクラスタリングの結合閾値を変えながら実験し、各手法で最良のスコアを載せています。キーワード抽出数 = 5 での実験結果は以下の通りです。
PositionRank が最良値、 MultipartieRank がそれに続き、後は提案された年の逆順という結果になりました。 平均F1スコアの最良値が 0.19 と残念な雰囲気ですが、以下に示した MultipartieRank の論文7 に掲載されたスコアを見てみると、このタスクの相場観としてこんな感じなのかもしれません11。
次にキーワード抽出数 = 10 の結果です。こちらもスコアに多少の上下動はありますが、概ね抽出数 = 5 の時と似たような結果になりました。
まず、 PositionRank が最良になった理由ですが、今回使用したデータセットとの相性が良かった可能性があります。 この実験のデータセットの元ネタは Yahoo ニュースの記事なのですが、ニュース記事というものはキーフレーズが先頭に出現する傾向が強いのは感覚的にわかってもらえるかと思います。そして、前述のとおり PositionRank は文章の先頭近くに出現したフレーズ候補を優先する手法ですので、その特性がうまくハマったのでしょう。 MultipartieRank にもトピック毎に最初の候補を優遇する調整が入ってますが、 PositionRank のシンプルな調整の方が有利だったようです。
もう一つ気になった点があります。今回のデータセットでは記事毎のキーフレーズ数が5つ強なので、それほどでもないですが、記事に含まれる実際のキーフレーズ数よりも多くのフレーズを抽出しようとした場合、TopicRank, MultipartieRank は抽出するキーフレーズができるだけ被らないようにする調整が入っているため、PositionRank が相対的に有利になるかもしれません。同じトピックでも気にせずに候補を提示できるので、数うちゃ当たる的な効果がありそうです。
5.3 実行サンプル
このままだと、長々と説明した割にイマイチ使えなさそうな印象になってしまいそうなので、実行サンプルをいくつか示しておきます。著作権等ありますので、元の文章を示すことはできませんが、内容が想像できそうなフレーズが抽出できている気がしませんか? サンプル2の PositionRank は抽出されたキーフレーズが全て「日本」がらみになっており、前述の数うちゃあたる的な傾向が顔を見せている感じですね。
サンプル1
サンプル2
サンプル3
6. おわりに
今回はグラフベースのキーフレーズ手法についてご説明し、GiNZA により spaCy ベースの自然言語処理ライブラリを日本語で利用する実例として、 pke を用いてキーフレーズ抽出手法の精度比較試験をしてみました。せっかく spaCy と GiNZA で係り受け解析などが手軽になったので次回はテキストマイニング関係で何かご紹介したいと思います。