수달이네 기술 블로그

8. Multi-class weather dataset(날씨 이미지 데이터셋)2 + 모델 학습 본문

AI공부/딥러닝

8. Multi-class weather dataset(날씨 이미지 데이터셋)2 + 모델 학습

슬픈 수달이 2026. 2. 4. 23:35

모델 만들기

기초 모델

클래스로 모델을 만들면 가중치만 따로 저장하는 등의 모델로서의 기능을 사용할 수 있다.

class Model1(nn.Module):
    def __init__(self): 
        super(Model1, self).__init__()
        self.linear1 = nn.Linear(256*256*3, 4)
        self.flatten = nn.Flatten()

    def forward(self, x):
        x = self.flatten(x)
        x = self.linear1(x)
        return x

_ init _: 생성자

  • super로 부모 생성자의 원래 기능을 불러옴
  • self.linear1 = nn.Linear(256 * 256 * 3, 4)
    • 이미지를 입력 받자마자 출력하는 즉 선형 모델을 만들어버림(머신러닝급)
  • self.flatten = nn.Flatten()
    • 모든 변수를 일렬로 만들어줌

forward: 과정

  • flatten: 일렬로
  • linear1: 위에서 만든 선형모델

모듈 상속

모델 구성요소 관리: 레이어와 파라미터를 자동으로 관리해준다.

순전파 정의(Forward): forward()메서드로 간단하고 일관된 순전파 과정을 정의할 수 있다.

계층적 설계: 서브모듈을 활용해 복잡한 모델을 쉽게 설계할 수 있다.

유틸리티 제공: 파라미터 저장, 로드/ 학습, 추론 모드 전환 등의 기능

파이토치 호환성: 최적화, 데이터 로더 등의 파이토치의 다른 기능과 통합 가능

추상화: 저수준의 작업을 추상화하여 개발자의 생산성을 향상시켜준다.

학습 함수

def train():
    start_time = time.time()  # 실행시간 확인
    print(f'[Epoch {epoch+1}] Training...')
    model.train() 
    total = 0
    running_loss = 0.0
    running_correct = 0

    for i, batch in enumerate(train_dataloader): # train_loader의 개수만큼 돈다
        imgs, labels = batch
        imgs, labels = imgs.cuda(), labels.cuda() # device를 cuda로 설정(gpu 설정)
        outputs = model(imgs)
        optimizer.zero_grad()
        _, preds  = torch.max(outputs, 1)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        total += labels.shape[0]   # 라벨의 개수를 total에 누적
        running_loss += loss.item()  # loss값 누적
        running_correct += (preds == labels).sum(preds == labels.data) # accuracy누적
        if i % log_step == 0: # 출력
            print(f'[Batch: {i + 1}] running train loss: {running_loss / total}, running train accuracy: {running_correct / total}')

    print(f'train loss: {running_loss / total}, accuracy: {running_correct / total}')
    print("elapsed time:", time.time() - start_time) # 연산시간 출력
    return running_loss / total, (running_correct / total).item()

model.train() : 학습용임을 나타냄(평가용이 아님)

outputs = model(imgs): 모델에 이미지를 넣어 결과를 받음

_, preds = torch.max(outputs, 1) : 가장 큰 확률의 클래스를 결과로 받음

loss = criterion(outputs, labels) : 해당 결과로 실제값과 비교하여 loss계산

loss.backward() : 역전파

optimizer.step() : 적용

검증함수

검증함수의 내용은 학습 함수와 비슷하나, 일부 기울기를 계산하지 않거나, 평가모드로 설정하는 등의 세부사항이 다르다.

def validate():
    start_time = time.time()
    print(f'[Epoch {epoch+1}] Validating...')
    model.eval() # 학습이 아닌 평가모드임
    total = 0
    running_loss = 0.0
    running_correct = 0
    
    for i, batch in enumerate(val_dataloader): # val_loader의 개수만큼 돈다
        imgs, labels = batch # 이미지, 라벨 개수 64개
        imgs, labels = imgs.cuda(), labels.cuda() #device를 cuda로 설정(gpu 설정)
        with torch.no_grad():  #평가모드이므로 기울기 계산 안함
            outputs = model(imgs) # 모델에 이미지를 넣어 예측
            _, preds  = torch.max(outputs, 1) # 예측값 중 가장 큰 확률의 클래스를 preds에 저장
            loss = criterion(outputs, labels) # 예측값과 실제값의 loss 계산
        total += labels.shape[0] # 라벨의 개수를 total에 누적
        running_loss += loss.item() 
        running_correct += torch.sum(preds == labels.data)
        if i % log_step == 0:
            print(f'[Batch: {i + 1}] running val loss: {running_loss / total}, running val accuracy: {running_correct / total}')
    
    print(f'val loss: {running_loss / total}, accuracy: {running_correct / total}')
    print("elapsed time:", time.time() - start_time) # 연산 시간 출력
    return running_loss / total, (running_correct / total).item()

model.eval() : 모델을 평가모드로 전환

with torch.no_grad(): : 평가모드 이므로 기울기 계산 안함 + 역전파또한 없음

테스트 함수

검증함수와 같다.

def test():
    start_time = time.time()
    print(f'Testing...')
    model.eval() # 학습이 아닌 평가모드임
    total = 0
    running_loss = 0.0
    running_correct = 0
    
    for i, batch in enumerate(test_dataloader):
        imgs, labels = batch # 이미지, 라벨 개수 64개
        imgs, labels = imgs.cuda(), labels.cuda() #device를 cuda로 설정(gpu 설정)
        with torch.no_grad():  #평가모드이므로 기울기 계산 안함
            outputs = model(imgs) # 모델에 이미지를 넣어 예측
            _, preds  = torch.max(outputs, 1) # 예측값 중 가장 큰 확률의 클래스를 preds에 저장
            loss = criterion(outputs, labels) # 예측값과 실제값의 loss 계산
        total += labels.shape[0] # 라벨의 개수를 total에 누적
        running_loss += loss.item() 
        running_correct += torch.sum(preds == labels.data)
        if i % log_step == 0:
            print(f'[Batch: {i + 1}] running test loss: {running_loss / total}, running test accuracy: {running_correct / total}')
    
    print(f'test loss: {running_loss / total}, accuracy: {running_correct / total}')
    print("elapsed time:", time.time() - start_time) # 연산 시간 출력
    return running_loss / total, (running_correct / total).item()

위의 함수들을 만들면 model, criterion등이 노란 줄이 뜨는데, 해당 함수 실행 전에 정의하면 상관없다.

학습률 함수

def adjust_learning_rate(optimizer, epoch):
    lr = learning_rate
    if epoch >= 3:
        lr /= 10
    if epoch >= 7:
        lr /= 10
    for param_group in optimizer.param_groups:
        param_group['lr'] = lr
    return lr

처음 몇 에폭에선 lr을 크게 주어 빨리 수렴하도록 한다.

이후엔 점차 줄여 정확도를 높인다.

  • param_group['lr'] = lr : 옵티마이저의 parameter들 중에 lr값을 현재 설정한 lr로 변경해준다.

옵티마이저

learning_rate = 0.01
log_step = 8

model = Model1().cuda()

criterion = nn.CrossEntropyLoss() # lossfunction
optimizer = optim.SGD(model.parameters(), lr=learning_rate, momentum=0.9)

옵티마이저는 위와 같이 설정한다.

  • cuda를 사용하기 위해선 pytorch또한 cuda버전으로 다운로드 받아주어야 한다. 만약 안된다면 새로 설치해보자
optimizer.param_groups

# [{'params': [Parameter containing:
#    tensor([[-1.8623e-03, -2.1910e-03, -4.8949e-04,  ..., -5.3811e-04,
#              6.7743e-04, -2.0790e-03],
#            [-7.6419e-04, -2.2354e-03,  1.3329e-03,  ...,  1.0763e-03,
#             -1.8846e-03, -4.0704e-04],
#            [-8.4138e-04, -5.5695e-04, -1.6299e-03,  ..., -1.5105e-03,
#             -4.9726e-04,  1.7225e-03],
#            [-2.0870e-03, -1.5302e-03,  4.9625e-05,  ..., -1.2296e-03,
#              1.7425e-03,  2.4341e-04]], device='cuda:0', requires_grad=True),
#    Parameter containing:
#    tensor([0.0015, 0.0007, 0.0003, 0.0017], device='cuda:0', requires_grad=True)],
#   'lr': 0.01,
#   'momentum': 0.9,
#   'dampening': 0,
#   'weight_decay': 0,
#   'nesterov': False,
#   'maximize': False,
#   'foreach': None,
#   'differentiable': False,
#   'fused': None}]

해당 함수는 옵티마이저 내의 파라미터를 나타내는 함수이다.

  • params: 파라미터(기울기, 편향 등)
  • lr: 학습률 등등. (원래는 처음 설정할 때의 학습률로 설정된다.)
num_epochs = 20 # 에폭 수
best_val_acc = 0.0 # 가장 좋은 acc
best_epoch = -1 # 가장 좋았떤 epoch
history = []
accuracy = []

os.makedirs('weights/Model1', exist_ok=True) # 가중치 저장

학습

for epoch in range (num_epochs):
    adjust_learning_rate(optimizer, epoch) # 현재 epoch에 맞게 learning rate 조정
    train_loss, train_acc = train() # train 함수 호출
    val_loss, val_acc = validate() # validate 함수 호출
    history.append((train_loss, val_loss)) # epoch별 train loss와 val loss 저장
    accuracy.append((train_acc, val_acc)) # epoch별 train accuracy와 val accuracy 저장

    if val_acc > best_val_acc:
        print('Saving best model...')
        best_val_acc = val_acc
        best_epoch = epoch
        torch.save(model.state_dict(), f'weights/Model1/best_model_epoch_{epoch + 1}.pth')

torch.save(model.state_dict(), f'weights/Model1/final_model_epoch_{num_epochs}.pth')

# [Epoch 1] Training...
# [Batch: 1] running train loss: 0.023097557947039604, running train accuracy: 0.125
# [Batch: 9] running train loss: 0.22011838170389333, running train accuracy: 0.5920138955116272
# train loss: 0.235046521844718, accuracy: 0.6175243258476257
# elapsed time: 4.41102409362793
# [Epoch 1] Validating...
# [Batch: 1] running val loss: 0.4582744240760803, running val accuracy: 0.53125
# val loss: 0.43976463741726346, accuracy: 0.5666666626930237
# elapsed time: 1.1149239540100098
# Saving best model...
  • 지금까지 정의한 함수들을 호출한 후
  • 해당 accuracy를 확인해 가장 높은 accuracy를 적용하여 모델을 저장한다.
  • pth파일을 통해 해당 모델을 저장하면 다른 곳에서도 학습없이 사용가능하다.
  • 해당 pth파일을 통해 이후 다른 앱이나 사이트에 적용 가능하다.

학습을 마무리 하면

plt.plot([x[0] for x in accuracy], 'b', label='train')
plt.plot([x[1] for x in accuracy], 'r--',label='validation')
plt.xlabel("Epochs")
plt.ylabel("Accuracy")
plt.legend()

test_loss, test_accuracy = test()
print(f"Test loss: {test_loss:.8f}")
print(f"Test accuracy: {test_accuracy * 100.:.2f}%")

# Testing...
# [Batch: 1] running test loss: 0.18146856129169464, running test accuracy: 0.609375
# test loss: 0.13867560319140948, accuracy: 0.7566371560096741
# elapsed time: 1.695519208908081
# Test loss: 0.13867560
# Test accuracy: 75.66%

테스트 결과를 확인해본다. accuracy는 75.66%이다.

처음에 모델을 설정할때 단순 선형 모델로 했기 때문에 accuracy가 낮은것은 어쩔수 없다.

에폭수를 더 늘리더라도 좋은 모델은 나오지 않을 것 같아보인다.

  • accuracy가 늘어나지 않았으므로

새로운 모델

기존 선형 모델

class Model1(nn.Module):
    def __init__(self):
        super(Model1, self).__init__()
        self.linear1 = nn.Linear(256 * 256 * 3, 4)
        self.flatten = nn.Flatten()

    def forward(self, x):
        x = self.flatten(x)
        x = self.linear1(x)
        return x
  • y = wx + b
  • (x픽셀 * y픽셀 * 3차원(rgb) + bias) * 클래스 4개: (2562563+1)*4 = 786,436개의 파라미터
  • 파라미터의 개수가 늘어날수록 표현할 수 있는 가짓수가 커지므로 더 상세하게 분류 가능하다.

2중 선형 모델

class Model2(nn.Module):
    def __init__(self):
        super(Model2, self).__init__()
        self.linear1 = nn.Linear(256 * 256 * 3, 64) # 64개로 내주고
        self.linear2 = nn.Linear(64, 4) # 4개로 다시 줄여준다.
        self.flatten = nn.Flatten()

    def forward(self, x):
        x = self.flatten(x) # 평탄화 이후
        x = self.linear1(x) # 
        x = self.linear2(x)
        return x
  • 위 함수는 2개의 선형 함수를 사용했으나, 활성화 함수를 사용하지 않았으므로, 사실상 선형함수이다
  • 그러나 표현할 수 있는 파라미터의 개수는 (2562563+1)*64 + (64+1)*4 = 12,583,236 이다.

비선형성 모델(딥러닝)

class Model3(nn.Module):
    def __init__(self):
        super(Model3, self).__init__()
        self.linear1 = nn.Linear(256 * 256 * 3, 128)
        self.dropout1 = nn.Dropout(0.5)
        self.linear2 = nn.Linear(128, 64)
        self.dropout2 = nn.Dropout(0.5)
        self.linear3 = nn.Linear(64, 32)
        self.dropout3 = nn.Dropout(0.5)
        self.linear4 = nn.Linear(32, 4)
        self.flatten = nn.Flatten()

    def forward(self, x):
        x = self.flatten(x)
        x = F.relu(self.linear1(x))
        x = self.dropout1(x)
        x = F.relu(self.linear2(x))
        x = self.dropout2(x)
        x = F.relu(self.linear3(x))
        x = self.dropout3(x)
        x = self.linear4(x)
        return x

dropout(0.5)은 학습하는 것 중 절반(0.5)을 랜덤하게 꺼준다.

  • 만약 파라미터 4 > 6개로 가면 24개의 파라미터지만 이중 절반 12개는 학습이 되지 않는다.
  • 이를 통해 과적합을 방지하고, 일반화 성능을 높일 수 있다.

위에선 layer가 4개 dropout을 3개를 주었다.

forward

  • 여기서 평탄화 > relu함수로 비선형성을 준 레이어 > dropout을 반복한다.
  • relu함수는 0이하를 0으로 만들어 버리는 활성화 함수

Model2

os.makedirs("weights/Model2", exist_ok=True)

learning_rate = 0.01
log_step = 8

model = Model2()
model = model.cuda()

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=learning_rate, momentum=0.9)

num_epochs = 20
best_val_acc = 0
best_epoch = 0

history = []
accuracy = []
for epoch in range(num_epochs):
    adjust_learning_rate(optimizer, epoch)
    train_loss, train_acc = train()
    val_loss, val_acc = validate()
    history.append((train_loss, val_loss))
    accuracy.append((train_acc, val_acc))

    if val_acc > best_val_acc:
        print("[Info] best validation accuracy!")
        best_val_acc = val_acc
        best_epoch = epoch
        torch.save(model.state_dict(), f"weights/Model2/best_checkpoint_epoch_{epoch + 1}.pth")

torch.save(model.state_dict(), f"weights/Model2/last_checkpoint_epoch_{num_epochs}.pth")

plt.plot([x[0] for x in accuracy], 'b', label='train')
plt.plot([x[1] for x in accuracy], 'r--',label='validation')
plt.xlabel("Epochs")
plt.ylabel("Accuracy")
plt.legend()

test_loss, test_accuracy = test()
print(f"Test loss: {test_loss:.8f}")
print(f"Test accuracy: {test_accuracy * 100.:.2f}%")
  • model1의 구조와 같으나 모델만 바꿔줌
test loss: 0.12633993298606536, accuracy: 0.7654867172241211
elapsed time: 0.7122688293457031
Test loss: 0.12633993
Test accuracy: 76.55%

  • accuracy는 76.55로 사실상 거의 비슷하다.
  • 어차피 선형이기 때문에 accuracy가 비슷함을 알 수 있다.

Model3

os.makedirs("weights/Model3", exist_ok=True)

learning_rate = 0.01
log_step = 20

model = Model3()
model = model.cuda()

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=learning_rate, momentum=0.9)

num_epochs = 20
best_val_acc = 0
best_epoch = 0

history = []
accuracy = []

for epoch in range(num_epochs):
    adjust_learning_rate(optimizer, epoch)
    train_loss, train_acc = train()
    val_loss, val_acc = validate()
    history.append((train_loss, val_loss))
    accuracy.append((train_acc, val_acc))

    if val_acc > best_val_acc:
        print("[Info] best validation accuracy!")
        best_val_acc = val_acc
        best_epoch = epoch
        torch.save(model.state_dict(), f"weights/Model3/best_checkpoint_epoch_{epoch + 1}.pth")

torch.save(model.state_dict(), f"weights/Model3/last_checkpoint_epoch_{num_epochs}.pth")

plt.plot([x[0] for x in accuracy], 'b', label='train')
plt.plot([x[1] for x in accuracy], 'r--',label='validation')
plt.xlabel("Epochs")
plt.ylabel("Accuracy")
plt.legend()

test_loss, test_accuracy = test()
print(f"Test loss: {test_loss:.8f}")
print(f"Test accuracy: {test_accuracy * 100.:.2f}%")
test loss: 0.010391020735280703, accuracy: 0.73893803358078
elapsed time: 0.7321090698242188
Test loss: 0.01039102
Test accuracy: 73.89%

  • 안정적인 학습성능을 보인다.
  • 그러나 막 드라마틱한 accuracy변화는 없었다.
  • 딥러닝을 사용하고, 레이어수를 늘렸는데도 변화가 없다는 것은.

그러나 위의 모델들은 모두 평탄화(1열로 바꿔줌)이후 학습을 진행했다.

따라서 이미지의 지역성을 파악하기 힘들다. 그렇기 때문에 CNN등의 2차원 학습 모델을 사용하면 성능이 좋아질 것이다.