지난 글에서 역전파가 연쇄 법칙을 통해 모든 층의 기울기를 계산한다는 것을 배웠다. 이 역전파로 학습할 수 있는 가장 기본적인 신경망 구조가 다층 퍼셉트론(Multi-Layer Perceptron, MLP)이다. 퍼셉트론은 입력에 가중치를 곱해 더한 값으로 참·거짓을 가르는 가장 작은 신경망 단위였고, MLP는 그 단위를 여러 겹 쌓아 앞 겹의 출력을 다음 겹의 입력으로 넘기는 구조다. MLP는 딥러닝의 출발점이자 가장 이해하기 쉬운 형태다. 이 글에서는 MLP의 구조를 XOR 문제로 손으로 따라가 보고, 보편 근사 정리가 정확히 무엇을 약속하고 무엇을 약속하지 않는지, 깊이와 너비 중 어느 쪽에 파라미터를 쓸지, 그리고 MNIST 손글씨 분류기를 끝까지 만들고 들여다보는 과정을 다룬다.
MLP의 구조
완전 연결 층
MLP를 이루는 벽돌은 완전 연결 층(fully connected layer)이다. 앞 겹의 값 하나하나가 다음 겹의 뉴런 하나하나에 전부 연결되어 있어서 붙은 이름이다. 뉴런 하나가 하는 일은 퍼셉트론과 같다 — 입력마다 가중치를 곱해 더하고, 편향을 더하고, 그 결과를 활성화 함수에 통과시킨다. 이 겹 전체를 행렬로 쓰면 한 줄이 된다.
여기서 는 가중치 행렬, 는 편향 벡터, 는 활성화 함수다. 입력이 2차원이고 이 겹의 뉴런이 4개면 는 4×2, 는 길이 4다. 이 와 에 든 숫자 전부가 모델의 파라미터이고, 학습은 역전파로 기울기를 구해 그 숫자들을 조금씩 옮기는 절차이며, 추론은 옮기기를 멈춘 숫자로 순전파만 하는 일이다.
MLP는 이 겹을 쌓은 것이다. 맨 앞에서 원시 특성을 받는 자리를 입력층, 맨 끝에서 예측을 내는 겹을 출력층이라 부르고, 그 사이에 끼어 바깥에서 값이 직접 보이지 않는 겹을 은닉층이라 부른다. 퍼셉트론과 MLP를 가르는 것이 바로 이 은닉층이다.
여기서 활성화 함수가 빠지면 안 되는 까닭이 있다. 활성화 없이 선형 변환 둘을 이어 붙이면 인데, 이것은 전개하면 , 즉 행렬 하나와 편향 하나짜리 선형 변환이다. 백 겹을 쌓아도 한 겹과 표현력이 같다. ReLU처럼 꺾이는 함수를 사이사이에 넣어야 겹을 쌓는 것이 의미를 갖는다.
XOR 풀이
XOR은 두 입력이 서로 다를 때만 1을 내는 함수다. 네 점 (0,0)·(1,1)은 0, (0,1)·(1,0)은 1인데, 평면 위에 직선 하나를 그어 이 둘을 가를 수 없다. 이렇게 직선(고차원에서는 평면) 하나로 두 부류를 가를 수 있는 성질을 선형 분리 가능성이라 하고, 퍼셉트론은 선형 분리 가능한 문제만 풀 수 있다.
은닉층 하나면 풀린다. 학습에 맡기기 전에 손으로 가중치를 정해 보자. 은닉 뉴런 둘을 이렇게 둔다.
출력은 다. 네 점을 차례로 넣으면 다음과 같다.
| 입력 | |||
|---|---|---|---|
| (0, 0) | 0 | 0 | 0 |
| (0, 1) | 1 | 0 | 1 |
| (1, 0) | 1 | 0 | 1 |
| (1, 1) | 2 | 1 | 0 |
정확히 XOR이다. 숫자 여덟 개(은닉 쪽 가중치 넷과 편향 둘, 출력 쪽 가중치 둘)만으로 퍼셉트론이 못 하던 일을 해냈다.
아래 코드는 같은 일을 손 대신 학습에 맡긴다. 은닉 뉴런을 넷 두는 것은 여유분이다. ReLU 뉴런은 초기값이 나쁘면 네 점 모두에서 0을 내고 기울기도 0이 되어 영영 움직이지 않을 수 있는데, 둘만 두면 하나만 그렇게 죽어도 풀 수 없게 된다. 넷이어도 시드에 따라 실패할 수 있으니 결과가 다르면 시드를 바꿔 보라.
import torch
import torch.nn as nn
# XOR 데이터
X = torch.tensor([[0.,0.],[0.,1.],[1.,0.],[1.,1.]])
y = torch.tensor([[0.],[1.],[1.],[0.]])
model = nn.Sequential(
nn.Linear(2, 4), # 은닉층: 2차원을 4차원으로
nn.ReLU(),
nn.Linear(4, 1), # 출력층
nn.Sigmoid(),
)
optimizer = torch.optim.Adam(model.parameters(), lr=0.1)
criterion = nn.BCELoss()
for _ in range(1000):
optimizer.zero_grad()
loss = criterion(model(X), y)
loss.backward()
optimizer.step()
pred = (model(X) > 0.5).float()
print(pred.t()) # 기대값: tensor([[0., 1., 1., 0.]])
은닉 표현
위 표에서 두 열만 떼어 보면 무슨 일이 일어났는지 보인다. 입력 공간에서 떨어져 있던 (0,1)과 (1,0)이 은닉 공간에서는 둘 다 (1,0)이라는 한 점으로 모였다. (0,0)은 (0,0)으로, (1,1)은 (2,1)로 갔다. 이제 세 점은 한 직선 위에 있지 않고, 1이어야 할 점 하나가 0이어야 할 두 점 사이에서 옆으로 비켜 서 있다. 출력층이 긋는 라는 직선 하나로 가를 수 있다.
은닉층이 한 일은 답을 낸 것이 아니라 좌표계를 바꾼 것이다. 입력을 새 좌표로 옮겨 적은 이 값을 은닉 표현(hidden representation)이라 부른다. 출력층은 여전히 선형 분류기일 뿐이고, 은닉층이 그 선형 분류기가 풀 수 있는 모양으로 데이터를 접어 준다. 층을 더 쌓는다는 것은 이 접기를 여러 번 한다는 뜻이고, 학습은 어떻게 접을지를 데이터에서 찾는 일이다. 이 관점은 뒤에서 깊이와 너비를 비교할 때 다시 쓴다.
보편 근사 정리
정리의 진술
XOR이 풀린다는 것은 한 사례다. 그렇다면 은닉층 하나로 어디까지 갈 수 있는가. 이 물음에 답한 것이 보편 근사 정리(universal approximation theorem)다. 1989년 Cybenko가 시그모이드 활성화에 대해, 같은 해 Hornik·Stinchcombe·White가 더 넓은 활성화에 대해 증명했고, 이후 조건이 더 느슨해져 다항식이 아닌 활성화면 된다는 데까지 갔다. 요지는 이렇다.
은닉층이 하나인 MLP는 뉴런을 충분히 많이 두면, 유계인 닫힌 구간(더 일반적으로 컴팩트 집합) 위의 임의의 연속 함수를 원하는 오차 이내로 근사할 수 있다.
조건을 하나씩 짚어야 정리를 과신하지 않는다. 첫째, 대상은 연속 함수다. 계단처럼 끊긴 함수는 경계 근처에서 오차가 남는다. 둘째, 정의역이 유한한 범위다. 학습 때 본 적 없는 먼 입력에서 잘 맞는다는 보장은 없다 — 외삽은 이 정리의 관심 밖이다. 셋째, 「충분히 많이」가 몇 개인지는 말하지 않는다. 함수에 따라 그 수가 입력 차원에 대해 지수적으로 커질 수 있다.
존재와 탐색
이 정리는 존재 증명이다. 원하는 함수를 근사하는 가중치 조합이 어딘가에 있다는 것만 말하고, 그것을 어떻게 찾는지는 말하지 않는다. 우리가 실제로 가진 도구는 역전파와 경사 하강법인데, 이 절차가 그 조합에 도달한다는 보장은 정리에 없다. 손실 곡면에는 평평한 구간과 나쁜 극소점이 있고, 앞에서 본 죽은 ReLU처럼 아예 움직이지 않는 뉴런도 생긴다.
더 중요한 공백은 일반화다. 정리는 함수 전체를 안다고 가정하지만 우리는 표본 몇만 개만 본다. 뉴런을 늘리면 학습 데이터에 맞추는 능력은 커지지만, 그 능력으로 표본의 잡음까지 외워 버리는 것이 과적합이다. 학습 데이터에서는 정확도가 오르는데 처음 보는 데이터에서는 떨어지는 현상으로 드러난다. 그래서 정리는 「MLP로 안 되는 함수는 없다」는 안심을 줄 뿐, 「이 크기로 이 데이터에서 잘 된다」는 설계 지침은 주지 않는다.
폭과 깊이
정리가 약속하는 길은 너비, 즉 한 층의 뉴런 수를 늘리는 쪽이다. 1차원 입력에서 ReLU 은닉 뉴런 하나는 꺾인 점 하나를 만든다. 뉴런이 개면 꺾인 점이 최대 개라서, 최대 조각의 꺾은선으로 곡선을 흉내 낸다. 조각을 촘촘히 할수록 곡선에 가까워지지만 뉴런 수는 조각 수에 비례해 는다.
깊이 쪽 길은 다르게 자란다. 구간 [0,1]에서 0 → 1 → 0으로 올라갔다 내려오는 삼각형 함수 는 ReLU 두 개로 만들 수 있다 — 다. 이것을 한 번 더 통과시키면 는 봉우리가 둘이고 조각이 넷이다. 세 번이면 여덟, 번이면 조각이다. 층마다 뉴런 둘씩, 10층이면 뉴런 20개로 1,024조각을 만든다. 은닉층 하나로 같은 꺾은선을 만들려면 꺾인 점마다 뉴런이 하나씩 들어 1,000개가 넘게 든다.
Telgarsky(2016) 등은 이런 구성을 써서, 깊은 망이 적은 뉴런으로 표현하는 함수 중에는 얕은 망이 지수적으로 많은 뉴런을 들여야 흉내 낼 수 있는 것이 있음을 보였다. 너비는 조각을 더하고 깊이는 조각을 곱한다. 이것이 「깊은 망이 같은 파라미터에서 더 효율적인 경우가 많다」는 말의 수학적 뒷받침이다. 다만 「있다」는 결과이지 모든 문제에서 그렇다는 뜻은 아니다.
깊이와 너비
파라미터 예산
실제로 비교하려면 파라미터 수를 맞춰 놓고 모양만 바꿔야 한다. 모양이 다른데 크기까지 다르면 차이가 어디서 왔는지 가를 수 없다. 완전 연결 층 하나의 파라미터는 입력 수 × 출력 수에 편향으로 출력 수를 더한 값이다.
import torch.nn as nn
# 넓고 얕은 네트워크: 은닉층 1개, 파라미터 203,530
wide_shallow = nn.Sequential(
nn.Linear(784, 256), nn.ReLU(),
nn.Linear(256, 10),
)
# 좁고 깊은 네트워크: 은닉층 4개, 파라미터 204,490
deep_narrow = nn.Sequential(
nn.Linear(784, 160), nn.ReLU(),
nn.Linear(160, 160), nn.ReLU(),
nn.Linear(160, 160), nn.ReLU(),
nn.Linear(160, 160), nn.ReLU(),
nn.Linear(160, 10),
)
count = lambda m: sum(p.numel() for p in m.parameters())
print(count(wide_shallow), count(deep_narrow))
넓은 쪽은 에 출력층 을 더해 203,530개다. 깊은 쪽은 폭을 160으로 두면 첫 층 125,600개, 가운데 세 층이 25,760개씩 77,280개, 출력층 1,610개로 204,490개가 되어 0.5% 안쪽으로 맞는다. 폭을 64로 줄여 은닉층 넷을 두면 63,370개라, 넓은 쪽의 3분의 1도 안 되는 크기로 비교하는 셈이 된다 — 이런 비교는 공정하지 않다.
두 모델을 같은 학습률·같은 에폭으로 돌려 테스트 정확도를 비교해 보라. MNIST처럼 쉬운 문제에서는 둘 다 높은 정확도에 이르러 차이가 작은 경우가 많고, 차이는 데이터가 복잡해질수록 벌어지는 경향이 있다. 결론을 한 번의 실행으로 내리지 말고 시드를 서너 개 바꿔 평균을 보는 것이 좋다.
계층적 특성
깊은 망이 유리하다는 경험적 관찰의 다른 한 축은 계층적 특성이다. 앞쪽 층은 단순한 패턴을, 중간 층은 그 패턴의 조합을, 뒤쪽 층은 더 추상적인 개념을 표현하는 경향이 있다는 관찰이다. 작은 필터를 이미지 위에 옮겨 가며 곱하는 합성곱 망에서 첫 층 필터를 그려 보면 가장자리·색 얼룩 같은 것이 나오고, 뒤로 갈수록 눈·바퀴처럼 부분을 조합한 반응이 나타난다는 시각화 연구가 여럿 있다.
앞의 은닉 표현 관점으로 보면 이것은 자연스럽다. 층마다 좌표계를 한 번씩 바꾸므로, 뒤 층은 앞 층이 만든 좌표 위에서 다시 접는다. 손글씨 숫자라면 획의 조각을 먼저 찾고, 그 조각이 어떻게 이어졌는지를 다음에 보고, 마지막에 그 배치가 어느 숫자인지를 고르는 식의 분업이 가능해진다. 다만 MLP는 입력을 한 줄로 펴서 받으므로 이 분업이 합성곱 망만큼 깔끔하게 드러나지는 않는다.
깊이의 비용
깊이가 공짜는 아니다. 역전파는 출력에서 입력 쪽으로 층마다 기울기를 곱해 나가므로, 곱하는 값이 계속 1보다 작으면 앞쪽 층의 기울기가 0에 가까워져 학습이 멈추는 기울기 소실이, 계속 1보다 크면 반대로 값이 터지는 기울기 폭발이 생긴다. 층이 깊을수록 곱셈이 길어져 둘 다 심해진다.
그래서 깊이를 늘릴 때는 따라오는 장치가 있다. 가중치 초기값의 크기를 층 폭에 맞추는 초기화, 층마다 값의 분포를 다시 맞추는 배치 정규화, 입력을 출력에 그대로 더해 기울기가 지나갈 지름길을 내는 잔차 연결이다. 잔차 연결을 쓴 ResNet(2015)이 152층을 학습시킨 것이 대표적인 예다. 이런 장치 없이 평범한 MLP를 열 겹 넘게 쌓으면 오히려 얕은 망보다 학습이 안 되는 일이 흔하다. 그래서 MLP에서는 은닉층 2~4개가 좋은 출발점이다.
MNIST 분류기
데이터와 모델
MNIST는 28×28 흑백 손글씨 숫자 이미지로, 학습용 60,000장과 테스트용 10,000장이다. 아래 코드는 이미지를 텐서로 바꾼 뒤 MNIST 전체 픽셀의 평균 0.1307과 표준편차 0.3081로 정규화하고, 784차원으로 펴서 512 → 256 → 10으로 좁혀 가는 MLP에 넣는다.
import torch
import torch.nn as nn
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
train_data = datasets.MNIST(root='./data', train=True,
download=True, transform=transform)
test_data = datasets.MNIST(root='./data', train=False,
download=True, transform=transform)
train_loader = DataLoader(train_data, batch_size=64, shuffle=True)
test_loader = DataLoader(test_data, batch_size=1000)
class MLP(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Flatten(),
nn.Linear(784, 512), nn.ReLU(),
nn.Linear(512, 256), nn.ReLU(),
nn.Linear(256, 10),
)
def forward(self, x):
return self.net(x)
model = MLP()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()
def evaluate():
model.eval()
correct = 0
with torch.no_grad():
for X, y in test_loader:
correct += (model(X).argmax(1) == y).sum().item()
return correct / len(test_data)
for epoch in range(5):
model.train()
total_loss, correct = 0, 0
for X, y in train_loader:
optimizer.zero_grad()
out = model(X)
loss = criterion(out, y)
loss.backward()
optimizer.step()
total_loss += loss.item()
correct += (out.argmax(1) == y).sum().item()
print(f"Epoch {epoch+1} | Loss: {total_loss/len(train_loader):.3f}"
f" | Train: {correct/len(train_data):.3f} | Test: {evaluate():.3f}")
모델의 파라미터는 , , 을 더해 535,818개다. 출력층에 소프트맥스가 없는 것은 실수가 아니다. CrossEntropyLoss가 소프트맥스와 로그 우도를 함께 계산하므로 모델은 정규화하지 않은 점수를 그대로 내면 된다.
학습 곡선
한 번에 모델에 넣는 표본 묶음을 배치, 학습 데이터 전체를 한 바퀴 도는 것을 에폭이라 한다. 배치가 64장이면 60,000장을 도는 데 938번(마지막 배치는 32장) 가중치를 고치고, 5에폭이면 4,690번이다. 이 정도 MLP는 5에폭이면 테스트 정확도가 대략 98% 안팎에 이르는 것이 보통이다.
에폭마다 찍히는 세 숫자를 함께 읽어야 한다. 학습 손실이 계속 내려가는데 테스트 정확도도 따라 오르면 아직 배울 것이 남았다는 뜻이니 에폭을 늘려 볼 만하다. 학습 정확도는 99%를 넘어가는데 테스트 정확도가 제자리이거나 떨어지기 시작하면 과적합이 시작된 것이다. 두 값의 틈이 벌어지는 에폭이 멈출 자리다. 반대로 학습 정확도부터 낮게 머문다면 학습 데이터조차 못 맞추는 과소적합이고, 이때는 정규화를 넣을 것이 아니라 모델을 키우거나 학습률을 먼저 본다.
하나 조심할 것은 위 코드의 학습 정확도가 에폭 도중의 값을 평균한 것이라는 점이다. 에폭 초반의 덜 배운 모델로 맞힌 개수가 섞여 있으므로, 같은 시점의 테스트 정확도보다 낮게 찍힐 수 있다. 첫 에폭에서 테스트 정확도가 학습 정확도보다 높게 나와도 이상한 일이 아니다.
오분류 분석
정확도 98%는 테스트 10,000장 중 200장 정도를 틀린다는 뜻이다. 숫자 하나로 끝내지 말고 틀린 것을 직접 열어 보면 모델이 무엇을 못 하는지가 보인다.
import torch
model.eval()
wrong = []
confusion = torch.zeros(10, 10, dtype=torch.long)
with torch.no_grad():
for X, y in test_loader:
pred = model(X).argmax(1)
for t, p in zip(y, pred):
confusion[t, p] += 1
wrong += [(x, t.item(), p.item()) for x, t, p in zip(X, y, pred) if t != p]
print(confusion) # 행: 정답, 열: 예측
print(len(wrong), "장 틀림")
혼동 행렬은 정답을 행에, 예측을 열에 두고 개수를 센 표다. 대각선이 맞힌 개수이고 대각선 밖의 큰 칸이 모델이 헷갈리는 짝이다. MNIST에서는 4와 9, 3과 5, 7과 2처럼 획 몇 개만 다른 짝이 자주 걸린다. 틀린 이미지를 그려 보면 사람이 봐도 애매한 글씨가 꽤 섞여 있다. 그 몫은 모델을 키워도 잘 안 줄어든다. 반면 사람은 쉽게 읽는데 모델이 틀린 것 중에는 숫자가 한쪽으로 치우쳐 있거나 기울어진 글씨가 많다. 이것은 글 뒤쪽의 「MLP의 한계」에서 말할 구조적 약점과 이어진다.
하이퍼파라미터 선택
폭과 깊이의 출발점
학습이 정하는 것은 가중치이고, 학습이 시작되기 전에 사람이 정해 두는 층 수·폭·학습률 같은 값이 하이퍼파라미터다. 아래 표는 MLP에서 흔히 쓰는 시작점이다.
| 하이퍼파라미터 | 일반적 시작점 | 조정 방법 |
|---|---|---|
| 은닉층 수 | 2~4개 | 과소적합 시 증가 |
| 은닉층 크기 | 64~512 | 입력에 가까운 층을 넓게 |
| 활성화 함수 | ReLU | GELU도 좋음 |
| 학습률 | 1e-3 (Adam) | 스케줄러로 줄여 감 |
| 배치 크기 | 32~256 | GPU 메모리에 맞게 |
| 드롭아웃 | 0.2~0.5 | 과적합 시 추가 |
폭은 MNIST 모델처럼 입력에 가까울수록 넓고 출력으로 갈수록 좁게 두는 것이 흔한 모양이다. 앞쪽에서 많은 특징을 뽑고 뒤로 가며 추리는 셈이다. 2의 거듭제곱(64·128·256)을 많이 쓰는 것은 GPU 연산이 그런 크기에서 조금 효율적이라는 관습이지 정확도와는 상관이 없다. 순서는 작게 시작해 과소적합이 보이면 키우는 쪽이 낫다. 큰 모델에서 시작하면 과적합을 막는 장치까지 한꺼번에 맞춰야 해서 무엇이 효과를 냈는지 가르기 어렵다.
학습률과 배치 크기
학습률은 기울기 방향으로 가중치를 한 번에 얼마나 옮길지 정하는 배율이다. Adam의 기본값 0.001은 MLP에서 대체로 무난하게 출발한다. 손실이 처음부터 들쭉날쭉하거나 NaN이 나오면 학습률을 10분의 1로, 손실이 너무 천천히 내려가면 3배쯤 올려 본다. 학습 후반에 학습률을 점점 줄이는 스케줄러를 붙이면 마지막 몇 에폭에서 정확도가 조금 더 오르는 경우가 많다.
배치 크기와 학습률은 함께 움직인다. 배치가 크면 기울기의 평균이 더 정확해지고 한 에폭에 가중치를 고치는 횟수는 준다. 64에서 256으로 늘리면 에폭당 갱신 횟수가 938번에서 235번으로 4분의 1이 된다. 같은 에폭 수로 비슷한 거리를 가려면 학습률을 그만큼 올려야 한다는 경험칙이 있지만, 항상 맞지는 않으므로 배치를 바꾸면 학습률도 다시 맞춰 보는 것이 안전하다.
정규화
과적합을 누르는 장치를 통틀어 정규화(regularization)라 부른다. MLP에서 가장 흔한 것은 드롭아웃이다. 학습 중에 은닉 뉴런 일부를 무작위로 0으로 꺼서, 망이 특정 뉴런 몇 개에 기대 외우지 못하게 한다. 추론 때는 끄지 않고 전부 쓴다. PyTorch에서는 model.train()과 model.eval()이 이 전환을 맡으므로, 위 코드의 evaluate()가 eval()을 부르는 것이 중요하다.
드롭아웃은 처음부터 넣지 않는다. 학습 곡선에서 학습·테스트 정확도의 틈이 벌어지는 것을 확인한 뒤에 넣어야 효과를 볼 수 있다. 과소적합인 모델에 드롭아웃을 넣으면 가뜩이나 부족한 표현력을 더 깎는다. 다른 선택지로는 가중치 크기에 벌점을 주는 가중치 감쇠와, 테스트 성능이 더 오르지 않으면 학습을 멈추는 조기 종료가 있다.
MLP의 한계
공간 구조 손실
MLP는 입력을 순서 없는 숫자 목록으로 받는다. 28×28 이미지를 784차원 벡터로 펴는 순간, 어느 픽셀이 어느 픽셀의 옆에 있었는지라는 정보가 모델 입장에서는 사라진다. 실제로 784개 픽셀의 순서를 고정된 규칙으로 뒤섞어 모든 이미지에 똑같이 적용해도, MLP가 도달할 수 있는 정확도는 원리상 달라지지 않는다. 첫 층 가중치의 열 순서만 같이 바뀌면 되기 때문이다. 사람은 그 이미지를 전혀 못 읽는데도 말이다.
같은 이유로 MLP는 위치 이동에 약하다. 숫자 7이 오른쪽으로 두 픽셀 옮겨 가면 켜지는 입력 칸이 통째로 바뀐다. 모델은 왼쪽의 7과 오른쪽의 7을 별개의 패턴으로 따로 배워야 한다. 앞에서 오분류 중 치우친 글씨가 많았던 까닭이 여기 있다.
파라미터 규모
완전 연결 층의 파라미터는 입력 크기에 비례한다. MNIST 모델에서 첫 층 하나가 401,920개로 전체의 75%를 차지한다. 입력이 커지면 이 비율은 금방 감당할 수 없게 된다. 224×224 컬러 이미지는 입력이 150,528개라, 뉴런 1,024개짜리 은닉층 하나만으로 가중치가 약 1억 5,414만 개다.
같은 이미지에 3×3 필터 64개짜리 합성곱 층을 쓰면 파라미터는 개다. 필터 하나를 이미지 전체에 옮겨 가며 같은 가중치로 쓰기 때문에, 이미지 크기가 커져도 파라미터 수가 늘지 않는다. 이 가중치 공유가 위치 이동에 대한 약점까지 함께 덜어 준다 — 어디에 있든 같은 필터가 같은 모양에 반응하기 때문이다.
CNN과 트랜스포머
그래서 이미지에는 합성곱 신경망(CNN)이, 문장처럼 순서가 있는 데이터에는 RNN이나 트랜스포머가 쓰인다. 둘 다 데이터가 가진 구조 — 이웃한 픽셀이 관련 있다, 단어의 차례가 뜻을 바꾼다 — 를 모델 설계에 미리 넣어서, MLP가 데이터에서 처음부터 배워야 했던 것을 공짜로 얻는다.
그렇다고 MLP가 사라진 것은 아니다. CNN 끝에서 추출한 특징을 받아 클래스를 고르는 분류 헤드가 MLP이고, 트랜스포머의 각 블록에서 어텐션 다음에 오는 피드포워드 층도 토큰마다 따로 적용되는 2층 MLP다. 표 형태의 데이터처럼 입력 순서에 뜻이 없는 문제에서는 지금도 MLP가 첫 선택지다. 구조를 모르는 데이터에는 MLP를, 구조를 아는 데이터에는 그 구조를 담은 모델을 쓴다고 정리할 수 있다.
MLP를 깊게 쌓을 때 가장 먼저 부딪히는 것은 가중치를 어떤 값에서 출발시키느냐다. 앞에서 깊이의 비용으로 짚은 초기화 문제를 다음 글에서 Xavier와 He 초기화로 이어서 다룬다.
읽어주셔서 감사합니다. 😊

