PythonのJanomeで称賛/罵倒の推測【練習, 自然言語処理】
目次
称賛と罵倒の解析
自然言語処理は人間の話す言葉をプログラミングで解析することです。
工程としては↓のようにわかれています。
字句解析
構文解析
意味解析
文脈解析
このうち、最初の工程である「字句解析」とは、文章を単語のリストに変換することを言います。
特に日本語の文章の字句解析を「形態素解析」といいます。
Pythonには「Janome」という形態素解析ライブラリがあります。
形態素解析器として有名な「MeCab」の辞書を使っているライブラリです。
今回はこのJanomeを使って、文章からポジティブなワードとネガティブなワードを検出するという処理をやってみたいと思います。
今回、紹介するスクリプトは形態素解析のみを行っているので、自然言語処理プログラムとしては非常に単純で、精度はよくない……というか、まったくありません。
ただこういった小さいプログラムを作るのは練習としてはいいですよね。
(^ _ ^) | それな |
スクリプトの全文
最初に今回作成したスクリプトの全文を紹介します。
from janome.analyzer import Analyzer from janome.tokenfilter import POSKeepFilter import unittest def is_positive_word(word): """ 引数wordがポジティブならTrue, でなければFalse """ return word in ['天才', '秀才'] def is_negative_word(word): """ 引数wordがネガティブならTrue, でなければFalse """ return word in ['馬鹿', 'アホ'] def targets_to_str(targets): """ 代名詞のトークン列であるtargetsを文字列に変換 """ ret = '' if len(targets): if len(targets) >= 2: for i in range(len(targets) - 1): ret += f'「{targets[i].surface}」と' ret += f'「{targets[-1].surface}」は' else: ret += '誰かが' return ret def analyze_nouns(nouns): """ 名詞のリストであるnounsを推測し、話しかけた対象(targets)と その話しかけの内容(positive_tok, negative_tok)に分類する """ targets = [] positive_tok = None negative_tok = None for tok in nouns: p = tok.part_of_speech.split(',') if is_positive_word(tok.surface): positive_tok = tok elif is_negative_word(tok.surface): negative_tok = tok elif '代名詞' in p: targets.append(tok) return targets, positive_tok, negative_tok def positive_tok_to_str(positive_tok): """ 引数positive_tokを文字列に変換 """ if not positive_tok: return '' return f'「{positive_tok.surface}」と褒められました。' def negative_tok_to_str(negative_tok): """ 引数negative_tokを文字列に変換 """ if not negative_tok: return '' return f'「{negative_tok.surface}」とけなされました。' def parse(line): """ 文字列の引数lineをパースし、推測を行ってその結果を表す文字列を返す """ # Janomeで名詞のトークン列を抽出 token_filters = [POSKeepFilter(['名詞'])] a = Analyzer(token_filters=token_filters) nouns = a.analyze(line) # 名詞のトークン列を推測 targets, positive_tok, negative_tok = analyze_nouns(nouns) # 推測の結果を文字列に変換 ret = targets_to_str(targets) ret += positive_tok_to_str(positive_tok) ret += negative_tok_to_str(negative_tok) return ret class Test(unittest.TestCase): def test_parse(self): def eq(a, b): self.assertEqual(parse(a), b) # 推測がうまくいっている eq('お前は天才か', '「お前」は「天才」と褒められました。') eq('お前はとんでもない天才だ', '「お前」は「天才」と褒められました。') eq('君は秀才か', '「君」は「秀才」と褒められました。') eq('うるせー天才', '誰かが「天才」と褒められました。') eq('あなた天才ね', '「あなた」は「天才」と褒められました。') eq('天才だな君は', '「君」は「天才」と褒められました。') eq('天才だな君は', '「君」は「天才」と褒められました。') eq('天才だな僕と君は', '「僕」と「君」は「天才」と褒められました。') eq('お前は馬鹿か', '「お前」は「馬鹿」とけなされました。') eq('お前はとんでもないアホだ', '「お前」は「アホ」とけなされました。') eq('お前が馬鹿かどうか今にわかる', '「お前」は「馬鹿」とけなされました。') # 推測がうまくいっていない eq('僕は君が天才だと思う', '「僕」と「君」は「天才」と褒められました。') eq('僕は天才だろうか?', '「僕」は「天才」と褒められました。') eq('君は天才をどう思う?', '「君」は「天才」と褒められました。')
このスクリプトを実行したい場合は、↑のコードを「pn.py
」などのファイルとして保存します。
それから↓のようにコマンドを実行します。
python -m unittest pn
このスクリプトは単体では使う想定をしていません。
ですので↑のようにunittest
という単体テスト・モジュールを使って実行します。
これによりTest.test_parse()
内の処理が実行されます。
Janomeによる形態素解析
このスクリプトのメインの処理は関数parse()
に書かれています。
まず↓の部分でJanomeを使い、引数line
の文字列を形態素解析して名詞のリストにしています。
def parse(line): """ 文字列の引数lineをパースし、推測を行ってその結果を表す文字列を返す """ # Janomeで名詞のトークン列を抽出 token_filters = [POSKeepFilter(['名詞'])] a = Analyzer(token_filters=token_filters) nouns = a.analyze(line) ...
↑のコードを書くには↓のようにJanomeのモジュールからクラスをインポートする必要があります。
from janome.analyzer import Analyzer from janome.tokenfilter import POSKeepFilter
janome.analyzer.Analyzer
は形態素解析を行うクラスです。
Janomeが持っているフィルターを指定することで、そのフィルターに応じた解析結果を取得できます。
janome.tokenfilter.POSKeepFilter
はフィルタークラスです。
POS
とはPart Of Speech
の略で、これは品詞のことです。
解析時にキープしたい品詞を指定するフィルターです。
POSKeepFilter
は↓のようにリストに保存しておきます。
POSKeepFilter
をオブジェクトにするときにキープしたい品詞のリスト([名詞]
)を渡しておきます。
token_filters = [POSKeepFilter(['名詞'])]
Analyzer
をオブジェクトにする時にこのtoken_filters
を渡しておきます。
こうするとAnalyzer
はanalyze()
メソッドを実行するときに、このフィルターを考慮してくれるようになります。
a = Analyzer(token_filters=token_filters)
Analyzer
をオブジェクトにしたらanalyze()
メソッドで文字列を解析し、名詞のトークン列を取得します。
nouns = a.analyze(line)
名詞列を使った称賛と罵倒の推測
今回はポジティブなワードとしては「天才」とか「秀才」というワードを検出します。
ネガティブなワードとしては「馬鹿」とか「アホ」です。
これらのワードの検出には関数を使います。
def is_positive_word(word): """ 引数wordがポジティブならTrue, でなければFalse """ return word in ['天才', '秀才'] def is_negative_word(word): """ 引数wordがネガティブならTrue, でなければFalse """ return word in ['馬鹿', 'アホ']
見ていただくとわかりますが、非常に単純な関数です。
これらの関数は引数word
がリストの中に含まれていればTrue
, そうでなければFalse
を返します。
名詞列を使った推測についてはanalyze_nouns()
関数で行っています。
この関数は↓のように呼び出し元にtargets
, positive_tok
, negative_tok
を返します。
targets
はJanomeのトークン列で、ポジティブ/ネガティブなワードが与えられている対象を表すトークン列です。
つまり「お前」とか「あなた」とかの代名詞ですね。
positive_tok
はポジティブなトークン、negative_tok
はネガティブなトークンで、どちらもJanomeのトークンです。
def parse(line): ... # 名詞のトークン列を推測 targets, positive_tok, negative_tok = analyze_nouns(nouns) ...
analyze_nouns()
を見てみます。
def analyze_nouns(nouns): """ 名詞のリストであるnounsを推測し、話しかけた対象(targets)と その話しかけの内容(positive_tok, negative_tok)に分類する """ targets = [] positive_tok = None negative_tok = None for tok in nouns: p = tok.part_of_speech.split(',') if is_positive_word(tok.surface): positive_tok = tok elif is_negative_word(tok.surface): negative_tok = tok elif '代名詞' in p: targets.append(tok) return targets, positive_tok, negative_tok
引数nouns
はJanomeの名詞のリストです。
関数の内容としてはfor
文でトークン列を回し、そのトークンのPOS(Part Of Speech)
, つまり品詞をチェックします。
Janomeのトークンは属性part_of_speech
を持っています。
これはカンマ(,
)区切りで区切られた品詞の文字列です。
tok.part_of_speech.split(',')
とやって品詞をリストに変換し、それをp
変数に代入してます。
tok.surface
はトークンのsurface
属性を参照しています。
この属性は表層形と呼ばれる元の文章そのままの表現が文字列として保存されています。
この表層形をis_positive_word()
やis_negative_word()
に渡すことで、そのトークンがポジティブあるいはネガティブなワードかチェックできるということですね。
トークンがポジティブでもネガティブでもない場合はトークンの品詞に「代名詞」が含まれていないかチェックします。
代名詞とは「お前」とか「あなた」などの名詞の代わりになるものです。
この代名詞が現れた場合は、そのトークンをtargets
に保存します。
やってることはトークンの表層形と品詞のチェックだけで、それを変数にフィルタリングしてるだけです。
推測の結果を結果の文字列に変換
parse()
の最後の方では推測の結果を文字列にしてreturn
しています。
analyze_nouns()
の結果であるtargets
, positive_tok
, negative_tok
を文字列に変換します。
def parse(line): ... # 推測の結果を文字列に変換 ret = targets_to_str(targets) ret += positive_tok_to_str(positive_tok) ret += negative_tok_to_str(negative_tok) return ret
targets
は長さが2
以上のリストである可能性があるため、↓のようにfor
文で回して文字列にします。
文字列への変換にはトークンのsurface
を使います。
targets
の長さが0
の場合は「誰かが」というワードを返すだけです。
def targets_to_str(targets): """ 代名詞のトークン列であるtargetsを文字列に変換 """ ret = '' if len(targets): if len(targets) >= 2: for i in range(len(targets) - 1): ret += f'「{targets[i].surface}」と' ret += f'「{targets[-1].surface}」は' else: ret += '誰かが' return ret
ポジティブ/ネガティブなトークンについては単純にsurface
を参照するだけです。
def positive_tok_to_str(positive_tok): """ 引数positive_tokを文字列に変換 """ if not positive_tok: return '' return f'「{positive_tok.surface}」と褒められました。' def negative_tok_to_str(negative_tok): """ 引数negative_tokを文字列に変換 """ if not negative_tok: return '' return f'「{negative_tok.surface}」とけなされました。'
この文字列の変換には「褒められた」とか「けなされた」とかが、能動的なのか受動的なのかの判定を行わず、一方的に決め打ちで受動的に処理しています。
ここの能動/受動を切り替えるにはおそらく助詞などの解析も必要なると思うんですが、今回はやっていません。
次回あたりチャレンジしてみたいところです。
テスト
最後にテストです。
unittest
という単体テスト用のPythonの標準ライブラリを使いますが、インポートは↓のように行います。
import unittest
unittest
モジュールのTestCase
クラスを継承したクラスTest
を作成します。
こうするとTest
クラスが各種テスト用のメソッドを使えるようになります。
実際のテストは↓のようになります。
class Test(unittest.TestCase): def test_parse(self): def eq(a, b): self.assertEqual(parse(a), b) # 推測がうまくいっている eq('お前は天才か', '「お前」は「天才」と褒められました。') eq('お前はとんでもない天才だ', '「お前」は「天才」と褒められました。') eq('君は秀才か', '「君」は「秀才」と褒められました。') eq('うるせー天才', '誰かが「天才」と褒められました。') eq('あなた天才ね', '「あなた」は「天才」と褒められました。') eq('天才だな君は', '「君」は「天才」と褒められました。') eq('天才だな君は', '「君」は「天才」と褒められました。') eq('天才だな僕と君は', '「僕」と「君」は「天才」と褒められました。') eq('お前は馬鹿か', '「お前」は「馬鹿」とけなされました。') eq('お前はとんでもないアホだ', '「お前」は「アホ」とけなされました。') eq('お前が馬鹿かどうか今にわかる', '「お前」は「馬鹿」とけなされました。') # 推測がうまくいっていない eq('僕は君が天才だと思う', '「僕」と「君」は「天才」と褒められました。') eq('僕は天才だろうか?', '「僕」は「天才」と褒められました。') eq('君は天才をどう思う?', '「君」は「天才」と褒められました。')
test_parse()
メソッドはテストを実行するメソッドです。
このようにメソッド名の先頭にtest_
と付けておくと、python -m unittest
を実行した時にこのメソッドが実行されます。
ショートカットとしてeq
という関数内関数を作っています。
def eq(a, b): self.assertEqual(parse(a), b)
内容的には引数a
をparse
し、self.assertEqual()
の第1引数に渡します。
引数b
はself.assertEqual()
の第2引数に渡します。
self.assertEqual()
は第1引数と第2引数がイコールでない場合にエラーを送出するメソッドです。
肝心のテストですが以下の内容をみるとうまく推測できているような錯覚を受けます。
# 推測がうまくいっている eq('お前は天才か', '「お前」は「天才」と褒められました。') eq('お前はとんでもない天才だ', '「お前」は「天才」と褒められました。') eq('君は秀才か', '「君」は「秀才」と褒められました。') eq('うるせー天才', '誰かが「天才」と褒められました。') eq('あなた天才ね', '「あなた」は「天才」と褒められました。') eq('天才だな君は', '「君」は「天才」と褒められました。') eq('天才だな君は', '「君」は「天才」と褒められました。') eq('天才だな僕と君は', '「僕」と「君」は「天才」と褒められました。') eq('お前は馬鹿か', '「お前」は「馬鹿」とけなされました。') eq('お前はとんでもないアホだ', '「お前」は「アホ」とけなされました。') eq('お前が馬鹿かどうか今にわかる', '「お前」は「馬鹿」とけなされました。')
eq()
の第1引数の入力をparse()
に与えると、第2引数の文字列の結果になるということです。
しかし、↓を見ると、やはり精度としては低いことがわかります。
# 推測がうまくいっていない eq('僕は君が天才だと思う', '「僕」と「君」は「天才」と褒められました。') eq('僕は天才だろうか?', '「僕」は「天才」と褒められました。') eq('君は天才をどう思う?', '「君」は「天才」と褒められました。')
おわりに
今回は自然言語処理の最初の工程である形態素解析で遊んでみました。
そろそろ構文解析を使った処理も書いてみたいところです。
今回作ったスクリプトのライセンスはMITです。
(^ _ ^) | 形態素解析はおもしろい |