본문 바로가기
AI/자연어처리

[생활속의 IT] 자연어 처리#7 - 직방 부동산 평가데이터 전처리(2/2)

by 생활속의 IT램프 2020. 3. 28.

[이전 글 보기]

2020/03/22 - [AI/자연어처리] - [생활속의 IT] 자연어 처리#1 - 아나콘다 설치하기

2020/03/22 - [AI/자연어처리] - [생활속의 IT] 자연어 처리 - 참고) Jupyter의 개념

2020/03/23 - [AI/자연어처리] - [생활속의 IT] 자연어 처리#2 - 크롤러 만들기

2020/03/23 - [AI/자연어처리] - [생활속의 IT] 자연어 처리#3 - 직방의 지리정보 Geohash 이해하기

2020/03/24 - [AI/자연어처리] - [생활속의 IT] 자연어 처리#4 - 직방 아파트ID 얻기

2020/03/24 - [AI/자연어처리] - [생활속의 IT] 자연어 처리#5 - 직방 부동산 평가 크롤링하기

2020/03/26 - [AI/자연어처리] - [생활속의 IT] 자연어 처리#6 - 직방 부동산 평가데이터 전처리(1/2)

 

 

 

전처리를 위한 설계까지 완성했습니다.

원래 전처리가 복잡하고 학습 시키는 것 자체는 코드는 몇 줄 되지 않습니다. 

(물론 깊게 들어가면 얘기는 달라집니다만)

 

이제 필요한 패키지를 설치하기 각 함수에 대한 상세 구현에 들어가도록 하겠습니다.

참고로 본 과정은 네이버 영화평점 분석하기(https://wikidocs.net/44249) 과정을 참조하였습니다.

 

 

1. 필요한 패키지 설치

 

2. 함수 상세 구현

 

3. 메인 구현

 

-----------------------------------------------------------------------------------------------------------------------------------

 

1. 필요한 패키지 설치

(1) Pandas

 

데이터 전처리는 상당한 노가다를 포함하지만

다행히도 파이썬에서는 pandas 라는 패키지에서 이러한 노가다를 편리하게 해주는 기법들을 많이 제공해줍니다.

pandas란 액셀처럼 행/열로 구성된 데이터(쉽게 데이터 프레임이라고 합니다)를

가지고 이리저리 가지고 놀 수 있는 수많은 라이브러리들을 제공해줍니다.

 

pandas로 무슨 일을 할 수 있는지는 아래 사이트를 참조해보면 좋을 듯 합니다.

https://dandyrilla.github.io/2017-08-12/pandas-10min/

 

위 사이트를 참고해보시면 알겠지만 갖가지 목적을 위한 수 많은 라이브러리들을 제공해주고 있어서

일일이 공부하겠다 생각하는것보단

본인이 원하는 행위를 하려면 어떻게 해야하는지 검색 위주로 보는게 좋다고 생각합니다.

 

그럼 이제 본격적인 전처리 작업을 진행해보도록 하겠습니다. 

pandas는 아래와 같이 파이참에서 설치할 수 있습니다. 

판다스 설치가 끝나면  텐서플로우와 케라스를 설치해보겠습니다.

 

 

(2) Tensorflow + Keras

 

텐서플로우와 Keras는 파이참에서 간편히 검색하여 설치할 수 있습니다.

Tensorflow를 먼저 설치하고 keras를 설치하도록 합니다.

잠깐 텐서플로우가 뭔지 알아볼까요? 많이 들어는 봤지만 뭐에 쓰는건지, 먹는건지 궁금하신 분들이 많습니다.

 

텐서플로우란?

 

텐서플로우는 구글에서 개발한 인공지능 개발 프레임워크입니다.

즉 전통적인 선형 회귀분석이나 요즘 핫하다는 딥러닝을 이용한 어플리케이션을

개발할 수 있도록 모아놓은 함수들과 라이브러리 모음집 이라고 보면 됩니다.

 

원래 인공지능 분야는 그 역사도 깊거니와 통계와 수학으로 점철된 학문입니다.

그만큼 알고리즘 하나에도 굉장히 튜닝 요소가 많은데 

텐서플로우는 인공지능을 공부한 사람이 여러 가지 실험하고 그 과정을 추적하며 시각화하기 좋게

만들어져 있습니다. 

 

참고로 텐서플로우는 원래 그 자체는 C++로 개발되었으나

파이썬의 강력함과 편리함으로 인해 파이썬 라이브러리로 가장 많이 지원하고 자료도 많습니다.

그래서 우리는 파이썬에서 텐서플로우 패키지를 설치하고 import 해서 편리하게 쓸 수 있는 것이죠.

 

그럼 Keras는 뭘까요?

 

 

Keras란?

 

텐서플로우가 있는데 Keras가 왜 필요할까요?

쉽게 말하면 텐서플로우는 약간 전문가 용입니다. 인공지능을 공부하지 않은 사람이 쓰기에는 좀 어렵죠.

여러분이 직인공지능을 전공하고 알고리즘을 공부해서 이를 코딩해서 쓰려면 할 수 있을까요?

거의 불가능하다고 봐야합니다.

 

그래서 Keras가 등장했습니다. 

Keras는 텐서플로우와 별도로 존재하는게 아니라 텐서플로우를 기반으로 동작합니다.

즉 사용하기 어려운 텐서플로우의 세부 알고리즘은 저 밑으로 숨겨두고

사용자는 필요한 파라미터 몇 개 던져주고 엔터치면 동작하도록 만들어 졌습니다.

 

물론 더 잘 쓰려면 통계나 해당 알고리즘에 대한 지식은 있어야 겠지만

일반인도 쓸 수 있을 정도라는 것이죠. 

 

참고로 Keras는 텐서플로우 뿐만 아니라 다른 프레임워크 위에서도 동작 가능합니다.

전문가라면 자유도가 높은 텐서플로우를 사용할 수 있지만 일반인은 Keras로도 대부분 할 수 있다고 볼 수 있습니다.

 

그리고 텐서플로우 2.0 버전 이후부터는 Keras를 기본적으로 포함하고 있습니다.

그래서 파이참에서 tensorflow 설치해보면

Keras-base 와 Keras-preprocessing  패키지가 같이 깔린 걸 확인할 수 있습니다. 

 

 

 

(3) konlpy

konlpy 패키지는 위에 언급하였던 형태소 분석을 통해 한국어에 대한 자연어 처리를 위한

라이브러리를 제공해주는 패키지라 할 수 있습니다.

설치는 파이참에서 설치가 안되기 때문에 prompt에서 설치하도록 합니다.

아래와 같이 realstate 가상환경에 들어가서 인스톨합니다.

 

 

그리고 konlpy 패키지 중 형태소 분석을 위해 Okt 라는 함수를 쓰는데 이 함수는 Java 위에서 동작하는 함수입니다.

그래서 형태소 분석을 위해선 자바도 설치해야 합니다.

 

그런데 Okt라는 함수는 호출은 파이썬에서 하고, 실제 동작은 자바 위에서 하다보니

파이썬과 자바간의 연결을 위한 패키지가 필요합니다.

이 패키지가 jpype1 이라는 패키지입니다.

 

pip install konlpy 를 통해 konlpy 패키지를 설치하였다면 jpype1도 잘 설치가 되었을 겁니다.

이제 남은 건 자바 설치입니다.

 

 

(4) 자바 설치

자바는 https://www.java.com/ko/download/manual.jsp 에서 다운로드 받도록 합니다.

여기서 본인의 PC가 32비트인지, 64비트인지 확인이 중요한데, 최근엔 64비트가 많으므로 

아래 64비트 다운로드 받는 화면을 캡쳐해 넣었습니다.

 

 

PC 비트 수 따라 알아서 설치되지 않습니다.

잘 확인해보시고 32 or 64 비트 맞게 다운로드 받아야 합니다.

 

 

참고로 위에 언급한 설명을 다 하고서도 okt 함수를 실행시킬 때 아래와 같이 오류가 나는 경우가 있는데

 

 

자바 홈경로가 잘 안 잡혀있는 경우이므로 자바 홈 설정을 해어야 합니다.

자바 홈 설정 방법은 다른 사이트에 잘 설명되어 있으므로 혹시 안되면 검색해서 조치해보세요.

 

여기까지 다 됐으면 패키지 설치는 모두 완료됐습니다.

이제 본격 전처리 작업을 시작해보겠습니다.

 

 

2. 함수 상세 구현

(1) getConcatData, getRawData 구현

 

당장 해야할 것은 수집했던 raw 데이터에 대한 중복을 제거하는 것입니다. 

직방 부동산 평가글에는 중복이 없을것 같지만 1개 혹은 2개 정도 있습니다.

아마도 복붙하는 사람들이 있는 모양입니다. 

 

getConcatData에 던져질 원본 데이터는 아래와 같이 작성되어 있는 상태입니다.

 

위 형식을 참고하여 함수는 아래와 같이 작성했습니다. 

 

def getConcatData(inputList):
    returnDF = pd.DataFrame()

    for i in inputList:
        name = i + ".csv"
        tmp_data = pd.read_csv(name)
        tmp_data = getRawData(tmp_data)
        returnDF = pd.concat([returnDF, tmp_data])

    returnDF.reset_index(drop=True, inplace=True)
    return returnDF

######################################################################################
###  점수와 설명을 제외한 모든 컬럼은 제거, 5종류의 점수와 설명을 score와 desc로 변경 후 통합
### 1~2 점은 0으로, 3점은 제거, 4~5점은 1로 변환

def getRawData(inputDF):

    # 중복 행 삭제
    cnt = len(inputDF)
    dupCnt=inputDF['totalDesc'].nunique()
    inputDF.drop_duplicates(subset=['totalDesc'], inplace=True)
    print( str(cnt - dupCnt) + " of duplicated item has been deleted.")

    # 필요없는 컬럼은 삭제
    inputDF.drop(["danji_id", "danji_name", "age", "sex", "residenceType", "married"], axis='columns', inplace=True)
    resultDF = pd.DataFrame()

    # 교통평가(3,4열) / 주변평가(5,6열) / 관리평가(7,8열), 주거평가(9,10열) 을 concat 하여 합침
    for i in range(0, 10, 2):
           inputDF.columns.values[i] = "score"
           inputDF.columns.values[i + 1] = "desc"
           resultDF = pd.concat([resultDF, inputDF.iloc[:, i:i + 2]])

    # 인덱스 재생성
    resultDF.reset_index(drop=True, inplace=True)

    # 긍정/부정 분류 (1~2 점은 부정, 3점은 제거, 4~5점은 긍정으로 분류)

    idx_nm = resultDF[resultDF['score'] == 3].index
    resultDF = resultDF.drop(idx_nm)
    resultDF["score"] = resultDF["score"].apply(lambda x: 1 if x >= 4 else 0)

    # resultDF.to_csv("resultDF.csv", mode='w')
    return resultDF

 

 

체크포인트는 아래와 같습니다.

 

○ 단지이름이나 성별, 주거형태 컬럼 등은 자연어 처리에 필요없으므로 삭제했습니다.

○ totalScore, totalDesc, trafficScore, trafficDesc 등 컬럼이름은 일괄적으로 score, desc로 변경하였고

    각 평점 데이터는 row수준 concat하여 이어붙였습니다.

 

○ 실행을 위한 메인은 일단 확인용도로만 간단히 작성했습니다.

 

 

(3) getTokenedData 구현

 

토큰화란 문장의 형태소 분석을 통해 형태소에 따라 분할시키는 것을 의미합니다.

그런데 형태소가 뭘까요?

 

형태소: 의미를 가지는 요소로서는 더 이상 분석할 수 없는 가장 작은 말의 단위

형태소란 "사랑" 같은 단어나 "을" "하다" 와 같이 의미를 가지면서 최소단위를 의미합니다.

(이전 포스트에서 설명했습니다)

 

한국어의 경우 원 상태에서 문장을 띄어쓰기 기반으로 나누게 되면

동일 의미임에도 학습할 단어가 너무 많아지게 됩니다.

 

그래서 한글에 대한 전처리가 어려운 편이지요. 

해결 방법은 저런 다양한 표현을 원형으로 바꾸어주는 것입니다. 

그래야 학습해야 할 단어 대상이 줄어들기 때문에 형태소 분석을 하는 겁니다. 

 

함수는 아래와 같이 구현하였습니다. 

 

def getTokenedData(input_data):

    input_data['desc'] = input_data['desc'].str.replace("[^ㄱ-ㅎㅏ-ㅣ가-힣 ]", "")
    stopwordList = getStopwords()

    splited_input=[]
    okt = Okt()
    for i in input_data['desc']:
        temp_X =[]
        temp_X = okt.morphs(i, stem=True)
        temp_X = [j for j in temp_X if j not in stopwordList ]

        splited_input.append(temp_X)

    return splited_input

 

 

def getStopwords():
    file = open('stopwords.csv','r',encoding='utf8')
    csvfile = csv.reader(file)
    returnList = [i[0] for i in csvfile]

    return returnList[1:]

 

 

○ 입력된 데이터에서 한글을 제외한 숫자나 영어, ~, !, ^ 등은 모두 삭제합니다.

○ Okt는 Open Korean Text라는 Keras에서 제공해주는 형태소 분석기입니다.

    참고로 Keras에는 형태소 분석해주는 라이브러리들을 많이 제공해줍니다.

    주로 사용하는건 Komoran(코모란), Hannanum(한나눔), Okt, Mecab, kkma 정도입니다. (5걔)

 

    어떤 라이브러리를 쓰냐에 따라 split 하는게 조금씩 다릅니다.

    가령 어떤건 "에는"을 하나로 보지만 어떤 것은 에/는 으로 나누기도 합니다.

 

○ Okt에는 여러 함수를 제공하는데 morphs는 형태소를 기준으로 나눠라 라는 함수이고

    stem 옵션은 어간을 추출하라는 뜻입니다. "예쁘니" 에서 "예쁘"만 추출한다는 의미입니다.

 

○ 불용어 사전인 stopwords 는 .csv 파일로 별도로 저장해놓은 것을 읽어오도록 했습니다.

    제가 만든 불용어 사전은 첨부파일에 올려놓도록 하겠습니다.

 

(4) getTokenizer 구현

 

def getTokenizer(splited_train_data):

    # 희귀 단어 제외하여 훈련시킬 단어 개수 정하기
    tokenizer = Tokenizer()
    tokenizer.fit_on_texts(splited_train_data)

    rareCnt = 0
    rareFreq = 0
    totalCnt = len(tokenizer.word_counts)
    totalFreq = 0
    for k, v in tokenizer.word_counts.items():
        totalFreq += v
        if v < 3:
            rareCnt += 1
            rareFreq += v

    print("Whole words count:" + str(totalCnt))
    print("Words appear under two times or less:" + str(rareCnt))
    print("Appearance rate of rare words:" + str(rareCnt / totalCnt * 100))
    print("Appearance portion of rare words:" + str(rareFreq / totalFreq * 100))

    # 훈련을 위한 토큰화 수행
    vocabSize = totalCnt - rareCnt + 1
    print("Words for train:" + str(vocabSize))

    tokenizer = Tokenizer(vocabSize)
    tokenizer.fit_on_texts(splited_train_data)
    with open('tokenizer.pickle', 'wb') as handle:
        pickle.dump(tokenizer, handle, protocol=pickle.HIGHEST_PROTOCOL)

    return tokenizer, vocabSize

 

 

○ tokenizer.fit_on_texts를 진행하면 split 된 문장을 입력받아 split item 별 등장 순위를 매깁니다. 

    가령 아파트가 가장 많이 나왔으면 아파트:1  / 그 다음 "역"이 많이 나왔으면 역:2 

    처럼 순위를 매기는 것이죠.

 

○ 전체적으로 보면 tokenizer 생성을 두 번 하고 있습니다.

    첫 번째 생성은 저빈도 단어(2회 이하 등장)을 탐색하기 위해 진행합니다.

    tokenizer.word_count_items 함수를 통해 단어:순위로 된 key:value 쌍을 뽑아낼 수 있으며

    이를 이용하여 value가 3 미만인 단어 개수를 체크합니다.

 

두 번째 생성은 저빈도 단어를 제외하고 토큰화하기 위해 진행합니다. 

   tokenizer = Tokenizer(vocabSize) 로 vocabSize를 전달하면 그 숫자 만큼만 순위를 매기고

   그 순위를 넘어간 단어는 버리도록 되어 있습니다.

   가령 vocabSize가 1000이면 등장횟수 순위 1000위 까지만 단어:순위 쌍으로 관리하고

   1001위 부터는 버린다는 말입니다.

 

○ tokenizer는 모델로 예측할 때도 이용됩니다. 그래서 만들어진 tokenizer는 파일로 저장해두도록 합니다. 

 

 

 

(5) getEncoding 구현

 

이제 전처리의 마지막 부분인 문장을 벡터화 시키는 부분입니다.

 

def getEncoding(tokenizer, splited_train_data, splited_test_data, y_train, y_test):

    encoded_train_data = tokenizer.texts_to_sequences(splited_train_data)
    encoded_test_data = tokenizer.texts_to_sequences(splited_test_data)

    drop_train = [i for i, v in enumerate(encoded_train_data) if len(v) == 0]
    drop_test = [i for i, v in enumerate(encoded_test_data) if len(v) == 0]

    encoded_train_data = np.delete(encoded_train_data, drop_train, axis=0)
    encoded_test_data = np.delete(encoded_test_data, drop_test, axis=0)

    '''
    print("\n")
    print("리뷰 최대길이: " , max(len(i) for i in encoded_train_data))
    print("리뷰 평균길이: " , sum(map(len, encoded_train_data))/len(encoded_train_data))
    cnt=sum([1 for i in encoded_train_data if len(i)<= 50])
    print("길이 50 이하의 비율: ", cnt/len(encoded_train_data)*100)
    
    plt.hist([len(s) for s in encoded_train_data], bins=50)
    plt.xlabel('length of samples')
    plt.ylabel('number of samples')
    plt.show()
    '''
    
    encoded_train_data = pad_sequences(encoded_train_data, maxlen = 50)
    encoded_test_data = pad_sequences(encoded_test_data, maxlen = 50)

    Y_train = np.delete(y_train, drop_train, axis=0)
    Y_test = np.delete(y_test, drop_test, axis=0)
    print(str(len(drop_train)) + " of null review train data has been removed")
    print(str(len(drop_test)) + " of null review test data has been removed")

    return encoded_train_data, encoded_test_data, Y_train, Y_test

 

 

○ tokenizer.texts_to_sequences 는 전 단계에서 만들었던 tokenizer를 이용하게 되는데

    전 단계에서 만든 tokenizer는 vocabSize를 인수로 받아 생성되었으므로 가령 1000을 받았다면

    단어 빈도순위 1000위 까지만 벡터화를 진행합니다. 

    빈도순위 1001위 이상의 단어는 그냥 없애버리게 됩니다. 

 

○ 특정 빈도 순위 이상의 단어는 날라감에 따라 저빈도 단어로만 이루어진 문장이 있다면

    해당 문장은 null 값만 남게 됩니다. 정확히는 빈 리스트만 남게 되죠.

    이런 문장은 삭제해줘야 하므로 drop_train, drop_test가 나오게 됩니다.

 

○ 문장마다 단어 개수가 다르므로 벡터 길이가 다릅니다.

    예를 들어 어떤 문장은 "바퀴벌레가 많아요" --> "바퀴벌레/많아" -->  2차원이 될 것이며

    어떤 문장은 "역이 가깝고 버스종류도 많아서 완전 추천합니다" --> "역/가깝/버스/종류/많아/완전/추천"

    -- 7차원 정도가 되어 서로 차원이 다르게 됩니다.

 

   학습을 위해선 차원을 서로 맞춰야 진행이 됩니다.

   이를 위해 패딩을 진행하며 패딩이란 벡터 길이를 맞추기 위해 default 문자열을 넣는 것입니다.

   기본적으로 0을 입력합니다.

   maxlen=50은 차원을 50차원으로 맞추겠다는 뜻이며 따라서 "바퀴벌레/많아" 는 48 차원이 덧붙여지게 됩니다.

   (48번의 0이 append 됨)

 

   만약 50차원이 넘어가는 긴 문장은 50차원에 맞추어 끝을 자르게 됩니다. 

   리뷰 데이터들이 대략 몇 차원으로 이루어졌는지 보기 위해 히스토그램을 보면 좋습니다.

   아래는 주석을 제거하고 plot을 그려본 그래프입니다. 98%가 50차원 미만이라고 뜹니다. 

   그래서 저는 padding 길이를 50으로 맞췄습니다. 

 

 

○ encoded 데이터는 영화 평점 문장에 대한 벡터화된 리스트만 담기게 되며

    Y_train, Y_test는 각 문장에 대한 긍정/부정 정보를 1과 0으로 담고 있습니다.

 

이제 함수 작성은 끝났고 메인 부분을 보도록 하겠습니다.

 

 

3. 메인 구현

 

# 0. 학습할 데이터 선정, 테스트를 위해 우선 소수 geohash만 입력하여 수행
train_geohash  = ['wydq0','wydq4','wydq5']
test_geohash = ['wydjn']

# 1. Raw 데이터 생성
train_data = getConcatData(train_geohash)
test_data = getConcatData(test_geohash)
y_train = np.array(train_data['score'])
y_test = np.array(test_data['score'])

# 2. 불용어 적용 및 Token처리
splited_train_data = getTokenedData(train_data)
splited_test_data= getTokenedData(test_data)

# 3. Tokenizer 생성 (만약 저장된 파일이 있으면 이 파일을 읽음, 새로 만드려면 지울 것)
tokenizer = Tokenizer()

if (os.path.exists("tokenizer.pickle")):
    with open('tokenizer.pickle', 'rb') as handle:
        tokenizer = pickle.load(handle)
else:
    tokenizer = getTokenizer(splited_train_data)

# 4. 벡터화된 데이터 생성
X_train, X_test, Y_train, Y_test = getEncoding(tokenizer, splited_train_data, splited_test_data, y_train, y_test)

print(X_train[:3])

print(X_test[:3])

print(Y_train[:3])

print(Y_test[:3])

 

벡터화가 완료된 데이터는 0~2줄 까지 출력하여 잘 만들어졌는지 확인해봅니다.

잘 나오면 이제 자연어 처리 모델을 만들고 돌려보도록 하겠습니다. 

댓글