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
覚え方
zuka4つとも同じような名前で覚えきれないよね。
この4つのメソッドはsequenceとpackの相互変換として捉えれば覚えやすいです。sequenceはバッチ化したいtensorのリスト,packはパディングを使わないようにするためのPytorchのクラス(型)というイメージです。
pad_sequenceはsequenceをパディングします。
pad_packed_sequenceはpackedされたsequenceをパディングします。
pack_sequenceはsequenceをpackします。
※あまり使われません。
pack_padded_sequenceはパディングされたsequenceをpackします。
packの活用方法
系列長の異なるtensorをリスト化する
リスト内にあるtensorの系列長を0パディングで揃える
0パディングされたtensorのリストをpackする
packされたtensorをRNNに入力する
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はクラスですので,xはpack_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から,data,batch_sized,sorted_indices,unsorted_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])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されたtensorをpad_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になってることが確認できたよ。
まとめ
zukaPytorchでは初心者殺しと呼ばれる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様
よかったです^^
稚拙な部分もあるかとは思いますが,今後も参考にしていただければ幸いです。