이번 포스팅에서는 Python Numpy 의 배열(array)을 특정 형상(shape)으로 변형할 때 빈 자리를 '0'이나 다른 값으로 채우는 2가지 방법을 소개하겠습니다.

1. numpy.pad() 함수를 사용하여 배열(array)을 특정 형상의 배열로 변형할 때 빈자리를 '0'으로 채우기
2. tensorflow.keras.preprocessing.sequence.pad_sequence() 함수를 사용하여 배열의 원소 개수가 다른 Ragged array 를 특정 형상의 배열로 바꾸면서 빈자리를 '0'으로 채우기


1. numpy 배열을 특정 형상의 배열로 변형할 때 빈자리를 '0'으로 채우기 (padding)

    : numpy.pad() 함수



먼저, numpy 라이브러리를 importing 하고, 예제로 사용할 2 by 3 의 간단한 2차원 배열(array)을 만들어보겠습니다.




import numpy as np


x = np.array([[1, 2, 3],
                 [7, 2, 5]])
print(x)

array([[1, 2, 3],
          [7, 2, 5]])




위의 2 by 3 의 2차원 배열 x 의 위, 아래에 1개씩의 행을 추가하고, 왼쪽, 오른쪽에 1개씩의 열을 추가하여 4 by 5 의 2차원 배열을 만들되, 새로 추가되는 행과 열의 자리는 '0'으로 채워넣기(padding)를 numpy.pad() 함수를 사용하여 해보겠습니다.

numpy.pad(array, pad_width, mode='constant', **kwargs)


# np.pad(x, (1, 1))

np.pad(x, (1, 1),
       mode='constant',
       constant_values=0)


array([[0, 0, 0, 0, 0]

    [0, 1, 2, 3, 0],     [0, 7, 2, 5, 0],     [0, 0, 0, 0, 0]])




만약 위의 행 1개 추가, 왼쪽 열 1개 추가, 아래쪽 행 2개 추가, 오른쪽 열 2개를 추가하고 싶다면 pad_width 매개변수에 (1, 2) 를 설정해주면 됩니다.


np.pad(x, (1, 2),
       mode='constant',
       constant_values=0)


array([[0, 0, 0, 0, 0, 0],
       [0, 1, 2, 3, 0, 0],
       [0, 7, 2, 5, 0, 0],
       [0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0]])

 



np.pad() 메소드를 사용하지 않고, 아래처럼 numpy.zeros() 로 원하는 형상(shape)대로 모두 '0'으로 채워진 배열을 먼저 만들어놓고, indexing을 사용하여 왼쪽, 위쪽, 오른쪽, 아래쪽에 행과 열을 1개씩 비워놓을 수 있는 위치에 기존 배열을 삽입하여 np.pad() 메소드를 사용했을 때와 동일한 결과를 얻을 수도 있습니다. 이때는 새로 만들어지는 배열 z의 형상(shape)과 기존 배열 x를 채워넣을 위치의 indexing에 신경을 써주어야 하므로 조금 신경이 쓰이는 편이기는 합니다. (위의 np.pad() 와 일처리 순서가 정 반대라고 생각하면 됩니다.)


z = np.zeros((4, 5))
print(z)

[[0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]]


z[1:-1, 1:-1] = x
print(z)
[[0. 0. 0. 0. 0.] [0. 1. 2. 3. 0.] [0. 7. 2. 5. 0.] [0. 0. 0. 0. 0.]]

 



np.pad() 함수에 몇가지 재미있는 옵션을 마저 소개하겠습니다.
constant_values 를 설정해주면 '0' 대신에 원하는 다른 특정 상수 값으로 빈 자리를 채워넣을 수 있습니다. '0' 대신에 '-1'을 한번 채워볼까요?


np.pad(x, (1, 1), constant_values=-1)


array([[-1, -1, -1, -1, -1],
       [-1,  1,  2,  3, -1],
       [-1,  7,  2,  5, -1],
       [-1, -1, -1, -1, -1]])




빈 자리 채워넣기하는 방법(mode)에는 항상 똑같은 상수('constant', default) 값을 채워넣는 방법 외에도 'edge', 'linear_ramp', 'maximum', 'mean', 'median', reflect', 'symmetric', 'wrap', 'empty' 등의 다양한 mode 옵션을 제공합니다. 이들 중에서 위의 예시에서 사용한 'constant' 이외에 'edge', 'maximum', 'wrap' 의 mode 옵션을 사용하여 채워넣기 padding을 해보겠습니다. (아래 결과에서 빨간색으로 표시한 부분이 padding된 부분입니다.)


  • mode = 'edge' : 가장 변두리의 원소 값으로 빈 곳 채우기


np.pad(x, (1, 1), mode='edge')


array([[1, 1, 2, 3, 3], [1, 1, 2, 3, 3], [7, 7, 2, 5, 5], [7, 7, 2, 5, 5]])

 


  • mode = 'maximum' : 행과 열의 가장 큰 값으로 빈 곳 채우기


np.pad(x, (1, 1), mode='maximum')


array([[7, 7, 2, 5, 7],
       [3, 1, 2, 3, 3],
       [7, 7, 2, 5, 7],
       [7, 7, 2, 5, 7]])



  • mode = 'wrap' : 행과 열의 반대편 끝에 있는 원소 값으로 빈 곳 채우기


np.pad(x, (1, 1), mode='wrap')


array([[5, 7, 2, 5, 7],
       [3, 1, 2, 3, 1],
       [5, 7, 2, 5, 7],
       [3, 1, 2, 3, 1]])


* Reference: https://numpy.org/doc/stable/reference/generated/numpy.pad.html



  2. 원소 개소가 다른 Ragged array를 특정 형상의 배열로 바꿀 때 빈자리를 '0'으로 채우기

     : tensorflow.keras.preprocessing.sequence.pad_sequence() 함수


위의 np.pad() 함수의 경우 변경하기 전의 원래 배열이 (m by n) 형상인 고정된 차원의 배열을 대상으로 채워넣기를 하였습니다. 두번째로 소개하려는 keras의 sequence.pad_sequence() 함수는 각 행의 원소 개수가 다른 Ragged array(?) 를 대상으로 특정 (j by k) 형상의 고정된 배열로 바꾸려고 할 때 빈 자리를 '0'으로 채워넣는데 사용할 수 있는 차이가 있습니다.


아래의 예를 보면 원소 개수가 1개, 2개, 3개, 4개로서 들쭉날쭉함을 알 수 있습니다. (list를 원소로 가지고 있고, data type 이 object 이네요.)



x2 = np.array([[1], [2, 3], [4, 5, 6], [7, 8, 9, 10]])
display(x2)


array([list([1]), 
      list([2, 3]),
      list([4, 5, 6]),
      list([7, 8, 9, 10])], dtype=object)

 



TensorFlow와 Keras의 tf.keras.preprocessing.sequence() 메소드를 importing 해보겠습니다.



import tensorflow as tf
from tensorflow.keras.preprocessing import sequence

print(tf.__version__)

2.3.0

 



이제 Keras의 pad_sequences() 함수를 사용하여 가장 많은 원소를 가진 행에 맞추어서 (4 by 4) 형상의 배열로 바꾸고, 왼쪽의 빈자리는 '0'으로 채워넣기(padding)를 해보겠습니다.

tf.keras.preprocessing.sequence.pad_sequences(
    sequences, maxlen=None, dtype='int32',
    padding='pre', truncating='pre',
    value=0.0
)

이렇게 (4 by 4) 형상으로 해서 빈자리는 '0'으로 채워주고 나니 각 행의 원소 개수가 모두 4개로서 배열다운 배열이 되었습니다. padding을 해주는 위치의 기본 설정값은 padding='pre' 로서 앞쪽(왼쪽)에 '0'을 채워줍니다. (value=0 이 기본 설정값으로서 '0' 값으로 채워줌)


sequence.pad_sequences(x2) # default: padding='pre', value=0


array([[ 0,  0,  0,  1],
       [ 0,  0,  2,  3],
       [ 0,  4,  5,  6],
       [ 7,  8,  9, 10]], dtype=int32)

 



만약 padding을 해주는 위치를 뒤쪽(오른쪽)으로 하고 싶다면 padding='post' 로 매개변수 설정을 바꿔주면 됩니다.


sequence.pad_sequences(x2, padding='post')


array([[ 1,  0,  0,  0],
       [ 2,  3,  0,  0],
       [ 4,  5,  6,  0],
       [ 7,  8,  9, 10]], dtype=int32)

 



물론 빈 곳 채워넣기(padding)하는 값을 '0'이 아니라 다른 값으로 할 수도 있습니다. '-1'을 사용(value=-1)해서 앞쪽에 빈 곳을 채워넣기해보겠습니다.



sequence.pad_sequences(x2, padding='pre', value=-1)


array([[-1, -1, -1,  1],
       [-1, -1,  2,  3],
       [-1,  4,  5,  6],
       [ 7,  8,  9, 10]], dtype=int32)

 



maxlen 매개변수값을 별도로 설정해주지 않으면 배열 내 행 중에서 가장 많은 원소를 가진 행을 기준으로 maxlen 이 자동으로 정해지는데요, 이를 사용자가 직접 설정해줄 수도 있습니다. 아래의 예에서 maxlen=5 로 설정해주면 (4 by 5) 의 padding 된 배열이 생성됩니다.


sequence.pad_sequences(x2, padding='pre', value=0, maxlen=5)


array([[ 0,  0,  0,  0,  1],
       [ 0,  0,  0,  2,  3],
       [ 0,  0,  4,  5,  6],
       [ 0,  7,  8,  9, 10]], dtype=int32)




아래 예에서처럼 원래의 배열 x2 의 최대길이는 4인데 maxlen=3 으로 값을 설정하게 되면 4-3=1 개의 길이만큼의 원소 값들을 잘라내기(truncating) 해주어야 합니다. 이때 truncating='post' 라고 설정해주면 뒤쪽(오른쪽)을 기준으로 '1'개의 값들을 잘라내주고, 앞쪽을 기준으로 비어있는 곳에는 '0'의 값을 채워주게 됩니다.


sequence.pad_sequences(x2, padding='pre', value=0, maxlen=3, truncating='post')


array([[0, 0, 1],
       [0, 2, 3],
       [4, 5, 6],
       [7, 8, 9]], dtype=int32)



* Reference: https://www.tensorflow.org/api_docs/python/tf/keras/preprocessing/sequence/pad_sequences

많은 도움이 되었기를 바랍니다.
이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)

반응형
Posted by Rfriend

댓글을 달아 주세요

  1. 열공중입니다 2022.05.09 15:37  댓글주소  수정/삭제  댓글쓰기

    안녕하세요 선생님! 보면서 공부중인 학생입니다.
    제가 csv파일이나 txt 파일 여러개의 행의 길이를 0으로 채워서 맞추고싶은데
    열의 길이는 일정한데 행의 길이가 각각 다른 파일들이라.. 패딩을 해야하는걸 알게되었는데
    어떤 방식으로 하는게 좋을까요?

    • Rfriend 2022.05.09 15:38 신고  댓글주소  수정/삭제

      안녕하세요. 간단한 인풋, 아웃풋 예시를 댓글로 남겨주시면 제가 정확하게 이해하는데 도움이 되겠습니다.

    • 익명 2022.05.09 21:35  댓글주소  수정/삭제

      비밀댓글입니다

    • 열공중입니다 2022.05.11 20:05  댓글주소  수정/삭제

      많이 어려울까요...?

    • Rfriend 2022.05.11 23:17 신고  댓글주소  수정/삭제

      제가 지금 해외출장 중인데요, 내일 오후에 시간이 좀 날거 같아요. 내일 오후에 코드 테스트해보고 올릴께요.

    • Rfriend 2022.05.12 15:30 신고  댓글주소  수정/삭제

      답변 많이 기다리셨을텐데 늦어져서 죄송합니다. 해외출장 중인데 시간 내기가 쉽지가 않네요.
      아래 코드 참고하시구요.

      import numpy as np

      ## sample NDarray
      a1 = np.arange(8).reshape(2, 4)
      a2 = np.arange(12).reshape(3, 4)
      a3 = np.arange(16).reshape(4, 4)
      a4 = np.arange(20).reshape(5, 4)


      ## get the maximum number of rows
      row_cnt = []
      for a in [a1, a2, a3, a4]:
      row_cnt.append(a.shape[0])

      row_cnt_max = max(row_cnt)
      col_cnt = a1.shape[1]


      ## padding with the maximum num. of rows
      def pad_max_row(arr, row_cnt_max, col_cnt):

      mask_arr = np.zeros((row_cnt_max, col_cnt))
      mask_arr[:arr.shape[0], :arr.shape[1]] = arr
      return mask_arr


      ## run UDF
      a1 = pad_max_row(a1, row_cnt_max, col_cnt)
      a2 = pad_max_row(a2, row_cnt_max, col_cnt)
      a3 = pad_max_row(a3, row_cnt_max, col_cnt)
      a4 = pad_max_row(a4, row_cnt_max, col_cnt)


      print(a1)
      #[[0. 1. 2. 3.]
      #[4. 5. 6. 7.]
      # [0. 0. 0. 0.]
      # [0. 0. 0. 0.]
      # [0. 0. 0. 0.]]