はじめに
頑張れば、何かがあるって、信じてる。nikkieです。
2019年12月末から自然言語処理のネタで毎週1本ブログを書いています。
2/3の週から自然言語処理の基礎固めとして『入門 自然言語処理』に取り組んでいます。
- 作者:Steven Bird,Ewan Klein,Edward Loper
- 出版社/メーカー: オライリージャパン
- 発売日: 2010/11/11
- メディア: 大型本
今週は5章「単語の分類とタグ付け」からタガーを使った自動タグ付けについてアウトプットします。
ここでやりたいことは、トークンに対してタグ(=品詞)をつけること。
キーワードはこちら:
- バックオフ
- Nグラムモデル
『入門 自然言語処理』英語版(かつ、Python 3対応版)は公開されています:
目次
動作環境
先週のWordCloudと同じ環境です。
$ 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
brown コーパス
5章で用いるコーパスは「brown」なるもの。
In [1]: import nltk In [3]: print(nltk.corpus.brown.readme()) BROWN CORPUS A Standard Corpus of Present-Day Edited American English, for use with Digital Computers. by W. N. Francis and H. Kucera (1964) Department of Linguistics, Brown University Providence, Rhode Island, USA Revised 1971, Revised and Amplified 1979 http://www.hit.uib.no/icame/brown/bcm.html Distributed with the permission of the copyright holder, redistribution permitted.
カテゴリがいくつかあり、5章ではnews (A?) や learned (J)、editorial (B)を用いました1。
brownコーパスのデータの取得方法はいくつかあるのでまとめます(前提としてfrom nltk.corpus import brown
しているとします):
brown.words
:トークンからなるリストlen
は1161192
- 例:
['The', 'Fulton', 'County', 'Grand', 'Jury', ..., 'boucle', 'dress', 'was', 'stupefying', '.']
brown.sents
:文(sentences)を表すトークンのリストからなるリストlen
は57340
- 文に当たる部分:
['The', 'Fulton', 'County', ..., 'took', 'place', '.']
brown.tagged_words
:(トークン, 品詞)
というタプルからなるリストlen
は1161192
- 例:
[('The', 'AT'), ('Fulton', 'NP-TL'), ('County', 'NN-TL'), ('Grand', 'JJ-TL'), ('Jury', 'NN-TL'), ...]
brown.tagged_sents
:文に相当する、(トークン, 品詞)
というタプルのリストからなるリストlen
は57340
brown.tagged_words
の返り値の形式で、brown.sents
の返り値のように文ごとに分かれている
これらのメソッドはcategories
引数で取得する文書のカテゴリを指定できます。
ルールを元にした自動タグ付け
自動タグ付けの手法は2つあります。
ルールベースのタガー(タグ付け器)を作る方法から学びました。
1. DefaultTagger
nltk.tag.sequential.DefaultTagger
2を使い、どんなトークンにも指定のタグ(以下の例ではNN)を付けます。
In [126]: raw = 'I do not like green eggs and ham, I do not like them Sam I am!' In [127]: tokens = nltk.word_tokenize(raw) In [128]: default_tagger = nltk.DefaultTagger('NN') In [129]: default_tagger.tag(tokens) Out[129]: [('I', 'NN'), ('do', 'NN'), ('not', 'NN'), ('like', 'NN'), ('green', 'NN'), ('eggs', 'NN'), ('and', 'NN'), ('ham', 'NN'), (',', 'NN'), ('I', 'NN'), ('do', 'NN'), ('not', 'NN'), ('like', 'NN'), ('them', 'NN'), ('Sam', 'NN'), ('I', 'NN'), ('am', 'NN'), ('!', 'NN')]
brownコーパスでタグ付けの性能を確認する3と、正解は8分の1程度です。
In [121]: from nltk.corpus import brown In [122]: brown_tagged_sents = brown.tagged_sents(categories='news') In [130]: default_tagger.evaluate(brown_tagged_sents) Out[130]: 0.13089484257215028
2. RegexpTagger
正規表現とタグの対応ルールのタプルで表し、そのリストをnltk.tag.sequential.RegexpTagger
に渡します。
In [133]: patterns = [ ...: (r'.*ing$', 'VBG'), ...: (r'.*ed$', 'VBD'), ...: (r'.*es$', 'VBZ'), ...: (r'.*ould$', 'MD'), ...: (r'.*\'s$', 'NN$'), ...: (r'.*s$', 'NNS'), ...: (r'^-?[0-9]+(.[0-9]+)?$', 'CD'), ...: (r'.*', 'NN'), ...: ] In [134]: regexp_tagger = nltk.RegexpTagger(patterns)
性能は
In [136]: regexp_tagger.evaluate(brown_tagged_sents) Out[136]: 0.20326391789486245
ドキュメントに記載されていたパターンを追加すると、性能は向上しました。
In [139]: patterns = [ ...: (r'.*ing$', 'VBG'), ...: (r'.*ed$', 'VBD'), ...: (r'.*es$', 'VBZ'), ...: (r'.*ould$', 'MD'), ...: (r'.*\'s$', 'NN$'), ...: (r'.*s$', 'NNS'), ...: (r'^-?[0-9]+(.[0-9]+)?$', 'CD'), ...: (r'(The|the|A|a|An|an)', 'AT'), ...: (r'.*able$', 'JJ'), ...: (r'.*ness$', 'NN'), ...: (r'.*ly$', 'RB'), ...: (r'.*', 'NN'), ...: ] In [140]: regexp_tagger = nltk.RegexpTagger(patterns) In [141]: regexp_tagger.evaluate(brown_tagged_sents) Out[141]: 0.2889591662191459
3. ルックアップタガー
brownコーパス最頻出トークン100語のタグ付けを参照(look up)するタガーです。
In [142]: fd = nltk.FreqDist(brown.words(categories='news')) In [143]: cfd = nltk.ConditionalFreqDist(brown.tagged_words(categories='news')) In [144]: most_freq_words = fd.most_common(100) In [146]: most_freq_words[:5] Out[146]: [('the', 5580), (',', 5188), ('.', 4030), ('of', 2849), ('and', 2146)] In [147]: likely_tags = dict((word, cfd[word].max()) for word, _ in most_freq_words) In [148]: list(likely_tags)[:5] Out[148]: ['the', ',', '.', 'of', 'and'] In [149]: likely_tags['the'] Out[149]: 'AT'
nltk.probability.FreqDist
は collections.Counter
を継承しています。
ここでは、トークンごとの出現回数を数え、上位100件をmost_freq_words
としました。
nltk.probability.ConditionalFreqDist
はcollections.defaultdict
を継承しています。
トークンごとに品詞の出現回数を数えました(例:トークンに対して品詞Aが◯回、品詞Bが△回出現)。
トークンごとに一番出現回数が多い品詞を対応付けてlikely_tags
という辞書を作っています。
likely_tags
(トークンに対応する品詞の辞書)を参照するタガーをnltk.tag.sequential.UnigramTagger
で作ります。
model
引数に参照する辞書を渡します。
In [150]: baseline_tagger = nltk.UnigramTagger(model=likely_tags)
性能確認です。
In [151]: baseline_tagger.evaluate(brown_tagged_sents) Out[151]: 0.45578495136941344
ルックアップタガーは知らないトークンについてタグ付けできません。
そこで、タグ付けできない場合は別のタガー(ここではDefaultTagger
)を使うことにします。
タグを付けられない場合に別のタガーを使うことをバックオフと言うそうです。
backoff
引数にDefaultTagger
を渡します。
In [154]: baseline_tagger = nltk.UnigramTagger(model=likely_tags, backoff=nltk.DefaultTagger('NN'))
性能が上がりました!
In [155]: baseline_tagger.evaluate(brown_tagged_sents) Out[155]: 0.5817769556656125
likely_tags
という辞書がある程度広い範囲をカバーするまで、タグ付け結果の正解率が増えていきます。
書籍中のグラフ4を元に、上位2000語で試してみました。
In [156]: lt = dict((word, cfd[word].max()) for word, _ in fd.most_common(2000)) In [157]: baseline_tagger2 = nltk.UnigramTagger(model=lt, backoff=nltk.DefaultTagger('NN')) In [158]: baseline_tagger2.evaluate(brown_tagged_sents) Out[158]: 0.7775424150207848
DefaultTaggerから比べて、ルックアップタガー+バックオフのDefaultTaggerはだいぶ性能が上がりました。
訓練を元にした自動タグ付け
ルールベースとは別の方法として、訓練でタガーを作る方法も学びました。
未知のテキストに対する評価をするために、訓練用とテスト用にテキストを分けます(これは機械学習モデルの性能評価と共通の考え方ですね)。
In [162]: size = int(len(brown_tagged_sents) * 0.9) In [163]: size Out[163]: 4160 In [165]: train_sents = brown_tagged_sents[:size] In [166]: test_sents = brown_tagged_sents[size:]
1. UnigramTagger
ルックアップタガーと同様にUnigramTagger
を使います。
ルックアップタガーとの違いは、文単位の(トークン, 品詞)
のリストのリストをtrain
引数に渡すことです(第1引数なのでtrain=
と明示しなくても渡せます)。
In [167]: unigram_tagger = nltk.UnigramTagger(train_sents)
train_sents
を用いて、最も多く登場した品詞でタグ付けするように訓練されました。
性能を確認しましょう。
In [168]: unigram_tagger.evaluate(test_sents) Out[168]: 0.8121200039868434
2. Nグラムタガー(BigramTagger)
UnigramTaggerがタグ付けに使う情報はトークンだけです。
それに対して、文脈の情報を使うことを考えます。
Nグラムタガーはタグを付けたいトークンの他に、トークンの前のN-1語の品詞という情報も使います。
N=2のBigramTaggerについて見てみましょう(NLTK ドキュメント)。
In [169]: bigram_tagger = nltk.BigramTagger(train_sents)
性能は。。。
In [172]: bigram_tagger.evaluate(test_sents) Out[172]: 0.10206319146815508
上がると思いきや、ガクッと下がりました。
これは、
ためだそうです。
知らないトークンにタグ付けできないというのはルックアップタガーでも見た事象ですね。
そこで、再度バックオフの出番です。
3. バックオフでタガーを組み合わせる
以下のように組合せます:
- BigramTaggerでタグ付け
- 1ができない場合、UnigramTaggerでタグ付け
- 2ができない場合、DefaultTaggerでタグ付け
実装は、各タガーのbackoff
引数に指定する形になります。
In [173]: t0 = nltk.DefaultTagger('NN') In [174]: t1 = nltk.UnigramTagger(train_sents, backoff=t0) In [175]: t2 = nltk.BigramTagger(train_sents, backoff=t1) In [176]: t2.evaluate(test_sents) Out[176]: 0.8452108043456593
BigramTagger単体と比べて性能が上がりました。
これはUnigramTaggerと比べても上がっています。
TrigramTagger (N=3) も試してみたところ、性能は同程度という結果でした。
In [177]: t3 = nltk.TrigramTagger(train_sents, backoff=t2) In [178]: t3.evaluate(test_sents) Out[178]: 0.843317053722715
タガーの取り扱い
保存
pickle.dump
を使ってファイルに保存します5:
In [179]: from pickle import dump In [180]: with open('t2.pkl', 'wb') as output: ...: dump(t2, output, -1) ...:
コマンドラインの別ウィンドウ(すなわち別のPythonプロセス)で読み込めることを確認します。
>>> from pickle import load >>> with open('t2.pkl', 'rb') as input: ... tagger = load(input) ... >>> text = """The board's action shows what free enterprise ... is up against in our complex maze of regulatory laws .""" >>> tokens = text.split() >>> tagger.tag(tokens) [('The', 'AT'), ("board's", 'NN$'), ('action', 'NN'), ('shows', 'NNS'), ('what', 'WDT'), ('free', 'JJ'), ('enterprise', 'NN'), ('is', 'BEZ'), ('up', 'RP'), ('against', 'IN'), ('in', 'IN'), ('our', 'PP$'), ('complex', 'JJ'), ('maze', 'NN'), ('of', 'IN'), ('regulatory', 'NN'), ('laws', 'NNS'), ('.', '.')]
性能確認
nltk.metrics.confusionmatrix.ConfusionMatrix
で混同行列が確認できます。
In [181]: test_tags = [tag for sent in brown.sents(categories='editorial') for word, tag in t2.tag(sent)] In [182]: gold_tags = [tag for word, tag in brown.tagged_words(categories='editorial')] In [184]: nltk.ConfusionMatrix(gold_tags, test_tags) Out[184]: <ConfusionMatrix: 52073/61604 correct>
まとめ
品詞の自動タグ付けについて以下を学びました。
- 自動タグ付け=タガーを作る
- タガーの作り方には、ルールベースの手法と訓練による手法がある
- 訓練による手法の1つ、Nグラムモデルは、トークンの前の語の品詞の情報もタグ付けに使う
- 複数のタガーを組み合わせる:バックオフ(例:Bigram, Unigram, DefaultTagger)
感想
品詞タグ付けに訓練という機械学習的なアプローチが使われているというのは初めて知りました。
3章のトークン化と5章の品詞タグ付けの話で、PyCon JP 2019で聞いたnagisaの話と繋がりが見え始めました。
英語テキストについて今回学んだことが、日本語の場合にどこまで当てはまるのか気になっています。
というわけで、次回は『入門 自然言語処理』12章「Pythonによる日本語自然言語処理」に取り組む予定です。
-
nltk.download('brown')
をしていれば、~/nltk_data/corpora/brown
にファイルがあり、直接見られます↩ -
nltk
の各モジュールはnltk.awesome_module
のようにnltk
パッケージ直下から参照できるのですが、これはnltk/__init__.py
で*
を使ってimportしているために実現されていました(ref)↩ -
evaluate
メソッドは、文単位の(トークン, 品詞)
タプルのリストを集めたリストを受け取ります ref: https://www.nltk.org/api/nltk.tag.html#nltk.tag.api.TaggerI.evaluate↩ -
protocolは0~5までの6段階(ref: ドキュメント)ですが、
protocol
引数に-1が渡されています。この場合、利用できる中で一番高い段階のprotocolとなるそうです ref: Pickling and Unpickling in python Explained - DEV Community 👩💻👨💻↩