【超初心者向け】Pytorchのpackを使いこなそう

zuka

こんにちは。zuka(@beginaid)です。

本記事では,Pytorchで系列データを扱う際に必要となるpackについて説明をしていきます。

目次

一次情報

本記事で参考にした一次情報を最初に掲載しておきます。

一次情報:Pytorch公式ドキュメント

環境

本記事を執筆した当時の環境は以下の通りです。

Pytorch:1.5.1+cu101

概要

Pytorchでは,系列データを扱う際にはバッチごとのデータが同じ長さである必要があります。しかし,通常系列データの長さはデータによってまちまちです。

そこで利用されるのがpackです。packを利用することで,バッチごとに一番長いデータの系列長に合わせてパディングを行うことができ,さらにパディングした部分は学習で利用しない(無視する)ように細工することが可能になります。Pytorchでは,packに関するメソッドが4種類用意されています。

キャプション

 pad_sequence

 pad_packed_sequence

 pack_sequence

 pack_padded_sequence

このうち,pack_sequenceはbatch_firstがサポートされていないため,あまり利用されません。本記事でもpack_sequenceは利用しないこととします。

覚え方

zuka

4つとも同じような名前で覚えきれないよね。

この4つのメソッドはsequencepackの相互変換として捉えれば覚えやすいです。sequenceはバッチ化したいtensorのリスト,packはパディングを使わないようにするためのPytorchのクラス(型)というイメージです。

pad_sequencesequenceをパディングします。

pad_packed_sequencepackedされたsequenceをパディングします。

pack_sequencesequencepackします。
※あまり使われません。

pack_padded_sequenceはパディングされたsequenceをpackします。

packの活用方法

STEP
sequence

系列長の異なるtensorをリスト化する

STEP
pad_sequence

リスト内にあるtensorの系列長を0パディングで揃える

STEP
pack_padded_sequence

0パディングされたtensorのリストをpackする

STEP
RNN

packされたtensorRNNに入力する

STEP
pad_packed_sequence

RNNの出力はpackされているのでpackを解凍する

具体例

以下では,packの利用方法を具体例を用いて確認していきます。まずは,必要となるライブラリをインポートしましょう。

import torch
import torch.nn as nn
from torch.nn.utils.rnn import pad_sequence, pad_packed_sequence, pack_sequence, pack_padded_sequence

さて,ここからは上でお伝えしたSTEPにしたがって系列データをpackで扱う流れを見ていきましょう。

STEP1 sequence

以下のような3つの系列データをバッチにすることを考えます。

a = torch.tensor([1,1,1])
b = torch.tensor([2,2,2,2])
c = torch.tensor([3,3,3,3,3])

STEP1では,バッチ化したいtensorをリスト化します。ただし,各データは(系列長,特徴量)の二次元である必要があるので,[:, None]を利用して特徴量次元方向に各データを拡張しています。

# dataはバッチ化したいtensorのリスト
data = [a[:, None], b[:, None], c[:, None]]

STEP2 pad_sequence

次に,pad_sequenceを利用して0パディングを施していきます。

# xは(バッチ,系列長,特徴量)
x = pad_sequence(data, batch_first=True, padding_value=0)
print(x.size())
# 可視化しやすいように(バッチ,特徴量,系列長)にする
print(x.permute(0,2,1))
torch.Size([3, 5, 1])
tensor([[[1, 1, 1, 0, 0]],

        [[2, 2, 2, 2, 0]],

        [[3, 3, 3, 3, 3]]])
zuka

しっかりとゼロパディングできているね。

STEP3 pack_padded_sequence

さらに,pack_padded_sequenceを利用して0パディングされたtensorのリストをpackします。

# 各データの系列長を引数として与えるので取得しておく
x_len = [len(i) for i in data]
# パディングされたxをpack_padded_sequenceに渡す
# batch_first=Trueとすることで(バッチ,系列長,特徴量)になる
# enforce_sorted=Falseとすることで系列長を昇順に用意する必要がなくなる
x = pack_padded_sequence(x, x_len, batch_first=True, enforce_sorted=False)

また,packはクラスですので,xpack_padded_sequenceのオブジェクトです。

PackedSequence(data=tensor([[3],
        [2],
        [1],
        [3],
        [2],
        [1],
        [3],
        [2],
        [1],
        [3],
        [2],
        [3]]), batch_sizes=tensor([3, 3, 3, 2, 1]), sorted_indices=tensor([2, 1, 0]), unsorted_indices=tensor([2, 1, 0]))

オブジェクトであるxから,databatch_sizedsorted_indicesunsorted_indicesなどを呼び出せます。

print(x.data.permute(1,0))
print(x.batch_sizes)
print(x.sorted_indices)
print(x.unsorted_indices)
tensor([[3, 2, 1, 3, 2, 1, 3, 2, 1, 3, 2, 3]])
tensor([3, 3, 3, 2, 1])
tensor([2, 1, 0])
tensor([2, 1, 0])

x.data(要素数, 1)という一次元化されたtensorになっています。これは,RNNが並列計算しやすいように元のtensorを並び替えた結果だと捉えられます。私たちが利用する分には,packdataをいじる必要はないため,dataの構造を把握しておく必要はないですが,一次元化されていることくらいは知っておくとよいでしょう。

STEP4 RNN

RNNにpackされたsequenceを突っ込みます。こうすることで,パディングされた部分は無視されるようになります。

rnn = nn.LSTM(input_size=1, hidden_size=1, num_layers=1, batch_first=True)
y, _ = rnn(x.float())

rnnの出力はpackのクラスになっています。

print(y)
print(y.data.permute(1,0))
PackedSequence(data=tensor([[-0.0245],
        [-0.0752],
        [-0.1656],
        [-0.0260],
        [-0.0861],
        [-0.2155],
        [-0.0261],
        [-0.0875],
        [-0.2298],
        [-0.0261],
        [-0.0877],
        [-0.0261]], grad_fn=<CatBackward>), batch_sizes=tensor([3, 3, 3, 2, 1]), sorted_indices=tensor([2, 1, 0]), unsorted_indices=tensor([2, 1, 0]))
tensor([[-0.0245, -0.0752, -0.1656, -0.0260, -0.0861, -0.2155, -0.0261, -0.0875,
         -0.2298, -0.0261, -0.0877, -0.0261]], grad_fn=<PermuteBackward>)

STEP5 pad_packed_sequence

packされたtensorpad_packed_sequenceで復元します。

# pad_packed_sequenceでPackedSequenceをtensorに変換
y, _ = pad_packed_sequence(y, batch_first=True, padding_value=0.0, total_length=None)
print(y.permute(0,2,1))
tensor([[[-0.1656, -0.2155, -0.2298,  0.0000,  0.0000]],

        [[-0.0752, -0.0861, -0.0875, -0.0877,  0.0000]],

        [[-0.0245, -0.0260, -0.0261, -0.0261, -0.0261]]],
       grad_fn=<PermuteBackward>)
zuka

しっかりと系列長分だけのデータが出力されているね。0パディングした部分はしっかりと0になってることが確認できたよ。

まとめ

zuka

Pytorchでは初心者殺しと呼ばれるpackについて説明してきたよ。RNNにバッチ化されたデータを突っ込むときに活用してみてね。

よかったらシェアしてね!

コメント

コメント一覧 (6件)

  • こんにちは。
    pytorchを勉強している者で、本記事を大変参考にさせていただきました。
    本記事の趣旨とは少し逸れるのですが、1点質問させてください。

    pad_packed_sequenceした結果を全結合層に入力する場合、うまいやり方はありますでしょうか?
    paddingをしていない場合は、y[:,-1,:]などを渡すのが一般的かと思われますが、
    paddingした場合はほぼ[[0],[0]…]のようになってしまうので、どうするのか気になりました。
    ご回答いただけると幸いです。

    • ML beginner様

      ご質問ありがとうございます。
      >pad_packed_sequenceした結果を全結合層に入力する場合、うまいやり方はありますでしょうか?
      全結合層にpackした系列を入力する必要はないように思えます。packはあくまでもpytorchのRNN,つまり再帰的な動作を行うDNNにおいてバッチ内の異なる系列を扱うための便宜的なクラス(型)のようなものです。単に全結合を利用するだけであれば,ゼロパディングを施したバッチをそのまま入力すればよいと思います。適切なラベルを用いれば,ゼロパディングを施した部分が無視されるような学習が行われるはずです。

      >paddingをしていない場合は、y[:,-1,:]などを渡すのが一般的かと思われますが、paddingした場合はほぼ[[0],[0]…]のようになってしまうので、どうするのか気になりました。
      上述の通り,非再帰型のネットワークであればゼロパディングを施した箇所が無視されるような学習になるはずです。再帰型であれば「0」も1つの情報として扱われてしまうためにpackを用いる必要性が出てくるのです。全結合層や畳み込み層などであればパディングを施してそのまま突っ込むのが普通と思います。なお,注意機構であればpackは使わず,予め用意してある系列長の情報に従ったマスクを利用することで異なる系列長を扱うことを可能にします。

      • ご回答ありがとうございます!大変申し訳ないですが、さらに具体的に質問させていただけないでしょうか。

        時系列予測で、LSTMを使用し、その出力をさらに全結合に入力して最終的な出力することを想定しています(余談ですが、このようなアーキテクチャを多々見たことがあるのですが、一般的ではないのでしょうか‥?)。
        この想定で、バッチデータ内の大半が短いsequence長であり、paddingされている場合、先の質問でもお聞きしたように、そのまま全結合に入力しようとすると大半が0になり([[0],[0]…])、うまく学習が進まないのではないでしょうか?

        • ML beginner様

          ご質問ありがとうございます。

          >時系列予測で、LSTMを使用し、その出力をさらに全結合に入力して最終的な出力することを想定しています(余談ですが、このようなアーキテクチャを多々見たことがあるのですが、一般的ではないのでしょうか‥?)。
          非常によく利用されるネットワーク構成です。

          >この想定で、バッチデータ内の大半が短いsequence長であり、paddingされている場合、先の質問でもお聞きしたように、そのまま全結合に入力しようとすると大半が0になり([[0],[0]…])、うまく学習が進まないのではないでしょうか?
          まず,バッチの作り方のポイントとして,できるだけ同じ長さの系列長で揃えるというものがあります。ですので,バッチ内ではパディングを施す部分は極力少なくするというコツがあります。画像処理や音声認識の分野では同じ系列長でトリミングしたものをバッチとして扱うため,パディングを施す部分は少なくなっています。ですので,「大半が0になり([[0],[0]…])」というような状況はあまり起こり得ません。
          とはいえ,おっしゃるとおり時系列予測などではバッチ内に異なる系列長が存在することは避けられません。そこで,あらかじめ系列長が分かっているという前提であれば,全結合層に通した後に系列長に対応するマスクをかけることで明示的にゼロパディングを復元します。こうすることで,損失関数を計算する際にパディングがなされている部分は無視されるようになります。
          ご質問の意図としては,全結合層に通す前の行列の要素が0となっている部分に関して学習が進まないのではないかというものだと私の方では理解しましたが,これは問題ありません。なぜなら,重要なのは全結合層に通す前の行列ではなく,損失関数を計算する際の行列だからです。すなわち,前述の通り,全結合層を通した後にマスクを施してパディングされた部分の要素を0にしてあげれば,その部分は損失関数に寄与しませんので,ネットワークはパディングした部分を無視するような学習を行います。
          問題となるのは,あらかじめ系列長が分かっていない場合です。訓練時は系列長が分かっており,推論時は分かっていないというような問題設定の場合ですね。この場合は,推論時は再帰的に系列を生成する必要があります。ですので,パディング操作はあまり関係ないように思えます。

          • マスクして損失関数を計算すれば良いのですね。元々の系列長はわかっている想定なので、マスク方法を調べてみたいと思います。
            zuka様のご回答により、モデルの学習と私自身の学習も進みそうです。お時間割いていただいきありがとうございました。これからも機械学習系の記事を期待しております!

          • ML beginner様

            よかったです^^
            稚拙な部分もあるかとは思いますが,今後も参考にしていただければ幸いです。

コメントする

目次
閉じる