새 질병을 공부하는 내과 의사가 “해부학/생리학” 같은 기본기를 재사용하듯,
모델도 “엣지/질감/패턴” 같은 기본 시각 특징은 그대로 재사용하고 마지막 결정 부분만 새로 학습한다.
CNN은 보통 아래와 같이 특징이 깊어질수록 “특화”된다.
그래서 전이학습은 보통:
백본(backbone)은 동결하고, 마지막 레이어(분류기)만 학습한다.
import torch
import torch.nn as nn
from torchvision import models
# 사전학습된 ResNet 로드
model = models.resnet18(pretrained=True) # ImageNet 사전학습
# 모든 파라미터 동결
for param in model.parameters():
param.requires_grad = False
# 마지막 FC층만 교체
num_features = model.fc.in_features
model.fc = nn.Linear(num_features, num_classes)
사전학습 모델의 일부 또는 전체를 재학습한다.
from torchvision import models
import torch.nn as nn
model = models.resnet18(pretrained=True)
num_features = model.fc.in_features
model.fc = nn.Linear(num_features, num_classes)
# 전략 1: 전체 미세조정
for param in model.parameters():
param.requires_grad = True
# 전략 2: 일부만 미세조정 (마지막 몇 층만)
for name, param in model.named_parameters():
if "layer4" in name or "fc" in name:
param.requires_grad = True
else:
param.requires_grad = False
| 조건 | 추천 |
|---|---|
| 데이터 적고, 도메인 유사 | Feature Extraction |
| 데이터 많고, 도메인 유사 | Full Fine-tuning |
| 데이터 적고, 도메인 다름(의료영상 등) | Feature Extraction(주의) |
| 데이터 많고, 도메인 다름 | Fine-tuning(초기층 동결부터) |
사전학습 모델이 ImageNet 통계(mean/std)에 맞춰 학습됬다면, 입력 정규화도 동일하게 맞추는 게 기본이다.
import torch
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
# 검증/테스트용(고정 변환)
transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
# 학습용(증강 포함)
train_transform = transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
train_dataset = datasets.ImageFolder('data/train', transform=train_transform)
val_dataset = datasets.ImageFolder('data/val', transform=transform)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)
import torch.nn as nn
from torchvision import models
def create_transfer_model(num_classes, freeze_backbone=True):
model = models.resnet18(pretrained=True)
if freeze_backbone:
for param in model.parameters():
param.requires_grad = False
num_features = model.fc.in_features
model.fc = nn.Sequential(
nn.Dropout(0.5),
nn.Linear(num_features, num_classes)
)
return model
model = create_transfer_model(num_classes=5, freeze_backbone=True)
데이터가 적어 특정 특징들만 집중해서 외워버릴 수 있음(즉, 과적합 위험이 크다는 말)
이를 방지하기 위해(일반화 성능을 높이기 위해) dropout을 사용.
import torch
import torch.optim as optim
import torch.nn as nn
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
params_to_update = [p for p in model.parameters() if p.requires_grad]
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(params_to_update, lr=0.001)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=7, gamma=0.1)
optimizer가 가진 학습률(lr)로 시작하고,step_size=7 에폭마다gamma=0.1을 곱해서 lr을 낮춘다.for epoch in range(num_epochs):
train(...)
validate(...)
scheduler.step() # epoch 끝날 때 lr 업데이트
한 번에 전체를 풀어버리면(Full FT) 과적합/불안정이 올 수 있다.
그래서 실무에선 “조금씩 풀어가는 방식”을 자주 쓴다.
전략
import torch.optim as optim
# 1단계: FC층만 학습
for param in model.parameters():
param.requires_grad = False
for param in model.fc.parameters():
param.requires_grad = True
optimizer = optim.Adam(model.fc.parameters(), lr=0.001)
# 학습...
# 2단계: layer4 + FC 학습
for name, param in model.named_parameters():
if "layer4" in name or "fc" in name:
param.requires_grad = True
optimizer = optim.Adam(
filter(lambda p: p.requires_grad, model.parameters()),
lr=0.0001
)
# 학습...
# 3단계: 전체 미세조정
for param in model.parameters():
param.requires_grad = True
optimizer = optim.Adam(model.parameters(), lr=0.00001)
# 학습...
백본은 아주 조금만 움직이고(작은 lr), 헤드는 더 크게 학습(큰 lr).
param_groups = [
{'params': model.backbone.parameters(), 'lr': 1e-5},
{'params': model.classifier.parameters(), 'lr': 1e-3}
]
optimizer = optim.Adam(param_groups)
딥러닝은 분류만 하는 게 아니라, 아래처럼 출력 형태 자체가 완전히 달라진다.
| Task | 설명 | 출력 |
|---|---|---|
| 분류(Classification) | 이미지 전체 클래스 | 클래스 라벨 |
| 탐지(Detection) | 물체 위치+클래스 | 바운딩 박스 + 클래스 |
| 분할(Segmentation) | 픽셀 단위 분류 | 분할 마스크 |
| 생성(Generation) | 새로운 데이터 생성 | 생성 이미지 |
예시를 들자면:
의료에서 예:
의료에서 예:

DoubleConv(Conv → BN → ReLU 두 번)로 특징 추출MaxPool2d(2)로 해상도를 1/2로 줄이면서 더 큰 문맥을 보게 함ConvTranspose2d로 업샘플링(해상도 복원)import torch
import torch.nn as nn
class DoubleConv(nn.Module):
"""Conv → BN → ReLU → Conv → BN → ReLU"""
def __init__(self, in_channels, out_channels):
super().__init__()
self.conv = nn.Sequential(
nn.Conv2d(in_channels, out_channels, 3, padding=1),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True),
nn.Conv2d(out_channels, out_channels, 3, padding=1),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True)
)
def forward(self, x):
return self.conv(x)
class UNet(nn.Module):
def __init__(self, in_channels=1, out_channels=1):
super().__init__()
# Encoder
self.enc1 = DoubleConv(in_channels, 64)
self.enc2 = DoubleConv(64, 128)
self.enc3 = DoubleConv(128, 256)
self.enc4 = DoubleConv(256, 512)
self.pool = nn.MaxPool2d(2)
# Bottleneck
self.bottleneck = DoubleConv(512, 1024)
# Decoder
self.up4 = nn.ConvTranspose2d(1024, 512, 2, stride=2)
self.dec4 = DoubleConv(1024, 512)
self.up3 = nn.ConvTranspose2d(512, 256, 2, stride=2)
self.dec3 = DoubleConv(512, 256)
self.up2 = nn.ConvTranspose2d(256, 128, 2, stride=2)
self.dec2 = DoubleConv(256, 128)
self.up1 = nn.ConvTranspose2d(128, 64, 2, stride=2)
self.dec1 = DoubleConv(128, 64)
# Output
self.out = nn.Conv2d(64, out_channels, 1)
def forward(self, x):
e1 = self.enc1(x)
e2 = self.enc2(self.pool(e1))
e3 = self.enc3(self.pool(e2))
e4 = self.enc4(self.pool(e3))
b = self.bottleneck(self.pool(e4))
d4 = self.dec4(torch.cat([self.up4(b), e4], dim=1))
d3 = self.dec3(torch.cat([self.up3(d4), e3], dim=1))
d2 = self.dec2(torch.cat([self.up2(d3), e2], dim=1))
d1 = self.dec1(torch.cat([self.up1(d2), e1], dim=1))
return self.out(d1)
model = UNet(in_channels=1, out_channels=1) # 흑백 입력, 이진 분할
self.up4(b) : 업샘플한 “요약 정보(문맥)”e4 : 인코더에서 저장해둔 “디테일/경계 정보”torch.cat(..., dim=1) : 채널 방향으로 붙임(Concatenate)즉, “큰 그림(문맥)” + “디테일(경계)”를 합쳐서 픽셀 단위로 더 정확하게 만드는 구조
criterion = nn.BCEWithLogitsLoss()
criterion = nn.CrossEntropyLoss()
예시:
이때 BCE/CE만 쓰면 모델이 이런 “편법”을 쓸 수 있다:
그래서 Dice는 “겹치는 정도”를 목표로 한다.
(“예측한 병변 영역”과 “정답 병변 영역”이 얼마나 많이 겹치냐)

그래서 Dice Loss는:
📌 의문 1. 그럼 Dice는 클수록 좋은 지표 아닌가?
📌 의문 2. DiceLoss는 미분을 어떻게 하지?
즉, 집합 연산처럼 보이지만 실제로는:
intersection = Σ (p_i * y_i)sum_p = Σ p_isum_y = Σ y_i (상수)이건 전부 덧셈/곱셈/나눗셈이라 미분 가능하다.

이때 특정 픽셀 에 대한 미분은 (형태만 보면):


근데 BCE와 차이가 나는 부분은:
✅ Dice는 “전역 합(P, I)”이 들어가서 전체 픽셀 상황을 보며 업데이트가 걸린다
그래서 병변이 작을 때 Dice가 강해지는 이유가 여기서 나온다:
class DiceLoss(nn.Module):
def forward(self, pred, target):
smooth = 1e-5
pred = torch.sigmoid(pred)
intersection = (pred * target).sum()
return 1 - (2. * intersection + smooth) / (pred.sum() + target.sum() + smooth)
이미지에서 객체의 위치(바운딩 박스)와 클래스를 동시에 예측.
탐지는 보통 1개 객체당:
이 출력된다.
한 번에 “여기 객체 있어?” + “어떤 클래스야?” + “박스는 어디야?”를 동시에 예측
2단계로 나눔
1) “객체일 것 같은 후보 박스들”을 먼저 뽑고(Region Proposal)
2) 그 후보에 대해 “정확한 분류 + 박스 보정”을 함

예측 박스와 정답 박스가 얼마나 겹치는지((겹치는 부분의 넓이) / (두 박스를 합친 전체 넓이))

클래스별 AP(정밀도-재현율 곡선 아래 면적)를 평균낸 값
mAP는 대략 이렇게 만들어진다:
1) confidence score(“진짜 객체일 확률”) 높은 박스부터 정렬
2) 임계값을 바꿔가며 precision-recall 곡선을 그림
3) 그 곡선 아래 면적이 AP
4) 모든 클래스의 AP 점수를 다 더한 뒤, 클래스 개수로 나눈 것이 mAP
mAP가 높다 → 컴퓨터가 다양한 종류의 물체를 전반적으로 아주 잘 탐지한다
cf)mAP@0.5의 의미:
위조지폐범과 경찰의 끝없는 숨바꼭질
import torch
import torch.nn as nn
class Generator(nn.Module):
def __init__(self, latent_dim=100, img_channels=1, img_size=28):
super().__init__()
self.model = nn.Sequential(
nn.Linear(latent_dim, 256),
nn.LeakyReLU(0.2),
nn.Linear(256, 512),
nn.LeakyReLU(0.2),
nn.Linear(512, 1024),
nn.LeakyReLU(0.2),
nn.Linear(1024, img_channels * img_size * img_size),
nn.Tanh()
)
self.img_shape = (img_channels, img_size, img_size)
def forward(self, z):
img = self.model(z)
return img.view(img.size(0), *self.img_shape)
class Discriminator(nn.Module):
def __init__(self, img_channels=1, img_size=28):
super().__init__()
self.model = nn.Sequential(
nn.Flatten(),
nn.Linear(img_channels * img_size * img_size, 512),
nn.LeakyReLU(0.2),
nn.Linear(512, 256),
nn.LeakyReLU(0.2),
nn.Linear(256, 1),
nn.Sigmoid()
)
def forward(self, img):
return self.model(img)
def train_gan(generator, discriminator, dataloader, epochs=100):
criterion = nn.BCELoss()
opt_g = torch.optim.Adam(generator.parameters(), lr=0.0002)
opt_d = torch.optim.Adam(discriminator.parameters(), lr=0.0002)
for epoch in range(epochs):
for real_imgs, _ in dataloader:
batch_size = real_imgs.size(0)
real_labels = torch.ones(batch_size, 1)
fake_labels = torch.zeros(batch_size, 1)
# Discriminator
z = torch.randn(batch_size, 100)
fake_imgs = generator(z)
d_real = discriminator(real_imgs)
d_fake = discriminator(fake_imgs.detach())
d_loss = criterion(d_real, real_labels) + criterion(d_fake, fake_labels)
opt_d.zero_grad()
d_loss.backward()
opt_d.step()
# Generator
z = torch.randn(batch_size, 100)
fake_imgs = generator(z)
d_fake = discriminator(fake_imgs)
g_loss = criterion(d_fake, real_labels)
opt_g.zero_grad()
g_loss.backward()
opt_g.step()
1) z = torch.randn(batch_size, 100)
2) fake_imgs = generator(z)
3) d_real = discriminator(real_imgs)
4) d_fake = discriminator(fake_imgs.detach())
5) d_loss = ... 및 업데이트
.detach()는 왜 있는가?그럼 아래쪽 Generator 학습에 z = ...가 또 있는 이유는?
모래성을 부수고, 부서진 역순으로 다시 쌓기
즉, 모델이 하는 일은:
“이 정도로 망가진 이미지에서, 노이즈를 얼마나 빼야 하나?”를 반복적으로 배우는 것
# 1. 원본 이미지(x_0)와 랜덤 스텝(t) 고르기
# 2. 순수한 노이즈(noise)를 무작위로 생성
noise = torch.randn_like(x_0)
# 3. 위에서 본 '수학의 마법' 공식(가우시안 분포 성질을 이용한 공식)으로 t번째 망가진 이미지(x_t) 한번에 생성
x_t = q_sample(x_0, t, noise)
# 4. 인공지능(U-Net)에게 망가진 이미지(x_t)와 현재 스텝(t)을 주고 노이즈를 예측
predicted_noise = unet(x_t, t)
# 5. 오차(Loss) 계산: 실제 넣었던 노이즈(noise)와 AI가 예측한 노이즈(predicted_noise)의 차이
loss = MSE(noise, predicted_noise)
# 6. 오차를 줄이는 방향으로 AI 업데이트 (학습)
loss.backward()
optimizer.step()
오토인코더는 이미지를 아주 작은 '숫자들의 압축본(잠재 공간)'으로 만듦.
하지만 VAE는 한 발 더 나아가, 압축본을 고정된 숫자가 아니라 '범위(확률 분포)'로 만듦.
이렇게 여지를 두면 비슷한 듯 다른 새로운 이미지를 부드럽게 만들어낼 수 있다.
여기서 문제!
모델이 스스로 오답 노트를 쓰며 발전하려면, 역전파(Backpropagation) 계산 과정이 수학적으로 쭉 이어져 있어야 한다.
하지만 중간에 '랜덤 주사위 던지기(샘플링)'가 들어가면 계산의 맥락이 뚝 끊겨버린다.
특징
import torch
import torch.nn as nn
class VAE(nn.Module):
def __init__(self, input_dim=784, hidden_dim=400, latent_dim=20):
super().__init__()
self.encoder = nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.ReLU()
)
self.fc_mu = nn.Linear(hidden_dim, latent_dim)
self.fc_var = nn.Linear(hidden_dim, latent_dim)
self.decoder = nn.Sequential(
nn.Linear(latent_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, input_dim),
nn.Sigmoid()
)
def encode(self, x):
h = self.encoder(x)
return self.fc_mu(h), self.fc_var(h)
def reparameterize(self, mu, log_var):
std = torch.exp(0.5 * log_var)
eps = torch.randn_like(std)
return mu + eps * std
def decode(self, z):
return self.decoder(z)
def forward(self, x):
mu, log_var = self.encode(x.view(-1, 784))
z = self.reparameterize(mu, log_var)
return self.decode(z), mu, log_var
LSTM이 기울기 소실을 어느 정도 완화했지만, 여전히 구조적 한계가 남는다.
Self-Attention은 시퀀스 내 모든 위치가 서로를 참고할 수 있게 해서
멀리 떨어진 토큰끼리도 바로 상호작용할 수 있다.
각 토큰이:
문장 속에 "Bank"라는 단어가 있다고 치자.
이게 은행인지 강둑인지 의미를 정확히 알려면 주변 단어들에게 물어봐야 한다.
절차:
1) Q로 K들을 전부 비교해서 “관련도 점수”를 만든다 (내적. dot product)
→ 값이 크다? = "서로 유사도가 높다, 관련이 깊다"
2) 점수가 너무 커지면 softmax가 극단적으로 쏠리니까 로 나눠 안정화(이게 Scaled Dot-Product Attention)
3) "Bank"의 Q와 "Deposit"의 K를 곱했더니 값이 아주 크게 나왔다.
→ 서로 강한 선(Attention Weight)으로 연결되고, "Deposit"이 가진 V(의미 정보)를 "Bank"에게 듬뿍 넘겨준다.
수식의 강제성:
사람은 네트워크 구조를 짤 때, 를 한 다음 거기에 를 곱하라고 수식을 고정해 둔다. 이 수식의 형태 자체가 '정보 검색(Information Retrieval)'의 구조를 띠고 있다.
오답 노트(Backpropagation)의 마법:
처음에는 모두 아무 의미 없는 랜덤 숫자로 시작한다.
당연히 번역 결과는 엉망진창이 되고, 오차(Loss)가 엄청나게 커진다.
컴퓨터는 이 오차를 줄이기 위해 미분을 하며 가중치 숫자를 조금씩 바꾼다.
역할의 자연스러운 분담:
오차를 가장 효과적으로 줄이려면 어떻게 해야 하는가?
모델 스스로 "아, 첫 번째 거울()은 다른 단어를 찾는 '질문' 형태로 숫자를 가공하고, 두 번째 거울()은 내 정체성을 나타내는 '명찰' 형태로 숫자를 가공해서 둘을 곱해야 오차가 확 줄어드는구나!"라는 것을 데이터를 수십억 번 보면서 수학적으로 깨닫게 된다.

💡 [정리] 실제 트랜스포머에서의 정확한 행렬 사이즈 흐름
(단어 3개, 임베딩 차원 512, 헤드 하나의 차원이 64라고 가정할 때)
토큰 하나의 벡터 길이(512) = '토큰의 특징의 개수'라고 비유 했을 때
한번에 다 처리하면 효율이 떨어질 수 있어서 이걸 여러개의 헤드(부서)로 나누어 처리.
일반적으로 8개로 나누고, 그래서 64가 된 것임(512 / 8 = 64)

import torch
import torch.nn as nn
import torch.nn.functional as F
import math
class ScaledDotProductAttention(nn.Module):
def __init__(self, d_k):
super().__init__()
self.scale = math.sqrt(d_k)
def forward(self, Q, K, V, mask=None):
scores = torch.matmul(Q, K.transpose(-2, -1)) / self.scale
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
attention_weights = F.softmax(scores, dim=-1)
output = torch.matmul(attention_weights, V)
return output, attention_weights
한 번의 attention만 쓰면 “한 종류의 관계”에만 강하게 쏠릴 수 있다.
Multi-head는 차원을 나눠서 여러 head가 서로 다른 관계를 학습하게 만든다.
(, , 세트를 여러개 넣어준다는 의미)
이렇게 n개의 서로 다른 관점에서 단어들을 섞은 결과물 n개가 나오면, 이것들을 가로로 길게 이어 붙인다(Concatenation).
그리고 마지막으로 (Output 가중치 행렬)라는 또 다른 거울을 한 번 더 통과시켜서, 길어진 결과를 원래 원했던 예쁜 사이즈로 다시 꾹 압축해 다음 층(Layer)으로 넘겨준다.
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, num_heads):
super().__init__()
assert d_model % num_heads == 0
self.d_model = d_model
self.num_heads = num_heads
self.d_k = d_model // num_heads
self.W_q = nn.Linear(d_model, d_model)
self.W_k = nn.Linear(d_model, d_model)
self.W_v = nn.Linear(d_model, d_model)
self.W_o = nn.Linear(d_model, d_model)
self.attention = ScaledDotProductAttention(self.d_k)
def forward(self, Q, K, V, mask=None):
batch_size = Q.size(0)
Q = self.W_q(Q).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
K = self.W_k(K).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
V = self.W_v(V).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
out, attention_weights = self.attention(Q, K, V, mask)
out = out.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
out = self.W_o(out)
return out, attention_weights
Transformer는 구조적으로 “순서”가 없다.
그래서 토큰 위치 정보를 별도로 더해준다.
대표적인 방식이 sin/cos 기반 위치 인코딩이다.
sin/cos 방식의 장점:
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=5000):
super().__init__()
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1).float()
div_term = torch.exp(torch.arange(0, d_model, 2).float() *
-(math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0)
self.register_buffer('pe', pe)
def forward(self, x):
return x + self.pe[:, :x.size(1)]
Attention은 “섞는” 역할이 강하고, FFN은 비선형 변환으로 한 번 더 “가공”한다(표현력을 늘려줌).
class FeedForward(nn.Module):
def __init__(self, d_model, d_ff=2048, dropout=0.1):
super().__init__()
self.linear1 = nn.Linear(d_model, d_ff)
self.linear2 = nn.Linear(d_ff, d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
return self.linear2(self.dropout(F.relu(self.linear1(x))))
Transformer는 “깊게 쌓기” 위해 Residual과 LayerNorm을 기본으로 쓴다.
class EncoderLayer(nn.Module):
def __init__(self, d_model, num_heads, d_ff, dropout=0.1):
super().__init__()
self.self_attention = MultiHeadAttention(d_model, num_heads)
self.feed_forward = FeedForward(d_model, d_ff, dropout)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x, mask=None):
attn_out, _ = self.self_attention(x, x, x, mask)
x = self.norm1(x + self.dropout(attn_out))
ff_out = self.feed_forward(x)
x = self.norm2(x + self.dropout(ff_out))
return x

이 기본 구조가 번역/요약/생성의 뼈대가 된다.

| 특징 | U-Net (이미지 분할) | Transformer (자연어/시퀀스) |
|---|---|---|
| 핵심 부품 | CNN (합성곱 연산) | Self-Attention (Q, K, V 연산) |
| 인코더의 역할 | 이미지를 돋보기(필터)로 줄여가며 특징 압축 | 단어들의 문맥과 관계를 Attention으로 파악해 압축 |
| 디코더의 역할 | 압축된 특징을 다시 큰 이미지 크기로 복원 (픽셀 칠하기) | 압축된 문맥을 바탕으로 새로운 시퀀스(단어) 생성 |
| 특징적 연결 | 스킵 커넥션 (인코더의 이미지를 디코더로 바로 복사해 넘겨줌) | 크로스 어텐션 (디코더가 인코더의 K, V를 참조함) |
핵심은 Patch Embedding: 이미지를 “토큰 시퀀스”로 바꾸기
즉, ViT는 “이미지의 픽셀 덩어리 = 단어 토큰”처럼 다룬다.
| 특성 | CNN | ViT |
|---|---|---|
| 학습 방식 | 지역→전역 | 전역 관계 직접 학습 |
| 데이터 요구 | 상대적으로 적음 | 대량 데이터 유리 |
| 계산/메모리 | 효율적 | 메모리 집약적 |
| 해석 | 특징맵 시각화 | Attention map 분석 |
| 의료 적용 | 국소 병변 감지 강함 | 전신 상관관계 분석 강함 |
“CNN vs Transformer”가 아니라, 실제론 서로 장점을 섞어 최적점을 찾는 흐름으로 발전 중이다.
from transformers import BertTokenizer, BertModel, BertForSequenceClassification
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertModel.from_pretrained('bert-base-uncased')
text = "Hello, how are you?"
inputs = tokenizer(text, return_tensors='pt')
outputs = model(**inputs)
last_hidden_state = outputs.last_hidden_state # (1, seq_len, 768)
pooled_output = outputs.pooler_output # (1, 768) - [CLS]
classifier = BertForSequenceClassification.from_pretrained(
'bert-base-uncased',
num_labels=2
)
from transformers import GPT2Tokenizer, GPT2LMHeadModel
tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
model = GPT2LMHeadModel.from_pretrained('gpt2')
prompt = "딥러닝은"
inputs = tokenizer(prompt, return_tensors='pt')
outputs = model.generate(
inputs['input_ids'],
max_length=50,
num_return_sequences=1,
temperature=0.7,
do_sample=True
)
generated_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(generated_text)