自然言語処理⑥~アウトオブコア学習~

ほんとはDoc2Vecとかtransformerとかに早くいきたいのですが、いつも参照しているテキストの方で知らない要素があったので、使います。

あと、100本ノックも再開します。

具体的に今回の内容はアウトオブコア学習と潜在的ディリクレ探索を解説します。

(追記:この時点ではLDAの予定でしたが、文章量が多くなりそうだったので、アウトオブコアのみと100本ノックにしました。次回LDAを解説します。)

自然言語処理のデータは前回の映画データセットみたく、形態素の数だけ次元数が増えたり、そもそも文章データが膨大になったりと重いことがよくあります。

マシンスペックとサーバーなどが潤沢であれば別ですが、一般のPCでかつローカルでの作業(これが嫌なので今回はcolaboratoryを使っている)はかなりしんどいので、アウトオブコア学習を用いることでデータを区切り多少の精度を犠牲にしても処理を高速化しようという試みです。

それでは、今回もよろしくお願いいたします。

・アウトオブコア学習とは?

導入部分でも書きましたが、アウトオブコア学習はデータセットを小さなバッチ単位で細切れに学習させるイメージです

理論という理論もないのでコードを見ていきながら進めていきます。

今回はデータが重い場合を想定したいため、スクレイピングでニュース記事を取ってくるよりも、前回の実戦で用いた映画データセットを用います。

では、準備していきましょう。(正規表現基本的に何も考えずコピペでいいです。本筋ではないので。。)

(毎回colaboratoryで上からコード実行するのが面倒という方はローカルでデータだけ落とし込み、毎度アップロードして実装コードの部分から始めるのもいいと思いますので、お好きなように!)

- データの落とし込み

!wget http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz
!tar -zxf aclImdb_v1.tar.gz

- データフレームに落とし込み

import os
import pandas as pd
import numpy as np


basepath = 'aclImdb'
labels = {'pos': 1, 'neg': 0}
df = pd.DataFrame()

for data_path in ('test', 'train'):
 for sentiment in ('pos', 'neg'):
   path = os.path.join(basepath, data_path, sentiment)

   for file in sorted(os.listdir(path)):
     with open(os.path.join(path, file), 'r', encoding='utf-8') as infile:
       txt = infile.read()

     df = df.append([[txt, labels[sentiment]]], ignore_index=True)


df.columns = ['review', 'sentiment']

np.random.seed(0)
df = df.reindex(np.random.permutation(df.index))

- データの保存(のちにpathなどで指定するため)

df.to_csv('movie_data.csv', index=False, encoding='utf-8')

- 正規表現でテキストのクレンジング準備

import numpy as np
import re

from nltk.corpus import stopwords

stop = stopwords('english')

def tokenizer(text):

 text = re.sub('<[^>]*>', '', text)
 emotions = re.findall('(?::|;|=)(?:-)?(?:\)|\(|D|P)', text)

 text = (re.sub('[\W]+', ' ', text.lower()) + ' '.join(emotions).replace('-', ' '))
 torkenized = [w for w in text.split() if w not in stop]
 return torkenized

- バッチ処理のための関数準備

def stream_docs(path):
 with open(path, 'r', encoding='utf-8') as csv:
   # headerがあるため最初はスキップする
   next(csv)
   for line in csv:
     text, label = line[:-3], int(line[-2])

     # yieldにすることでlineごとに処理を停止できる
     yield text, label

ちょっとだけ上の関数を細くすると、今回処理の高速化を目的としているため、returnを使うとすべての処理が終わるまで結果を返してくれませんが、yieldにすることでforの中の処理が一回終わるごとに次のforの繰り返しを行わず結果を出力してくれます。

この辺はgenerator関数の部分なのでnext関数が使えます

試しに簡易的に見てみましょう

print(stream_docs('movie_data.csv'))
next(stream_docs('movie_data.csv'))

スクリーンショット 2021-08-28 14.40.34

この辺の詳細は一旦飛ばして進めます。

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

この関数は自分で欲しい数(size)を決めることでその数だけnext関数を呼び出しテキストとその感情ラベルを抽出します。

(StopIterationはこれ以上呼び出せない時に発生するエラー処理です。)

----準備おしまい----

それでは準備が終わりましたので、本題へ。

今回sklearnのHashingVectorizerを用いて実装します。

まずはコードから。

from sklearn.feature_extraction.text import HashingVectorizer
from sklearn.linear_model import SGDClassifier

vect = HashingVectorizer(decode_error='ignore', 
                         n_features=2**21, 
                         preprocessor=None, 
                         tokenizer=tokenizer)

clf = SGDClassifier(loss='log', random_state=1)
doc_stream = stream_docs('movie_data.csv')

コードの続きをいく前にこのHashingVectorizerが何者なのか、しれっと出て生きているSGDClassifierはなんなのかをみてから進みたいと思います。

まず、HashingVectorizerを見ていくにあたり、前提を押さえておかなければなりません。

前回まではone-hot、BoWなどで文書を定量的に変換する方法を学びましたが、今回のようにgenerator関数を用いてしまうとメモリ消費を抑えるために逐次的処理にシフトしています。

しかし、前回まで使っていたCountVectorizerなどはメモリに全データを保持していることが前提のため、CountVectorizerでの単語のカウントをしていくことができません。

そこで事前のデータの読み込みが必要ないハッシュ関数(※)を用いて、以下の処理で特徴量の行列を低次元に変換する作業をします。

1:特徴量を入れてハッシュ値を取得

2:1 or -1を返すハッシュ値に変換する関数に特徴量を入れて、1で得られたハッシュ値に加算する

(※簡単に言えば、中身はどうなっているかわからないけれど、値を入れたら何かしら返ってくる関数。得られた結果(ハッシュ値)から入力されたデータを解析することはほぼ不可能と言われていて、暗号化などに用いられる。何かしら、とはいうものの、同じものを入れれば必ず同じものが返ってきます。)

class sklearn.feature_extraction.text.HashingVectorizer(*, input='content', encoding='utf-8', decode_error='strict', strip_accents=None, lowercase=True, preprocessor=None, tokenizer=None, stop_words=None, token_pattern='(?u)\b\w\w+\b', ngram_range=(1, 1), analyzer='word', n_features=1048576, binary=False, norm='l2', alternate_sign=True, dtype=<class 'numpy.float64'>)

こちらもパラメータがおおいので、今回利用したパラメータを中心に見ていきます

decode_error: エンコーディングが指定されていないcharacterを解析する際にUnicodeDecodeErrorを表示するかどうか(今回ignoreなので、エラー表示は無視)

n_features: 特徴量の個数を指定

preprocessor, tokenizerは前回と同様

今回のHashingVectorizerのパラメータtokenizerには事前に準備したtokenizer関数(今回は顔文字だけ残して残りをクレンジングしたりする)を指定してtokenizeしていきます。

では、SGDClassifierに行きましょう
公式ドキュメント

そもそもSGDClassifierは勾配降下法のことで、予測と正解の誤差を最小化するために重みを更新する方法の一つです。

いわゆる順々に勾配を降るように下げていくイメージ(本筋ではないので、詳細は割愛。)

class sklearn.linear_model.SGDClassifier(loss='hinge', *, penalty='l2', alpha=0.0001, l1_ratio=0.15, fit_intercept=True, max_iter=1000, tol=0.001, shuffle=True, verbose=0, epsilon=0.1, n_jobs=None, random_state=None, learning_rate='optimal', eta0=0.0, power_t=0.5, early_stopping=False, validation_fraction=0.1, n_iter_no_change=5, class_weight=None, warm_start=False, average=False)

loss: ‘hinge’, ‘log’, ‘modified_huber’, ‘squared_hinge’, ‘perceptron’などなど。この'log'はロジスティック分類器のlogで対数関数ではないです。

他にもたくさんパラメータがあるのですが、自然言語処理が主題のため、このシリーズで飛ばしているところは、またおいおいします(たぶん。。)

では、結果を見てみます

ちなみに、処理時間の計測なども見ていけばいいのですが、ちょっと今回は割愛します。

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)
X_test, y_test = get_minibatch(doc_stream, size=5000)
X_test = vect.transform(X_test)

clf.score(X_test, y_test)

スクリーンショット 2021-08-29 17.26.53

前回が0.89くらいだったので精度自体は落ちていますが、処理速度は早くなります(確認方法はいくつかありますが、%timeitとかでいいと思います。)

・100本ノック第4章

いよいよ4章からMeCabに取り組みます。

全てを解説しませんが、個人的に気になったものをピックアップします。

まず、導入でつまづきました。。

夏目漱石の小説『吾輩は猫である』の文章(neko.txt)をMeCabを使って形態素解析し,その結果をneko.txt.mecabというファイルに保存せよ.このファイルを用いて,以下の問に対応するプログラムを実装せよ.

普通にimport MeCabとかで進めていくかと思いきや、コマンドラインでmecabコマンドを使うんですねw

コマンドライン上でのmecabコマンドが何をできるのかあまり知らなかったので、最初で詰みましたw

一応、参考になったURLは以下。

mecabのオプションで「o」というのがあり、outputすることで.mecabファイルに保存ができます。

- 30. 形態素解析結果の読み込み

形態素解析結果(neko.txt.mecab)を読み込むプログラムを実装せよ.ただし,各形態素は表層形(surface),基本形(base),品詞(pos),品詞細分類1(pos1)をキーとするマッピング型に格納し,1文を形態素(マッピング型)のリストとして表現せよ.第4章の残りの問題では,ここで作ったプログラムを活用せよ.

この辺も中身の取り出し方に工夫が必要で、コードの「EOS\n」とか空白の検知などちょっとクセのある問題でした。

この準備ができてしまえば、他の問題は理解しやすかったかなと思います。

・終わり

Doc2Vecに行きたいのと、transformer とか触りたいのですが、なんか道にそれている最近。

次回はLDAなどのトピックモデルを扱ってみます。

ではまた次回もよろしくお願いします。


この記事が気に入ったらサポートをしてみませんか?