概要
個人的な備忘録を兼ねたPyTorchの基本的な解説とまとめです。今回はテキスト分類問題になります。
日本語のテキスト分類といえば、ライブドアのニュースコーパスやレビューの感情2値分類が代表的です1。最初の一歩としてはややハードルが高そうです。いきなり長文を扱うのではなく、短文を用いたテキスト分類の演習を通して、ネットワーク構造ついて確認していきます。第20回目になりますがコードの大まかな流れや構造が第1回と類似していることも確認できます。
方針
- できるだけ同じコード進行
- できるだけ簡潔(細かい内容は割愛)
- 特徴量などの部分,あえて数値で記入(どのように変わるかがわかりやすい)
演習用のファイル
データファイルdata_60.zipを解凍してから演習に利用してください。解凍すると以下の4種類になります。
- nlc_data_60.csv:テキスト分類のデータ
- x_id_vector.csv:IDベクトル(入力データ)
- y_label.csv:ラベルデータ
- id_dic.txt:単語とIDの対応表
1. テキスト分類
分類問題とは、データをクラス・カテゴリーに振り分ける問題となります
。画像の分類はその一例です。ニューラルネットワークを用いた分類では、最終層の全結合層を分類したいクラス数に設定し、出力値の大きさでどのクラスに属するのかを表すことが一般的です。これまでも分類問題の演習をいくつか紹介してきました。
今回は、文章を用いた分類問題の演習を行ってみたいと思います。アンケートの自由回答記述欄や商品レビューに対して「好意的、中立的、否定的」を判定することに使えそうです。
PyTorchによるプログラムの流れを確認します。基本的に下記の5つの流れとなります。Juypyter Labなどで実際に入力しながら進めるのがオススメ
- データの読み込みとtorchテンソルへの変換 (2.1)
- ネットワークモデルの定義と作成 (2.2)
- 誤差関数と誤差最小化の手法の選択 (2.3)
- 変数更新のループ (2.4)
- 検証 (2.5)
テキストデータは、第19回【単語の分散表現】で紹介したように、IDベクトル化(トークナイズ)したあと、埋め込み層を利用して、分散表現の行列の形で表現できることがわかっています。
行列の形なら画像に似ている!ということで画像分類の手法を援用して文章を分類してみたいと思います。検証精度は低めです![]()
CNN を利用した文章分類を例に大まかな構造を確認します。
図1:CNNを利用したテキスト分類
IDベクトル化(トークナイズ)されたテキストは、埋め込み層によって、文章は,単語毎にベクトル化されるます。ベクトルを文の単語順に並べることで「単語数 × 分散表現ベクトル」の行列が構成できます。見た目は1チャンネルの画像と同様のデータ構造になります。この分散表現の行列に対して畳み込みを適用してテキスト分類を行っていきます。
図1は、5×4 行列の分散表現行列に対して、カーネルサイズが2×4 の CNN を適用したものです。カーネルサイズの高さを2とすることで、畳み込み計算時に単語の前後関係の特徴を抽出することができそうです。CNN によって、4次元の縦ベクトルが抽出されます2。これらの特徴量に全結合層を適用することで分類問題として扱うことが可能となります。
2. コードと解説
2.0 データについて
利用するデータについて簡単に紹介します。ある架空のwebサービスを利用する場面を想定しています。利用サービスに関するテキスト分類で、「ログインに関する内容」、「登録に関する内容」、「解約に関する内容」の3種類のカテゴリーを分類するものとなります。<eos>タグでも句点を代用できそうなため、利用する文章の句点を省略してあります。3。具体的には、表1のような形になります。
| 文章 | 分類名 |
|---|---|
| ログインできない | ログイン |
| 入会したい | 登録 |
| 退会したい | 解約 |
表1:データサンプル
MeCabなどで形態素解析を行い、単語IDの辞書を作成します。4種類のタグを準備しました。<unk> は辞書にない単語、<bos>は文の開始記号、<eos> は文の終端記号、<pad>は文の長さを調整する記号をそれぞれ表しています。
<unk> : 0
<bos> : 1
<eos> : 2
<pad> : 3
ID:4
︙
ログイン:57
︙
辞書を利用して、文章をIDベクトルで表現します。それぞれの文章データの長さを揃えるために、<pad>を用いて文章の系列長を等しく整えます。
等長化した文のIDベクトルは表2のようになります。分類IDは分類名を番号で記したものです。
| 文章 | IDベクトル | 分類名 | 分類ID |
|---|---|---|---|
| ログインできない | 1,57,18,23,2,3,3,3,3,3,3 | ログイン | 1 |
| 入会したい | 1,70,11,15,2,3,3,3,3,3,3 | 登録 | 2 |
| 退会したい | 1,100,11,15,2,3,3,3,3,3,3 | 解約 | 0 |
表2:等長化IDベクトル
等長化されたIDベクトルが入力データ、分類IDが教師データとなります。入力データも教師データも整数になっているのだな〜って気が付きます![]()
2.1 データの読み込みとtorchテンソルへの変換
まず利用するライブラリを読み込みます。
import numpy as np
import torch
import torch.nn as nn
from sklearn.model_selection import train_test_split
データの読み込み
numpyのloadtxtを利用してcsvファイルを読み込みます。$x$がIDベクトルになった文、$t$が分類IDで0,1,2という整数値の教師データです。scikit-learnのtrain_test_splitを利用して、学習用データとテスト(検証)用データとに分割します。
# (1) デバイスの選択
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print("利用デバイス:", device)
# (2) データの読み込み
# ファイルパスは適宜変更してください
# x: IDベクトル(文)
# t: ラベル(0, 1, 2)
# torchテンソルに変換
x = np.loadtxt("data_60/x_id_vector.csv", delimiter=",")
t = np.loadtxt("data_60/y_label.csv", delimiter=",")
x = torch.LongTensor(x)
t = torch.LongTensor(t)
# (3) 学習用データと検証用データに分割
x, x_test, t, t_test = train_test_split(x,t, stratify=t, random_state=55)
# (4) GPU使える場合はGPUへ
x = x.to(device)
t = t.to(device)
x_test = x_test.to(device)
t_test = t_test.to(device)
# x.shape: torch.Size([45, 11])
# x_test.shape: torch.Size([15, 11])
説明メモ
- (1) 利用するデバイスの設定。GPUあるときは使うぞ。
- (2) np.loadtxt(ファイル名,句切り記号)を利用してCSVファイルを読み込みます。
- 入力データも教師データも整数なので、torch.LongTensor()を使います。
- (3) train_test_splitのオプション stragify=t を指定すると、学習用データと検証用データで分類IDの比率を元のデータと同じに保つように分割できます。tと同じような割合で分割という意味です。
2.2 ネットワークモデルの定義と作成
下図のようなCNN(畳み込みネットワーク)と分類のための全結合層を利用したネットワークで分類問題を扱ってみたいと思います。
具体的には埋め込み層と形状変換によって(1チャンネル、系列長、分散表現の次元) のデータに整形した分散表現行列をCNNの入力データとします。CNN、一列化、全結合層を経て、3次元ベクトル(3種類の予測)を出力する形となります。
畳み込み層
nn.Conv2d(in_channels, out_channels, kernel_size, stride)
- in_channels : 入力されるチャンネル数
- out_channels : 出力されるチャンネル数
- kernel_size : カーネルサイズ(縦 × 横)
- stride : カーネルの移動量
基本的な確認事項
ネットワークモデルに登場する数値の意味をまとめておきます4。
- WORDS = 105:単語数 作成した辞書から数を求める
- SEQ_LEN = 11:入力するIDベクトルの長さ(系列長) x.shape[1]
- CLASSES = 3:分類数
ネットワークの書き方はこれまで同様クラスを利用して記述します。
(1) __init__() : 利用するネットワーク名・活性化関数をすべて記述
(2) forward() : 実際の流れを記す
class DNN(nn.Module):
def __init__(self):
super(DNN, self).__init__()
self.embed = nn.Embedding(num_embeddings=105, embedding_dim=4)
self.cnn = nn.Conv2d(in_channels=1, out_channels=10 ,kernel_size=(2,4)) # チャンネル数,フィルター数,カーネルサイズ kernel_size=(2,4)
self.flat = nn.Flatten()
self.fc = nn.Linear(in_features=10*(11-1), out_features=3) # 11=SEQ_LEN: 入力するIDベクトルの長さ
def forward(self, x):
# idベクトルを(バッチサイズ,高さ(文の単語),幅(単語の埋め込み表現))のデータに変換
wv = self.embed(x)
# (バッチサイズ,チャンネル,高さ(系列長),幅(埋め込み次元) [bs 1, 11, 4]
h = wv.reshape(wv.shape[0], 1, wv.shape[1], wv.shape[2])
h = self.cnn(h) # 出力 shape = [bs, 10, 10, 1]
h = self.flat(h)
y = self.fc(h)
return y
model = DNN()
model.to(device)
説明メモ
- Embeddingによって入力文を(11, 4)の表現行列に変換します。11は文の長さ、系列長になります。
- Conv2dの入力に使えるように、 reshape() や unsqueeze() を使い、チャンネルの次元を追加します。
- CNNに入力されるデータは(1チャンネル、縦11、横4)のテンソル
- 1×11×4の文の分散表現行列(テンソル?)に対して、カーネルサイズ2x4のCNNを適用します。出力される特徴量の形状は「 1 x 10 」になります。
- out_channels=10としているので1x10の特徴量が10種類となります。(10, 1, 10)のテンソル
- (10, 1, 10)を一列化(Flatten)するので、10x1x10が全結合層fcへの入力となります。つまり、
Linear(in_features=10*10, ...)となります。 - 出力は3分類なので全結合層fcは、
Linear(..., out_features=3)となります。
今回は活性化関数やdropout関数は利用しない単純な構成になっています。追加することで分類精度を向上させることが可能となります。
2.3 誤差関数と誤差最小化の手法の選択
分類問題では予測値$y$ と教師データ$t$ (0, 1, 2の分類ID)の誤差を計算するのに、クロスエントロピーを用いることが定番です。PyTorchではCrossEntropyLoss() と記述します。
criterion = nn.CrossEntropyLoss() # 損失関数:cross_entropy
optimizer = torch.optim.AdamW(model.parameters(), lr=0.01) # 学習率:lr=0.001がデフォルト
- nn.CrossEntropyLoss() は予測値と実測値(教師データ)のクロスエントロピー損失。使用時は、criterion(x,t) とする。
- torch.optim.AdamW() は誤差の最小値を求める方法の一つ
PyTorchで利用できるoptimizerについては、公式ドキュメントにたくさん書かれています。
2.4 変数更新のループ
LOOPで指定した回数、次の内容を繰り返します。
- y=model(x) で予測値を求め、
- criterion(y, t) で指定した誤差関数を使い予測値と教師データの誤差を計算、
- 誤差が小さくなるようにoptimizerに従い全結合層の重みとバイアスをアップデート
LOOP = 25 # LOOP : 学習回数 25〜30あれば訓練データでの精度1.0
for epoch in range(LOOP):
optimizer.zero_grad() # 勾配初期化PyTorchの約束事項
y = model(x) # ネットワークモデルによる予測
loss = criterion(y, t) # 誤差計算PyTorchの約束事項
acc = accuracy(y,t) # 精度の計算
print(f"{epoch}: loss: {loss.item()}, acc:{acc}") # 損失と精度の表示
loss.backward() # backward(微分する部分)
optimizer.step() # パラメータ更新PyTorchの約束事項
平均精度について
accuracyの関数について簡単な説明を残しておきます。予測値と教師データで等しい値なら正解として、正解数/問題数で精度を求める単純な平均精度を求める作成してみました。
def accuracy(y, t):
_,argmax_list = torch.max(y, dim=1)
accuracy = sum(argmax_list == t).item()/len(t)
return accuracy
2.5 検証
2.1のデータ分割で作成したテストデータ x_test と t_test を利用して学習結果をテストしてみましょう。x_testをmodelに入れた値 y_test = model(x_test) が予測値となります。accuracyで平均精度を求めれば完成です。
y_test = model(x_test)
acc = accuracy(y_test, t_test)
print("平均精度", acc)
# 平均精度 0.533... orz
平均精度50%
残念な結果。ちなみに、53%は割と良い結果です。ほとんど40%前後だと思います。運が悪いと20%ということもありえます。もはやランダムより悪い結果ですが![]()
![]()
地味に一つずつ確認して表にまとめてみました。本当は「解約」の内容なのに「登録」と予測しているようです。同様に、本当は「登録」の内容なのに「解約」と誤判定していることが伺えます。
予測値
解約 ログイン 登録
実際の値 解約 4 0 1
ログイン 2 3 0
登録 3 1 1
embedding_dim、out_channelsやkernel_sizeの高さを変更することで平均精度は55〜60%くらいになります。CNN層やプーリング層を追加するなどの工夫をすることでさらなる改善が見込めます。
その他・次回
各カテゴリー10個ずつデータを増やして、合計90個にして学習を行ってみました。同一のネットワーク構造でも検証精度が70%程度に上昇します。個別の結果は下記の表のようになります。データ量は正義であることがわかります。90個のデータは第21回のデータセットからダウンロードできる予定です。
次回、文章は順番に並ぶ文字列、つまり、「時系列データ」 ということで1次元畳み込みを利用したテキスト分類いわゆる text CNNネットワークを扱う予定です。
予測値
解約 ログイン 登録
実際の値 解約 5 1 2
ログイン 0 7 1
登録 3 0 4
目次ページ
注
-
HuggingFaceのsbintuitions/JMTEB からニュースコーパス、Amazonの商品レビュー、感情分類コーパスなどの日本語データセットを取得することができます。 ↩
-
出力チャンネル数が1の場合となります。CNN の出力チャンネル数を増やすことでより詳細な特徴量を抽出できる可能性もあります。この場合は最終層に入力する特徴量を調整する工夫が必要となりそうです。 ↩
-
日本語の「文」は、たしか「句点(。)で終わるもの」と小学校か中学校で習った気がするのですが気にせず進めていきましょう
↩ -
文字で置き換えたほうが間違えも少なくなるのですが、当初の予定通りあえて数値で書いてあります。 ↩


