こんにちは。 DSOC R&D グループの高橋寛治です。
最近、アルゴリズムを実装して勉強することにハマっています。 ある程度理解しないと実装できないですし、そもそも実装したものが動くということは非常に嬉しいものです。
さて、今回は最近実装した「出現頻度と連接頻度に基づく専門用語抽出器」について理論と実装の紹介を行います。 早く実装が見たいというかたは、こちらからすぐにご覧いただけます。
「出現頻度と連接頻度に基づく専門用語抽出」の理論
次の論文の内容を実装します。
この論文は、単語を構成する名詞の頻度に着目して専門用語(あるドメインの用語)を対象ドメイン内のコーパスから抽出する手法を提案しています。 あるドメインのコーパスを用意し単語分割と品詞付与を行うことで、そのドメインの専門用語を抽出できるというのが、この研究の面白い点です。
専門用語の抽出は、複合名詞を構成する名詞の頻度もしくは種類数を数え上げることにより行われます。 名詞もしくは複合名詞を構成する名詞を単名詞と呼んでいます。 大きく分けて次の3つのスコア付けを組みあわせることにより、専門用語抽出のための重要度を計算しています。
- 単名詞バイグラムを用いた種類数もしくは頻度による単名詞のスコア付け
- 単名詞のスコアを用いた複合名詞のスコア付け
- 候補語の出現頻度を考慮した重み付け
単名詞バイグラムを用いた単名詞のスコア付けには、左右それぞれに連接する単名詞の種類数もしくは頻度を利用します 「単語A 単語B」というバイグラムを例に説明します。 単名詞「N」の左側の単名詞の種類数をLDN(N)とし、すなわち単語Bに対する左側に連接する単名詞の種類数を数え上げたものです。右側は同様にRDN(N)とします。 種類数であるということが重要な点です。 次に頻度ですが、ある単名詞Nが左側に出現する頻度と右側に出現する頻度を数えるだけです。
複合名詞のスコアは、複合名詞を構成する単名詞それぞれのバイグラムの左右のスコアを用いて計算します。 次の計算式で表されます。
$$ LR(CN) = ( \prod_{i=1}^{L} (FL(N_i)+1) (FR(N_i)+1 )^{\frac{1}{2L}} $$
CNは単名詞で構成される複合名詞、LはCNを構成する次の単名詞を示します。
$$ N_1, N_2, ..., N_L $$
FLとFRは種類数もしくは頻度を利用した単名詞のスコアです。 複合名詞を構成する単名詞のスコアの積和の相乗平均を取っています。 これは実験でよい結果が得られたから選ばれたそうです。
最終的なスコアは候補語となる複合名詞の頻度と複合名詞のスコアの積をとることによりスコアを算出します。
$$ FLR(CN) = f(CN) \times LR(CN) $$
実装
Perl・Python用モジュールが公式でも開発されています。 勉強のために実装していきます。
こちらの実装を部分的に説明していきます。
まず実装方針ですが、scikit-learnと同様に利用できるように同じインターフェイスを実装します。 scikit-learnの基底クラスを継承してもいいのですが、今回は同じインターフェイスを用意することで代替します。
class LRValue: def __init__(self): # パラメータなどをインスタンス変数に持つ pass def fit(self, X): # Xには複合名詞のリストを与える。パラメータを学習するメソッド return self def transform(self, X): # 学習したパラメータを用いて入力を変換するメソッド return X def fit_transform(self, X): # fitとtransformを同時に行うメソッド return self.fit(X).transform(X)
プログラムのフローは次のようになります。
- パラメータ学習:左右の種類数・頻度、複合名詞をコーパスから計算
- 専門用語抽出:複合名詞のスコアを学習したパラメータを用いて算出
まずパラメータ学習のための箱を用意します。 バイグラムですので、キー(例えば左の単名詞)に対するバリュー(右の単名詞)とその頻度を格納する辞書を考えます。
ldn = { "単語A": { "単語B": 1, "単語C": 3, }, "単語B": {.... }
これを左右分作れば数え上げられます。 Pythonではdefaultdictを用いて簡単な単語の数え上げを実現します。
辞書型で作成することにより、種類数がほしい場合は、len(ldn["単語A"])
で計算できます。
頻度がほしい場合は、sum(ldn["単語A"].values())
となります。
簡単に計算できることがわかります。
パラメータ学習のためにupdate
というメソッドを用意しました。
バイグラムを入力することにより、左右の連接頻度を更新します。
def update(self, bigram): self._ldn[bigram[1]][bigram[0]] += 1 self._rdn[bigram[0]][bigram[1]] += 1
複合名詞スコア算出のために、LR
メソッドを用意します。
def LR(self, compound_noun): a = np.array([(self.ln(n) + 1) * (self.rn(n) + 1) for n in compound_noun]) return np.power(a.cumprod()[-1], 1/(2 * len(compound_noun)))
やや複雑に見えるかもしれませんが、ある複合名詞を構成する単名詞のスコアを計算し、累積(cumprod
)と相乗平均(power
)をとっているだけです。
最後に頻度とのかけ算ですが、次のように計算したLRスコアに対して、複合名詞の頻度をかけ算するだけです。
def FLR(self, compound_noun): return self._compound_noun[" ".join(compound_noun)] * self.LR(compound_noun)
非常に単純な実装ですが中心部分は以上となります。
あとは、['人工', '知能'], ['名前'], ['ダート', 'マス', '会議'], ['ジョン'], ['マッカーシー'], ['命名']]
のように計算したい名詞群を与えることで、計算に必要なパラメータを学習します。
Wikipediaの人工知能の記事を抜粋して計算すると、 次のように「人工知能」が一番高いスコアとなります。
print(lr_values) """ {'人工 知能': 3.4641016151377544, '名前': 1.0, 'ダート マス 会議': 1.5874010519681994, 'ジョン': 1.0, 'マッカーシー': 1.0, '命名': 1.0, '記号 処理': 1.4142135623730951, '知能': 1.7320508075688772, '記述': 1.0, '主体': 1.0, '情報処理': 1.0, '研究': 1.0, 'アプローチ': 1.0, '意味あい': 1.0, '日常 語': 1.4142135623730951, '呼び名': 1.0, '非常': 1.0, '曖昧': 1.0, '気': 1.0, '家庭 用 電気 機械 器具': 1.7411011265922482, '制御 システム': 1.4142135623730951, 'ゲーム ソフト': 1.4142135623730951, '思考 ルーチン': 1.4142135623730951} """
良い感じにキーワードを抽出するためにはもう少し実装が必要で、日本語を分かち書きし、複合名詞を取り出す部分を別プログラムに記述しています。 本家でもいろいろな前処理が適用されていますが、複合名詞を取り出すために品詞による抽出ルールを記述しています。 この前処理を少し工夫するだけで、抽出される結果が大きく変わりますので、ぜひ変更してみてください。
実装するとよくわかる
実際に実装してみると考えることが多く、非常に勉強になります。 もちろん実装をするということは、時間を非常に要するため、勉強方法としてそのバランスを見極めないといけないなと思う日々です。
実は他のキーワード抽出アルゴリズムも実装しており、RAKEと呼ばれる別のキーワード抽出手法を日本語でも動作するようにしました(機会があれば紹介したいと思います)。 一つ実装すると、新しいものもやってみよう! という気持ちになって、勉強が進みます。
みなさんもぜひ実装してみてください。
執筆者プロフィール
高橋寛治 Sansan株式会社 Data Strategy & Operation Center R&Dグループ研究員
阿南工業高等専門学校卒業後に、長岡技術科学大学に編入学。同大学大学院電気電子情報工学専攻修了。在学中は、自然言語処理の研究に取り組み、解析ツールの開発や機械翻訳に関連する研究を行う。大学院を卒業後、2017年にSansan株式会社に入社。現在はキーワード抽出など自然言語処理を生かした研究に取り組んでいる。