はじめに
最近何かと話題のChainerを使って文書のポジネガを判定する2値分類器を実装してみました。
初めてChainerを使ったので、練習用ということで簡単なモデルとなっています。
筆者のようにChainerでディープニューラルネットを実装して何かしてみたいという方向けです。
間違い等はコメント欄で指摘していただけると助かります。
全コードはこちらからご参照下さい。
事前準備
- chainer, gensim, scikit-learnのインストール
環境
- Python 2.7系
- Chainer 1.6.2.1
使用するデータの例
使用したデータは、英語の何かのレビューに関する文書です。
各行が一文書に対応しており、文書中の各単語は半角スペースで区切られています。
各行の先頭の数字(e.g. 1, 0)は、ラベルです。
*データセットは各自用意して下さい。
*日本語の文書を使用する場合は、MeCabなどで分かち書きを行って下さい。
0 each scene drags , underscoring the obvious , and sentiment is slathered on top .
0 afraid to pitch into farce , yet only half-hearted in its spy mechanics , all the queen's men is finally just one long drag .
1 clooney directs this film always keeping the balance between the fantastic and the believable . . .
1 just about the best straight-up , old-school horror film of the last 15 years .
文書のベクトル化
各文書をニューラルネットワークの入力として扱うために、bag-of-wordsでベクトル化を行います。
ベクトル化には、gensimの関数を利用しました。
詳しくはこちらの記事を参考にして下さい。→ scikit-learnとgensimでニュース記事を分類する
関数load_data
では、入力データを読み込み、各文書のラベルと単語列をl.strip().split(" ", 1)
で分割しています。
target
には文書ラベル、source
には文書ベクトルをそれぞれ格納し、dataset
にまとめて戻り値として返します。
corpora.Dictionary(document_list)
では、各単語を要素とする文書リストのリスト(document_list
)を渡すことで、単語辞書を作成しています。
本来なら、訓練データのみを使って単語辞書を作成しないといけないのですが、未知語処理を省きたかったので全文書を使って単語辞書を作成しています。
ここでvocab_size
は、全文書の語彙数であり、文書ベクトルの次元数に対応します。
そのため、今回実装したニューラルネットの入力層のユニット数は、vocab_size
と等しいです。
def load_data(fname):
source = []
target = []
f = open(fname, "r")
document_list = [] #各行に一文書. 文書内の要素は単語
for l in f.readlines():
sample = l.strip().split(" ", 1) #ラベルと単語列を分ける
label = int(sample[0]) #ラベル
target.append(label)
document_list.append(sample[1].split()) #単語分割して文書リストに追加
#単語辞書を作成
dictionary = corpora.Dictionary(document_list)
dictionary.filter_extremes(no_below=5, no_above=0.8)
# no_below: 使われている文書がno_below個以下の単語を無視
# no_above: 使われてる文章の割合がno_above以上の場合無視
#文書のベクトル化
for document in document_list:
tmp = dictionary.doc2bow(document) #文書をBoW表現
vec = list(matutils.corpus2dense([tmp], num_terms=len(dictionary)).T[0])
source.append(vec)
dataset = {}
dataset['target'] = np.array(target)
dataset['source'] = np.array(source)
print "vocab size:", len(dictionary.items()) #語彙数 = 入力層のユニット数
return dataset, dictionary
モデルの定義
今回は練習用ということで、簡単なモデルを実装しました。
(さきほどの関数load_data
から受け取ったdataset
をscikit-learnに入っている関数train_test_split
を利用して、訓練データとテストデータに分割しています。)
入力層のユニット数in_units
は、文書ベクトルの次元数(x_train.shape[1]
)を入れています。
隠れ層(中間層)は、適当に設定して構いません。今回は、デフォルトで500を渡すようにしています。
出力層は、ソフトマックス関数を使うので、ユニット数はラベルのタイプ数である2としています。
x_train, x_test, y_train, y_test = train_test_split(dataset['source'], dataset['target'], test_size=0.15)
N_test = y_test.size # test data size
N = len(x_train) # train data size
in_units = x_train.shape[1] # 入力層のユニット数 (語彙数)
n_units = args.units # 隠れ層のユニット数
n_label = 2 # 出力層のユニット数
#モデルの定義
model = chainer.Chain(l1=L.Linear(in_units, n_units),
l2=L.Linear(n_units, n_units),
l3=L.Linear(n_units, n_label))
順伝搬
関数forward
では順伝搬を行います。
入力層->隠れ層、隠れ層->隠れ層の活性化関数には、シグモイド関数を使用しました。
def forward(x, t, train=True):
h1 = F.sigmoid(model.l1(x))
h2 = F.sigmoid(model.l2(h1))
y = model.l3(h2)
return F.softmax_cross_entropy(y, t), F.accuracy(y, t)
学習
全体の流れとしては、
1. 訓練データからバッチを作成
2. 順伝搬
3. 誤差逆伝播
4. パラメータ更新
という感じです。(あまり自信ない…)
各epochで、訓練データに対する誤差・テストデータに対する誤差を計算しています。
また、分類問題なので分類正解率accuracy
もそれぞれ計算します。
# Setup optimizer
optimizer = optimizers.Adam()
optimizer.setup(model)
# Learning loop
for epoch in six.moves.range(1, n_epoch + 1):
print 'epoch', epoch
# training
perm = np.random.permutation(N) #ランダムな整数列リストを取得
sum_train_loss = 0.0
sum_train_accuracy = 0.0
for i in six.moves.range(0, N, batchsize):
#perm を使い x_train, y_trainからデータセットを選択 (毎回対象となるデータは異なる)
x = chainer.Variable(xp.asarray(x_train[perm[i:i + batchsize]])) #source
t = chainer.Variable(xp.asarray(y_train[perm[i:i + batchsize]])) #target
model.zerograds() # 勾配をゼロ初期化
loss, acc = forward(x, t) # 順伝搬
sum_train_loss += float(cuda.to_cpu(loss.data)) * len(t) # 平均誤差計算用
sum_train_accuracy += float(cuda.to_cpu(acc.data )) * len(t) # 平均正解率計算用
loss.backward() # 誤差逆伝播
optimizer.update() # 最適化
print('train mean loss={}, accuracy={}'.format(
sum_train_loss / N, sum_train_accuracy / N)) #平均誤差
# evaluation
sum_test_loss = 0.0
sum_test_accuracy = 0.0
for i in six.moves.range(0, N_test, batchsize):
# all test data
x = chainer.Variable(xp.asarray(x_test[i:i + batchsize]))
t = chainer.Variable(xp.asarray(y_test[i:i + batchsize]))
loss, acc = forward(x, t, train=False)
sum_test_loss += float(cuda.to_cpu(loss.data)) * len(t)
sum_test_accuracy += float(cuda.to_cpu(acc.data)) * len(t)
print(' test mean loss={}, accuracy={}'.format(
sum_test_loss / N_test, sum_test_accuracy / N_test)) #平均誤差
#modelとoptimizerを保存
print 'save the model'
serializers.save_npz('pn_classifier_ffnn.model', model)
print 'save the optimizer'
serializers.save_npz('pn_classifier_ffnn.state', optimizer)
実行結果
最終的なテストデータに関する分類正解率はaccuracy=0.716875001788
となりました。
しかし、学習が進むにつれてテスト誤差が増加しており、過学習が起きています...
おそらくてきとうにモデルを組んだことが原因でしょう。
>python train.py --gpu 1 --data input.dat --units 1000
vocab size: 4442
epoch 1
train mean loss=0.746377664579, accuracy=0.554684912523
test mean loss=0.622971419245, accuracy=0.706875003874
epoch 2
train mean loss=0.50845754933, accuracy=0.759408453399
test mean loss=0.503996372223, accuracy=0.761249992996
epoch 3
train mean loss=0.386604680468, accuracy=0.826067760105
test mean loss=0.506066314876, accuracy=0.769374992698
epoch 4
train mean loss=0.301527346359, accuracy=0.870433726909
test mean loss=0.553729468957, accuracy=0.774999994785
epoch 5
train mean loss=0.264981631757, accuracy=0.889085094432
test mean loss=0.599407823756, accuracy=0.766874998808
epoch 6
train mean loss=0.231274759588, accuracy=0.901114668847
test mean loss=0.68350501731, accuracy=0.755625002086
...
epoch 95
train mean loss=0.0158744356008, accuracy=0.993598945303
test mean loss=5.08019682765, accuracy=0.717499997467
epoch 96
train mean loss=0.0149783944279, accuracy=0.994261124581
test mean loss=5.30629962683, accuracy=0.723749995232
epoch 97
train mean loss=0.00772037562047, accuracy=0.997351288256
test mean loss=5.49559159577, accuracy=0.720624998212
epoch 98
train mean loss=0.00569957431572, accuracy=0.99834455516
test mean loss=5.67661693692, accuracy=0.716875001788
epoch 99
train mean loss=0.00772406136085, accuracy=0.997240925267
test mean loss=5.63734056056, accuracy=0.720000002533
epoch 100
train mean loss=0.0125463016702, accuracy=0.995916569395
test mean loss=5.23713605106, accuracy=0.716875001788
save the model
save the optimizer
おわりに
Chainerを使って、文書分類のためのフィードフォワードニューラルネットワークを実装しました。
過学習が起きないように、モデルの改善を行いたいと思います。
Chainerの勉強用にコードを見てみたい方は、こちらからご参照下さい。