日本語の文章を分析する

column M1 21林

M1の林です。

今回は、Pゼミで私が触れている自然言語処理について書きたいと思います。

自然言語処理とは

そもそも自然言語処理とは、人が使っている言語(自然言語)をコンピュータで処理・分析する技術のことです。NLP(Natural Language Processing)とも呼ばれます。
この技術は、AlexaのようなAIスピーカーや検索エンジン、チャットボットなどに活用されています。

自然言語処理の流れ

自然言語処理は、次のステップで行われます。

  1. 形態素解析
  2. 文章構造の解析
  3. 文章の意味を分析
  4. 文脈を理解したうえでの情報抽出

形態素解析

形態素解析とは、文章を最小の単位である単語に分割する作業です。
ここで、実際に形態素解析を行なってみます。Google Colaboratory、Python3、MeCabを用いて行いました。Mecabは、日本語の形態素解析ツールです。
まず、Python3で動作するMeCabをインストールします。Google Colaboratoryのセルで以下を実行します。

!pip install mecab-python3

次に辞書データとして、UniDicをインストールします。これは、、国立国語研究所が開発している辞書だそうです。

!pip install unidic
!python -m unidic download

準備は整いました。いよいよ実行してみましょう。

import MeCab
import unidic
mecab = MeCab.Tagger()
print(mecab.parse("私は、形態素解析を行いました。"))

出力結果は、以下の通りです。

私   代名詞,,,,,,ワタクシ,私-代名詞,私,ワタクシ,私,ワタクシ,和,"","","","","","",体,ワタクシ,ワタクシ,ワタクシ,ワタクシ,"0","","",11345327978324480,41274
は   助詞,係助詞,,,,,ハ,は,は,ワ,は,ワ,和,"","","","","","",係助,ハ,ハ,ハ,ハ,"","動詞%F2@0,名詞%F1,形容詞%F2@-1","",8059703733133824,29321
、   補助記号,読点,,,,,,、,、,,、,,記号,"","","","","","",補助,,,,,"","","",6605693395456,24
形態  名詞,普通名詞,一般,,,,ケイタイ,形態,形態,ケータイ,形態,ケータイ,漢,"","","","","","",体,ケイタイ,ケイタイ,ケイタイ,ケイタイ,"0","C2","",3024215389381120,11002
素   接尾辞,名詞的,一般,,,,ソ,素,素,ソ,素,ソ,漢,"","","","","","",接尾体,ソ,ソ,ソ,ソ,"","C3","",5752103704338944,20926
解析  名詞,普通名詞,サ変可能,,,,カイセキ,解析,解析,カイセキ,解析,カイセキ,漢,"","","","","","",体,カイセキ,カイセキ,カイセキ,カイセキ,"0","C2","",1590177315299840,5785
を   助詞,格助詞,,,,,ヲ,を,を,オ,を,オ,和,"","","","","","",格助,ヲ,ヲ,ヲ,ヲ,"","動詞%F2@0,名詞%F1,形容詞%F2@-1","",11381878116459008,41407
行い  動詞,一般,,,五段-ワア行,連用形-一般,オコナウ,行う,行い,オコナイ,行う,オコナウ,和,"","","","","","",用,オコナイ,オコナウ,オコナイ,オコナウ,"0","C2","",1340038453535361,4875
まし  助動詞,,,,助動詞-マス,連用形-一般,マス,ます,まし,マシ,ます,マス,和,"","","","","","",助動,マシ,マス,マシ,マス,"","動詞%F4@1","",9812325267808897,35697
た   助動詞,,,,助動詞-タ,終止形-一般,タ,た,た,タ,た,タ,和,"","","","","","",助動,タ,タ,タ,タ,"","動詞%F2@1,形容詞%F4@-2","",5948916285711019,21642
。   補助記号,句点,,,,,,。,。,,。,,記号,"","","","","","",補助,,,,,"","","",6880571302400,25
EOS

出力結果は左から順に、以下のようになっています[1]。

  • 表層形(文章中に使用されている単語)
  • 品詞
  • 品詞分類 × 4
  • 活用型
  • 活用形
  • ...
  • 語彙表ID
  • 語彙素ID

UniDicを使用しているので、最後にIDが入っています。このように単語に対するさまざまな情報が表示されます。

次に、単語で分ける分かち書きは以下のようにして行いました。

import MeCab
import unidic
mecab_wakati = MeCab.Tagger('-Owakati')
print(mecab_wakati.parse("私は、形態素解析を行いました。"))

出力結果は以下のようになりました。

私 は 、 形態 素 解析 を 行い まし た 。 

このように、単語間に空白を挿入して1行で表示されました。
この形態素解析の作業は、英語のように単語間に空白を挿入することで分析しやすくします。

文章構造の解析

構文解析とは、単語同士の関係性を解析する作業です。
NLTK(Natural Language Toolkit)というプラットフォームを使用して、句構造の解析を行いました。
NLTKのインストール及び機能の一括ダウンロードを次で行いました。

!pip install nltk
import nltk
nltk.download('all')

以下で文脈自由文法を定義します。
ここで、S、NP、VP、N、Vについては、英語と同様、文、名詞句、動詞句、名詞、動詞をそれぞれ表します。Pは後置詞、PPは後置詞句です。

from nltk import CFG
jpcfg1 = CFG.fromstring("""
S -> PP VP
PP -> NP P
VP -> PP VP
VP -> V TENS
NP -> NP 'の' NP
NP -> NP 'と' NP
NP -> N
N -> '先生' | '自転車' | '学校' | '僕'
P -> 'は' | 'が' | 'を' | 'で' | 'に'
V -> '行k' | '殴r' | '見'
TENS -> 'ru' | 'ita'
""")

ここで、S、NP、VP、N、Vについては、英語と同様、文、名詞句、動詞句、名詞、動詞をそれぞれ表します。Pは後置詞、PPは後置詞句です。
ここで、定義した文脈自由文法にのっとって、「先生 は 自転車 で 学校 に 行k ita」という風に分割された文章を構文解析しました。形態素解析された文章とイメージしてください。

sent = "先生 は 自転車 で 学校 に 行k ita".split(' ')
parser = nltk.ChartParser(jpcfg1)
for tree in parser.parse(sent):
  print(tree)

出力結果は以下のようになりました。

(S
  (PP (NP (N 先生)) (P は))
  (VP
    (PP (NP (N 自転車)) (P で))
    (VP (PP (NP (N 学校)) (P に)) (VP (V 行k) (TENS ita)))))

出力結果をグラフとして表すと、以下のようになります。

グラフとして見ると、この文章は大きく2つに分かれた構造をしていることがわかります。

文章の意味を分析

構文解析をした文章は、まだ意味が定まっていません。例えば、「望遠鏡で泳ぐ彼女を見る」という文章の場合、 望遠鏡で 、泳ぐ彼女を見ているのか、望遠鏡で泳ぐ彼女を 見ているのかわかりません。そこで、意味の分析が必要になります。

spaCyで「望遠鏡で~を見る」を解析する【自然言語処理, Python】
↑のサイトを参考に意味を分析してみました。 Pythonで利用できる自然言語処理ライブラリのspaCyとginzaを用いました。
まず、ginzaをインストールします。

!pip install -U ginza ja-ginza

次に、コードを実行してみます。このコードでは、「望遠鏡で〜を見る」または「望遠鏡で〜を叩く」という文章のみを対象にしています。そのため、始まりと終わりが指定の形ではない文章は、解析できません。
imi_dbは定義された辞書だと思ってください。出力結果は、動詞に係っている、文の最初の名詞をもとに「望遠鏡で見る」または「望遠鏡で叩く」となっていればその内容が出力され、そうでなければ、Noneが出力されました。

"""
spaCyで「望遠鏡で~を見る」を解析する
"""
import spacy 

nlp = spacy.load('ja_ginza')

class Analyzer:
    def __init__(self):
        # 単語の意味が保存された意味データベース
        self.imi_db = {
            '望遠鏡': {
                '活用': ['見る', '叩く']  # 望遠鏡は見る、叩くの活用が可能
            },
        }

    def analyze(self, text):
        """
        引数textを解析して結果を返す
        """
        doc = nlp(text)  # ドキュメントの生成

        for sent in doc.sents:  # 1文ずつ取り出す
            result = self.analyze_sent(sent)  # 文を解析
            if result:
                return result  # 解析に成功

        return None  # 解析に失敗

    def analyze_sent(self, sent):
        """
        文(sent)を解析して結果を返す
        """
        for tok in sent:  # トークンを取り出す
            if tok.pos_ == 'VERB':  # トークンが動詞だったら
                result = self.analyze_bouenkyo(tok)  # 望遠鏡ライクの解析
                if result:
                    return result  # 解析に成功

        return None  # 解析に失敗

    def analyze_bouenkyo(self, verb):
        """
        引数verbをキーに望遠鏡ライクの解析を行う
        """
        # 左の子(NOUN)を集める
        nouns = []
        self.collect_lefts(nouns, verb.head, ('NOUN', ))
        if not len(nouns):
            return None
        noun = nouns[-1]  # 最も左の子を取り出す

        # 右の子からADPを探す
        adp = self.find_rights(noun, ('ADP', ))
        if adp is None:
            return None

        # 意味を探す
        imi = self.find_imi(noun.text)
        if imi is None:
            return None

        # 意味の活用を調べる
        katsuyou = imi['活用']
        if verb.lemma_ not in katsuyou:
            return None

        # 結果を生成
        return f'{noun.text}{adp.text}{verb.lemma_}'

    def find_rights(self, tok, poses_):
        """
        右の子を再帰的に検索し、poses_にマッチするpos_を持つトークンを返す
        """
        if tok.pos_ in poses_:
            return tok  # 見つかった

        for child in tok.rights:
            found = self.find_rights(child, poses_)  # 再帰呼び出し
            if found:
                return found  # 見つかった

        return None  # 見つからなかった

    def collect_lefts(self, dst, tok, poses_):
        """
        poses_にマッチするpos_を持つ左の子を再帰的に集める
        """
        if tok.pos_ in poses_:
            dst.append(tok)

        for child in tok.lefts:
            self.collect_lefts(dst, child, poses_)

    def find_imi(self, key):
        """
        引数keyをキーに意味データベースを参照する
        """
        if key in self.imi_db.keys():
            return self.imi_db[key]
        return None
  • 双眼鏡と望遠鏡で叩いた
  • 望遠鏡で泳ぐ彼女を見る
  • 望遠鏡で見たロボットを叩いた 以上の3文で意味解析を行いました。
    text = '双眼鏡と望遠鏡で叩いた'
    text2 = '望遠鏡で泳ぐ彼女を見る'
    text3 = '望遠鏡で見たロボットを叩いた'
    x = Analyzer()
    y_1 = x.analyze(text)
    y_2 = x.analyze(text2)
    y_3 = x.analyze(text3)
    print(y_1)
    print(y_2)
    print(y_3)

    出力結果は以下のようになりました。

    None
    望遠鏡で見る
    望遠鏡で見る

    1文目は「叩いた」が双眼鏡に係ってしまうためNoneが出力されました。2文目は泳いでいる彼女を望遠鏡で見たので望遠鏡で見るが出力されました。3文目は望遠鏡で見たロボットを何かしらで叩いたので、望遠鏡では見ることをしたので、望遠鏡で見るが出力される結果となりました。
    このように、簡易的ではありますが文章の意味を分析することができました。

文脈を理解したうえでの情報抽出

同じ文章でも、文脈によって異なる意味をもつことがあります。例えば、

ファイナンシャルプランナーの弟は会社経営者だ。

という文章があった場合に、前の文章で「◯◯さんの弟はファイナンシャルプランナーだ」という記述があれば、弟がファイナンシャルプランナーで会社の経営者となります。また、少し変ですが「◯◯さんはファイナンシャルプランナーだ」という記述があれば、◯◯さんはファイナンシャルプランナーで◯◯さんの弟が会社の経営者となります。
このようなことが起こるため自然言語処理では、前後の文章にも構文解析と意味解析を実施し、文脈を明らかにします。
なお、文章同士の関係性を明らかにするには言葉の意味や文法だけではなく、さまざまな知識が必要不可欠です。それゆえ、充分な実用性を誇る文脈解析システムはいまだに登場していません。
最近では照応解析[2]や談話解析[3]などの手法が提唱され、研究が進められています。私たちは、人間と同程度かそれ以上に文章を深く理解できるシステムを研究して行かなければなりません。

まとめ

今回は、自然言語処理の流れを書きました。自然言語処理は感情分析にも使用されます。感情分析の精度を上げるためにも文脈を理解した上で感情を推定することが必要です。文脈を読むことで情報を省略しても会話を成り立たせるのが日本語なので、英語とは異なり分析が難しいです。
文脈を読むことで機械らしくなく、私たちが自然だと感じる会話をするエージェントが出てくると面白いなと思います。

参考文献

[1]UniDic, https://clrd.ninjal.ac.jp/unidic/faq.html, 最終閲覧日:2022/7/18
[2]spaCyで「望遠鏡で~を見る」を解析する【自然言語処理, Python】,ユーニックス総合研究所, https://yu-nix.com/archives/spacy-telescope/, 最終閲覧日:2022/7/20
[3]杉村 和徳,松田 源立,原田 実,意味解析に基づく照応解析の研究,情報処理学会第69回全国大会,2,pp79-80,2007
[4]西原 鈴子,第6回 談話分析-ことばはどのように使われているか-,日本語教育通信第29号,1997

Previous Post Next Post