본문 바로가기
Python/Deep Learning

[DL]_텐서 + 텐서 = ????????? - 브로드캐스팅

by ssolLEE 2023. 8. 31.
반응형

  • 딥러닝에 대해서 공부할 때에는 다음의 책과 함께 했습니다. 챕터 1에서는 인공지능과 머신러닝, 그리고 딥러닝에 대한 자세한 설명과 흐름을 얘기해주어 비교적 친숙하게 학습을 시작할 수 있었습니다. 감사합니다!

 

저번 포스팅에서 행렬의 형태를 가진 텐서에 대해서 알아보았습니다.

행렬로 연산할 수 있는 것처럼, 우리는 텐서에 적용되는 연산을 오늘 함께 실습해보겠습니다.

쉽게 말하면 덧셈, 뺄셈을 해본다는 것입니다.  

그럼 시작해볼까요? 

 

원소별 연산

  • 원소별 연산을 구현해보겠습니다. 
  • relu 연산입니다. 
    • relu 함수는 입력이 0보다 크면 입력을 그대로 반환하고, 0보다 작으면 0을 반환합니다. 
    • relu(x) = max(x, 0)
# relu 연산
def naive_relu(x):
    assert len(x.shape) == 2  # x는 랭크-2 numpy 배열이다.
    x = x.copy()    # 입력 텐서 자체를 바꾸지 않도록 복사
    for i in range(x.shape[0]):
        for j in range(x.shape[1]):
            x[i, j] = max(x[i, j], 0)
    return X
  • 덧셈입니다. 이와 같은 원리로 뺄셈, 곱셈도 가능합니다. 
# 덧셈
def naive_add(x, y):
    assert len(x.shape) == 2    # x와 y는 랭크-2 numpy 배열이다.
    assert x.shape == y.shape
    x = x.copy()
    for i in range(x.shape[0]):
        for j in range(x.shape[1]):
            x[i, j] += y[i, j]
    return x
  • numpy 내장 함수로 위와 같은 연산을 빠르게 처리할 수도 있습니다. 
  • 우리가 위에서 만든 함수와 내장 함수의 속도 차이를 비교해봅시다. 
  • 아래와 같이 실행을 하면 [[걸린 시간: 1.88 s]]이라고 출력됩니다. 
import numpy as np
import time

t0 = time.time()
for _ in range(1000):
    z = naive_add(x, y)
    z = naive_relu(z)
print("걸린 시간: {0:.2f} s".format(time.time() - t0))
  • 아래와 같이 내장 함수로 실행을 하면 [[걸린 시간: 0.01 s]]이라고 출력됩니다. 훨씬 빠릅니다.
import numpy as np
import time

x = np.random.random((20, 100))
y = np.random.random((20, 100))

t0 = time.time()

for _ in range(1000):
    z = x + y   # 원소별 덧셈
    z = np.maximum(x, 0.)   # 원소별 relu함수
print("걸린 시간: {0:.2f} s".format(time.time() - t0))

 

브로드캐스팅

  • 위에서 우리가 구현한 naive_add는 assert했듯이 동일한 크기의 랭크-2 텐서만 지원합니다. 
  • 크기가 다른 두 텐서를 더할 수도 있을까요? 
  • 모호하지 않고 실행 가능하다면 작은 텐서가 큰 텐서의 크기에 맞추어 브로드캐스팅됩니다. 
  • 브로드캐스팅은 다음과 같이 설명할 수 있습니다. (https://numpy.org/doc/stable/user/basics.broadcasting.html)
    • 크기가 다른 a, b가 있습니다. 스칼라 형태의 b가 a의 크기만큼 늘어나면서 곱셈이 이루어지고 값이 출력됩니다.
a = np.array([1.0, 2.0, 3.0])
b = 2.0
a * b

>>> array([2.,  4.,  6.])

  • 아래 그림과 같이 축을 늘리는 것도 가능합니다. 

  • 행렬은 두 행렬이 같은 꼴일 때(또는 위처럼 같은 꼴이 될 수 있을 때) 덧셈, 뺄셈이 가능합니다. 
  • 같은 꼴이라는 것은 [행렬 a의 행의 수 = 행렬 b의 행의 수], [행렬 a의 열의 수 = 행렬 b의 열의 수]라는 것입니다. 
  • 아래 그림이 안되는 이유는 행렬 a의 열의 수와 행렬 b의 열의 수가 맞지 않으므로 브로드캐스팅도 되지 않는 것입니다.

  • 브로드캐스팅은 다음과 같은 두 단계로 이루어집니다. 
    • 큰 텐서의 ndim에 맞게 작은 텐서의 축이 추가됩니다.
    • 작은 텐서가 새 축을 따라서 큰 텐서의 크기에 맞도록 반복됩니다. 
  • 다음과 같이 크기가 다른 X와 y를 만듭니다. 
import numpy as np
X = np.random.random((32, 10))  # 크기가 (32, 10)인 랜덤한 행렬
y = np.random.random((10,))  # 크기가 (10,)인 랜덤한 행렬
  • y를 단계적으로 stretch하여 Y를 만듭니다. 
y = np.expand_dims(y, axis=0)   # y의 크기는 (1, 10)
Y = np.concatenate([y] * 32, axis = 0)  # 축 0(행)을 따라 32번 반복 => (32, 10)이 됨
  • 이제 둘의 덧셈이 가능합니다. 

출력물의 일부이다.

  • 다음과 같이 단순하게 구현할 수도 있습니다. 
def naive_add_matrix_and_vector(x, y):
    assert len(x.shape) == 2
    assert len(y.shape) == 1
    assert x.shape[1] == y.shape[0]
    x = x.copy()
    for i in range(x.shape[0]):
        for j in range(x.shape[1]):
            x[i, j] += y[j]
    return x
  • 다음은 크기가 다른 두 텐서에 브로드캐스팅으로 원소별 maximum 연산을 적용하는 예입니다. 
import numpy as np

x = np.random.random((64, 3, 32, 10))
y = np.random.random((32, 10))
z = np.maximum(x, y)    # 출력z크기는 x와 동일하게 (64, 3, 32, 10)입니다. 
z.shape

>>> (64, 3, 32, 10)

 

오늘 브로드캐스팅이라는 중요한 개념을 학습했습니다. 

다음 딥러닝 포스팅에선 텐서 곱셈과 크기 변환을 알아보겠습니다. 

오늘도 감사합니다.