ユーニックス総合研究所

  • home
  • archives
  • python-janome-positive-and-negative

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を渡しておきます。
こうするとAnalyzeranalyze()メソッドを実行するときに、このフィルターを考慮してくれるようになります。

    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)  

内容的には引数aparseし、self.assertEqual()の第1引数に渡します。
引数bself.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です。

🦝 < 形態素解析はおもしろい