はじめに
頑張れば、何かがあるって、信じてる。nikkieです。
2019年12月末から自然言語処理のネタで毎週1本ブログを書いています。
2/3の週から自然言語処理の基礎固めとして『入門 自然言語処理』に取り組んでいます。
- 作者:Steven Bird,Ewan Klein,Edward Loper
- 発売日: 2010/11/11
- メディア: 大型本
今週は、6章「テキスト分類の学習」に取り組みました。
6章は以下で公開されています:
目次
動作環境
先週までと同じ環境を引き続き使っていきます(macOSのアップデートによりBuildVersionが変わりました)。
$ sw_vers ProductName: Mac OS X ProductVersion: 10.14.6 BuildVersion: 18G3020 $ 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
自然言語処理と機械学習
自然言語処理の問題設定には、機械学習における分類1として扱えるものがあります。
例えば、
- 名前が与えられた時に、男性か女性か分類する
- 映画のレビューを肯定的か否定的か分類する
- 品詞タグ付け(トークンが与えられた時に、品詞(名詞、動詞など)を分類する)
- 文の分割(セグメンテーション) 句読点を文が終了するかどうかで分類する
などです。
NLTKと機械学習
分類の問題設定に使える実装は、NLTKのnltk.classify
パッケージに用意されています。
分類器(classifier)のクラスに用意されたtrain
メソッドを使って、データを学習させた分類器を作成します。
分類器のクラスは、次の5つが定義されています2(太字のものが6章で言及されています)。
- ConditionalExponentialClassifier
- DecisionTreeClassifier
- MaxentClassifier
- NaiveBayesClassifier
- WekaClassifier
NLTKで分類に取り組む際の流れ
『入門 自然言語処理』では feature という語を素性と訳します。
訳注にありましたが、featureは特徴量とも訳される語だそうです(p.241)。
私には特徴量という呼び方のほうが馴染みがあるので、素性は特徴量に読み替えました。
分類に取り組む時、ゴールは分類器の作成です。
- 入力に素性(特徴量)抽出器(feature extractor)を適用して特徴量を取り出す
- 分類に関連する特徴量を決定
- 特徴量の符号化方法を決定
- 特徴量とラベルから分類器を作成
1と2を1回行っただけで高性能な分類器が作られることはまれです。
分類器の誤りをもとに、手順1から再度取り組むこと(エラー分析)が有益だと知りました。
エラー分析を行うためにはデータの分割がポイントになります3。
- 開発セット
- 訓練セット 分類器の学習に使う
- 検証セット エラー分析に使う
- テストセット 分類器が知らないデータで性能を評価する
データの分割について
開発セットとテストセットを分けることにはトレードオフがあります。
- 開発セットのデータが少なければ、十分に学習した分類器はできません
- テストセットのデータが少なければ、分類器の汎用的な性能は分かりません
6.3には、ラベル付けされた大量のデータが利用可能な場合は、全データの10%をテストデータとして使うのが一般的とありました(p.257)。
また、ある文書から開発セットにもテストセットにもデータを分けるべきではありません4。
開発セットに使う文書とテストセットに使う文書というように文書単位で分けます(同様に、開発セットの中で、訓練セットに使う文書と検証セットに使う文書も分けます)。
🙆OKな例(開発セットとテストセットで文書を分ける5)
In [3]: from nltk.corpus import brown In [4]: file_ids = brown.fileids(categories='news') In [5]: len(file_ids) Out[5]: 44 In [6]: type(file_ids) Out[6]: list In [7]: file_ids[:3] Out[7]: ['ca01', 'ca02', 'ca03'] In [8]: size = int(len(file_ids) * 0.1) In [9]: size Out[9]: 4 In [10]: train_set = brown.tagged_sents(file_ids[size:]) In [11]: test_set = brown.tagged_sents(file_ids[:size]) In [12]: len(train_set) Out[12]: 4227 In [13]: len(test_set) Out[13]: 396
file_ids
をshuffleしてもよさそうですね。
🙅NGな例(tagged_sents
を使ったことでリーケージの懸念あり)
In [1]: import random In [2]: random.seed(42) In [14]: tagged_sents = list(brown.tagged_sents(categories='news')) # NGなので真似しないでください In [15]: random.shuffle(tagged_sents) In [16]: size = int(len(tagged_sents) * 0.1) In [17]: size Out[17]: 462 In [18]: train_set, test_set = tagged_sents[size:], tagged_sents[:size] In [19]: len(train_set) Out[19]: 4161 In [20]: len(test_set) Out[20]: 462
名前が男性か女性かの分類に取り組む
それでは、例題として名前が男性か女性かの分類に取り組んでみます。
アルファベットの名前が与えられた時に、男性の名前か女性の名前かを分類します。
- namesコーパスを用います
- 名前の最後の1文字を特徴にします
- NaiveBayesClassifierを扱います(後ほどDecisionTreeClassifier、MaxentClassifierも試します)
- 書籍に沿ってエラー分析を実施し、特徴量抽出器を更新します
namesコーパス
In [21]: from nltk.corpus import names In [29]: print(names.readme()) Names Corpus, Version 1.3 (1994-03-29) Copyright (C) 1991 Mark Kantrowitz Additions by Bill Ross This corpus contains 5001 female names and 2943 male names, sorted alphabetically, one per line. # 省略 Mark Kantrowitz <mkant+@cs.cmu.edu> http://www-2.cs.cmu.edu/afs/cs/project/ai-repository/ai/areas/nlp/corpora/names/ In [30]: names = [(name, 'male') for name in names.words('male.txt')] + [(name, 'female') for name in names.words('female.txt')] In [31]: len(names) Out[31]: 7944
特徴量抽出 & データの分割
In [32]: random.shuffle(names) In [33]: def gender_features(word): # 最後の1文字を取り出す ...: return {'last_letter': word[-1]} ...: In [35]: names[:3] Out[35]: [('Raye', 'female'), ('Marita', 'female'), ('Fey', 'female')] In [51]: devtest_names = names[500:1500] # 1000件 In [52]: test_names = names[:500] # 500件 In [54]: train_names = names[1500:] In [55]: len(train_names) Out[55]: 6444 In [57]: train_set = [(gender_features(n), g) for n, g in train_names] In [58]: devtest_set = [(gender_features(n), g) for n, g in devtest_names] In [59]: test_set = [(gender_features(n), g) for n, g in test_names]
分類器作成(NaiveBayesClassifier)
nltk.classify.naivebayes.NaiveBayesClassifier
から分類器を作ります(仕組みについては6.5で説明されています)。
In [26]: import nltk In [60]: classifier = nltk.NaiveBayesClassifier.train(train_set) In [61]: nltk.classify.accuracy(classifier, devtest_set) Out[61]: 0.766
検証セットにおける正解率は76.6%でした。
分類に有益な特徴量の上位も確認できます。
In [62]: classifier.show_most_informative_features(5) Most Informative Features last_letter = 'a' female : male = 31.4 : 1.0 last_letter = 'f' male : female = 26.9 : 1.0 last_letter = 'k' male : female = 26.8 : 1.0 last_letter = 'p' male : female = 11.3 : 1.0 last_letter = 'd' male : female = 9.8 : 1.0
エラー分析
In [63]: errors = [] In [65]: for name, tag in devtest_names: ...: guess = classifier.classify(gender_features(name)) ...: if guess != tag: ...: errors.append((tag, guess, name)) ...: In [66]: len(errors) Out[66]: 234 In [67]: errors[:5] Out[67]: [('female', 'male', 'Marlo'), ('female', 'male', 'Hildagard'), ('female', 'male', 'Melicent'), ('female', 'male', 'Moll'), ('male', 'female', 'Georgie')]
エラー分析でどう間違えたかを確認するためにnames
(名前と教師ラベルの組)を訓練、検証、テストに分けています。
たしかに、この出力を見れば工夫できそうですね。
エラーから分かることの一例:
分類器はhで終わる名前をfemaleと分類するが、chで終わる名前はmaleに分類してほしい
そこで、最後の2文字も特徴量として抽出します。
In [68]: def gender_features(word): ...: return {'suffix1': word[-1:], ...: 'suffix2': word[-2:]} ...: In [69]: train_set = [(gender_features(n), g) for n, g in train_names] In [70]: devtest_set = [(gender_features(n), g) for n, g in devtest_names] In [72]: classifier = nltk.NaiveBayesClassifier.train(train_set) In [73]: nltk.classify.accuracy(classifier, devtest_set) Out[73]: 0.788 In [75]: classifier.show_most_informative_features(5) Most Informative Features suffix2 = 'na' female : male = 87.6 : 1.0 suffix2 = 'la' female : male = 62.8 : 1.0 suffix2 = 'ta' female : male = 37.4 : 1.0 suffix2 = 'rd' male : female = 35.3 : 1.0 suffix2 = 'ia' female : male = 33.7 : 1.0
正解率が78.8%に増加しました。
エラー分析はさらに繰り返せそうです。
なお、エラー分析を繰り返すたびに、訓練セットと検証セットを分け直したほうがいいそうです。
理由は検証セットのデータの偏りを分類器に反映させないためです。
別の分類器を試す
1.決定木(DecisionTreeClassifier)
nltk.classify.decisiontree.DecisionTreeClassifier
を試します。
In [76]: classifier = nltk.DecisionTreeClassifier.train(train_set) In [77]: nltk.classify.accuracy(classifier, devtest_set) Out[77]: 0.782
決定木は6.4で説明されています。
決定木はフローチャートであり、解釈しやすい場合が多いという特徴があるそうです。
pseudocode
メソッドでフローチャートを文字で確認できます。
In [80]: print(classifier.pseudocode(depth=2)) if suffix2 == 'Ag': return 'female' if suffix2 == 'Al': return 'male' if suffix2 == 'Bo': return 'male' if suffix2 == 'Cy': return 'male' if suffix2 == 'Di': return 'female' # 省略
決定木は決定株(1つの特徴量に基づき、分岐を1つだけ持つ決定木)を選ぶ6ことで作られます。
根となる決定株を選び、葉の正解率を調べ、十分な正解率でない場合は葉を決定株で置き換えて、決定木を育てていきます。
解釈しやすいという利点がある決定木ですが、欠点もあります。
- 特徴量が比較的独立したものである場合であっても、特定の順番で調べることを強制する
- ラベル付けに関する関与の小さな特徴量を扱うのが得意でない
決定木はフローチャートにせざるを得ないので、特徴量を特定の順番でチェックすることになるのだと理解しました。
なお、単純ベイズ分類器(NaiveBayesClassifier)は、全ての特徴量を「並列に」扱うことで、決定木の問題を克服しているそうです。
2. 最大エントロピー分類器(MaxentClassifier)
nltk.classify.maxent.MaxentClassifier
を試します。
In [81]: classifier = nltk.MaxentClassifier.train(train_set) ==> Training (100 iterations) Iteration Log Likelihood Accuracy --------------------------------------- 1 -0.69315 0.367 2 -0.34214 0.792 # 省略 99 -0.30114 0.805 Final -0.30113 0.805 In [82]: nltk.classify.accuracy(classifier, devtest_set) Out[82]: 0.788
最大エントロピー分類器は6.6で説明されています。
※理論部分は今回は読めておらず、宿題事項です
試したいこと
LazyMap
を作るnltk.classify.util.apply_features
(メモリ消費を抑える)- 名前の判定以外の例:映画レビュー
- 品詞タグ付け
- 文脈を利用する
- 同時分類器の利用(系列分類器)
- アルゴリズムの部分は『見て試してわかる機械学習アルゴリズムの仕組み 機械学習図鑑』で分かるところから補強
- 訓練セットと検証セットに交差検証を適用したい(
nltk
にある?sklearn
から使う?) - 「recommended machine learning packages that are supported by NLTK」があるとのことなのですが、どんなパッケージがあるのだろう(NLTKのサイトに見つけること能わず)
感想
「NLTKは高機能!」この一言につきます。
これまではステミングやタグ付けなどの自然言語の取り扱い方や、コーパスを使っての集計方法を学んできました。
それだけではなく、scikit-learn
にあるような分類器も実装されていたとは!
Web開発におけるDjangoのような"電池同梱"っぷりですね。
書籍で紹介されていた交差検証までNLTKでできるのか、それともscikit-learn
に任せたほうがいいのか、NLTKにおける機械学習の限界が気になります。
これまで『入門 自然言語処理』で自然言語処理の基本を見てきました。
「実務のあの部分はもっとうまくできたな」という発見がいくつもありました。
さて、この本が書かれた時点(2009)と現在とでは、自然言語処理を取り巻く状況は異なります。
10年前と比べて機械学習の発展はめざましく、この本には載っていない深層学習を使った文書分類は、各フレームワークのチュートリアルにも見られます。
そこで次回は機械学習のいまへのキャッチアップを目指し、ついにBERTを触る予定です。
最近出て話題の"あの本"が候補です。