MeCabの半角スペース、全角スペース、タブ、改行に対する挙動について

某所でMeCabは半角スペースを無視するというコメントを見かけ、ちょっと疑問に思ったので調べました。そのついでに、スペースと似たような文字(全角スペースやタブ、改行など)についても調査しています。
ちなみに、Pythonラッパーの mecab-python3==1.0.4 で動作確認していますがコマンドラインの生MeCabでも挙動は同様です。

まず前提として、単語(形態素)の途中に半角スペースが入った場合、MeCabはその半角スペースの位置で単語を区切ります。こういう意味ではMeCabは半角スペースを無視しないと言えます。

print(tagger.parse("メロスは激怒した"))
"""
メロス	名詞,一般,*,*,*,*,*
は	助詞,係助詞,*,*,*,*,は,ハ,ワ
激怒	名詞,サ変接続,*,*,*,*,激怒,ゲキド,ゲキド
し	動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
た	助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
EOS
"""

# 激怒 の激と怒の間に半角スペースを挟むと単語が割れる
print(tagger.parse("メロスは激 怒した"))
"""
メロス	名詞,一般,*,*,*,*,*
は	助詞,係助詞,*,*,*,*,は,ハ,ワ
激	名詞,サ変接続,*,*,*,*,激,ゲキ,ゲキ
怒	動詞,自立,*,*,五段・ラ行,体言接続特殊2,怒る,イカ,イカ
し	動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
た	助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
EOS*,特殊・タ,基本形,た,タ,タ
EOS
"""

これだけ見ると、MeCabは半角スペースを無視しないじゃないか、という話なのですが、実はそれは僕の早とちりで、MeCabが半角スペースを無視するケースはありました。それは単語と単語の間に半角スペースがあった場合です。

今度は「激怒」の前に半角スペースを置いてみます。そして事業をもっと正確に見るために、単語の生起コストと連接コストを出力するようにします。
参考: MeCabの出力形式を変更する
%cが生起コストで、%pcが連接コストです。

# コストを表示する設定でtaggerを生成
tagger = MeCab.Tagger(f"-d {dicdir}/ipadic" +
                      r" -F %m\\t%c\\t%pC\\t%H\\n"
                     )

print(tagger.parse("メロスは激怒した"))
"""
メロス	9461	-283	名詞,一般,*,*,*,*,*
は	3865	-3845	助詞,係助詞,*,*,*,*,は,ハ,ワ
激怒	4467	238	名詞,サ変接続,*,*,*,*,激怒,ゲキド,ゲキド
し	8718	-5350	動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
た	5500	-7956	助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
EOS
"""

# 激怒の前(はの後ろ)に半角スペースを挟む
print(tagger.parse("メロスは 激怒した"))
"""
メロス	9461	-283	名詞,一般,*,*,*,*,*
は	3865	-3845	助詞,係助詞,*,*,*,*,は,ハ,ワ
激怒	4467	238	名詞,サ変接続,*,*,*,*,激怒,ゲキド,ゲキド
し	8718	-5350	動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
た	5500	-7956	助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
EOS
"""

結果が全く同じになりましたね。注目すべきは、はと激怒の間の連接コスト、238です。
前の単語との連接コストが計算されているのですが、空白との連接コストではなくその前の「助詞,系助詞のは」との連接コストが使われています。
この例では、半角スペースは無視された、と言えるでしょう。

ちなみに、タブ、改行(\n)は半角スペース同様に無視されます。一方で全角スペース、改行(\r\n)は無視されません。(正確には\r\nの\n部分は無視されますが、\rが記号,一般として残ります。)

# タブは無視される
print(tagger.parse("メロスは\t激怒した"))
"""
メロス	9461	-283	名詞,一般,*,*,*,*,*
は	3865	-3845	助詞,係助詞,*,*,*,*,は,ハ,ワ
激怒	4467	238	名詞,サ変接続,*,*,*,*,激怒,ゲキド,ゲキド
し	8718	-5350	動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
た	5500	-7956	助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
EOS
"""

# 改行(\n)も無視される
print(tagger.parse("メロスは\n激怒した"))
"""
メロス	9461	-283	名詞,一般,*,*,*,*,*
は	3865	-3845	助詞,係助詞,*,*,*,*,は,ハ,ワ
激怒	4467	238	名詞,サ変接続,*,*,*,*,激怒,ゲキド,ゲキド
し	8718	-5350	動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
た	5500	-7956	助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
EOS
"""

# 全角スペースは無視されない
print(tagger.parse("メロスは 激怒した"))
"""
メロス	9461	-283	名詞,一般,*,*,*,*,*
は	3865	-3845	助詞,係助詞,*,*,*,*,は,ハ,ワ
 	1287	-355	記号,空白,*,*,*,*, , , 
激怒	4467	341	名詞,サ変接続,*,*,*,*,激怒,ゲキド,ゲキド
し	8718	-5350	動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
た	5500	-7956	助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
EOS
"""

# 改行(\r\n)も無視されない。printすると見えませんが表層形に\r だけ残ります。\nは消えて。
print(tagger.parse("メロスは\r\n激怒した"))
"""
メロス	9461	-283	名詞,一般,*,*,*,*,*
は	3865	-3845	助詞,係助詞,*,*,*,*,は,ハ,ワ
	4769	-71	記号,一般,*,*,*,*,*
激怒	4467	-272	名詞,サ変接続,*,*,*,*,激怒,ゲキド,ゲキド
し	8718	-5350	動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
た	5500	-7956	助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
EOS
"""

4パターンとも激怒の前に各記号を入れましたが、無視されるものと考慮されるものに分かれました。全角スペースや\rについては形態素の一つとして扱われ、その次の単語である「激怒」の前の単語との連接コストの計算にも反映されています。

テキストデータを前処理するときに、表記揺れの対応として全角スペースを半角スペースに置き換えたり、改行コードを\r\nを\nに揃えたりといったことをよくやっていたのですが、この操作はその後の形態素解析の結果に影響を与えてしまっていたのですね。
大体その後、改行を空白スペースに置換したりするのですが、これは影響なさそうです。

この改行を無視して、その前の単語との間の連接コストが形態素解析の結果に反映されるというのは僕にとっては非常に驚きでした。(BOSを挿入してるわけではないので、言われてみればそうかという気もしますが。)

例えば、次の2つのテキスト中の「中国語を勉強します。」の形態素分析結果が違う、ということが予想できた人ってあまりいなのではないでしょうか。

text1 = """来月から留学します。
中国語を勉強します。"""
text2= """来月から留学します
中国語を勉強します"""

# 文末に。があるテキストの場合
print(tagger.parse(text1))
"""
来月	5123	-316	名詞,副詞可能,*,*,*,*,来月,ライゲツ,ライゲツ
から	4159	-4367	助詞,格助詞,一般,*,*,*,から,カラ,カラ
留学	5355	2	名詞,サ変接続,*,*,*,*,留学,リュウガク,リューガク
し	8718	-5350	動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
ます	5537	-9478	助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス
。	215	-3050	記号,句点,*,*,*,*,。,。,。
中国語	5383	-952	名詞,一般,*,*,*,*,中国語,チュウゴクゴ,チューゴクゴ
を	4183	-4993	助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
勉強	4452	-1142	名詞,サ変接続,*,*,*,*,勉強,ベンキョウ,ベンキョー
し	8718	-5350	動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
ます	5537	-9478	助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス
。	215	-3050	記号,句点,*,*,*,*,。,。,。
EOS
"""

# 句読点が省略されたテキストの場合
print(tagger.parse(text2))
"""
来月	5123	-316	名詞,副詞可能,*,*,*,*,来月,ライゲツ,ライゲツ
から	4159	-4367	助詞,格助詞,一般,*,*,*,から,カラ,カラ
留学	5355	2	名詞,サ変接続,*,*,*,*,留学,リュウガク,リューガク
し	8718	-5350	動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
ます	5537	-9478	助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス
中国	4757	825	名詞,固有名詞,地域,国,*,*,中国,チュウゴク,チューゴク
語	7810	-7313	名詞,接尾,一般,*,*,*,語,ゴ,ゴ
を	4183	-4541	助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
勉強	4452	-1142	名詞,サ変接続,*,*,*,*,勉強,ベンキョウ,ベンキョー
し	8718	-5350	動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
ます	5537	-9478	助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス
"""

text1の方は、「中国語」という単語が登場しましたが、text2の方は、「中国」と「語」という2つの単語に割れましたね。
これは、改行を無視して、その前の単語である「助動詞のます」との連接が考慮された結果になります。「記号,句点の。」と「中国語」の連接コストは小さいですが、「ます」と「中国語」の連接コストは大きい(1436)ので「中国語」という形態素が採用されなかったのです。(中国と語の連接コストが非常に小さく、単語が一つ増えるデメリットがあまりなかったのも要因)

以上をまとめると、以下のようになるでしょうか。
– 全角スペースや\rは他の文字と同じように形態素(単語)として扱われる。
– 半角スペース、タブ、改行(\n)は区切り位置として使われその位置で必ず形態素は切られる。
– 半角スペース、タブ、改行(\n)はそれ自体は形態素としては扱わず結果にも表示されない。
– 半角スペース、タブ、改行(\n)は連接コストの計算時は無視される。

半角スペースも形態素結果に表示してほしいよ、という場合は、表示形式のオプションで、%Mを使うことで表示できます。半角スペースの次の単語の表層系に存在したスペースをくっつけて表示してくれるようです。ただ、正直これを使う場面がすぐには思いつきません。

# %m の代わりに %M を使うと半角スペースも表示される
tagger = MeCab.Tagger(f"-d {dicdir}/ipadic" +
                      r" -F %M\\t%H\\n"
                      )

print(tagger.parse("メロスは 激怒した"))
"""
メロス	名詞,一般,*,*,*,*,*
は	助詞,係助詞,*,*,*,*,は,ハ,ワ
 激怒	名詞,サ変接続,*,*,*,*,激怒,ゲキド,ゲキド
し	動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
た	助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
EOS
"""

激怒、の前にスペースが入って半角1文字分字下げされているのがわかりますね。
読みはゲキドだけなので、原型、読み、発音では無視されたままであることもわかります。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です