두피 진단 프로젝트 시스템 구현 과정

BABY CAT·2022년 9월 1일
0

GwangJu AI Academy

목록 보기
3/6

1. 요약

모델 개발 :
저희는 주어진 두피이미지와 이미지 분류 모델인 efficientnet-b7을 통해 두피타입을 진단하는 ai모델을 개발하였습니다.

웹 시스템 구현 :
그리고, 이것을 웹에 구현하여 사용자들이 자신의 두피를 자가진단할 수 있는 시스템을 구현하였습니다.

2. 시스템 구현 과정

학습용 데이터 탐색 및 전처리

데이터를 중증도에 따라 0(정상), 1(경증), 2(중등도), 3(중증), 총 4가지로 나누어 라벨링.
train set, validation set, test set 으로 나누기.

전처리 코드

#transforms.Compose : Rescale 과 RandomCrop 을 한번에 수행
#Rescale: 이미지의 크기를 조절
#RandomCrop: 이미지를 무작위로 자른다
#정규화
def func(x):  #아래 transforms_train = 코드에서 transforms.Lambda(lambda x: x.rotate(90)) 에서 나는 에러를 잡기 위해 def로 빼주고 람다 속 람다를 제거함
    return x.rotate(90)
transforms_train = transforms.Compose([
    transforms.Resize([int(600), int(600)], interpolation=transforms.InterpolationMode.BOX), # interpolation=4 워닝을 제거하기 위해 변형
    transforms.RandomHorizontalFlip(p=0.5),
    transforms.RandomVerticalFlip(p=0.5),
    transforms.Lambda(func),
    transforms.RandomRotation(10),
    transforms.RandomAffine(0, shear=10, scale=(0.8, 1.2)),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
transforms_val = transforms.Compose([
    transforms.Resize([int(600), int(600)], interpolation=transforms.InterpolationMode.BOX),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
#data_train_path 경로의 이미지를 transforms.Compose 로 정규화한 데이터 기준으로 트랜스폼
train_data_set = datasets.ImageFolder(data_train_path, transform=transforms_train)
val_data_set = datasets.ImageFolder(data_validation_path, transform=transforms_val)
# 변수 선언
dataloaders, batch_num = {}, {}
# dataloaders 빈딕셔너리에 train/val 키랑 DataLoder 밸류 넣기
# DataLoader로 학습용 데이터 준비 : 데이터셋의 특징(feature)을 가져오고 하나의 샘플에 정답(label)을 지정하는 일을 한다
dataloaders['train'] = DataLoader(train_data_set,
                                  batch_size=hyper_param_batch,
                                  shuffle=True,
                                  num_workers=0)  # aihub코드  num_workers = 4
dataloaders['val'] = DataLoader(val_data_set,
                                batch_size=hyper_param_batch,
                                shuffle=False,
                                num_workers=0)  # aihub코드  num_workers = 4
#즉 dataloaders 딕셔너리에는 train / val 이 key 각 밸류는 정규화한 이미지 데이터에 + 라벨이 붙음
#DataLoader를 통해 네트워크에 올리기
#from torch.utils.data import Dataset,DataLoader 
#testloader = DataLoader(testset, batch_size=2, shuffle=False, num_workers=0)
    #데이터 로더는 데이터의 대량 가져오기 또는 내보내기를 위한 클라이언트 응용 프로그램 
    #for data, target in testloader: 에서 data는 데이터의 특징  target은 데이터의 정답값

모델 학습 및 세이브

efficientnet-b7 모델을 활용하여 코랩pro상에서 학습.

def train_model(model, criterion, optimizer, scheduler, num_epochs=25):
    if __name__ == '__main__':  #프롬프트에서 돌리기 위해 추가. 네임메인에 관해 런타임에러를 디버그 
        ##변수 선언
        #시간변수 선언
        start_time = time.time()  # end_sec 종료시간 = time.time() - start_time, # 종료시간 :
        since = time.time()  # time_elapsed 경과시간 = time.time() - since, # 경과시간 : 모든 에폭을 돌리는데 걸린 시간
        best_acc = 0.0  # 베스트 정확도 갱신시킬 변수
        best_model_wts = copy.deepcopy(model.state_dict())  # 베스트가중치도 갱신: 베스트 정확도 갱신할 때 같이 갱신
        #state_dict 는 간단히 말해 각 계층을 매개변수 텐서로 매핑되는 Python 사전(dict) 객체입니다.
        #state_dict : 모델의 매개변수를 딕셔너리로 저장
        #copy.deepcopy 깊은복사: 완전한복사 (얕은복사:일종의 링크 형태)
        #손실, 정확도 빈리스트 선언
        train_loss, train_acc, val_loss, val_acc = [], [], [], []
        #for문
        for epoch in tqdm(range(num_epochs)):  # epoch만큼 실행
            print('Epoch {}/{}'.format(epoch, num_epochs - 1))  # 1000에폭을 넣으면 Epoch 0/999 이렇게 출력      왜 -1을 넣었을가 ?
            print('-' * 10)  # ---------- 구분 선
            epoch_start = time.time()  # 매 에폭을 돌리는 시간
            for phase in ['train', 'val']:
                if phase == 'train':
                    model.train()  # model.train()  ≫ 모델을 학습 모드로 변환
                else:
                    model.eval()  # model.eval()  ≫ 모델을 평가 모드로 변환
                #train이 들어가면 학습모드로 아래 코드 실행, val이 들어가면 평가모드로 val로 평가
                #변수
                running_loss = 0.0
                running_corrects = 0
                num_cnt = 0
                #아래코드이해를위한
                #dataloaders 빈딕셔너리에 train/val 키랑 DataLoder 밸류 넣기
                #DataLoader로 학습용 데이터 준비 : 데이터셋의 특징(feature)을 가져오고 하나의 샘플에 정답(label)을 지정하는 일을 한다
                #dataloaders['train'] = DataLoader(train_data_set,
                #                                   batch_size=hyper_param_batch,
                #                                   shuffle=True,
                #                                   num_workers=4)
                #dataloaders['val'] = DataLoader(val_data_set,
                #                                 batch_size=hyper_param_batch,
                #                                 shuffle=False,
                #                                 num_workers=4)
                for inputs, labels in tqdm(dataloaders[phase]):  # phase 에 train or val 이 들어가서 인풋과 라벨로 나뉜다
                    inputs = inputs.to(device)
                    labels = labels.to(device)
                    optimizer.zero_grad()  # optimizer.zero_grad() : Pytorch에서는 gradients값들을 추후에 backward를 해줄때 계속 더해주기 때문"에
                    #우리는 항상 backpropagation을 하기전에 gradients를 zero로 만들어주고 시작을 해야합니다.
                    #한번 학습이 완료가 되면 gradients를 0으로 초기화
                    with torch.set_grad_enabled(phase == 'train'):
                        #torch.set_grad_enabled
                        #그래디언트 계산을 켜키거나 끄는 설정을 하는 컨텍스트 관리자
                        #phase == 'train' 이 true 면 gradients를 활성화 한다.
                        outputs = model(inputs)  # 모델에 인풋을 넣어서 아웃풋 생성
                        _, preds = torch.max(outputs, 1)  # _, preds ?
                        #torch.max(input-tensor) : 인풋에서 최댓값을 리턴하는데 tensor라 각 묶음마다 최댓값을 받고 ,1 은 축소할 차원이1이라는 뜻
                        loss = criterion(outputs, labels)  # 로스 계산
                        #매 epoch, 매 iteration 마다 back propagation을 통해 모델의 파라미터를 업데이트 시켜주는 과정이 필요한데,
                        #아래 다섯 줄의 코드는 공식처럼 외우는 것을 추천드립니다.
                        #optimizer.zero_grad()   	# init grad
                        #pred = model(x)  # forward
                        #loss = criterion(pred, x_labels) # 로스 계산
                        #loss.backward()  # backpropagation
                        #optimizer.step()  	# weight update
                        if phase == 'train':
                            loss.backward()  # backpropagation
                            optimizer.step()  # weight update
                    running_loss += loss.item() * inputs.size(0)  # 학습과정 출력   #   running_loss = 0.0    # loss 는 로스계산  ?
                    running_corrects += torch.sum(preds == labels.data)  # running_corrects = 0                    ?
                    num_cnt += len(labels)  # num_cnt = 0                             ?
                #for inputs, labels in dataloaders[phase]: # phase 에 train or val 이 들어가서 인풋과 라벨로 나뉜다
                #                 inputs = inputs.to(device)
                #                 labels = labels.to(device)
                if phase == 'train':
                    scheduler.step()  # 학습 규제
                #학습률이 크면 가중치 업데이트가 많아 가중치가 overflow 될 수도 있습니다
                #훈련 초기에 학습률은 충분히 좋은 가중치에 도달하기 위해 크게 설정됩니다. 시간이 지남에 따라
                #이러한 가중치는 작은 학습률을 활용하여 더 높은 정확도에 도달하도록 미세 조정됩니다.
                #결국, 가중치를 규제(regularization)하는 방식과 비슷하게, 학습률을 규제하는(Learning Rate Decay)것이 Learning Rate Scheduler라고 할 수 있습니다.
                #optimizer와 scheduler를 먼저 정의한 후, 학습할 때 batch마다 optimizer.step() 하고 epoch마다 scheduler.step()을 해주면 됩니다.
                #def 밖에서  op sc 선언 def안에서 op.step  sc.stop 완료
                epoch_loss = float(running_loss / num_cnt)  # ? 에폭손실
                epoch_acc = float((running_corrects.double() / num_cnt).cpu() * 100)  # ? 에폭 정확도
                #      손실, 정확도 빈리스트 선언
                #      train_loss, train_acc, val_loss, val_acc = [], [], [], []
                if phase == 'train':
                    train_loss.append(epoch_loss)
                    train_acc.append(epoch_acc)
                else:
                    val_loss.append(epoch_loss)
                    val_acc.append(epoch_acc)
                print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))  # 출력 train/val, 손실, 정확도
                if phase == 'val' and epoch_acc > best_acc:
                    best_idx = epoch  # 에폭인덱
                    best_acc = epoch_acc  # 베스트정확도
                    best_model_wts = copy.deepcopy(model.state_dict())
                    print('==> best model saved - %d / %.1f' % (best_idx, best_acc))  # 몇번째 에폭의 베스트 정확도가 세이브되었나 출력
                #       best_acc = 0.0 # 베스트 정확도 갱신시킬 변수
                #       best_model_wts = copy.deepcopy(model.state_dict()) # 베스트가중치도 갱신: 베스트 정확도 갱신할 때 같이 갱신
                #state_dict 는 간단히 말해 각 계층을 매개변수 텐서로 매핑되는 Python 사전(dict) 객체입니다.
                #state_dict : 모델의 매개변수를 딕셔너리로 저장
                #copy.deepcopy 깊은복사: 완전한복사 (얕은복사:일종의 링크 형태)
                epoch_end = time.time() - epoch_start  # train/val 전부 에폭 한번 돌리는 시간을 구해서 아래 출력
                print('Training epochs {} in {:.0f}m {:.0f}s'.format(epoch, epoch_end // 60,
                                                                    epoch_end % 60))  # 트레이닝에폭 epoch 몇분 몇초
                print()
                #for문 끝
    time_elapsed = time.time() - since  # 경과시간 : 모든 에폭을 돌리는데 걸린 시간, for 문이 끝났으니까
    print('Training complete in {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))  # 경과시간을 몇분 몇초로 출력
    print('Best valid Acc: %d - %.1f' % (best_idx, best_acc))  # best_idx : 몇번째 에폭이 베스트인지, 베스트정확도 출력
    model.load_state_dict(best_model_wts)  # state_dict: 모델의 매개변수를 딕셔너리에 담은 > 것을 load 한다
    #best_model_wts = copy.deepcopy(model.state_dict())
    #PATH = './scalp_weights/' # 경로 설정 현재폴더 하위에 scalp_weights 폴더
    torch.save(model, PATH + 'aram_' + train_name + '.pt')  # 모델을 PATH경로에 aram_트레인네임(model1).pt 라는 이름으로 저장한다
    torch.save(model.state_dict(), PATH + 'president_aram_' + train_name + '.pt')  # 모델의 매개변수를               -  저장
    print('model saved')
    end_sec = time.time() - start_time  # 종료시간    # 초단위에서
    end_times = str(datetime.timedelta(seconds=end_sec)).split('.')  # 시분초로 치환
    #import datetime
    #end = 8888
    #datetime.timedelta(seconds=end)                                #출력  datetime.timedelta(seconds=8888)
    #str(datetime.timedelta(seconds=end))  # type str              #출력  '2:28:08'
    #str(datetime.timedelta(seconds=end)).split('.') # type list   #출력   ['2:28:08']  ?
    #str(datetime.timedelta(seconds=end)).split('.')[0] # type str  #출력  '2:28:08'
    end_time = end_times[0]  # 종료시간 시분초
    print("end time :", end_time)  # 출력
    return model, best_idx, best_acc, train_loss, train_acc, val_loss, val_acc

모델별 테스트셋 성능 측정

PATH = '/content/drive/MyDrive/project/scalp_weights/'+'aram_model5.pt'   # 모델경로 
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")     # 쿠다 쓸 수 있으면 사용
model = torch.load(PATH, map_location=device)     # 모델로드      
with torch.no_grad(): # 평가할 땐  gradient를 backpropagation 하지 않기 때문에 no grad로 gradient 계산을 막아서 연산 속도를 높인다
            for data, target in tqdm(testloader):                                   
                data, target  = data.to(device), target.to(device) 
                output = model(data)   # model1에 데이터를 넣어서 아웃풋 > [a,b,c,d] 각 0,1,2,3 의 확률값 리턴 가장 큰 것이 pred
                # output_list.append(output);
                test_loss += F.nll_loss(output, target, reduction = 'sum').item()  # test_loss변수에 각 로스를 축적
                pred = output.argmax(dim=1, keepdim=True) # argmax : 리스트에서 최댓값의 인덱스를 뽑아줌 > y값아웃풋인덱
                correct += pred.eq(target.view_as(pred)).sum().item() # accuracy 측정을 위한 변수 # 각 예측이 맞았는지 틀렸는지 correct변수에 축적 맞을 때마다 +1  # # view_as() 함수는 target 텐서를 view_as() 함수 안에 들어가는 인수(pred)의 모양대로 다시 정렬한다. #  view_as() 함수는 target 텐서를 view_as() 함수 안에 들어가는 인수(pred)의 모양대로 다시 정렬한다. #  pred.eq(data) : pred와 data가 일치하는지 검사
test_loss /= len(testloader.dataset)  # 로스축적된 로스를 데이터 수(경로안jpg수)로 나누기
# 로스, 아큐러시 출력 ( :.4f 소수점반올림 )
print('\nTest set: Average Loss: {:.4f}, Accuracy: {}/{} ({:.4f}%)\n'.format(test_loss, correct, len(testloader.dataset), 100. * correct / len(testloader.dataset)))  # 축적된 예측값을 데이터 개수로 나누기 *100  확률%값
model1 미세각질 : Test set: Average Loss: -1.9017, Accuracy: 289/476 (60.7143%)
model2 피지과다 : Test set                           Accuracy: 324/662 (48.9426%)
model3 모낭사이홍반 : Test set: Average Loss: -2.2255, Accuracy: 461/718 (64.2061%)
model4 모낭홍반농포 : Test set: Average Loss: -3.4693, Accuracy: 281/394 (71.3198%)
model5 비듬 : Test set: Average Loss: -2.0950, Accuracy: 370/620 (59.6774%)
model6 탈모 : Test set: Average Loss: -1.3145, Accuracy: 633/1154 (54.8527%)

앞서 학습한 모델 파일 로드

#웹페이지에서 이미지를 업로드 받은 후 연산시간을 줄이기 위해 미리 로드 
#PATH
PATH1 = '/content/drive/MyDrive/project/scalp_weights/'+'aram_model1.pt'  # 모델1 미세각질
PATH2 = '/content/drive/MyDrive/project/scalp_weights/'+'aram_model2.pt'  # 모델2 피지과다
PATH3 = '/content/drive/MyDrive/project/scalp_weights/'+'aram_model3.pt'  # 모델3 모낭사이홍반
PATH4 = '/content/drive/MyDrive/project/scalp_weights/'+'aram_model4.pt'  # 모델4 모낭홍반농포
PATH5 = '/content/drive/MyDrive/project/scalp_weights/'+'aram_model5.pt'  # 모델5 비듬
PATH6 = '/content/drive/MyDrive/project/scalp_weights/'+'aram_model6.pt'  # 모델6 탈모
#cuda        
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# model load        
model1 = torch.load(PATH1, map_location=device)
model2 = torch.load(PATH2, map_location=device)
model3 = torch.load(PATH3, map_location=device)
model4 = torch.load(PATH4, map_location=device)
model5 = torch.load(PATH5, map_location=device)
model6 = torch.load(PATH6, map_location=device)
#모델을 평가모드로 전환   # 평가모드와 학습모드의 layer 구성이 다르다
model1.eval() 
model2.eval()
model3.eval()
model4.eval()
model5.eval()
model6.eval()

웹에서 유저가 업로드한 파일을 전처리

#전처리 : 트랜스폼 규칙 선언 # validation set 의 트랜스폼 규칙과 동일 
transforms_test = transforms.Compose([  transforms.Resize([int(600), int(600)], interpolation=transforms.InterpolationMode.BOX),
                                        transforms.ToTensor(),
                                        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])    ])                                   
#root 경로 폴더 속 jpg를 전처리, 텐서화  
testset = torchvision.datasets.ImageFolder(root = '/content/drive/MyDrive/project/static/upload' ,
                    transform = transforms_test)
#앞서 전처리한 testset 을 DataLoader를 통해 네트워크에 올리기 
testloader = DataLoader(testset, batch_size=2, shuffle=False, num_workers=0)
    #데이터 로더는 데이터의 대량 가져오기 또는 내보내기를 위한 클라이언트 응용 프로그램 

모델1~6에서 predict값 총 6개를 도출

    #for data, target in testloader: 에서 data는 데이터의 특징  target은 데이터의 정답값
with torch.no_grad(): # 평가할 땐  gradient를 backpropagation 하지 않기 때문에 no grad로 gradient 계산을 막아서 연산 속도를 높인다
    for data, target in testloader:                                   
        data, target  = data.to(device), target.to(device) 
        output1 = model1(data)   # model1에 데이터를 넣어서 아웃풋 > [a,b,c,d] 각 0,1,2,3 의 확률값 리턴 가장 큰 것이 pred
        output2 = model2(data) 
        output3 = model3(data) 
        output4 = model4(data) 
        output5 = model5(data) 
        output6 = model6(data)  
#predict # # 0~3값만 뽑기 
m1p = output1.argmax(dim=1, keepdim=True)[0][0].tolist()
m2p = output2.argmax(dim=1, keepdim=True)[0][0].tolist()
m3p = output3.argmax(dim=1, keepdim=True)[0][0].tolist()
m4p = output4.argmax(dim=1, keepdim=True)[0][0].tolist()
m5p = output5.argmax(dim=1, keepdim=True)[0][0].tolist()
m6p = output6.argmax(dim=1, keepdim=True)[0][0].tolist()

9개의 두피 타입 중 하나를 진단

  • 위 모델6개에서 뽑은 6개 프레딕트값을 가지고 최종 진단을 한다. 예를들면 모델6은 탈모의 정도를 알려주는 모델인데 여기서 탈모로 나오면 탈모 두피이고 다른 문제도 복합적으로 가지고 있으면 복합성 두피로 진단하는 것이다.

model1 : 미세각질
model2 : 피지과다
model3 : 모낭사이홍반
model4 : 모낭홍반농포
model5 : 비듬
model6 : 탈모

#진단
    d_list = [] # 두피유형진단결과
    # 두피 유형 진단법
    if m1p == 0 and m2p == 0 and m3p == 0 and m4p == 0 and m5p == 0 and m6p == 0 :
        d1 = '정상입니다.'
        d_list.append(d1)
    elif m1p != 0 and m2p == 0 and m3p == 0 and m4p == 0 and m5p == 0 and m6p == 0 :
        d2 = '건성 두피입니다.'
        d_list.append(d2)
    elif m1p == 0 and m2p != 0 and m3p == 0 and m4p == 0 and m5p == 0 and m6p == 0 :
        d3 = '지성 두피입니다.'
        d_list.append(d3)
    elif m2p == 0 and m3p != 0 and m4p == 0 and m5p == 0 and m6p == 0 :
        d4 = '민감성 두피입니다.'
        d_list.append(d4)
    elif m2p != 0 and m3p != 0 and m4p == 0 and m6p == 0 :
        d5 = '지루성 두피입니다.'
        d_list.append(d5)
    elif m3p == 0 and m4p != 0 and m6p == 0 :
        d6 = '염증성 두피입니다.'
        d_list.append(d6)
    elif m3p == 0 and m4p == 0 and m5p != 0 and m6p == 0 :
        d7 = '비듬성 두피입니다.'
        d_list.append(d7)
    elif m1p == 0 and m2p != 0 and m3p == 0 and m4p == 0 and m5p == 0 and m6p != 0 :
        d8 = '탈모입니다.'
        d_list.append(d8)
    else:
        d9 = '복합성 두피입니다.'
        d_list.append(d9)

두피 타입별 정보와 케어제품을 추천




0개의 댓글