[250429화591H] 딥러닝으로 폐렴 환자 자동 분류 (2)

윤승호·2025년 4월 29일

오늘도 많이 건졌다. 확실히 인공지능 개발자는 논문을 많이 읽어야 성장하는 것 같다.

학습시간 09:00~02:00(당일17H/누적591H)


◆ 학습내용

딥러닝으로 폐렴 환자 자동 분류하기.

어제(1~5번)에 이어서 6번부터 진행!


6. 기본모델 평가

어제 훈련 돌려놓고 잔 모델이다. 결과가 기대된다 두근두근!!

잘잤니 나의 사랑스런 가중치야...^^

def evaluate_model(model, test_loader, device):
    model.eval()
    y_true = []
    y_pred = []

    with torch.no_grad():
        for inputs, labels in test_loader:
            inputs, labels = inputs.to(device), labels.to(device)

            with torch.amp.autocast(device_type=device.type):
                outputs = model(inputs)

            _, preds = torch.max(outputs, 1)
            y_true.extend(labels.cpu().numpy())
            y_pred.extend(preds.cpu().numpy())

평가용 함수를 하나 만들었다.

음,, 저번에 해보니까 Confusion Matrix랑 Classification Report만 있으면 다 해결되는 느낌이었으니 이번에도 그렇게 해보자!

    print("▶ Confusion Matrix & Classification Report:\n", confusion_matrix(y_true, y_pred))
    print(classification_report(y_true, y_pred, target_names=['NORMAL', 'PNEUMONIA']))

요거 두개만 있으면 될 것 같다.

model.load_state_dict(torch.load('./model/DoubleLayerCNN_0428_1.pth', map_location=device))
evaluate_model(model, test_loader, device)

저장해둔 모델을 가져와서 평가 함수에 넣었다.

더블레이어 모델의 정확도는 85%라고 한다.

오잉?? 근데 뭔가 이상하다. 폐렴 맞출 확률은 99%인데, 정상폐를 맞출 확률이 62%다.

이건 분명 폐렴 이미지에 과적합되었다는 뜻일 것이다.

흠,, 그래도 폐렴을 정상이라고 오진한 것 보다는 괜찮은 건가?? 아니지, 내가 환자라고 생각하면 반대 상황에서 진짜 기분 나쁠듯. 정상인데 폐렴이라니요~

이번 미션의 핵심은 폐렴을 잘 맞추면서도 정상폐를 폐렴이라 오진할 확률까지 낮추는 것인 듯하다.

일단 기본 모델은 여기까지하고, 전이학습을 해보자!

7. ResNet

이틀 전에 연습했던 resnet18 모델은 정확도가 50% 가량 나왔었다. 물론 내가 뭔가 코드를 잘못 작성했겠지만, 이번엔 중간 모델로 써보자! resnet50 선택!

V1와 V2가 있는데, V2가 조금 더 개선된 모델이라고 한다. 디폴트값도 V2라고 함.

(1) Classifier Tuning

model_resnet50 = resnet50(weights=ResNet50_Weights.IMAGENET1K_V2)
model_resnet50

모델을 불러왔다. 홀리 쉣 뭐가 이렇게 길어?

일단 classifier 부분만 학습할 거니까, 이 부분만 고치면 될 것 같다.

이진 분류니까 out_features만 2로 바꿔주면 끝이다.

model_resnet50.fc = nn.Linear(2048, 2)
model_resnet50.fc

오케이 변경 완료!

디바이스에 옮기기 전에 requires_grad 값을 변경해야 한다.

for name, param in model_resnet50.named_parameters():
    print(f"{name}: {param.requires_grad}")

파라미터 확인!

따로 건들인 게 없어서 전부 True로 되어 있다. 이대로 돌리면 feature 추출 레이어까지 학습이 되어버리니 반드시 변경해야 한다.

for name, param in model_resnet50.named_parameters():
    if name.startswith('layer'):
        param.requires_grad = False
    print(f"{name}: {param.requires_grad}")

'layer'로 시작하는 파라미터를 전부 False 처리했다.

True랑 False 중에 뭐가 Freeze인지 가끔 헷갈리곤 하는데, 그냥 F-F 로 외우면 될 것같다.

Freeze 하고 싶으면 False 하기!

fc 부분만 True고 나머지는 전부 False로 바뀌었다.

model_resnet50 = model_resnet50.to(device)

모델 수정이 끝났으니 디바이스에 올린다. ★이게 세상에서 제일 중요함★

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model_resnet50.parameters(), lr=1e-4)
train_model(model_resnet50, train_loader, val_loader, epochs=20)
torch.save(model_resnet50.state_dict(), './model/resnet50_fc_tuning_0429_1.pth')

손실함수와 최적화 알고리즘를 새로 만들어서 초기화해주고 학습 함수에 넣었다. 혹시 모르니 학습 후에 바로 저장되도록 했다.

20에폭 돌렸다. 와 시간이 진짜 오래 걸린다.

train 정확도가 94%로 꽤 높다. 로스도 0.1583 이면 양호한 것 같다.
val 정확도는 왔다갔다 하네...

이제 resnet50 fine-tuning 할 차례다. 일단 부분적으로 해보자.

model_resnet50_2 = resnet50(weights=ResNet50_Weights.IMAGENET1K_V2)
model_resnet50_2

???????????????????????????????

아니 이게 뭐여??

layer1 전에 conv1이 또 있었다.

그럼 혹시 나 이거 방금 전에 True로 하고 돌린건가!!!!!!!!

for name, param in model_resnet50.named_parameters():
    print(f"{name}: {param.requires_grad}")

맞네 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

하 씨 ㅠㅠㅠㅠㅠㅠ 20에폭 또 기다려야 하네...

for name, param in model_resnet50_1.named_parameters():
    if name.startswith('fc'):
        param.requires_grad = True
    else:
        param.requires_grad = False
    print(f"{name}: {param.requires_grad}")

내가 지정한 것만 True로 하고 나머진 False로 하도록 코드를 변경했다.

두번 다신 속지 않겠다 이녀석.

conv1: False인 거 확인했고,

fc: True인 것도 확인했다.

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model_resnet50_1.parameters(), lr=1e-4)
train_model(model_resnet50_1, train_loader, val_loader, epochs=10)
torch.save(model_resnet50_1.state_dict(), './model/resnet50_1.pth')

시간이 오래 걸리니 10에폭만 돌리자....

오랜 기다림 끝에 결과가 나왔다.

근데 이상하네. 실수로 conv1까지 학습했을 때보다 결과가 더 안 좋다.

Train 정확도 92.5%, 로스는 0.2204 이다.

흠 이상하군..

일단 다음으로 넘어가 보자.

(2) Partial Fine Tuning

model_resnet50_2 = resnet50(weights=ResNet50_Weights.IMAGENET1K_V2)
model_resnet50_2.fc = nn.Linear(2048, 2)

for name, param in model_resnet50_2.named_parameters():
    if name.startswith(('fc', 'layer3', 'layer4')):
        param.requires_grad = True
    else:
        param.requires_grad = False
    print(f"{name}: {param.requires_grad}")    
    

이번엔 layer3, 4, fc만 학습시킨다. if문에 튜플로 넣었다.


layer3을 기준으로 True로 바뀌는 것도 잘 확인했다.

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model_resnet50_2.parameters(), lr=1e-4)
train_model(model_resnet50_2, train_loader, val_loader, epochs=10)
torch.save(model_resnet50_2.state_dict(), './model/resnet50_2.pth')

손실함수와 옵티 초기화 후 학습 시작!

10에폭 돌렸다.

헉!? 레이어 층 2개만 더 풀었는데 성능이 엄청 올라갔다.

Train 정확도 99%, 로스 0.0202

테스트 셋 평가가 기대되는걸...

(3) Full Fine Tuning

풀튜닝 차례다.

model_resnet50_3 = resnet50(weights=ResNet50_Weights.IMAGENET1K_V2)
model_resnet50_3.fc = nn.Linear(2048, 2)

for name, param in model_resnet50_3.named_parameters():
    print(f"{name}: {param.requires_grad}")

model_resnet50_3 = model_resnet50_3.to(device)

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model_resnet50_3.parameters(), lr=1e-4)
train_model(model_resnet50_3, train_loader, val_loader, epochs=10)
torch.save(model_resnet50_3.state_dict(), './model/resnet50_3.pth')

이번엔 모든 레이어 싹 다 학습한다.

부분적으로 해도 99%면,,, 풀로 돌리면 100% 나오는 건가??

10에폭 돌렸다. 정확도랑 로스만 보았을 때 파셜튜닝이랑 풀튜닝은 엄청 드라마틱한 차이는 없어보인다.

Train 정확도 99.3%, 로스 0.0175

로스가 조금 더 개선되긴 했네.

근데 학습이 너무 오래걸린다.

이거 해도해도 너무한 거 아닌가 진짜....

가벼우면서 성능 좋은 모델 어디 없나 ㅜㅜ


8. DenseNet

뭔가 좋은 모델이 없을까 찾아보다가 또 어떤 논문을 발견했다. 벌써 개발자 인생 세 번째 논문이다.

[ 논문 링크 ]

Densely Connected Convolutional Networks. 줄여서 DenseNet이라고 한다.

일단 논문을 함 들여다 봐야겠다. 과연 어떤 모델일까!?

신기하게도 논문에 ResNet에 대한 내용이 직접적으로 담겨 있다.

그렇다. 이건 ResNet의 업그레이드 버전인 것이다.

기울기 소실 문제를 더 해결했고, 파라미터 수까지 많이 감소되었다고 한다.

장비가 안 좋아 학습이 오래 걸리는 나로써 굉장히 솔깃한 내용이다.

DenseNet의 구조는 이런 느낌이라고 한다.

첫 레이어의 정보가 마지막 레이어까지 전달되는 구조다.

헐 그런데도 파라미터 수가 줄어든다고???

[ ResNet(좌) & DenseNet(우) ]

ResNet과 DenseNet을 잘 비교해 놓은 블로그를 찾았다. 이렇게 보니까 확 이해가 된다.

DenseNet의 핵심은 Dense Block인 것 같다. 이것도 내가 더블레이어 했던 것처럼 클래스로 나누는 거겠지??

구조가 아주 친절하게 다 나와있다. 여기도 7x7 글로벌 풀링 하네. 분류 테스크에서는 이게 핵심인가?

에러율 대비 파라미터 수와 연산량이 나와있다.

DenseNet121의 성능은 ResNet50보다 소폭 낮지만, 파라미터 수와 연산량은 3배 정도 적은 것으로 보인다.

최종 버전인 DenseNet264까지 오면 ResNet152의 성능을 잡아먹는다.

즉, DenseNet은 ResNet보다 성능도 좋고 파라미터 수도 적은데다가 연산까지 빠른 모델이다.

하,,, 진짜 너무 좋아서 미치겠다

빨리 해보자.

(1) Classifier Tuning

파이토치 싸이트에 DenseNet이 있었다.

왜 지금까지 못봤지? 역시 뭐든 아는 사람 눈에만 보인다.

근데 논문과 다르게 모델이 4개밖에 없다. 최종모델은 왜 없지? 너무 짱이라서 공개를 안 했나? 하긴 그럴만도 함.

논문에서 언급한 바와 같이, ResNet50보다 성능이 살짝 안 좋았던 DenseNet121로 해보자.

model_densenet121_1 = densenet121(weights=DenseNet121_Weights.IMAGENET1K_V1)
model_densenet121_1

DenseNet은 denselayer 라는 클래스를 사용하는 것 같다.

이것도 classifier에 out_features=1000을 2로만 바꾸면 끝이다.

for name, param in model_densenet121_1.named_parameters():
    if name.startswith('classifier'):
        param.requires_grad = True
    else:
        param.requires_grad = False
    print(f"{name}: {param.requires_grad}")

classifier를 제외하고 다 freeze 했다.

model_densenet121_1 = model_densenet121_1.to(device)

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model_densenet121_1.parameters(), lr=1e-4)
train_model(model_densenet121_1, train_loader, val_loader, epochs=10)
torch.save(model_densenet121_1.state_dict(), './model/densenet121_1.pth')

수정이 끝나서 디바이스에 올렸다.

이것도 10에폭 돌려보자.

10에폭 돌렸다. 체감만 그런지 모르겠는데 진짜 학습이 빠른 것 같다.

게다가 수치상으로 보았을 때 ResNet50 분류기 튜닝한 것보다 성능이 좋다.

ResNet50 Classifier Tuning
Train 정확도 92.5% & 로스 0.2204

DenseNet121 Classifier Tuning
Train 정확도 93% & 로스 0.1894

(2) Partial Fine Tuning

model_densenet121_2 = densenet121(weights=DenseNet121_Weights.IMAGENET1K_V1)
model_densenet121_2.classifier = nn.Linear(1024, 2)

for name, param in model_densenet121_2.named_parameters():
    if name.startswith((
        'features.denseblock3',
        'features.transition3',
        'features.denseblock4',
        'features.norm5',
        'classifier')):
        param.requires_grad = True
    else:
        param.requires_grad = False
    print(f"{name}: {param.requires_grad}")

모델의 앞부분 절반 정도 Freeze했다.

모델 찍어보니 학습해야 할 부분은 아래 5개 층이다.

features.denseblock3
features.transition3
features.denseblock4
features.norm5
classifier

denseblock3 기준으로 True로 전환된 것을 확인했다.

model_densenet121_2 = model_densenet121_2.to(device)

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model_densenet121_2.parameters(), lr=1e-4)
train_model(model_densenet121_2, train_loader, val_loader, epochs=10)
torch.save(model_densenet121_2.state_dict(), './model/densenet121_2.pth')

학습 시작!

10에폭 돌렸다. 학습 속도도 빠른데 성능도 진짜 좋다.

ResNet50 Partial Fine Tuning
Train 정확도 99%, 로스 0.0202

DenseNet121 Partial Fine Tuning
Train 정확도 99.4% & 로스 0.0154

(3) Full Fine Tuning

model_densenet121_3 = densenet121(weights=DenseNet121_Weights.IMAGENET1K_V1)
model_densenet121_3.classifier = nn.Linear(1024, 2)

for name, param in model_densenet121_3.named_parameters():
    print(f"{name}: {param.requires_grad}")

마지막으로 풀튜닝. 모든 레이어를 열고 학습을 돌린다.

다 True인 것을 확인했다.

model_densenet121_3 = model_densenet121_3.to(device)

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model_densenet121_3.parameters(), lr=1e-4)
train_model(model_densenet121_3, train_loader, val_loader, epochs=10)
torch.save(model_densenet121_3.state_dict(), './model/densenet121_3.pth')

학습 시작! 결과가 기대된다.

10에폭 돌렸다. 어라? 파셜튜닝이랑 큰 차이가 없다.

ResNet50 Full Fine Tuning
Train 정확도 99.3%, 로스 0.0175

DenseNet121 Full Fine Tuning
Train 정확도 98.9% & 로스 0.0283

게다가 ResNet50 Full Fine Tuning이랑 비교해도 성능이 소폭 낮은 것 같다.

흠,, 일단 오늘은 여기까지 하자.


헉 생각해 보니까 ResNet과 DenseNet 파라미터 수 비교를 안 했다. 잠자기 전에 보고 자야지!!

resnet50 = resnet50(weights=ResNet50_Weights.IMAGENET1K_V2)
resnet50.fc = nn.Linear(2048, 2)
summary(resnet50, (1, 3, 224, 224))

densenet121 = densenet121(weights=DenseNet121_Weights.IMAGENET1K_V1)
densenet121.classifier = nn.Linear(1024, 2)
summary(densenet121, (1, 3, 224, 224))

먼저 ReeNet50.

파라미터 약 2,550만 개.

다음 DenseNet121.

파라미터 약 700만 개.

와우! DenseNet 파라미터 수가 약 3.5배 정도 더 적다!! 어쩐지 학습이 빠르다 했더니...

학습도 빠른데 성능도 비슷하면 이제 굳이 ResNet을 쓸 필요가 없을 것 같다.

일단 분석은 내일 하자!

내일은 지금까지 만든 7개의 모델을 비교해 봐야겠다.

와 오늘 뭔가 너무 많이 했다... 머리가 아프다.

사랑스런 나의 가중치들아 굿나잇하렴, 내일 보자!

profile
나는 AI 엔지니어가 된다.

0개의 댓글