모델 개발 :
저희는 주어진 두피이미지와 이미지 분류 모델인 efficientnet-b7을 통해 두피타입을 진단하는 ai모델을 개발하였습니다.
웹 시스템 구현 :
그리고, 이것을 웹에 구현하여 사용자들이 자신의 두피를 자가진단할 수 있는 시스템을 구현하였습니다.
데이터를 중증도에 따라 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) #데이터 로더는 데이터의 대량 가져오기 또는 내보내기를 위한 클라이언트 응용 프로그램
#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()
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)