인공 신경망(ANN; Artificial Neural Network)

인공 신경망이 뭐지?

인공신경망(人工神經網, 영어: artificial neural network, ANN)은 기계학습과 인지과학에서 생물학의 신경망(동물의 중추신경계중 특히 뇌)에서 영감을 얻은 통계학적 학습 알고리즘이다. 인공신경망은 시냅스의 결합으로 네트워크를 형성한 인공 뉴런(노드)이 학습을 통해 시냅스의 결합 세기를 변화시켜, 문제 해결 능력을 가지는 모델 전반을 가리킨다. 좁은 의미에서는 오차역전파법을 이용한 다층 퍼셉트론을 가리키는 경우도 있지만, 이것은 잘못된 용법으로, 인공신경망은 이에 국한되지 않는다.

- 위키백과 (인공 신경망)

인공 신경망은 생물의 신경계가 작동하는 방식을 모방하여 만든 머신 러닝 모델이라고 볼 수 있다.

우리의 신경계는 수많은 신경 세포, 즉 뉴런(neuron)으로 이루어져 있다. 일반적인 뉴런의 구조는 아래와 같이 생겼다.

일반적인 뉴런의 구조


간단하게 설명하자면, 뉴런은 수상돌기를 통해 신호를 받아들인 후 신경세포체와 축삭을 거쳐서 다른 뉴런으로 신호를 전달한다.

우리 몸의 신경계가 뉴런으로 구성되듯이 인공 신경망을 이루는 가장 작은 단위가 있는데, 이를 퍼셉트론(perceptron)이라고 한다.

퍼셉트론(perceptron)

위에서 뉴런이 '수상돌기 -> 신경세포체/축삭 -> 다음 뉴런의 수상돌기'라는 과정을 거쳐서 신호를 전달하는 것을 보았다. 퍼셉트론도 이와 비슷한 구조를 갖고 있다.

먼저 여러 개의 입력값을 받는다. 각각의 입력값에는 신호의 세기, 즉 가중치가 부여되어 있다. 각각의 입력값과 가중치가 곱해진 값을 합한 후, 이에 편향을 더해준다(가중치-편향 연산). 이를 식으로 나타내면 아래와 같다.
$$w_1x_1 + w_2x_2+ b$$
위에서는 입력값이 2개뿐이다. 만약 입력값이 n개라고 하면 식이 어떻게 될까?
$$\sum_{k=1}^{n}w_kx_k + b\\
= (w_1x_1+w_2x_2+...+w_nx_n) + b\\
= \begin{bmatrix}w_1&w_2&\cdots&w_n\end{bmatrix}\begin{bmatrix}x_1\\x_2\\...\\x_n\end{bmatrix} + b\\
=Wx + b$$
각각의 입력값과 가중치가 곱해지는 연산을 벡터로 표현하였고, 그 결과 $Wx+b$와 같은 형태가 되었다(여기서 $W$는 가중치 행렬$^*$$^1$이라고 함).
어디서 많이 보던 모양이다. 그렇다. 일차 함수, 직선이다!

그렇다. 직선이다. 그게 뭐 특별한건가?
특별하다. 왜냐하면 이대로는 복잡한 문제 해결이 불가능하다.
어떤 복잡한 문제인가? XOR 이라고 하는 문제가 있다.

XOR (배타적 논리합)

XOR을 알기 위해서는 먼저 AND, NAND, OR와 같은 논리 게이트가 어떻게 작동하는 것들인지 이해해야 한다.

1) AND
두 명의 사람이 있고, 저녁에 치킨을 먹을지 말지 고민 중이다.
둘 모두 치킨을 먹는다고 할 때에만 먹는(1) 경우다.

AND GATE

2) NAND
이는 AND의 반대, Not AND라서 NAND이다.

NAND GATE

2) OR
이는 둘 중 한 명이라도 치킨 먹자! 라고 하면 치킨을 먹는 경우다.

OR GATE

3) XOR
둘 다 먹자고 하면 안 먹고, 둘 다 안 먹자고 하면 안 먹는데 둘 중 하나만 먹자고 하면 먹는 이상한 경우다. 둘 모두 먹으면 둘 다 살이 찌니까 한 명만 먹기로 하는 결정인걸까? 확실히 단순하지는 않다.

XOR GATE

XOR이 어떤 식으로 작동하는지 보았다. 이번에는 위 논리 게이트의 Output들이 직선을 통해 어떤 식으로 분류되는지 보자.

논리 게이트 Output 분류


AND와 OR의 경우 하나의 직선으로 0과 1의 Output들이 깔끔하게 분류가 된다. 그런데 XOR은? 이래서 아까 위에서 나온 Wx+b의 직선 형태로는 XOR 문제를 해결할 수가 없다.

자, 직선으로는 답이 안나온다. 그럼 어떻게 하지?
일단 휘게 만든다. 뭘로? 활성화 함수로.

활성화 함수(Activation Function)

활성화 함수. 이름대로 자기가 받은 신호를 잘 살려서 내보내주는 역할을 한다. 마치 아이돌 연습생을 잘 트레이닝해서 데뷔를 시키는 프로듀서와도 같다.

잘 살려서 내보내주는 역할이란 무엇인가? 여기에는 두 가지가 있다.

1) 가중합(가중치*입력값 들의 합, Wx)이 계산되고 나면 값이 너무 커지거나 작아질 수가 있다. 이때 활성화 함수를 통해서 이 값을 0에서 1 또는 -1에서 1 사이의 값 등으로 바꿔준다(활성화 함수 유형에 따라 다름. 전부 다 상하한의 제한이 있는 것은 아님).
아이돌 연습생의 과도한 똘끼(?)를 진정시켜 마음을 가다듬게 해준다고 보면 되겠다.

2) 비선형성을 가진 활성화 함수를 통해서 Wx+b를 휘게 만들어준다.
연습생이 이상한 고집 부리지 않게끔 유연한 사고방식을 심어준다고 생각하면 될 것 같다.

(구체적인 활성화 함수 내용은 다음 링크 참고 : 활성화 함수)

자, 우리 연습생의 똘끼도 진정시켰고, 이상한 고집도 안 부리게끔 만들었다. 이제 세상으로 내보내보려고 한다.
잠깐, 그 전에 잘 하는지 한번 보자.

### XOR 문제 구성 ###
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(0)

x11 = np.random.uniform(low=0, high=5, size=(50,))
x12 = np.random.uniform(low=10, high=15, size=(50,))
x21 = np.random.uniform(low=0, high=5, size=(50,))
x22 = np.random.uniform(low=10, high=15, size=(50,))


x1 = np.append(x11, x12)
x2 = np.append(x21, x22)

y11 = np.random.uniform(low=10, high=15, size=(50,))
y12 = np.random.uniform(low=0, high=5, size=(50,))
y21 = np.random.uniform(low=0, high=5, size=(50,))
y22 = np.random.uniform(low=10, high=15, size=(50,))

y1 = np.append(y11, y12)
y2 = np.append(y21, y22)

x_1 = np.vstack([x1, y1]).T
x_2 = np.vstack([x2, y2]).T
y_1 = np.ones_like(x_1[:, 0])
y_2 = np.zeros_like(x_2[:, 0])
x = np.vstack([x_1, x_2])
y = np.hstack([y_1, y_2])


fig, ax = plt.subplots(figsize = (12,5))
ax.plot(x_1[:, 0], x_1[:,1], 'bo')
ax.plot(x_2[:,0], x_2[:,1], 'ro')
ax.grid()

XOR problem

### XOR 문제 해결시켜보기(단층 퍼셉트론) ###
import tensorflow as tf

model = tf.keras.models.Sequential([
    # 한 개의 층으로만 구성
    tf.keras.layers.Dense(1, input_dim=2, activation='sigmoid')
])

model.compile(optimizer='sgd',
              loss='binary_crossentropy',
              metrics=['accuracy'])

model.fit(x, y, epochs=1000, verbose=0)

preds = model.predict(x)
preds_1d = preds.flatten()
pred_class = np.where(preds_1d > 0.5, 1 , 0)

y_true = x[pred_class==1]
y_false = x[pred_class==0]

fig, ax = plt.subplots(figsize = (12,5))
ax.plot(y_true[:, 0], y_true[:,1], 'bo')
ax.plot(y_false[:,0], y_false[:,1], 'ro')
ax.grid()

XOR problem solution failed

큰일이다. 이 친구가 혼자서 해내지 못한다.
솔로 데뷔가 힘들 것 같은데... 그렇다면 다른 연습생들과 묶어서 그룹으로 데뷔를 시켜야 할까?

### XOR 문제 해결시켜보기(다층 퍼셉트론) ###
model = tf.keras.models.Sequential([
    # 여러 개의 층으로 구성
    tf.keras.layers.Dense(8, input_dim=2, activation='sigmoid'),
    tf.keras.layers.Dense(1, activation='sigmoid')
])

model.compile(optimizer='sgd',
              loss='binary_crossentropy',
              metrics=['accuracy'])

model.fit(x, y, epochs=1000, verbose=0)

preds = model.predict(x)
preds_1d = preds.flatten()
pred_class = np.where(preds_1d > 0.5, 1 , 0)

y_true = x[pred_class==1]
y_false = x[pred_class==0]

fig, ax = plt.subplots(figsize = (12,5))
ax.plot(y_true[:, 0], y_true[:,1], 'bo')
ax.plot(y_false[:,0], y_false[:,1], 'ro')
ax.grid()

XOR problem solution successed

다른 연습생들과 함께 그룹으로 묶으니 이제 복잡한 문제도 자기들끼리 척척 잘 해결한다!
이제는 믿고 데뷔를 시킬 수 있을 것 같다.

이렇게 여러 층의 퍼셉트론으로 구축한 신경망을 다층 퍼셉트론 신경망(MLP; Multi Layer Perceptron)이라고 한다.


인공 신경망은 어떻게 생긴 것이지?

인공 신경망의 가장 기본이 되는 단위인 퍼셉트론 하나부터 시작해서 퍼셉트론 여러 개가 여러 층으로 모인 다층 퍼셉트론 신경망까지 살펴봤다.
이번에는 인공 신경망의 전체적인 그림을 보자.

인공 신경망

인공 신경망은 위 그림처럼 생겼다. 그림 속 각각의 원은 노드(node)라고 하며, 전체는 크게 세 부분 - 입력층(Input Layer), 은닉층(Hidden Layers), 출력층(Output Layer)으로 나누어진다.

1) 입력층(Input Layer)

  • 데이터셋이 입력되는 층
  • 데이터셋의 특성(feature) 개수에 맞춰 입력층의 노드 수가 결정됨
  • 어떤 계산 없이 입력값의 전달만 수행 -> 신경망 층수(깊이, depth)에 포함되지 않음

2) 은닉층(Hidden Layers)

  • 입력층에서 들어온 값이 가중치-편향 연산 및 활성화 함수를 거쳐가는 층
  • 일반적으로 입력층과 출력층 사이에 있는 층임
  • 사용자가 계산 결과를 볼 수 없으므로 은닉(hidden)층이라 함
  • 입력 데이터셋의 특성 수와 관계없이 노드 수 구성 가능
    ※ 딥 러닝 : 2개 이상의 은닉층을 가진 신경망

3) 출력층(Output Layer)

  • 은닉층의 연산을 마친 값이 출력되는 층
  • 해결할 문제에 따라 출력층의 노드 수가 결정됨
  노드 수 결과 값 활성화 함수
이진 분류 1
(∵0 또는 1의
값 1개)
0~1 사이의 확률값 Sigmoid
다중 분류 레이블(타겟)
클래스 수
각 클래스별 0~1
사이의 확률값
Softmax
회귀 출력값의
특성(타겟) 수
타겟 값 일반적으로는
지정 X

 

인공 신경망 구현 예시(Tensorflow & Keras)

1) 데이터 불러오기

입력 데이터 샘플과 Features : 1077 샘플 x 69 Features (변수)
데이터 label: 다운증후군 (1), 정상군 (2)

(데이터는 다운증후군과 정상군 마우스 피질의 핵 분획에서 검출 가능한 신호를 생성하는 69 개 단백질의 발현 수준으로 구성되어 있습니다.
라벨로는 다운증후군 1, 정상군 2로 할당되어 있습니다.)

import pandas as pd
df = pd.read_excel("https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/MouseProtein/mouse_protein_X.xls", header=None)
df_label = pd.read_excel("https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/MouseProtein/mouse_protein_label.xls", header=None)

2) 라벨 값 변경

# 기존에 다운증후군(1), 정상군(2)였던 값을 정상군(0), 다운증후군(1)로 변경
df_label = df_label.replace(2, 0).iloc[:, 0].values
df_label.astype(object)

3) 훈련 / 테스트 데이터셋 나누기

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(df, df_label, test_size=0.2, random_state=42)

4) 신경망 모델 구성

# 모델 초기화
model = tf.keras.models.Sequential([
    tf.keras.layers.Dense(128, input_dim=69, activation='relu'),
    tf.keras.layers.Dense(128, activation='relu'),
    tf.keras.layers.Dense(1, activation='sigmoid')
])
# Dense : 각각의 신경망 층을 나타냄
# 맨 처음과 마지막이 입력층과 출력층, 그 사이는 은닉층
# 각 층 맨 앞의 숫자(128, 128, 1)는 해당 층의 노드 수임
# input_dim : 입력 데이터 특성 수(input_shape=(69,) 이렇게도 입력 가능)
# activation : 해당 층의 노드들이 사용할 활성화 함수 지정
# keras 모델 초기화 후에는 compile 과정을 거쳐야 함
model.compile(optimizer='sgd',
              loss='binary_crossentropy',
              metrics=['accuracy'])
# optimizer : 손실 함수의 최소값을 찾는 방법 지정
# loss : 손실 함수 종류 지정
# metrics : 훈련 시 평가 지표 지정
# 모델 훈련시키기
model.fit(X_train, y_train, epochs=500)
# epochs : 전체 데이터셋을 한 번 훈련한 것이 1 epoch임
# epochs=500 이라는 것은 전체 데이터셋에 대해 훈련을 500번 진행한다는 의미

Epoch 1/500
27/27 [==============================] - 0s 876us/step - loss: 0.6981 - accuracy: 0.5017
Epoch 2/500
27/27 [==============================] - 0s 778us/step - loss: 0.6906 - accuracy: 0.5296
Epoch 3/500
27/27 [==============================] - 0s 815us/step - loss: 0.6804 - accuracy: 0.5935
...
Epoch 499/500
27/27 [==============================] - 0s 852us/step - loss: 0.0252 - accuracy: 0.9988
Epoch 500/500
27/27 [==============================] - 0s 852us/step - loss: 0.0287 - accuracy: 0.9954

# 테스트셋을 통한 모델 평가
model.evaluate(X_test, y_test, verbose=2)
# verbose : 결과 출력의 단계 설정
# auto - 대부분 1로 지정됨
# 0 - 출력 없음
# 1 - 진행 상황 출력(프로그레스바 포함)
# 2 - 진행 상황 출력(프로그레스바 제외, 1에 비해 간소화)

7/7 - 0s - loss: 0.0478 - accuracy: 0.9861
[0.04781070724129677, 0.9861111044883728]


*1 가중치 행렬(Weight Matrix)
위에서는 퍼셉트론 하나라서 행렬이라기보다는 벡터였다.
하지만 아래와 같이 퍼셉트론 여러 개로 구성된 신경망이라면?

가중치 행렬


이렇게 되면 가중치 연산은 (편향은 없다 하면)
$$(w_1x_1+w_3x_1+w_5x_1+w_2x_2+w_4x_2+w_6x_2)\\
= \begin{bmatrix}x_1&x_2\end{bmatrix}\begin{bmatrix}w_1&w_3&w_5\\w_2&w_4&w_6\end{bmatrix} = \begin{bmatrix}y_1&y_2&y_3\end{bmatrix}$$
이렇게 되고, 결과를 정리해서 표현하면 $y = Wx$ 라고 할 수 있다.
여기서 입력값의 벡터 $x$와 출력값의 벡터 $y$를 이어주는 행렬 $W$를 가중치 행렬이라고 한다.

코드 상에서 가중치 행렬의 형태는 어떻게 알 수 있을까?

model = tf.keras.models.Sequential([
    tf.keras.layers.Dense(10, activation='relu', input_shape=100), # 은닉층
    tf.keras.layers.Dense(1, activation='sigmoid') # 출력층
])

입력 데이터의 특성이 100개이므로 입력층 노드 수는 100, 은닉층의 노드 수는 10이므로 둘 사이의 가중치 행렬 형태는 (100, 10)이다.
같은 원리로 은닉층과 출력층 사이에서는 (10, 1)이 된다.


<참고 자료>

+ Recent posts