텐서플로우에서 범주형 데이터 다루기

2020. 4. 5. 16:05분석 Python/Data Preprocessing

 

광고 한 번씩 눌러주세요! 블로그 운영에 큰 힘이 됩니다 :)

보통 딥러닝 프레임워크로 모델을 만드는 경우, 데이터를 다 준비하고 넣는 경우가 가장 쉽다. 

그래서 범주형 데이터를 처리하게 되면, 보통 원핫 인코딩을 통해 sparse data를 만든다. 

이게 보통 적은 데이터에서는 전혀 문제가 되지 않지만, 만약 데이터가 커지면 커질수록 모델 분석보다, 전처리하는데 많은 시간이 소요하게 된다. 뿐만 아니라 sparse data를 만들게 되면, 그만큼 메모리도 많이 잡아먹기 때문에, 범주에 따른 메모리 변동량이 커지게 된다. 

  • data Preprocessing time
  • memory issue

memory : sparse data vs data 

그래서 필자는 이러한 부분에 대해서 해소해보고자 텐서플로우 그래프에서 처리하는 방법으로 해봤다. 
아무래도 텐서플로우에서 범주형 처리를 배치 단위로 진행하니, 크게 데이터 핸들링 시간이 모델 속도에 크게 영향을 주지 않으면서, 데이터도 바로 사용하니 메모리적인 부분에 대해서 해소가 될 것이라 생각했다.

사용하는 데이터 중에 범주형이 3개가 있다 (SEX, EDUCATION, MARRIAGE)

각 범주마다 있는 값은 보통은 string 문자값이 들어갈 것인데, 현재 데이터는 위의 그림 형태로 있으니 그대로 사용하겠다. 
일단 범주형 데이터의 결측치도 처리를 해야하기 때문에, 모두 string으로 바꾸고 시작한다.

이제 이것을 LabelEncoder를 활용해서 숫자형으로 맵핑하는 것을 하나 만들어주자.

위에 있는 코드 중에서 diz_map_train을 보면 위에 있던 값을 숫자형으로 맵핑해줄 수가 있다.
그래서 이것들을 다 모아주면 다음과 같이 된다.

이제 이렇게 맵핑된 값을 다시 변환은 pandas에 replace함수를 사용하면 가능하다.

train.replace(category_info)

이제 이것을 보면 nan이 3으로 변경된 것을 알 수 있다. 
그러면 여기서 하나 문제가 생길 수 있다. 바로 valid, test 에만 있는 범주는 어떻게 처리해야 할까?
어차피 새로운 데이터는 학습시킬 때 기존 train 분포에는 없는 것이므로, 특정한 값으로 전부 값을 바꿔준다.
현재 주어진 key값에다가 valid key값을 비교한다. 
비교하고 나서, 각 dict에 i : 0으로 바꿔준다.

실제 아래를 보면, 성별이라는 범주에서 4.0 ,5.0이 train에 없는데, 여기서 다 0으로 맵핑을 해준다. 
이런 식으로 기존 train에 없는 것은 한쪽으로 몰아줘서 처리한다. 

이제 이걸 tensorflow에서 처리하려면 index로 위치를 알아야 한다.
그래서 아래처럼 index를 찾아서 기존 이름을 index로 바꿔줍니다. 

그래서 데이터를 label encoding 방법으로 처리했을 때 다시 한번 사이즈를 비교해보자. 확실히 사이즈가 줄어든다!
만약 데이터가 범주가 많을 경우에는 이 효과가 더 많이 날 것이다.

이제 그러면 인덱스를 알았으니, tensorflow에서 slice 하기 위해서는 어디서 시작하고 몇 개를 자를지를 알야아햔다.
그래서 고민을 하다가 발견한 것이 np.split이었다!

total = np.arange(len(in_var))
fac_idx = list(cat_info.keys()) ## [1,2,3]
split = list(set(np.array(fac_idx)) | set(np.array(fac_idx ) + 1))
lists = np.split(total , split)
lists

그러면 여기서 보면 범주형 인덱스는 [1,2,3]이다. 그래서 0은 연속형 변수 1은 범주형 변수 2는 범주형 변수 3은 범주형 변수 4~22는 연속형 변수이다. 

그래서 이것을 찾기 위해서 다음과 같은 코드를 짰다.

index_store = []
for idx , i in enumerate(lists) :
    if len(i) == 1 :
        index_store.append([i[0],1])
    else :
        index_store.append([i[0],len(i) ] ) 
index_store

이것의 의미는 다음과 같다.

  • 0번째 인덱스에서 길이 1만큼 자르기 (연속형)
  • 1번째 인덱스에서 길이 1만큼 자르기 (범주형)
  • 2번째 인덱스에서 길이 1만큼 자르기 (범주형)
  • 3번째 인덱스에서 길이 1만큼 자르기 (범주형)
  • 4번째 인덱스부터 길이 19만큼 자르기 (연속형)

이런 식으로 하는 이유는 연속형 변수는 그대로 사용하고 범주형 변수에는 tf.one_hot을 써야 하기 때문이다. 

위의 코드를 그리면 다음과 같다.

concatenated_layer = tf.concat(inputs, axis=1, name='concatenate')

그다음에 합쳐주면 범주형 데이터를 처리한 데이터를 얻을 수 있게 된다!
그래서 전체적인 프로세스는 다음과 같다. 

logit = Layer(concatenated_layer)
prob= tf.nn.sigmoid(logit)
auc_value , update_auc = tf.metrics.auc(label_y , prob , curve="ROC")
learning_rate = tf.train.cosine_decay_restarts(1e-4, global_step,
                                               first_decay_steps=100, t_mul=1.5,m_mul=0.9, alpha=0.0)
loss2 = tf.reduce_mean(tf.nn.weighted_cross_entropy_with_logits(labels = label_y ,
                                                                logits=logit,pos_weight=1.5))
update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
with tf.control_dependencies(update_ops):
    solver2 = tf.train.AdamOptimizer(learning_rate= learning_rate).minimize(loss2 ,var_list = totalvars)

다음에는 기존에 하던 것처럼 손실 함수를 정의해서 사용하면 된다.
현재는 데이터를 읽은 다음에 처리하지만, 이 과정도 오래 걸릴 수 있으므로 좀 더 발전하는 과정에 대해서 생각해본다면 다음과 같다.
tf.data에서 배치 단위로 읽으면서, tf.py_func을 사용해서 python 연산자를 활용할 수 있게 해서 
그 배치 데이터에 대해서, 미리 데이터에 대해서 학습한 전처리 알고리즘을 사용하여, 배치 단위로 전처리를 하여 데이터를 Tensorflow Graph에 태워서 학습시키는 방식도 생각하고 있다. 
이러한 경우는 일단 전처리 알고리즘을 학습하기 위해서 데이터 관련해서 모두 읽어서 해야 하는 문제는 있지만, 
그 이후에 읽고 나서 만든 알고리즘을 바탕으로 그 후에 학습하거나 추론할 때는 더 빠르게 할 수 있지 않을까 생각한다.

 

728x90