機械学習の適用1 感情分析
自然言語処理の一分野である感情分析をやる
機械学習のアルゴリズムを使用することで、極性(書き手の意見)に基づいてドキュメントを分類する手法を学ぶ
感情分析は意見マイニングとも呼ばれ、自然言語処理の一つである
感情分析でよく用いられるタスクは、ある話題に関して書き手が表明した意見や感情に基づいてドキュメントを分類することである
本章ではIMDd(Internet Movie Dataset)から映画レビューの大きなデータセットを扱う
このレビューは「肯定的」「否定的」の両極に分類される50000件の映画レビューで構成されている
レビューには最大10点の☆がついており
6個以上:肯定的
5個以下:否定的
とする
映画レビューから意味のある情報を抽出し、とあるレビューが「好き」もしくは「嫌い」と評価したのかを予測する
1 IMDbの映画レビューデータセットの取得
映画のレビューセットは( http://ai.stanford.edu/~amaas/data/sentiment/ )で取得できる
import pyprind
import pandas as pd
import os
# change the `basepath` to the directory of the
# unzipped movie dataset
#basepath = '/Users/Sebastian/Desktop/aclImdb/'
basepath = './aclImdb'
labels = {'pos': 1, 'neg': 0}#positiveを1 negativeを0とする
pbar = pyprind.ProgBar(50000)
df = pd.DataFrame()
for s in ('test', 'train'):
for l in ('pos', 'neg'):
path = os.path.join(basepath, s, l)
for file in os.listdir(path):
with open(os.path.join(path, file), 'r', encoding='utf-8') as infile:
txt = infile.read()
df = df.append([[txt, labels[l]]], ignore_index=True)
pbar.update()
df.columns = ['review', 'sentiment']
作業を行いやすくするため、データをひとまとめにした上でシャッフルした映画レビューデータセットをCSVファイルに保存する。
import numpy as np
np.random.seed(0)
df = df.reindex(np.random.permutation(df.index))
df.to_csv('./movie_data.csv', index=False)
データが正しいフォーマットで保存できている事を確認する
import pandas as pd
df = pd.read_csv('./movie_data.csv')
df.head(3)
BoWモデルの紹介
文章のカテゴリデータは、機械学習アルゴリズムに渡す前に数値に変換する必要がある。
ここでは、テキストを数値の特徴ベクトルとして表現できるBoW(Bag-of-Words)モデルを紹介する。
1,ドキュメントの集合全体から一意なトークンからなる語彙を作成する。
2,各ドキュメントでの各単語の出現回数を含んだ特徴ベクトルを構築
BoWの語彙を構成しているすべての単語の一部にすぎない
このとき、特徴ベクトルの大半の成分が0になる(疎ベクトル)
scikit-learnでやってみる
BoWモデルを構築するには、sciki-learnのCountVectorizerクラスが使える
テキストデータの配列を入力として、BoWモデルを自動的に生成する。
mport numpy as np
from sklearn.feature_extraction.text import CountVectorizer
count = CountVectorizer()
docs = np.array([
'The sun is shining',
'The weather is sweet',
'The sun is shining, the weather is sweet, and one and one is two'])
bag = count.fit_transform(docs)
CountVectorizerクラスのfit-transformメソッドを呼び出すことでBoWモデルの語彙ベクトルを生成す
語彙の中身を出力
print(count.vocabulary_)
{'one': 2, 'sweet': 5, 'the': 6, 'shining': 3, 'weather': 8, 'and': 0, 'two': 7, 'is': 1, 'sun': 4}
このように単語のその番号がディクショナリ形式で保存されている
作成した特徴ベクトルを出力してみる
print(count.vocabulary_)
{'one': 2, 'sweet': 5, 'the': 6, 'shining': 3, 'weather': 8, 'and': 0, 'two': 7, 'is': 1, 'sun': 4}
この特徴ベクトルの値は先ほどのディクショナリの番号に対応している
TD-IDFを使って単語の関連性を評価する
「肯定的」「否定的」どちらのコメントにも同じ単語が出現する事はよくある
そしてこういった頻繁に出現する単語は大抵意味のある情報や識別情報を含んでいない
TD-IDFを使えば、そんな単語の重みを減らす事ができる
TD-IDFは、TF(単語の出現頻度)とIDF(逆文書頻度)の積として定義できる
【数式8.2.1】
td-idf(t,d) = tf(t,d) × idf(t,d) \tag{8.2.2}
idfは、以下の方法で求められる
idf(t,d) = log\frac{a}{1 + } = log\frac{(ドキュメントの総数)}{1 + ある単語を含むドキュメント数} \tag{8.2.2}
scikit-learnにはTfidfTransformerクラスというのが用意されている
このクラスはCountvectorizerからの出力を受け取り、TD-IDF変換する
from sklearn.feature_extraction.text import TfidfTransformer
tfidf = TfidfTransformer(use_idf=True, norm='l2', smooth_idf=True)
print(tfidf.fit_transform(count.fit_transform(docs)).toarray())
[[ 0. 0.43 0. 0.56 0.56 0. 0.43 0. 0. ]
[ 0. 0.43 0. 0. 0. 0.56 0.43 0. 0.56]
[ 0.5 0.45 0.5 0.19 0.19 0.19 0.3 0.25 0.19]]
みると三番目の文章にしかない「and」の値は0.5となっていたり
isが0.45になっている事が確認できる
テキストデータのクレンジング
テキストデータをクレンジングする
映画レビューのデータセットの一つ目のドキュメントから最後の50
文字を出力してみる
df.loc[0, 'review'][-50:]
'is seven.<br /><br />Title (Brazil): Not Available'
見ての通り、htmlの構文が含まれているためそれを削除する
それにあたりpythonの正規表現ライブラリのreを使用する
※htmlの解析に正規表現を使うのはほんとは良くないようです
import re
def preprocessor(text):
text = re.sub('<[^>]*>', '', text)
emoticons = re.findall('(?::|;|=)(?:-)?(?:\)|\(|D|P)', text)
text = re.sub('[\W]+', ' ', text.lower()) +\
' '.join(emoticons).replace('-', '')
return text
preprocessor(df.loc[0, 'review'][-50:])
'is seven title brazil not available'
使えてそうなので先ほど作成したDataframeに含まれている全ての映画レビューにpreprocessor関数を適用する
df['review'] = df['review'].apply(preprocessor)
ドキュメントをトークン化する
テキストデータを分析するためにテキストコーパスを個々の要素に分割する方法について考える
方法としては、ドキュメントを空白(スペース、タブ、改行、リターン、改ページ)で区切る
def tokenizer(text):
return text.split()
tokenizer('runners like running and thus they run')
['runners', 'like', 'running', 'and', 'thus', 'they', 'run']
分割できた
ワードステミング
ワードステミングは単語を原形に変換する事で、関連する単語を同じ語幹にマッピングできるようにする
このアルゴリズムはNLTKという対自然言語処理のライブラリに実装されている
from nltk.stem.porter import PorterStemmer
porter = PorterStemmer()
def tokenizer_porter(text):
return [porter.stem(word) for word in text.split()]
tokenizer_porter('runners like running and thus they run')
['runner', 'like', 'run', 'and', 'thu', 'they', 'run']
ストップワードの除去
ストップワードとはあらゆる種類のテキストで見られるごくありふれた単語の事である
ドキュメントを区別するにあたって有益な情報をまったく含んでいないとみなされている
例えばis,and,hasなど
実際に除去する
ここでは、nltkに含まれる127個のストップワードを使用する
import nltk
from nltk.corpus import stopwords
nltk.download('stopwords')
stop = stopwords.words('english')
[w for w in tokenizer_porter('a runner likes running and runs a lot')[-10:]
if w not in stop]
['runner', 'like', 'run', 'run', 'lot']
除去できた
ドキュメントを分類するロジスティック回帰モデルのトレーニング
実際に分類してみる
まず、テキストをテストセットとトレーニングセットに分割する(25000づつ)
X_train = df.loc[:25000, 'review'].values
y_train = df.loc[:25000, 'sentiment'].values
X_test = df.loc[25000:, 'review'].values
y_test = df.loc[25000:, 'sentiment'].values
次に、GridSearchCVを使ってロジスティック回帰の最適なパラメータを求める
※詳しくは6章を参照
rom sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.feature_extraction.text import TfidfVectorizer
if Version(sklearn_version) < '0.18':
from sklearn.grid_search import GridSearchCV
else:
from sklearn.model_selection import GridSearchCV
tfidf = TfidfVectorizer(strip_accents=None,
lowercase=False,
preprocessor=None)
param_grid = [{'vect__ngram_range': [(1, 1)],
'vect__stop_words': [stop, None],
'vect__tokenizer': [tokenizer, tokenizer_porter],
'clf__penalty': ['l1', 'l2'],
'clf__C': [1.0, 10.0, 100.0]},
{'vect__ngram_range': [(1, 1)],
'vect__stop_words': [stop, None],
'vect__tokenizer': [tokenizer, tokenizer_porter],
'vect__use_idf':[False],
'vect__norm':[None],
'clf__penalty': ['l1', 'l2'],
'clf__C': [1.0, 10.0, 100.0]},
]
lr_tfidf = Pipeline([('vect', tfidf),
('clf', LogisticRegression(random_state=0))])
gs_lr_tfidf = GridSearchCV(lr_tfidf, param_grid,
scoring='accuracy',
cv=5,
verbose=1,
n_jobs=-1)
gs_lr_tfidf.fit(X_train, y_train)
グリッドサーチによって得られた最良のモデルを使用して
トレーニングセットでの正解率と
テストセットでの正解率を
出力してみる
print('CV Accuracy: %.3f' % gs_lr_tfidf.best_score_)
clf = gs_lr_tfidf.best_estimator_
print('Test Accuracy: %.3f' % clf.score(X_test, y_test))
CV Accuracy: 0.897
Test Accuracy: 0.899
結果だいたい90%ほどの精度で分けられる事がわかった
アウトオブコア学習
先ほどの計算、実はかなり重いです。
もっと大量のデータを普通にBoWしようとするとメモリが詰まります。
そんな時アウトオブコア学習を使えば、大規模なデータセットも使えるようになる
行う事は二つ
・ベクトル化したテキストをハッシュ化(圧縮)して使用する
・ミニバッチ学習を用いる
これで大丈夫(概ね)
scikit-learnにはハッシュ化ようにHashinfVectorizerが用意されている
from sklearn.feature_extraction.text import HashingVectorizer
from sklearn.linear_model import SGDClassifier
mport numpy as np
import re
from nltk.corpus import stopwords
def tokenizer(text):
text = re.sub('<[^>]*>', '', text)
emoticons = re.findall('(?::|;|=)(?:-)?(?:\)|\(|D|P)', text.lower())
text = re.sub('[\W]+', ' ', text.lower()) +\
' '.join(emoticons).replace('-', '')
tokenized = [w for w in text.split() if w not in stop]
return tokenized
def stream_docs(path):
with open(path, 'r', encoding='utf-8') as csv:
next(csv) # skip header
for line in csv:
text, label = line[:-3], int(line[-2])
yield text, label
vect = HashingVectorizer(decode_error='ignore',
n_features=2**21,
preprocessor=None,
tokenizer=tokenizer)
clf = SGDClassifier(loss='log', random_state=1, n_iter=1)
doc_stream = stream_docs(path='./movie_data.csv')
そしてミニバッチで回す
import pyprind
pbar = pyprind.ProgBar(45)
def get_minibatch(doc_stream, size):
docs, y = [], []
try:
for _ in range(size):
text, label = next(doc_stream)
docs.append(text)
y.append(label)
except StopIteration:
return None, None
return docs, y
classes = np.array([0, 1])
for _ in range(45):
X_train, y_train = get_minibatch(doc_stream, size=1000)
if not X_train:
break
X_train = vect.transform(X_train)
clf.partial_fit(X_train, y_train, classes=classes)
pbar.update()
結果を見てみる
X_test, y_test = get_minibatch(doc_stream, size=5000)
X_test = vect.transform(X_test)
print('Accuracy: %.3f' % clf.score(X_test, y_test))
Accuracy: 0.867
87%の正解率、最初より少し落ちてはいるが効率よく学習が行えた