nikkie-ftnextの日記

イベントレポートや読書メモを発信

『入門 自然言語処理』12章から、分かち書きした日本語のテキストがNLTKに読み込め、扱いは意外と英語テキストと共通と学びました

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
2019年12月末から自然言語処理のネタで毎週1本ブログを書いています。

2/3の週から自然言語処理の基礎固めとして『入門 自然言語処理』に取り組んでいます。

入門 自然言語処理

入門 自然言語処理

今週は日本語版限定の12章「Python による日本語自然言語処理」の一部に取り組みました。

  • 日本語のテキストはNLTKでどのように扱うのか知る
  • 形態素解析MeCabを使う

※前提として、週1ブログの取り組みの中で、日本語のテキストはjanomeで扱ってきました1

12章は以下で公開されています:

公開されている12章のコードは書籍と同様にPython2系向けのようです2

目次

動作環境

今週の開始時は先週までと同じ環境です。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G103
$ python -V  # venvによる仮想環境を使用
Python 3.7.3
$ pip list  # grepを使って抜粋して表示
beautifulsoup4   4.8.2
ipython          7.12.0
matplotlib       3.1.3
nltk             3.4.5
wordcloud        1.6.0

ファイル配置

.
├── ch12_ja  # 日本語のテキストを配置
├── env
├── # 日本語のWordCloud作成のために置いたフォントファイルなどは省略

扱う日本語テキストの取得

青空文庫から宮沢賢治の『銀河鉄道の夜』を使います。
宮沢賢治 銀河鉄道の夜

書籍では取得が省略されているので、3章の復習がてら手を動かしてみました。
urllibを使ってWebから取得し、HTMLタグを削除します。
エンコーディングがShift JISでした!(UTF-8だと想定していたらUnicodeDecodeErrorで落ちました)

<?xml version="1.0" encoding="Shift_JIS"?>
In [12]: import urllib.request

In [20]: from bs4 import BeautifulSoup

In [21]: with urllib.request.urlopen('https://www.aozora.gr.jp/cards/000081/files/456_15050.html') as f:
    ...:     html_bytes = f.read()
    ...:     html = text_bytes.decode('shift-jis')
    ...:

In [22]: soup = BeautifulSoup(html, 'html.parser')

In [23]: text = soup.get_text()

In [24]: with open('ch12_ja/gingatetsudono_yoru.txt', 'w') as fout:
    ...:     fout.write(text)
    ...:

この方法だとhead要素のtitleタグが残り、先頭に空行が多かったので、bodyタグを指定するのがよさそうです(本と実行結果を比べづらかったので手動で削除しました) 。

分かち書きされていないコーパスの扱い

上で取得した『銀河鉄道の夜』をNLTKのコーパスとして読み込みます。
先週の5章の取り組みではbrownコーパスを使いましたが、ここでは『銀河鉄道の夜』のテキストからgingaコーパスを作ります。

テキストの読み込みにはnltk.corpus.reader.PlaintextCorpusReaderを使います。

イニシャライザに渡す引数は4

の5つです。

In [29]: from nltk import RegexpTokenizer, Text

In [30]: from nltk.corpus.reader import PlaintextCorpusReader

In [31]: from nltk.corpus.reader.util import read_line_block

word_tokenizer, sent_tokenizerにはnltk.tokenize.regexp.RegexpTokenizerを渡します。

sent_tokenizer(文の区切り方を指定)はセリフの「」を考慮した上で、!または?または。で区切っています。

In [32]: jp_sent_tokenizer = RegexpTokenizer(r'[^ 「」!?。]*[!?。]')

word_tokenizer(単語の区切り方を指定)は、連続するひらがな、カタカナ、漢字を単語をすると仮で決めています。
例えば「一つ一つ」が「一/つ/一/つ」と区切られてしまうので、この分け方はあくまで仮のものです。

In [33]: jp_char_tokenizer = RegexpTokenizer(r'([ぁ-んー]+|[ァ-ンー]+|[\u4E00-\u9FFF]+|^[ぁ-んァ-ンー\u4E00-\u9FFF]+)')

コーパスを作りましょう。

In [34]: ginga = PlaintextCorpusReader('ch12_ja', 'gingatetsudono_yoru.txt',
    ...:     encoding='utf-8', para_block_reader=read_line_block,
    ...:     sent_tokenizer=jp_sent_tokenizer, word_tokenizer=jp_char_tokenizer)
    ...:

コーパスからは

  • rawメソッドでPlaintextCorpusReaderに渡したファイルの全文(複数ある場合は連結)
  • wordsメソッドでトークンのリスト

を取得できます。

In [38]: print(ginga.raw()[:50])
銀河鉄道の夜
宮沢賢治




一、午后(ごご)の授業

「ではみなさんは、そういう

In [39]: print('/'.join(ginga.words()[:50]))
銀河鉄道/の/夜/宮沢賢治/一/午后/ごご/の/授業/ではみなさんは/そういうふうに/川/だと/云/い/われたり/乳/の/流/れたあとだと/云/われたりしていたこのぼんやりと/白/いものがほんとうは/何/かご/承知/ですか/先生/は/黒板/に/吊/つる/した/大/きな/黒/い/星座/の/図/の/上/から/下/へ/白/くけぶった/銀河帯

wordsメソッドは5章で使ったbrownにもありました。
NLTKのコーパスとして読み込むことで、英語でも日本語でも扱い方(インターフェース)が揃うと理解しました。

トークンのリストをnltk.text.Textに変換します。

In [40]: ginga_t = Text(w for w in ginga.words())  # ginga_t = Text(ginga.words()) で済むように思われる

In [41]: ginga_t.concordance('川')
Displaying 25 of 57 matches:
 の 夜 宮沢賢治 一 午后 ごご の 授業 ではみなさんは そういうふうに 川 だと 云 い われたり 乳 の 流 れたあとだと 云 われたりしていたこのぼ
がするのでした 先生 はまた 云 いました ですからもしもこの 天 あま の 川 がわ がほんとうに 川 だと 考 えるなら その 一 つ 一 つの 小 さな
# [省略]

concordanceメソッドで指定された単語について、索引を表示しています。

分かち書きされたコーパスの扱い

NLTKに用意されたjeitaコーパスを読み込みます。

In [43]: import nltk

In [45]: nltk.download('jeita')
Out[45]: True

[nltk_data] Downloading package jeita to
[nltk_data]     /Users/.../nltk_data...

/Users/.../nltk_data/corpora/にはjeita.zipがダウンロードされますが、展開されません。
そこでコマンドラインからunzipしました

In [49]: !unzip ~/nltk_data/corpora/jeita.zip -d ~/nltk_data/corpora/jeita

In [50]: !ls ~/nltk_data/corpora/jeita/jeita/
README      a0680.chasen    a1370.chasen    a2060.chasen    g0037.chasen
# [省略]

READMEの内容はこちらでも確認できました:

このコーパスChaSen形式で分かち書きされています。
ChaSen形式とは、各語について

  1. 出現形
  2. 読み
  3. 原形
  4. 品詞
  5. 活用

がタブ区切りで並んだ形式です(後ろの項目は品詞によってはありません)。

In [67]: !head ~/nltk_data/corpora/jeita/jeita/a0010.chase
    ...: n
      記号-空白
新潟  ニイガタ    新潟  名詞-固有名詞-地域-一般
の ノ の 助詞-連体化
停車場   テイシャジョウ   停車場   名詞-一般
を ヲ を 助詞-格助詞-一般
出る  デル  出る  動詞-自立   一段  基本形
と ト と 助詞-接続助詞
列車  レッシャ    列車  名詞-一般
の ノ の 助詞-連体化
箱 ハコ  箱 名詞-一般

読み込むにはnltk.corpus.reader.ChasenCorpusReaderを使います。

In [52]: from nltk.corpus.reader import ChasenCorpusReader

In [55]: jeita = ChasenCorpusReader('/Users/.../nltk_data/corpora/jeita/', '.*chasen', encoding='utf-8')

In [56]: print('/'.join(jeita.words()[22100:22140]))
たい/という/気持/が/、/この上なく/純粋/に/、/この上なく/強烈/で/あれ/ば/、/ついに/は/そのもの/に/なれる/。/なれ/ない/の/は/、/まだ/その/気持/が/そこ/まで/至っ/て/い/ない/から/だ/。/法

分かち書きされており、品詞タグ付け済みのため、5章で扱ったbrownのようにtagged_sentメソッドが使えました。

In [63]: tab = '\t'

In [64]: print('\nEOS\n'.join(['\n'.join(f'{w[0]}/{w[1].split(tab)[2]}' for w in sent) for sent in jeita.tagged_sents()[2170:2171]]))
を/助詞-格助詞-一般
まくっ/動詞-自立
た/助動詞
とき/名詞-非自立-副詞可能
# [省略]

※EOSが出力されなかったので、コードを写し間違えているかもしれません5

MeCabを導入する

12.2で登場するMeCabを導入します。
導入方法は『Pythonによるあたらしいデータ分析の教科書』にならいました。

$ brew install mecab-ipadic  # 依存関係にあるmecabもインストールされる
$ mecab -v
mecab of 0.996

動作確認です。

$ mecab  # 対話的に使います
天気の子
天気  名詞,一般,*,*,*,*,天気,テンキ,テンキ
の 助詞,連体化,*,*,*,*,の,ノ,ノ
子 名詞,一般,*,*,*,*,子,コ,コ
EOS

最新語に対応できるようmecab-ipadic-NEologdも入れました。

$ git clone --depth 1 git@github.com:neologd/mecab-ipadic-neologd.git  # ホームディレクトリで実行しています
$ cd mecab-ipadic-neologd/
$ ./bin/install-mecab-ipadic-neologd -n

先ほどの例を使うと、最新語に対応できているかも確認できます。

$ mecab -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd/
天気の子
天気の子    名詞,固有名詞,一般,*,*,*,天気の子,テンキノコ,テンキノコ
EOS

別の日本語テキストを試す:先人の残したまどマギのセリフ

12章を調べながら取り組んでいたところ、偉大な先人(yutakikuchiさん)の功績を偶然にも発見しました。

ここまでに学んだことから

  1. まどマギのテキストをMeCabChaSen形式にする
  2. ChaSen形式の分かち書きされたテキストをChasenCorpusReaderコーパスとして読み込む
  3. コーパスに含まれるトークンからTextを作るとconcordanceが見られる

んじゃないかと電波を受信し、手を動かしてみました。Let's try!

yutakikuchiさんがリポジトリに残したまどマギのセリフテキストを取得します。

$ wget https://raw.githubusercontent.com/yutakikuchi/NLTK/master/madmagi/madmagi_corpus-euc.txt -O ch12_ja/madomagi-euc.txt

文字コードEUC JPだったので、PythonUTF-8に変換してもらいます。

In [69]: with open('ch12_ja/madomagi-euc.txt', 'rb') as fin, open('ch12_ja/madom
    ...: agi_utf8.txt', 'wb') as fout:
    ...:     euc_bytes = fin.read()
    ...:     euc_text = euc_bytes.decode('euc_jp')
    ...:     utf8_bytes = euc_text.encode()
    ...:     fout.write(utf8_bytes)
    ...:

MeCabChaSen形式にします。
人名に対応できるようにneologdを指定しています。

$ mecab -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd/ -O chasen < ch12_ja/madomagi_utf8.txt > ch12_ja/madomagi.chasen

以上が1です。
では2でNLTKのコーパスとして読み込みましょう。

In [71]: madomagi = ChasenCorpusReader('ch12_ja/', 'madomagi.chasen', encoding='utf-8')

In [73]: madomagi_t = Text(madomagi.words())

concordanceを見てみます。

In [74]: madomagi_t.concordance('助ける')
Displaying 1 of 1 matches:
 … ? 死ん じゃ うって 、 わかっ て た のに … 。 私 なんか 助ける より も 、 あなたに … … 生き て て ほしかっ た のに … あな

In [75]: madomagi_t.concordance('助け')
Displaying 1 of 1 matches:
よ ね うん … キュゥべえ に 騙さ れる 前 の バカ な 私 を 、 助け て あげ て くれ ない か な ? 約束 する わ 。 絶対 に あなた

In [78]: madomagi_t.concordance('いや')
Displaying 1 of 1 matches:
思う ん だ 鹿目 さん … さよなら 。 ほむらちゃん 。 元気 で ね いや ! 行かないで … 鹿目 さ ぁぁぁん ! ! どうして … ? 死ん じ

yutakikuchiさんは

事前の予想では「いやだ」とか「助けて」などの台詞が頻繁に使われていると考えた

と綴っていますが、予想通り「助ける」や「いや」はネガティブなセリフで使われていますね。

あとは、まどマギを象徴するこの語句:

In [80]: madomagi_t.concordance('魔法少女')
Displaying 7 of 7 matches:
変え られる の ? もちろん さ 。 だから 僕 と 契約 し て 、 魔法少女 に なっ て よ ! 私 は 巴マミ あなた たち と 同じ 、 見滝
 、 見滝 原中 の 3 年生 そして キュゥべえ と 契約 し た 、 魔法少女 よ は あー はぁ 。 うん やあ はい 、 これ う わぁ … 。 い
夫だよ 、 ほむらちゃん あ 、 あなた たち は … 彼女たち は 、 魔法少女 。 魔女 を 狩る 者 たち さ いきなり 秘密 が バレ ちゃっ た
 ない ! 鹿目 さん まで 死ん じゃう よ ? それでも 、 私 は 魔法少女 だ から 。 みんな の こと 、 守ら なきゃ いけ ない から ねぇ
 時 、 間に合っ て 。 今 でも それ が 自慢 な の だから 、 魔法少女 に なっ て 、 本当に よかっ た って 。 そう 思う ん だ 鹿目
 は 心臓 の 病気 で ずっと ・ ・ ・ あ 鹿目 さん 、 私 も 魔法少女 に なっ た ん だ よ ! これから 一緒 に 頑張ろ う ね ! え
 られる の ? もちろん さ 。 だから 、 僕 と 契約 し て 、 魔法少女 に なっ て よ ! ダメ ぇぇ ぇぇ ぇぇ ぇぇ ぇぇ ぇぇ ! !

魔法少女は「契約してなるもの」「魔女を狩る者」とわかりますね(まどマギを知らない方がどの程度分かるかは未知数ですが)。

まとめ

日本語テキストの扱いですが、12章を少し読んだところ、以下の方法でNLTKのコーパスとして扱えそうという認識です。

  1. MeCabChaSen形式に分かち書き
  2. NLTKのChasenCorpusReaderコーパスとして読み込む(brownなどのコーパスと同じように扱える)

NLTKにコーパスとして取り込めれば、英語テキストと日本語テキストの扱いにそれほど大きな違いはないというのが暫定的な結論です。

感想

まどマギのテキストの扱いが受信した電波の通りにできたので、「もしかしてどんな日本語テキストでもこの扱いでいける!?」という期待半分、間違っているかもという不安半分という心境です。

不安要素は12章が1-2割程度しか読み進められず、この扱いに見落としがあるかもしれないと思うからです。
時間を見つけて読み進められればと思っています。

今回はconcordanceを使いましたが、Textでできることに他に何があるのか見ておきたいところです(similarityもあったように思います)。
また、5章で見たのと同様に指定した品詞の単語の取り出しもできそうです。

次回は『入門 自然言語処理』6章「テキスト分類の学習」に取り組む予定です。


  1. 例えば、「Janome ではじめるテキストマイニング」の中のWordCloudのチュートリアルに取り組み、janomeを全然使いこなせていなかったと思い知りました - nikkie-ftnextの日記

  2. 公開されている英語版はPython 3系で書き直されています。例 3.3 ch03.rst2

  3. ドキュメントによれば「Unicode 実装で使用される現在のデフォルトエンコーディング名を返」すので、期待通りの結果ですね

  4. ドキュメントにはっきり書いていないので、コードを見ました

  5. tab変数はSyntaxError: f-string expression part cannot include a backslashへの対応です ref: https://stackoverflow.com/a/44780467