ブログ名

【第10回】5W1H抽出AI 係り受け解析を用いた手法

係り受け解析を用いた手法の検討

前回、係り受け解析による手法は文ごとに区切られていないと効果がないことが分かりまししたが、
Siri等の音声サービスを使用する場合は基本文章ごとの出力になるため、文ごとに区切られていることを前提として係り受け解析を使用した5W1Hの抽出を検討します。

まず、cabochaをによって係り受け解析を行なうと、出力は以下の構成になります。

tree
 └ token
   └ chunk     ←ない場合(NULL)もある
   │  └ link    ←かかり先ID
   │  └ head_pos  ←主辞
   │  └ func_pos  ←機能語
   │  └ score   ←スコア
   └ surface    ←形態素部分(元の文字列)
   └ feature    ←品詞や読みなど形態素の情報部分
   └ ne      ←??

https://qiita.com/ayuchiy/items/c3f314889154c4efa71eより引用

それぞれの単語の意味は下記です。
tree:文のようなもの
token:単語のようなもの
chunk:文節のようなもの
主辞:それだけで意味を持つ単語
機能語:それだけで意味を持たない単語
score:この数値が高いほど係やすい
※chunkはそのトークンがチャンクの最初の形態素である場合のみ存在

f:id:iTD_GRP:20200715135001p:plain

かかり先がない場合は token.chunk.link = -1になります。

f:id:iTD_GRP:20200715135028p:plain

以上のように、cabochaはMecabの拡張版と言えます。
そのため、WhenやWhere要素では前回までの手法を使いつつ、Who、What、Why、Howを前回までの手法 + 係り受けの関係から分類することで精度の向上が期待できます。

また、ソースコードは以下のようになりました。

ソースコード

ステップ1

python
import CaboCha

c = CaboCha.Parser('-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd')

words = []
poss = []
chunkId = [] 
toId = []
ID = -1
sentence = "年末年始をふるさとなどで過ごす人たちで各交通機関の混雑が始まりました。"
tree =  c.parse(sentence) 

for i in range(0, tree.size()):
    token = tree.token(i)
    # 単語を取得
    word = token.surface
    words.append(word) # 文字をスタック
    # 品詞を取得
    pos = token.feature.split(",")
    poss.append(pos) # 品詞をスタック
    # チャンクIDを取得
    if token.chunk != None:
            ID += 1
    chunkId.append(ID)
    # かかり先IDを取得
    if token.chunk != None:
            ID2 = token.chunk.link
    toId.append(ID2)

上記のコードを用いて、形態素解析による品詞、単語の取得、係り受け解析によるチャンクIDと係り先IDを取得します。

ステップ2

python
# 特徴的パターン
# 特徴的パターン
d = ['病院', '大学'] # Whereのパターン

dare = ['は'] # Whoの後に続く助詞
nani = ['が','を','の'] # Whatの後に続く助詞
naze =['だから', 'それで', 'そのため', 'このため', 'そこで', 'したがって', 'ゆえに', 'それゆえに','から','んで']# Whyの後に続く接続助詞
naze = ['ため']
suji = ['0','1','2','3','4','5','6','7','8','9']
itu1 = ['今日','昨日','明日','午前','午後','明後日','本日','年末'] #Whenのパターン1
itu2 = ['年','月','日'] #Whenのパターン2
itu3 = ''.join(itu1)
howId = []

num = len(words)-1
while num > -1:

##Where#
    if poss[num-1][2] == '地域' : # 一つ先を探索
            words.insert(num,'Where)')
            words.insert(num-1,'(') 
####

##When##
    for c in itu1:
        if c in words[num-1]: # 一つ先を探索
            words.insert(num,'When)')
            words.insert(num-1,'(')
            num -= 1

    for c in itu2:
        if (c in words[num-1]) and poss[num-1][1]=='固有名詞':
                for s in suji:
                    if s in words[num-1] :
                        words.insert(num,'When)')
                        words.insert(num-1,'(')
                        num -= 1


####

##Why##
    for c in naze:  
        if words[num] == c: # 続く助詞がWhyの後に続くものの場合
            words.insert(num,'Why)')
            nowId = chunkId[num] # 最初のチャンクIDを保存
        
            while num>0:
            
                if chunkId[num-1] != nowId and toId[num-1] > nowId and words[num-1] != '(': # 今のチャンクIdが最初のIdでなく, かかり先Idが最初のId出ない
                    break
                
                num -=1
        
            words.insert(num,'(')
        
    if words[num] == 'に' and words[num-1] == 'ため':
        words.insert(num,'Why)')
        nowId = chunkId[num] # 最初のチャンクIDを保存
        
        while num>0:
            
            if chunkId[num-1] != nowId and toId[num-1] > nowId and words[num-1] != '(': # 今のチャンクIdが最初のIdでなく, かかり先Idが最初のId出ない
                break
                
            num -=1
        
        words.insert(num,'(')
        

####    

##How##    
    if toId[num] == -1:
        words.insert(num+1,'How)')
        howId.append(chunkId[num])
        while num > 0:
            num -= 1
            if (poss[num][0] =='名詞' and poss[num][1] != '非自立' and poss[num][1] != '接尾' and poss[num-1][6] != 'という'):
                break

            howId.append(chunkId[num])
            
        words.insert(num,'(')
        
####

##Who##    
    for c in dare:    
        if poss[num][0] == '助詞' and words[num] == c and toId[num] in howId: # 続く助詞がWhoの後に続くものの場合
                words.insert(num,'Who)')
                while num > 0:
                    if (poss[num][0] =='名詞' and poss[num][1] != '非自立' and poss[num][1] != '接尾') or (poss[num][1] == '自立'  and poss[num][6] != 'する' and poss[num][6] != 'なる'):
                        break
                    num -= 1
                    

                words.insert(num,'(')

####

##What## 
        
    for c in nani:
        if poss[num][0] == '助詞' and words[num] == c and toId[num] in howId: # 続く助詞がWhatの後に続くものの場合1
            words.insert(num+1,'What)')
            while num > 0:
                if (poss[num][0] =='名詞' and poss[num][1] != '非自立' and poss[num][1] != '接尾') or (poss[num][1] == '自立'  and poss[num][6] != 'する' and poss[num][6] != 'なる'):
                    break
                num -= 1
                    


        
            words.insert(num,'(')    
#            num += 1
            
####
        
    else:
        num -= 1
        

主な変更点は、Howの取得時にHow要素のチャンクIDを取得し、Howのチャンクに係るWhat要素とWho要素を探して抽出することです。
また、How要素を決定する際に、複数チャンクに渡ってHow要素が構成される場合はHow要素すべてのチャンクをHowのチャンクとするようにしています。
Why要素は係り受けの関係を使うよりも、助詞などの関係から抽出した方がよさそうです。
その他に、How要素の判定を少し甘くして、「どのようなのか」がわかりやすくなるようにしました。

例えば、入力が

「年末年始をふるさとなどで過ごす人たちで各交通機関の混雑が始まりました。」  

の時、今までの抽出器だと下記の通り出力されましたが、この場合「過ごす」はHow要素ではないため誤っていました。

「 (年末年始When)をふるさとなどで, (過ごすHow)人たちで各交通機関の, (混雑Who)が, (始まりましたHow)。」

これが、今回の係る受け解析を用いた手法だと、下記の通りに出力されます。

「(年末年始When)をふるさとなどで過ごす人たちで各(交通機関のWhat)(混雑が始まりました。How)」

そのほか何パターンか入力してみました。

入力:「会見があるから見ると聞かれるなっていうのがやっぱり想像できるじゃないですか。」
出力:「(会見があるWhy)から見ると(聞かれるなっていうのがWhat)やっぱり(想像できるじゃないですか。How)」
入力:「ええ答えなきゃいけなくなることを回避するために皆さんには申し訳ないですけど見ないという手段を僕はとりました。」
出力:「ええ(答えなきゃいけなくなることを回避するためWhy)に皆さんには申し訳ないですけど見ないという(手段をWhat)(僕はとりました。How)」
入力:「箱根の駅伝があるということで神奈川県を中心にお天気を伺えればと思います。」
出力:「(箱根Where)の駅伝があるということで(神奈川県Where)を中心に(お天気を伺えればと思います。How)」

このように、きちんと助詞があれば、概ね正確な抽出ができています。

次回の予定

助詞がない場合の処理について検討していきます。


次の記事へ

前の記事へ 戻る