はじめに
音声処理でよく使うフレームシフト処理(スライディングウィンドウとか,Pandasのrolling的な処理(?)とも呼ぶ)をしたい.
numpy.lib.stride_tricks.as_strided
を使えばよいという情報にたどり着くも,思いのほかはまってしまったので,備忘録的にいくつかの例を書いてみた.
1次元のデータ
いろいろ調べてみたものの,2次元の例が多く自分にはイメージできなかったので,
最初は1次元から考えていく.1次元ならワタシチョトダケワカルので...
import numpy as np
from numpy.lib.stride_tricks import as_strided
まずは,適当に長さ10のndarray
を作る.そして,いくつかの情報を確認して,データ構造のイメージを持っておく.
特に,itemsize
は,後でstrideのbyte数を考えるときに必要になる.
x = np.arange(10, dtype='float64')
# -> array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])
x.size
# -> 10
x.shape
# -> (10,)
x.strides
# -> (8,)
x.itemsize
# -> 8
早速as_stride
を試してみる.まずは,1つとばしにしてみる.
要素1つで8バイトなので,1つスキップするなら,こうだろうか?
as_strided(x, strides=(16,))
# -> array([0.00000000e+000, 2.00000000e+000, 4.00000000e+000, 6.00000000e+000,
# 8.00000000e+000, 0.00000000e+000, 4.80906177e-073, 4.63229019e-073,
# 3.74803829e-073, 4.16239459e+201])
末尾が怪しいが,,,C言語で配列外にアクセスしてしまった時が思い出される.
きちんとshapeも与えておくなら,こんな感じか.
as_strided(x, shape=(5,), strides=(16,))
# -> array([0., 2., 4., 6., 8.])
つぎは,スライディングウィンドウ処理の基本として,2つずつのデータとして取り出してみたい.こんな感じか.
as_strided(x, shape=(5,2))
# -> array([[0., 1.],
# [2., 3.],
# [4., 5.],
# [6., 7.],
# [8., 9.]])
できたけど,この時のstrideはどうなっているのだろうか?
as_strided(x, shape=(5,2)).strides
# -> (16, 8)
最初に調べたx.stirdes
から自動で変わっている?ようだ.
1つ目の16は下側に見ていく時に次のデータが存在する場所までのバイト数で,
2つ目の8は右側に見ていく時に次のデータが存在する場所までのバイト数である.
つまり,右には1つずつ見ていけばよく,下には1つとばして2つ目をみればよい,ということだ.
C言語の画像処理でよくやるアレに似てるな,と思ったし,これがイメージできれば怖くないのではないか.
int W = 16;
int H = 32;
unsigned char Image[W*H];
for(w=0; w<W; w++) for(h=0; h<H; h++) {
Image[h*W + w] = ...;
}
さて,次はオーバーラップを考慮しよう.下に見るときに2つ隣ではなく少し手前を見に行けばよい.
ということは,
as_strided(x, shape=(9,2), strides=(8,8))
# -> array([[0., 1.],
# [1., 2.],
# [2., 3.],
# [3., 4.],
# [4., 5.],
# [5., 6.],
# [6., 7.],
# [7., 8.],
# [8., 9.]])
はみ出したらどうなるだろうか?
as_strided(x, shape=(10,2), strides=(8,8))
# -> array([[0., 1.],
# [1., 2.],
# [2., 3.],
# [3., 4.],
# [4., 5.],
# [5., 6.],
# [6., 7.],
# [7., 8.],
# [8., 9.],
# [9., 0.]])
ゼロ詰めかな・・・?もう少し違う形にしてみよう.3つずつの塊で,2つずらして読む形だとどうなるか.
as_strided(x, shape=(5,3), strides=(16,8))
# -> array([[0., 1., 2.],
# [2., 3., 4.],
# [4., 5., 6.],
# [6., 7., 8.],
# [8., 9., 0.]])
2次元のデータ
次に,2次元の特徴量の配列から,スライディングウィンドウ処理をしたい時を考えておきたい.
X = np.arange(10, dtype='float64').reshape(5,2)
# X -> array([[0., 1.],
# [2., 3.],
# [4., 5.],
# [6., 7.],
# [8., 9.]])
3次元のデータにすることを考えるとして,元の構造(2次元の特徴量)をキープするなら,stridesの最後の2つの数字はそのままにすればいい.つまり,元のfloat64で8 byteを指定,それを2つ組にしているので16 byteを指定.
あとは,その2つ組を1つずつずらしたいなら 1 x 16 =16 byte, 2つずつずらしていきたいなら 2 x 16 = 32 bytes となる.
as_strided(X, shape=(4,2,2), strides=(16,16,8))
# -> array([[[0., 1.],
# [2., 3.]],
#
# [[2., 3.],
# [4., 5.]],
#
# [[4., 5.],
# [6., 7.]],
#
# [[6., 7.],
# [8., 9.]]])
as_strided(X, shape=(3,2,2), strides=(32,16,8))
# -> array([[[ 0.00000000e+000, 1.00000000e+000],
# [ 2.00000000e+000, 3.00000000e+000]],
#
# [[ 4.00000000e+000, 5.00000000e+000],
# [ 6.00000000e+000, 7.00000000e+000]],
#
# [[ 8.00000000e+000, 9.00000000e+000],
# [ 0.00000000e+000, -1.29986726e-231]]])
shapeを変えれば,2次元特徴量を3つ分見る,みたいなことも可能.
as_strided(X, shape=(4,3,2), strides=(16,16,8))
# -> array([[[ 0.00000000e+000, 1.00000000e+000],
# [ 2.00000000e+000, 3.00000000e+000],
# [ 4.00000000e+000, 5.00000000e+000]],
#
# [[ 2.00000000e+000, 3.00000000e+000],
# [ 4.00000000e+000, 5.00000000e+000],
# [ 6.00000000e+000, 7.00000000e+000]],
#
# [[ 4.00000000e+000, 5.00000000e+000],
# [ 6.00000000e+000, 7.00000000e+000],
# [ 8.00000000e+000, 9.00000000e+000]],
#
# [[ 6.00000000e+000, 7.00000000e+000],
# [ 8.00000000e+000, 9.00000000e+000],
# [ 0.00000000e+000, -1.29986726e-231]]])
なんとなく使い方がわかった.だが,shapeの計算&ゼロ詰めは自分でしたほうがよいかもしれない.
また,繰り返しになるがstridesはバイト数である.自分が扱ってるデータのバイト数がわからないならitemsizeなどで確認すればよい.(C言語の sizeof 的な感じ.)