두피 관리 방법을 잘 모르는 사람들을 위해 질환의 발현 초기부터 지속적인 관리를 도와주는 서비스 '두픽' 기획하였다.
총 3명이 진행하였고 디자인 / 앱 / 모델 3가지 부분으로 나누어 진행하였다.
나는 모델 개발 및 배포를 담당하여 프로젝트르 진행하였다.
두피 유형을 진단해줄 딥러닝 모델이 필요하며 모델을 api화 하여 서버상으로 배포할 필요가 있었다.
먼저 두피 진단 딥러닝 모델이다.
Google Colab GPU T4 환경에서 Resnet-18을 활용하여 학습을 진행하였다. 사용자의 두피 이미지를 학습시켜 6가지 항목의 중증도와 8가지 두피 유형 중 해당 두피 유형으로 진단이 가능하도록 하였다.
이를 위해선 6가지의 비듬, 탈모, 모낭사이홍반, 모낭홍반농포, 미세각질, 피지과다 항목을 0,1,2,3의 중증도를 나타내야 하며 8가지 두피 유형으로 양호, 건성, 지성, 민감성, 지루성, 염증성, 비듬성, 탈모성을 판단해야 한다.
즉, 두피 이미지에서 6가지 중증도를 파악하여 8가지 두피 유형 중 하나의 유형으로 진단해주는 것이다.
6가지 모델 개발을 진행하였다.
배치 사이즈는 5, 에폭시는 15로 설정하였다.
각 유형별로 데이터가 1만건에서 많게는 8만건이 있어 더욱 성능이 좋은 모델이나 더 높은 배치 사이즈, 에폭시를 사용했으면 좋았겠지만 기본 구글 코랩 환경에서 진행하다 보니 한계가 있었다.
평균 모델 정확도는 85%로 출력되긴 하였지만 그저 train/test 데이터 셋을 활용한 정확도일 뿐 실제 사용자의 두피 이미지에 따른 정확도는 아니다.
우선 사용자는 다음과 같은 장비를 활용해서 두피 사진을 촬영해야 한다.
현미경을 활용하여 찍은 사진은 아래와 같다.
생각보다 선명하게 나오고 전문 기기로 찍은 데이터 셋과 큰 차이가 없었다.
하지만 사용자들이 찍은 사진에 대한 라벨링이 없고 의학적 지식을 가지고 있지 않기에 라벨링을 할 수도 없었다.
따라서 위에서 이야기했듯 실질적인 모델의 정확도는 산출해보지 못했다.(하지만 육안으로 보았을 때 어느정도 경증이 있어보이면 1~2 단계의 중증도로 출력되곤 했다.)
다음은 모델 학습에 활용한 일부 코드다.
# Set device
device = torch.device("cuda")
# torch.cuda.device(device) : 선택된 장치를 변경하는 context 관리자
# torch.cuda.device 의 파라미터 : device ( torch.device 또는 int ) – 선택할 장치 인덱스, 인수가 음의 정수 또는 None이면 작동X(no-op)
hyper_param_batch = 5 # 배치 사이즈
random_seed = 100 # 랜덤 시드
# random_seed = 100 활용, 랜덤값 고정
random.seed(random_seed)
torch.manual_seed(random_seed)
model_name = 'resnet18' # 진짜 모델 이름
train_name = 'model_msh' # 트레인, 벨리, 테스트 셋 상위폴더 이름
# 여기에 모델.pt가 save
PATH = '/content/drive/MyDrive/msh_data'
transforms_train = transforms.Compose([
transforms.Resize([int(224), int(224)], interpolation=transforms.InterpolationMode.BICUBIC), # interpolation=4 워닝을 제거하기 위해 변형
transforms.RandomHorizontalFlip(),
transforms.RandomVerticalFlip(),
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(256), int(256)], interpolation=transforms.InterpolationMode.BICUBIC),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
def train_model(model, criterion, optimizer, scheduler, num_epochs=20):
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))
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 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을 통해 모델의 파라미터를 업데이트 시켜주는 과정이 필요한데,
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() # 학습 규제
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())
torch.save(model, PATH + train_name + '.pt') # 모델을 PATH경로에 트레인네임(model1).pt 라는 이름으로 저장한다
torch.save(model.state_dict(), PATH + train_name + '.pt') # 모델의 매개변수를 - 저장
print('model saved')
end_sec = time.time() - start_time # 종료시간 # 초단위에서
end_times = str(datetime.timedelta(seconds=end_sec)).split('.') # 시분초로 치환
end_time = end_times[0] # 종료시간 시분초
print("end time :", end_time) # 출력
return model, best_idx, best_acc, train_loss, train_acc, val_loss, val_acc
다음 편에서는 fastapi를 활용한 모델 api 제작으로 글을 작성해보겠다.