Myeongchan-Kim / Mobile_Doctor

For Mobile Doctor's summer project
1 stars 3 forks source link

증상으로 질병 예측하기 - 학습 결과 #20

Open root169 opened 7 years ago

root169 commented 7 years ago

증상과 아이 신상정보를 입력하면 어떤 질병에 걸릴 확률이 높은지를 예측해주는 프로그램을 설계했습니다. 표본 수가 너무 작은 구협염, 볼거리, 성홍염, 수두는 예측하지 않는 것으로 했습니다. ( 모두 100건 미만 ) 예측 결과 Test set에서 Top-1 accuracy 36% 정도, Top-3 accuracy( 가장 가능성 높은 질병 3개를 예측했을 때, 실제 질병이 그 3개의 질병 중에 포함되는 경우)가 73.3%정도였습니다. 특별한 데이터 및 모델의 조작 없이 단순히 2-hidden layer perceptron을 적용만 했을 때 얻은 결과이고, ensemble이나 sample 수 조정, hyperparameter tuning을 거치면 Top-3 accuracy를 77% 정도까지 높일 수 있을 것이라 예상하고 있습니다. 지난 번과 비슷하게 preprocessing.py / classification.py / predict.py 로 구성되어있으며, preprocessing.py에는 "disexpt_real2.csv", classification.py 에는 "final%d.csv" %datanum, predict.py에는 model.index, model.meta, model.data-00000-of-00001 파일이 있어야 프로그램이 작동합니다.

Dependencies : python3.5, python libraries(numpy, tensorflow, sklearn)

GY-jung commented 7 years ago

위에 러닝에 사용된 데이터와 추출 코드 밑에 첨부합니다

GY-jung commented 7 years ago
symp <- fread("symp.csv")
symp <- `colnames<-`(symp[,c(2,3,5,7)], c("Aid","baby_id","symp","sy_date"))

sp <- strsplit(symp$symp, split = "_")
symp$sp <- sp

symp.1 <- symp %>%
  group_by(1:n()) %>%
  mutate(B1 = sp[[1]][1],
         B2 = sp[[1]][2],
         B3 = sp[[1]][3],
         B4 = sp[[1]][4],
         B5 = sp[[1]][5],
         B6 = sp[[1]][6],
         B7 = sp[[1]][7],
         B8 = sp[[1]][8],
         B9 = sp[[1]][9],
         B10 = sp[[1]][10],
         B11 = sp[[1]][11],
         B12 = sp[[1]][12],
         B13 = sp[[1]][13],
         B14 = sp[[1]][14],
         B15 = sp[[1]][15],
         B16 = sp[[1]][16],
         B17 = sp[[1]][17],
         B18 = sp[[1]][18],
         B19 = sp[[1]][19],
         B20 = sp[[1]][20],
         B21 = sp[[1]][21],
         B22 = sp[[1]][22],
         B23 = sp[[1]][23],
         B24 = sp[[1]][24],
         B25 = sp[[1]][25],
         B26 = sp[[1]][26],
         B27 = sp[[1]][27],
         B28 = sp[[1]][28])

symp.2 <- symp.1 %>%
  select(-symp, -sp) %>%
  arrange(sy_date)

write.csv(symp.2, "symp.real.csv")

symp.2 <- fread("symp.real.csv")

smerge <- merge(symp.2, dis.1, by = c("Aid", "baby_id"), allow.cartesian=TRUE)
sd <- smerge %>%
  filter(ymd_hm(sy_date) - ymd_hm(S_date) <= 0) %>%
  filter(ymd_hm(sy_date) - ymd_hm(S_date) >= -86400)

sda <- merge(sd, baby.f, by = c("Aid", "baby_id"), allow.cartesian=TRUE )

write.csv(sda, "disexpt.csv")

dfd <- merge(dis.1, fva.1, by = c("Aid", "baby_id"), allow.cartesian=TRUE )
dfd.1 <- dfd %>%
  filter(key %in% sda$key) %>%
  filter(ymd_hm(date) - ymd_hm(S_date) <= 86400) %>%
  filter(ymd_hm(date) - ymd_hm(S_date) >= -259200)

dfd.2 <- dfd.1 %>%
  group_by(key) %>%
  mutate(maxfever = max(fever)) %>%
  mutate(minfever = min(fever))

dfd.3 <- dfd.2 %>%
  select(key, maxfever, minfever) %>%
  distinct(key, maxfever, minfever)

dsda <- merge(sda, dfd.3 ,by = c("key"))
write.csv(dsda, "disexpt_real2.csv")

sd.1 <- dsda %>%
  group_by(kind) %>%
  summarise(S1 = sum(as.numeric(B1)),
            S2 = sum(as.numeric(B2)),
            S3 = sum(as.numeric(B3)),
            S4 = sum(as.numeric(B4)),
            S5 = sum(as.numeric(B5)),
            S6 = sum(as.numeric(B6)),
            S7 = sum(as.numeric(B7)),
            S8 = sum(as.numeric(B8)),
            S9 = sum(as.numeric(B9)),
            S10 = sum(as.numeric(B10)),
            S11 = sum(as.numeric(B11)),
            S12 = sum(as.numeric(B12)),
            S13 = sum(as.numeric(B13)),
            S14 = sum(as.numeric(B14)),
            S15 = sum(as.numeric(B15)),
            S16 = sum(as.numeric(B16)),
            S17 = sum(as.numeric(B17)),
            S18 = sum(as.numeric(B18)),
            S19 = sum(as.numeric(B19)),
            S20 = sum(as.numeric(B20)),
            S21 = sum(as.numeric(B21)),
            S22 = sum(as.numeric(B22)),
            S23 = sum(as.numeric(B23)),
            S24 = sum(as.numeric(B24)),
            S25 = sum(as.numeric(B25)),
            S26 = sum(as.numeric(B26)),
            S27 = sum(as.numeric(B27))) %>%
  arrange(as.numeric(kind))
GY-jung commented 7 years ago

열 패턴으로 진단 전 3일동안의 최고와 최저 체온을 가져와서 같이 러닝했지만 결과가 크게 좋아지지 않아서 word2vector를 활용, sympton2vector 를 다음주에 돌려볼 생각입니다. 추가할만한 feature나 정확도를 높힐 방법이 있다면 조언 부탁드립니다

root169 commented 7 years ago

utils.py

import numpy as np

def csvreader(filename, delimiter = ','):
    fever = []
    with open(filename, 'r') as csvfile:
        feverreader = csv.reader(csvfile, delimiter = delimiter)
        for row in feverreader:
            fever.append(row)
    return fever

def is_number(s):

    try:
        float(s)
        return True
    except ValueError:
        return False

def tonumpy(fever) : 
    fever = np.asarray(fever)
    return fever

def normalize(row):
    avg = np.mean(row)
    row -= avg
    var = np.var(row)
    row /= np.sqrt(var)
    return row, avg, var

def printmax(data):
    print ("max")
    print (np.amax(data, axis = 0))

def printmin(data):
    print ("min")
    print (np.amin(data, axis = 0))
root169 commented 7 years ago

preprocessing.py

#coding=utf-8
# 데이터셋을 합치고 가공하는 과정. 비정상적인 값들을 제거하고, 필요한 feature들을 더 넣어줌.
# 디렉토리에 있는 "disexpt_real2.csv" 파일을 받아 "final%d.csv (% datanum)", "avg.csv", "var.csv" 파일을 저장함. 
import csv
import numpy as np
import time
from utils import *

scalable = [29,31] # 체중, 나이의 경우 평균 0, variance 1로 scale 해주는 것이 도움될 것으로 추정됨. scale 가능한 변수들의 index임.  
datanum = 29779  # 사용할 data 개수. 

def regvalue(data):  # dataset에서 regression에 쓸 value만 남김

  total__ = data[:,[7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,38,39,40]]
  return total__

def nanfilter(data,p) : # dataset에서 missing value 지움
  nan= data[:, p] 
  nan_mask = np.zeros_like(nan, dtype = "bool")
  for i in np.arange(len(nan)):
    if ( is_number(nan[i]) ) :
      nan_mask[i] = True
  data = (data[nan_mask])
  return data

def elimoutlier(data): # 체중, 해열제 섭취량이 정상 범위에서 벗어난 것 지움.
  weight = data[:,29]
  weight_mask = np.zeros_like(weight, dtype = "bool")
  for i in np.arange(len(weight)):
    if is_number(weight[i]) and ( float(weight[i]) >=2 ) and (float(weight[i])<=40):
      weight_mask[i] = True
  data = data[weight_mask]
  return data

def tosmallfilter(data): #환자가 너무 적은 질병 지움
  kind = data[:,28]
  kind_mask = np.ones_like(kind, dtype = "bool")
  for i in np.arange(len(kind)):
    if kind[i] in [10, 16, 17, 18]:
      kind_mask[i] = False
  data = data[kind_mask]
  return data

def filterwrapper(data): # 위의 5개 filter wrap함
  data1 = data[:datanum]
  data2 = regvalue(data1)
  print data2[0]
  for cnt in np.arange(data2.shape[1]):
    data3 = nanfilter(data2, cnt)

  data4 = np.array(data3, dtype = "float") 
  data5 = elimoutlier(data4)  
  data6 = tosmallfilter(data5)
  return data6

def renamekind(data):  #질병 rename
  kind = data[:, 28]
  for i in np.arange(len(kind)) :
    if data[i, 28] == 20 :
      data[i, 28] = 16
    if data[i, 28] > 10 :
      data[i,28] -= 1
  return data

def normalizescalable(data, scalable) : # scale 가능한 변수와 그것의 역수들을 normalize함. 나중에 평균과 분산을 predict.py에서 사용할 것이기 때문에 csv로 저장.
  avg = np.zeros(len(scalable))
  var = np.zeros(len(scalable))  
  for cnt in np.arange(len(scalable)):
    data[:, scalable[cnt]], avg[cnt], var[cnt] = normalize(data[:, scalable[cnt]])
  return data, avg, var

def oversampling(data, kinds): # 환자 수가 조금 적은 질병에 대해서, label이 biased되어있어 training이 안되는 문제 해결하기 위해 조금 적은 질병에 대해서 한 환자를 여러번 sampling함.
  kind = data[:, 28]
  oversamples = np.array(data[0])
  for i in np.arange(len(kind)):
    if kind[i] in kinds:
      oversample = data[i]
      oversamples = np.append(oversamples, oversample)
  oversamples = oversamples.reshape(-1,32)
  data = np.concatenate((data, oversamples), axis = 0)
  return data     

def featureaddwrapper(data) : # 위의 4개 feature 변환 및 추가하는 함수들 wrap함

  data1 = renamekind(data)
  data2 = oversampling(data1, [3,8,9,12,14,15])
  data3 = oversampling(data2, [9,12,14,15])
  data4 = oversampling(data3, [12])
  data5, avg, var = normalizescalable(data4, scalable)

  with open("avg.csv", 'wb') as writer :
    np.savetxt("avg.csv", avg, fmt = '%s', delimiter = ' ')

  with open("var.csv", 'wb') as writer : 
    np.savetxt("var.csv", var, fmt = '%s', delimiter = ' ' )
  return data5

def generate_data(datanum, now = False): # 실제로 사용할 data 만들어서 csv로 저장
  if now : 
    data = tonumpy(csvreader("disexpt_real2.csv"))
    data1 = data[:datanum]
    data2 = filterwrapper(data1)
    data3 = featureaddwrapper(data2)
    with open("final_%d.csv" %datanum, 'wb') as writer:
      np.savetxt("final_%d.csv" %datanum, data3, fmt = '%s', delimiter = ' ')

generate_data(datanum, now = False) # 데이터 만들 때 True
root169 commented 7 years ago

classification.py

# coding=utf-8 

# 디렉토리에 "final.csv" 을 필요로 함 

import numpy as np
import csv
import time
import tensorflow as tf
from preprocessing import *
from sklearn.metrics import confusion_matrix
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
# 모델 hyperparameter

lr = 1e-4
decay = 0.99    
hidden1 = 50
hidden2 = 50    
datanum = 29779
n_classes = 16

""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
#데이터 로딩

data = tonumpy(csvreader("final_%d.csv" %datanum, delimiter = ' '))
data = np.array(data, dtype = "float")

np.random.shuffle(data)
printmax(data)
printmin(data)
data = np.transpose(data)

""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
#dataset 분할

train_set = data[:, 0:int(data.shape[1] * 0.8)]
dev_set = data[:, int(data.shape[1]* 0.8) : int(data.shape[1]* 0.9)]
test_set = data[:, int(data.shape[1]* 0.9):]             # 8 : 1 : 1 로 training / validation / test 을 분할함

""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
# 체온 감소량을 예측할 것이므로 체온 감소량을 제외한 값들을 training에 사용
temp_mask = np.ones(data.shape[0], dtype = "bool")
temp_mask[28] = False                                    

X_train = np.array(train_set[temp_mask], dtype = "float")
temp_train = np.array(train_set[28], dtype = "float")

X_dev = np.array(dev_set[temp_mask], dtype = "float32")
temp_dev = np.array(dev_set[28], dtype = "float32")

X_test = np.array(test_set[temp_mask], dtype = "float32")
temp_test = np.array(test_set[28], dtype = "float32")

X_train = np.transpose(X_train)
X_dev = np.transpose(X_dev)
X_test = np.transpose(X_test)
temp_train = np.transpose(temp_train)
temp_dev = np.transpose(temp_dev)   
temp_test = np.transpose(temp_test)
temp_train = np.squeeze(temp_train)
temp_dev = np.squeeze(temp_dev )
temp_test = np.squeeze(temp_test)

""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
#some tensorflow...

#placeholder
inputs_placeholder = tf.placeholder(tf.float32, (None, X_train.shape[1]), name = "inputs")
temps_placeholder = tf.placeholder(tf.int32, (None, ), name = "temps")

#affine layers
W1 = tf.Variable(tf.random_uniform((X_train.shape[1],hidden1), minval = -np.sqrt(6.0/(X_train.shape[1] + hidden1) ), maxval = np.sqrt(6.0/(X_train.shape[1] + hidden1))), dtype = tf.float32)
b1 = tf.Variable(tf.zeros((hidden1,)) , dtype = tf.float32)
W2 = tf.Variable(tf.random_uniform((hidden1,hidden2), minval = -np.sqrt(6.0/(hidden1 + hidden2)), maxval = np.sqrt(6.0/(hidden1 + hidden2))), dtype = tf.float32)
b2 = tf.Variable(tf.zeros((hidden2,)), dtype = tf.float32)
W3 = tf.Variable(tf.random_uniform((hidden2,n_classes), minval = -np.sqrt(6.0/hidden2), maxval = np.sqrt(6.0/hidden2)), dtype = tf.float32)
b3 = tf.Variable(tf.zeros((n_classes,)), dtype = tf.float32)

#activation : tanh 
z1 = tf.matmul(inputs_placeholder, W1)  + b1
h1 = tf.nn.tanh(z1)
z2 = tf.matmul(h1, W2) + b2
h2 = tf.nn.tanh(z2)
z3 = tf.add(tf.matmul(h2, W3) , b3)  # logits
softmax = tf.nn.softmax(z3, name = "op_to_restore")

#softmax loss
loss = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits = z3, labels = temps_placeholder))
value, pred = tf.nn.top_k(softmax, k=3, sorted = True)

#model 재사용을 위한 saver
saver = tf.train.Saver()

#Adamoptimizer
optimizer = tf.train.AdamOptimizer(lr)  
train_op = optimizer.minimize(loss)

init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init)

#Training. 100번의 step마다 loss 확인. devset에서 loss가 늘어난 경우에 learning rate를 0.99배로 decay

best_dev = 10
for step in range(5001):
    sess.run(train_op, feed_dict = {inputs_placeholder : X_train, temps_placeholder : temp_train})

    if step%100 == 0:   # 매 스텝마다 질병별 예상 확률과 실제 질병, train_loss, dev_loss 출력
        print(step, sess.run(loss,  feed_dict = {inputs_placeholder : X_train, temps_placeholder : temp_train}), sess.run(loss, feed_dict = {inputs_placeholder : X_dev, temps_placeholder : temp_dev}))
        print(sess.run(softmax[0], feed_dict = {inputs_placeholder : X_train, temps_placeholder : temp_train}))
        print (temp_train[0])
        if best_dev < sess.run(loss, feed_dict = {inputs_placeholder : X_dev, temps_placeholder : temp_dev}) : 
            lr *= decay
            print ("decay")
            print (lr)
        else :
            best_dev = sess.run(loss, feed_dict = {inputs_placeholder : X_dev, temps_placeholder : temp_dev})

print ("test set loss")
print (sess.run(loss, feed_dict = {inputs_placeholder : X_test, temps_placeholder : temp_test}))

print ("top-3 error rate") 
values = (sess.run(value, feed_dict = {inputs_placeholder : X_test, temps_placeholder : temp_test}))
preds =  (sess.run(pred, feed_dict = {inputs_placeholder : X_test, temps_placeholder : temp_test}))

#cm = confusion_matrix(temp_test, preds, labels=range(16))
#print cm
correct = 0.0
for cnt in np.arange(len(preds)) :
  if temp_test[cnt] in preds[cnt]:
    correct += 1
accuracy = correct / len(preds)
print (1 - accuracy)

#save model. predict에서 사용함

save_path = saver.save(sess, 'model')
root169 commented 7 years ago

predict.py

# coding=utf-8 
# regression.py 에서 만든 모델을 바탕으로 예상 감소 체온을 예측해주는 프로그램. example_info에 차례로 체온, 해열제 양, 해열제 섭취 후 경과 시간, 해열제 종류, 체중, 성별, 나이( 단위 : day) 를 입력하면, 예상 감소 체온을 알려주는 프로그램.
import tensorflow as tf
from preprocessing import *
from utils import *

scalable = [29,31]
example_symptoms = (6, 13)
example_baby_info = (15.0, 0, 1140)  # 예시 데이터. 

def predict_temp(symptoms, info): 
    processed_data = np.zeros((32,1))
    for symptom in symptoms:
      processed_data[symptom-1] = 1
    weight, gender, born_date = info
    processed_data[28] = 0 # dummy
    processed_data[29] = weight
    processed_data[30] = gender
    processed_data[31] = born_date
    return processed_data

def pseudonormalize(data): 
    avg = tonumpy(csvreader("avg.csv", delimiter = ' '))
    var = tonumpy(csvreader("var.csv", delimiter = ' '))

    for cnt in np.arange(len(scalable)):
        data[scalable[cnt]] -= float(np.asscalar(avg[cnt]))
        data[scalable[cnt]] /= np.sqrt(float(np.asscalar(var[cnt])))
    return data

""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
# example_info를 모델에 들어갈 수 있게 변환
test = pseudonormalize(predict_temp(example_symptoms, example_baby_info))
time_mask = np.ones(len(test), dtype = "bool")
time_mask[28] = False

""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

#model loading

sess = tf.Session()
saver = tf.train.import_meta_graph("model1.meta")
saver.restore(sess, "model1")

""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
#모델에 데이터 넣어 계산
X_test = np.array(test[time_mask], dtype = "float32")
temp_test = np.array(test[28], dtype = "float32")

X_test = np.transpose(X_test)
temp_test = np.transpose(temp_test)
graph = tf.get_default_graph()
temps_placeholder = graph.get_tensor_by_name("temps:0")
inputs_placeholder = graph.get_tensor_by_name("inputs:0")
feed_dict = { inputs_placeholder : X_test, temps_placeholder : temp_test }
op_to_restore = graph.get_tensor_by_name("op_to_restore:0")

value, pred = tf.nn.top_k(op_to_restore, k=3, sorted = True)
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
#예상 질병 top3 및 확률 출력

print ("3 estimated disease number, probability")
value, pred = (sess.run((value, pred), feed_dict = feed_dict))
print pred
print value
ChanhoPark93 commented 7 years ago

'증상들의 지속기간'을 분석에 사용해보면 어떨까 싶은데요, 다만 정보량이 충분할지가 조금 걱정됩니다. 자세한 논의는 월요일에 하는게 좋을 것 같습니다.

Myeongchan-Kim commented 7 years ago

중간에 체온 감소량은 뭐죠? 여기 주제가 좀 헷갈리네요.

그리고 Input variable 정의에 관해서 좀더 명확하게 해 주실수 있을까요? 어떤것을 토대로 예측했는지 잘 모르겠습니다.

왜냐하면 진단명 예측은 시계열 예측에 가까워서 단순 그냥 dnn보다 rnn이나 다른 시계열 적용방법을 사용하는게 좋을거에요.

그리고 그냥 가장 흔한질환 3개로 예측을 하면 몇 % 정도 나오나요? baseline이랑 goal로 삼을 만한 정확도의 기준이 필요합니다.

Myeongchan-Kim commented 7 years ago

모든 질병을 다 예측하려고 하면 절대 좋은 결과가 나올 수 없습니다.

제일 맞추기 쉽다고 생각되는 한가지 질병을 target 삼아서 먼저 시도해 보는게 어떨까 싶은데요?

root169 commented 7 years ago

정확도를 많이 개선하여 Top-3 accuracy 89.5%에 도달했습니다. 전의 73%보다 많이 나아졌는데요 i. 비슷한 증상 및 비슷한 치료법을 갖는 질병들을 합쳤습니다. 기관지염, 모세기관지염, 폐렴을 하나의 질병으로, 열감기와 중이염을 하나의 질병으로 합쳤습니다. ii. 샘플 수를 맞추기 위해서 oversampling을 조금 강하게 적용했습니다. iii. Hyper parameter tuning도 열심히 했고요...

코드도 조금 refactoring해서 첨부합니다.

root169 commented 7 years ago

classification.py

# coding=utf-8
# This program trains a model that can classify 10 diseases from various symptoms and baby's information.

# You need datafile "final_%d.csv" %datanum in your directory . 

import numpy as np
import csv
import time
import tensorflow as tf
from preprocessing import *
from sklearn.metrics import confusion_matrix
import pylab
from matplotlib import mpl, pyplot
import matplotlib.pyplot as plt
import itertools
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

#hyperparameters. Probably the best. You may tweak some of them, but the accuracy would not increase more than 2%.

lr = 1e-5
n_features = 31
decay = 0.96
hidden1 = 100
hidden2 = 100
reg = 0.001
datanum = 9602
n_classes = 10
num_iters = 20000
global_step = tf.Variable(0, trainable=False)
learning_rate = tf.train.exponential_decay(lr, global_step, 100, decay, staircase=True) #decays learning rate while training. After 1000 steps, learning rate decreases to about 2/3.

train_loss = []
dev_loss = []
train_accuracy = []
dev_accuracy = []

# helper function that returns accuracy
def accuracy(preds, labels) : 
  correct = 0.0

  for cnt in np.arange(len(preds)) :
    if labels[cnt] in preds[cnt]:
      correct += 1
  accuracy = correct / len(preds)
  return (accuracy)

#helper function that plots confusion matrix
def plot_confusion_matrix(cm, classes,
                          normalize=False,
                          title='Confusion matrix',
                          cmap=plt.cm.Blues):
    """
    This function prints and plots the confusion matrix.
    Normalization can be applied by setting `normalize=True`.
    """
    plt.imshow(cm, interpolation='nearest', cmap=cmap)
    plt.title(title)
    plt.colorbar()
    tick_marks = np.arange(len(classes))
    plt.xticks(tick_marks, classes, rotation=45)
    plt.yticks(tick_marks, classes)

    if normalize:
        cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
    np.set_printoptions(precision=2)

        print("Normalized confusion matrix")
    else:
        print('Confusion matrix, without normalization')

    print(cm)

    thresh = cm.max() / 2.
    for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
        plt.text(j, i, format(cm[i, j], '.2f'),
                 horizontalalignment="center",
                 color="white" if cm[i, j] > thresh else "black")

    plt.tight_layout()
    plt.ylabel('True label')
    plt.xlabel('Predicted label')

#dataloader
def dataloading():
  data = tonumpy(csvreader("final_%d.csv" %datanum, delimiter = ' '))
  data = np.array(data, dtype = "float")
  np.random.shuffle(data)
  data = np.transpose(data)
  return data

#train validation test data split on ratio of 6 : 2 : 2
def datasplit(data):
  train_set = data[:, 0:int(data.shape[1] * 0.6)]
  dev_set = data[:, int(data.shape[1]* 0.6) : int(data.shape[1]* 0.8)]
  test_set = data[:, int(data.shape[1]* 0.8):]             
  return train_set, dev_set, test_set

#extract labels
def labelgenerator(data):
  temp_mask = np.ones(data.shape[0], dtype = "bool")
  temp_mask[0] = False                                    
  return temp_mask

#match number of data that has each label in similar scale. It helps machine by making it look at every label similar times 
def oversamples(train_set):
  train_set = np.transpose(train_set)
  train2 = oversampling(train_set, [0,1,2,5,6,7,8,9])
  train3 = oversampling(train2, [1,5,6,7,8,9])
  train4 = oversampling(train3, [1,5,6,7,8,9])
  train5 = oversampling(train4, [5,6,9])
  train6 = oversampling(train5, [5,6,9])
  train_set = np.transpose(train6)
  return train_set

# data generation wrapper
def datacreation(data):
  train_set, dev_set, test_set = datasplit(data)
  temp_mask = labelgenerator(data)
  train_set = oversamples(train_set)
  X_train = np.array(train_set[temp_mask], dtype = "float")
  temp_train = np.array(train_set[0], dtype = "float")

  X_dev = np.array(dev_set[temp_mask], dtype = "float32")
  temp_dev = np.array(dev_set[0], dtype = "float32")

  X_test = np.array(test_set[temp_mask], dtype = "float32")
  temp_test = np.array(test_set[0], dtype = "float32")

  X_train = np.transpose(X_train)

  X_dev = np.transpose(X_dev)

  X_test = np.transpose(X_test)

  temp_train = np.transpose(temp_train)
  temp_dev = np.transpose(temp_dev) 
  temp_test = np.transpose(temp_test)

  temp_train = np.squeeze(temp_train)
  temp_dev = np.squeeze(temp_dev)
  temp_test = np.squeeze(temp_test)

  return X_train, X_dev, X_test, temp_train, temp_dev, temp_test

# Ready is the data!
data = dataloading()
X_train, X_dev, X_test, temp_train, temp_dev, temp_test = datacreation(data)

#tensorflow model generation..
#I use 2-hidden layer perceptron. Increasing layers did NOT help much.
#placeholders
inputs_placeholder = tf.placeholder(tf.float32, (None, n_features), name = "inputs")
temps_placeholder = tf.placeholder(tf.int32, (None, ), name = "temps")

#weights and biases
W1 = tf.Variable(tf.random_uniform((n_features,hidden1), minval = -np.sqrt(6.0/(n_features + hidden1) ), maxval = np.sqrt(6.0/(n_features + hidden1))), dtype = tf.float32)
b1 = tf.Variable(tf.zeros((hidden1,)) , dtype = tf.float32)
W2 = tf.Variable(tf.random_uniform((hidden1,hidden2), minval = -np.sqrt(6.0/(hidden1 + hidden2)), maxval = np.sqrt(6.0/(hidden1 + hidden2))), dtype = tf.float32)
b2 = tf.Variable(tf.zeros((hidden2,)), dtype = tf.float32)
W3 = tf.Variable(tf.random_uniform((hidden2,n_classes), minval = -np.sqrt(6.0/hidden2), maxval = np.sqrt(6.0/hidden2)), dtype = tf.float32)
b3 = tf.Variable(tf.zeros((n_classes,)), dtype = tf.float32)

#I used tanh as activation since it is zero-centered, unlike relu / sigmoid / etc... 
#It is especially important to keep activation zero-centered unless layers are deep enough or you use other normalization techniques, such as batch normalization or selu activation.

z1 = tf.matmul(inputs_placeholder, W1)  + b1 #affine layer
h1 = tf.nn.tanh(z1) #non-linearity
z2 = tf.matmul(h1, W2) + b2 #affine layer
h2 = tf.nn.tanh(z2) #non-linearity
z3 = tf.add(tf.matmul(h2, W3) , b3)  #affine layer
softmax = tf.nn.softmax(z3, name = "op_to_restore") # logits

loss_ = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits = z3, labels = temps_placeholder)) # cross-entropy loss
loss__ = reg / 2.0 * (tf.reduce_sum(tf.square(W1)) + tf.reduce_sum(tf.square(W2)) + tf.reduce_sum(tf.square(W3))) #regularizer
loss = loss_ + loss__ #total loss. Model minimizes this.

value1, pred1 = tf.nn.top_k(softmax, k=1, sorted=True) # top-1 prediction
value2, pred2 = tf.nn.top_k(softmax, k=2, sorted=True) # top-2 prediction
value3, pred3 = tf.nn.top_k(softmax, k=3, sorted=True) # top-3 prediction

optimizer = tf.train.AdamOptimizer(learning_rate)   #used AdamOptimizer. It converges well compared to other optimizers. 
train_op = optimizer.minimize(loss)
saver = tf.train.Saver()

# call session to start training
init = tf.global_variables_initializer()
sess = tf.Session()    
sess.run(init)

best_dev = 10

#Training. Training is stopped when dev_loss stops to decrease. It takes about 15000 iterations.
#Train_loss reaches about 1.00 or a little bit less, and dev_los about reaches about 1.50 or less when dev_loss stops to decrease.
#If the model randomly guesses label, then it will get loss about ln(10) + 0.1 ~= 2.4

for step in range(num_iters + 1):
  sess.run(train_op, feed_dict = {inputs_placeholder : X_train, temps_placeholder : temp_train})
  if step%100 == 0:  
    current_loss_on_train = sess.run(loss,  feed_dict = {inputs_placeholder : X_train, temps_placeholder : temp_train}) 
    current_loss_on_dev = sess.run(loss,  feed_dict = {inputs_placeholder : X_dev, temps_placeholder : temp_dev}) 
    print (step, current_loss_on_train, current_loss_on_dev)
    train_loss.append(current_loss_on_train)
    dev_loss.append(current_loss_on_dev)
    pred3_train = (sess.run(pred3, feed_dict = {inputs_placeholder : X_train, temps_placeholder : temp_train}))
    pred3_dev = (sess.run(pred3, feed_dict = {inputs_placeholder : X_dev, temps_placeholder : temp_dev}))

    train_accuracy.append(accuracy(pred3_train, temp_train))
    dev_accuracy.append(accuracy(pred3_dev, temp_dev))

    if best_dev < current_loss_on_dev : 
      break
    else : 
      best_dev = current_loss_on_dev

print ("test set loss")
print (sess.run(loss, feed_dict = {inputs_placeholder : X_test, temps_placeholder : temp_test}))

#call tensors
values1 = (sess.run(value1, feed_dict = {inputs_placeholder : X_test, temps_placeholder : temp_test}))
preds1 =  (sess.run(pred1, feed_dict = {inputs_placeholder : X_test, temps_placeholder : temp_test}))

values2 = (sess.run(value2, feed_dict = {inputs_placeholder : X_test, temps_placeholder : temp_test}))
preds2 =  (sess.run(pred2, feed_dict = {inputs_placeholder : X_test, temps_placeholder : temp_test}))

values3 = (sess.run(value3, feed_dict = {inputs_placeholder : X_test, temps_placeholder : temp_test}))
preds3 =  (sess.run(pred3, feed_dict = {inputs_placeholder : X_test, temps_placeholder : temp_test}))

# plot some fancy figures...

# loss figure
plt.plot(train_loss)
plt.plot(dev_loss)
plt.ylabel('losses')
plt.xlabel('num_iters')
plt.show()

# accuracy figure
plt.plot(train_accuracy)
plt.plot(dev_accuracy)
plt.ylabel('accuracies')
plt.xlabel('num_iters')
plt.show()

# Plot normalized confusion matrix

cnf_matrix = confusion_matrix(temp_test, preds1)
np.set_printoptions(precision=2)

plt.figure()
plot_confusion_matrix(cnf_matrix, classes=np.arange(10), normalize=True,
                      title='Normalized confusion matrix')

plt.show()

#check accuracy

print accuracy(preds1, temp_test)
print accuracy(preds2, temp_test)
print accuracy  (preds3, temp_test)

saver.save(sess,'model')
root169 commented 7 years ago

Top1 accuracy : 45.9% Top2 accuracy : 73.0% Top3 accuracy : 89.5%

root169 commented 7 years ago

loss

트레이닝이 잘 진행되나 100 iterations 마다 확인해 보았습니다.

accuracy

curve들에서는 파란색이 training set, 초록색이 validation set입니다.

confusion_matrix 모델이 예상하는 질병 목록은 총 10개로, 0 : 기관지염, 모세기관지염, 폐렴 (1650 samples) 1 : 후두염 / 크룹 (246 samples) 2 : 인두염 / 편도염 (1747 samples) 3 : 열감기, 중이염 (3723 samples) 4 : 독감 (1202 samples) 5 : 구내염(단순포진) (131 samples) 6 : 구내염(헤르페안지나) (36 samples) 7 : 수족구 (269 samples) 8 : 장염 / 식중독 (478 samples) 9 : 요로감염 (58 samples) 이었습니다.

Confusion matrix는 모델이 특정 아이의 증상과 정보(체중, 성별, 체온) 을 보고 예상한 가장 가능성 높은 질병과 실제 질병을 표시한 행렬입니다. 샘플 숫자를 고려하면 틀린 것들이 열감기 및 중이염, 독감에서 나왔는데, 특히 열감기의 경우 명확한 진단이 없는 것이 특징이기 때문에 일어나는 현상인 것 같습니다. 독감도 증상으로만 예측하기에는 어려움이 있습니다,

정확도를 더 개선해야할지, 서비스 적용에 있어서 어떠한 요소들이 더 필요할지 도움 주시면 좋겠습니다.

Myeongchan-Kim commented 7 years ago

ㅎㅎ 좋은 시도 잘 보았습니다. 흥미롭네요. 개인적으로 오랜만에 파이썬 코드를 봐서 좋네요ㅎㅎ 이제부터 무간지옥에 오신것을 환영합니다... 1%올리기가 쉽지 않을겁니다.

먼저 증상으로 봤을때 비슷한 질병들을 묶은것 같은데 , 여기서 약간의 오류가 있네요. 중이염과 열감기는 전혀 다른 질환이기 때문에 같이 묶으면 안됩니다. 치료법도 항생제 여부가 갈라지게 되요. 열감기는 오히려 인두염에 가깝고, 중이염은 다른질환과 합치기가 어렵습니다. 질병들을 모으는것은 의학적인 조언을 참고하는게 더 좋았을 것 같습니다. 예를들면 5,6은 하나로 묶어도 괜찮습니다.

각 질환들을 PCA 분석을 통해서 혹시 그룹화 할 수 있는지 들여다 보는것도 좋을것 같아요. t-sne라는 파이썬 라이브러리가 있는데 혹시 사용해 볼 수 있으면 시각화 하는데 도움이 됩니다.

질환들이 너무 다양하기 때문에 feature engineering 하기가 쉽지 않네요. 한 두개 확실히 맞출 수 있는 질환으로 targeting 해보면 feature 뽑아낼 질환들이 보일 것 같습니다. 예를들어 수족구나 후두염은 쉽게 분리 할 수 있을것 같아요. 시기별 유행도 같은 feature를 뽑아서 추가해보는것도 좋을 것 같습니다.

root169 commented 7 years ago

신재원 대표님께서도 중이염에 대해 비슷한 말씀을 하셨습니다. 중이염은 저희 데이터로 뭔가 예측을 하기가 어렵다는 것이 결론이었는데, 중이염의 가장 결정적인 증상이 이통인데 저희 앱에서는 이통을 입력할 수 없어서 아마 예측하는 것이 어려울 것이라는게 그 이유였습니다. 아예 중이염을 빼고 예측해보는것은 어떤지 여쭤보고싶습니다. 5랑 6은 묶어보겠습니다. 모델이 6을 5로 헷갈리는 것 같더라고요.

질병을 PCA하려면 질병이 뭔가 높은 차원의 공간에 mapping 되어있어야 합니다. word2vec 한 결과를 보통 PCA나 t-sne library의 함수들을 이용해서 보는 것으로 알고 있는데, classification 결과를 어떻게 mapping하라는 것인가요?

유행도 feature도 독감 예측할 때 많이 쓰는 것 같던데, 유행도를 도입하는 것은 또 아예 새로운 문제네요. 장세윤 씨 도움 받아서 한 번 해보겠습니다.

이 프로그램을 앱 속의 서비스로 제공한다고 했을 때, 어떤 점들을 더 개선해야 하나요?

ChanhoPark93 commented 7 years ago

중이염을 빼고 러닝을 했더니 오히려 정확도가 떨어졌다고 합니다..

사실 제가 중이염과 열감기를 combine해보라고 제안했는데요, 열감기라는 질단이 사실 '증상이 이것저것 있는데 뭔지 잘 모르겠을 때 붙이는 추정진단'이기도 하고, 중이염은 선행질환으로 호흡기 감염을 가지고 있어서 둘을 하나로 묶어보라고 하였습니다. 서비스를 제공할 때 호흡기 감염이 중이염으로 진행될 수 있다는 안내를 하여 보호자에게 신뢰도를 주면 어떨까 하고 생각하였습니다.

Myeongchan-Kim commented 7 years ago

음... 정확도가 떨어지는건 어쩔수 없는거긴 한데, 열감기를 진단 받았다면 이경검사정도는 했는데 아무것도 안나왔을 것 같아요. 합치기 전/후 에 열감기+중이염 더한거랑 비교하면 실제로 더 나아진 것이 아닐 수도 있습니다.

같은 논리라면 요로감염하고 같이 감염증 으로 묶어볼 수도 있겠죠. 서비스 제공할때를 고려해서 생각해본거면 좋은 시도는 맞네요.

PCA는 차원 축소에 쓰이니까 지금 증상 데이터가 26차원인가 그렇죠? 그걸 4차원 정도로 줄여보는 거죠. 지금 은호학생이 진행했으니까 비슷하게 해보면 좋을것 같습니다. t-sne는 시각화 툴이니까, 데이터를 이해하려고 하는 것이라서 ㅎㅎ 꼭 할 필요는 없구요, 저는 데이터가 이해가 안되서 속터질때 한번씩 해봅니다.

root169 commented 7 years ago

중이염까지 다 예측을 하면 결과가 85% 전후로 나옵니다. 열감기를 아예 제거하는 경우 92% 정도의 정확도가 나타나고요. 다양한 시도를 해보고 있습니다.

  1. Oversampling안 하기. 라벨별로 개수를 맞춰주려는 시도가 빈도수가 적은 질병을 잘 예측하게 해주는 것은 사실이나, 오히려 실제 세계의 유병률 분포를 왜곡하기 때문에 도움이 되지 않음을 확인했습니다.
  2. Hyperparameter tuning. Hidden layer 수를 많이 줄였습니다.
  3. 요로감염(샘플수 58/9600), 중이염 예측(샘플수 821/9600) 목록에서 제거. Oversampling이 없는 경우에 실제로 요로감염인 어떤 데이터도 요로감염으로 예측하지 못하고, 과반 이상을 열감기로 예상합니다. 나머지는 기관지염, 인후염 등 질병을 비슷한 비율로 예상합니다. 따라서 예상하는 것이 큰 의미가 없을 것이라 판단하여, 요로감염은 예측하지 않는 것으로 했습니다. 중이염도 비슷한 현상을 보여서 제거해 보았았습니다. 실제 서비스 차원에서는 열감기가 예상되었을 때 중이염이 동반할 수 있다는 멘트를 넣어주면 어떤지 생각하고 있습니다. 요로감염 빼지 않는 경우 84.5% -> 요로감염 뺀 경우 87.0% 요로감염만 뺀 경우 87.0% -> 요로감염, 중이염 뺀 경우 92.1%

더 해보려는 시도

  1. 유행도 개념 도입(은호씨 도움을 받을 계획입니다)
  2. 교과서를 따라서 인위적으로 만들어진 data 트레이닝에 사용하기( 열심히 만드는 중입니다. 의외로 기관지염과 후두염, 인후염과 같이 샘플 수가 많은 것들을 헷갈리는 경우가 많아서, 데이터의 부족인지 아니면 데이터 자체의 분산이 큰 것인지 보려고 합니다.)
  3. 앙상블(텐서플로우가 앙상블 하기가 귀찮은 구조라 아직 안해봤는데, 하면 확실히 2%정도 나아지지 않을까요...) 추가적으로 시도해 볼만한 것들이 있으면 도움주시기 바랍니다.

PCA 해본 결과 의외로 증상들이 많이 독립적인 편이었습니다. 맨 마지막의 27번째 요소도 전체 분산에 1% 이상 기여하고 있었고, 첫 번째 요소도 13%정도의 비중밖에 차지하고 있지 않았습니다. 그래서 그냥 증상 전체를 다 활용하려고 합니다.

Myeongchan-Kim commented 7 years ago

재밌는 시도 많이 해보고 있네요. 좋습니다.

27번째 증상이은 경련을 말하는건가요? 아니면 id 27 (28번째) 기타를 말하는 건가요? PCA는 비독립적인 요소들을 배제하려고도 하는것이지만, 기타 다른 성능을 위해서도 차원축소를 시행합니다. Feature 가 n 수에 비해서 너무 많은경우에는 학습이 잘 안되거든요, 여튼 결국은 결과가 잘 나오는게 중요하겠지만 feature가 너무 많으면 overfitting 의 가능성이 커집니다. 도메인 지식에 기반해서 어느정도 차원으로 줄여야 할지 고민해봐도 되고, 2~25까지 다양하게 시도해보면서 제일 결과가 잘 나오는 걸 찾아볼 수도 있겠네요.

질환들을 어떻게 묶을지 고민이 많이 되는데, 우리가 physicial exam을 통해서 감별해야 하는 질환과 증상만으로 맞출 수 있는 질환기준으로 나누어 보면 어떨까 싶습니다. 중이염, 요로감염 등은 신체진찰이 있어야만 감별이 가능한데 인두염은 목을 보지 않아도 먹지 못하는증상이 주로 나타나지 않을까 하는 가설이 있네요. 이 부분은 다른분들이랑 같이 고민해보면 좋을것 같습니다.

그리고 코드는 가독성을 높이기 위헤 backqoute 뒤에 python 이라고 적으면 문법에 따라 highlight 색이 들어가요 ㅎㅎ 제가 맨 밑 코드만 수정했습니다.

root169 commented 7 years ago

증상이 총 27개가 있는데요,. 27번째 요소가 PCA 했을때 분산에 기여하는 비중이 가장 적은 요소를 의미합니다. 신체 진찰 없이 진단할 수 있는 것만 예측하고, 신체 진찰이 필요한 것은 "가능성이 있습니다" 정도로 제시해주는 식으로 서비스를 제공해야 할 것 같습니다. 여러 시도들로 정확도를 조금 더 높여보겠습니다.

root169 commented 7 years ago

오랜만의 업데이트입니다. 신재원 대표님께서 증상으로 질병을 예측하는 프로그램을 '프리미엄 서비스'에 넣기를 원하십니다. 어떠한 방식으로 서비스를 제공할 것인지 논의한 결과가 다음과 같습니다. 가능성이 가장 높은 3개의 질병을 제시하되, 그 3개 중 열감기와 중이염이 포함되어있으면 열감기와 중이염을 제시하는 질병 목록에서 제거하고 그 다음으로 가능성이 높은 질병을 제시하되, "증상만으로 진단이 어려운 열감기나 중이염일 수 있습니다" 식의 멘트를 띄우는 식입니다. 예를 들어 증상으로 질병을 예측한 결과 가장 가능성이 높은 순서대로 인후염, 기관지염, 중이염, 열감기, 후두염 순이면

  1. 인후염
  2. 기관지염
  3. 후두염 "증상만으로 진단이 어려운 열감기나 중이염일 수 있습니다" 라는 멘트가 나오는 것이죠.

결론적으로 최소 3개에서 최대 5개의 질병을 제시하는 셈이 될텐데, 너무 많은 것이 아닌가 하는 생각도 조금 듭니다.

위의 방식으로 예측했을 때 정확도는 97%가 나오긴 합니다만, 큰 의미가 없는 숫자일 수도 있겠습니다.