cha_kabuのNotebooks

個人的な機械学習関連勉強のアウトプット置き場です。素人の勉強録なので、こちらに辿り着いた稀有な方、情報はあまり信じない方が身のためです。

ゼロから作るDeep Learning3 フレームワーク編を読む その⑩ステップ49~51

はじめに

こちらの記事シリーズの続きです。

cha-kabu.hatenablog.com

本編

ステップ49 Datasetクラスと前処理

他のライブラリはあまり詳しくありませんが、PytorchではおなじみのDatasetクラスを定義します。カスタムデータセットを作成するときに継承するやつですね。書籍では(今のところ)datasetsモジュールに用意されたデータしか用いないのでコンペ等で使う時と少しお作法が異なりますが、あまり意識せずに写経しながら利用していた__getitem__()__len__()の二つの意味がやっとわかりました。前のステップとの変化で見てみます。

# ステップ48
x,t = dezero.datasets.get_spiral(train=True)
print(x[0],t[0]) # [-0.13981389 -0.00721657] 1
print(len(x)) # 300

# ステップ49
train_set = dezero.datasets.Spiral(train=True)
print(train_set[0]) # (array([-0.13981389, -0.00721657], dtype=float32), 1)
print(len(train_set)) # 300

以下の様な違いが分かります。

命令 ステップ48 ステップ49
教師データとラベルデータの取得 別々(x,t) セット(train_set)
インデックスを指定したとき 各データを取得 or
各データをタプルにまとめて取得
各データの組をタプルで取得
※クラス次第で、100%ではない
len()を使用したとき 各データの長さを返す 設定により、self.dataの長さを返す

__getitem__()はDataset(を継承した)インスタンスをndarrayみたいにインデックス指定で中身のデータを覗けるように指定し、__len__()も似たように長さの概念がないインスタンスでも普通のデータの様に長さを返せるようにlen関数が使われたときはlen(train_set.data)を返す様にクラスを定義します。

…とまぁ違いは分かったのですが、書籍で説明がある、

Datasetクラスを使う利点は、別のデータセットで学習を行う時に実感できます。

についてはこれまでコピペ&写経でしか使ってこなかったからか余り実感がないですね。。コードの短さ的にも(Dataloaderやtransformなどを無視すると)直接取得したほうが手っ取り早い気がしますし、例として提示されているBigDataクラスでは各データをタプルで返さずに別個で返していてそこは統一する必要はなさそうだったり、いまいちどこが肝なのかがまだ分かりません…とりあえず中身については分かったので先に進みます。


それともう一つ未だに分かっていないのがDataset(を継承した)クラスをインスタンス化した際、インスタンス変数のデータはどこに保存されているのかということです。BigDataクラスの説明で以下の文章があります。

BigDataクラスの初期化時にはそれらのデータの読み込みは行わずに、データへアクセスがあったタイミングで読み込むようにします。

ということで__getitem__()内でnp.load()が使用されていて、それ自体は分かるのですが、ここで先ほどインスタンス化したtrain_setとget_spiral()で取得したxの容量を見てみます。

import sys
print(sys.getsizeof(train_set)) # 56
print(sys.getsizeof(train_set.data)) # 2512
print(sys.getsizeof(x)) # 2512

インスタンス変数に格納したデータって、インスタンス自身が持っているのだと思っていたのですが違うのですね。しかしオブジェクトの一覧を出してみてもtrain_set.dataは出てこず…いったいどこでどんな形で保存されているんでしょう?この辺の知識は全然ないので何を調べていいのかすら分かっていません。。


ステップの後半ではtransform機能を実装するのですが、肝心のtransformsモジュール内の各クラスに関する説明が随分とあっさりしているので簡単にまとめます。まずはコードを見れば分かることですが基本的な使い方をしたときに何が起こっているのかのフローから。

f:id:cha_kabu:20201122074458p:plain

transformsモジュールを使うことが必須なわけでなければ、Composeクラスを使う必要もないのですが、とにかく行いたい処理を実装したものをインスタンス化したり、関数化します。それをDataset(を継承した)クラスをインスタンス化する際に引数transformsに与えてやると、__getitem__()でデータを返す時にデータをそのインスタンスや関数に通したものを返してくれます。

さて、突然出てきたComposeが何者かということでコードを見てみます。

class Compose:
    """Compose several transforms.
    Args:
        transforms (list): list of transforms
    """
    def __init__(self, transforms=[]):
        self.transforms = transforms

    def __call__(self, img):
        if not self.transforms:
            return img
        for t in self.transforms:
            img = t(img)
        return img

注記にもありますが、インスタンス化時の引数にはtransforms(変換処理)をリストで受け取ります。そしてDataset(を継承した)クラスの__getitem__()内でインスタンスに入力が与えられると、__call__()によって自動的に呼び出される処理で、transformsがNoneでなければ各処理を順に適用したうえで返します。

Composeの引数に入れるtransformsにどんなものがあるかはたくさんあるので省きますが、最低限「transformsモジュールのComposeクラスに、同じくtransformsモジュールの他の変換処理を行うクラスをリストで渡してインスタンス化する」というお作法を覚えておけば、混乱することは無いかと思います。※リストで渡すのは必ずしもtransformモジュールのものでなくとも、自作の関数などでも問題ないです。

ステップ50 ミニバッチを取り出すDataLoader

こちらもpytorchではおなじみDataLoaderです。イテレーターなど細かいところを深掘るとちょっとややこしい気もしますが、流れを理解するだけであればそう難しくありません。DataLoderクラスの有無でコードがどう変わったかだけメモしておきます(accuracy算出の追加など、関係のない修正は無視)。コメントアウトしている部分が以前の実装で、DataLoderクラス内で処理が行われています。

max_epoch = 300
batch_size = 30
hidden_size = 10
lr = 1.0

train_set = dezero.datasets.Spiral(train=True)
test_set = dezero.datasets.Spiral(train=False)
############################# 追加 #############################
train_loader = DataLoader(train_set, batch_size)
test_loader = DataLoader(test_set, batch_size, shuffle=False)
################################################################

model = MLP((hidden_size, 10))
optimizer = optimizers.SGD(lr).setup(model)

# data_size = len(x)
# max_iter = math.ceil(data_size / batch_size)

for epoch in range(max_epoch):
    # データセットのインデックスのシャッフル
    # index = np.random.permutation(data_size)
    sum_loss, sum_acc = 0,0
    
    # for i in range(max_iter):
    for x, t in train_loader:
    # ミニバッチの作成
    # batch_index = index[i * batch_size:(i + 1) * batch_size]
    # batch = [train_set[i] for i in batch_index]
    # batch_x = np.array([example[0] for example in batch])
    # batch_t = np.array([example[1] for example in batch])
        
        y = model(x)
        loss = F.softmax_cross_entropy(y, t)
        acc = F.accuracy(y, t)
        model.cleargrads()
        loss.backward()
        optimizer.update()
        
        sum_loss += float(loss.data) * len(t)
        sum_acc += float(acc.data) * len(t)
    
    print("epoch:{}".format(epoch+1))
    print("train loss: {:.4f}, accuracy: {:.4f}".format(
        sum_loss / len(train_set), sum_acc / len(train_set)))

ステップ51 MNISTの学習

他でDeepLearningを学んだ人ならみんな通る道MNISTです。特に新しいこともないのでまとめるものはありませんが、これまでの積み重ねでコードがめちゃくちゃ分かる様になりました…!特にデータセット作成のところは今まではソースコードを見ることもしていなかったので「この引数なんなの???」と思っていたのですが、とてもクリアになりました。。

最後に

やっと第4ステージ終了です!次からラストステージへ!次回は~ステップ54を予定。