본문 바로가기
Study 공부할레나

[스터디/밑시딥3] 제 4고지 신경망 만들기_40. 브로드캐스트 함수

by LENA-cfg 2023. 10. 9.

40.1 broadcast_to 함수와 sum_to 함수(넘파이 버전)

np.broadcast_to(x, shape)는 ndarray 인스턴스인 x의 원소를 복제하여 shape 인수로 지정한 형상이 되도록 해줍니다.

broadcast_to 함수의 역전파의 입력 x의 형상과 같아지도록 기울기의 합을 구해줍니다.

sum_to(x, shape)로 해당 부분을 구현해주면 되고 이 함수는 x의 원소의 합을 구해 shape 형상으로 만들어주는 함수 입니다. 

 

넘파이에서는 이런함수가 없고 DeZero에서 sum_to 함수를 만들어줍니다.

def sum_to(x, shape):
    """Sum elements along axes to output an array of a given shape.

    Args:
        x (ndarray): Input array.
        shape:

    Returns:
        ndarray: Output array of the shape.
    """
    ndim = len(shape)
    lead = x.ndim - ndim
    lead_axis = tuple(range(lead))

    axis = tuple([i + lead for i, sx in enumerate(shape) if sx == 1])
    y = x.sum(lead_axis + axis, keepdims=True)
    if lead > 0:
        y = y.squeeze(lead_axis)
    return y

 

 

위의 코드는 주어진 배열의 원소를 특정 축(axes)을 따라 합하여 주어진 shape를 가진 배열을 반환하는 기능을 수행합니다.

x는 입력배열, shape는 출력 배열입니다. 

lead는 입력 배열과 출력 배열간의 차원의 차이입니다.

lead_axis는 차원의 차이의 갯수를 축으로 튜플 타입으로 담은 것입니다.

 

axis = tuple([i + lead for i, sx in enumerate(shape) if sx == 1])

여기가 제일 이해가 안가는 부분이였는데 그래도 이해한 부분까지 설명하자면,

출력 배열에서 1인 부분은 해당 차원에서 모든 원소를 합산하는 것을 의미합니다.

합산해야할 축들의 인덱스를 결정합니다.

 

합산해야할 축을 결정했으니

x.sum(lead_axis + axis, keepdims=True)로 리드축과 axis에서 원소를 합산해서 새로운 배열 y를 만들어줍니다.

 

그 이후 lead가 0보다 크면 squeeze 함수를 사용해서 불필요한 차원을 제거합니다. 

 

최종적으로 합산된 배열 y를 반환합니다.

 

실제 사용하는데 예시코드는 아래에 있습니다.

import numpy as np
from dezero.utils import sum_to 

x = np.array([[1,2,3],[4,5,6]])
y = sum_to(x, (1,3))
print(y)

y = sum_to(x, (2,1))
print(y)

x는 2행 3열이기때문에 shape는 (2,3)입니다.

이 x를 1행 3열로 만들어 주는게 첫번째 y의 코드입니다. 

 

2행을 하나의 행으로 만들어줄때 각 위치에 있는 원소를 더하고 하나의 차원을 뺴줘서 y는 [[5, 7, 9]]가 됩니다. 

 

두번째 y는 2행은 그대로이고 열에 있는 원소를 하나로 만들어줍니다. 열을 하나로 만들어주려면 각 행자리에 있는 원소들을 더해줍니다. [[1+2+3], [4+5+6]]으로 해주면 두번째 y의 결과는 [[6],[15]]가 됩니다.

위의 그림은 sum_to 함수의 역전파는 broadcast_to함수를 사용하여 입력  ㅌ의 형상과 같아지도록 기울기의 원소를 복제합니다. 

 

40.2 broadcast_to 함수와 sum_to 함수(DeZero 버전)

class BroadcastTo(Function):
    def __init__(self, shape):
        self.shape = shape

    def forward(self, x):
        self.x_shape = x.shape
        xp = dezero.cuda.get_array_module(x)
        y = xp.broadcast_to(x, self.shape)
        return y

    def backward(self, gy):
        gx = sum_to(gy, self.x_shape)
        return gx


def broadcast_to(x, shape):
    if x.shape == shape:
        return as_variable(x)
    return BroadcastTo(shape)(x)
    
class SumTo(Function):
    def __init__(self, shape):
        self.shape = shape

    def forward(self, x):
        self.x_shape = x.shape
        y = utils.sum_to(x, self.shape)
        return y

    def backward(self, gy):
        gx = broadcast_to(gy, self.x_shape)
        return gx


def sum_to(x, shape):
    if x.shape == shape:
        return as_variable(x)
    return SumTo(shape)(x)

위의 코드에서 역전파 부분을 잘 보면되는데 

BroadcastTo 백워드에서 gy를 인풋 쉐이프로 형상을 일치 시키고 

SumTo의 백워드에서는 입력 x와 쉐이프가 같아지게 기울기 원소를 복제합니다.

 

이부분이 이해하기 너무 복잡한데 아래의그림을 다시 보면

sum_to는 자리값을 더해서 쉐이프를 변경해주면 브로드캐스트는 아웃풋 쉐잎대로 그대로 늘려주는 거라고 이해하면 좋을 것 같습니다.

40.3 브로드캐스트 대응

브로드캐스트란 형상이 다른 다차원 배열끼리의 연산을 가능하게 하는 넘파이 기능입니다. 

sum_to를 구현한 이유는 넘파이 브로드 캐스트에 대응하기 위해서입니다.

 

현재, DeZero에서 브로드캐스트의 역전파가 일어나지 않기 때문에 이 부분을 수정해줘야합니다. 

class Add(Function):
    def forward(self, x0, x1):
        self.x0_shape, self.x1_shape = x0.shape, x1.shape
        y = x0 + x1
        return y

    def backward(self, gy):
        gx0, gx1 = gy, gy
        if self.x0_shape != self.x1_shape:  # for broadcaset
            gx0 = dezero.functions.sum_to(gx0, self.x0_shape)
            gx1 = dezero.functions.sum_to(gx1, self.x1_shape)
        return gx0, gx1

순전파 때 브로드캐스트가 일어난다면 입력되는 x0과 x1의 형상이 다를 것이고 두 형상이 다르면 브로드캐스트용 역전파를 계산하는 것입니다. 

기울기 gx0을 x0의 형상이 되도록 합을 구하고 기울기 gx1은 x1의 형상이 되도록 합을 구합니다. 

 

위의 코드가 잘 적용됬는지 확인하는 코드는 아래에 있습니다.

if '__file__' in globals():
    import os, sys
    sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
import numpy as np
from dezero import Variable

x0 = Variable(np.array([1, 2, 3]))
x1 = Variable(np.array([10]))
y = x0 + x1
print(y)

y.backward()
print(x1.grad)