LLM 엔지니어링2장 · 신경망과 딥러닝의 기본 원리5 / 23← 이전목차다음 →MetaContents

2장. 신경망과 딥러닝의 기본 원리

활성화 함수

수도꼭지를 떠올려 보세요. 꼭지를 아주 조금 돌릴 때는 물이 나오지 않습니다. 어느 만큼을 넘기면 그때부터 돌린 만큼 물이 나옵니다. 그리고 아무리 많이 돌려도 수압보다 세게 나오지는 않습니다. 활성화 함수는 뉴런의 가중합을 다음 층으로 얼마나 넘길지 정하는 이런 수도꼭지입니다.

시그모이드와 ReLU 의 모양을 나란히 그린 개념 그림

왼쪽 마젠타 곡선이 시그모이드, 오른쪽 청록 선이 ReLU 입니다. 시그모이드는 어떤 값이든 0 과 1 사이로 부드럽게 눌러 줍니다. ReLU 는 0 보다 작으면 꺼 버리고(0), 크면 들어온 만큼 그대로 넘깁니다. 여기서 꼭지를 조금 돌려도 물이 안 나오는 구간이 ReLU 의 왼쪽 평평한 부분이고, 넘긴 뒤 돌린 만큼 나오는 것이 오른쪽의 곧은 선입니다. 아무리 돌려도 수압 이상은 나오지 않는 것은 시그모이드가 1 에서 멈추는 모양과 닮았습니다. 다만 ReLU 에는 이런 끝이 없어 들어온 만큼 얼마든지 커집니다.

대표적인 활성화 함수

함수계산나오는 값주로 쓰는 곳
ReLU0 과 입력 가운데 큰 값0 이상은닉층의 기본값
시그모이드1 ÷ (1 + e의 -입력 제곱)0 ~ 1둘 중 하나를 고르는 출력(확률)
없음(그대로)입력 그대로아무 값회귀의 출력층

1장의 로지스틱 회귀가 바로 "가중합 → 시그모이드"였습니다. 뉴런 하나짜리 신경망이었던 셈입니다.

왜 꼭 있어야 하나

활성화 함수 없이 층을 쌓으면, 직선을 직선에 넣은 것이라 여러 층을 쌓아도 결국 직선 하나와 같습니다. 활성화 함수가 사이사이에 꺾임을 넣어 주어야 층을 쌓을수록 복잡한 곡선을 그릴 수 있습니다.

Python
import numpy as np

z = np.array([-3.0, -0.5, 0.0, 0.5, 3.0])   # 여러 뉴런의 가중합

relu = np.maximum(0, z)          # 음수는 0, 양수는 그대로
sigmoid = 1 / (1 + np.exp(-z))   # 0 과 1 사이로 눌러 준다
print(relu)
print(sigmoid.round(3))

ReLU 는 [0. 0. 0. 0.5 3.] 으로 음수를 모두 0 으로 바꿉니다. 시그모이드는 -3 을 0.047, 0 을 0.5, 3 을 0.953 으로 바꿉니다. 아무리 큰 값도 1 을 넘지 않습니다.

np.exp 는 무엇을 하나

시그모이드 식에 들어 있는 np.exp 는 같은 비율로 계속 불어나는 계산입니다. 복리 저금을 떠올려 보세요. 이자가 원금에 붙고, 다음에는 불어난 돈에 또 이자가 붙습니다. 그래서 처음에는 천천히 늘다가 갈수록 빠르게 늘어납니다. 거꾸로 돌리면 같은 비율로 계속 줄어드는데, 아무리 줄어도 0 이 되지는 않습니다. 여기서 한 칸마다 곱하는 비율이 e(약 2.718)이고, 몇 칸 갔는지가 np.exp(z) 의 z 입니다. 다만 은행 이자는 몇 % 안팎이지만 e 는 한 칸에 2.7 배쯤 늘어나는 훨씬 가파른 비율입니다.

np.exp(z) 곡선이 왼쪽에서는 0 에 가까이 붙어 있다가 0 에서 1 을 지나고 오른쪽으로 갈수록 가파르게 올라가는 개념 그림

그림의 청록 곡선이 np.exp(z) 입니다. 가로축이 들어온 값 z, 세로축이 나온 값입니다. 곡선은 늘 가로축 위에 있어 결과가 언제나 양수입니다. z 가 0 이면 1 이고, 오른쪽으로 한 칸 갈 때마다 약 2.718 배가 되어 가파르게 올라갑니다. 왼쪽으로 갈수록 0 에 가까워지지만 닿지는 않습니다.

np.exp(z) 는 e 를 z 번 곱한 값(e 의 z 제곱)을 배열의 칸마다 구합니다. e 는 2.71828… 로 끝없이 이어지는 수학의 상수이고, NumPy 에는 np.e 로 들어 있습니다.

Python
import numpy as np

print(np.e)                              # 상수 e
z = np.array([-3.0, -1.0, 0.0, 1.0, 3.0])
print(np.exp(z).round(3))                # 칸마다 e 의 z 제곱

실행하면 np.e 는 2.718281828459045 이고, 다섯 칸은 차례로 0.05, 0.368, 1, 2.718, 20.086 입니다.

znp.exp(z)읽는 법
-30.052.718 로 세 번 나눈 값. 0 에 가깝다
-10.3682.718 로 한 번 나눈 값
01한 번도 곱하지 않았다
12.718e 그 자체
320.0862.718 을 세 번 곱한 값. 빠르게 커진다

시그모이드 안에서 하는 일

시그모이드 1 / (1 + np.exp(-z)) 를 안쪽부터 따라가면 np.exp 가 왜 들어갔는지 보입니다. np.exp 는 언제나 양수를 내므로 분모 1 + np.exp(-z) 는 늘 1 보다 큽니다. 그래서 결과는 0 과 1 사이에 머뭅니다.

가중합 znp.exp(-z)1 + np.exp(-z)시그모이드
3 (큰 양수)0.05 (0 에 가깝다)1.050.953 (1 에 가깝다)
0120.5
-3 (큰 음수)20.086 (크다)21.0860.047 (0 에 가깝다)

가중합이 클수록 np.exp(-z) 가 0 으로 줄어 시그모이드는 1 에 다가가고, 가중합이 작을수록 np.exp(-z) 가 커져 0 에 다가갑니다. 위 코드의 0.047, 0.5, 0.953 이 이렇게 나온 값입니다. 앞에 붙은 마이너스(-z)는 "가중합이 클수록 1 에 가깝게" 방향을 맞추려고 넣은 것입니다.

자주 나는 오류

  • np.exp 는 아주 빨리 커집니다. 709 를 조금 넘는 값을 넣으면 컴퓨터가 담을 수 있는 수를 넘어 inf(무한대)가 되고 RuntimeWarning: overflow encountered in exp 경고가 납니다. 이 코드는 경고를 일부러 내야 해서 본문에 싣지 않았습니다. PyTorch 의 torch.sigmoid 같은 라이브러리 함수는 이런 경우를 안에서 다뤄 주므로, 실제 모델에서는 식을 직접 쓰기보다 라이브러리 함수를 씁니다.
  • np.exp 는 배열을 받아 칸마다 계산합니다. 파이썬의 math.exp 는 숫자 하나만 받으므로 칸이 여럿인 배열에 쓰면 TypeError 가 납니다.

주의할 점

  • 회귀의 출력층에는 활성화 함수를 넣지 않습니다. 주문량 같은 값에 ReLU 나 시그모이드를 씌우면 나올 수 있는 값이 막힙니다.
  • 은닉층에 시그모이드를 여러 겹 쓰면 학습이 느려지기 쉽습니다. 은닉층은 ReLU 로 시작합니다.
← 이전 절2장 · 인공 뉴런다음 절 →2장 · 층을 쌓은 신경망