言語処理100本ノック-71(StanfordNLP使用):ストップワード

[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)の71本目の記録です。
今回はストップワード除外のために[`nltk`パッケージ](https://www.nltk.org/)と[`stanfordNLP`パッケージ](https://stanfordnlp.github.io/stanfordnlp/)を使っています。単純なストップワードの辞書を[`nltk`パッケージ](https://www.nltk.org/)から取得し、品詞によって記号なども判定しています。
今までは基本的に[「素人の言語処理100本ノック」](https://qiita.com/segavvy/items/fb50ba8097d59475f760)とほぼ同じ内容にしていたのでブロクに投稿していなかったのですが、[「第8章: 機械学習」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#ch8)については、真剣に時間をかけて取り組んでいてある程度変えているので投稿します。[StanfordNLP](https://stanfordnlp.github.io/stanfordnlp/)をメインに使用しています。

# 参考リンク

|リンク|備考|
|:--|:--|
|[071_1.ストップワード(準備).ipynb](https://github.com/YoheiFukuhara/nlp100/blob/master/08.%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92/071_1.%E3%82%B9%E3%83%88%E3%83%83%E3%83%97%E3%83%AF%E3%83%BC%E3%83%89(%E6%BA%96%E5%82%99).ipynb)|回答プログラム(準備編)のGitHubリンク|
|[071_2.ストップワード(実行).ipynb](https://github.com/YoheiFukuhara/nlp100/blob/master/08.%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92/071_2.%E3%82%B9%E3%83%88%E3%83%83%E3%83%97%E3%83%AF%E3%83%BC%E3%83%89(%E5%AE%9F%E8%A1%8C).ipynb)|回答プログラム(実行編)のGitHubリンク|
|[素人の言語処理100本ノック:71](http://qiita.com/segavvy/items/007ed0523bf4e79e12fb)|言語処理100本ノックで常にお世話になっています|
|[PythonによるStanfordNLP入門](https://qiita.com/Nakamurus/items/03f674b0eab161dd8477)|Stanford Core NLPとの違いがわかりやすかったです|

# 環境

|種類|バージョン|内容|
|:--|:--|:--|
|OS|Ubuntu18.04.01 LTS|仮想で動かしています|
|pyenv|1.2.15|複数Python環境を使うことがあるのでpyenv使っています|
|Python|3.6.9|pyenv上でpython3.6.9を使っています<br>3.7や3.8系を使っていないことに深い理由はありません<br>パッケージはvenvを使って管理しています|

上記環境で、以下のPython追加パッケージを使っています。通常のpipでインストールするだけです。

|種類|バージョン|
|:--|:--|
|nltk|3.4.5|
|stanfordnlp|0.2.0|


# 課題
## [第8章: 機械学習](http://www.cl.ecei.tohoku.ac.jp/nlp100/#ch8)
>本章では，Bo Pang氏とLillian Lee氏が公開している[Movie Review Data](http://www.cs.cornell.edu/people/pabo/movie-review-data/)の[sentence polarity dataset v1.0](http://www.cs.cornell.edu/people/pabo/movie-review-data/rt-polaritydata.README.1.0.txt)を用い，文を肯定的（ポジティブ）もしくは否定的（ネガティブ）に分類するタスク（極性分析）に取り組む．

### 71. ストップワード
>英語のストップワードのリスト（ストップリスト）を適当に作成せよ．さらに，引数に与えられた単語（文字列）がストップリストに含まれている場合は真，それ以外は偽を返す関数を実装せよ．さらに，その関数に対するテストを記述せよ．

**「適当に」**ですか・・・

# 回答
## 回答前提
課題の**「適当に」**をどうしようかと思案しました。その結果、[`nltk`パッケージ](https://www.nltk.org/)で定義しているストップワードと、形態素解析した結果の品詞情報を使い真偽判定することに決めました。

## 回答プログラム(準備編) [071_1.ストップワード(準備).ipynb](https://github.com/YoheiFukuhara/nlp100/blob/master/08.%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92/071_1.%E3%82%B9%E3%83%88%E3%83%83%E3%83%97%E3%83%AF%E3%83%BC%E3%83%89(%E6%BA%96%E5%82%99).ipynb)

まずは準備があります。これは回答の実行と別でパッケージインストール後に1回だけ実行する必要があります。
ntlkパッケージのストップワード一覧をダウンロードします。これは`pip install`と別に最初に実行します。

```python
import nltk

# ストップワードのダウンロード
nltk.download('stopwords')

# ストップワード確認
print(nltk.corpus.stopwords.words('english'))
```

またstanfordNLPパッケージの英語モデルをダウンロードします。約250MBあるので注意してください。これも`pip install`と別に最初に実行します。

```python
import stanfordnlp

stanfordnlp.download('en')

stanfordnlp.Pipeline()
```

## 回答プログラム(実行編) [071_2.ストップワード(実行).ipynb](https://github.com/YoheiFukuhara/nlp100/blob/master/08.%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92/071_2.%E3%82%B9%E3%83%88%E3%83%83%E3%83%97%E3%83%AF%E3%83%BC%E3%83%89(%E5%AE%9F%E8%A1%8C).ipynb)

```python
import re

from nltk.corpus import stopwords
from nltk.stem.porter import PorterStemmer as PS
import stanfordnlp

# 速くするためにタプルとして定義
STOP_WORDS = set(stopwords.words('english'))

ps = PS()

# Universal POS tags に準拠していそう
# https://universaldependencies.org/u/pos/
EXC_POS = {'PUNCT',   # 句読点
           'X',       # その他
           'SYM',     # 記号
           'PART',    # 助詞('sなど)
           'CCONJ',   # 接続詞(andなど)
           'AUX',     # 助動詞(woudlなど)
           'PRON',    # 代名詞
           'SCONJ',   # 従位接続詞(whetherなど)
           'ADP',     # 接置詞(inなど)
           'NUM'}     # 番号

# プロセッサをデフォルトの全指定にすると遅かったので最低限に絞る
# https://stanfordnlp.github.io/stanfordnlp/processors.html
nlp = stanfordnlp.Pipeline(processors='tokenize,pos,lemma')

reg_sym = re.compile(r'^[!-/:-@[-`{-~]|[!-/:-@[-`{-~]$')
reg_dit = re.compile('[0-9]')

# 先頭と末尾の記号除去
def remove_symbols(lemma):
    return reg_sym.sub('', lemma)

# ストップワード真偽判定
def is_stopword(word):
    lemma = remove_symbols(word.lemma)
    return True if lemma in STOP_WORDS \
                  or lemma == '' \
                  or word.upos in EXC_POS \
                  or len(lemma) == 1 \
                  or reg_dit.search(lemma)\
                else False

# 試しに3文を判定
with open('./sentiment.txt') as file:
    for i, line in enumerate(file):
        
        # 最初の3文字はネガポジを示すだけなのでnlp処理しない(少しでも速くする)
        doc = nlp(line[3:])
        print(i, line)
        for sentence in doc.sentences:
            for word in sentence.words:
                print(word.text, word.upos, remove_symbols(word.lemma), ps.stem(remove_symbols(word.lemma)), is_stopword(word))
        
        if i == 2:
            break
```

## 回答解説
今回は単純なストップワード除外だけでなく、形態素解析して助詞なども除外しています。
まず、ストップワードをタプル形式で取得しています。

```python
# 速くするためにタプルとして定義
STOP_WORDS = set(stopwords.words('english'))
```

ストップワードの中身はこいつらです。

```
['i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you', "you're", "you've", "you'll", "you'd", 'your', 'yours', 'yourself', 'yourselves', 'he', 'him', 'his', 'himself', 'she', "she's", 'her', 'hers', 'herself', 'it', "it's", 'its', 'itself', 'they', 'them', 'their', 'theirs', 'themselves', 'what', 'which', 'who', 'whom', 'this', 'that', "that'll", 'these', 'those', 'am', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'having', 'do', 'does', 'did', 'doing', 'a', 'an', 'the', 'and', 'but', 'if', 'or', 'because', 'as', 'until', 'while', 'of', 'at', 'by', 'for', 'with', 'about', 'against', 'between', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'to', 'from', 'up', 'down', 'in', 'out', 'on', 'off', 'over', 'under', 'again', 'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all', 'any', 'both', 'each', 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 's', 't', 'can', 'will', 'just', 'don', "don't", 'should', "should've", 'now', 'd', 'll', 'm', 'o', 're', 've', 'y', 'ain', 'aren', "aren't", 'couldn', "couldn't", 'didn', "didn't", 'doesn', "doesn't", 'hadn', "hadn't", 'hasn', "hasn't", 'haven', "haven't", 'isn', "isn't", 'ma', 'mightn', "mightn't", 'mustn', "mustn't", 'needn', "needn't", 'shan', "shan't", 'shouldn', "shouldn't", 'wasn', "wasn't", 'weren', "weren't", 'won', "won't", 'wouldn', "wouldn't"]
```

また、使わない品詞として以下の定義をしています。あとで結果を見て、どんどん増えていきました。
この処理をすることで嬉しいのは、例えば"I **like** this movie"の`like`は動詞としてストップワード対象外なのですが、"he is **like** my hero"のような場合は`like`をADP(接置詞)として対象外にしてくれることです。
ここの種類は[Universal POS tabs](https://universaldependencies.org/u/pos/)として定義されているものと同様っぽいです。

```python
# Universal POS tags に準拠していそう
# https://universaldependencies.org/u/pos/
EXC_POS = {'PUNCT',   # 句読点
           'X',       # その他
           'SYM',     # 記号
           'PART',    # 助詞('sなど)
           'CCONJ',   # 接続詞(andなど)
           'AUX',     # 助動詞(wouldなど)
           'PRON',    # 代名詞
           'SCONJ',   # 従位接続詞(whetherなど)
           'ADP',     # 接置詞(inなど)
           'NUM'}     # 番号
```

後に使う正規表現のコンパイルです。1行目は先頭と末尾から半角記号を探す正規表現。2行目は数字を探す正規表現です。

```python
reg_sym = re.compile(r'^[!-/:-@[-`{-~]|[!-/:-@[-`{-~]$')
reg_dit = re.compile('[0-9]')
```

先頭と末尾から半角記号を除去する関数です。例えば`-a`みたいな文字があったときに、1文字目を除去します。

```python
# 先頭と末尾の記号除去
def remove_symbols(lemma):
    return reg_sym.sub('', lemma)
```


肝心の関数は以下の定義です。`lemma`は[Lemmatisation](https://en.wikipedia.org/wiki/Lemmatisation)にあるようにレンマと言って辞書に定義されている形式に変換します(例：better -> good)。
以下の場合にストップワードとして判定しています。

1. レンマがストップワードに含まれる場合は真
2. レンマがブランクの場合(ここでのレンマは頭と末尾が記号の場合除去していているので、すべてが記号だった場合)は真
3. 品詞が上で定義した感情に関係なさそうなものは真
4. 文字長が1の場合は真
5. 数字を含んでいた場合は真(12thなどはここの場合に含まれます)


```python
# ストップワード真偽判定
def is_stopword(word):
    lemma = remove_symbols(word.lemma)
    return True if lemma in STOP_WORDS \
                  or lemma == '' \
                  or word.upos in EXC_POS \
                  or len(lemma) == 1 \
                  or reg_dit.search(lemma)\
                else False
```

あとはファイルを読み込んでストップワード判定させています。stanfordnlpの処理は遅いので、少しでも早くするためにネガポジを示す最初の3文字を除外しています。
今回は試しに最初の3文だけを実行しています。
最後に`ps.stem`を使ってステミングした形で出力しています。これは、例えばadhere adherence adherentの3つの単語をadherとして共通化させるためです。後続の機械学習部分で、この形の方が良いかと考え使っています。

```python
with open('./sentiment.txt') as file:
    for i, line in enumerate(file):
        
        # 最初の3文字はネガポジを示すだけなのでnlp処理しない(少しでも速くする)
        doc = nlp(line[3:])
        print(i, line)
        for sentence in doc.sentences:
            for word in sentence.words:
                print(word.text, word.upos, remove_symbols(word.lemma), ps.stem(remove_symbols(word.lemma)), is_stopword(word))
        
        if i == 2:
            break
```

実行結果はこんな感じです。

```
0 +1 a chick flick for guys .

a DET a a True
chick NOUN chick chick False
flick NOUN flick flick False
for ADP for for True
guys NOUN guy guy False
. PUNCT   True
1 +1 an impressive if flawed effort that indicates real talent .

an DET a a True
impressive ADJ impressive impress False
if SCONJ if if True
flawed VERB flaw flaw False
effort NOUN effort effort False
that PRON that that True
indicates VERB indicate indic False
real ADJ real real False
talent NOUN talent talent False
. PUNCT   True
2 +1 displaying about equal amounts of naiveté , passion and talent , beneath clouds establishes sen as a filmmaker of considerable potential .

displaying VERB displaying display False
about ADP about about True
equal ADJ equal equal False
amounts NOUN amount amount False
of ADP of of True
naiveté NOUN naiveté naiveté False
, PUNCT   True
passion NOUN passion passion False
and CCONJ and and True
talent NOUN talent talent False
, PUNCT   True
beneath ADP beneath beneath True
clouds NOUN cloud cloud False
establishes VERB establish establish False
sen NOUN sen sen False
as ADP as as True
a DET a a True
filmmaker NOUN filmmaker filmmak False
of ADP of of True
considerable ADJ considerable consider False
potential NOUN potential potenti False
. PUNCT   True
```