머신러닝·신경망

DL / 23번째 글

신경망 기초: 층, 파라미터, 순전파의 모든 것

신경망의 구성 요소인 층(Layer), 가중치(Weight), 편향(Bias), 활성화 함수를 수학과 코드로 이해한다. 순전파(Forward Pass)가 어떻게 예측을 만드는지, 파라미터 수는 어떻게 계산하는지를 완전히 파악한다.

PALDYN Team29 MIN READ

지난 글에서 단일 퍼셉트론이 XOR을 풀지 못하는 한계를 확인했다. 이 한계를 깨기 위해 퍼셉트론을 여러 겹으로 쌓은 것이 다층 퍼셉트론(Multi-Layer Perceptron, MLP), 곧 현대적 의미의 신경망(neural network)이다. 이 글에서는 신경망을 부품 단위로 뜯어본다. 층이 무엇이고 그 안에서 어떤 계산이 일어나는지, 파라미터가 어디서 나오고 몇 개인지, 입력이 출력이 되기까지 숫자가 어떻게 흘러가는지를 행렬의 모양을 하나하나 따라가며 확인하고, 작은 신경망 하나는 손으로 끝까지 계산해 본다.

층의 세 종류

입력층

신경망에서 계산의 단위는 뉴런(neuron) 하나다. 노드라고도 부른다. 뉴런은 퍼셉트론과 같은 일을 한다 — 들어온 값마다 가중치를 곱해 더하고, 편향을 더하고, 그 결과를 활성화 함수에 통과시킨다. 이런 뉴런을 나란히 여럿 세운 한 줄을 층(layer)이라 부르고, 신경망은 층을 차례로 이어 놓은 것이다.

맨 앞의 입력층은 이름과 달리 계산을 하지 않는다. 데이터의 특성 값을 그대로 받아 다음 층에 넘기는 자리일 뿐이라, 가중치도 편향도 없다. 그래서 "3층 신경망"이라고 할 때 입력층을 세지 않는 관례가 많다. 입력층의 크기는 설계자가 고르는 값이 아니라 데이터가 정한다. 28×28 흑백 이미지를 한 줄로 펴면 784개, 특성이 4개인 붓꽃 데이터면 4개다.

입력층에 무엇을 넣느냐는 생각보다 많은 것을 정한다. 이미지를 한 줄로 펴는 순간 "이 픽셀과 저 픽셀이 위아래로 붙어 있다"는 정보가 사라진다. 완전 연결 층은 784개 값을 순서 없는 목록으로 받으므로, 픽셀의 순서를 한 가지 규칙으로 뒤섞어 모든 이미지에 똑같이 적용해도 학습 결과가 같다. 값의 크기도 맞춰 둬야 한다. 0255 픽셀 값을 그대로 넣으면 첫 층의 가중합이 수천 단위로 커져 활성화 함수가 극단으로 밀리고 학습이 불안정해진다. 흔히 01로 나누거나 평균 0, 분산 1로 표준화해서 넣는다.

은닉층의 폭

입력층과 출력층 사이의 층을 은닉층이라 한다. 바깥에서 값이 직접 보이지 않아 붙은 이름이다. 은닉층의 뉴런 수를 그 층의 폭(width)이라 부르고, 은닉층을 몇 겹 쌓았는지를 깊이(depth)라 부른다. 폭과 깊이가 신경망이 표현할 수 있는 함수의 복잡도, 곧 용량(capacity)을 정한다.

폭은 "이 층이 입력을 몇 개의 서로 다른 관점으로 다시 표현하는가"로 읽으면 이해가 쉽다. 폭이 3이면 입력을 3개의 가중합으로 요약하고, 폭이 256이면 256개로 요약한다. 폭이 너무 좁으면 앞 층의 정보가 그 좁은 통로에서 손실되고, 너무 넓으면 파라미터가 불어나 적은 데이터로는 과적합한다. 실무에서는 입력보다 조금 크게 시작해 출력 쪽으로 갈수록 좁히는 모양(784 → 256 → 128 → 10)이 흔하다.

출력층

마지막 출력층의 크기와 활성화는 풀려는 문제가 정한다. 설계자가 고를 여지가 거의 없는 자리다.

문제 출력 뉴런 수 출력 활성화 짝이 되는 손실
이진 분류 1 시그모이드 이진 교차 엔트로피
다중 분류 부류 수 소프트맥스 교차 엔트로피
회귀 타깃 수 없음(항등) 평균제곱오차

PyTorch에서는 흔히 출력층에 활성화를 달지 않고 날것의 점수, 곧 로짓(logit)을 그대로 내보낸다. nn.CrossEntropyLoss가 소프트맥스를 안에서 함께 계산하기 때문이다. 이때 모델에 소프트맥스를 또 달면 소프트맥스가 두 번 걸려 학습이 느려진다. 흔한 실수이니 손실 함수가 무엇을 기대하는지 먼저 확인한다.

회귀의 출력층에 활성화를 달지 않는 이유도 같은 맥락이다. 집값처럼 범위가 정해지지 않은 수를 맞혀야 하는데 마지막에 ReLU를 달면 음수를 못 내고, 시그모이드를 달면 0~1 밖을 못 낸다. 타깃이 늘 양수라는 것을 알고 일부러 그 제약을 거는 경우가 아니라면, 회귀의 마지막 층은 가중합을 그대로 내보낸다. 출력층의 활성화는 "예측이 가질 수 있는 값의 범위"를 정하는 선택이다.

신경망 기본 구조

층 하나의 계산

모양 추적

모든 뉴런이 앞 층의 모든 값과 연결된 층을 완전 연결 층(fully connected layer)이라 부르고, PyTorch에서는 nn.Linear다. 뉴런 하나의 계산을 층 전체로 모으면 행렬 곱 하나가 된다. 앞 층의 출력을 a(l−1)\mathbf{a}^{(l-1)}, 이 층의 가중치 행렬을 W(l)W^{(l)}, 편향 벡터를 b(l)\mathbf{b}^{(l)}, 활성화 함수를 ff 라 하면 이렇다.

z(l)=W(l)a(l−1)+b(l),a(l)=f(z(l))\mathbf{z}^{(l)} = W^{(l)}\mathbf{a}^{(l-1)} + \mathbf{b}^{(l)}, \qquad \mathbf{a}^{(l)} = f\big(\mathbf{z}^{(l)}\big)

여기서 W(l)W^{(l)} 와 b(l)\mathbf{b}^{(l)} 에 든 숫자가 이 층의 파라미터다. 학습은 이 숫자들을 정하는 절차이고, 순전파는 정해진 숫자로 위 식을 계산하기만 하는 일이다. 입력이 ninn_{in} 개, 뉴런이 noutn_{out} 개면 WW 의 모양은 (nout,nin)(n_{out}, n_{in}) 이다. 행 하나가 뉴런 하나의 가중치다. 신경망 코드를 읽을 때 가장 먼저 할 일이 이 모양을 층마다 적어 가며 따라가는 것이다. 오류의 대부분이 모양이 안 맞는 곳에서 난다.

편향 브로드캐스트

편향(bias)은 뉴런마다 하나씩 붙는 상수로, 가중합을 위아래로 옮겨 뉴런이 켜지는 문턱을 정한다. 편향이 없으면 모든 입력이 0일 때 가중합도 반드시 0이라, 원점을 지나지 않는 경계를 그릴 수 없다.

배치로 계산할 때 편향은 한 번 더 생각할 거리가 생긴다. 샘플 64개를 한꺼번에 넣으면 가중합은 (64,nout)(64, n_{out}) 행렬인데, 편향은 길이 noutn_{out} 벡터 하나다. 이 둘을 더할 수 있는 것은 브로드캐스트 덕분이다. 모양이 안 맞는 축에서 편향을 64번 복사한 것처럼 취급해 모든 샘플에 같은 편향을 더한다. 실제로 복사하지는 않으므로 메모리도 들지 않는다. 편향이 샘플마다 다르지 않고 뉴런마다 다르다는 사실이 이 모양에 그대로 드러난다.

배치 차원

실제 코드에서는 샘플을 한 개씩이 아니라 배치(batch)로 묶어 넣는다. 입력은 (B,nin)(B, n_{in}) 행렬이 되고, PyTorch의 nn.Linear는 Z=XW⊤+bZ = XW^\top + \mathbf{b} 를 계산해 (B,nout)(B, n_{out}) 을 낸다. 수식의 WaW\mathbf{a} 가 코드에서 XW⊤XW^\top 로 바뀌는 것은 샘플이 행으로 쌓였기 때문이고, 계산하는 내용은 같다.

배치 차원 B는 층을 지나도 바뀌지 않는다. 층은 특성 차원만 ninn_{in} 에서 noutn_{out} 으로 바꾸고, 배치 안의 샘플끼리는 서로 섞이지 않는다. 그래서 배치를 묶는 것은 결과를 바꾸지 않고 속도만 바꾼다. GPU는 작은 행렬 곱 64번보다 큰 행렬 곱 한 번을 훨씬 빨리 하기 때문이다.

예외가 하나 있다. 배치 정규화 층은 배치 안 샘플들의 평균과 분산으로 값을 정규화하므로, 학습 중에는 같은 샘플이라도 어떤 배치에 들어갔느냐에 따라 출력이 조금씩 달라진다. 이 층이 든 모델은 model.eval()로 추론 모드를 켜야 학습 때 모아 둔 통계를 써서 샘플마다 같은 결과를 낸다. 완전 연결 층만 쌓은 모델에서는 이런 차이가 없다.

층별 행렬 연산

파라미터 세기

세는 규칙

완전 연결 층 하나의 파라미터 수는 가중치 nin×noutn_{in} \times n_{out} 개에 편향 noutn_{out} 개를 더한 것이다. 줄여 쓰면 (nin+1)×nout(n_{in} + 1) \times n_{out} 이다. 뉴런 하나가 앞 층 값마다 가중치 하나씩, 그리고 편향 하나를 가진다고 생각하면 외울 필요가 없다. 신경망 전체는 층마다 이 값을 구해 더하면 된다. 활성화 함수 ReLU나 입력층에는 파라미터가 없으니 세지 않는다.

위 구조 그림의 4-5-4-3 신경망으로 연습해 보면, 첫 층이 4×5+5=254 \times 5 + 5 = 25, 둘째 층이 5×4+4=245 \times 4 + 4 = 24, 출력층이 4×3+3=154 \times 3 + 3 = 15 로 모두 64개다. 그림에 선이 20+20+12=5220 + 20 + 12 = 52 개 그어져 있고 여기에 편향 12개가 더해진 수다. 선 하나가 가중치 하나라는 대응을 기억해 두면, 그림만 보고도 파라미터 수를 어림할 수 있다.

784-256-128-10 MLP

MNIST 손글씨 분류에 흔히 쓰는 구조로 세어 보자.

import torch.nn as nn

model = nn.Sequential(
    nn.Linear(784, 256),   # 784*256 + 256 = 201,216
    nn.ReLU(),
    nn.Linear(256, 128),   # 256*128 + 128 = 32,896
    nn.ReLU(),
    nn.Linear(128, 10),    # 128*10 + 10 = 1,290
)
print(sum(p.numel() for p in model.parameters()))   # 235,402

for name, p in model.named_parameters():
    print(f"{name:10s} {tuple(p.shape)}")           # 0.weight (256, 784) ...

235,402개 중 201,216개, 곧 85%가 첫 층에 있다. 입력이 784개로 가장 넓은 자리에서 가장 넓은 은닉층이 만났기 때문이다. 파라미터를 줄이고 싶으면 뒤쪽 층을 줄여 봐야 효과가 작고, 첫 은닉층의 폭을 줄이거나 입력의 차원을 먼저 낮추는 편이 크게 듣는다. 이미지에서 완전 연결 층 대신 합성곱을 쓰는 이유도 이 자리에 있다(마지막 절에서 다룬다).

메모리와 연산량

파라미터 수는 곧 메모리다. 32비트 부동소수점 하나가 4바이트이므로 235,402개는 약 0.94MB다. 학습할 때는 여기에 파라미터마다 기울기가 하나씩 붙고, Adam 옵티마이저는 파라미터마다 이동 평균 두 개를 더 들고 있어 모두 네 벌, 약 3.8MB가 된다. 추론만 할 때는 한 벌이면 된다. 대형 언어 모델에서 "학습에는 추론보다 몇 배의 GPU 메모리가 든다"는 말이 바로 이 계산이다.

연산량도 파라미터에서 나온다. 가중치 하나마다 곱셈 하나와 덧셈 하나가 일어나므로, 샘플 하나의 순전파는 가중치 수의 약 두 배만큼 부동소수점 연산을 한다. 이 모델의 가중치는 234,752개라 샘플당 약 47만 번, 배치 64개면 약 3,000만 번이다. 역전파는 기울기를 입력 쪽과 가중치 쪽 두 방향으로 구해야 해서 순전파의 약 두 배가 든다. 파라미터를 세어 두면 모델을 돌리기 전에 메모리와 시간을 어림할 수 있다.

순전파

2-3-1 손계산

입력에서 출력까지 층을 차례로 통과하며 예측을 만드는 계산을 순전파(forward pass)라 부른다. 입력 2개, 은닉 뉴런 3개, 출력 1개인 작은 신경망으로 숫자를 끝까지 따라가 보자. 파라미터는 은닉층 가중치 6개와 편향 3개, 출력층 가중치 3개와 편향 1개로 모두 13개다.

W(1)=[10011−1],b(1)=[0−10.5],W(2)=[123],b(2)=−1W^{(1)} = \begin{bmatrix} 1 & 0 \\ 0 & 1 \\ 1 & -1 \end{bmatrix}, \quad \mathbf{b}^{(1)} = \begin{bmatrix} 0 \\ -1 \\ 0.5 \end{bmatrix}, \quad W^{(2)} = \begin{bmatrix} 1 & 2 & 3 \end{bmatrix}, \quad b^{(2)} = -1

입력은 x=(1,2)\mathbf{x} = (1, 2) 다. 첫 뉴런은 1⋅1+0⋅2+0=11 \cdot 1 + 0 \cdot 2 + 0 = 1, 둘째는 0⋅1+1⋅2−1=10 \cdot 1 + 1 \cdot 2 - 1 = 1, 셋째는 1⋅1−1⋅2+0.5=−0.51 \cdot 1 - 1 \cdot 2 + 0.5 = -0.5 다. 가중합 z(1)=(1,1,−0.5)\mathbf{z}^{(1)} = (1, 1, -0.5) 에 ReLU를 걸면 음수가 0이 되어 a(1)=(1,1,0)\mathbf{a}^{(1)} = (1, 1, 0) 이다. 출력층은 1⋅1+2⋅1+3⋅0−1=21 \cdot 1 + 2 \cdot 1 + 3 \cdot 0 - 1 = 2 다.

2-3-1 신경망의 순전파

셋째 뉴런을 눈여겨보자. 출력층이 이 뉴런에 가장 큰 가중치 3을 주고 있지만, 이 입력에서는 ReLU가 0을 내서 그 3이 아무것도 보태지 못했다. 이 입력에 대해 셋째 뉴런은 꺼져 있는 것이다. 다른 입력, 예컨대 (2,0)(2, 0) 을 넣으면 셋째 뉴런의 가중합이 2.5가 되어 켜진다. 입력마다 켜지는 뉴런의 조합이 달라지고, 그 조합이 곧 이 신경망이 입력 공간을 나누는 방식이다.

중간 활성값

순전파를 하는 동안 층마다 계산한 z\mathbf{z} 와 a\mathbf{a} 를 활성값(activation)이라 부른다. 추론만 할 때는 다음 층에 넘기고 버려도 되지만, 학습할 때는 버리면 안 된다. 역전파가 기울기를 계산할 때 이 값들이 다시 필요하기 때문이다. 위 예에서 첫 은닉층의 가중치에 대한 기울기를 구하려면 입력 x\mathbf{x} 가, ReLU를 거꾸로 지나려면 z(1)\mathbf{z}^{(1)} 의 부호가 있어야 한다. z3=−0.5z_3 = -0.5 였던 셋째 뉴런은 ReLU의 기울기가 0이라, 이번 입력에서는 셋째 뉴런으로 들어오는 가중치가 전혀 갱신되지 않는다.

그래서 학습 중의 메모리는 파라미터만이 아니라 활성값이 함께 먹는다. 활성값은 배치 크기와 층 수에 비례해 늘어나므로, 앞 절의 작은 MLP에서는 무시할 만하지만 긴 문장을 다루는 트랜스포머에서는 파라미터보다 활성값이 더 큰 경우가 흔하다. 배치를 줄이면 메모리 부족이 풀리는 것도 이 때문이다.

PyTorch 대조

손계산이 맞는지 같은 파라미터를 PyTorch에 넣어 확인한다. nn.Linear(2, 3)의 가중치 모양이 (3,2)(3, 2) 인 것도 여기서 다시 확인된다.

import torch
import torch.nn as nn

net = nn.Sequential(nn.Linear(2, 3), nn.ReLU(), nn.Linear(3, 1))
with torch.no_grad():
    net[0].weight.copy_(torch.tensor([[1., 0.], [0., 1.], [1., -1.]]))
    net[0].bias.copy_(torch.tensor([0., -1., 0.5]))
    net[2].weight.copy_(torch.tensor([[1., 2., 3.]]))
    net[2].bias.copy_(torch.tensor([-1.]))

x = torch.tensor([[1., 2.], [2., 0.]])    # 배치 2개
print(net[0](x))   # [[1.0, 1.0, -0.5], [2.0, -1.0, 2.5]]
print(net(x))      # [[2.0], [8.5]]

첫 샘플은 손계산과 같은 2.0이다. 둘째 샘플 (2,0)(2, 0) 은 은닉 가중합이 (2,−1,2.5)(2, -1, 2.5) 라 이번에는 둘째 뉴런이 꺼지고 셋째가 켜져 2+0+3×2.5−1=8.52 + 0 + 3 \times 2.5 - 1 = 8.5 가 나온다. 배치로 넣어도 두 샘플이 서로 섞이지 않는다는 것이 여기서도 보인다.

코드에서 파라미터를 채울 때 torch.no_grad()로 감싼 이유도 짚어 두자. PyTorch의 파라미터는 기울기를 추적하도록 표시되어 있어서, 그대로 값을 덮어쓰면 그 덮어쓰기까지 계산 그래프에 기록하려다 오류가 난다. 값을 손으로 정하는 일은 학습의 일부가 아니므로 추적을 끄고 한다. 학습이 끝난 모델로 예측만 할 때도 같은 블록으로 감싸면, 역전파에 쓸 활성값을 저장하지 않아 메모리와 시간이 준다.

층 쌓기

선형 합성

활성화 함수를 빼고 층을 쌓으면 어떻게 될까. 두 층을 이어 쓰면 이렇다.

W2(W1x+b1)+b2=(W2W1)x+(W2b1+b2)W_2(W_1\mathbf{x} + \mathbf{b}_1) + \mathbf{b}_2 = (W_2W_1)\mathbf{x} + (W_2\mathbf{b}_1 + \mathbf{b}_2)

W2W1W_2W_1 은 행렬 하나이고 W2b1+b2W_2\mathbf{b}_1 + \mathbf{b}_2 는 벡터 하나다. 두 층이 한 층과 똑같은 모양의 함수가 됐다. 같은 논리를 되풀이하면 백 층을 쌓아도 한 층이다. 숫자로 확인해도 같다. W1=[2103]W_1 = \begin{bmatrix} 2 & 1 \\ 0 & 3 \end{bmatrix}, W2=[1−1]W_2 = \begin{bmatrix} 1 & -1 \end{bmatrix} 이면 W2W1=[2−2]W_2W_1 = \begin{bmatrix} 2 & -2 \end{bmatrix} 이다. 은닉 뉴런 둘을 거친 계산이 "첫 입력의 두 배에서 둘째 입력의 두 배를 뺀다"는 한 줄로 줄어든다. 가운데 층이 무엇을 계산했든 그 흔적이 남지 않는다. 선형 변환을 합성하면 다시 선형 변환이라는 이 한 줄이, 활성화 함수가 층 사이에 반드시 있어야 하는 근거다. 파라미터만 늘고 표현할 수 있는 함수는 하나도 늘지 않는다.

위 손계산에서 ReLU가 한 일을 되짚으면 비선형이 무엇을 보태는지 보인다. 입력마다 켜지는 뉴런이 달랐고, 그래서 입력 공간의 영역마다 서로 다른 선형 함수가 적용됐다. ReLU 신경망은 입력 공간을 조각으로 나누고 조각마다 다른 직선(평면)을 쓰는 조각별 선형 함수다. 조각을 충분히 잘게 나누면 어떤 곡선도 흉내 낼 수 있다.

깊이와 표현력

조각 수는 폭보다 깊이에 더 민감하다. 은닉층 하나에 뉴런을 늘리면 조각이 뉴런 수에 비례해 늘지만, 층을 겹치면 앞 층이 만든 조각 하나하나를 뒤 층이 다시 쪼개므로 조각이 곱으로 늘어난다. MLP에서 뉴런 둘짜리 톱니 함수를 열 번 겹쳐 조각 1,024개를 만드는 예로 이것을 자세히 본다.

그렇다고 무조건 깊게 쌓으면 되는 것은 아니다. 층이 깊어질수록 역전파에서 기울기가 곱해지며 사라지거나 폭발하기 쉽고, 초기화와 정규화를 신경 써야 학습이 된다. 표현할 수 있는 것과 실제로 학습해 낼 수 있는 것은 다르다. 딥러닝의 역사에서 "깊게"가 가능해진 것은 ReLU, 좋은 초기화, 잔차 연결 같은 장치가 차례로 그 간격을 좁힌 덕분이다.

학습 한 바퀴

지금까지는 파라미터가 이미 정해져 있다고 보고 순전파만 봤다. 학습은 그 파라미터를 정하는 절차이고, 한 바퀴는 네 걸음이다. 순전파로 예측을 만들고, 손실 함수로 예측과 정답의 차이를 재고, 역전파로 파라미터마다 손실의 기울기를 구하고, 옵티마이저가 그 기울기를 따라 파라미터를 조금 옮긴다.

optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()

for X_batch, y_batch in dataloader:
    logits = model(X_batch)             # 1. 순전파
    loss = criterion(logits, y_batch)   # 2. 손실
    optimizer.zero_grad()
    loss.backward()                     # 3. 역전파
    optimizer.step()                    # 4. 갱신

zero_grad()를 빠뜨리면 PyTorch가 기울기를 덮어쓰지 않고 누적해서, 매 바퀴 앞선 배치의 기울기가 섞여 들어간다. 이 네 걸음 가운데 순전파는 이 글에서 다뤘고, 역전파가 연쇄 법칙으로 기울기를 계산하는 과정은 뒤의 글에서 따로 다룬다.

다른 층들

Conv2d

완전 연결 층은 모든 입력을 모든 출력에 잇기 때문에 입력이 커지면 파라미터가 감당할 수 없이 불어난다. 32×32 컬러 이미지(값 3,072개)를 같은 크기의 16채널 출력(값 16,384개)으로 바꾸는 완전 연결 층은 가중치가 5,000만 개를 넘는다. 합성곱 층(nn.Conv2d)은 같은 일을 3×3 크기의 작은 필터로 한다. 필터 하나가 이미지 위를 미끄러지며 모든 위치에서 같은 가중치로 계산하므로, 3채널 입력에 16개 필터면 파라미터가 3×3×3×16+16=4483 \times 3 \times 3 \times 16 + 16 = 448 개다.

이것이 가능한 이유는 이미지에 대한 두 가지 가정이다. 한 픽셀은 주변 픽셀과만 깊은 관계가 있고(지역성), 가장자리 같은 무늬는 이미지 어디에 나타나도 같은 방식으로 알아볼 수 있다(위치 불변). 가정이 데이터와 맞으니 파라미터를 10만 분의 1로 줄이고도 성능은 오히려 좋아진다.

작은 필터로도 큰 무늬를 볼 수 있는 것은 층을 쌓기 때문이다. 3×3 필터를 두 번 겹치면 둘째 층의 값 하나가 원본의 5×5 영역을, 세 번 겹치면 7×7 영역을 보게 된다. 앞 절에서 본 깊이의 이득이 여기서는 보는 범위로 나타난다.

임베딩과 어텐션

임베딩 층(nn.Embedding)은 정수 ID를 벡터로 바꾼다. 어휘 50,000개를 768차원으로 표현하면 (50000,768)(50000, 768) 표 하나, 파라미터 3,840만 개다. 수학적으로는 원-핫 벡터에 이 표를 곱하는 완전 연결 층과 같지만, 원-핫 벡터는 한 자리만 1이라 곱셈 대신 그 행을 꺼내 오기만 하면 된다. 계산은 거의 없고 메모리만 든다.

트랜스포머의 어텐션 층은 문장 안의 각 위치가 다른 위치들을 얼마나 참고할지를 계산한다. 이 층의 파라미터는 질의·키·값·출력을 만드는 d×dd \times d 행렬 넷이라, d=768d = 768 이면 약 236만 개다. 이 수는 문장 길이와 무관하다. 문장이 10토큰이든 1만 토큰이든 같은 네 행렬을 모든 위치에 쓴다.

가중치 공유

세 층을 나란히 놓으면 공통점이 보인다. 합성곱은 같은 필터를 이미지의 모든 위치에, 어텐션은 같은 행렬을 문장의 모든 위치에, 임베딩은 같은 단어 벡터를 그 단어가 나오는 모든 자리에 쓴다. 한 벌의 파라미터를 여러 자리에서 되풀이해 쓰는 이 발상을 가중치 공유라 부른다.

가중치 공유는 파라미터를 아끼는 기술이면서 동시에 가정을 심는 방법이다. "이미지의 왼쪽 위와 오른쪽 아래에서 고양이 귀는 같은 모양이다", "문장의 어느 위치에서든 단어 사이의 관계는 같은 규칙을 따른다"는 믿음을 구조에 박아 넣는 것이다. 완전 연결 층은 그런 가정 없이 모든 연결을 따로 배우므로 가장 유연하지만 가장 많은 데이터를 요구한다. 신경망 설계는 결국 데이터에 대해 무엇을 가정할지를 층의 종류로 고르는 일이다.

다음 글에서는 이 글에서 계속 등장한 ReLU를 포함해, 층 사이에서 비선형을 만드는 활성화 함수들을 차례로 비교한다. 시그모이드가 왜 은닉층에서 밀려났는지, ReLU의 뉴런이 왜 죽는지, GELU가 무엇을 고쳤는지를 본다.


읽어주셔서 감사합니다. 😊

LATEST

머신러닝·신경망의 최신 글

머신러닝·신경망2026.05.08

문맥적 임베딩: ELMo부터 BERT까지

정적 임베딩의 다의어 문제를 해결하는 문맥적 임베딩의 원리, ELMo의 양방향 LSTM 레이어 표현, BERT의 트랜스포머 기반 서브워드 임베딩 추출법을 수식과 코드로 완전히 해설한다.

12 MIN
머신러닝·신경망2026.05.08

FastText: 부분 단어로 OOV를 정복하다

FastText가 문자 n-gram 기반의 부분 단어 모델로 OOV 문제를 해결하는 방법, 한국어 형태론에서의 강점, 실전 학습과 추론 코드를 완전히 해설한다.

11 MIN
머신러닝·신경망2026.05.08

GloVe: 전역 공기 통계로 단어 벡터를 만들다

GloVe가 공기 행렬의 전역 통계와 국소 문맥 창의 장점을 결합하는 방법, 목적 함수의 수학적 의미, 사전 학습 벡터 활용법을 깊이 있게 다룬다.

11 MIN