機械学習・自然言語処理の勉強メモ

学んだことのメモやまとめ

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モジュールについて
y=Wx+bの線形変換を行ってくれる。
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

はじめに



流行りに乗っかってPyTorchを勉強する。
最近、PyTorchで実装する論文が急増しているらしく、とりあえずソースコードが読めるようになりたい。

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の値も変わる。

CUDA Tensors

GPU上でTensorを操作する場合、

if torch.cuda.is_available():
    x = x.cuda()
    y = y.cuda()
    x + y

Variable



Tensorをラップしたクラス。
このようなクラス構造。
f:id:kento1109:20180213152129p:plain

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()

こんな感じで表示される。
f:id:kento1109:20180207134558p:plain

MeCabのコスト計算を理解する。

はじめに



久しぶりにMeCabをいじる必要があったのだが、形態素解析のコスト計算らへんで理解できてなかったところがあったのでまとめる。

ある単語を形態素解析する場合、その候補が複数ある場合にどう考えるか。例えば、

日本テレビ東京

辞書に「日本テレビ東京」という単語が存在しない場合、どのようにこの単語を構成すべきか。候補としては、

が考えられる。(各単語は辞書に存在するとする)
MeCabでは「各候補のコストを計算し、パスの総和が最小コストとなる候補」を選択する。
上記の例の場合にどのように候補を決定するかは下記参照。
日本テレビ東京で学ぶMeCabのコスト計算 | mwSoft

この例では「どのように分かち書きするのが最適か」を考えた。ここでは、「複数の意味を持つ単語が使用された場合、どの意味が最適なのか」について考える。

今回は、「」という単語について考える。
コトバンクによると、この単語は

  • [接尾]助数詞。
    • 物事の順位・等級・位階などを表す。「第三位」「従五
  • [名]くらい。位階。
    • 「一品以下。初位(そゐ)以上を―と曰ふ」

の2つの意味をもつとされる。
どっちの意味が最適化はその文脈によって決定される。

コスト計算について



以下の2通りを考える。

  1. 「位を継承」
  2. 「三位」

1.をMeCab分かち書きすると、

$ 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
となり、この時点では「名詞(副詞可能)」の意味の方がコストは小さい。絵にするとこんな感じ。
f:id:kento1109:20180124101028p:plain:w250

続けてコストを見ていく。次の「を」(文脈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のコストの方が小さくなった。
続きを絵にするとこうなる。
f:id:kento1109:20180124101523p:plain
この時点で、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の識別モデルについて(理論編) - 機械学習・自然言語処理の勉強メモ

生成モデルにデータを入れることで、確率値を出力された。チュートリアルの例で言うと、配偶者の関係の候補となる人物名のペアを入力とすることで、その人物名同士が配偶者である確率が出力された。
f:id:kento1109:20180116143903p:plain
https://hazyresearch.github.io/snorkel/pdfs/snorkel_demo.pdfより引用

識別モデルではこの確率値を正解ラベルとして使用し、訓練を行う。
f:id:kento1109:20180110145722p:plain
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)