多次元配列のDatasetを扱っていると気が狂いそうになることがありませんか ?
for loopで展開してもいいけど、せめて内包表記でシンプルに書けないかしらとか。DatasetのAugmentation(水増し)を考え始めると、メモリーのことも気になるし、あんまりコピーしたくないしとかで煮詰まることってありますよね。
そんなときに、np.concatenate()とnp.random.shuffle()でaxis指定したら案外すっきり書けたので紹介します。
もちろんグッディーの事例ではとてもうまく使えたけど、あなたの事例では使い物にならないかも知れませんが、簡単な例も載せておきますので、見てみてください。
やりたかったこと
いつものLSTMなので、データセットは、例えば512個の特徴量を16時刻分で1シーケンスデータとしそれが50000個などで、データセットとしては3次元(シーケンスデータ数 x シーケンス数 x データ数)です。今回は、16時刻分のシーケンスデータをData Augmentationしたくなったので、最終的には4次元(シーケンスデータ数 x シーケンス数 x 水増し比率 (N) x データ数)データを扱うことになりました。
さらに 時刻ごとに水増ししたN個のデータ配列をシャッフルする必要がありました。これをどうにかエレガントに書けないかと試行錯誤したわけですが、np.concatenate()とnp.random.shuffle()でaxis指定したら案外すっきり書けたというわけです。
言葉で言ってもわかりにくいと思いますので、簡単な例で示します。
簡単な例
$ l1 = np.arange(30)
$ l2 = np.arange(30, 120)
l1 =
[ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
24 25 26 27 28 29]
l2 =
[ 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119]
$ l1 = l1.reshape([2,3,1,5])
$ l2 = l2.reshape([2,3,3,5])
l1 =
[[[[ 0 1 2 3 4]]
[[ 5 6 7 8 9]]
[[10 11 12 13 14]]]
[[[15 16 17 18 19]]
[20 21 22 23 24]]
[[25 26 27 28 29]]]]
l2 =
[[[[ 30 31 32 33 34]
[ 35 36 37 38 39]
[ 40 41 42 43 44]]
[[ 45 46 47 48 49]
[ 50 51 52 53 54]
[ 55 56 57 58 59]]
[[ 60 61 62 63 64]
[ 65 66 67 68 69]
[ 70 71 72 73 74]]]
[[[ 75 76 77 78 79]
[ 80 81 82 83 84]
[ 85 86 87 88 89]]
[[ 90 91 92 93 94]
[ 95 96 97 98 99]
[100 101 102 103 104]]
[[105 106 107 108 109]
[110 111 112 113 114]
[115 116 117 118 119]]]]
$ l = np.concatenate([l1, l2], axis=2)
l =
[[[[ 0 1 2 3 4]
[ 30 31 32 33 34]
[ 35 36 37 38 39]
[ 40 41 42 43 44]]
[[ 5 6 7 8 9]
[ 45 46 47 48 49]
[ 50 51 52 53 54]
[ 55 56 57 58 59]]
[[ 10 11 12 13 14]
[ 60 61 62 63 64]
[ 65 66 67 68 69]
[ 70 71 72 73 74]]]
[[[ 15 16 17 18 19]
[ 75 76 77 78 79]
[ 80 81 82 83 84]
[ 85 86 87 88 89]]
[[ 20 21 22 23 24]
[ 90 91 92 93 94]
[ 95 96 97 98 99]
[100 101 102 103 104]]
[[ 25 26 27 28 29]
[105 106 107 108 109]
[110 111 112 113 114]
[115 116 117 118 119]]]]
$ rng = np.random.default_rng()
$ rng.shuffle(l, axis=2)
l =
[[[[ 40 41 42 43 44]
[ 0 1 2 3 4]
[ 35 36 37 38 39]
[ 30 31 32 33 34]]
[[ 55 56 57 58 59]
[ 5 6 7 8 9]
[ 50 51 52 53 54]
[ 45 46 47 48 49]]
[[ 70 71 72 73 74]
[ 10 11 12 13 14]
[ 65 66 67 68 69]
[ 60 61 62 63 64]]]
[[[ 85 86 87 88 89]
[ 15 16 17 18 19]
[ 80 81 82 83 84]
[ 75 76 77 78 79]]
[[100 101 102 103 104]
[ 20 21 22 23 24]
[ 95 96 97 98 99]
[ 90 91 92 93 94]]
[[115 116 117 118 119]
[ 25 26 27 28 29]
[110 111 112 113 114]
[105 106 107 108 109]]]]
わかりますかね ?
1時刻のデータが5個の特徴量からなり、1シーケンスは3時刻、このシーケンスデータが2個あるのが元のデータセット l1 (2x3x1x5) です。これに対して、同じラベルに対する3倍のシーケンスデータ l2 (2x3x3x5) を追加して、合計4倍のデータセット (2x3x4x5) に水増ししようというわけです。axis=2に対するnp.concatenate()で意図したとおりの水増しができていることがわかります。
そのあとさらにaxis=2でシャッフルして4本の配列が入れ替わっていることがわかります。
ただ、グッディーのケースではこの4本のデータを軸に沿って一律に入れ替えるのではなく、3時刻それぞればらばらに入れ替えたかったので、こんな感じになりました。
$ for i in range(l.shape[0]):
$ for j in range(l.shape[1]):
$ np.random.shuffle(l[i][j])
l =
[[[[ 40 41 42 43 44]
[ 0 1 2 3 4]
[ 35 36 37 38 39]
[ 30 31 32 33 34]]
[[ 5 6 7 8 9]
[ 55 56 57 58 59]
[ 50 51 52 53 54]
[ 45 46 47 48 49]]
[[ 70 71 72 73 74]
[ 60 61 62 63 64]
[ 65 66 67 68 69]
[ 10 11 12 13 14]]]
[[[ 85 86 87 88 89]
[ 80 81 82 83 84]
[ 75 76 77 78 79]
[ 15 16 17 18 19]]
[[100 101 102 103 104]
[ 20 21 22 23 24]
[ 90 91 92 93 94]
[ 95 96 97 98 99]]
[[105 106 107 108 109]
[110 111 112 113 114]
[ 25 26 27 28 29]
[115 116 117 118 119]]]]
内包表記でも書けると思いますが、これでめでたく毎時刻違う入れ替え方にできました。
見やすさのためにこのような配列構成 (2x3x4x5) にしましたが、このデータ構成だと、最終的には transpose(0,2,1,3) で軸を入れ替えたあと reshape(8,3,5) で2個のデータセットが8個のデータセットに水増しされることになりますね。
さいごに
やりたかったことは上記の通りなのですが、これだと汎化は進むものの、Validation Accuracy も下がってしまって想定とは違う結果になってしまいました。そう、ランダマイズしたことによって、元々のシーケンスデータが消えてしまったからです。
ということで、実際には最後にさらに元データを np.concatenate() して、この例で言えば 2x3x(4+1)x5 -> 10x3x5 の10個のデータセットに水増しし、めでたく意図通りの汎化ができました。
簡単な例でみればこんなの当たり前に感じるのですが、巨大な配列を扱っていると本当にそうなっているのかとても不安になります。しかも多少間違ってデータ操作しても結構学習が収束してしまったりするので、そこで安心せずに意図通りの操作になっているか、たまに確認してみましょう。
