딥러닝은 보통 "인간의 뇌를 모방하다"고들 한다.
그 모방에서 기초적으로 사용되는 개념이 뉴런이다.
오늘은 그 뉴런의 역할을 하는 Dense와 뉴런 신경망인 Dense Layer 에 대해 알아보도록하겠다.
이 포스팅의 목표는 Dense layer를 이해하고, 관련한 코드를 분석하고, Class화까지 진행해보는 것이다.
1. 뉴런, 퍼셉트론
뉴런의 동작 과정을 간단히 보면,
감각기각으로부터의 Input -> 뉴런 -> output 이다.
여기서 우리 뇌라는 기관에서 바라보면,
감각기각으로부터의 Input -> 뉴런 -> 뉴런 -> 뉴런 -> 뉴런 -> 뉴런 -> 뉴런 ->output
과 같은 식으로 뉴런이 체인처럼 연결되서 output을 가질 것이다.
* 사실 시냅스 등 구체적인 내용들이 있지만, 생략ㅠ
그림으로 보면,
위 그림처럼 뉴런이 망처럼 펼쳐져서 신경망을 이루고, 신호를 보내 임의의 output를 가져올 것이다.
이러한 뉴런의 신경망을 딥러닝에서는 어떻게 컴퓨터로 가져왔을까?
이때, 먼저 알아야할 것이 "퍼셉트론(Perceptron)"이다.
퍼셉트론은 하나의 신경세포 뉴런의 동작을 본딴 개념이다.
위의 그림에서 뉴런 하나의 동작을 좀 더 자세히 그려보자
우리가 input으로 하나의 이미지를 바라보았다고 가정해보자.
수많은 픽셀이 있고, 수많은 뉴런이 있겠으나,
먼저 1픽셀의 데이터와 하나의 인공뉴런의 동작 과정을 바라보자
이미지의 하나의 픽셀에는 R, G, B인 3개의 값을 담겨 있을 것이다.
이 R, G, B 값이 하나의 인공뉴런을 만나 R, G, B값이 각자 인공뉴런에게 들어간다.
그때, 인공뉴런은 R, G, B값에 대해서 각각 가중치를 가지고 있다.
가중치이 존재하는 이유는 실제 뇌에서 뉴런이 이런 방식으로 동작한다고 한다. 어떤 똑똑한 사람이 이러한 방식의 학습 규칙을 제안했고, 이 개념을 "퍼셉트론"이라고 이름지었다. 일단 그냥 받아들이도록 하자.
그러면, 각각의 가중치와 R, G, B값이 벡터와 벡터가 되기에, 벡터간의 연산이 Dot Product가 진행된다. 그리고, 이후 활성화함수를 거치게 된다(활성화함수는 추후에 더 자세히 포스팅하겠다). 그러면 하나의 인공뉴런에서는 한 픽셀에 대해 하나의 스칼라 값을 가지게 된다.
2. 뉴런 신경망
* 뉴런 스펠링 오타. 그림도 다시 그려야해서 일단은 그대로 패스하겠습니다.
- 하나의 데이터와 복수의 인공뉴런
위 챕터를 이해했다면, 우리는 1픽셀의 데이터와 복수의 인공뉴런의 동작 과정도 이해할 수 있다.
관련한 이론을 실제 코드를 이용해 구현해 보았다.
인공 뉴런을 3개로 가정함
import numpy as np
width = 300
height = 300
image_data = np.random.randint(0, 256, (width, height, 3)) # 이미지의 각각의 픽셀의 R,G,B값
w = np.random.randint(0, 256)
h = np.random.randint(0, 256)
rgb = image_data[w, h] # 임의의 픽셀의 RGB값
# print(rgb.shape) # (3, )
# nureon1_weightes = np.random.randint(0, 100, (rgb.shape, ))
nureon1_weightes = np.random.randint(0, 100, (3, )) # RGB값에 대한 뉴런1의 가중치
nureon2_weightes = np.random.randint(0, 100, (3, )) # RGB값에 대한 뉴런2의 가중치
nureon3_weightes = np.random.randint(0, 100, (3, )) # RGB값에 대한 뉴런3의 가중치
y1 = np.dot(rgb, nureon1_weightes)
y2 = np.dot(rgb, nureon2_weightes)
y3 = np.dot(rgb, nureon3_weightes)
# 이해를 위해 정수를 사용했기에
# 활성화함수는 일단 생략
print(y1) # 16337
print(y2) # 29984
print(y3) # 27301
위 코드를 보면, 임의의 픽셀의 RGB값(shape: (3, ))과 뉴런들(각각 (3, ) (3,) (3, ))이 Dot Product를 거쳐 3개의 스칼라값을 결과로 가져왔다.
활성화 함수 활용을 위해 데이터 값을 바꾸고, 인공 뉴런 역시 n개로 늘려보겠다.
import numpy as np
n_data = 100 # 데이터의 개수
n_data_feature = 5 # 각각의 데이터의 모양의 개수 / 하나의 픽셀의 RGB값이라면, 3
n_nureon = 10 # 뉴런의 개수
data = np.random.uniform(0, 1, (n_data, n_data_feature)) # (100, 5)
nureons_weightes = np.random.uniform(0, 1, (n_nureon, n_data_feature)) # (10, 5) # 뉴런의 개수, 데이터의 모양의 개수
# y = ..?
위의 코드를 보고, data와 nureons_wegithes가 어떻게 배열의 형태로 담겨있는지 이해할 수 있나?
그림으로 살펴보고, 어떻게 연산해야할지 생각해보자
.
위 그림과 같이 이해해봤다. data가 있고, 데이터를 받아들일 뉴런들의 가중치값이 있을 때,
np.matmul(data, nureons_weightes.T)으로 연산을 함으로써,
(100, 10)이 shape인 행렬을 만들 수 있겠다.
이때의 행렬은 "행"을 기준으로 데이터, "열"을 기준으로 뉴런을 의미한다는 것 역시 알 수 있었다.
활성화함수를 추가해 실제 코드 구현 해보면,
import numpy as np
n_data = 100 # 데이터의 개수
n_data_feature = 5 # 각각의 데이터의 모양의 개수 / 하나의 픽셀의 RGB값이라면, 3
n_nureon = 10 # 뉴런의 개수
data = np.random.uniform(0, 1, (n_data, n_data_feature)) # (100, 5)
nureons_weightes = np.random.uniform(0, 1, (n_nureon, n_data_feature)) # (10, 5) # 뉴런의 개수, 데이터의 모양의 개수
Z = np.matmul(data, nureons_weightes.T)
# print(Z.shape) # (100, 10)
print(Z[13, 8]) # 1.4047738718879816
# 활성화함수 / sigmoid 활용
A = 1/(1 + np.exp(-Z))
# print(A.shape) # (100, 10)
print(A[13, 8]) # 0.8029403372499935
실제 위 코드와 같이 구현될 수 있으며,
예시와 같이 만약 Z[13, 8]이라면, 데이터13과 뉴런8의 Dot Product 값을 의미하게 된다.
그리고 Z의 연산의 경우, nureons_weightes.T가 불필요하다고 느낀다면
import numpy as np
n_data = 100 # 데이터의 개수
n_data_feature = 5 # 각각의 데이터의 모양의 개수 / 하나의 픽셀의 RGB값이라면, 3
n_nureon = 10 # 뉴런의 개수
data = np.random.uniform(0, 1, (n_data, n_data_feature)) # (100, 5)
nureons_weightes = np.random.uniform(0, 1, (n_nureon, n_data_feature)) # (10, 5)
Z = np.matmul(data, nureons_weightes.T)
data = np.random.uniform(0, 1, (n_data, n_data_feature)) # (100, 5)
nureons_weightes = np.random.uniform(0, 1, (n_data_feature, n_nureon)) # (5, 10)
Z = np.matmul(data, nureons_weightes)
위 코드에서 Z의 두번째 연산 방법과 같이 nereons_weightes를 만들어도 좋을 듯하다.
수학적 표현으로도 두번째 방법이 옳다고 생각해 앞으로 두번째 방법을 사용하겠다.
여기까지 이해했다면, 우리는 뉴런 신경망, 인공 신경망의 하나의 Dense layer가 연산되는 과정을 이해한 것이다. 그리고 인공 신경망은 여러 층, 다층으로 구성돼있다.
- 하나의 데이터와 복수의 인공뉴런 그리고 다층 layer
우리가 지금까지 하나의 뉴런층이 연산하는 과정을 이해했다면, 이번엔 그 뉴런층이 다층, 복수인 경우에 대해 공부해보려한다.
그림으로 표현하면
복수의 뉴런의 집합을 하나의 레이어로 보고, 이 레이어들이 쌓여서 다층을 이룬다.
이 흐름을 확장시키고 확장시키다보면, 인간의 뇌처럼 "신경망"이 만들어질 것이다.
관련해 코드 작성 전에 생각을 정리해보자.
레이어가 다층이 되었을때, 이전 레이어에서의 output이 다음 레이어 input으로 들어가려면,
이전 레이어의 뉴런이 기준이기때문에 shape이 위와 같이 구성돼야한다.
그런데, 위에서 구현해본 Z는
import numpy as np
n_data = 100 # 데이터의 개수
n_data_feature = 5 # 각각의 데이터의 모양의 개수 / 하나의 픽셀의 RGB값이라면, 3
n_nureon = 10 # 뉴런의 개수
data = np.random.uniform(0, 1, (n_data, n_data_feature)) # (100, 5)
nureons_weightes = np.random.uniform(0, 1, (n_data_feature, n_nureon)) # (5, 10)
Z = np.matmul(data, nureons_weightes)
print(Z.shape) # (100, 10) / (n_data, n_nureon)
shape이 (100, 10)으로 (데이터의 개수, 뉴런의 개수)이다.
그렇다면, 다음 layer의 입력이 되려면 transpose을 활용해야겠다.
import numpy as np
# layer 1
n_data = 100 # 데이터의 개수
n_data_feature = 5 # 각각의 데이터의 모양의 개수 / 하나의 픽셀의 RGB값이라면, 3
n_layer1_nureon = 10 # 첫번째 레이어의 뉴런의 개수
data = np.random.uniform(0, 1, (n_data, n_data_feature)) # (100, 5)
layer1_nureons_weightes = np.random.uniform(0, 1, (n_data_feature, n_layer1_nureon)) # (5, 10)
Z1 = np.matmul(data, layer1_nureons_weightes).T
A1 = 1/(1 + np.exp(-Z1))
# print(A1.shape) # (10, 100) / (n_layer1_nureon, n_data)
# layer 2
n_layer2_nureon = 7
layer2_nureons_weightes = np.random.uniform(0, 1, (A1.shape[1], n_layer2_nureon)) # (n_data, n_layer2_nureon)
Z2 = np.matmul(A1, layer2_nureons_weightes).T
A2 = 1/(1 + np.exp(-Z2))
# print(A2.shape) # (7, 10) / (n_layer2_nureon, n_layer1_nureon)
# layer 3
n_layer3_nureon = 30
layer3_nureons_weightes = np.random.uniform(0, 1, (A2.shape[1], n_layer3_nureon)) # (n_layer1_nureon, n_layer3_nureon)
Z3 = np.matmul(A2, layer3_nureons_weightes).T
A3 = 1/(1 + np.exp(-Z3))
print(A3.shape) # (30, 7) / (n_layer3_nureon, n_layer2_nureon)
관련한 이해를 바탕으로 코드를 작성해봤다.
그림으로 이해해보자면
내가 이해한 내용들로 설명해보자면, 위의 그림과 같다.
다층 레이어의 경우에는 "이전 레이어의 뉴런의 개수"가 현재 레이어의 뉴런의 가중치의 shape을 결정한다는 점을 이해할 수 있었다.
생각해보면 서로 연결돼있는 체인, 망 형태이기에 서로에게 영향을 준다는 말은 당연하다.
하지만, 우리는 정확한 연산 및 동작과정을 이해하는 것이 중요하기에, 실제로 코드로 구현해보고 실제 동작과정을 그려보면서 이해하는 것이 굉장히 필요하다.
좀 더 전문적인 내용은 추후 포스팅에서 다루겠다.(이런 말할때마다 매번 부끄럽)
* 뉴런 스펠링 오타. 그림도 다시 그려야해서 일단은 그대로 패스하겠습니다.
3. 관련 코드 분석
아래 코드는 현재 듣고 있는 머신러닝, 딥러닝 강의에서 가져온 코드이다.
import numpy as np
# first layer
N, n_feature, n_neuron = 16, 5, 4
X = np.random.normal(0, 1, (n_feature, N)) # (5, 16)
W = np.random.normal(0, 1, (n_feature, n_neuron)) # (5, 4)
B = np.random.normal(0, 1, (n_neuron, 1)) # (4, 1)
# print(X.shape, W.shape, B.shape)
Z = (X.T @ W + B.T).T
print(Z.shape) # (4, 16)
A = 1/(1 + np.exp(-Z))
# second layer
n_neuron2 = 30
W2 = np.random.normal(0, 1, (n_neuron, n_neuron2)) # (4, 30)
B2 = np.random.normal(0, 1, (n_neuron2, 1)) # (30, 1)
# print(A.shape, W2.shape, B2.shape)
Z2 = (A.T @ W2 + B2.T).T
print(Z2.shape) # (30, 16)
A2 = 1/(1 + np.exp(-Z2))
X, W, B들의 shape이 살짝 다를뿐, 내가 위에서 공부해본 내용과 비슷하다.
하지만, 데이터 사이언스나 딥러닝의 레퍼던스, 논문 등에서 일반적인 형식으로 가정하고 사용하는 구조이기때문에, 하나하나 뜯어 분석해볼 필요가 있다.
3.1
import numpy as np
# first layer
N, n_feature, n_neuron = 16, 5, 4 # 데이터의 개수, 데이터의 feature의 개수, 뉴런의 개수
먼저 첫번째 layer의 시작이다.
주석에 작성한 것처럼 위 코드에서는 N(데이터의 개수), n_feature(데이터의 feature의 개수), n_neuron(첫번째 layer의 뉴런의 개수)를 지정했다.
3.2
X = np.random.normal(0, 1, (n_feature, N)) # (5, 16)
다음 코드는 임의의 데이터 X(평균:0, 표준편차:1, shape:(n_feature, N))를 추출한다.
Latex로 X 행렬을 그려보고, 관련해 행과 열이 어떤 의미가 있는지 확인해봤다.
확인해보면, 열기준이 data 하나하나를 나타낸다.
그래서 아래의 연산 코드에서 X.T가 있나보다
3.3
W = np.random.normal(0, 1, (n_feature, n_neuron)) # (5, 4)
다음 코드는 첫번째 레이어의 뉴런의 가중치들(shape: (n_feature, n_neuron))을 추출한다.
* 평균, 표준편차 생략
W는 열기준이 뉴런의 가중치(neuron weight)이다.
아래의 코드인, X.T @ W가 데이터와 가중치의 Dot Product가 이루어짐을 이해할 수 있겠다.
3.4
B = np.random.normal(0, 1, (n_neuron, 1)) # (4, 1)
B와 같은 경우에는 가중치와 함께 각각의 뉴런이 가지는 상수이다.
아래의 코드인, X.T @ W + B.T에서 B.T은
의 형태이고, 행렬의 덧셈연산이 진행될 것이다.
3.5
Z = (X.T @ W + B.T).T
한 레이어의 "input"과 "neuron weight"의 연산이다. 관련해서 그림으로 그려보자
(X.T @ W + B.T)는
위 그림같이 연산이 진행되고, shape은 (16, 4)가 되고
(X.T @ W + B.T).T이 되면, shape이 (4, 16)이 된다.
여기서 shape은 (4, 16) = (n_neuron, N) = (뉴런의 개수, 데이터의 개수)이다.
3.6
A = 1/(1 + np.exp(-Z))
활성화함수(Activation Function)이다.
sigmoid 함수가 활성화함수로 사용돼, 0와 1, 성공과 실패 등 데이터를 두개의 그룹으로 분류하는데 활용된 것이다.
이 활성화 함수까지 지난 "A"는 첫번째 layer output이 된다.
3.7
# second layer
n_neuron2 = 30
W2 = np.random.normal(0, 1, (n_neuron, n_neuron2)) # (4, 30)
B2 = np.random.normal(0, 1, (n_neuron2, 1)) # (30, 1)
# print(A.shape, W2.shape, B2.shape)
Z2 = (A.T @ W2 + B2.T).T
print(Z2.shape) # (30, 16)
A2 = 1/(1 + np.exp(-Z2))
그 다음은 두번째 레이어 코드이다.
n_neuron2 : 두번째 레이어의 뉴런의 개수
W2 : 두번째 레이어의 가중치들
B2 : 두번째 레이어의 상수들
Z2 : 두번째 레이어의 뉴런과 이전 레이어의 output의 연산
A2 : Z2가 활성화함수 sigmoid 함수를 거친 결과, 두번째 레이어의 output
이런 식으로 정리된다.
여기서 중요하게 살펴야할 점은 아래 코드이다.
W2 = np.random.normal(0, 1, (n_neuron, n_neuron2)) # (4, 30)
두번째 레이어부터 가중치를 추출할때, 행이 이전 레이어의 뉴런의 개수가 된다.
포스팅 위쪽에서도 확인했지만, data의 feature가 여기선, 이전 레이어의 뉴런이 된다.
이 개념을 이해하고 있으면, 두세개의 레이어 연결 뿐만아니라 수천개, 수만개의 레이어 연결도 어렵지않게 이해할 수 있을 것이다.
전문적인 지식이 부족해 전문적인 키워드 활용이 부족한 점이 너무 아쉽다.
일단은 포스팅의 목적이 강의 내용들을 스스로 이해하는데 있기때문에, 이 포스팅을 시작으로, 추후 성장해 수준높고 전문적인 포스팅을 할 수 있기를 희망해보장.
4. Class화
여기까지해서 뉴런, 퍼셉트론, 뉴런 Layer, 뉴런 Layer 내부 연산 및 동작과정, 다층 뉴런 Layer들의 연산 및 동작과정에 대해 살펴보았다. 그러면 이 공부를 정리하고, 마무리하는 Class화를 진행해보겠다.
import numpy as np
# Neuron Layer Class
class Layer:
def __init__(self, n_neuron, activation='sigmoid'):
# 뉴런의 개수
self.n_neuron = n_neuron
# 활성화 함수
self.activation = activation
# 뉴런의 가중치
self.W = list()
# 뉴런의 상수
self.B = list()
def _init_params(self, x):
n_feature = x.shape[0]
# W의 shape: (n_feature, self.n_neuron) / (이전 레이어의 feature, 현재 뉴런의 개수)
self.W = np.random.uniform(0, 1, (n_feature, self.n_neuron))
# B의 shape: (뉴런의 개수, 1)
self.B = np.random.uniform(0, 1, (self.n_neuron, 1))
def __call__(self, x):
if(len(self.W) == 0 or len(self.B) == 0):
self._init_params(x)
# x.T.shape : (이전 레이어의 n_feature, 이전 레이어의 n_neuron)이던 x transpose
# -> (n_previous_neuron, n_feature)
# W.shape : (n_feature, n_current_neuron)
# B.T.shape : (1, n_current_neuron)
Z = (x.T @ self.W + self.B.T) # Z.shape: (n_previous_neuron, n_current_neuron)
# 다음 layer에서 n_previous_neuron은 x.T.shape의 n_feature가 된다.
A = self._activating(Z)
return A
def _activating(self, Z):
# 활성화 함수 조건들
if self.activation == 'sigmoid':
return 1/(1+np.exp(-Z))
elif self.activation == 'tanh':
return (np.exp(Z) - np.exp(-Z)) / (np.exp(Z) + np.exp(-Z))
elif self.activation == 'relu':
return np.maximum(0, Z)
# 다층 Layer Class
class Model:
def __init__(self, n_layer, n_layers_neuron) -> None:
self.n_layer = n_layer # layer의 개수
self.n_layers_neuron = n_layers_neuron # layer의 뉴런의 개수
# 다층의 layer를 담는다
self.layers = list()
self._make_layer()
def _make_layer(self):
for layer_idx in range(self.n_layer):
n_layer_neuron = self.n_layers_neuron[layer_idx]
layer = Layer(n_neuron=n_layer_neuron, activation='tanh')
self.layers.append(layer)
def __call__(self, X):
# 최초 input data X
A = X
for layer_idx in range(self.n_layer):
layer = self.layers[layer_idx]
A = layer(A)
print(f"layer{layer_idx} output shape: {A.shape}")
# 모든 layer 통과 후
# shape이 (n_last_layer_neuron, n_previous_layer_neuron)인
# output으로 종료
return A.T
n_layer = 3
n_layers_neuron = np.random.randint(1, 100+1, (n_layer, ))
model = Model(n_layer=n_layer, n_layers_neuron=n_layers_neuron)
n_data, n_data_feature = 10, 5
X = np.random.uniform(0, 1, (n_data_feature, n_data))
print(f"X.shape: {X.shape}, n_layers_neuron: {n_layers_neuron}")
result = model(X=X)
print(f"result shape: {result.shape}")
* 참조
- https://blog.naver.com/samsjang/220948258166
[2편] 퍼셉트론(Perceptron) - 인공신경망의 기초개념
퍼셉트론(Perceptron) 인공지능(AI)은 우리 사람의 뇌를 흉내내는 인공신경망과 다양한 머신러닝 알고리...
blog.naver.com
- https://m.blog.naver.com/samsjang/221030487369
[34편] 딥러닝의 기초 - 다층 퍼셉트론(Multi-Layer Perceptron; MLP)
[33편]까지 머신러닝의 기초적인 내용에 대해 거의 모두 다루었으므로, 이번 포스팅부터는 요즘 핫하게 뜨...
blog.naver.com
댓글