PyTorch入門③:Logistic Regression
Logistic Regression
今回はLogistic Regression。
下記のチュートリアルを参考に実装した。
github.com
尚、コード全体はここに置いた。
github.com
データセットだが、チュートリアルはMNISTを使っているが、今回はsklearnのdigits datasetを使った。
(ロードが手っ取り早かったので。。)
import
必要なライブラリをインポート
import numpy as np import torch import torch.nn as nn import torch.optim as optim from torch.autograd import Variable from sklearn.datasets import load_digits from sklearn.model_selection import train_test_split
ネットワークの定義
ネットワーク自体は線形モデル。
下記のモデルはただの線形変換。
class LogisticNet(torch.nn.Module): def __init__(self, D_in, D_out): super(LogisticNet, self).__init__() self.linear = nn.Linear(D_in, D_out) def forward(self, x): lin = self.linear(x) return lin
損失関数nn.CrossEntropyLoss()
の中で、softmax
関数が含まれているので、モデル自体は線形変換でも構わないみたい。
これは、下記のようにsoftmax
関数を通して、損失関数nn.NLLLoss()
で評価するのと同じ。
class LogisticNet(torch.nn.Module): def __init__(self, D_in, D_out): super(LogisticNet, self).__init__() self.linear = nn.Linear(D_in, D_out) def forward(self, x): lin = self.linear(x) # return lin return F.log_softmax(lin)
train
訓練データを使って下記の処理を順番に実行。
- 訓練データをテンソル変数で定義
- 勾配初期化
- 訓練データの出力値計算
- 出力(予測値)と正解データの損失値の計算
- 損失値に基づく勾配計算
- 最適化関数によるパラメータ更新
def train(model, loss_func, optimizer, trX, trY): x = Variable(trX, requires_grad=False) y = Variable(trY, requires_grad=False) optimizer.zero_grad() y_pred = model(x) loss = loss_func(y_pred, y) loss.backward() optimizer.step() return loss.data[0]
valid
評価データを使って下記の処理を順番に実行。
- 評価データをテンソル変数で定義
- 評価データの出力値計算
- 出力値の最大値のインデックス取得
- 出力(予測値)と正解データの一致数を計算
- 一致率(正解数/評価データ数)の計算
def valid(model, loss_func, valX, valY): x = Variable(valX, requires_grad=False) y = Variable(valY, requires_grad=False) outputs = model(x) val_loss = loss_func(outputs, y) _, predY = torch.max(outputs.data, 1) correct = (predY == y.data).sum() val_acc = float(correct) / y.size(0) return val_loss.data[0], val_acc
torch.max
について
モデルの出力はデータ数×クラス数の行列。
一方、正解データはデータ数次元のベクトル。
このままでは比べられないので、torch.max
で出力を変換する。
torch.max
は、行列(ベクトル)の最大値とそのインデックスを返す。
下記の行列で確認する。
0.2 | 1.4 | 1.7 |
0.3 | 1.5 | 0.7 |
1.1 | 1.3 | 0.4 |
0.9 | 2.3 | 0.5 |
testX = np.array([[0.2, 1.4, 1.7],[0.3, 1.5, 0.7],[1.1, 1.3, 0.4],[0.9, 2.3, 0.5]]) testY = np.array([[2, 1, 0, 1]]) x = Variable(torch.from_numpy(testX).float(), requires_grad=False) y = Variable(torch.from_numpy(testY.astype(np.int64)), requires_grad=False)
value, index = torch.max(x.data, 0) # row
>> value 1.1000 2.3000 1.7000 [torch.FloatTensor of size 3] >> index 2 3 0 [torch.LongTensor of size 3]
value, index = torch.max(x.data, 1) # colomn
>> value 1.7000 1.5000 1.3000 2.3000 [torch.FloatTensor of size 4] >> index 2 1 1 1 [torch.LongTensor of size 4]
列方向で最大値を取ったインデックスのベクトルがモデルのラベル予測値に相当する。
これにより、予測時にソフトマックス関数を通す必要がなくなる。
main
データのロードと分割
ここは、sklearn
のライブラリを利用する。
digits = load_digits() data = digits['data'] target = digits['target'] # separate data trX, teX, trY, teY = train_test_split(data, target, test_size=0.2, random_state=0)
学習の実行
モデルや損失関数などを定義して、繰り返し処理を回す。
n_samples = trX.shape[0] input_dim = trX.shape[1] n_classes = 10 model = LogisticNet(input_dim, n_classes) optimizer = optim.SGD(model.parameters(), lr=0.0001, momentum=0.9) loss_func = nn.CrossEntropyLoss() trX = torch.from_numpy(trX).float() teX = torch.from_numpy(teX).float() trY = torch.from_numpy(trY.astype(np.int64)) teY = torch.from_numpy(teY.astype(np.int64)) N_EPOCHS = 200 for epoch in range(N_EPOCHS): loss = train(model, loss_func, optimizer, trX, trY) val_loss, val_acc = valid(model, loss_func, teX, teY) print 'val loss:%.3f val acc:%.3f' % (val_loss, val_acc)
Kerasで中間層の出力結果を得る
中間層の出力結果を得たい場合の方法。
FAQに書いてあることをまとめただけ。
FAQ - Keras Documentation
やり方は2つある。
①新しいモデルの作成
シンプルな方法は,着目しているレイヤーの出力を行うための新しい Model を作成する
# build model from keras.models import Model model = ... # create the original model model.add(Dense(128, name='out1')) model.add(Dense(num_classes, activation='softmax', name='out2')) layer_name = 'out2' intermediate_layer_model = Model(inputs=model.input, outputs=model.get_layer(layer_name).output) intermediate_output = intermediate_layer_model.predict(x_test) print intermediate_output[0]
②Keras functionの定義
ある入力が与えられたときにに,あるレイヤーの出力を返すKeras functionを以下のように記述する
from keras import backend as K # with a Sequential model get_3rd_layer_output = K.function([model.layers[0].input], [model.layers[3].output]) layer_output = get_3rd_layer_output([x])[0]
※Dropout
など学習時とテスト時でモデルの振る舞いが異なる場合は、learning phase
フラグを利用
get_3rd_layer_output = K.function([model.layers[0].input, K.learning_phase()], [model.layers[3].output]) # output in test mode = 0 layer_output = get_3rd_layer_output([x, 0])[0] # output in train mode = 1 layer_output = get_3rd_layer_output([x, 1])[0]
PyTorch入門②:Neural Network
ネットワークはtorch.nn
パッケージを使用して構築する。
今回は下記にあるサンプルコードを使う。
Learning PyTorch with Examples — PyTorch Tutorials 0.3.0.post4 documentation
ネットワークは「入力層ー中間層ー出力層」の三層構造で線型回帰問題を想定。
import torch from torch.autograd import Variable # N is batch size; D_in is input dimension; # H is hidden dimension; D_out is output dimension. N, D_in, H, D_out = 64, 1000, 100, 10 x = Variable(torch.randn(N, D_in)) y = Variable(torch.randn(N, D_out), requires_grad=False) model = torch.nn.Sequential( torch.nn.Linear(D_in, H), torch.nn.ReLU(), torch.nn.Linear(H, D_out), )
ネットワークがシーケンシャルな構造の場合、nn.Sequential
モジュールが使える。
KerasのSequentialと同じような使い方だと思われる。
シーケンス処理以外のネットワークや独自でクラスを定義したい場合、以下のようにしてクラスをつくる。
# linear regression model class MLP(torch.nn.Module): def __init__(self, D_in, H, D_out): super(MLP, self).__init__() self.linear1 = nn.Linear(D_in, H) self.relu = nn.ReLU() self.linear2 = nn.Linear(H, D_out) def forward(self, x): out = self.linear1(x) out = self.relu(out) out = self.linear2(out) return out
呼ぶときはこのように呼ぶ。
model = MLP(D_in, H, D_out)
forward関数は、modelにデータを与えた際に呼ばれる。
また、モデル内のLinearモジュールで線形関数を使って出力を計算してくれる。
※Linearモジュールについて
の線形変換を行ってくれる。
torch.nn — PyTorch master documentation
class torch.nn.Linear(in_features, out_features, bias=True)
(デフォルトでバイアス付)
例)以下のように線形変換を行う。
import torch import torch.nn as nn from torch.autograd import Variable m = nn.Linear(20, 30) input = Variable(torch.randn(128, 20)) output = m(input) print(output.size()) # torch.Size([128, 30])
内積をとる行列のサイズが不正な場合、
m = nn.Linear(20, 30) input = Variable(torch.randn(128, 10)) output = m(input) # RuntimeError: size mismatch, m1: [128 x 10], m2: [20 x 30] at
のようなエラーを出力する。
Kerasライクにも書ける。
layer = [] layer.append(torch.nn.Linear(D_in, H)) layer.append(torch.nn.ReLU()) layer.append(torch.nn.Linear(H, D_out)) model = nn.Sequential(*layer)
次に損失関数の定義。
loss_fn = torch.nn.MSELoss(size_average=False)
パラメータ最適化
learning_rate = 1e-4 for t in range(500): # Forward pass: compute predicted y by passing x to the model. y_pred = model(x) # Compute and print loss. loss = loss_fn(y_pred, y) print(t, loss.data[0]) # Zero the gradients before running the backward pass. model.zero_grad() # Backward pass: compute gradient of the loss with respect to all the learnable # parameters of the model. loss.backward() # Update the weights using gradient descent. Each parameter is a Variable, so # we can access its data and gradients like we did before. for param in model.parameters(): param.data -= learning_rate * param.grad.data
zero_grad()
は「勾配の初期化」のような意味合いかと思われる。
torch.optim
を使う場合、このように書く。
learning_rate = 1e-4 optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate) for t in range(500): # Forward pass: compute predicted y by passing x to the model. y_pred = model(x) # Compute and print loss. loss = loss_fn(y_pred, y) print(t, loss.data[0]) # Zero the gradients before running the backward pass. model.zero_grad() # Backward pass: compute gradient of the loss with respect to all the learnable # parameters of the model. loss.backward() # Calling the step function on an Optimizer makes an update to its # parameters optimizer.step()
PyTorch入門①:Tensors~Autograd
Tensors
Numpyのndarrays(多次元配列)のようなもの。
GPUの高速計算のために使われるオブジェクトとのこと。
以降のサンプルはチュートリアルのもの。
What is PyTorch? — PyTorch Tutorials 0.3.0.post4 documentation
作り方
1)初期化を行わず作成
from __future__ import print_function import torch x = torch.Tensor(5, 3) print(x)
2.3942e-37 0.0000e+00 -2.6457e+08
4.5848e-41 -3.8321e+08 4.5848e-41
1.0433e-37 0.0000e+00 1.0433e-37
0.0000e+00 -7.6205e+15 4.5848e-41
-7.5137e+15 4.5848e-41 -2.2547e+05
[torch.FloatTensor of size 5x3]
2)ランダム値で初期化
x = torch.rand(5, 3) print(x)
0.2182 0.7903 0.7750
0.4161 0.2828 0.3396
0.9726 0.6395 0.3299
0.3787 0.3909 0.6220
0.5164 0.6238 0.5192
[torch.FloatTensor of size 5x3]
サイズの取得
print(x.size()) # torch.Size([5, 3])
足し算
y = torch.rand(5, 3) print(x + y)
このような書き方でも良い。
result = torch.Tensor(5, 3) torch.add(x, y, out=result) # adds x to y y.add_(x)
リサイズ
reshape
ではないので注意が必要。
x = torch.randn(4, 4) y = x.view(16) z = x.view(-1, 8) # the size -1 is inferred from other dimensions print(x.size()) # torch.Size([4, 4]) print(y.size()) # torch.Size([16]) print(z.size()) # torch.Size([2, 8])
Numpyとの橋渡し
1)Torch Tensor → NumPy Array
a = torch.ones(5) b = a.numpy() print(b) # [ 1. 1. 1. 1. 1.] a.add_(1) print(b) # [ 2. 2. 2. 2. 2.]
*Tensorで値を変えるとNumpyのArrayの値も変わる。
2)NumPy Array → Torch Tensor
import numpy as np a = np.ones(5) b = torch.from_numpy(a) np.add(a, 1, out=a)
*Numpyで値を変えるとのTensorの値も変わる。
Variable
Tensorをラップしたクラス。
このようなクラス構造。
x = Variable(torch.ones(2, 2), requires_grad=True) print(x)
Variable containing:
1 1
1 1
[torch.FloatTensor of size 2x2]
Autograd
自動微分機能のこと。
Autograd: automatic differentiation — PyTorch Tutorials 0.3.0.post4 documentation
Theanoよりシンプル。
import torch from torch.autograd import Variable x = Variable(torch.Tensor([2]), requires_grad = True) w = Variable(torch.Tensor([1]), requires_grad = True) y = w*x**2 y.backward() print(x.grad) print(w.grad)
Variable containing:
4
[torch.FloatTensor of size 1]Variable containing:
1
[torch.FloatTensor of size 1]
MNISTの読み込み&描画
調べたら描画方法が色々あり、少し迷った。
今後、無駄な時間を省くための備忘録。
ダウンロード
from six.moves import urllib origin = ( 'http://www.iro.umontreal.ca/~lisa/deep/data/mnist/mnist.pkl.gz' ) urllib.request.urlretrieve(origin,'mnist.pkl.gz')
読み込み
import numpy as np import gzip import cPickle as pickle import matplotlib.pyplot as plt with gzip.open('mnist.pkl.gz', 'rb') as f: train_set, valid_set, test_set = pickle.load(f) train_set_x, train_set_y = train_set valid_set_x, valid_set_y = valid_set test_set_x, test_set_y = valid_set print "train_set_x:", train_set_x.shape print "train_set_y:", train_set_y.shape print "valid_set_x:", valid_set_x.shape print "valid_set_y:", valid_set_y.shape print "test_set_x:", test_set_x.shape print "test_set_y:", test_set_y.shape print "shape:", train_set_x[0].shape # train_set_x: (50000, 784) # train_set_y: (50000,) # valid_set_x: (10000, 784) # valid_set_y: (10000,) # test_set_x: (10000, 784) # test_set_y: (10000,) # shape: (784,)
描画
pos = 1 for i in range(100): plt.subplot(10, 10, pos) plt.subplots_adjust(wspace=0, hspace=0) plt.imshow(train_set_x[i].reshape(28, 28)) plt.gray() plt.axis('off') pos += 1 plt.show()
こんな感じで表示される。
MeCabのコスト計算を理解する。
はじめに
久しぶりにMeCabをいじる必要があったのだが、形態素解析のコスト計算らへんで理解できてなかったところがあったのでまとめる。
ある単語を形態素解析する場合、その候補が複数ある場合にどう考えるか。例えば、
日本テレビ東京
辞書に「日本テレビ東京」という単語が存在しない場合、どのようにこの単語を構成すべきか。候補としては、
が考えられる。(各単語は辞書に存在するとする)
MeCabでは「各候補のコストを計算し、パスの総和が最小コストとなる候補」を選択する。
上記の例の場合にどのように候補を決定するかは下記参照。
日本テレビ東京で学ぶMeCabのコスト計算 | mwSoft
この例では「どのように分かち書きするのが最適か」を考えた。ここでは、「複数の意味を持つ単語が使用された場合、どの意味が最適なのか」について考える。
今回は、「位」という単語について考える。
コトバンクによると、この単語は
- [接尾]助数詞。
- 物事の順位・等級・位階などを表す。「第三位」「従五
- [名]くらい。位階。
- 「一品以下。初位(そゐ)以上を―と曰ふ」
の2つの意味をもつとされる。
どっちの意味が最適化はその文脈によって決定される。
コスト計算について
以下の2通りを考える。
- 「位を継承」
- 「三位」
$ echo 位を継承|mecab 位 名詞,一般,*,*,*,*,位,クライ,クライ を 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ 継承 名詞,サ変接続,*,*,*,*,継承,ケイショウ,ケイショー
一方、2.は
$ echo 三位|mecab 三 名詞,数,*,*,*,*,三,サン,サン 位 名詞,接尾,助数詞,*,*,*,位,イ,イ
それぞれ正しい意味となっている。
では、それぞれの意味はどのように決まったのか。
キーワードは「生起コストと連結コスト」である。
文章中に出現する単語自体のコスト(生起コスト)とその単語の品詞と次の単語の品詞の遷移コスト(連結コスト)の総和が最小となるパスをもつ候補を選択する。
この考えはCRFの分類と同じである(MeCabはCRFにより学習されているため)。
CRFに関しては下記参照
kento1109.hatenablog.com
文脈ID
先ほどは単語自体のコストと述べたが、これは正確ではない。
「位 」の場合、意味が異なるのに全てが同じコストではどっちが最適か判断できない。
辞書に「位」のエントリは合計5つ存在する。
ファイル名 | 表層形 | 左文脈ID | 右文脈ID | コスト | 品詞 | 品詞細分類1 | 品詞細分類2 | 品詞細分類3 | 活用形 | 活用型 | 原形 | 読み | 発音 |
Noun.adverbal.csv | 位 | 1314 | 1314 | 5941 | 名詞 | 副詞可能 | * | * | * | * | 位 | クライ | クライ |
Noun.csv | 位 | 1285 | 1285 | 6572 | 名詞 | 一般 | * | * | * | * | 位 | クライ | クライ |
Noun.csv | 位 | 1285 | 1285 | 7643 | 名詞 | 一般 | * | * | * | * | 位 | イ | イ |
Suffix.csv | 位 | 1300 | 1300 | 9198 | 名詞 | 接尾 | 助数詞 | * | * | * | 位 | イ | イ |
Suffix.csv | 位 | 1298 | 1298 | 8893 | 名詞 | 接尾 | 一般 | * | * | * | 位 | イ | イ |
この「文脈ID」がどの「位」の意味が文脈的に最も自然か(コストが小さいか)を決定するうえで重要になる。
文脈IDは下記ファイルで定義されている。
- left-id.def
- right-id.def
1.「位を継承」のコスト計算
コストは以下のコマンドで確認できる。
$ echo 位を継承|mecab -F"%m,%phl,%phr,%pb,%pw,%pc,%pn\n" -N2 位,1285,1285,*,6572,6289,6289 を,156,156,*,4183,5479,-810 継承,1283,1283,*,4452,8789,3310 EOS
まず、「位」の生起コストは、辞書データファイルより、「6572」と分かる。次に「位」の左文脈ID(先頭なので左文脈は0)、右文脈IDより連結コストを「matrix.def」より確認すると、
左文脈ID | 右文脈ID | コスト |
0 | 1285 | -283 |
というエントリがあることが分かる。
なので、コスト合計は
6572-283=6289
ちなみに、生起コストを見ると先頭のエントリ(Noun.adverbal.csv)の方が低い(5941)ことが分かる。
また、この素性IDの連結コストは
左文脈ID | 右文脈ID | コスト |
0 | 1314 | -316 |
なので、コスト合計は
5941-316=5625
となり、この時点では「名詞(副詞可能)」の意味の方がコストは小さい。絵にするとこんな感じ。
続けてコストを見ていく。次の「を」(文脈ID:156)の生成コストを調べると「4183」とうことが分かる。
文脈ID:1285からの連結コストは、
左文脈ID | 右文脈ID | コスト |
1285 | 156 | -4993 |
である。この時点の総和は
6289 -4993 + 4183 = 5479
一方、ID:1314からの連結コストは
左文脈ID | 右文脈ID | コスト |
1314 | 156 | -3144 |
である。この時点の総和は
5625 -3144 + 4183 =6664
この時点でID:1285のコストの方が小さくなった。
続きを絵にするとこうなる。
この時点で、ID:1285の方がコストが小さいので、「位」に関してはこっちが最適なパスとなる。
2.「三位」のコスト計算
$ echo 三位|mecab -F"%m,%phl,%phr,%pb,%pw,%pc,%pn\n" -N3 三,1295,1295,*,2725,3295,3295 位,1300,1300,*,9198,1762,-1533 EOS 三位,1285,1285, ,7054,6771,6771 EOS 三,1295,1295,*,2725,3295,3295 位,1314,1314, ,5941,8071,4776 EOS
生成コストだけをみると、文脈ID:1314が最も小さい。
しかし、ID:1295からの連結コストは以下の通り。
左文脈ID | 右文脈ID | コスト |
1295 | 1300 | 9198 |
1295 | 1314 | 5941 |
ID:1300の連結コストの方がはるかに小さい。
(数を指す名詞が直前にくる場合、「位」を接尾とする方が自然と考える)
コストの総和は、
文脈ID | 生起コスト | 連結コスト | コスト総和 |
1300 | 9198 | -10731 | -1533 |
1314 | 5941 | -1165 | 4776 |
なので、ID:1300が最適なパスとなる。
Snorkelの識別モデルについて(実装編)
はじめに
前回は生成モデルの構築について確認した。
kento1109.hatenablog.com
尚、識別モデルに関する理論的なことをはこっちにまとめた。
(大したこと書いてないが・・)
Snorkelの識別モデルについて(理論編) - 機械学習・自然言語処理の勉強メモ
生成モデルにデータを入れることで、確率値を出力された。チュートリアルの例で言うと、配偶者の関係の候補となる人物名のペアを入力とすることで、その人物名同士が配偶者である確率が出力された。
https://hazyresearch.github.io/snorkel/pdfs/snorkel_demo.pdfより引用
識別モデルではこの確率値を正解ラベルとして使用し、訓練を行う。
Weak Supervisionより引用
つまり、訓練データから0~1の連続値を出力するモデルを構築することを意味する。なので、使用するモデルはロジスティック回帰・SVMs・LSTMなど自由。
なぜ、最終的な予測に識別モデルを使うのか。生成モデルの結果ではダメなのか。識別モデルを用いる理由については、
The discriminative model learns a feature representation of our LFs.
This makes it better able to generalize to unseen candidates.
snorkel/Snorkel-Workshop-FINAL.pdf at master · HazyResearch/snorkel · GitHub
と説明されている。
要は「汎化性能を向上させるための表現学習」として必要だということである。
コードを読んでいく
今回のチュートリアルは以下にある。
github.com
Training a LSTM Neural Network
train_cands
:訓練データ(候補集合)train_marginals
:正解ラベル(生成モデルから出力された確率値)
from snorkel.learning.disc_models.rnn import reRNN train_kwargs = { 'lr': 0.001, 'dim': 100, 'n_epochs': 10, 'dropout': 0.25, 'print_freq': 1, 'batch_size': 128, 'max_sentence_length': 100 } lstm = reRNN(seed=1701, n_threads=1) lstm.train(train_cands, train_marginals, X_dev=dev_cands, Y_dev=L_gold_dev, **train_kwargs)
今回は真の正解ラベル付きデータ(**_dev)があるので、精度も評価できる。
p, r, f1 = lstm.score(test_cands, L_gold_test) print("Prec: {0:.3f}, Recall: {1:.3f}, F1 Score: {2:.3f}".format(p, r, f1)) tp, fp, tn, fn = lstm.error_analysis(session, test_cands, L_gold_test)