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

[딥러닝 기초] Artifitial Neuron (with numpy)

꼬예 2022. 3. 23.

딥러닝하면 아래와 같이 복잡한 그림들을 떠올릴수 있을텐데

(동그라미들을 우리는 뉴런이라고 부르고 뉴런들이 모여있는 각 열들을 layer라고 부른다.(초록색 동그라미들은 데이터이기때문에 제외)

 

이러한 복잡한 연산을 이해하기위해선 가장 기본 단위인 뉴런에서 어떻게 연산과 동작이 일어나는지 이해해야 합니다

이를 위해 가장 단순한 하나의 뉴런을 관찰하면서 조금씩 복잡한 연산과정을 이해해보도록 하겠습니다.

 

 

1. 스칼라 연산

1) 수학적 정의

input data(x)가 뉴런($\nu$)를 통과하여 연산 후 출력값(a)를 내뱉는 간단한 형태입니다.

이 뉴런 내부에서는 그림과같이 두개의 연산을 하는데 ,

$f(x;w,b)$는 affine연산

$g(z)$는 activation연산이라 합니다.

x가 xw+b라는 함수를 통과하고 그 출력된 값이 다시 activation함수(여기선 sigmoid함수)를 통과하여 a라는 최종 출력값을 내뱉는 형태라고 보면 됩니다.

(참고로 activation함수는 sigmoid외에 다양한 함수가 많으니 해당 링크를 참조하길 바랍니다.)

 

2) 파이썬 코드

위에서 배웠던 수식을 실제 파이썬 코드로 옮겨보도록 하겠습니다.

x = 3
w, b = 5, -1
z = x*w + b
a = 1/(1+np.exp(-z))

사실 위와 같이 단순하게 코드를 작성할 수도 있지만, $f(x;w,b)$ 는 parametric function으로서 w,b라는 파라미터를 내재하고있는 함수입니다.

예를 들어 w=2, b=3 라고 정의된 $f(x;w,b)$는 $z = x \cdot2+3$ 입니다.

그런데 단순히 xw,b를 같은 선상에서 바라보는건 해당함수의 정확한 뉘앙스를 담았다고 보기 어렵습니다.

이를 위해 우리는 파이썬 클래스를 이용해 코드를 작성해보도록 하겠습니다.

 

(1) parameter 정의

class ArtificialNeuron:
  def __init__(self, w, b):
    self.w = w
    self.b = b

파라미터값을 뉴런이 내재하고있다는걸 가장 잘 내포하는것은 클래스 init함수입니다.

 

왜냐하면 아래와 같이 클래스를 정의하기만 했는데도 init함수가 자동적으로 실행되고, 실제 전달받은 객체를 통해 해당 파라미터의 접근이 가능하기 때문입니다.

nu1 = ArtificialNeuron(w=5, b=-1)
print(nu1.w, nu1.b)

output:
5 -1

 

하지만 위코드에는 조금 아쉬운점이 있습니다.

클래스를 정의할때 특정 w,b에 값을 할당해줘야 하기 때문입니다.

다시말해 값을 할당 하지 않을시 자체적으로 랜덤하게 초기값을 설정하는 기능을 추가하고 싶다면,

아래와 같은 방법은 코드를 작성해야합니다.

 

class ArtificialNeuron:
  def __init__(self, w=None, b=None): # None을 디폴트로 설정
    self.w = w
    self.b = b

    self._init_params()

  def _init_params(self):
    if self.w == None:
      self.w = np.random.normal(0, 1, ())
    if self.b == None:
      self.b = np.random.normal(0, 1, ())

참고) _(언더스코어)가 있는 함수는 사용자를 위한 method가 아니다, 달리 말하면 _가 없는건사용자를 위한 method,_가 있는것은클래스내에서 내부적으로 동작에 필요한 함수라는 것을 의미한다. c나, c+ 처럼 private 란 개념은 없지만 파이썬에서는 이런식으로 private를 간접적으로 표현해준다.

 

변화된 부분부터 하나씩 살펴보면 디폴트로 None이 설정되어있습니다.

그 이유는 디폴트값을 설정하지 않은채로 인자없이 코드를 실행하면 아래와 TypeError가 발생하기 때문입니다.

우리가 임의로 w, b를 넣을 수 도있고 , 안넣고 랜덤하게 지정하기 위한 방법으로 디폴트 값을 설정한 것입니다.

 

다음으로 self._init_params 함수 안을 뜯어보면

None일 경우 즉 파라미터를 지정하지 않았을경우 랜덤한 값을 파라미터로 지정하는 코드가 작성되어있습니다. (numpy 해당 코드 모르시는 분들은 해당 링크 참조)

그리고 이 함수를 init함수 내부에서 실행시켰음으로, 클래스가 정의되는시기에 파라미터가 지정되는 효과를 얻을 수 있습니다.

 

(2) affine 연산

class ArtificialNeuron:
  def __init__(self, w=None, b=None): # None을 디폴트로 설정
    self.w = w
    self.b = b

    self._init_params()

  def _init_params(self):
    if self.w == None:
      self.w = np.random.normal(0, 1, ())
    if self.b == None:
      self.b = np.random.normal(0, 1, ())

	def _affine(self, x): # 사용자를 위한 method X
	    z = x * self.w + self.b
	      return z

_affine 함수를 추가해줌으로써 x*w+b를 코드를 구현했습니다.

 

 

(3) activation 연산

class ArtificialNeuron:
  def __init__(self, w=None, b=None): # None을 디폴트로 설정
    self.w = w
    self.b = b

    self._init_params()

  def _init_params(self):
    if self.w == None:
      self.w = np.random.normal(0, 1, ())
    if self.b == None:
      self.b = np.random.normal(0, 1, ())

	def forward(self, x): # 사용자를 위한 method
      z = self._affine(x)
      a = self._sigmoid(z)
      return a

	def _affine(self, x): # 사용자를 위한 method X
	    z = x * self.w + self.b
      return z
	
	def _sigmoid(self, z):
      a = 1 / (1 + np.exp(-z))
      return a

_sigmoid 함수는 activation 함수중 sigmoid 함수 연산을 위한 코드이고,

forward함수는 affine연산과 activation연산을 한번에 동작시키는 함수라고 보면 됩니다.

 

실제로 우리가 작성한 코드가 잘 작동하는지 확인하기 위해서는 아래와 같이 코드를 작성해주면됩니다.

nu = ArtificialNeuron() # w,b를 설정하지 않았음으로 랜덤한 초기값으로 파라미터가 설정됨
x = np.random.normal(0, 5, ()) # 평균 0, 표준편차 5인 값들중 랜덤한 값을 x로 지정
nu_a = nu.forward(x) # 뉴런 연산 실행

 

 

2. 벡터 연산

지금 까지는 입력받은 input 데이터 x가 스칼라 값($x \in \mathbb{R}$)이 였다면 이번에는 x가 벡터값($\vec{x} \in \mathbb{R}^{l_I \times 1}$) 이 들어온다면 어떻게 연산이 될까요?

참고사항 :

벡터는 기본적으로 column vector로 가정하겠습니다.

$I_I$는 length of input으로 인풋값의 길이를 의미하는 notation입니다.

 

1) 수학적 정의

 

앞서 스칼라값에서 정의한 형태와 큰 흐름은 같지만 내부적으로 파라미터의 차원이 변경됩니다.

x벡터는 당연히 바뀌었고, x벡터 shape에 따라 w의 shape도 따라 변경됩니다.

 

이 말이 무슨말이냐하면, 벡터에서 Affine 연산은 아래와 같이 정의됩니다.

위와 같이 내적을 하기위해선 서로 shape가 맞아야 합니다.

예를 들어 x벡터의 차원이 $(\vec{x})^T \in \mathbb{R}^{1\times3}$ 이라면 w벡터는 내적 연산이 가능하도록 $\vec{w} \in \mathbb{R}^{3\times1}$이 되어야 합니다.

벡터 연산에서 주의할점은 연산을 하면서 차원이 어떻게 변하는지 추적해야 합니다.

input데이터는 벡터이지만 z값(affine 연산 후 값)은 $z \in \mathbb{R}$ 이고 당연히 a(activation 연산 후 값)도 $a \in \mathbb{R}$로 둘다 스칼라값이라는 겁니다.

 

2) 파이썬 코드

앞서 만들어 두었던 코드를 벡터에도 적용시키려면 _affine함수를 수정해야 합니다.

@는 행렬연산을 수행 하기 위한 연산자이고, 연산을 위해서 shape을 맞춰주기 위해 x값을 Transpose 시켜주어야 합니다.

def _affine(self, x): # 사용자를 위한 method X
	    z = x.T @ self.w + self.b
      return z

다음으로는 들어오는 input 값에 따라 w의 shape이 달라져야하므로 init함수내에 있는 파라미터값이 유동적으로 달라질 수있는 코드로 변경해줘야 합니다.

 

x의 shape을 알기 위해서는 x가 최초 들어가는 함수인 forward함수에서 input값의 shape을 추출해야 합니다.

class ArtificialNeuron:
    def __init__(self, w=None, b=None):
                                
        self.w = w
        self.b = b
		
			#  self._init_params() <-- 이함수는 forward함수 안으로 들어감
    def _init_params(self):
      self.w = np.random.normal(0, 1, (self.n_feature,1)) # forward함수에서 얻은 값 사용
      self.b = np.random.normal(0, 1, ())
		
		def forward(self, x): 
	      self.n_feature = X.shape[0] # affine연산에서 Transpose되기 때문에 
	      self._init_params()         # 맞춰야 하는 shape은 0번째 인덱스이다.
																			# shape추출 후 _init_parmas 실행
	      z = self._affine(x)
	      a = self._sigmoid(z)
	      return a
	
    
    def _affine(self, X): 
        Z = X.T @ self.w + self.b
        return z

하지만 위 코드는 치명적인 문제가 하나있는데 forward를 할때마다 새로운 파라미터로 초기화 된다는점입니다.

이게 왜 문제가 되냐 하면 backpropagation을 통해 파라미터가 수정되고 최적의 파라미터로 학습되어 가야 하는데, forward를 할때마다 학습한 보람도 없이 랜덤한 파라미터로 계속 변경 되기 때문입니다.

 

이 문제를 해결하기 위해서는 _init_params 함수가 최초 한번만 실행 될수있도록 코드를 짜야 합니다.

 

class ArtificialNeuron:
    def __init__(self, w=None, b=None):
                                
        self.w = w
        self.b = b
				self.IS_INIT_PARAM_FLAG = False

    def _init_params(self):
	      self.w = np.random.normal(0, 1, (self.n_feature,1))
	      self.b = np.random.normal(0, 1, ())
				self.IS_INIT_PARAM_FLAG = True
		
		def forward(self, x): 
				if self.IS_INIT_PARAM_FLAG = False
	        self.n_feature = X.shape[0] 
	        self._init_params()         
																			
	      z = self._affine(x)
	      a = self._sigmoid(z)
	      return a
	
    
    def _affine(self, X): 
        z = X.T @ self.w + self.b
        return z

IS_INIT_PARAM_FLAG를 지정하고 init_params 함수가 실행될때 해당 값을 True로 바꿔줌으로써

forward 단에서 무분별하게 파라미터 초기화가 되는걸 방지해줍니다.

 

3. 행렬 연산

이번에는 input 값을 행렬이 들어올 경우에 대해 알아보도록 하겠습니다.

다시 말해 $l_I$개의 feature 가진 벡터데이터가 N개가 들어올 경우를 가정해본다는 것입니다.

 

1) 수학적 정의

기존과 달라진점이 있다면 x → X 로 행렬을 나타내는 문자로 바뀌었고, a → $\vec{a}$ 로 벡터형태로 바뀌었습니다.

우리가 여기서 주의해야할점은 shape이 어떻게 변화되었는가 입니다.

최종 출력값인 a의 shape은 (N,1)입니다. feature 의 차원이 1로 바뀌었고, row는 입력데이터의 갯수인 N으로 변경 되었습니다.

 

내부적인 연산 방법은 차이가 없습니다. $(\vec{x})^T \in \mathbb{R}^{1\times l_I}$ 인 벡터가 N개 들어와서 행렬의 형태가 된것이지, 내부적인 계산 방법은 벡터 연산일때와 같습니다.

2) 파이썬 코드

행렬에서의 파이썬 코드도 벡터에서의 파이썬코드와 다른점이 없습니다.

다만 여기서는 batch라는 개념을 추가해 코드로 구현해보도록 하겠습니다.

 

데이터를 학습할시 큰데이터를 한번에 학습할시 메모리 감당하지 못하는경우 많기 때문에 큰 데이터를 batch 씩 나누어 학습 시키는 방법입니다.

n_data = 100
batch_size = 8
n_batch = n_data // batch_size
n_feature = 20

data_X = np.random.normal(0, 1, (n_feature, n_data))

model = ArtificialNeuron()

for batch_idx in range(n_batch):
    batch_X = data_X[:,
              batch_idx * batch_size : (batch_idx + 1) * batch_size]

    pred = model.forward(batch_X)
    print(batch_X.shape, pred.shape)

output:
(20, 8) (8,1)
(20, 8) (8,1)
(20, 8) (8,1)
(20, 8) (8,1)
(20, 8) (8,1)
(20, 8) (8,1)
(20, 8) (8,1)
(20, 8) (8,1)
(20, 8) (8,1)
(20, 8) (8,1)
(20, 8) (8,1)
(20, 8) (8,1)

1.위 코드에서는 전체 데이터의 갯수(N)을 100개, 각 데이터의 feature( $l_I$)는 20으로 지정하였습니다.

2.지정한 값을 기준으로 data_X라는 변수명으로 X 데이터를 형성합니다.

3.각각의 batch의 크기를 8로 지정합니다.X.T @ W + B.T

 

우리는 for loop을 통해 batch마다 연산을 실행할 것이기 때문에 몇번 연산 할지 먼저 정해줘야 합니다.

이를 위해 전체 n_data 를 batch 사이즈로 나눈 몫을 구합니다.

 

몫을 이용할경우에는 나머지가 떨어지지 않으면 나머지만큼 갯수는 무시된다는 문제가 있지만 여기서는 일단 그부분은 무시하고 지나가겠습니다.

 

그 후 위와 같이 코드를 짜주면 매번 for loop을 돌때마다 batch_X 크기인 8개 뽑아서 연산을 수행하는 걸 알 수 있습니다.

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

댓글