무효 클릭 IP 추적 중...
머신러닝,딥러닝/넘파이,numpy

[넘파이 기초] vstack | hstack | concatenate | dstack | stack 마스터

꼬예 2021. 5. 13.

오늘은 array들을 합치는데 사용되는 다양한 api에 대해 알아보겠습니다. 

 

 

 

vstack vs hstack

1차원 벡터끼리의 결합

import numpy as np

a = np.random.randint(0, 10, (4,)) # 1차 벡터
b = np.random.randint(0, 10, (4,)) # 1차 벡터

print(f"a: {a.shape}\n{a}")
print(f"a: {b.shape}\n{b}\n")

vstack = np.vstack([a, b]) 
hstack = np.hstack([a, b]) 

print(f"vstack: {vstack.shape}\n{vstack}")
print(f"hstack: {hstack.shape}\n{hstack}\n")

 

output :

a, b는 각각 4개의 원소를 가진 1차원 벡터 입니다.

우리는 여기서 vstack과 hstack를 이용해 두 배열을 합치려고 합니다. 

 

여기서 vstackvertical stack수직방향으로 쌓는다는 의미이고, hstackhorizontal stack수평방향으로 쌓는 다는 의미입니다.

 

사용방법은 vstack/hstack 인자로 리스트튜플과 함께 값을 넣어주면됩니다. 여기선 리스트안에 각각의 벡터를 a , b 변수로 받아 집어 넣었습니다. 

 

참고사항으로 수학에서는 벡터를 column 벡터를 기본으로 간주하지만 데이터사이언스에는 row벡터를 기본으로 설정합니다. 

 

이러한 이유로 넘파이에선 기본적으로 1차원벡터를 row 벡터라고 가정을 하지요.

 

즉 vstack을 이용해 수직으로 쌓는다면 오른쪽이 아닌 왼쪽같이 쌓인다는 말이죠.

마찬가지로 hstack 을 이용하면  오른쪽이 아닌 왼쪽 처럼 쌓아 나가게 됩니다. 

2차원 벡터끼리의 결합

import numpy as np

a = np.random.randint(0, 10, (1,4)) # row 벡터
b = np.random.randint(0, 10, (1,4)) # row 벡터

print(f"a: {a.shape}\n{a}")
print(f"a: {b.shape}\n{b}\n")

vstack = np.vstack((a, b)) # 튜플 사용
hstack = np.hstack((a, b)) 

print(f"vstack: {vstack.shape}\n{vstack}")
print(f"hstack: {hstack.shape}\n{hstack}\n") 

output :

2차원 벡터터부터는 shap을 통해 row, 또는column 벡터를 명확하게 지정해줄 수 있습니다.

a, b 각각 (1,4)의 shape인 row 벡터로 만들었습니다.

 

앞선 1차원 벡터끼리의 결합과 차원은 다르지만 같은 row 벡터임으로 합칠때도 같은 모양으로 작동합니다.

(다만 차원이 하나 높아졌기때문에 대괄호 하나가 더 추가된것을 확인할 수 있습니다.)

 

 

이번엔 컬럼 벡터를 만들어볼까요?

import numpy as np

a = np.random.randint(0, 10, (4,1)) # column 벡터
b = np.random.randint(0, 10, (4,1)) # column 벡터

print(f"a: {a.shape}\n{a}")
print(f"a: {b.shape}\n{b}\n")

vstack = np.vstack((a, b)) 
hstack = np.hstack((a, b)) 

print(f"vstack: {vstack.shape}\n{vstack}")
print(f"hstack: {hstack.shape}\n{hstack}\n") 
#

output :

output을 보면 어떤식으로 기능하는지 아실 수 있을 겁니다.

 

1차원 벡터와 2차원 행렬 결합(서로다른 차원의 결합)

 import numpy as np

a = np.random.randint(0, 10, (3,4)) # matrix
b = np.random.randint(0, 10, (4,)) # row 벡터

print(f"a: {a.shape}\n{a}")
print(f"a: {b.shape}\n{b}\n")

vstack = np.vstack([a, b]) 

print(f"vstack: {vstack.shape}\n{vstack}")

output :

 

(3,4)의 matrix와 (4,) row벡터와 결합입니다. 즉 둘 사이의 차원이 다른 경우입니다.

이때는 vstack만 사용 가능합니다. 

앞서 배운대로 1차원 벡터는 row벡터를 가정합니다. 

그렇기 때문에 hstack 실행할경우 아래와 같은 이상한 모습이 되죠.

과감하게 합쳐보시면.. ValueError(all the input arrays must have same number of dimensions...)를 만나게 됩니다. 

다음 코드는 올바르게 실행이 될까요?

 import numpy as np

a = np.random.randint(0, 10, (3,4)) # matrix
b = np.random.randint(0, 10, (3,)) # row 벡터

print(f"a: {a.shape}\n{a}")
print(f"a: {b.shape}\n{b}\n")

vstack = np.hstack([a, b]) 

print(f"vstack: {vstack.shape}\n{vstack}")

output :

역시나 ValueError 가 발생합니다. 왜냐하면 앞서 말씀드렸듯이 b가 row 벡터이기 때문에 아래와 같이 이상한 모양으로 합쳐지게되기 때문입니다.  

a 의 row 열과 b의 배열의 수를 같게 했다고해서.. 넘파이에서 알아서 연산을 하지 않는다는 것을 확인했습니다. 

 

그러면 어떻게해야할까요?

reshape을 통해 직접 row벡터를  column 벡터로 변경을 해주면됩니다. 

 import numpy as np

a = np.random.randint(0, 10, (3,4)) # matrix
b = np.random.randint(0, 10, (3,)) # row 벡터

print(f"a: {a.shape}\n{a}")
print(f"a: {b.shape}\n{b}\n")

hstack = np.hstack([a, b.reshape(-1,1)]) # 컬럼 벡터로 변경


print(f"hstack: {hstack.shape}\n{hstack}\n")

output :

for loop을 이용한 배열 쌓기

 

vstack과 hstack은 for문과도 함께 자주 쓰이는데요! 

쓰임새를 같이 확인해보겠습니다. 

 

import numpy as np 

dataset = np.empty((0, 4))
print(f"initial shape: {dataset.shape}\n")

for iter in range(5):
    data_sample = np.random.uniform(0, 5, (1, 4))
    dataset = np.vstack((dataset, data_sample))
    print(f"iter/shape: {iter}/{dataset.shape}")

output :

 

우선 for문을 이용할때는 vstack 안에 들어가는 인자 모습이 조금 다른걸 알 수 있습니다. 

이전에는 리스트나, 튜플을 적고 그안에 배열들을 적었었죠?

하지만 이번 예시에서는 첫번째 인자로 빈 array(empty 이용) , 그 다음 인자에는 그 빈 array에 넣을 값을 의미 합니다. 

 

빈 array는  empty를 이용해서 만드는데요, row를 0으로 설정했다는 의미는 row방향으로 쌓겠다는 의미를 나타냅니다. column 에 4는 크기 4로 고정하겠다는 의미구요.

 

즉 정리하면 0은 고정된 숫자가 아니라 우리가 for문을 통해 바꿀 숫자입니다.

 

코드를 보시면 vstack를 통해 row방향으로 하나씩 쌓아가면서 shape이 어떻게 변해가는지 충분히 이해하실 수 있을 겁니다. 

 

그렇다면 hstack은 어떻게 쓸까요?

import numpy as np 

dataset = np.empty((4, 0))
print(f"initial shape: {dataset.shape}\n")

for iter in range(5):
    data_sample = np.random.uniform(0, 5, (4, 1))
    dataset = np.hstack((dataset, data_sample))
    print(f"iter/shape: {iter}/{dataset.shape}")
    

output :

이번에는 empty를 (4,0) 즉 row를 4 열로 고정하고 column을 0으로 설정함으로써 column방향으로 쌓겠다는것을 의미합니다. 

 

마찬가지로 hstack를 통해 shape이 변해가는모습을 보면 충분히 이해하실 수 있을 겁니다. 

 

하지만 이와 같은 방식은 대량의 데이터를 다를 경우 연산 속도가 느려 효율성이 떨어지는 경향이 있습니다. 

 

그렇다면 어떻게 작성하는것이 더 효율적인 방법일까요? 

아래 코드를 보시죠!

 

import numpy as np

dataset_tmp = list()
for iter in range(100):
    data_sample = np.random.uniform(0, 5, (1, 4))
    dataset_tmp.append(data_sample)
    
dataset = np.vstack(dataset_tmp)
print(f"final shape: {dataset.shape}")

output :

우선 앞선 예와 다르고 empty가 아닌 그냥 빈 리스트를 만듭니다. 

여기서는 dataset_tmp라는 빈리스트를 만들었습니다. 

 

그리고 앞서와 마찬가지로 for문을 도는데요.

1바퀴 돌때마다 append를 통해 array들을 쌓아 나갑니다.

 

for문이 끝나면 리스트안에 array들이 쌓여있는 상태가 됩니다. 

이 모습 뭔가 익숙하지 않으신가요?

 

네 맞습니다. 제일 처음에 봤던 형태 vstack/hstack 안에 리스트를 넣고 그안에 배열을 넣은 형태입니다.

for문을 이용은 했지만 제일 처음봤던 방법으로 모든 값들을 합치는 것이지요.

 

이번 예에서는 vstack 을 통해 수직방향으로 쌓았습니다.  

 

 

 

concatenate

영어로 연결시키다라는 뜻을 가진 concatenate는 그 뜻대로 합치는 기능을 합니다.

앞서 배운 vstack과 hstack보다 좀 더 general한 api로 봐주시면 되는데요!

코드를 통해 확인해 보겠습니다. 

import numpy as np

a = np.random.randint(0, 10, (3,))
b = np.random.randint(0, 10, (4,))

concat = np.concatenate([a,b])
concat0 = np.concatenate([a, b], axis = 0)

print(f"a: {a.shape}\n {a}")
print(f"a: {b.shape}\n {b}\n")

print(f"concat.shape: {concat.shape}\n {concat}")
print(f"concat0.shape: {concat0.shape}\n {concat0}\n")

 

output :

사용법은 vstack, hstack과 마찬가지로 인자에 리스트 형태로 배열의 뭉치를 넣습니다.

차이점이 있다면 vstack, hstack처럼 합치는 방향이 이미 정해진것이 아니라 axis 를 통해 합칠 방향을 직접 정해줄 수 있다는 것입니다. 

(axis 개념이 낯설거나 어려우신분들은 이전 포스팅 axis 편을 참고해주시기 바랍니다.)

 

a, b는 1차원 벡터로  axis 가 하나(여기선 axis = 0, column 방향)밖에 없으니 사실상 axis가 의미가 없습니다.

(axis를 지정하지 않을 경우 디폴트로 가장 바깥차원의 방향을 의미하는 axis = 0 이 설정됩니다. )

1차원 벡터는 concat 을 하면 그냥 오른쪽으로 붙힌다라고 생각하시면 됩니다.

 

위코드에서 디폴트 설정과 axis=0이 같은 값으로 출력되는것을 확인할 수 있습니다.

 

 

다음은 2차원 행렬끼리 연산을 보겠습니다.

import numpy as np

a = np.random.randint(0, 10, (1,3))
b = np.random.randint(0, 10, (1,3)) # row 벡터/ 위에서는 벡터 ndarray였다면 여기선 행렬 ndarray


axis0 = np.concatenate([a,b], axis = 0) # row 방향  # >> 여기선 vertical stack과 동일하게 작용
axis1 = np.concatenate([a, b], axis = 1) # column 방향 # >> 여기선 horizontal stack과 동일하게 작용
axis_n1 = np.concatenate([a, b], axis = -1) # 1이랑 똑같은거임

print(f"a: {a.shape}\n {a}")
print(f"b: {b.shape}\n {b}\n")

print(f"axis0.shape: {axis0.shape}\n {axis0}")
print(f"axis1.shape: {axis1.shape}\n {axis1}")
print(f"axis_n1.shape: {axis_n1.shape}\n {axis_n1}")

output :

2차원이기 때문에 axis 가 두개(axis = 0, axis =1) 이 사용 가능합니다. 

 

여기선 a, b가 2차원 row 벡터 형태인데요.

output을 보시면 aixs 방향에 따라 어떻게 합쳐지는지 감이 오실 겁니다.  

 

참고로 axis =1이 가장 안쪽 차원 즉 가장 마지막 차원이기 때문에 axis = -1 로도 대체 가능하다는 점도 확인하시기 바랍니다:)

 

shape가 서로 다른 행렬의 연산

이번에 shape이 다를때는 어떤것을 조심해야 하는지 확인해 보겠습니다.

import numpy as np

a = np.random.randint(0, 10, (3,4))
b = np.random.randint(0, 10, (3,2))

concat = np.concatenate([a,b], axis = 1) # column 방향으로 합치겠다는것.

print(f"a: {a.shape}\n {a}")
print(f"b: {b.shape}\n {b}\n")

print(f"concat.shape: {concat.shape}\n {concat}")

output :

 

shapq이 다를경우 어떤 axis 는 동작을 안하기 때문에 각 배열의 shape을 잘 확인해야 합니다. 

 

a, b의 shape을 먼저 확인해 봅시다.

a는 (3,4) b는 (3,2)로 column 의 shape이 다릅니다. 

 

예시 코드에서는 axis =1 즉 column 방향으로 연산을 실행했는데요. 아무 문제없이 작동합니다.

 

하지만 axis = 0 방향으로 했을때는 오류가 납니다.  왜냐하면 아래와같이 빈곳(?)이 생기기 때문이죠.

 

이번엔 3차원 텐서끼리의 연산(shape이 다를 경우)을 보겠습니다. 

3차원이기 때문에 axis는 3개(채널방향, row방향, column 방향)가 있겠죠 .

 

아래와 같은 (4,4,5) (5,4,5) 2개 텐서를 연산을 하려고 합니다.

shape 형태를 보니 채널 의shape만 다르고 나머지는 다 같은 형태입니다. 

 

이때 가능한 연산방향은 아래 그림과 같은 채널방향입니다. 

 

코드를 통해 확인해 보겠습니다. 

import numpy as np

a = np.random.randint(0, 10, (4, 4, 5))

b = np.random.randint(0, 10, (5, 4, 5)) 

concat0 = np.concatenate([a, b], axis = 0)
print(f"concat0.shape: {concat0.shape}")

output :

채널 방향으로 연산(axis = 0)을 하다보니 채널의 shape이 합쳐져서 9가 된것을 볼수가있네요

 

혹시 이해가 안되시는 분들을 위해 row 방향(axis = 1)으로 하면 어떻게 될까요? 그림은 아래와 같습니다. 

채널 갯수가 다르다 보니 파란색 행렬 뒤가 좀 휑한게(?) 보이시죠? 짝이 맞지 않습니다.

 

아래는 axis =1로 계산한 코드입니다.

import numpy as np

a = np.random.randint(0, 10, (3, 4, 5))

b = np.random.randint(0, 10, (5, 4, 5)) 

concat0 = np.concatenate([a, b], axis = 1) # row방향으로 했을때는 그림을 보면 파란색이 초록색 에 비해 차원갯수가 하나가 적어서 row #방향으로 했을때 연산이 실패한다.
print(f"concat0.shape: {concat0.shape}")

output :

all the input array dimensions for the concatenation axis must match exatly... 라는 ValueError가 발생합니다.

 

그럼 확인사살을 위해 제일 안쪽차원인 column 방향(-1 or 2) 어떨까요?

보시는것처럼 파란색 채널수가 부족하니 짝이 맞지 않습니다. 

mport numpy as np

a = np.random.randint(0, 10, (3, 4, 5))

b = np.random.randint(0, 10, (5, 4, 5)) 

concat0 = np.concatenate([a, b], axis = -1)
print(f"concat0.shape: {concat0.shape}")

output :

이 또한 같은 ValueError가 발생합니다. 

 

앞에서는 채널이 다를 경우 어떤 axis 방향의 연산이 가능한지 알아보았는데요.

이번에는 row만 다른 경우를 알아보겠습니다.  

정답부터 말하자면 row가 다른 텐서에서는 row 방향 즉 axis = 1 연산만 가능합니다. 

import numpy as np

a = np.random.randint(0, 10, (4, 4, 5))

b = np.random.randint(0, 10, (4, 5, 5)) 

concat0 = np.concatenate([a, b], axis = 1)
print(f"concat0.shape: {concat0.shape}")

output :

 

row만 다른경우는 axis = 0, axis = 2에서는 왜 안되는지 설명하진 않겠습니다. 

앞서 배운대로 머리에 그림을 떠올려보시면 충분히 아실 수 있을 겁니다.

 

 

이번에는 마지막으로 column shape만 다른 경우입니다.

정답부터 말하면 column 방향 즉 axis = -1 or 2 만 가능합니다. 

import numpy as np

a = np.random.randint(0, 10, (4, 4, 5))

b = np.random.randint(0, 10, (4, 4, 6)) 

concat0 = np.concatenate([a, b], axis = -1)
print(f"concat0.shape: {concat0.shape}")

output :

차원이 더커지게 되면 헷갈릴 수 있지만 여기서 우리는 한가지 규칙을 알 수 있습니다.

(4,4,5) (4,4,6) column만 다를때 -> column방향 axis = 2 or -1
(4,4,5) (5,4,5) 채널만 다를때 -> 채널방향 axis = 0
(4,4,5) (4,5,5) row만 다를때 -> row 방향 aixs = 1

 

np.dstack (demension stack)

이어서 배워볼 api는 demension stack 이라 불리는 dstack 입니다.

이름에서 추론할수있듯이 차원방향 즉 채널 방향으로 합치는 것입니다. 

 

이 api 특이한점은 2차원같이 행 과 열밖에 없는 shape의 배열들끼리 합칠때도 

채널을 기여코(?) 만들어 채널 연산을 수행한다는 것인데요..

 

바로 코드를 통해 작동 원리를 알아보겠습니다. 

import numpy as np

R = np.random.randint(0, 10, (100, 200))
G = np.random.randint(0, 10, size=R.shape)
B = np.random.randint(0, 10, size=R.shape)

image = np.dstack([R, G, B])

print(image.shape)

(100,200)의 shape을 가진 R, G, B  행렬을 만듭니다. 

이전에 배웠던 방식처럼 dstack에도 인자로 리스트와 각각의 행렬들을 집어넣는데요 어떤 결과가 나올까요?

 

output :

분명 2차원끼리 합쳤는데 차원이 하나가 추가된것을 알수가 있습니다. 

차원이 하나가 추가되긴했는데 (3, 100, 200) 이 아니라 (100, 200, 3) 안쪽 차원 방향으로 추가가 되었네요..

앞서 말씀드린대로 차원끼리 더한걸 알 수있습니다. 

 

이번엔 3차원 배열들 끼리의 연산입니다. 

 

import numpy as np

a = np.random.randint(0, 10, (100, 200, 3))
b = np.random.randint(0, 10, size=a.shape)
c = np.random.randint(0, 10, size=a.shape)

d = np.dstack([a, b, c])
print(d.shape)

 

output : 

3차원에는 차원 방향으로 연산을할때 굳이 새로운 차원을 만들어 줄 필요가 없으니 차원 변화 없이 제일 안쪽 차원끼리 더하는 것을 알 수 있습니다. 

 

마지막으로 그럼 1차원 벡터들끼리는 여떻게 연산이 될까요?

import numpy as np

a = np.random.randint(0, 10, (100,))
b = np.random.randint(0, 10, size=R.shape)
c = np.random.randint(0, 10, size=R.shape)

d = np.dstack([a, b, c])
print(d.shape)

1차원은 차원 방향으로 더하기에는 애초에 row 방향 밖에 없는데 어떻게 계산이 될까요?

output : 

제일 바깥쪽 차원에 1을 만들어 size를 유지한다음 기어코(?) 제일 안쪽 차원 방향으로 합하는 것을 알 수가 있습니다. 

 

stack

stack은 dstack과 비슷한것같으면 서도 다른데요. 더 general 하게 사용할수있는 api라고 보실 수 있습니다. 

우선 코드를 통해 어떻게 작동하는지 알아볼게요

 

import numpy as np

a = np.random.randint(0, 10, (100, 200))
b = np.random.randint(0, 10, size=a.shape)
c = np.random.randint(0, 10, size=a.shape)

d = np.stack([a, b, c])
print(d.shape)

output : 

차원이 새롭게 생기는데 여기선 제일 바깥쪽 차원이 생긴것을 알 수있습니다. 

뭔가 dstack 은 제일 안쪽 차원을 더했다면, stack은 제일 바깥차원끼리 더하는건가? 라고 의문이 들기 시작할겁니다. 

이번엔 이어서 3차원 배열의 연산입니다.

a = np.random.randint(0, 10, (100, 200, 300))
b = np.random.randint(0, 10, size=a.shape)
c = np.random.randint(0, 10, size=a.shape)

d = np.stack([a, b, c])
print(d.shape)

output : 

dstack과 같은 원리라면 (300, 200,300) 이 되어야 하는데 또 차원이 추가되네요.

여기서 알아야할것은 stack 은 무조건 한차원을 추가한다는 겁니다. 

 

이때 중요한 것은 axis 를 통해 어떤 부분에 차원을 더할건지 커스터마이징이 가능하다는것인데요.

디폴트값으로 axis = 0 입니다. 그래서 앞선 예에서 가장 바깥쪽 차원방향으로 차원이 더해진 것이구요.

 

이번엔 axis를 바꿔가며 어떠한 연산을 하는지 자세히 알아보겠습니다. 

 

1. axis = 0

a = np.random.randint(0, 10, (100, 200, 300))
b = np.random.randint(0, 10, size=a.shape)
c = np.random.randint(0, 10, size=a.shape)

d = np.stack([a, b, c], axis = 0)
print(d.shape)

output : 

앞서 디폴트로 지정했을때와 같은 값이죠?

제일 바깥 쪽 차원에 새로운 차원값이 추가되었습니다. 

 

2. axis = 1

a = np.random.randint(0, 10, (100, 200, 300))
b = np.random.randint(0, 10, size=a.shape)
c = np.random.randint(0, 10, size=a.shape)

d = np.stack([a, b, c], axis = 1)
print(d.shape)

output : 

axis =1 로 하자 그다음 차원에서 새로운 차원을 추가하네요

 

3. axis = 2

a = np.random.randint(0, 10, (100, 200, 300))
b = np.random.randint(0, 10, size=a.shape)
c = np.random.randint(0, 10, size=a.shape)

d = np.stack([a, b, c], axis = 2)
print(d.shape)

output : 

 

이제 어느정도 잠이 잡히셨죠?

 

 

정리

일반적으로 hstack, vstack , dstack은 2차원 행렬에 자주 사용되고,

concatenate, stack은 더 고차원에서 자주 사용됩니다.

 

 

 

이 글과 읽으면 좋은글

  • 트위터 공유하기
  • 페이스북 공유하기
  • 카카오톡 공유하기
이 컨텐츠가 마음에 드셨다면 커피 한잔(후원) ☕

댓글