흐름을 이해해야 층을 쌓을 수 있음!






Conv2D(
filters=32
, kernel_size=(5,5)
, padding="valid"
, input_shape=(28,28,1)
, activation="relu"
, strides=(2,2)
)
class CNN(nn.Module):
def __init__(self): # 이미지 데이터는 크기가 fixed → 굳이 input/output 받을 필요가 없음
super(CNN, self).__init__()
# 특성추출부 (Convolution, Pooling)
self.layer1 = nn.Sequential(
nn.Conv2d( # 2차원의 이미지 데이터를 사용하는 함수: nn.Conv2d(입력, 필터 개수, 필터 크기, 스트라이드, 패딩)
in_channels=1 # 입력 받는 색상 채널을 의미
, out_channels=32 # 필터의 개수를 의미: 한 개의 이미지를 몇 개로 추출할 것인지
, kernel_size=3 # 커널 사이즈(3x3)
, stride=1
, padding=1 # 1: zero padding을 의미
)
, nn.ReLU() # Conv 층은 항상 활성화 함수를 데리고 다님
, nn.MaxPool2d(
kernel_size=2 # 커널 사이즈(2x2)
, stride=2
)
)
# 28*28 → 14*14
self.layer2 = nn.Sequential(
nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)
, nn.ReLU()
, nn.MaxPool2d(kernel_size=2, stride=2)
)
# 14*14 → 7*7
# 분류부 (완전연결층; fully connected layer)
self.fc = nn.Linear(
in_features=7*7*64
, out_features=10
, bias=True
)
# in_features = 가로 픽셀 * 세로 픽셀 * 이전 층의 필터 개수
def forward(self, x):
h = self.layer1(x)
h = self.layer2(h)
h = h.view(h.size(0),-1) # 데이터 구조 변경 → Flatten() 역할
y = self.fc(h)
return y
# CNN 모델 객체 생성 → GPU로 이동
model = CNN().to(device)

# 학습 파라미터 설정
learning_rate = 0.001
n_epochs = 15
print_interval = 1
lowest_loss = np.inf
lowest_epoch = np.inf
best_model = None
early_stop = 5
# 손실 함수, 최적화 함수
loss_func = nn.CrossEntropyLoss().to(device)
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
train_loss_his, valid_loss_his = [], []
train_acc_his, valid_acc_his = [], []
for i in tqdm(range(n_epochs)):
train_acc, valid_acc, train_loss, valid_loss = 0, 0, 0, 0
y_pred_list = []
# dataloader 안에 배치 사이즈로 데이터가 분리되어 있음 → 불러오기
for X_train, y_train in train_loader:
# GPU로 이동
X_train = X_train.to(device)
y_train = y_train.to(device)
optimizer.zero_grad()
y_pred = model(X_train)
# loss 계산
loss = loss_func(y_pred, y_train)
# 정확도 계산
corr_cnt = y_pred.argmax(dim=1) == y_train
acc = corr_cnt.float().mean().item() # item(): 값만 추출
# .item() 써야 하는 이유: .mean()까지만 하면 tensor(0.98545, device = 'cuda:0')가 나옴
# 역전파
loss.backward()
# 업데이트
optimizer.step()
train_loss += float(loss)
train_acc += float(acc)
# 미니 배치 결과를 평균 내서 저장
train_loss = train_loss/len(train_loader)
train_acc = train_acc/len(train_loader)
# 검증 → valid 따로 분류하지 않아서 test 데이터로 진행
with torch.no_grad():
for X_test, y_test in test_loader:
X_test = X_test.to(device)
y_test = y_test.to(device)
y_pred = model(X_test)
loss = loss_func(y_pred, y_test)
acc = (y_pred.argmax(dim=1) == y_test).float().mean().item()
valid_loss += float(loss)
valid_acc += float(acc)
y_pred_list.append(y_pred)
valid_loss = valid_loss/len(test_loader)
valid_acc = valid_acc/len(test_loader)
train_loss_his.append(train_loss)
train_acc_his.append(train_acc)
valid_loss_his.append(valid_loss)
valid_acc_his.append(valid_acc)
if (i+1) % print_interval == 0:
print(f"\t epoch {i+1}\n loss: {train_loss:.4f}, acc: {train_acc:.4f}, val_loss: {valid_loss:.4f}, valid_acc: {valid_acc:.4f} lowest_loss: {lowest_loss:.4f}")
if valid_loss <= lowest_loss:
lowest_loss = valid_loss
lowest_epoch = i
best_model = deepcopy(model.state_dict())
else:
if early_stop > 0 and lowest_epoch + early_stop < i+1:
print(f"{early_stop} epochs 동안 모델이 개선되지 않았음")
break
print(f"{lowest_epoch} epoch에서 가장 낮은 검증 손실값: {lowest_loss}")
model.load_state_dict(best_model)
7%|▋ | 1/15 [00:12<02:57, 12.66s/it] epoch 1
loss: 0.2036, acc: 0.9395, val_loss: 0.0584, valid_acc: 0.9819 lowest_loss: inf
13%|█▎ | 2/15 [00:23<02:28, 11.44s/it] epoch 2
loss: 0.0587, acc: 0.9825, val_loss: 0.0551, valid_acc: 0.9812 lowest_loss: 0.0584
20%|██ | 3/15 [00:33<02:12, 11.05s/it] epoch 3
loss: 0.0433, acc: 0.9862, val_loss: 0.0501, valid_acc: 0.9851 lowest_loss: 0.0551
27%|██▋ | 4/15 [00:44<01:59, 10.86s/it] epoch 4
loss: 0.0370, acc: 0.9883, val_loss: 0.0378, valid_acc: 0.9881 lowest_loss: 0.0501
33%|███▎ | 5/15 [00:56<01:51, 11.15s/it] epoch 5
loss: 0.0298, acc: 0.9905, val_loss: 0.0330, valid_acc: 0.9902 lowest_loss: 0.0378
40%|████ | 6/15 [01:06<01:38, 10.99s/it] epoch 6
loss: 0.0245, acc: 0.9925, val_loss: 0.0324, valid_acc: 0.9891 lowest_loss: 0.0330
47%|████▋ | 7/15 [01:17<01:27, 10.92s/it] epoch 7
loss: 0.0214, acc: 0.9928, val_loss: 0.0326, valid_acc: 0.9898 lowest_loss: 0.0324
53%|█████▎ | 8/15 [01:32<01:25, 12.26s/it] epoch 8
loss: 0.0175, acc: 0.9945, val_loss: 0.0342, valid_acc: 0.9889 lowest_loss: 0.0324
60%|██████ | 9/15 [01:43<01:10, 11.72s/it] epoch 9
loss: 0.0139, acc: 0.9953, val_loss: 0.0329, valid_acc: 0.9900 lowest_loss: 0.0324
67%|██████▋ | 10/15 [01:53<00:56, 11.32s/it] epoch 10
loss: 0.0130, acc: 0.9957, val_loss: 0.0357, valid_acc: 0.9897 lowest_loss: 0.0324
67%|██████▋ | 10/15 [02:04<01:02, 12.44s/it] epoch 11
loss: 0.0113, acc: 0.9963, val_loss: 0.0362, valid_acc: 0.9898 lowest_loss: 0.0324
5 epochs 동안 모델이 개선되지 않았음
5 epoch에서 가장 낮은 검증 손실값: 0.032391682291054165
<All keys matched successfully>
# 시각화
fig, ax = plt.subplots(2,1,figsize=(8,10))
# loss
ax[0].plot(range(1,len(train_loss_his)+1), train_loss_his, label="train loss")
ax[0].plot(range(1,len(valid_loss_his)+1), valid_loss_his, label="valid loss")
ax[0].set_title("loss")
ax[0].set_xlabel("epoch")
ax[0].set_ylabel("loss")
ax[0].grid()
ax[0].legend()
# accuracy
ax[1].plot(range(1,len(train_acc_his)+1), train_acc_his, label="train acc")
ax[1].plot(range(1,len(valid_acc_his)+1), valid_acc_his, label="train acc")
ax[1].set_title("accuracy")
ax[1].set_xlabel("epoch")
ax[1].set_ylabel("accuracy")
ax[1].grid()
ax[1].legend()
plt.tight_layout()
plt.show()

# lib 폴더가 없을 경우 오류가 발생
import os
os.makedirs("lib", exist_ok=True)
%%writefile ./lib/util.py # 지금부터 이 셀에 있는 모든 내용을 util.py 파일로 저장하겠다는 코드
from tqdm import tqdm
from copy import deepcopy
import numpy as np
import torch
# 함수 정의
def fit(
model
, train_loader
, test_loader
, loss_func
, optimizer
, device
, n_epochs
, print_interval=1
, lowest_loss=np.inf
, lowest_epoch=np.inf
, early_stop=5
):
train_loss_his,train_acc_his,valid_loss_his,valid_acc_his=[],[],[],[]
best_model=None
# DataLoader 안에 배치 사이즈로 데이터가 분리되어 있음
for i in tqdm(range(n_epochs)):
train_loss,train_acc,valid_loss,valid_acc=0,0,0,0
y_pred_list=[]
# 학습
for X_train,y_train in train_loader:
# GPU로 이동
X_train=X_train.to(device)
y_train=y_train.to(device)
# 최적화 함수 초기화
optimizer.zero_grad()
# 모델 순전파
y_pred=model(X_train)
# loss 계산
loss=loss_func(y_pred,y_train)
# accuracy 계산
acc=(torch.argmax(y_pred,dim=1)==y_train).float().mean().item()
# 역전파
loss.backward()
# w,b 업데이트
optimizer.step()
# 배치별 loss값 모으기
train_loss+=float(loss)
# 배치별 accuracy값 모으기
train_acc+=float(acc)
# epoch 평균 loss, acc 값
train_loss=train_loss/len(train_loader)
train_acc=train_acc/len(train_loader)
# 검증
with torch.no_grad():
for X_test,y_test in test_loader:
X_test=X_test.to(device)
y_test=y_test.to(device)
y_pred=model(X_test)
loss=loss_func(y_pred,y_test)
acc=(torch.argmax(y_pred,dim=1)==y_test).float().mean().item()
valid_loss+=float(loss)
valid_acc+=float(acc)
y_pred_list.append(y_pred)
valid_loss=valid_loss/len(test_loader)
valid_acc=valid_acc/len(test_loader)
train_loss_his.append(train_loss)
train_acc_his.append(train_acc)
valid_loss_his.append(valid_loss)
valid_acc_his.append(valid_acc)
if (i+1)%print_interval==0:
print(f"epoch{i+1} acc:{train_acc:.4f} loss:{train_loss:.4f} val_acc:{valid_acc:.4f} val_loss:{valid_loss:.4f} lowest_loss:{lowest_loss:.4f}")
if valid_loss<=lowest_loss:
lowest_loss=valid_loss
lowest_epoch=i
best_model=deepcopy(model.state_dict())
else:
if early_stop>0 and lowest_epoch+early_stop<i+1:
print(f"{early_stop} epochs 동안 모델이 개선되지 않았음")
break
print(f"{lowest_epoch+1} epoch에서 가장 낮은 검증 손실값: {lowest_loss}")
return best_model, train_acc_his, train_loss_his, valid_acc_his, valid_loss_his
Writing ./lib/util.py
# 기존 파일이 있을 경우: Overwriting ./lib/util.py
class CNN2(nn.Module):
def __init__(self):
super().__init__()
# layer 1 Conv(필터: 32, 커널: (3,3), 스트라이드: 1, 패딩: zero padding), 폴링(커널: 2, 스트라이드: 2)
self.layer1=nn.Sequential(
nn.Conv2d(1,32,kernel_size=3,stride=1,padding=1)
, nn.ReLU()
, nn.MaxPool2d(kernel_size=2,stride=2)
) # 28x28 → 14x14
# layer 2 Conv(필터: 64, 커널: (3,3), 스트라이드: 1, 패딩: zero padding), 폴링(커널: 2, 스트라이드: 2)
self.layer2=nn.Sequential(
nn.Conv2d(32,64,kernel_size=3,stride=1,padding=1)
, nn.ReLU()
, nn.MaxPool2d(kernel_size=2,stride=2)
) # 14x14 → 7x7
# layer 3 Conv(필터: 128, 커널: (3,3), 스트라이드: 1, 패딩: zero padding), 폴링(커널: 2, 스트라이드: 2, 패딩: zero padding)
self.layer3=nn.Sequential(
nn.Conv2d(64,128,kernel_size=3,stride=1,padding=1)
, nn.ReLU()
, nn.MaxPool2d(kernel_size=2,stride=2,padding=1)
) # 7x7 → 4x4
# 분류기 fc1(입력: ?, 출력: 625), fc2(입력: ?, 출력: ?)
self.fc1=nn.Linear(in_features=4*4*128,out_features=625,bias=True)
self.layer4=nn.Sequential(self.fc1,nn.ReLU(),nn.Dropout(0.2)) # 20퍼센트 비율로 뉴런을 비활성화 해 과대적합을 방지할 때 사용
self.fc2=nn.Linear(625,10,bias=True)
def forward(self, x):
# 특성추출부
h=self.layer1(x)
h=self.layer2(h)
h=self.layer3(h)
# 1차원 변환
h=h.view(h.size(0),-1)
# 분류부
h=self.layer4(h)
y=self.fc2(h)
return y
model2 = CNN2().to(device)
nn.Dropout(비활성화 비율)
from lib.util import fit # lib/util.py에 정의된 fit 함수를 사용하겠다는 뜻
# 손실 함수, 최적화 함수
loss_func = torch.nn.CrossEntropyLoss().to(device)
optimizer = torch.optim.Adam(model2.parameters())
b_model, acc, loss, val_acc, val_loss = fit(
model = model2
, train_loader=train_loader
, test_loader=test_loader
, loss_func=loss_func
, optimizer=optimizer
, device=device
, n_epochs=10
, early_stop=3
)
10%|█ | 1/10 [00:12<01:55, 12.87s/it]epoch1 acc:0.9395 loss:0.1890 val_acc:0.9847 val_loss:0.0448 lowest_loss:inf
20%|██ | 2/10 [00:25<01:39, 12.48s/it]epoch2 acc:0.9841 loss:0.0494 val_acc:0.9875 val_loss:0.0414 lowest_loss:0.0448
30%|███ | 3/10 [00:37<01:27, 12.49s/it]epoch3 acc:0.9887 loss:0.0352 val_acc:0.9913 val_loss:0.0303 lowest_loss:0.0414
40%|████ | 4/10 [00:49<01:14, 12.39s/it]epoch4 acc:0.9918 loss:0.0253 val_acc:0.9912 val_loss:0.0295 lowest_loss:0.0303
50%|█████ | 5/10 [01:02<01:01, 12.32s/it]epoch5 acc:0.9930 loss:0.0219 val_acc:0.9906 val_loss:0.0312 lowest_loss:0.0295
60%|██████ | 6/10 [01:14<00:49, 12.39s/it]epoch6 acc:0.9948 loss:0.0165 val_acc:0.9935 val_loss:0.0273 lowest_loss:0.0295
70%|███████ | 7/10 [01:27<00:37, 12.42s/it]epoch7 acc:0.9953 loss:0.0152 val_acc:0.9908 val_loss:0.0340 lowest_loss:0.0273
80%|████████ | 8/10 [01:39<00:24, 12.31s/it]epoch8 acc:0.9959 loss:0.0122 val_acc:0.9915 val_loss:0.0292 lowest_loss:0.0273
90%|█████████ | 9/10 [01:51<00:12, 12.19s/it]epoch9 acc:0.9967 loss:0.0108 val_acc:0.9930 val_loss:0.0243 lowest_loss:0.0273
100%|██████████| 10/10 [02:02<00:00, 12.28s/it]epoch10 acc:0.9970 loss:0.0093 val_acc:0.9861 val_loss:0.0536 lowest_loss:0.0243
9 epoch에서 가장 낮은 검증 손실값: 0.024339423169221844
# 시각화
fig, ax = plt.subplots(1,2,figsize=(16,4))
# loss
ax[0].plot(range(1,len(loss)+1), loss, label="train loss")
ax[0].plot(range(1,len(val_loss)+1), val_loss, label="valid loss")
ax[0].set_title("loss")
ax[0].set_xlabel("epoch")
ax[0].set_ylabel("loss")
ax[0].grid()
ax[0].legend()
# accuracy
ax[1].plot(range(1,len(acc)+1), acc, label="train acc")
ax[1].plot(range(1,len(val_acc)+1), val_acc, label="valid acc")
ax[1].set_title("accuracy")
ax[1].set_xlabel("epoch")
ax[1].set_ylabel("accuracy")
ax[1].grid()
ax[1].legend()
plt.tight_layout()
plt.show()




| 증강 기법 | 설명 |
|---|---|
| Horizontal Flip | 이미지를 좌우로 뒤집음 (mirroring) |
| Vertical Flip | 이미지를 상하로 뒤집음 |
| Rotation +45° | 이미지를 +45도 회전 |
| Rotation -45° | 이미지를 -45도 회전 |
| Blur | 이미지에 블러 효과를 적용하여 흐릿하게 만듦 |
| Brighter | 이미지의 밝기를 증가시킴 |
| Noise Added | 이미지에 **잡음(Noise)**를 추가 |
| Darker | 이미지의 밝기를 감소시켜 어둡게 만듦 |
| Grayscale | 이미지를 **흑백(Grayscale)**으로 변환 |
| Crop | 이미지의 일부를 잘라냄(Cropping) |


# 이미지 불러오기
from PIL import Image
pil_image=Image.open("./data/cat.png")
plt.imshow(pil_image)
plt.axis("off")
plt.show()

plt.axis("off")를 안 하면 아래와 같이 출력된다:
# 파이토치 내부적으로 정의하고 있는 정책을 사용하여 이미지 증식 설정: 이미지 증강 정책
# 이미지에 최적화된 증강 방법들을 조합하여 자동으로 적용(Auto Augment)
transform=transforms.AutoAugment(transforms.AutoAugmentPolicy.IMAGENET) # IMAGENET: 대규모 이미지 데이터셋 이름(약 1,400만 장 정도(2만 1천 개의 카테고리) 제공)
# 증식 결과 출력
for i in range(9):
# NumPy ndarray 형태의 이미지 → tensor 형태로 변환
# NumPy: (Height, Width, Channel) 순으로 배치
# Tensor: (C, H, W) 순으로 배치
# 따라서 shape 변경해 줘야 한다 → permute(): 차원의 순서를 변경
tensor=torch.as_tensor(np.asarray(pil_image)).permute(2,0,1)
# 증식 수행
applied_image=transform(tensor)
# 결과 출력
plt.subplot(3,3,i+1)
plt.imshow(applied_image.permute(1,2,0).numpy()) # permute()는 텐서에만 사용 가능
plt.axis('off')







앞으로는 모델을 처음부터 끝까지 만들기보다는 다른 사람들이 만든 모델 가져와서 fine tunning 하는 일이 훨씬 많기 때문에 해당 개념을 잘 알아둬야 함


.from_numpy(), .as_tensor() 차이.permute().permute()는 텐서에만 적용 가능함view(): reshapes the tensor to a different but compatible shape)subplot()