素人の言語処理100本ノック:72


[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)の挑戦記録です。環境はUbuntu 16.04 LTS ＋ Python 3.5.2 \:\: Anaconda 4.1.1 (64-bit)です。過去のノックの一覧は[こちら](http://qiita.com/segavvy/items/fb50ba8097d59475f760)からどうぞ。

## 第8章: 機械学習
>本章では，Bo Pang氏とLillian Lee氏が公開しているMovie Review Dataのsentence polarity dataset v1.0を用い，文を肯定的（ポジティブ）もしくは否定的（ネガティブ）に分類するタスク（極性分析）に取り組む．

###72. 素性抽出
>極性分析に有用そうな素性を各自で設計し，学習データから素性を抽出せよ．素性としては，レビューからストップワードを除去し，各単語をステミング処理したものが最低限のベースラインとなるであろう．

####出来上がったコード：

```python:main.py
# coding: utf-8
import codecs
import snowballstemmer
from collections import Counter

fname_sentiment = 'sentiment.txt'
fname_features = 'features.txt'
fencoding = 'cp1252'		# Windows-1252らしい

# ストップワードのリスト	 http://xpo6.com/list-of-english-stop-words/ のCSV Formatより
stop_words = (
	'a,able,about,across,after,all,almost,also,am,among,an,and,any,are,'
	'as,at,be,because,been,but,by,can,cannot,could,dear,did,do,does,'
	'either,else,ever,every,for,from,get,got,had,has,have,he,her,hers,'
	'him,his,how,however,i,if,in,into,is,it,its,just,least,let,like,'
	'likely,may,me,might,most,must,my,neither,no,nor,not,of,off,often,'
	'on,only,or,other,our,own,rather,said,say,says,she,should,since,so,'
	'some,than,that,the,their,them,then,there,these,they,this,tis,to,too,'
	'twas,us,wants,was,we,were,what,when,where,which,while,who,whom,why,'
	'will,with,would,yet,you,your').lower().split(',')


def is_stopword(str):
	'''文字がストップワードかどうかを返す
	大小文字は同一視する

	戻り値：
	ストップワードならTrue、違う場合はFalse
	'''
	return str.lower() in stop_words


# 素性抽出
stemmer = snowballstemmer.stemmer('english')
word_counter = Counter()

with codecs.open(fname_sentiment, 'r', fencoding) as file_in:
	for line in file_in:
		for word in line[3:].split(' '):		# line[3:]で極性ラベル除去

			# 前後の空白文字除去
			word = word.strip()

			# ストップワード除去
			if is_stopword(word):
				continue

			# ステミング
			word = stemmer.stemWord(word)

			# '!'と'?'を除く1文字以下は除外
			if word != '!' and word != '?' and len(word) <= 1:
				continue

			# 候補に追加
			word_counter.update([word])

# 出現数が6以上のものを採用
features = [word for word, count in word_counter.items() if count >= 6]

# 書き出し
with codecs.open(fname_features, 'w', fencoding) as file_out:
	print(*features, sep='\n', file=file_out)
```

####実行結果：
結果のfeatures.txtの先頭部分です。

```console:features.txtの先頭部分
accompani
soulless
affection
contribut
15
bag
heavy-hand
add
reaction
documentari
seen
nobodi
silli
eyr
robin
motiv
celluloid
level
japanes
serious
（以下略）
```

ファイルの全体は[GitHub](https://github.com/segavvy/nlp100_Python/tree/master/72)にアップしています。

###素性とは
「すじょう」ではなく「そせい」と読みます。英語ではfeatureです。
第8章のゴールは、映画のレビューコメントが肯定的か否定的かを判定するプログラムを作ることですが、文章をそのまま使って学習するのは大変なので、前処理として文章から特徴を取り出します。その特徴のことを素性と呼んでいます。

###機械学習の流れ

第8章で私が行おうとしている機械学習の基本ロジックはすごく単純なので、先にざっと流れをまとめます。

####1.素性の決定
まず、素性を何にするかを決めます。今回の問題はこのステップになります。
ここでは例を単純にするために、「よかった」と「イマイチ」と「派手」の3つの言葉が含まれているかどうか？を素性として選んだとします。

####2.特徴抽出
次に、正解データに対してどの素性が含まれているかをチェックします。このような作業を特徴抽出と呼びます。
4件の正解データがあった場合、次のような感じで特徴を抽出します。

| 【レビューデータ】 |【正解のラベル】| 素性#1「よかった」|素性#2「イマイチ」|素性#3「派手」
|--------|:-------:|:---:|:---:|:---:|
|なかなか **よかった** 。|肯定的|あり|なし|なし|
|ラストが **イマイチ** 。|否定的|なし|あり|なし|
|演出が **派手** で **イマイチ** 。|否定的|なし|あり|あり|
|アクションが **派手** で **よかった** 。|肯定的|あり|なし|あり|

ここで、正解のラベルを$ y $とし、その値が肯定的なら1、否定的なら0とします。また各素性の有無をそれぞれ$ x_1 $、$ x_2 $、$ x_3 $として「あり」が1、「なし」が0とします。そうすると次のような表ができあがります。

|$ y $(ラベル)|$ x_1 $(素性#1の有無)|$ x_2 $(素性#2の有無)|$ x_3 $(素性#3の有無)|
|:---:|:---:|:---:|:---:|
|1|1|0|0|
|0|0|1|0|
|0|0|1|1|
|1|1|0|1|

この表ができたら特徴抽出は終わりです。これで文章が数値の表に変換されたことになります。以降は元のレビューの文章は利用しません。

####3.学習
この表を使って学習を行います。学習とは、数式を決めて、その数式の値をいい感じになるように調整する作業になります。例えば次の数式を使う場合、学習とは$ \theta_0 $から$ \theta_3 $をどんな値にするのか調整していく作業になります。（ちなみに$ \theta $はシータと読みます。機械学習で調整するパラメータとして使うことが多いみたいです。）

$$ y = \theta_0 + \theta_1 x_1 + \theta_2 x_2 + \theta_3 x_3 $$

$ \theta $の調整方法は今回は割愛しますが、最終的に$ \theta_0 = 0.5$、$ \theta_1 = 0.5 $、$ \theta_2 = -0.5 $、$ \theta_3 = 0 $という値に調整できたとしましょう。

$$ y = 0.5 + 0.5 x_1 - 0.5 x_2 + 0 x_3 $$


これで学習は完了です。

####4.実際に予測させてみる
実際の予測は、対象の文章から2.の特徴抽出を行い、3.の式に入れて$ y $がいくつになるのかを計算するだけです。
例えば「なかなか **派手** で **よかった** ね」というレビューなら、$ x_1 = 1 $、$ x_2 = 0 $、$ x_3 = 1 $になって、計算すると$ y = 1 $になり「肯定的」と推測できるわけです。ただ「時間の無駄だった、金返せ！」みたいなレビューの場合は素性が1つも抽出できず、$ x_1 = 0 $、$ x_2 = 0 $、$ x_3 = 0 $になって、$ y = 0.5 $になってしまいます。これでは肯定的なのか否定的なのか分かりません。機械学習では素性の選び方が精度に大きく影響します。

また、今回の式では、$ \theta $の値によっては$ y $が1を超えたり0未満になったりもします。そのため、実際には式にももっと工夫が必要になります。

簡単ですが、以上が機械学習の大まかな流れです。次回、もう少し補足しようと思います。

###素性の抽出
機械学習は今まさに勉強している真っ最中なので、素性の設計のコツが全くわかりません。そのため、完全に想像で考えてみました。

まず、問題にあるように、ストップワードの除去（[問題71](http://qiita.com/segavvy/items/007ed0523bf4e79e12fb)まんまです）とステミング（[問題52](http://qiita.com/segavvy/items/d47fa799883ed16eddc2)で勉強しました）を実行してみたところ、1文字のものが抽出されることがわかりました。1文字の単語や記号はレビューの判定にあまり意味を持つとは思えないので、これらは除去しています。ただし、「!」と「?」は少し感情が入った意味ありげな記号なので残すことにしました。

あと、単語の出現数に着目し、5以下のものは除去しました。1万件もあるレビューの中で数回しか出てこないような単語は、おそらく情報が足りなさすぎて肯定的とも否定的とも判断ができない素性になってしまうのでは？と考えたからです。ちなみに出現数の集計は[問題36](http://qiita.com/segavvy/items/932c1413e2552f208dfc)で使った[collections.Counter](http://docs.python.jp/3/library/collections.html#collections.Counter)を使えば簡単です。

この結果、今回抽出した素性は3,227個になりました。これで次の問題へ進みます。

なお、この推測があっているかどうかは良くわかりません^^;
次の問題以降で実際に学習させてみたのですが、1文字や出現数が5以下のものを削らなくても結果の精度はほとんど変わりませんでした。ただし、削れば素性の数が大幅に減るので、学習速度が大きく縮みます。精度がほとんど変わらずに素性を削れたので、意味はあったのではないかと思っています。

　
73本目のノックは以上です。誤りなどありましたら、ご指摘いただけますと幸いです。
<hr>

*実行結果には、[100本ノックで用いるコーパス・データ](http://www.cl.ecei.tohoku.ac.jp/nlp100/#data)で配布されているデータの一部が含まれます。*