ITANDI TECH BLOG

イタンジのスタッフブログです。イベントや技術情報などを発信しています。

Deep LearningをやるならTensorflowよりもPyTorch!

Screenshot 2017-03-08 11.53.00

こんにちは、エンジニアの建三です。

Deep Learningのライブラリと言えばTensorflowが有名ですよね。1年半前にリリースされて以来、一瞬にして知名度を手にしました。僕はその頃Deep Learningを勉強していたので、Hacker NewsでTensorflowがバズってるのを見て何となく僕も興奮していたのを覚えています。しかし早速使ってみようと思いチュートリアルを進めたものの、LSTMやCNNの作り方が分からず断念しました。

僕はStanfordのコースDeep Learningを勉強したんですが(無料でクオリティ超高いです)インストラクターのAndrej KarpathyはTensorflowよりもTorchを勧めていました。ホームワークはnumpyを使うんですが、アーキテクチャが正にTorchのPython版という感じで、すごくしっくりくるんですよね。それまで色んなリソースを使ってDeep Learningを勉強して中々理解出来なかったのがやっと理解出来たんです。

しかしTorchの言語はLuaで、Python大好きの僕にはちょっと辛かったのでKerasを使うことにしました。KerasはTorchに影響されてるので、レイヤーを重ねていくというところがすごく似ています。でもTorchよりももっと簡単に書けるのでscikit-learnのDeep Learning版といった感じです。イタンジでもAIチャットやクローラーにKerasを使っています。

しかし最近やっと強化学習を勉強し始め、Kerasよりもっとフレキシブルなライブラリを使った方がいいなと思い他のライブラリを探し始めました。すると何とTorchのPython版が出てるじゃありませんか!

その名もPyTorch(そのまんま)。

PyTorchを賞賛する声

TensorflowよりもPyTorchを好むのは僕だけではありません。

Redditでは、Kaggleの優勝者のJeremy HowardがPyTorchの方が使い易いと言っています。

Hacker NewsではSalesforceのエンジニアがChainerからPyTorchに移行する予定と言っています。

そして僕にTorchの素晴らしさを教えてくれたAndrej KarpathyはTwitterでもはやTensorflowは古いという爆弾発言...

DeepMindはTorchからTensorflowに移行しましたが、もうPyTorchを使ってるだろうと予想しています。facebookもTorchを使ってる代表的な会社の一つで、PyTorchの開発にがっつり携わっています。

Angular(Google)からReact(facebook)の流れを思い出します。一時期AngularはJS frameworkの頂点に立ち誰もAngularを超えられないと思いきや、Reactがあっさりと超えてしまいましたよね。

JSにしろDeep Learningにしろ僕はfacebookの作るものの方が好きみたいです...

Tensorflowを使う一番のメリットは、研究と実装で同じライブラリを使えるというところにあります。Deep Learningの学び易さを重視するならPyTorchの方が良いでしょう。numpyからtensorへのコンバートが出来るのですごく楽です。

PyTorchでGRUを作ろう!

単純なGRUのClassifierを作りました。 まだチュートリアルが全然ないので、こんな簡単なモデルですらかなり苦労しました... 何度も諦め「Kerasでいいや...」と思ったんですが、踏ん張った甲斐がありました。

完成版のGistはこちらにあります。

Pytorchではモデルをクラスで定義します。initで必要なレイヤーをinitializeします。この場合だとnn.GRUnn.Linearの2つです。 そしてforwardでどうそれらのレイヤーを繋げるかを書きます。Input -> GRU -> Linearというシンプルな構造です。

import torch
import torch.nn as nn
from torch.autograd import Variable

class GRU(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(GRU, self).__init__()

        self.hidden_size = hidden_size

        self.gru = nn.GRU(input_size, hidden_size)
        self.linear = nn.Linear(hidden_size, output_size)

    def forward(self, input, hidden):
        _, hn = self.gru(input, hidden)
        ## from (1, N, hidden) to (N, hidden)
        rearranged = hn.view(hn.size()[1], hn.size(2))
        out1 = self.linear(rearranged)
        return out1

    def initHidden(self, N):
        return Variable(torch.randn(1, N, self.hidden_size))

PyTorchはKerasやscikit-learnのようにmodel.fit(X,y)で勝手に学習してくれるわけではないので、ボイラープレートを書く必要があります。

import numpy as np
np.random.seed(1337)

def batch(tensor, batch_size):
    tensor_list = []
    length = tensor.shape[0]
    i = 0
    while True:
        if (i+1) * batch_size >= length:
            tensor_list.append(tensor[i * batch_size: length])
            return tensor_list
        tensor_list.append(tensor[i * batch_size: (i+1) * batch_size])
        i += 1

class Estimator(object):

    def __init__(self, model):
        self.model = model

    def compile(self, optimizer, loss):
        self.optimizer = optimizer
        self.loss_f = loss

    def _fit(self, X_list, y_list):
        """
        train one epoch
        """
        loss_list = []
        acc_list = []
        for X, y in zip(X_list, y_list):
            X_v = Variable(torch.from_numpy(np.swapaxes(X,0,1)).float())
            y_v = Variable(torch.from_numpy(y).long(), requires_grad=False)

            self.optimizer.zero_grad()
            y_pred = self.model(X_v, self.model.initHidden(X_v.size()[1]))
            loss = self.loss_f(y_pred, y_v)
            loss.backward()
            self.optimizer.step()

            ## for log
            loss_list.append(loss.data[0])
            classes = torch.topk(y_pred, 1)[1].data.numpy().flatten()
            acc = self._accuracy(classes, y)
            acc_list.append(acc)

        return sum(loss_list) / len(loss_list), sum(acc_list) / len(acc_list)

    def fit(self, X, y, batch_size=32, nb_epoch=10, validation_data=()):
        X_list = batch(X, batch_size)
        y_list = batch(y, batch_size)

        for t in range(1, nb_epoch + 1):
            loss, acc = self._fit(X_list, y_list)
            val_log = ''
            if validation_data:
                val_loss, val_acc = self.evaluate(validation_data[0], validation_data[1], batch_size)
                val_log = "- val_loss: %06.4f - val_acc: %06.4f" % (val_loss, val_acc)
            print("Epoch %s/%s loss: %06.4f - acc: %06.4f %s" % (t, nb_epoch, loss, acc, val_log))

    def evaluate(self, X, y, batch_size=32):
        y_pred = self.predict(X)

        y_v = Variable(torch.from_numpy(y).long(), requires_grad=False)
        loss = self.loss_f(y_pred, y_v)

        classes = torch.topk(y_pred, 1)[1].data.numpy().flatten()
        acc = self._accuracy(classes, y)
        return loss.data[0], acc

    def _accuracy(self, y_pred, y):
        return sum(y_pred == y) / y.shape[0]

    def predict(self, X):
        X = Variable(torch.from_numpy(np.swapaxes(X,0,1)).float())      
        y_pred = self.model(X, self.model.initHidden(X.size()[1]))
        return y_pred       

    def predict_classes(self, X):
        return torch.topk(self.predict(X), 1)[1].data.numpy().flatten()


これでKerasと同じように使えます。イタンジでは住所、物件名、最寄り駅など7種類のテキストを判別するClassifierにこのモデルを使っています。データだけダミーに置き換えました。

from sklearn.model_selection import train_test_split

MAX_LEN = 30
EMBEDDING_SIZE = 64
BATCH_SIZE = 32
EPOCH = 40
DATA_SIZE = 1000
INPUT_SIZE = 300

def main():
    class_size = 7

    ## Fake data
    X = np.random.randn(DATA_SIZE * class_size, MAX_LEN, INPUT_SIZE)
    y = np.array([i for i in range(class_size) for _ in range(DATA_SIZE)])

    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.2)

    model = GRU(INPUT_SIZE, EMBEDDING_SIZE, class_size)
    clf = Estimator(model)
    clf.compile(optimizer=torch.optim.Adam(model.parameters(), lr=1e-4),
                loss=nn.CrossEntropyLoss())
    clf.fit(X_train, y_train, batch_size=BATCH_SIZE, nb_epoch=EPOCH,
            validation_data=(X_test, y_test))
    score, acc = clf.evaluate(X_test, y_test)
    print('Test score:', score)
    print('Test accuracy:', acc)

    torch.save(model, 'model.pt')


if __name__ == '__main__':
    main()

こんな感じでKerasっぽいログが出ます。

Epoch 1/40 loss: 1.9836 - acc: 0.1379 - val_loss: 1.9776 - val_acc: 0.1557
Epoch 2/40 loss: 1.9634 - acc: 0.1550 - val_loss: 1.9748 - val_acc: 0.1521
Epoch 3/40 loss: 1.9454 - acc: 0.1711 - val_loss: 1.9727 - val_acc: 0.1579
Epoch 4/40 loss: 1.9285 - acc: 0.1904 - val_loss: 1.9712 - val_acc: 0.1579
Epoch 5/40 loss: 1.9121 - acc: 0.2061 - val_loss: 1.9701 - val_acc: 0.1614

PyTorchの不満

一つPyTorchの不満があるとすれば、Tnesorのタイプが間違っているが為にエラーになることが多々あり苦戦しました。 上記の_fitのところでX.float()でfloatにしy.long()にしています。何故かそうしないと「Typeが違うよ!」というエラーが出ます。何故そのタイプじゃなきゃいけないのか分からないし、それくらい自動でやってくれと思いました。 しかもエラーメッセージがすごく分かり辛く、どこが間違ってるか理解するまでに時間がかかりました。

Documentationでもあまり触れられてないので、ここは改善してほしいと思っています。

PyTorchの勉強の仕方

正直オフィシャルのチュートリアルは微妙です。PyTorchのリソースをまとめたRepoにもっとクオリティの高いチュートリアルがあるので、こっちがオススメです。

まとめ

Tensorflowや他のライブラリを使ってる人は是非PyTorchを一度試してみることをオススメします。僕もまだまだ学びたてですが、もっと多くの人にPyTorchの素晴らしさを知ってほしいと思っています!