Copyright (c) 2020 NLP100-MATLAB
第6章で取り組んだニュース記事のカテゴリ分類を題材として,ニューラルネットワークでカテゴリ分類モデルを実装する.なお,この章ではPyTorch, TensorFlow, Chainerなどの機械学習プラットフォームを活用せよ.
問題50で構築した学習データ,検証データ,評価データを行列・ベクトルに変換したい.例えば,学習データについて,すべての事例の特徴ベクトル
を並べた行列
と,正解ラベルを並べた行列(ベクトル)
を作成したい.
ここで,
は学習データの事例数であり,
と
はそれぞれ,
番目の事例の特徴量ベクトルと正解ラベルを表す.なお,今回は「ビジネス」「科学技術」「エンターテイメント」「健康」の4カテゴリ分類である.
で4未満の自然数(0を含む)を表すことにすれば,任意の事例の正解ラベル
は
で表現できる.以降では,ラベルの種類数を
で表す(今回の分類タスクでは
である).
ここで,
番目の事例は
個の(記事見出しの)単語列
から構成され,
は単語
に対応する単語ベクトル(次元数は
)である.すなわち,
番目の事例の記事見出しを,その見出しに含まれる単語のベクトルの平均で表現したものが
である.今回は単語ベクトルとして,問題60でダウンロードしたものを用いればよい.
次元の単語ベクトルを用いたので,
である.
なお,カテゴリ名とラベルの番号が一対一で対応付いていれば,上式の通りの対応付けでなくてもよい.
以上の仕様に基づき,以下の行列・ベクトルを作成し,ファイルに保存せよ.
なお,はそれぞれ,学習データの事例数,検証データの事例数,評価データの事例数である.
問題50で保存したデータを読み込みます。
opt = detectImportOptions('train.txt');
opt = setvartype(opt,["categorical","string"]);
dataTrain = readtable('train.txt',opt);
dataValid = readtable('valid.txt',opt);
dataTest = readtable('test.txt',opt);
単語ベクトルを作成するためのWord2Vecモデルを読み込みます(学習済みモデルのファイルは同じフォルダにあるものと仮定しています).読み込みにはバイナリ形式のword2vecファイルを読み込むための関数readW2Vbin.m を自前で作ったので、それを使っています.興味のある方はMathWorks の FileExchange に上げてありますので,そちらをどうぞ.
emb = readW2Vbin('GoogleNews-vectors-negative300.bin');
特徴量を計算するために、形態素解析にかけましょう。
docsTrain = tokenizedDocument(dataTrain.Title);
docsValid = tokenizedDocument(dataValid.Title);
docsTest = tokenizedDocument(dataTest.Title);
tknTrain = tokenDetails(docsTrain);
tknValid = tokenDetails(docsValid);
tknTest = tokenDetails(docsTest);
特徴量を計算するためには、各文を構成する単語一つ一つについて、word2vec を適用する必要があります.そこで、まずtokenDetails を使って単語の一覧テーブルを生成します.そして,列Token に対して一括でword2vec を適用し,結果を新たな列 vec に格納します.文毎に平均をとるには,列 DocumentNumber をキーにして関数 grpstats を適用します.
tknTrain.vec = word2vec(emb,tknTrain.Token);
tknValid.vec = word2vec(emb,tknValid.Token);
tknTest.vec = word2vec(emb,tknTest.Token);
XTrainTable = grpstats(tknTrain(:,[2 end]),'DocumentNumber');
XValidTable = grpstats(tknValid(:,[2 end]),'DocumentNumber');
XTestTable = grpstats(tknTest(:,[2 end]),'DocumentNumber');
XTrain = XTrainTable.mean_vec;
XValid = XValidTable.mean_vec;
XTest = XTestTable.mean_vec;
MATLABでラベルデータを扱う場合は,Categorical配列として扱った方が楽な場合が多いので,作業としては読み込んだデータテーブルから該当する部分を抜きだすだけになります.
YTrain = dataTrain.Category;
YValid = dataValid.Category;
YTest = dataTest.Category;
どうしてもベクトル化したいならば,すでに付いている文字列のラベルを数値に変換すればよいので,これも簡単です.各データに含まれているCategory列(b, t, e, m のいずれかの値をとる)をChar 型に変換して文字行列を作成し,その文字行列の各行と文字列'btem'とを比較します.比較の結果は,Category列 と同じ行数で4列の論理配列になります.これは,ラベルデータのOne-Hot表現ともよばれる表現方法ですね.
TTrain = char(dataTrain.Category)=='btem';
TValid = char(dataValid.Category)=='btem';
TTest = char(dataTest.Category) =='btem'
TTest = 1334x4 の logical 配列
1 0 0 0
1 0 0 0
1 0 0 0
1 0 0 0
1 0 0 0
1 0 0 0
1 0 0 0
1 0 0 0
0 0 1 0
0 0 1 0
この配列に対してfindを適用すると,該当するカテゴリーの線形インデックスが得られます.得られた線形インデックスを,通常の行列の添え字形式に変換してその第二成分をとることで,ラベルベクトルになります.
[~,YTrainVec] = ind2sub([height(dataTrain) 4],find(TTrain));
[~,YValidVec] = ind2sub([height(dataValid) 4],find(TValid));
[~,YTestVec ] = ind2sub([height(dataTest) 4], find(TTest))
YTestVec = 1334x1
1
1
1
1
1
1
1
1
1
1
問題70で保存した行列を読み込み,学習データについて以下の計算を実行せよ.
ただし,
はソフトマックス関数,
は特徴ベクトル
を縦に並べた行列である.
行列
は単層ニューラルネットワークの重み行列で,ここではランダムな値で初期化すればよい(問題73以降で学習して求める).なお,
は未学習の行列
で事例
を分類したときに,各カテゴリに属する確率を表すベクトルである. 同様に,
は,学習データの事例
について,各カテゴリに属する確率を行列として表現している.
word2vec モデルからモデル次元,学習データからカテゴリー数を取得して,行列を生成します.
inputSize = emb.Dimension;
numClasses = numel( categories(dataTrain.Category));
W = rand([inputSize, numClasses]);
数式の通りに記述するだけ,と言いたいのですが一つだけ注意が必要です.一般に機械学習ではデータの行列は行が観測,列が特徴量に対応していますが,深層学習ではそれが逆になります.MATLABの関数の実装も概ねこの原理に従っていますので,softmax関数の入力はを転置したものになります.結果の確率ベクトルを行ベクトルにするためには,さらに
softmax関数の計算結果をもう一度転置する必要があります.
Yhat = softmax((XTrain(1:4,:)*W)')'
Yhat = 4x4 の single 行列
0.2863 0.2195 0.2776 0.2165
0.4017 0.1729 0.1647 0.2607
0.2441 0.1912 0.2021 0.3626
0.3929 0.2097 0.2409 0.1566
の計算ができていれば,単一の事例に対する計算結果は対応する行を抜き出すだけです.
yhat_1 = Yhat(1,:)
yhat_1 = 1x4 の single 行ベクトル
0.2863 0.2195 0.2776 0.2165
学習データの事例
と事例集合
に対して,クロスエントロピー損失と,行列
に対する勾配を計算せよ.なお,ある事例
に対して損失は次式で計算される.
ただし,事例集合に対するクロスエントロピー損失は,その集合に含まれる各事例の損失の平均とする.
クロスエントロピー損失と勾配については,ラベルデータをOne-Hot表現で表現すると扱いが楽になります.事例集合全体の損失関数を
とすると,
これを実装すると次のようになります.あってるかな?
Yhat = softmax((XTrain*W)')';
L = -sum(sum(TTrain.*log(Yhat)))
L = 1.5060e+04
dLdW = -XTrain'*(TTrain-Yhat)
dLdW = 300x4 の single 行列
23.0598 -3.0855 -118.0084 98.0341
-39.3303 13.8832 -53.9515 79.3985
21.0242 -38.0043 128.0468 -111.0667
-110.2795 38.3179 -3.8562 75.8178
60.7615 17.1151 -72.9147 -4.9619
56.3110 -36.3838 -21.5720 1.6448
15.8674 -34.8244 60.4932 -41.5362
22.8266 -77.9163 189.9676 -134.8779
-208.1470 53.7098 -51.3764 205.8136
-118.0010 18.3425 -85.6335 185.2921
確率的勾配降下法(SGD: Stochastic Gradient Descent)を用いて,行列を学習せよ.なお,学習は適当な基準で終了させればよい(例えば「100エポックで終了」など).
問題73以降では手を抜きます.というのも,MATLABのDeepLearning Toolbox では重み行列・損失関数・勾配は内部で自動的に計算してくれるので直接扱うことはほとんどないからです.ここでは,今考えている問題をネットワークオブジェクトとして定義して学習を行います.ここまでの内容から,ネットワークは入力層,全結合層(重み行列に相当),ソフトマックス層,分類出力層(交差エントロピー損失の計算に相当)の4層で構成されることになります.
Deep Learning Toolbox では,画像・シーケンスデータ両方を扱うことができますが,この例題の場合には,サイズが300×1のグレースケール画像(チャンネル数1)として扱うことにします.
layers = [ ...
imageInputLayer([300 1 1],"Name","Input","Normalization","none"); % 入力層
fullyConnectedLayer(numClasses,"Name","FC"); ... % 全結合層
softmaxLayer; ... % ソフトマックス層
classificationLayer ... % 分類出力層
];
また,学習・検証・評価データについて,各事例のサイズを300×1×1に変換します.
nTrain = size(XTrain,1);
nValid = size(XValid,1);
nTest = size(XTest, 1);
XpTrain = reshape(XTrain',[inputSize 1 1 nTrain]);
XpValid = reshape(XValid',[inputSize 1 1 nValid]);
XpTest = reshape(XTest', [inputSize 1 1 nTest ]);
ネットワークの学習については,学習アルゴリズムとしてSGDを,最大エポック数を100として,あとはデフォルト値を使ってみます.それから,後のほうで「GPUを使うようにコードを改変せよ」とのお題があるので,あえてCPUで計算させるオプションを設定します.また,ここではミニバッチは使用しないので,MiniBatchSize オプションを全事例数とします.
options = trainingOptions('sgdm','MaxEpochs',100,"ExecutionEnvironment","cpu","MiniBatchSize",nTrain);
netInit = trainNetwork(XpTrain,YTrain,layers,options);
|=================================================================|
| エポック | 反復 | 経過時間 | ミニバッチの精度 | ミニバッチ損失 | 基本学習率 |
| | | (hh:mm:ss) | | | |
|=================================================================|
| 1 | 1 | 00:00:00 | 21.24% | 1.3853 | 0.0100 |
| 50 | 50 | 00:00:01 | 76.19% | 1.0490 | 0.0100 |
| 100 | 100 | 00:00:01 | 77.39% | 0.9311 | 0.0100 |
|=================================================================|
問題73で求めた行列を用いて学習データおよび評価データの事例を分類したとき,その正解率をそれぞれ求めよ.
これは簡単.
predResub = classify(netInit,XpTrain);
okRatioResub = nnz(predResub == YTrain)/numel(predResub)
okRatioResub = 0.7739
predTest = classify(netInit,XpTest);
okRatioTest = nnz(predTest == YTest)/numel(predTest)
okRatioTest = 0.7744
問題73のコードを改変し,各エポックのパラメータ更新が完了するたびに,訓練データでの損失,正解率,検証データでの損失,正解率をグラフにプロットし,学習の進捗状況を確認できるようにせよ.
これらのことは学習オプションのパラメータ値を調整することで実現できます.まず,学習オプションの"Plot"プロパティを "training-progress" にすれば,学習中の状況を表示してくれます.また,プロパティ"ValidationData"に検証用データと検証用ラベルデータをセル配列として与えることで検証データの設定ができます.また,"ValidationFrequency" で検証頻度が設定できます.
options = trainingOptions('sgdm','MaxEpochs',100,'MiniBatchSize',nTrain, ...
"ExecutionEnvironment","cpu", ...
"Plots","training-progress", ...
"ValidationData",{XpValid,YValid}, ...
"ValidationFrequency",10);
net0 = trainNetwork(XpTrain,YTrain,layers, options);
|===================================================================================|
| エポック | 反復 | 経過時間 | ミニバッチの精度 | 検証精度 | ミニバッチ損失 | 検証損失 | 基本学習率 |
| | | (hh:mm:ss) | | | | | |
|===================================================================================|
| 1 | 1 | 00:00:00 | 20.31% | 21.29% | 1.4049 | 1.3974 | 0.0100 |
| 10 | 10 | 00:00:01 | 32.43% | 36.88% | 1.3401 | 1.3204 | 0.0100 |
| 20 | 20 | 00:00:01 | 52.48% | 56.82% | 1.2331 | 1.2092 | 0.0100 |
| 30 | 30 | 00:00:01 | 66.43% | 68.82% | 1.1530 | 1.1276 | 0.0100 |
| 40 | 40 | 00:00:01 | 71.98% | 72.79% | 1.0996 | 1.0732 | 0.0100 |
| 50 | 50 | 00:00:01 | 74.53% | 75.19% | 1.0610 | 1.0343 | 0.0100 |
| 60 | 60 | 00:00:02 | 75.82% | 76.46% | 1.0302 | 1.0035 | 0.0100 |
| 70 | 70 | 00:00:02 | 76.40% | 77.06% | 1.0037 | 0.9776 | 0.0100 |
| 80 | 80 | 00:00:02 | 76.72% | 77.29% | 0.9802 | 0.9548 | 0.0100 |
| 90 | 90 | 00:00:02 | 76.86% | 77.21% | 0.9590 | 0.9344 | 0.0100 |
| 100 | 100 | 00:00:03 | 76.91% | 77.36% | 0.9396 | 0.9158 | 0.0100 |
|===================================================================================|
問題75のコードを改変し,各エポックのパラメータ更新が完了するたびに,チェックポイント(学習途中のパラメータ(重み行列など)の値や最適化アルゴリズムの内部状態)をファイルに書き出せ.
これも簡単で,学習オプションの CheckpointPath プロパティにチェックぽいのとファイルを保存するフォルダ名をセットすれば,各エポック毎にチェックポイントを.mat ファイルに保存してくれます.
options = trainingOptions('sgdm','MaxEpochs',100,'MiniBatchSize',nTrain, ...
"ExecutionEnvironment","cpu", ...
"Plots","training-progress", ...
"ValidationData",{XpValid,YValid}, ...
"ValidationFrequency",10, ...
"CheckpointPath",pwd);
net1 = trainNetwork(XpTrain,YTrain,layers,options);
|===================================================================================|
| エポック | 反復 | 経過時間 | ミニバッチの精度 | 検証精度 | ミニバッチ損失 | 検証損失 | 基本学習率 |
| | | (hh:mm:ss) | | | | | |
|===================================================================================|
| 1 | 1 | 00:00:00 | 23.66% | 25.64% | 1.4035 | 1.4007 | 0.0100 |
| 10 | 10 | 00:00:01 | 44.42% | 46.70% | 1.3350 | 1.3197 | 0.0100 |
| 20 | 20 | 00:00:01 | 57.16% | 58.17% | 1.2234 | 1.2044 | 0.0100 |
| 30 | 30 | 00:00:01 | 65.17% | 65.14% | 1.1419 | 1.1217 | 0.0100 |
| 40 | 40 | 00:00:02 | 70.10% | 70.61% | 1.0889 | 1.0680 | 0.0100 |
| 50 | 50 | 00:00:02 | 73.09% | 72.64% | 1.0517 | 1.0304 | 0.0100 |
| 60 | 60 | 00:00:03 | 74.57% | 74.36% | 1.0224 | 1.0010 | 0.0100 |
| 70 | 70 | 00:00:03 | 75.48% | 75.86% | 0.9974 | 0.9763 | 0.0100 |
| 80 | 80 | 00:00:04 | 75.92% | 76.54% | 0.9752 | 0.9546 | 0.0100 |
| 90 | 90 | 00:00:04 | 76.26% | 76.76% | 0.9551 | 0.9351 | 0.0100 |
| 100 | 100 | 00:00:05 | 76.54% | 77.29% | 0.9367 | 0.9174 | 0.0100 |
|===================================================================================|
問題76のコードを改変し,事例ごとに損失・勾配を計算し,行列
の値を更新せよ(ミニバッチ化).
の値を
と変化させながら,1エポックの学習に要する時間を比較せよ.
ミニバッチのサイズを振ってループを回せば良いのですね.
miniBatchSize = [64 128 256 512];
elapsedTime = zeros(size(miniBatchSize));
net2 = cell(size(miniBatchSize));
options = trainingOptions('sgdm','MaxEpochs',100, ...
"ExecutionEnvironment","cpu", ...
"Plots","training-progress", ...
"ValidationData",{XpValid,YValid}, ...
"Verbose",false);
for kk = 1:length(miniBatchSize)
options.MiniBatchSize = miniBatchSize(kk);
options.ValidationFrequency = floor(nTrain/miniBatchSize(kk));
tic
net2{kk} = trainNetwork(XpTrain,YTrain,layers,options);
elapsedTime(kk) = toc;
end
問題77のコードを改変し,GPU上で学習を実行せよ.
これも学習オプションのプロパティ値を変更すれば良いだけです."ExecutionEnvironment"の値を"gpu"に変えるだけ.
options = trainingOptions('sgdm','MaxEpochs',100, ...
"MiniBatchSize",256, ...
"ExecutionEnvironment","gpu", ...
"Plots","training-progress", ...
"ValidationData",{XpValid,YValid}, ...
"ValidationFrequency",20, ...
"Verbose",false);
net3 = trainNetwork(XpTrain,YTrain,layers,options);
問題78のコードを改変し,バイアス項の導入や多層化など,ニューラルネットワークの形状を変更しながら,高性能なカテゴリ分類器を構築せよ.
間に適当に2層入れて順々に絞ってゆくような作りにしてみます(単なる思いつき).
layers = [ ...
imageInputLayer([inputSize 1 1],"Name","Input"); ... % 入力層
fullyConnectedLayer(128,"Name","FC1"); ...
tanhLayer; ...
fullyConnectedLayer(32,"Name","FC2"); ...
reluLayer; ...
fullyConnectedLayer(8,"Name","FC3"); ...
reluLayer; ...
fullyConnectedLayer(numClasses,"Name","FC4"); ...
softmaxLayer; ... % ソフトマックス層
classificationLayer ... % 分類出力層
];
MiniBatchSize = 500;
MaxEpochs = floor(MiniBatchSize/4);
options = trainingOptions('sgdm','MaxEpochs',MaxEpochs, ...
"MiniBatchSize", MiniBatchSize, ...
"ExecutionEnvironment","gpu", ...
"Plots","training-progress", ...
"ValidationData",{XpValid,YValid}, ...
"ValidationFrequency",floor(nTrain/MiniBatchSize), ...
"Verbose",false, ...
"Shuffle","every-epoch", ...
"LearnRateSchedule","piecewise", ...
"LearnRateDropPeriod",10, ...
"LearnRateDropFactor",0.8, ...
"InitialLearnRate",0.02, ...
"L2Regularization",0.005, ...
"Momentum",0.95);
net4 = trainNetwork(XpTrain,YTrain,layers,options);
predTest = classify(net4,XpTest);
okRatioTestNew = nnz(predTest == YTest)/numel(predTest)
okRatioTestNew = 0.8951
他にもいろいろやってみましたが,検証精度で90%程度が限界っぽいです.







