対象者
深層学習で、特に畳み込みニューラルネットワーク(CNN)を勉強中に見かけるnumpy.pad
関数について、いまいち動作が掴めないという方へ。
公式ドキュメントを噛み砕いて和訳していきます。
目次
pad
関数とは
CNNで登場するpad
関数、結構わかりづらい動作をしますよね?
大抵の書籍ではメインではないのでさらっと
x = np.pad(x, [(0, 0), (0, 0), (pad, pad), (pad, pad)], "constant")
とすればOK、ぐらいしか書いてないのではないでしょうか。
ということで、この関数を徹底解剖します。
公式ドキュメントには
numpy.pad(array, pad_width, mode='constant', **kwargs)
のように引数が指定されると書いていますね。まずはそれぞれ見ていきましょう。
第一引数について
まずは公式ドキュメントを見てみましょう。
array : array_like of rank N
The array to pad.
和訳すると、
array : ランクNの配列かそれに類するもの
パディング対象の配列
となります。ランク(階数)とは線形代数の専門用語で、まあここでは次元数という認識でいいんじゃないでしょうか...詳しくはこちらとこちらをご覧ください。
とりあえず、これについてはお分かりかと思います。パディング対象の配列を指定します。
第二引数について
さて、問題は第二引数ですね。
pad_width : {sequence, array_like, int}
Number of values padded to the edges of each axis. ((before_1, after_1), ..., (before_N, after_N)) unique pad widths for each axis. ((before, after),) yields same before and after pad for each axis. (pad,) or int is a shortcut for before = after = pad width for all axes.
和訳してみます。
pad_width : {シーケンス、配列かそれに類するもの、整数}のいずれか
それぞれの次元の端にパディングされる数字の数。
((before_1, after_1), ..., (before_N, after_N)) : それぞれの次元特有のパディング幅(before_i, after_i)を指定します。
((before, after),) : それぞれの次元に同じパディング幅(before, after)を指定します。
(pad,) または整数 : 全ての次元に対して同じパディング幅(before = after = pad)を指定します。
さて、意味がわかりにくいですね。実装も交えて見てみましょう。
import numpy as np
x_1d = np.arange(1, 3 + 1)
print(x_1d)
1次元の場合
まずは1次元配列から見てみましょう。それぞれのドキュメントで指定されているようにやってみます。
まずは
((before_1, after_1), ..., (before_N, after_N))
ですね。
print(np.pad(x_1d, ((1, 1))))
print(np.pad(x_1d, ((2, 1))))
print(np.pad(x_1d, ((1, 2))))
これならなんとなくわかりますよね?1次元ですので指定可能なtuple
は1つだけで、before_1
で指定している数だけ配列の左に、after_1
で指定している数だけ配列の右に、それぞれ$0$でパディングされていますね。
ちなみに二重タプルで書いているつもりですが、実はPythonでは単タプルと同様の扱いをされています。
続いて
((before, after),)
でやってみましょう。
print(np.pad(x_1d, ((1, 1),)))
print(np.pad(x_1d, ((2, 1),)))
print(np.pad(x_1d, ((1, 2),)))
はい、結果は一緒ですね。こちらでは明示的に二重タプルとして引数を送っています。
最後に
(pad,)または整数
でやってみましょう。
print(np.pad(x_1d, (1,)))
print(np.pad(x_1d, (2,)))
print(np.pad(x_1d, 1))
print(np.pad(x_1d, 2))
両端に指定した数だけ$0$が埋められていますね。この指定方法だと両端とも同じ数だけパディングされます。
2次元の場合
次は2次元配列でやってみます。
x_2d = np.arange(1, 3*3 + 1).reshape(3, 3)
print(x_2d)
print(np.pad(x_2d, ((1, 1), (2, 2))))
print(np.pad(x_2d, ((2, 2), (1, 1))))
print(np.pad(x_2d, ((1, 2), (1, 2))))
print(np.pad(x_2d, ((2, 1), (1, 2))))
print(np.pad(x_2d, ((1, 1),)))
print(np.pad(x_2d, ((1, 2),)))
print(np.pad(x_2d, ((2, 1),)))
print(np.pad(x_2d, ((2, 2),)))
print(np.pad(x_2d, (1,)))
print(np.pad(x_2d, (2,)))
print(np.pad(x_2d, 1))
print(np.pad(x_2d, 2))
((before_i, after_i))
の結果
((before, after),)
の結果
(pad,)
の結果
整数の結果
さて、2次元の場合は1次元目である行(上下)にまずパディングされ、その次に2次元目の列(左右)にパディングされています。それ以外は1次元の時と同じですね。
4次元の場合
もう大体お分かりかと思いますので、3次元は飛ばして4次元で実験します。
一つずつコメント外して実行することをお勧めします。出力が縦に長くなってしまうので大変見づらいです。
def print_4darray(x):
first, second, third, fourth = x.shape
x_str_size = len(str(np.max(x)))
for i in range(first):
for k in range(third):
for j in range(second):
str_size = len(str(np.max(x[i, j, k, :])))
if x_str_size != str_size:
add_size = "{: " +str(x_str_size - str_size)+ "d}"
np.set_printoptions(
formatter={'int': add_size.format})
else:
np.set_printoptions()
print(x[i, j, k, :], end=" ")
print()
print()
x_4d = np.arange(1, 3*3*3*3 + 1).reshape(3, 3, 3, 3)
print_4darray(x_4d)
print_4darray(np.pad(x_4d, ((1, 1), (2, 2), (0, 0), (0, 0))))
print_4darray(np.pad(x_4d, ((0, 0), (0, 0), (2, 2), (1, 1))))
print_4darray(np.pad(x_4d, ((1, 1), (0, 0), (2, 2), (0, 0))))
print_4darray(np.pad(x_4d, ((0, 0), (1, 1), (0, 0), (2, 2))))
print_4darray(np.pad(x_4d, ((0, 0), (1, 1), (2, 2), (0, 0))))
print_4darray(np.pad(x_4d, ((1, 1), (0, 0), (0, 0), (2, 2))))
#print_4darray(np.pad(x_4d, ((1, 1),)))
#print_4darray(np.pad(x_4d, ((1, 2),)))
#print_4darray(np.pad(x_4d, ((2, 1),)))
#print_4darray(np.pad(x_4d, ((2, 2),)))
#print_4darray(np.pad(x_4d, (1,)))
#print_4darray(np.pad(x_4d, (2,)))
#print_4darray(np.pad(x_4d, 1))
#print_4darray(np.pad(x_4d, 2))
print_4darray
関数ですが、1次元目、3次元目、2次元目の順にループを回し、4次元目をprint
関数で出力しています。この時end=" "
で改行ではなく半角スペースを出力しています。
あとは調整用の改行とかnp.set_printoptions
関数で出力時の空白を制御していたりとかしています。
numpyの標準出力では見づらいので作成しました。
ちなみに、コードを実行すると多分いくつか画面内に収まらないものがあると思います。画像は複数枚のスクショを一生懸命重ねました。笑
あと、jupyter notebookのセル幅を広げたりもしました。
第三引数について
第三引数についても見てみましょう。長いので部分ごとに。
modestr or function, optional
One of the following string values or a user supplied function.
モードを指定する文字列または関数を指定するオプション引数です。
下記の文字列から一つ、またはユーザが関数を指定します。
引数自体の説明は特に問題ありませんね。ユーザが指定する関数というのは後述します。
`constant`の説明
‘constant’ (default)
Pads with a constant value.
constant
(デフォルト)
定数(0のこと)でパディングします。
`edge`の説明
‘edge’
Pads with the edge values of array.
edge
行列の端の値でパディングします。
print(np.pad(x_2d, 1, "edge"))
2次元配列でのパディング例です。
中心部分の$3 \times 3$要素が元々の配列でしたね。パディング後の配列の端から縦・横・斜めに中心に向かっていき、一番最初に出会った値をコピーしています。
`linear_ramp`の説明
‘linear_ramp’
Pads with the linear ramp between end_value and the array edge value.
linear_ramp
最後の値と端の値との間のランプ関数でパディングします。
print(np.pad(x_2d, 3, "linear_ramp"))
実際に動かしてもぱっと見ではわかりづらいですね笑。ブロックで分けてみましょう。
赤色のブロックは動作が読めませんが、他の色のブロックおよびブロックの間はわかるのではないでしょうか?
例として水色のブロックに注目します。
$3$の縦横に注目すると、端の値$0$に向かって$3210$となっていますね。イメージとしては$0 \le x \le 3$を4等分して切り捨てる感じです。ここでは$0, 1, 2, 3$のように等分されますので、それがそのまま現れていますね。
緑色のブロックに注目します。ここも同じルールが適用できます。$0 \le x \le 7$を4等分すると$0, 2.\dot3, 4.\dot6, 7$となり、$0, 2, 4, 7$が現れています。
また、各ブロックの他の要素は上記の例で決定された値を斜め方向に帯状に並べていますね。端の値は$0$となっています。
`maximum`の説明
‘maximum’
Pads with the maximum value of all or part of the vector along each axis.
maximum
軸ごとの全体または一部のベクトルの最大値でパディングします。
微妙にんん??となる感じですが、ルールが分かれば納得します。
紫色のブロックが元々の配列ですね。これにパディングを施していきます。
まずは縦横の値がそれぞれのブロックの中の最大値でパディングされます。そしてそれらが全て終わったあと角の値がブロック内の最大値でパディングされます。
`mean`の説明
‘mean’
Pads with the mean value of all or part of the vector along each axis.
mean
軸ごとの全体または一部のベクトルの平均値でパディングします。
print(np.pad(x_2d, 1, "mean"))
`median`の説明
‘median’
Pads with the median value of all or part of the vector along each axis.
median
軸ごとの全体または一部のベクトルの中央値でパディングします。
print(np.pad(x_2d, 1, "median"))
`minimum`の説明
‘minimum’
Pads with the minimum value of all or part of the vector along each axis.
minimum
軸ごとの全体または一部のベクトルの最小値でパディングします。
print(np.pad(x_2d, 1, "minimum"))
`reflect`の説明
‘reflect’
Pads with the reflection of the vector mirrored on the first and last values of the vector along each axis.
reflect
軸ごとのベクトルの、最初と最後の値を写したベクトルの反射でパディングします。
print(np.pad(x_2d, 2, "reflect"))
わかりにくい...けどなんとなくわからなくもないですね。例の如くブロック分けします。
これでどうでしょう。同じ色のブロックの間に位置する値に対して対称的にパディングされていますね。
水色のブロックに注目すると、$1$を中心としてその下の$4, 7$が「反射したベクトル」でパディングしていますね。
緑色のブロックでは中心が$3$で、その左側の$1, 2$が「反射したベクトル」でパディングされていますし、赤色のブロックでは中心を$1$として$5, 6, 8, 9$が「反射したベクトル」でパディングされています。
`symmetric`の説明
‘symmetric’
Pads with the reflection of the vector mirrored along the edge of the array.
symmetric
配列の端にそって写したベクトルの反射でパディングします。
print(np.pad(x_2d, 2, "symmetric"))
reflect
との最大の違いは、元々の配列の端の値を「含んで反射する」か「含まずに反射する」かですね。
`wrap`の説明
‘wrap’
Pads with the wrap of the vector along the axis. The first values are used to pad the end and the end values are used to pad the beginning.
wrap
軸に沿ったベクトルのラップでパディングします。最初の値は最後をパディングするために使われ、最後の値は最初をパディングするために使われます。
print(np.pad(x_2d, 2, "wrap"))
こうして見ると明白ですね。reflect
の反射しないバージョンです。もはや公式ドキュメントとしてそう書いて欲しい...
`empty`の説明
‘empty’
Pads with undefined values.
New in version 1.17.
empty
不定の値でパディングします。
numpyのバージョン1.17で追加されました。
import numpy as np
print(np.pad(np.arange(1, 3*3+1).reshape(3, 3), 2, "empty"))
print(np.pad(np.arange(1, 3*3+1).reshape(3, 3), 5, "empty"))
実験のため新しいノートブックを作成しています(新しいセルではありません)。
ぼくの環境では上記のようになりました。empty
コマンドでのパディング幅4までは$0$パディングで、$5$以上では不定の、おそらく確保したメモリ先に残っていた値が出力されています。ちなみにパディング幅$5$の画像は全体を写しても仕方ないので一部のみです。
\の説明
<function>
Padding function, see Notes.
Notes
New in version 1.7.0.
For an array with rank greater than 1, some of the padding of later axes is calculated from padding of previous axes. This is easiest to think about with a rank 2 array where the corners of the padded array are calculated by using padded values from the first axis.
The padding function, if used, should modify a rank 1 array in-place. It has the following signature:
padding_func(vector, iaxis_pad_width, iaxis, kwargs)
where
vector: ndarray
A rank 1 array already padded with zeros. Padded values are vector[:iaxis_pad_width[0]] and vector[-iaxis_pad_width[1]:].
iaxis_pad_width: tuple
A 2-tuple of ints, iaxis_pad_width[0] represents the number of values padded at the beginning of vector where iaxis_pad_width[1] represents the number of values padded at the end of vector.
iaxis: int
The axis currently being calculated.
kwargs: dict
Any keyword arguments the function requires.
<function>
パディング関数。ノートを参照。
ノート
numpyのバージョン1.7.0で追加。
ランク1以上の配列のため、高次のいくつかのパディングは低次のパディングから計算されます。これは2次元配列に対してパディングを施した配列の角の要素を、既に施したパディングの値を用いて決定することを考えるともっともわかりやすいでしょう。
パディング関数を用いる場合は1次元配列を所定の方法で変更する必要があります。次のような感じです:
padding_func(vector, iaxis_pad_width, iaxis, kwargs)
それぞれの引数について、
vector
: ndarray
1次元配列は既に0でパディングされています。パディングされる値はvector[:iaxis_pad_width[0]]
とvector[-iaxis_pad_width[1]:]
です。
iaxis_pad_width
: tuple
整数の二重タプルで、iaxis_pad_width[0]
はベクトルの最初にパディングされる値の数を表し、iaxis_pad_width[1]
はベクトルの最後にパディングされる値の数を表します。
iaxis
: int
現在計算されている次元。
kwargs
: dict
関数が要求するいくつかのキーワード引数。
def pad_with(vector, pad_width, iaxis, kwargs):
pad_value = kwargs.get('padder', 10)
vector[:pad_width[0]] = pad_value
vector[-pad_width[1]:] = pad_value
print(np.pad(x_2d, 2, pad_with))
print(np.pad(x_2d, 2, pad_with, padder=100))
vector
とpad_width
とiaxis
は自動で渡されます。他にユーザが引数を指定したい場合はキーワード引数としてnumpy.pad
関数に渡しておき、パディング関数内で取り出すようにします(pad_value = kwargs.get('padder', 10)
)。
キーワード引数`stat_length`の説明
stat_length: sequence or int, optional
Used in ‘maximum’, ‘mean’, ‘median’, and ‘minimum’. Number of values at edge of each axis used to calculate the statistic value.
((before_1, after_1), … (before_N, after_N)) unique statistic lengths for each axis.
((before, after),) yields same before and after statistic lengths for each axis.
(stat_length,) or int is a shortcut for before = after = statistic length for all axes.
Default is None, to use the entire axis.
stat_length
: シーケンスまたは整数、オプションです。
maximum
、mean
、median
、minimum
で指定できるオプションです。それぞれの次元の端の値の数が統計値の計算に用いられます。
((before_1, after_1), … (before_N, after_N))
ではそれぞれの次元で統計幅を個別に指定しています。
((before, after),)
ではそれぞれの次元に対して同じ統計幅が用いられます。
(stat_length,)
または整数は全ての次元に対してbefore = after
な統計幅を用いるためのショートカットです。
デフォルトはNone
で、全ての次元に用います。
print(np.pad(x_2d, 1, "maximum", stat_length=2))
maximum
での出力結果と見比べていただけるとわかりますが、最大値を取るベクトルのサイズが$3$(全体)ではなく$2$となっています。
キーワード引数`constant_values`の説明
constant_values: sequence or scalar, optional
Used in ‘constant’. The values to set the padded values for each axis.
((before_1, after_1), ... (before_N, after_N)) unique pad constants for each axis.
((before, after),) yields same before and after constants for each axis.
(constant,) or constant is a shortcut for before = after = constant for all axes.
Default is 0.
constant_values
: シーケンスまたは実数、オプションです。
constant
で指定できるオプションです。それぞれの次元にパディングする値を設定することができます。
((before_1, after_1), ... (before_N, after_N))
では、各次元に対してそれぞれ個別にパディング用の定数を設定します。
((before, after),)
では、それぞれの次元に対して同じパディング用の定数を設定します。
(constant,)
か定数はbefore = after
な定数を全ての次元に適用するショートカットです。
デフォルトでは$0$です。
print(np.pad(x_2d, 1, "constant", constant_values=(-1, -2),))
キーワード引数`end_values`の説明
end_values: sequence or scalar, optional
Used in ‘linear_ramp’. The values used for the ending value of the linear_ramp and that will form the edge of the padded array.
((before_1, after_1), ... (before_N, after_N)) unique end values for each axis.
((before, after),) yields same before and after end values for each axis.
(constant,) or constant is a shortcut for before = after = constant for all axes.
Default is 0.
end_values
: シーケンスまたは実数、オプションです。
linear_ramp
で指定できるオプションです。linear_ramp関数での最後の値を設定し、また端の値を指定の値で埋めます。
((before_1, after_1), ... (before_N, after_N))
では、各次元に対してそれぞれ個別に設定します。
((before, after),)
では、それぞれの次元に対して同じものを設定します。
(constant,)
か定数はbefore = after
な値を全ての次元に適用するショートカットです。
print(np.pad(x_2d, 3, "linear_ramp", end_values=((-1, -2), (-3, -4))))
キーワード引数`reflect_type`の説明
reflect_type: {‘even’, ‘odd’}, optional
Used in ‘reflect’, and ‘symmetric’. The ‘even’ style is the default with an unaltered reflection around the edge value. For the ‘odd’ style, the extended part of the array is created by subtracting the reflected values from two times the edge value.
reflect_type
:even
かodd
か、オプションです。
reflect
とsymmetric
で指定できるオプションです。even
はデフォルトのスタイルで、端の値周りに不変な反射をします。odd
スタイルでは、配列のパディング部分の値が、端の値の2倍から反射された値を引くことで決定されます。
print(np.pad(x_2d, 2, "reflect", reflect_type="odd"))
水色のブロックに注目すると、$1$を中心としている点はeven
(デフォルト)と同じですが、パディングの値が全く異なりますね。
説明文に則って計算してみましょう。
説明文には「端の値の2倍から反射された値を引くことで決定されます」とあるので、端の値$1$の2倍である$2$から、反射された値$7, 4$を引くと$-5, -2$となり、出力画像と一致しますね!
赤色のブロックでも同様です。
端の値$3$の2倍である$6$から、反射された値$2, 1$を引くと$4, 5$となりますね。
print(np.pad(x_2d, 2, "constant").base)
# 出力は None となります。
base
属性は配列がオリジナル(何ともメモリを共有していない)の場合はNone
を返し、そうでない場合は配列の値を返します。
im2col
での動作
さて、この記事でim2col
関数について徹底的に説明しているのですが、ここに登場するコードには
pad_zero = (0, 0)
O_h = int(np.ceil((I_h - F_h + 2*pad_ud)/stride_ud) + 1)
O_w = int(np.ceil((I_w - F_w + 2*pad_lr)/stride_lr) + 1)
pad_ud = int(np.ceil(pad_ud))
pad_lr = int(np.ceil(pad_lr))
pad_ud = (pad_ud, pad_ud)
pad_lr = (pad_lr, pad_lr)
images = np.pad(images, [pad_zero, pad_zero, pad_ud, pad_lr], \
"constant")
という部分があります。
ここでのpad
関数が何をしているか、もうお分かりかと思います。
1,2次元目はpad_zero
ですのでパディングなし、3,4次元目はそれぞれpad_ud
, pad_lr
だけパディングしています。全体の括りは実はタプル型でなくても大丈夫なんですね。
1,2次元目はバッチ、チャンネル数で、3,4次元目というのは画像データですので、画像まわりだけパディングしていることが理解できると思います。
おわりに
pad
関数、奥が深い...