はじめに
頑張れば、何かがあるって、信じてる。nikkieです。
2019年12月末から自然言語処理のネタで毎週1本ブログを書いています。
今週は、1本目のブログで作ったWordCloudに使っているjanome
について、チュートリアルに取り組んでの学びをまとめます。
前回までのnikkieとjanome
過去にjanome
を形態素解析に使って、CfPや自分のブログのWordCloudを作っています。
その中でjanomeのドキュメントを見たところ、形態素解析以外に前処理・後処理もできるらしいと分かり1、janomeのチュートリアルをブログ駆動開発のネタリストに入れていました。
今回チュートリアルに取り組んでみて、「わたし、janome
のこと、なんにも分かってなかったんだ。。」という心境です。
janomeのチュートリアル
取り組んだのは「Janome ではじめるテキストマイニング」です。
Google Colaboratoryでもできますが、今回は手元のマシンで実行しました。
hands-onフォルダのnotebookのうち、01〜03を写経しました。
宮沢賢治の『風の又三郎』のWordCloudを作ります。
動作環境
$ sw_vers ProductName: Mac OS X ProductVersion: 10.14.6 BuildVersion: 18G103 $ python -V # venvによる仮想環境を利用 Python 3.8.1 $ janome --version janome 0.3.10 $ pip list | grep wordcloud wordcloud 1.6.0 $ pip list | grep matplotlib matplotlib 3.1.2
フォルダ配置
Pythonのスクリプトもカレントディレクトリに置いていますが、配置から省略しています。
. ├── data │ └── kazeno_matasaburo_utf8.txt ├── env # 仮想環境 ├── ipagp.ttf ├── stop_words.txt # ストップワードを列挙したファイル ├── text_sentences.txt └── udic.csv # ユーザ定義辞書
- WordCloudに日本語を表示するためのIPAフォントは、こちらからダウンロード:IPAフォントのダウンロード
- テキストデータの入手:
wget https://raw.githubusercontent.com/mocobeta/janome-tutorial/master/hands-on/data/kazeno_matasaburo_utf8.txt -O data/kazeno_matasaburo_utf8.txt
(目次) チュートリアルに取り組んでの学び
以下の順で学んだことや気づいたことを記していきます。
- コマンドラインから
janome
コマンドが使えた Tokenizer
を使った形態素解析(と分かち書き)- Pythonicなファイル操作
Analyzer
を使った形態素解析 + 前処理・後処理- WordCloud(小ネタ)
- チュートリアルのコードで気になる点
janome
コマンド
初めて知ったのですが、pip install janome
すると、janome
コマンドが使えるようになります。
オプション無しでjanome
コマンドを実行した後は、形態素解析したい一文を入力してEnterを押すと結果が出ます。
$ janome すもももももももものうち すもも 名詞,一般,*,*,*,*,すもも,スモモ,スモモ も 助詞,係助詞,*,*,*,*,も,モ,モ もも 名詞,一般,*,*,*,*,もも,モモ,モモ も 助詞,係助詞,*,*,*,*,も,モ,モ もも 名詞,一般,*,*,*,*,もも,モモ,モモ の 助詞,連体化,*,*,*,*,の,ノ,ノ うち 名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
パイプを使ってjanome
コマンドを呼び出すこともできます。
$ echo "テキストマイニングを始めよう" | janome テキスト 名詞,一般,*,*,*,*,テキスト,テキスト,テキスト マイニング 名詞,サ変接続,*,*,*,*,マイニング,マイニング,マイニング を 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ 始めよ 動詞,自立,*,*,一段,未然ウ接続,始める,ハジメヨ,ハジメヨ う 助動詞,*,*,*,不変化型,基本形,う,ウ,ウ
janome
コマンドの主なオプションは次の通り(janome -h
で確認できます)。
--udic
:ユーザ定義辞書ファイルのパスの指定--udic-type
:ユーザ定義辞書のタイプの指定(デフォルトはipadic。simpledicも取れる)-g
:「ラティスグラフ」のpngファイルを出力(要graphviz2)
ユーザ定義辞書を使って形態素解析し、ラティスグラフを出力する例
In [23]: !echo '美ら海図面コンクール' | janome --udic udic.csv --udic-type simpledic -g 美ら海 カスタム名詞,*,*,*,*,*,美ら海,チュラウミ,チュラウミ 図面 名詞,一般,*,*,*,*,図面,ズメン,ズメン コンクール 名詞,一般,*,*,*,*,コンクール,コンクール,コンクール Graph was successfully output to lattice.gv.png
$ cat udic.csv 美ら海,カスタム名詞,チュラウミ
Tokenizer
コマンドラインからの使い方を知った後はAPIについて。
使ったことのあるTokenizer
ですが、ドキュメントをたどりつつ見ていきます。
In [1]: from janome.tokenizer import Tokenizer In [2]: t = Tokenizer() In [9]: for token in t.tokenize('テキストマイニングを始めよう'): ...: print(token) ...: テキスト 名詞,一般,*,*,*,*,テキスト,テキスト,テキスト マイニング 名詞,サ変接続,*,*,*,*,マイニング,マイニング,マイニング を 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ 始めよ 動詞,自立,*,*,一段,未然ウ接続,始める,ハジメヨ,ハジメヨ う 助動詞,*,*,*,不変化型,基本形,う,ウ,ウ
Tokenizer.tokenize
ですが、デフォルトではstream
引数とwakati
引数がどちらもFalse
なので、Tokenのリスト3が返ります。
(上記のコードは、t.tokenize('テキストマイニングを始めよう')
の返すリストの各要素について処理をしています)
では、Tokenとは何でしょうか。
手を動かした感触としては、「(解析された)形態素と辞書に登録された情報を表すもの」になるかと思います(ドキュメント)
In [10]: token = t.tokenize('テキストマイニングを始めよう')[3] # 「始めよ」を取り出す In [11]: token.base_form, token.infl_form, token.infl_type, token.part_of_speech ...: , token.phonetic, token.reading, token.surface Out[11]: ('始める', '未然ウ接続', '一段', '動詞,自立,*,*', 'ハジメヨ', 'ハジメヨ', '始めよ')
print(token)
をしたとき、以下の対応で表示されています4:
token.surface
:表層形'始めよ'
token.part_of_speech
:品詞'動詞,自立,*,*'
token.infl_type
:活用型'一段'
token.infl_form
:活用形'未然ウ接続'
token.base_form
:基本形'始める'
token.reading
:読み'ハジメヨ'
token.phonetic
:発音'ハジメヨ'
基本形の取り出し方
In [17]: for token in t.tokenize('テキストマイニングを始めました'): ...: print(token.base_form) ...: テキスト マイニング を 始める ます た
表層系の取り出し方
In [16]: for token in t.tokenize('テキストマイニングを始めよう'): ...: print(token.surface) ...: テキスト マイニング を 始めよ う
またtokenize
メソッドのwakati
引数をTrue
に指定5することで、表層形(str型)のリストが返り、token.surface
というアクセスは不要になります。
すごくシンプルに書けるので、今回知ってよかったです。
In [2]: t.tokenize('テキストマイニングを始めよう', wakati=True) Out[2]: ['テキスト', 'マイニング', 'を', '始めよ', 'う']
補足ですが、Tokenizer
を作るときに、ユーザ定義辞書を指定できます。
In [19]: t = Tokenizer('udic.csv', udic_type='simpledic') In [20]: for token in t.tokenize('美ら海図面コンクール'): ...: print(token) ...: 美ら海 カスタム名詞,*,*,*,*,*,美ら海,チュラウミ,チュラウミ 図面 名詞,一般,*,*,*,*,図面,ズメン,ズメン コンクール 名詞,一般,*,*,*,*,コンクール,コンクール,コンクール
Pythonicなファイルの扱い
少しjanome
から離れてPythonの話を。
組み込み関数openのencoding引数
チュートリアルではencoding
引数を'utf8'
と指定しています。
デフォルト値はNone
で、指定しないとプラットフォームに依存するそうです(ドキュメント)。
チュートリアルの進行でエラーを出さないように指定しているのだと思います。
なおこの引数は、ファイルをテキストモードで扱う際に有効とのことです(バイナリでは無効と理解)。
openしたファイルの扱い
1行ずつ取り出す方法、for in
でいけるんですね!
$ cat text_sentences.txt すもももももももものうち テキストマイニングを始めよう
In [4]: with open('text_sentences.txt', encoding='utf8') as f: ...: for line in f: ...: print(repr(line)) ...: 'すもももももももものうち\n' 'テキストマイニングを始めよう\n'
右端の改行コードはstrip()
などで削除できます。
「f.read()
したものをsplit()
して」と処理していったら、改行以外の空白文字(全角スペース)でも分割してしまって想定外の出力になりました(ドキュメント)。
f.read()
したあとは、split('\n')
ですね6。
改行文字を形態素解析しても問題ない!
janome
の話題に戻ります。
上記のfor in
で1行取り出す書き方も衝撃でしたが、それを上回る衝撃が。
それはf.read()
した文字列(改行文字含む)をtokenize
に渡すコード!
In [7]: with open('text_sentences.txt', encoding='utf8') as f: ...: text = f.read() ...: for token in t.tokenize(text): ...: print(token) ...: すもも 名詞,一般,*,*,*,*,すもも,スモモ,スモモ も 助詞,係助詞,*,*,*,*,も,モ,モ もも 名詞,一般,*,*,*,*,もも,モモ,モモ も 助詞,係助詞,*,*,*,*,も,モ,モ もも 名詞,一般,*,*,*,*,もも,モモ,モモ の 助詞,連体化,*,*,*,*,の,ノ,ノ うち 名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ 記号,空白,*,*,*,*, ,*,* テキスト 名詞,一般,*,*,*,*,テキスト,テキスト,テキスト マイニング 名詞,サ変接続,*,*,*,*,マイニング,マイニング,マイニング を 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ 始めよ 動詞,自立,*,*,一段,未然ウ接続,始める,ハジメヨ,ハジメヨ う 助動詞,*,*,*,不変化型,基本形,う,ウ,ウ
改行文字の形態素解析結果はsurfaceとbase_formが'\n'
であるために、改行されていますね。
後述のAnalyzerの話題になりますが、形態素解析結果の中から品詞を指定して取り出せばいいので、改行コードを含むf.read()
をtokenize
に放り込めると気づきました。
Analyzer
いよいよ、nikkieの知らないjanome
の世界、Analyzer
です!
Analyzer
を作る
Tokenizer
と2種類のフィルタのリストを渡して作ります(ドキュメント)。
token_filters
:形態素レベルのフィルタのリストchar_filters
:文字レベルのフィルタのリスト
チュートリアルではtoken_filters
を中心に扱いました。
なお、フィルタのリストを空にすれば、渡したTokenizer
そのものです。
Analyzer
を作るということは、Tokenizer
に渡す文字列の加工や、解析結果のフィルタリングを設定しているので、sklearn
のパイプラインに近いなと思いました。
Analyzer
はanalyze
メソッドを持ち、「形態素解析 + 設定した処理の結果」をgeneratorで返します7。
チュートリアルのお題:指定した品詞だけを取り出し、ストップワードがあれば除き、基本形で取り出すフィルタ
Analyzerを使って処理を追加し、風の又三郎と分かるようなWordCloudを作っていきます。
- 指定した品詞を取り出す:
POSKeepFilter
- 使い方:
POSKeepFilter(['名詞', '動詞', 'カスタム名詞'])
- 品詞が、名詞・動詞・カスタム名詞のいずれかで始まる形態素を取り出す
- ユーザ定義辞書で使っている「カスタム名詞」の指定も必要
- 使い方:
- 基本形で取り出す:
ExtractAttributeFilter
- 使い方:
ExtractAttributeFilter('base_form')
- フィルタのリストの最後に置く
- 使い方:
- 含まれる数を求める:TokenCountFilter
Analyzer
の紹介で何回も登場(WordCloud作成には寄与しない)- 基本形を登場回数の多いものから順に取り出す:
TokenCountFilter('base_form', sorted=True)
f.read()
したテキスト全体を渡せば、数行のコードで登場回数の多い単語が分かります(結果から意味を導くには品詞での抽出も必要かもしれません)
- フィルタのリストの最後に置く
ストップワードがあれば除く:フィルタの自作
自作のフィルタはTokenFilter
を継承することで作れます。
継承したクラスでは
- イニシャライザ
apply
メソッド
を実装しました。
class StopWordFilter(TokenFilter): def __init__(self, word_list=None, word_list_file=''): if word_list is None: word_list = [] self._word_list = [] self._word_list += word_list if word_list_file: with open(word_list_file, encoding='utf8') as f: words_in_file = [word.strip() for word in f] self._word_list += words_in_file def apply(self, tokens): for token in tokens: if token.base_form not in self._word_list: # 基本形がストップワードのリストに含まれない場合に返す yield token
WordCloudを作り、ストップワードを追加するということを何度も繰り返してできたのが、冒頭で見せたWordCloudです。
上記で使わなかったフィルタたち
他にも便利そうなものがありました。
POSStopFilter
CompoundNounFilter
- 名詞を複合して返すようにするフィルタ
LowerCaseFilter
/UpperCaseFilter
- 形態素解析の結果に含まれる英単語を揃えるのによさそうです
続く2つはchar_filters
に指定するフィルタです。
RegexReplaceCharFilter
UnicodeNormalizeCharFilter
- 全角/半角などを揃えるフィルタ
- 過去に
jaconv
を使った経験があるのですが、janome
にもあるのですね
WordCloud
英文は半角スペースで区切られているので、ファイルをopenしてreadしたもの(str, 改行文字含む)をWordCloud.generate
に渡すことができます。
The input “text” is expected to be a natural text.
日本語の場合は、形態素解析し、その結果の単語を英文にならって、半角スペースで区切ってファイルに保存します。
あとはそのファイルをopenしてreadしてWordCloud.generate
に渡せばいいわけですね。
IMO: 引数のデフォルト値に空のリストというのが、私、気になります
janome
のチュートリアル、ファイルのPythonicな扱い方を知れて素晴らしい内容だったのですが、1つ気になることが。
def wc(file, pos=[]): with open(file, encoding='utf8') as f: """ ここに処理を書く。 戻り値として,(単語, 出現回数) のタプルのリストを返す。 """
それは、pos=[]
と、引数のデフォルト値に空のリストを指定している点。
チュートリアルで作る関数では、pos引数の指す値を加工しないので副作用はなさそうですが、予期せぬバグの原因になるかと以下に書き直して取り組んでいました。
def wc(file, pos=None): if pos is None: pos = [] with open(file, encoding='utf8') as f: """ ここに処理を書く。 戻り値として,(単語, 出現回数) のタプルのリストを返す。 """
「引数のデフォルト値に空のリスト」を避けたほうがいい理由は、色々なところで言われていますが、最近読んだものを紹介。
The 10 Most Common Mistakes That Python Developers Make | Toptal
the default value for a function argument is only evaluated once, at the time that the function is defined. (Common Mistake #1: Misusing expressions as defaults for function arguments より)
感想
検索して見つけたスニペットの切り貼りレベルでこれまで使っていたjanome
ですが、チュートリアルを一通りやったところ、だいぶ全体感が掴めました。
今回得た知識を元に活用できそうです。
今週は"木こりの斧を研ぐ"経験でした。
手を動かしたコードはgistに上げています。
「Janome ではじめるテキストマイニング」02, 03の写経(WordCloud) ref:https://github.com/mocobeta/janome-tutorial · GitHub
janome
とmecabの違いは、前者がPure Python9であることくらいしか認識していなかったのですが、janome
のAnalyzer
は非常に魅力的に思います。
mecab-python3
はmecabのラッパーで、janome
のAnalyzer
に相当するものはmecabには見つけられていません(ご存じの方いたら教えていただけると嬉しいです)。
今週を振り返ると、推しの生誕祭祝いを優先しすぎました。
janome
チュートリアルを終えた後、自分のコードの書き直しまでやりたかったのですが、時間が足りず積み残しです。
振り返って思うのは、
- ブログ駆動開発は最優先なので、毎週スタートダッシュをかける
- ブログ駆動開発外のネタの差し込みは、すごく負荷が上がるので、計画的に(やりたいことリストの先頭には入れない。ブログ駆動開発の次くらいで)
です。
引き続き週一自然言語処理ネタで手を動かしていきます。
Future Works(今後のネタ帳)
- チュートリアルの04 キーワード抽出
- cfp_wordcloud作り直し(自分のブログのWordCloudの作り直しにもつながる)
- janomeとmecab、性能の違いを確認したい(janomeでneologdを指定する、辞書中の品詞に違いはあるのか)
-
https://mocobeta.github.io/slides-html/janome-tutorial/tutorial-slides.html#(16) この前後のスライドです↩
-
brew install graphviz
しました↩ -
stream
引数をTrue
にすると、返り値がgeneratorになります↩ -
__str__
メソッドの実装を確認しました ref: https://github.com/mocobeta/janome/blob/master/janome/tokenizer.py#L131↩ -
splitlines()
という道もありそうです。写経では衝撃的だったfor inを多用しました↩ -
中で
Tokenizer
のtokenize()
が呼ばれていました ref: https://github.com/mocobeta/janome/blob/master/janome/analyzer.py#L101↩ -
品詞にどんな種類があるのかは一度見ておくとよさそうです。ドキュメント中にあるのかな(宿題)↩
-
中の実装を一度見てみたいです(
janome
コマンドの実装など)↩