이번 프로젝트는 아파트 실거래가를 예측하는 것이다.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import timeit
import sklearn
import warnings
warnings.filterwarnings('ignore')
import sys

train_data=pd.read_csv('d:/data/real_estate/train.csv')
test_data=pd.read_csv('d:/data/real_estate/test.csv')

print('train.csv. Shape: ',train_data.shape)
print('test.csv. Shape: ', test_data.shape)

train.csv. Shape:  (1216553, 13)
test.csv. Shape:  (5463, 12)

훈련데이터는 총 120만개정도이고 , 테스트 데이터는 5400여개가 존재한다.

각 데이터의 정보를 간략하게 살펴보자.

 

train_data.info()

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1216553 entries, 0 to 1216552
Data columns (total 13 columns):
 #   Column                  Non-Null Count    Dtype  
---  ------                  --------------    -----  
 0   transaction_id          1216553 non-null  int64  
 1   apartment_id            1216553 non-null  int64  
 2   city                    1216553 non-null  object 
 3   dong                    1216553 non-null  object 
 4   jibun                   1216553 non-null  object 
 5   apt                     1216553 non-null  object 
 6   addr_kr                 1216553 non-null  object 
 7   exclusive_use_area      1216553 non-null  float64
 8   year_of_completion      1216553 non-null  int64  
 9   transaction_year_month  1216553 non-null  int64  
 10  transaction_date        1216553 non-null  object 
 11  floor                   1216553 non-null  int64  
 12  transaction_real_price  1216553 non-null  int64  
dtypes: float64(1), int64(6), object(6)
memory usage: 120.7+ MB
test_data.info()

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 5463 entries, 0 to 5462
Data columns (total 12 columns):
 #   Column                  Non-Null Count  Dtype  
---  ------                  --------------  -----  
 0   transaction_id          5463 non-null   int64  
 1   apartment_id            5463 non-null   int64  
 2   city                    5463 non-null   object 
 3   dong                    5463 non-null   object 
 4   jibun                   5463 non-null   object 
 5   apt                     5463 non-null   object 
 6   addr_kr                 5463 non-null   object 
 7   exclusive_use_area      5463 non-null   float64
 8   year_of_completion      5463 non-null   int64  
 9   transaction_year_month  5463 non-null   int64  
 10  transaction_date        5463 non-null   object 
 11  floor                   5463 non-null   int64  
dtypes: float64(1), int64(5), object(6)
memory usage: 512.3+ KB

훈련데이터와 테스트데이터의 레이블을 살펴보면 훈련데이터에만 transaction_real_price가 있는것을 확인할 수 있다. 이는 결국 훈련 데이터를 통해 가격을 예측해보기 위한점이다.

 

[Train Column name Description]

  • transaction_id : 아파트 거래에 대한 유니크한 아이디
  • apartment_id : 아파트 아이디
  • city : 도시
  • dong : 동
  • jibun : 지번
  • apt : 아파트단지 이름
  • addr_kr : 주소
  • exclusive_use_area : 전용면적
  • year_of_completion : 설립일자
  • transaction_year_month : 거래년월
  • transaction_date : 거래날짜
  • floor : 층
  • transaction_real_price : 실거래가 (target variable)

이제 결측값이 있는지 확인해보자. 

train_null=train_data.drop('transaction_real_price',axis=1).isnull().sum()/len(train_data)*100
test_null=test_data.isnull().sum()/len(test_data)*100
pd.DataFrame({'train_null_count': train_null,'test_null_count':test_null})

결측치 확인

딱히, 결측치는 존재하지 않는다.

 

간단하게 훈련 데이터를 살펴보자.

train_data.head()


transaction_id	apartment_id	city	dong	jibun	apt	addr_kr	exclusive_use_area	year_of_completion	transaction_year_month	transaction_date	floor	transaction_real_price
0	0	7622	서울특별시	신교동	6-13	신현(101동)	신교동 6-13 신현(101동)	84.82	2002	200801	21~31	2	37500
1	1	5399	서울특별시	필운동	142	사직파크맨션	필운동 142 사직파크맨션	99.17	1973	200801	1~10	6	20000
2	2	3578	서울특별시	필운동	174-1	두레엘리시안	필운동 174-1 두레엘리시안	84.74	2007	200801	1~10	6	38500
3	3	10957	서울특별시	내수동	95	파크팰리스	내수동 95 파크팰리스	146.39	2003	200801	11~20	15	118000
4	4	10639	서울특별시	내수동	110-15	킹스매너	내수동 110-15 킹스매너	194.43	2004	200801	21~31	3	120000

탐색적 데이터 분석 및 처리

훈련데이터 ['transaction_real_price']레이블의 통계적 요약을 살펴보면 다음과 같다.

train_data['transaction_real_price'].describe()

count    1.216553e+06
mean     3.822769e+04
std      3.104898e+04
min      1.000000e+02
25%      1.900000e+04
50%      3.090000e+04
75%      4.700000e+04
max      8.200000e+05
Name: transaction_real_price, dtype: float64

평균 38827 Min: 100 MAX: 820000 표준편차:31048

 

이를 그래프로 막대그래프와 왜도 첨도를 통해 그래프를 살펴보자. 

왜도(Skewness): 왼쪽으로 치우쳐져 있을수록 값이 크다.

첨도(Kurtosis): 첨도 값이 3에 가까울 경우 정규분포에 근사한다. 첨도 값이 클수록 뾰족하고 값이 작을수록 완만해진다.

f,ax=plt.subplots(figsize=(8,6))
sns.distplot(train_data['transaction_real_price'])
print("%s->Skewness: %f, Kurtosis: %f" % ('transaction_real_price',train_data['transaction_real_price'].skew(),train_data['transaction_real_price'].kurt()))

transaction_real_price->Skewness: 3.407169, Kurtosis: 24.839821

일반 분포

이 그래프를 정규화에 근사시키기 위해 log1p 함수를 이용해 로그를씌워 다시 한번확인해보자.

train_data['transaction_real_price'] = np.log1p(train_data['transaction_real_price'])
print("%s -> Skewness: %f, Kurtosis: %f" %  ('transaction_real_price',train_data['transaction_real_price'].skew(),train_data['transaction_real_price'].kurt()))

transaction_real_price -> Skewness: -0.094932, Kurtosis: 0.248866

f,ax=plt.subplots(figsize=(8,6))
sns.distplot(train_data['transaction_real_price'])

log를 씌운 그래프 분포

칼럼들의 상관관계

각 칼럼들간의 상관관계를 확인해보자. 

k=train_data.shape[1]
corrmat=train_data.corr()
cols=corrmat.nlargest(k,'transaction_real_price')['transaction_real_price'].index
cm=np.corrcoef(train_data[cols].values.T)
f,ax=plt.subplots(figsize=(8,6))
sns.heatmap(data=cm,annot=True,square=True,fmt='.2f',linewidths=5,cmap='Reds',yticklabels=cols.values,xticklabels=cols.values)

각 칼럼들간의 상관관계

훈련데이터의 'apartment_id' 칼럼의 unique_value의 갯수를 확인해보자.

 

len(train_data['apartment_id'].unique())

12533

 

이후 'transaction_id'를 따로 변수로 지정한 후, 본래의 데이터프레임에서는 없애주자.

train_id=train_data['transaction_id']
train=train_data.drop('transaction_id',axis=1)
test_id=test_data['transaction_id']
test=test_data.drop('transaction_id',axis=1)

Floor(층)

가설) 층수가 높아질수록 집값도 높아진다.

 

'floor' 과 'transaction_real_price'의 연관성을 확인하기 위해 산점도표에 케스팅해보자. 

 

 

f,ax=plt.subplots(figsize=(8,6))
plt.scatter(train['floor'],train['transaction_real_price'])
plt.xlabel('floor')
plt.ylabel('transaction_real_price')
plt.show()

집값과 층수의 연관성

위 그래프를 확인해보면 층수가 높아질수록 집값도 높아진다라는 본인의 생각은 틀린것을 알 수 있다.

 

그렇다면,

floor(층수)칼럼에 대해서 막대그래프를 통한 분포표도 확인해보자.

 

f,ax=plt.subplots(figsize=(8,6))
sns.distplot(train['floor'])
print('%s->Skewness: %f, Kurtosis: %f'% ('floor',train['floor'].skew(),train['floor'].kurt()))

floor->Skewness: 1.324710, Kurtosis: 3.796603

exclusive_use_area(전용면적)

가설) 전용면적이 넓어지면 넓어질수록 집값은 비싸다

우선 훈련데이터의 'exclusive_use_area'를 간단하게 통계적 요약으로 살펴보자.

train['exclusive_use_area'].describe()

count    1.216553e+06
mean     7.816549e+01
std      2.915113e+01
min      9.260000e+00
25%      5.976000e+01
50%      8.241000e+01
75%      8.497000e+01
max      4.243200e+02
Name: exclusive_use_area, dtype: float64

Mean: 78.1 Min: 9.2 Max: 424.3

이를 산점도표에 케스팅하여 전용면적과 집값의 연관성을 확인해보자.

 

f,ax=plt.subplots(figsize=(8,6))
plt.scatter(train['exclusive_use_area'],train['transaction_real_price'])
plt.xlabel('exclusive_use_area')
plt.ylabel('transaction_real_price')
plt.show()

집값과 전용면적의 연관성

대략 위 산점도표를 살펴보면, 전용면적이 넓어질수록 집값도 올라가는 경향을 보인다. 그러므로 가설은 맞다고 가정하자.

 

이상치들을 확인해보자.

 

train[train['exclusive_use_area']>400]

	apartment_id	city	dong	jibun	apt	addr_kr	exclusive_use_area	year_of_completion	transaction_year_month	transaction_date	floor	transaction_real_price
563870	12633	서울특별시	도곡동	193-1	힐데스하임빌라	도곡동 193-1 힐데스하임빌라	424.32	1998	201604	11~20	10	498000
train[(train['exclusive_use_area']<150)&(train['transaction_real_price']<200)]

	apartment_id	city	dong	jibun	apt	addr_kr	exclusive_use_area	year_of_completion	transaction_year_month	transaction_date	floor	transaction_real_price
722888	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	138.25	1974	201606	21~30	-1	100

크게 이상치라고 판단할수 있는 건 없는것 같다.

이를 한번더 막대 그래프로 살펴보자.

 

f,ax=plt.subplots(figsize=(8,6))
sns.distplot(train['exclusive_use_area'])
print("%s->Skewness: %f, Kurtosis= %f"% ('exclusive_use_area',train['exclusive_use_area'].skew(),train['exclusive_use_area'].kurt()))

transaction_year_month(거래 년,월)

훈련데이터와 테스트데이터에 있는 년월 칼럼을 따로 뽑아와 새로운 칼럼으로 만들어보자.

train_test_data=[train,test]
for dataset in train_test_data:
    dataset['transaction_year_month']=dataset['transaction_year_month'].astype(str)
    dataset['year']=dataset['transaction_year_month'].str[:4].astype(int)
    dataset['month']=dataset['transaction_year_month'].str[4:6].astype(int)
    dataset['transaction_year_month']=dataset['transaction_year_month'].astype(int)

년도 별로 그래프를 확인해보자.

f,ax=plt.subplots(figsize=(8,6))
sns.boxplot(train['year'],train['transaction_real_price'])
plt.show()

보면 2016년도에 이상치같이 보이는 것이 하나 생겨있다. 이를 좀더 확인해보자.

train[(train['year']==2016)&(train['transaction_real_price']<150)]

apartment_id	city	dong	jibun	apt	addr_kr	exclusive_use_area	year_of_completion	transaction_year_month	transaction_date	floor	transaction_real_price	year	month
722888	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	138.25	1974	201606	21~30	-1	100	2016	6

서면 아파트가 값이 낮다. 

그렇다면 서면 전체에 대해서 한번 확인해보자.

 

train[train['apt']=='서면']
	apartment_id	city	dong	jibun	apt	addr_kr	exclusive_use_area	year_of_completion	transaction_year_month	transaction_date	floor	transaction_real_price	year	month
695725	6238	부산광역시	전포동	산99-46	서면	전포동 산99-46 서면	57.59	1976	201105	21~31	3	5900	2011	5
696834	6238	부산광역시	전포동	산99-46	서면	전포동 산99-46 서면	57.59	1976	201108	21~31	1	7500	2011	8
700884	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	88.93	1974	201206	1~10	1	25000	2012	6
702589	6238	부산광역시	전포동	산99-46	서면	전포동 산99-46 서면	57.59	1976	201212	21~31	5	6000	2012	12
702876	6238	부산광역시	전포동	산99-46	서면	전포동 산99-46 서면	57.59	1976	201301	11~20	4	7400	2013	1
703979	6238	부산광역시	전포동	산99-46	서면	전포동 산99-46 서면	57.59	1976	201304	1~10	1	7000	2013	4
711024	6238	부산광역시	전포동	산99-46	서면	전포동 산99-46 서면	57.59	1976	201407	21~31	5	7000	2014	7
712441	6238	부산광역시	전포동	산99-46	서면	전포동 산99-46 서면	57.59	1976	201410	21~31	3	8800	2014	10
713864	6238	부산광역시	전포동	산99-46	서면	전포동 산99-46 서면	57.59	1976	201501	1~10	4	7750	2015	1
721684	6238	부산광역시	전포동	산99-46	서면	전포동 산99-46 서면	57.59	1976	201604	1~10	1	8600	2016	4
722888	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	138.25	1974	201606	21~30	-1	100	2016	6
723957	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	55.04	1974	201608	21~31	2	23200	2016	8
726597	6238	부산광역시	전포동	산99-46	서면	전포동 산99-46 서면	57.59	1976	201612	11~20	4	6650	2016	12
1186983	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	34.08	1974	201702	11~20	1	16400	2017	2
1189112	6238	부산광역시	전포동	산99-46	서면	전포동 산99-46 서면	57.59	1976	201706	21~30	3	9300	2017	6
1189212	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	54.15	1974	201706	1~10	5	26200	2017	6

서면 아파트 값이 전체적으로 싼것은아니다.. 그럼 뭐때문에 이렇게 값이 떨어진걸까? 확인해보니 floor=-1 인것을 볼수있다.

그렇다면 다른 -1층은 어떨까?

train[train['floor']==-1].sort_values('transaction_real_price')
	apartment_id	city	dong	jibun	apt	addr_kr	exclusive_use_area	year_of_completion	transaction_year_month	transaction_date	floor	transaction_real_price	year	month
722888	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	138.250	1974	201606	21~30	-1	100	2016	6
651052	1514	부산광역시	대청동4가	75-176	근영빌라2동	대청동4가 75-176 근영빌라2동	47.170	1996	200809	21~30	-1	1300	2008	9
651087	1513	부산광역시	대청동4가	75-181	근영빌라1동	대청동4가 75-181 근영빌라1동	59.890	1996	200810	11~20	-1	2500	2008	10
651641	1514	부산광역시	대청동4가	75-176	근영빌라2동	대청동4가 75-176 근영빌라2동	47.170	1996	201101	21~31	-1	3200	2011	1
674249	9764	부산광역시	동삼동	213-19	조은아크로빌	동삼동 213-19 조은아크로빌	46.800	2001	201506	11~20	-1	3500	2015	6
...	...	...	...	...	...	...	...	...	...	...	...	...	...	...
537384	10310	서울특별시	청담동	102-13	청담파라곤Ⅱ 2단지	청담동 102-13 청담파라곤Ⅱ 2단지	241.880	2010	201012	11~20	-1	290000	2010	12
1095417	11320	서울특별시	한남동	810	한남더힐	한남동 810 한남더힐	212.524	2011	201705	21~31	-1	480000	2017	5
1095416	11320	서울특별시	한남동	810	한남더힐	한남동 810 한남더힐	212.524	2011	201705	21~31	-1	490000	2017	5
23585	11320	서울특별시	한남동	810	한남더힐	한남동 810 한남더힐	240.230	2011	201612	1~10	-1	543000	2016	12
23376	11320	서울특별시	한남동	810	한남더힐	한남동 810 한남더힐	240.230	2011	201610	11~20	-1	575000	2016	10

지하 1층 치고 비싼곳도 있는것이 확인된다. 그러므로 지하1층은 값이 싸다라는 가정도 패스.

 

그러면 transaction_real_price가 100인 위치를 한번 싹다 확인해보자.

train[train['jibun']=='263-5'].sort_values('floor')

 

	apartment_id	city	dong	jibun	apt	addr_kr	exclusive_use_area	year_of_completion	transaction_year_month	transaction_date	floor	transaction_real_price	year	month
722888	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	138.25	1974	201606	21~30	-1	100	2016	6
700884	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	88.93	1974	201206	1~10	1	25000	2012	6
1186983	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	34.08	1974	201702	11~20	1	16400	2017	2
723957	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	55.04	1974	201608	21~31	2	23200	2016	8
1189212	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	54.15	1974	201706	1~10	5	26200	2017	6

확인해보니 같은 위치의 가격에 비해 price가 100인 값만 현저하게 작다..

이것을 이상치라고 판단하고 넘어가야하는가... 고민된다 통계적 계산을 통해 이상치 판단 근거를 만들 수 있지만 이는 다음에 해보는 것으로하고 우선 넘어가자.

 

그 다음은 월별과 가격의 연관성을 확인해보자.

위 그림을 보면 6월에 이상치같은 것이 있는데 이는 위에서 얘기한 price:100인 집이다.

 

year_of_completion(설립일자)

f,ax=plt.subplots(figsize=(22,6))
sns.boxplot(train['year_of_completion'],train['transaction_real_price'])
plt.show()

다음 설립일자에서... 너무많지만 가장 이상치같이 보이는 점을 확대해서 확인해보자..

1974년도 설립일자

1974년도에 가장 낮은 값이 있었는데 이는 무엇인지 확인해보자.

train[train['year_of_completion']==1974].sort_values('transaction_real_price',ascending=True)
	apartment_id	city	dong	jibun	apt	addr_kr	exclusive_use_area	year_of_completion	transaction_year_month	transaction_date	floor	transaction_real_price	year	month
722888	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	138.25	1974	201606	21~30	-1	4.615121	2016	6
350513	12414	서울특별시	화곡동	354-54	화곡동복지	화곡동 354-54 화곡동복지	53.94	1974	201206	21~30	1	7.601402	2012	6
689192	7162	부산광역시	가야동	385-2	시영	가야동 385-2 시영	39.67	1974	201004	21~30	6	7.696667	2010	4
679153	7162	부산광역시	가야동	385-2	시영	가야동 385-2 시영	39.67	1974	200807	11~20	4	7.824446	2008	7
678316	7162	부산광역시	가야동	385-2	시영	가야동 385-2 시영	39.67	1974	200805	11~20	5	7.863651	2008	5
...	...	...	...	...	...	...	...	...	...	...	...	...	...	...
1163472	4695	서울특별시	반포동	757	반포 주공1단지	반포동 757 반포 주공1단지	140.33	1974	201706	11~20	2	12.706851	2017	6
1164677	4695	서울특별시	반포동	757	반포 주공1단지	반포동 757 반포 주공1단지	140.33	1974	201709	11~20	3	12.736704	2017	9
1164022	4695	서울특별시	반포동	757	반포 주공1단지	반포동 757 반포 주공1단지	140.33	1974	201707	21~31	5	12.765691	2017	7
1164377	4695	서울특별시	반포동	757	반포 주공1단지	반포동 757 반포 주공1단지	140.33	1974	201708	1~10	3	12.765691	2017	8
1164370	4695	서울특별시	반포동	757	반포 주공1단지	반포동 757 반포 주공1단지	140.33	1974	201708	1~10	4	12.779876	2017	8

이또한 똑같은 데이터였다.. 

 

변수 카테고리화

city(도시)

우선 서울특별시와 부산광역시를 레이블 0,1로 구분시켜 다시 데이터프레임을 구성하자.

 

replace_name={'서울특별시':0,'부산광역시':1}
train=train.replace({'city':replace_name})
test=test.replace({'city':replace_name})
f,ax=plt.subplots(figsize=(8,6))
sns.boxplot(train['city'],train['transaction_real_price'])
plt.show()

지역별 집값 추세

보니 서울이 부산보다 집값이 높은것을 확인할 수 있다. 거래량도 확인해보자

f,ax=plt.subplots(figsize=(8,6))
sns.countplot(train['city'])
plt.show()

지역별 거래량

이를 통해 집값은 서울이 더 높고, 거래량도 서울이 더 높은것을 확인할 수 있다.

 

addr_kr(주소)

동, 지번, 아파트단지이름을 합친 것을 addr_kr로 하였는데 이는 그냥 삭제해버리자.

train=train.drop('addr_kr',axis=1)
test=test.drop('addr_kr',axis=1)

dong(동)

데이콘 튜토리얼 샘플코드에서는 한강의 유무에 따른 feature를 하나 생성해준다. 그러므로 본인도 만들어놓아본다.

train['hangang']=train['dong'].isin(['성수동1가','삼성동','이촌동','공덕동','서교동','한강로3가','목동']).astype(int)
test['hangang']=train['dong'].isin(['성수동1가','삼성동','이촌동','공덕동','서교동','한강로3가','목동']).astype(int)
len(train['dong'].unique())

473

dong은 그 지역을 그룹핑한 것이다. 그러므로 지역에 따라 가격차이를 확인해 줄 수 있다.

아파트 거래가격의 평균순으로 데이터를 레이블링해보자.

 

train_dong=train[['transaction_real_price','dong']].groupby('dong').mean().sort_values('transaction_real_price').reset_index()
train_dong.head()
dong	transaction_real_price
0	신선동3가	5500.000000
1	장안읍 명례리	5508.333333
2	신창동2가	5531.250000
3	봉래동5가	5732.546012
4	중앙동4가	6312.500000
dong_num={}
for i in range(len(train_dong)):
    dong=train_dong['dong'].iloc[i]
    dong_num[dong]=i
dong_num

{'신선동3가': 0,
 '장안읍 명례리': 1,
 '신창동2가': 2,
 '봉래동5가': 3,
 '중앙동4가': 4,
 '장충동2가': 5,
 '신선동2가': 6,
 '부평동2가': 7,
 '필동1가': 8,
 '아미동2가': 9,
 '초장동': 10,
 '수정동': 11,
 '기장읍 대변리': 12,
 '동광동5가': 13,
 '보수동1가': 14,
 '남항동3가': 15,
 '대청동1가': 16,
 '누상동': 17,
 '반송동': 18,
 '기장읍 서부리': 19,
 '동삼동': 20,
 '영등포동3가': 21,
 '일광면 이천리': 22,
 '봉래동4가': 23,
 '서동': 24,
 '일광면 삼성리': 25,
 '대청동4가': 26,
 '영등포동2가': 27,
 '덕천동': 28,
 '모라동': 29,
 '감만동': 30,
 '미근동': 31,
 '장림동': 32,
 '학장동': 33,
 '동대신동3가': 34,
 '구평동': 35,
 '보수동2가': 36,
 ...
 '대치동': 464,
 '서빙고동': 465,
 '한남동': 466,
 '반포동': 467,
 '회현동2가': 468,
 '용산동5가': 469,
 '청암동': 470,
 '압구정동': 471,
 '장충동1가': 472}
train=train.replace({'dong': dong_num})
test=test.replace({'dong':dong_num})
train.head()
apartment_id	city	dong	jibun	apt	exclusive_use_area	year_of_completion	transaction_year_month	transaction_date	floor	transaction_real_price	year	month	hangang
0	7622	0	335	6-13	신현(101동)	84.82	2002	200801	21~31	2	37500	2008	1	0
1	5399	0	408	142	사직파크맨션	99.17	1973	200801	1~10	6	20000	2008	1	0
2	3578	0	408	174-1	두레엘리시안	84.74	2007	200801	1~10	6	38500	2008	1	0
3	10957	0	459	95	파크팰리스	146.39	2003	200801	11~20	15	118000	2008	1	0
4	10639	0	459	110-15	킹스매너	194.43	2004	200801	21~31	3	120000	2008	1	0

Jibun(지번)

지번의 unique_value를 확인해보자.

len(train['jibun'].unique())

8961
train_jibun=train[['transaction_real_price','jibun']].groupby('jibun').mean().sort_values('transaction_real_price').reset_index()
train_jibun.head()
	jibun	transaction_real_price
0	산3-148	1428.263889
1	737-1	1852.413793
2	741-1	1855.584416
3	11-174	2000.000000
4	1181-4	2043.055556

apt(아파트)

아파트의 unique_value를 확인해보자

len(train['apt'].unique())

10440
train_apt=train[['transaction_real_price','apt']].groupby('apt').mean().sort_values('transaction_real_price').reset_index()
train_apt.head()
	apt	transaction_real_price
0	좌천시민(737-1)	1852.413793
1	좌천시민(741-1)	1855.584416
2	수정(1181-4)	2043.055556
3	수정(1175-1)	2111.785714
4	수정(1186-1)	2292.333333

확인해보니 지번과 아파트 데이터는 unique_value가 너무 많다.. 따로 레이블링은 하지말자..

 

transaction_date(거래 기간)

위 데이터는 처음 거래시작한 day와 거래가 끝난 day의 차이를 칼럼으로 변경시켜주자.

train['day_diff']=train['transaction_date'].str.extract('(~\d+)')[0].str[1:].astype(int)-train['transaction_date'].str.extract('(\d+~)')[0].str[:-1].astype(int)
test['day_diff']=test['transaction_date'].str.extract('(~\d+)')[0].str[1:].astype(int)-test['transaction_date'].str.extract('(\d+~)')[0].str[:-1].astype(int)

이것에 대해 unique_value를 확인해보자.

len(train['transaction_date'].unique())

6

거래기간은 unique_value가 6개이므로 이는 레이블링 처리를 해주자.

 

train_date=train[['transaction_real_price','transaction_date']].groupby('transaction_date').mean().sort_values('transaction_real_price').reset_index()
train_date.head()
transaction_date	transaction_real_price
0	21~29	33055.585346
1	21~28	37182.344295
2	1~10	37732.015054
3	11~20	38217.678678
4	21~31	38879.439698
date_num={}
for i in range(len(train_date)):
    date=train_date['transaction_date'].iloc[i]
    date_num[date]=i
date_num

{'21~29': 0, '21~28': 1, '1~10': 2, '11~20': 3, '21~31': 4, '21~30': 5}
train=train.replace({'transaction_date':date_num})
test=test.replace({'transaction_date':date_num})
train.head()
apartment_id	city	dong	jibun	apt	exclusive_use_area	year_of_completion	transaction_year_month	transaction_date	floor	transaction_real_price	year	month	hangang	day_diff
0	7622	0	335	6-13	신현(101동)	84.82	2002	200801	4	2	37500	2008	1	0	10
1	5399	0	408	142	사직파크맨션	99.17	1973	200801	2	6	20000	2008	1	0	9
2	3578	0	408	174-1	두레엘리시안	84.74	2007	200801	2	6	38500	2008	1	0	9
3	10957	0	459	95	파크팰리스	146.39	2003	200801	3	15	118000	2008	1	0	9
4	10639	0	459	110-15	킹스매너	194.43	2004	200801	4	3	120000	2008	1	0	10

전처리

floor 같은 경우 음수값이 존재하기 때문에 log를 취하기 전에 각 값에 +5를 해주어 모든 값을 양수로 만들어주자.

train['floor']=np.log(train['floor']+5)
test['floor']=np.log(test['floor']+5)
f,ax=plt.subplots(figsize=(8,6))
sns.distplot(train['floor'])
print("%s->Skewness: %f, Kurtosis: %f"%('floor',train['floor'].skew(),train['floor'].kurt()))

floor->Skewness: 0.089636, Kurtosis: -0.640885

이제 아파트와 지번 그리고 거래년월을 없애버리자.

drop_columns=['apt','jibun','transaction_year_month']

train=train.drop(drop_columns,axis=1)
test=test.drop(drop_columns,axis=1)
train.head()
	apartment_id	city	dong	exclusive_use_area	year_of_completion	transaction_date	floor	transaction_real_price	year	month	hangang	day_diff
0	7622	0	335	84.82	2002	4	1.945910	37500	2008	1	0	10
1	5399	0	408	99.17	1973	2	2.397895	20000	2008	1	0	9
2	3578	0	408	84.74	2007	2	2.397895	38500	2008	1	0	9
3	10957	0	459	146.39	2003	3	2.995732	118000	2008	1	0	9
4	10639	0	459	194.43	2004	4	2.079442	120000	2008	1	0	10

Feature Engineering

train_test_data=[train,test]

for dataset in train_test_data:
    dataset['age']=dataset['year']-dataset['year_of_completion']
    dataset['is_rebuild']=(dataset['age']>=30).astype(int)
    

거래하는 기간까지의 아파트 나이를 특징으로 생성해주고, 샘플코드에 있는 아파트의 재건축 유무를 판단하는 특징을 만들어준다.

train_columns=[]
for column in train.columns[:]:
    if train[column].skew()>=1:
        print('%s -> Skewness: %f, Kurtosis: %f'%(column,train[column].skew(),train[column].kurt()))
        train_columns.append(column)
    elif train[column].kurt()>=3:
        print('%s -->Skewness: %f, Kurtosis: %f'%(column,train[column].skew(),train[column].kurt()))
        train_columns.append(column)
        
exclusive_use_area -> Skewness: 1.227509, Kurtosis: 3.100517
transaction_real_price -> Skewness: 3.407169, Kurtosis: 24.839821
hangang -> Skewness: 6.358349, Kurtosis: 38.428662
day_diff -->Skewness: -0.589751, Kurtosis: 4.573129
is_rebuild -> Skewness: 3.324832, Kurtosis: 9.054522

정규분포 근사화를 위해 첨도와 왜도를 조정해준다.

for column in train_columns :
    train[column] = np.log1p(train[column])
    test[column] = np.log1p(test[column])
    print("%s -> Skewness: %f, Kurtosis: %f" %  (column,train[column].skew(), 
                                                 train[column].kurt()))
                                                 
                                                 

exclusive_use_area -> Skewness: -0.438156, Kurtosis: 1.744119
hangang -> Skewness: 6.358349, Kurtosis: 38.428662
day_diff -> Skewness: -1.128137, Kurtosis: 6.788569
is_rebuild -> Skewness: 3.324832, Kurtosis: 9.054522

이제 상관관계를 다시 확인해보자.

#상관관계 확인
k=train.shape[1] #히트맵 변수 갯수
corrmat = train.corr() #변수간의 상관관계
cols = corrmat.nlargest(k, 'transaction_real_price')['transaction_real_price'].index #price기준으로 제일 큰순서대로 20개를 뽑아냄
cm = np.corrcoef(train[cols].values.T)
f, ax = plt.subplots(figsize=(20, 6))
sns.heatmap(data = cm, annot=True, square=True, fmt = '.2f', linewidths=.5, cmap='Reds', 
            yticklabels = cols.values, xticklabels = cols.values)

 

모델링

from sklearn.linear_model import ElasticNet, Lasso
from sklearn.ensemble import GradientBoostingRegressor, RandomForestRegressor
from sklearn.kernel_ridge import KernelRidge
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import RobustScaler
from sklearn.model_selection import KFold, cross_val_score, train_test_split
from sklearn.metrics import mean_squared_error
import xgboost as xgb
import lightgbm as lgb
target = train['transaction_real_price']
del train['transaction_real_price']
#cross validation score
n_folds = 2

def cv_score(models):
    kfold = KFold(n_splits=n_folds, shuffle=True ,random_state=42).get_n_splits(train.values)
    for m in models:
        cvs = np.mean(cross_val_score(m['model'], train.values, target, cv=kfold))
        rmse = np.mean(np.sqrt(-cross_val_score(m['model'], train.values, np.expm1(target), scoring = "neg_mean_squared_error", cv = kfold)))
        print("Model {} CV score : {:.4f}".format(m['name'], cvs))
        print("RMSE : {:.4f}".format(rmse))
lasso = make_pipeline(RobustScaler(), Lasso(alpha = 0.0005, random_state=42))
ENet = make_pipeline(RobustScaler(), ElasticNet(alpha=0.0005, l1_ratio=.9, random_state=42))
gboost = GradientBoostingRegressor(random_state=42)
forest = RandomForestRegressor(n_estimators = 100, n_jobs = -1, random_state=42)
xgboost = xgb.XGBRegressor(random_state=42)
lightgbm = lgb.LGBMRegressor(random_state=42, num_leaves = 100, min_data_in_leaf = 15, max_depth=6,
                            learning_rate = 0.1, min_child_samples = 30, feature_fraction=0.9, bagging_freq= 1,
                            bagging_fraction = 0.9, bagging_seed = 11, lambda_l1 = 0.1, verbosity = -1 )

models = [{'model': gboost, 'name':'GradientBoosting'}, {'model': xgboost, 'name':'XGBoost'},
          {'model': lightgbm, 'name':'LightGBM'}, {'model' : lasso, 'name' : 'LASSO Regression'}, 
          {'model' : ENet, 'name' : 'Elastic Net Regression'}, {'model' : forest, 'name' : 'RandomForset'}]
start = timeit.default_timer()
cv_score(models)
stop = timeit.default_timer()
print('불러오는데 걸린 시간 : {}초'.format(stop - start))
Model GradientBoosting CV score : 0.5706
RMSE : 0.0108
Model XGBoost CV score : 0.5788
RMSE : 0.0108
Model LightGBM CV score : 0.5681
RMSE : 0.0108
Model LASSO Regression CV score : 0.5526
RMSE : 0.0107
Model Elastic Net Regression CV score : 0.5602
RMSE : 0.0106
Model RandomForset CV score : 0.4911
RMSE : 0.0118
불러오는데 걸린 시간 : 910.3417148000003초
#여러개의 모델로 만들어진 predict 데이터들의 평균을 구한다.

models = [{'model':xgboost, 'name':'XGBoost'},
          {'model':lightgbm, 'name':'LightGBM'},
         {'model':forest, 'name' : 'RandomForest'}]

def AveragingBlending(models, x, y, sub_x):
    for m in models : 
        m['model'].fit(x.values, y)
    
    predictions = np.column_stack([m['model'].predict(sub_x.values) for m in models])
    return predictions
start = timeit.default_timer()

y_test_pred = AveragingBlending(models, train, target, test)
y_test_pred = (y_test_pred[:, 0]*0.05 + y_test_pred[:, 1]*0.1 + y_test_pred[:, 2]*0.85)
predictions = y_test_pred

stop = timeit.default_timer()
print('불러오는데 걸린 시간 : {}초'.format(stop - start))
[LightGBM] [Warning] feature_fraction is set=0.9, colsample_bytree=1.0 will be ignored. Current value: feature_fraction=0.9
[LightGBM] [Warning] min_data_in_leaf is set=15, min_child_samples=30 will be ignored. Current value: min_data_in_leaf=15
[LightGBM] [Warning] lambda_l1 is set=0.1, reg_alpha=0.0 will be ignored. Current value: lambda_l1=0.1
[LightGBM] [Warning] bagging_fraction is set=0.9, subsample=1.0 will be ignored. Current value: bagging_fraction=0.9
[LightGBM] [Warning] bagging_freq is set=1, subsample_freq=0 will be ignored. Current value: bagging_freq=1
불러오는데 걸린 시간 : 260.0541379999995초
sub = pd.read_csv('d:/data/real_estate/submission.csv')
sub['transaction_real_price'] = np.expm1(predictions)
sub.to_csv('d:/data/real_estate/submission.csv', index=False)

 

 

GRU 기법을 사용해 주제를 분류해보자.

앞선 GRU기법에 대한 포스팅은 조만간 하겠다.

우선 필요한 라이브러리를 불러오자.

 

import pandas as pd
import urllib.request
%matplotlib inline
import matplotlib.pyplot as plt
import re
from konlpy.tag import Okt
from tensorflow.keras.preprocessing.text import Tokenizer
import numpy as np
from tensorflow.keras.preprocessing.sequence import pad_sequences
import os
from tensorflow.keras.datasets import reuters
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense,LSTM,Embedding
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.models import load_model
from tensorflow.keras.layers import Embedding,Dense,GRU
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint
from tqdm import tqdm

데이터를 불러오고,

train_data=pd.read_csv('C:/data/Bluehouse/train.csv',encoding='utf-8')
test_data=pd.read_csv('C:/data/Bluehouse/test.csv',encoding='utf-8')
sample_submission=pd.read_csv('C:/data/Bluehouse/sample_submission.csv',encoding='utf-8')

어떤 데이터인지 확인해보자.

우선 train_data부터.

train_data

[40000열*3행]

우선 인덱스와 real_index는 같지만(!?) 카테고리와 데이터는 정확하게 나왔다.

 

 

테스트 데이터를 살펴보면,

test_data

[5000열&2행]

위 데이터는 실제 청원의 대한 내용이다.

 

이제 train_data,test_data에 대한 전처리 및 불용어 처리를 시행해보자. 

앞서 설명했던 포스트처럼 불용어 처리할때, 개인의 dictionary를 사용하는게 좋다. 

여기서 주의점은 사용하는 용도에 따라 dictionary는 바뀌어야한다. 

train_data=train_data.dropna(how='any')
train_data['data']=train_data['data'].str.replace("[^ㄱ-ㅎㅏ-ㅣ가-힣]","")
test_data['data']=test_data['data'].str.replace('[^ㄱ-ㅎㅏ-ㅣ가-힣]','')
stopwords=['의','가','이','은','들','는','좀','질','걍','과','도','를','으로','자','에','와','한','하다','을','도', '는', '다', '의', '가', '이', '은', '한', '에', '하', '고', '을', '를', '인', '듯', '과', '와', '네', '들', '듯', '지', '임', '게','요','거','로','으로',
            '것','수','할','하는','제','에서','그','데','번','해도','죠','된','건','바','구','세']

 

가장 기본적인 전처리 작업 후, 본인이 애용하는 pykospacing을 사용해보자

from pykospacing import spacing
train_data['data']=train_data['data'].apply(spacing)
test_data['data']=test_data['data'].apply(spacing)

 

앞선 포스팅을 봤다면 알것이다. 이쯤되면 데이터 처리속도가 상당히 느려질 뿐더러 위 데이터 전처리 과정조차도 상당한 시간이 걸린다. 

이에 대해 전처리된 데이터를 저장하자.

 

import pickle
train_data.to_pickle("C:/data/Bluehouse/train_data.pkl")
test_data.to_pickle("C:/data/Bluehouse/test_data.pkl")

 

이제 자연어 처리를 해보자.

okt=Okt()
test_data['tokenized']=test_data['data'].apply(okt.morphs)
test_data['tokenized']=test_data['tokenized'].apply(lambda x:[item for item in x if item not in stopwords])
train_data['tokenized']=train_data['data'].apply(okt.morphs)
train_data['tokenized']=train_data['tokenized'].apply(lambda x:[item for item in x if item not in stopwords])

 

 

단어 집합의 크기와 회귀수 등 여러가지로 중요한 점을 파악해보자.

X_Train=train_data['tokenized'].values
X_Test=test_data['tokenized'].values
y_Train=to_categorical(train_data['category'])

tokenizer=Tokenizer()
tokenizer.fit_on_texts(X_Train)

threshold=2
total_cnt=len(tokenizer.word_index)
rare_cnt=0
total_freq=0
rare_freq=0

for key,value in tokenizer.word_counts.items():
    total_freq=total_freq+value
    if(value<threshold):
        rare_cnt=rare_cnt+1
        rare_freq=rare_freq+value
        
print("단어집합의 크기:",total_cnt)
print("등장 빈도가 %s번 이하인 회귀 단어의 수: %s"%(threshold-1,rare_cnt))
print("단어집합에서 회귀 단어의 비율:",(rare_cnt/total_cnt)*100)
print("전체 등장 빈도에서 회귀 단어 등장 빈도 비율:",(rare_freq/total_freq)*100)

단어집합의 크기: 133738
등장 빈도가 1번 이하인 회귀 단어의 수: 61680
단어집합에서 회귀 단어의 비율: 46.120025721933935
전체 등장 빈도에서 회귀 단어 등장 빈도 비율: 1.0407867591945446

단어집합의 크기는 1333738개 존재한다. 등장빈도가 threshold값인 2회 미만, 즉, 1회인 단어들은 집합에서 단어 집합에서 약 46퍼센트 차치한다. 실제로 훈련 데이터에서 등장빈도로 차지하는 비중은 매우 작은 수치인 1퍼센트 밖에 되지 않는다. 이 계산법 및 코드에 대해서는 차후 포스팅 하겠다.

 

vocab_size=total_cnt-rare_cnt+2
print("단어 집합의 크기:",vocab_size)

단어 집합의 크기: 72060

위 단어집합 크기를 사용해 토크나이징을 시행해보자.

 

tokenizer=Tokenizer(vocab_size,oov_token='OOV')
tokenizer.fit_on_texts(X_Train)
X_Train=tokenizer.texts_to_sequences(X_Train)
X_Test=tokenizer.texts_to_sequences(X_Test)

print(X_Test[:2])
print(X_Train[:2])

text_to_sequences메서드를 시행함으로써 다음 결과는 이러하다.

[[435, 49, 1075, 3863, 799, 113, 577, 8, 27, 265, 113, 436, 12, 2, 9, 413, 27, 926, 829, 15, 2790, 2917, 6420, 9, 435, 49, 192], [357, 684, 105, 5549, 303, 23, 357, 684, 105, 3131, 35906, 3641, 64, 5549, 298, 866, 5061, 3131, 298, 19646, 1087, 803, 2801, 36356, 64, 2591, 1146, 12988, 5447, 1226, 3131, 1411, 7766, 3077, 819, 16487, 236, 970, 3301, 400, 1769, 319, 4050, 670]]
[[14825, 406, 4960, 2644, 131, 129, 254, 527, 5118, 16, 340, 381, 42, 131, 17717, 1300, 4289, 4, 89, 16, 7791, 7, 714, 131, 54362, 125, 11, 6880, 746, 6010, 45, 10, 4248, 45, 38, 2644, 131, 169, 3, 16, 340, 381, 10, 13, 5494, 2183, 3145, 382, 14, 4062, 340, 41, 8, 2945, 2869, 7, 254, 527, 574, 894, 66, 1882, 14826, 65, 89, 648, 229, 197, 894, 174, 23799, 231, 129, 344, 2869, 719, 1302, 2828, 45, 24, 11852, 4885, 182, 522, 3934, 575, 3035, 522, 2080, 174, 892, 2253, 404, 20871, 3035, 41, 8, 141, 1019, 6829, 213, 61, 5119, 515, 594, 2612, 246, 39625, 2796, 1221, 2167, 5495, 62, 131, 58, 12, 2309, 246, 2796, 338, 7342, 25011, 20872, 10427, 11853], [22, 533, 158, 20078, 2529, 137, 506, 1858, 864, 17, 5, 442, 1949, 26428, 6, 127, 41, 2529, 672, 2529, 506, 183, 2182, 78, 151, 11173, 151, 8478, 167, 4455, 41, 22, 15, 137, 30057, 5825, 6197, 158, 10168, 3178, 31, 54363, 22, 159, 155, 1293, 2859, 2529, 506, 11173, 116, 158, 7046, 2529, 158, 506, 1725, 66, 2029, 22, 533, 4710, 2663, 31, 116, 41, 22, 1177, 136, 60, 11173, 338, 1177, 1737, 159, 17205, 199, 26, 328, 2014, 3707, 578, 698, 54364, 1293, 187, 1729, 3609, 31, 170, 10285, 4063, 54, 5579, 10556, 357, 86, 38, 22, 54365, 2529, 225, 806, 148, 842, 81, 60, 258, 7, 1284, 29, 214, 1228, 3]]

결과는 위와 같으며, 실제 각 단어마다 정수 인코딩을 시행함으로써 각 단어에 numbering을 한것이라 볼 수 있다.

그렇다면 청원의 평균 길이는 얼마나 될까?

흔히 이것을 패딩이라한다. 이는 앞선 포스트에서 찾아볼 수 있다.

print("청원의 최대 길이:",max(len(i) for i in X_Train))
print("청원의 평균 길이:",sum(map(len,X_Train))/len(X_Train))
plt.hist([len(s) for s in X_Train],bins=50)
plt.xlabel('length of samples')
plt.ylabel('number of samples')
plt.show()

청원의 최대 길이: 9005

청원의 평균 길이: 148.1867873574715

대략, 청원의 최대 길이는 9005이며 평균 150 길이정도 된다는 것을 그림 및 수치적으로 알 수 있다.

 

이제 category에 대한 labeling을 시행해보자.

 

Y_Train=to_categorical(train_data['category'])
def below_threshold_len(max_len,nested_list):
    cnt=0
    for s in nested_list:
        if(len(s)<=max_len):
            cnt+=1
    print("전체 샘플 중 길이가 %s 이하인 샘플의 비율: %s"%(max_len,(cnt/len(nested_list))*100))

최대 길이가 5000인 경우 몇 개의 샘플들을 온전히 보전할 수 있는지 확인해보자.

 

max_len=5000
below_threshold_len(max_len,X_Train)

전체 샘플 중 길이가 5000 이하인 샘플의 비율: 99.99249849969995

test용 청원은 99.99퍼센트가 5000이하의 길이를 가진다. 그러므로, 훈련용 리뷰의 길이를 5000으로 지정하겠다. 

 

X_Train=pad_sequences(X_Train,maxlen=max_len)
X_Test=pad_sequences(X_Test,maxlen=max_len)

 

이제 GRU기법을 통해 얼마나 category에 근접한지 확인해보자.

modeling에 대해서는 가장 간단한 기초이므로 생략하겠다. 이는 앞선 딥러닝 기초에 대한 포스팅을 읽기 바란다.

 

model=Sequential()
model.add(Embedding(vocab_size,100))
model.add(GRU(120))
model.add(Dense(3,activation='softmax'))

es=EarlyStopping(monitor='val_loss',mode='min',verbose=1,patience=4)
mc=ModelCheckpoint('best_blue_GRU.h5',monitor='val_acc',mode='max',verbose=1,save_best_only=True)

model.compile(loss='categorical_crossentropy',optimizer='adam',metrics=['acc'])
history=model.fit(X_Train,Y_Train,epochs=5,batch_size=32)

Epoch 1/5 1250/1250 [==============================] - 9332s 7s/step - loss: 0.4483 - acc: 0.8139 Epoch 2/5 1250/1250 [==============================] - 9820s 8s/step - loss: 0.2283 - acc: 0.9159 Epoch 3/5 1250/1250 [==============================] - 8772s 7s/step - loss: 0.1363 - acc: 0.9493 Epoch 4/5 1250/1250 [==============================] - 8487s 7s/step - loss: 0.0820 - acc: 0.9689 Epoch 5/5 1250/1250 [==============================] - 8341s 7s/step - loss: 0.0526 - acc: 0.9796

 

 

y_pred=model.predict_classes(X_Test)
sample_submission['category']=y_pred
sample_submission.to_csv('c:/data/Bluehouse/submission.csv',encoding='utf-8',index=False)

 

이후 제출을 함으로써 얼마나 정확성이 나오나 확인해보자.

 

여기서 알 수 있는점은 단순히 불용어처리 즉 dictionary를 얼마나 알차게 짯느냐, 또한 알고리즘을 LSTM으로 짯느냐 혹은 GRU기법을 짯는냐에 따라 정확도는 올라가고 내려간다는것을 알 수 있다. 본인이 이 포스팅을 짜기전에 불용어(stopwords)는 똑같이 지정하고,  기법만 다르게 해봤다. 무려 0.4퍼센트 차이가 난다. 그러므로 다시한번 느끼지만 NLP는 얼마나 사전작업을 치느냐가 관건이라고 생각한다.

Dacon에서 연습용으로 할 수 있는 것이다. 

 

import pandas as pd
import urllib.request
%matplotlib inline

import matplotlib.pyplot as plt
import re
from konlpy.tag import Okt
from tensorflow.keras.preprocessing.text import Tokenizer
import numpy as np
from tensorflow.keras.preprocessing.sequence import pad_sequences
import os
from tensorflow.keras.datasets import reuters
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense,LSTM,Embedding
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.models import load_model
from tqdm import tqdm

 

데이터 불러오기

train_data=pd.read_csv('d:/data/train.csv',encoding='utf-8')
test_data=pd.read_csv('d:/data/test.csv',encoding='utf-8')
sample_submission=pd.read_csv('d:/data/sample_submission.csv',encoding='utf-8')

결측치 제거 및 간단한 전처리

train_data=train_data.dropna(how='any')
train_data['data']=train_data['data'].str.replace("[^ㄱ-ㅎㅏ-ㅣ가-힣 ]","")
test_data['data']=test_data['data'].str.replace("[^ㄱ-ㅎㅏ-ㅣ가-힣 ]","")
stopwords= ['의','가','이','은','들','는','좀','잘','걍','과','도','를','으로','자','에','와','한','하다','을']

konlpy에서 okt라는 자연어처리 라이브러리 불러오기

okt=Okt()

훈련용 데이터에 대해 자연어 처리 및 불용어처리 

*tqdm에 대해서는 추후 포스팅 하겠다.

X_train=[]
for sentence,i in zip(train_data['data'],tqdm(range(len(train_data['data'])))):
    temp_X=[]
    temp_X=okt.morphs(sentence,stem=True)
    temp_X=[word for word in temp_X if not word in stopwords]
    X_train.append(temp_X) 

테스트용 데이터에 대해 자연어 처리 및 불용어처리

X_test=[]
for sentence in test_data['data']:
    temp_X=[]
    temp_X=okt.morphs(sentence,stem=True)
    temp_X=[word for word in temp_X if not word in stopwords]
    X_test.append(temp_X)

fit_on_texts() 메서드를 통해 문자데이터를 입력받아 리스트 형식으로 변환.

text_to_sequences() 메서드를 통해 단어들을 시퀀스형식으로 변환

tokenizer=Tokenizer()
tokenizer.fit_on_texts(X_train)

vocab_size=30000
tokenizer=Tokenizer(vocab_size)
tokenizer.fit_on_texts(X_train)
X_train=tokenizer.texts_to_sequences(X_train)
X_test=tokenizer.texts_to_sequences(X_test)

pad_sequences() 메서드를 통해 서로 다른 개수의 단어로 이루어진 문장을 같은 길이로 만들어주기 위해 패딩한다.

500자로 동일하게 맞춰준다.

max_len=500
X_train=pad_sequences(X_train,maxlen=max_len)
X_test=pad_sequences(X_test,maxlen=max_len)

to_categorical() 메서드를 통해 One-hot인코딩 시행

*One-hot encoding:10진 정수 형식을 특수한 2진 바이너리 형식으로 변환, 파라미터로 값에 크기만큼 0으로 된 배열을 만들고, 파라미터 값 위치에만 1(hot)을 넣어준다.

y_train=to_categorical(train['category'])

데이터 모델링

Sequential(): Sequential()모델은 각 레이어에 정확히 하나의 입력 텐서와 하나의 출력 텐서가 있는 일반 레이어 스택에 적합.

Sequential 모델은 다음의 경우에 적합하지 않습니다.

  • 모델에 다중 입력 또는 다중 출력이 있습니다
  • 레이어에 다중 입력 또는 다중 출력이 있습니다
  • 레이어 공유를 해야 합니다
  • 비선형 토폴로지를 원합니다(예: 잔류 연결, 다중 분기 모델)

언어의 벡터화를 위해 Word Embedding 시행

LSTM기법 사용(120 layer)

활성화 함수:softmax 

전체 compile: 손실함수는 'categorical_crossentrophy'  이는 레이블이 n개일때 사용한다. 2개일때는 'binary cross entrophy'를  사용한다.

 

model=Sequential()
model.add(Embedding(vocab_size,120))
model.add(LSTM(120))
model.add(Dense(3,activation='softmax'))

model.compile(loss='categorical_crossentrophy',optimizer='adam',metrics=['acc'])
history=model.fit(X_train,y_train,batch_size=128,epochs=15)

 

예측한 자료를 csv 파일로 내보내자.

y_pred=model.predict_classes(X_test)
sample_submission['category']=y_pred
sample_submission.to_csv('d:/data/submission.csv',encoding='utf-8',index=False)

 

챗봇을 만들기 위해서는 우선 앞선 포스팅인 트랜스포머에 대해서 이해하고 다음을 알아보자.

https://undeadkwandoll.tistory.com/26

 

트랜스포머(Transformer)(1)

Transformer는 2017년 구글이 발표한 논문인 'Attention is all you need'에서 나온 모델이다. 기존의 seq2seq 구조인 encoder-decoder를 따르면서도 논문의 이름처럼 어텐션 기법만으로 구현한 모델이다. 이 모델..

undeadkwandoll.tistory.com

undeadkwandoll.tistory.com/27

 

트랜스포머(Transformer)(2)

2021/03/04 - [Deep Learning/Deep Leaning inside] - 트랜스포머(Transformer)(1) 8)패딩 마스크(Padding Mask) 앞서 포스팅한 Scaled dot-product attention함수 내부를 보면 mask라는 값을 인자로 받아서, 이 m..

undeadkwandoll.tistory.com

 

데이터 로드

 

시작하기전 필요한 라이브러리를 불러오자.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import re
import urllib.request
import time
import tensorflow_datasets as tfds
import tensorflow as tf

챗봇 데이터를 로드하여 상위 5개 샘플 출력해보자.

urllib.request.urlretrieve("https://raw.githubusercontent.com/songys/Chatbot_data/master/ChatbotData%20.csv", filename="ChatBotData.csv")
train_data = pd.read_csv('ChatBotData.csv')
train_data.head()

상위 5개 샘플

위 데이터를 보면 Q(question), A(answer) 의 쌍으로 이루어진 데이터이다.

#총 샘플 개수 확인
print('챗봇 샘플의 개수 :',len(train_data))

챗봇 샘플의 개수 : 11823

총 샘플 갯수는 11823개이며, 불필요한 NULL값이 있는지 확인해보자.

 

print(train_data.isnull().sum())

Null값은 별도로 존재하지 않는다. 기본적으로 사용하는 토큰화를 위한 형태소 분석툴을 사용하지 않고, 다른 방법인 학습 기반의 토크나이저를 사용할 것이다. 그러므로, row data에서 구두점을 미리 처리해야한다. 

구두점들은 단순히 제거해버릴수 있지만, 구두점 앞에 띄어쓰기를 추가하여 다른 문자들과 구분해보자.

 

questions=[]
for sentence in train_data['Q']:
    #구두점에 대해서 띄어쓰기
    #ex) 12시 땡! ->12시 땡 !
    sentence=re.sub(r"([?.!,])", r" \1",sentence)
    sentence=sentence.strip()
    questions.append(sentence)
answers=[]
for sentence in train_data['A']:
    #구두점에 대해서 띄어쓰기
    #ex) 12시 땡! ->12시 땡 !
    sentence=re.sub(r"([?.!,])",r" \1",sentence)
    sentence=sentence.strip()
    answers.append(sentence)

이후 잘 처리가 되었는지 확인해보자.

 

print(questions[:5])
print(answers[:5])



['12시 땡 !', '1지망 학교 떨어졌어', '3박4일 놀러가고 싶다', '3박4일 정도 놀러가고 싶다', 'PPL 심하네']
['하루가 또 가네요 .', '위로해 드립니다 .', '여행은 언제나 좋죠 .', '여행은 언제나 좋죠 .', '눈살이 찌푸려지죠 .']

다음과 같이 잘 처리되었다.

 

단어 집합 생성

서브워드텍스트인코더를 사용해 자주 사용되는 서브워드 단위로 토큰을 분리하는 토크나이저로 학습 데이터로부터 학습하여 서브워드로 구성된 단어 집합을 생성해보자.

#서브워드텍스트인코더를 사용하여 질문, 답변 데이터로부터 단어 집합(Vocabulary) 생성
tokenizer=tfds.deprecated.text.SubwordTextEncoder.build_from_corpus(questions+answers,target_vocab_size=2**13)

seq2seq에서의 인코더-디코더 모델 계열에는 디코더의 입력으로 사용할 시작을 의미하는 시작 토큰 SOS와 종료 토큰 EOS 또한 존재한다. 해당 토큰들도 단어 집합에 포함시킬 필요가 있으므로 이 두 토큰에 정수를 부여하자.

 

#시작 토큰과 종료 토큰에 대한 정수 부여.
START_TOKEN,END_TOKEN=[tokenizer.vocab_size],[tokenizer.vocab_size+1]

#시작 토큰과 종료 토큰을 고려햐여 단어 집합의 크기를 +2
VOCAB_SIZE=tokenizer.vocab_size+2

print('시작 토큰 번호: ',START_TOKEN)
print('종료 토큰 번호: ',END_TOKEN)
print('단어 집합의 크기: ',VOCAB_SIZE)


시작 토큰 번호:  [8170]
종료 토큰 번호:  [8171]
단어 집합의 크기:  8172

padding에 사용될 0번 토큰부터 마지막 토큰인 8171번 토큰까지의 개수를 카운트하면 단어 집합의 크기는 8172개이다.

 

정수 인코딩과 패딩

단어 집합 생성 후, 서브워드텍스트인코더의 토크나이저로 정수 인코딩을 진행할 수 있다. 이는 토크나이저의 .encode()를 사용하여 가능하다. 랜덤샘플 20번 질문 샘플, 즉 ,questions[20]을 갖고 인코딩 해보자.

 

#서브워드텍스트인코더 토크나이저의 .encode()를 사용하여 텍스트 시퀀스를 정수 시퀀스로 변환.
print('임의의 질문 샘플을 정수 인코딩 : {}'.format(tokenizer.encode(questions[20])))

임의의 질문 샘플을 정수 인코딩 : [5759, 607, 3502, 138, 681, 3740, 846]

임의의 질문 문장이 정수 시퀀스로 변환되었다. 반대로 정수 인코딩 된 결과는 다시 decode()를 사용해 기존의 텍스트 시퀀스로 복원할 수 있다. 

 

#서브워드텍스트인코더 토크나이저의 .encode()와 .decode() 테스트해보기
#임의의 입력 문장을 sample_string에 저장
sample_string=questions[20]

#encode() : 텍스트 시퀀스 -->정수 시퀀스
tokenized_string=tokenizer.encode(sample_string)
print('정수 인코딩 후의 문장 {}'.format(tokenized_string))

#decode(): 정수 시퀀스-->텍스트 시퀀스
original_string=tokenizer.decode(tokenized_string)
print('기존 문장: {}'.format(original_string))


정수 인코딩 후의 문장 [5759, 607, 3502, 138, 681, 3740, 846]
기존 문장: 가스비 비싼데 감기 걸리겠어

정수 인코딩 된 문장을 .decode()를 하면 자동으로 서브워드들까지 다시 붙여서 기존 단어로 복원해준다. 위 결과를 보면 정수가 7개인데 기존 문장의 띄어쓰기 단위인 어절은 4개밖에 존재하지 않는다. 이는 결국 '가스비','비싼데'라는 한 어절이 정수 인코딩 후에는 두 개 이상의 정수일 수 있다는 것이다.

확인해보자.

 

#각 정수는 각 단어와 어떻게 mapping되는지 병렬로 출력
#서브워드텍스트인코더는 의미있는 단위의 서브워드로 토크나이징한다. 띄어쓰기 단위 x형태소 분석 단위 x
for ts in tokenized_string:
    print('{}----->{}'.format(ts,tokenizer.decode([ts])))
    
    
5759----->가스
607----->비 
3502----->비싼
138----->데 
681----->감기 
3740----->걸리
846----->겠어

 

샘플1개를 갖고 인코딩 디코딩을 해보았다. 이제 전체 데이터에 대해 정수 인코딩과 패딩을 하자. 

 

#최대 길이를 40으로 정의
MAX_LENGTH=40

#토큰화/정수 인코딩/ 시작 토큰과 종료 토큰 추가/ 패딩
def tokenize_and_filter(inputs,outputs):
    tokenized_inputs,tokenized_outputs=[],[]
    
    for (sentence1,sentence2) in zip(inputs,outputs):
        #encode(토큰화 +정수 인코딩), 시작 토큰과 종료 토큰 추가
        sentence1=START_TOKEN+tokenizer.encode(sentence1)+END_TOKEN
        sentence2=START_TOKEN+tokenizer.encode(sentence2)+END_TOKEN
        
        tokenized_inputs.append(sentence1)
        tokenized_outputs.append(sentence2)
        
    #패딩
    tokenized_inputs=tf.keras.preprocessing.sequence.pad_sequences(tokenized_inputs,maxlen=MAX_LENGTH,padding='post')
    tokenized_outputs=tf.keras.preprocessing.sequence.pad_sequences(tokenized_outputs,maxlen=MAX_LENGTH,padding='post')

    return tokenized_inputs,tokenized_outputs
questions,answers=tokenize_and_filter(questions,answers)

데이터의 크기를 확인해보자.

print('질문 데이터의 크기(shape): ',questions.shape)
print('답변 데이터의 크기(shape): ',answers.shape)

질문 데이터의 크기(shape):  (11823, 40)
답변 데이터의 크기(shape):  (11823, 40)

임의로 3번 샘플을 출력해보자.

 

print(questions[3])
print(answers[3])

3번째 샘플

길이 40을 맞추기 위해 뒤에 0이 임의로 패딩되어있는 것을 확인할 수 있다.

 

인코더와 디코더의 입력, 레이블 만들기.

데이터를 배치 단위로 불러오기 위해 tf.data.Dataset을 사용한다.

 

#텐서플로우 dataset을 이용하여 셔플(shuffle)을 수행하되, 배치 크기로 데이터를 묶는다.
#또한 이 과정에서 교사 강요(teacher forcing)을 사용하기 위해서 디코더의 입력과 실제 값 시퀀스를 구성한다.
BATCH_SIZE=64
BUFFER_SIZE=20000

#디코더의 실제값 시퀀스에서는 시작 토큰을 제거해야 한다.
dataset=tf.data.Dataset.from_tensor_slices((
    {
        'inputs':questions,
        'dec_inputs':answers[:,:-1] #디코더의 입력. 마지막 패딩 토큰이 제거된다.
    },
    {
      'outputs':answers[:,1:]  
    },
))

dataset=dataset.cache()
dataset=dataset.shuffle(BUFFER_SIZE)
dataset=dataset.batch(BATCH_SIZE)
dataset=dataset.prefetch(tf.data.experimental.AUTOTUNE)
#임의의 샘플에 대해서 [:,:-1] 과 [:,1:]이 어떤 의미를 가지는지 테스트해본다.
#기존 샘플
print(answers[0]) 
#마지막 패딩 토큰 제거하면서 길이가 39가 된다
print(answers[:1][:,:-1]) 
#맨 처음 토큰이 제거된다. 다시 말해 시작 토큰이 제거된다. 길이는 역시 39가 된다
print(answers[:1][:,1:]) 

 

트랜스포머 만들기

하이퍼파라미터를 조정하여 실제 논문의 트랜스포머보다는 작은 모델을 만든다.

주요 하이퍼파라미터는 다음과 같다.

tf.keras.backend.clear_session()

#Hyper-parameters
D_MODEL=256
NUM_LAYERS=2
NUM_HEADS=8
DFF=512
DROPOUT=0.1

model=transformer(
    vocab_size=VOCAB_SIZE,
    num_layers=NUM_LAYERS,
    dff=DFF,
    d_model=D_MODEL,
    num_heads=NUM_HEADS,
    dropout=DROPOUT)

learning rate, optimizer을 정의하고 compile 해보자

learning_rate=CustomSchedule(D_MODEL)
optimizer=tf.keras.optimizers.Adam(learning_rate,beta_1=0.9,beta_2=0.98,epsilon=1e-9)

def accuracy(y_true,y_pred):
    #레이블의 크기는 (batch_size, MAX_LENGTH-1)
    y_true=tf.reshape(y_true,shape=(-1,MAX_LENGTH-1))
    return tf.keras.metrics.sparse_categorical_accuracy(y_true,y_pred)

model.compile(optimizer=optimizer,loss=loss_function,metrics=[accuracy])
EPOCHS=50
model.fit(dataset,epochs=EPOCHS)
Epoch 1/50
185/185 [==============================] - 272s 1s/step - loss: 1.4499 - accuracy: 0.0304
Epoch 2/50
185/185 [==============================] - 256s 1s/step - loss: 1.1818 - accuracy: 0.0495
Epoch 3/50
185/185 [==============================] - 273s 1s/step - loss: 1.0029 - accuracy: 0.0507
Epoch 4/50
185/185 [==============================] - 276s 1s/step - loss: 0.9276 - accuracy: 0.0547
Epoch 5/50
185/185 [==============================] - 263s 1s/step - loss: 0.8703 - accuracy: 0.0575
										.
                                        .
                                        .
Epoch 45/50
185/185 [==============================] - 204s 1s/step - loss: 0.0068 - accuracy: 0.1734
Epoch 46/50
185/185 [==============================] - 206s 1s/step - loss: 0.0068 - accuracy: 0.1735
Epoch 47/50
185/185 [==============================] - 206s 1s/step - loss: 0.0062 - accuracy: 0.1736
Epoch 48/50
185/185 [==============================] - 203s 1s/step - loss: 0.0063 - accuracy: 0.1736
Epoch 49/50
185/185 [==============================] - 204s 1s/step - loss: 0.0058 - accuracy: 0.1737
Epoch 50/50
185/185 [==============================] - 204s 1s/step - loss: 0.0058 - accuracy: 0.1737
<tensorflow.python.keras.callbacks.History at 0x2076df63ca0>

챗봇 평가

def evaluate(sentence):
    sentence = preprocess_sentence(sentence)

    sentence = tf.expand_dims(
      START_TOKEN + tokenizer.encode(sentence) + END_TOKEN, axis=0)

    output = tf.expand_dims(START_TOKEN, 0)

  # 디코더의 예측 시작
    for i in range(MAX_LENGTH):
        predictions = model(inputs=[sentence, output], training=False)

    # 현재(마지막) 시점의 예측 단어를 받아온다.
        predictions = predictions[:, -1:, :]
        predicted_id = tf.cast(tf.argmax(predictions, axis=-1), tf.int32)

    # 만약 마지막 시점의 예측 단어가 종료 토큰이라면 예측을 중단
        if tf.equal(predicted_id, END_TOKEN[0]):
            break

    # 마지막 시점의 예측 단어를 출력에 연결한다.
    # 이는 for문을 통해서 디코더의 입력으로 사용될 예정이다.
        output = tf.concat([output, predicted_id], axis=-1)

    return tf.squeeze(output, axis=0)
def predict(sentence):
    prediction = evaluate(sentence)

    predicted_sentence = tokenizer.decode(
      [i for i in prediction if i < tokenizer.vocab_size])

    print('Input: {}'.format(sentence))
    print('Output: {}'.format(predicted_sentence))

    return predicted_sentence 
def preprocess_sentence(sentence):
    sentence = re.sub(r"([?.!,])", r" \1 ", sentence)
    sentence = sentence.strip()
    return sentence
output = predict("영화 볼래?")
Input: 영화 볼래?
Output: 최신 영화가 좋을 것 같아요.

output=predict("머신러닝이란..")
Input: 머신러닝이란..
Output: 축하할 일이죠.

output=predict("너 불굴의관돌이 알아?")
Input: 너 불굴의관돌이 알아?
Output: 저도 쉬고 놀고 하고 싶어요.

output=predict("힘들구나")
Input: 힘들구나
Output: 건강에 유의하세요.

대략 4개정도 확인해보았지만 어느정도는 대답을 해주는 것을 볼 수 있다. 위 과정은 간단하게 만든 것이기에 이정도이지만, 실제 데이터를 더 많이 늘려서 시행해본다면 더 좋은 결과를 나을 수 있다고 생각해본다.

 

 

 

 

 

[출처]https://wikidocs.net/89786

 

 

우선 간단한 이미지분류기를 하기 위해 앞서 포스팅한 전이학습에 대한 이해가 있어야 한다. 

링크를 통해 먼저 학습하고 다음 글을 이해하자.

https://undeadkwandoll.tistory.com/22

 

Transfer Learning(전이 학습)

"Transfer learning(TL) is a research problem in machine learning(ML) that focuses on storing knowledge gained while solving one problem and applying it to a different but related problem" [Definitio..

undeadkwandoll.tistory.com

 

마동석, 이병헌, 김종국 분류기를 만들어보자.

 

import matplotlib
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm

fontpath='/usr/share/fonts/truetype/nanum/NanumBarunGothic.ttf'
font=fm.FontProperties(fname=fontpath, size=10)
plt.rc('font',family='NanumBarunGothic')
matplotlib.font_manager._rebuild()

import os
os.environ['KMP_DUPLICATE_LIB_OK']='True'

matplotlib는 누구나 알듯 그래픽 출력 라이브러리.

한국어 출력을 위한 font 설정.

!git clone https://github.com/ndb796/bing_image_downloader

fatal: destination path 'bing_image_downloader' already exists and is not an empty directory.

위 github 사이트에서 필요한 이미지 다운로더 라이브러리 다운.

 

import shutil
from bing_image_downloader.bing_image_downloader import downloader

그 후, 라이브러리 import.

 

 

directory_list=['C:/data/train/',
               'C:/data/test/',]

#초기 데릭토리 만들기
for directory in directory_list:
    if not os.path.isdir(directory):
        os.makedirs(directory)
        
#수집한 이미지를 학습 데이터와 평가 데이터로 구분하는 함수
def dataset_split(query,train_cnt):
    for directory in directory_list:
        if not os.path.isdir(directory+'/'+query):
            os.makedirs(directory+'/'+query)
    cnt=0
    for file_name in os.listdir(query):
        if cnt<train_cnt:
            print(f'[Train Dataset]{file_name}')
            shutil.move(query+'/'+file_name,'C:/data/train/'+query+'/'+file_name)
        else:
            print(f'[Test Dataset]{file_name}')
            shutil.move(query+'/'+file_name,'C:/data/test/'+query+'/'+file_name)
        cnt+=1
    shutil.rmtree(query)
            

 

 

 

초기 본인이 원하는 디렉토리를 지정하여, test,train data 디렉토리를 지정해준다. 그 후 우리가 받아야 할 이미지 파일의 저장경로를 지정해준다.

 

query='마동석'
downloader.download(query,limit=40,output_dir='./',adult_filter_off=True,force_replace=False,timeout=60)
dataset_split(query,30)

query = '김종국'
downloader.download(query, limit=40,  output_dir='./', adult_filter_off=True, force_replace=False, timeout=60)
dataset_split(query, 30)

query = '이병헌'
downloader.download(query, limit=40,  output_dir='./', adult_filter_off=True, force_replace=False, timeout=60)
dataset_split(query, 30)

 

위 코드를 통해, 마동석, 김종국, 이병헌에 대한 이미지 파일을 다운로드 한다.

 

import torch
import torch.nn as nn
import torch.optim as optim

import torchvision
from torchvision import datasets, models,transforms
import numpy as np
import time

device=torch.device("cuda:0" if torch.cuda.is_available() else 'cpu')

 

GPU 사용을 최소화하기 위해 , 다음과같이 torch를 import 하여 모델을 학습시키고, Flask Server를 통해 이미지를 주고받는 식으로 사용하는 방법이다.

 

# 데이터셋을 불러올 때 사용할 변형(transformation) 객체 정의
transforms_train = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.RandomHorizontalFlip(), # 데이터 증진(augmentation)
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) # 정규화(normalization)
])

transforms_test = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])

data_dir = 'C:/data'
train_datasets = datasets.ImageFolder(os.path.join(data_dir, 'train'), transforms_train)
test_datasets = datasets.ImageFolder(os.path.join(data_dir, 'test'), transforms_test)

train_dataloader = torch.utils.data.DataLoader(train_datasets, batch_size=4, shuffle=True, num_workers=4)
test_dataloader = torch.utils.data.DataLoader(test_datasets, batch_size=4, shuffle=True, num_workers=4)

print('학습 데이터셋 크기:', len(train_datasets))
print('테스트 데이터셋 크기:', len(test_datasets))

class_names = train_datasets.classes
print('클래스:', class_names)


학습 데이터셋 크기: 90
테스트 데이터셋 크기: 32
클래스: ['김종국', '마동석', '이병헌']

train data와 test data에 대해 transformation을 시킨 후 , 정규화 하여 pytorch tensor에 맞게 변형시켜준다.

 

이후, 확인해 보기 위한 코드를 진행한다.

def imshow(input, title):
    # torch.Tensor를 numpy 객체로 변환
    input = input.numpy().transpose((1, 2, 0))
    # 이미지 정규화 해제하기
    mean = np.array([0.485, 0.456, 0.406])
    std = np.array([0.229, 0.224, 0.225])
    input = std * input + mean
    input = np.clip(input, 0, 1)
    # 이미지 출력
    plt.imshow(input)
    plt.title(title)
    plt.show()


# 학습 데이터를 배치 단위로 불러오기
iterator = iter(train_dataloader)

# 현재 배치를 이용해 격자 형태의 이미지를 만들어 시각화
inputs, classes = next(iterator)
out = torchvision.utils.make_grid(inputs)
imshow(out, title=[class_names[x] for x in classes])

model = models.resnet34(pretrained=True)
num_features = model.fc.in_features
# 전이 학습(transfer learning): 모델의 출력 뉴런 수를 3개로 교체하여 마지막 레이어 다시 학습
model.fc = nn.Linear(num_features, 3)
model = model.to(device)

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
num_epochs = 50
model.train()
start_time = time.time()

# 전체 반복(epoch) 수 만큼 반복하며
for epoch in range(num_epochs):
    running_loss = 0.
    running_corrects = 0

    # 배치 단위로 학습 데이터 불러오기
    for inputs, labels in train_dataloader:
        inputs = inputs.to(device)
        labels = labels.to(device)

        # 모델에 입력(forward)하고 결과 계산
        optimizer.zero_grad()
        outputs = model(inputs)
        _, preds = torch.max(outputs, 1)
        loss = criterion(outputs, labels)

        # 역전파를 통해 기울기(gradient) 계산 및 학습 진행
        loss.backward()
        optimizer.step()

        running_loss += loss.item() * inputs.size(0)
        running_corrects += torch.sum(preds == labels.data)

    epoch_loss = running_loss / len(train_datasets)
    epoch_acc = running_corrects / len(train_datasets) * 100.

    # 학습 과정 중에 결과 출력
    print('#{} Loss: {:.4f} Acc: {:.4f}% Time: {:.4f}s'.format(epoch, epoch_loss, epoch_acc, time.time() - start_time))
#0 Loss: 1.0096 Acc: 51.1111% Time: 25.6392s
#1 Loss: 0.4748 Acc: 80.0000% Time: 54.3030s
#2 Loss: 0.5483 Acc: 80.0000% Time: 84.5246s
#3 Loss: 0.5021 Acc: 81.1111% Time: 118.2342s
#4 Loss: 0.2609 Acc: 90.0000% Time: 150.8176s
#5 Loss: 0.1877 Acc: 91.1111% Time: 184.1703s
#6 Loss: 0.1991 Acc: 92.2222% Time: 214.8244s
#7 Loss: 0.1990 Acc: 95.5556% Time: 245.0987s
#8 Loss: 0.3073 Acc: 84.4444% Time: 275.1495s
#9 Loss: 0.1584 Acc: 93.3333% Time: 304.9051s
#10 Loss: 0.2887 Acc: 94.4444% Time: 335.1631s
#11 Loss: 0.2231 Acc: 92.2222% Time: 365.2182s
#12 Loss: 0.2435 Acc: 90.0000% Time: 397.8394s
#13 Loss: 0.2753 Acc: 87.7778% Time: 428.1891s
#14 Loss: 0.4128 Acc: 93.3333% Time: 458.8629s
#15 Loss: 0.4522 Acc: 86.6667% Time: 488.5716s
#16 Loss: 0.0865 Acc: 97.7778% Time: 518.9268s
#17 Loss: 0.2886 Acc: 92.2222% Time: 550.2028s
#18 Loss: 0.5119 Acc: 77.7778% Time: 580.1998s
#19 Loss: 0.4541 Acc: 88.8889% Time: 612.7564s
#20 Loss: 0.0438 Acc: 98.8889% Time: 643.0622s
#21 Loss: 0.1214 Acc: 96.6667% Time: 673.6289s
#22 Loss: 0.0683 Acc: 97.7778% Time: 703.9003s
#23 Loss: 0.0541 Acc: 98.8889% Time: 734.5212s
#24 Loss: 0.1325 Acc: 95.5556% Time: 764.8524s
#25 Loss: 0.4077 Acc: 90.0000% Time: 795.4360s
#26 Loss: 0.0673 Acc: 98.8889% Time: 826.0624s
#27 Loss: 0.0974 Acc: 95.5556% Time: 857.2442s
#28 Loss: 0.2435 Acc: 92.2222% Time: 887.8970s
#29 Loss: 0.1925 Acc: 95.5556% Time: 919.2276s
#30 Loss: 0.2115 Acc: 95.5556% Time: 950.5770s
#31 Loss: 0.0892 Acc: 96.6667% Time: 980.9778s
#32 Loss: 0.0289 Acc: 98.8889% Time: 1011.7911s
#33 Loss: 0.0399 Acc: 98.8889% Time: 1042.0252s
#34 Loss: 0.0207 Acc: 100.0000% Time: 1072.0983s
#35 Loss: 0.0575 Acc: 96.6667% Time: 1102.7425s
#36 Loss: 0.1135 Acc: 96.6667% Time: 1133.0551s
#37 Loss: 0.1599 Acc: 96.6667% Time: 1162.9883s
#38 Loss: 0.1392 Acc: 95.5556% Time: 1193.0512s
#39 Loss: 0.4575 Acc: 88.8889% Time: 1223.0743s
#40 Loss: 0.1242 Acc: 95.5556% Time: 1253.0653s
#41 Loss: 0.2291 Acc: 93.3333% Time: 1283.8887s
#42 Loss: 0.2359 Acc: 92.2222% Time: 1314.8435s
#43 Loss: 0.0434 Acc: 97.7778% Time: 1345.1724s
#44 Loss: 0.1595 Acc: 96.6667% Time: 1375.3669s
#45 Loss: 0.0501 Acc: 98.8889% Time: 1406.0365s
#46 Loss: 0.0097 Acc: 100.0000% Time: 1437.1793s
#47 Loss: 0.0804 Acc: 98.8889% Time: 1467.5719s
#48 Loss: 0.0749 Acc: 97.7778% Time: 1498.2033s
#49 Loss: 0.0100 Acc: 100.0000% Time: 1528.7953s
model.eval()
start_time = time.time()

with torch.no_grad():
    running_loss = 0.
    running_corrects = 0

    for inputs, labels in test_dataloader:
        inputs = inputs.to(device)
        labels = labels.to(device)

        outputs = model(inputs)
        _, preds = torch.max(outputs, 1)
        loss = criterion(outputs, labels)

        running_loss += loss.item() * inputs.size(0)
        running_corrects += torch.sum(preds == labels.data)

        # 한 배치의 첫 번째 이미지에 대하여 결과 시각화
        print(f'[예측 결과: {class_names[preds[0]]}] (실제 정답: {class_names[labels.data[0]]})')
        imshow(inputs.cpu().data[0], title='예측 결과: ' + class_names[preds[0]])

    epoch_loss = running_loss / len(test_datasets)
    epoch_acc = running_corrects / len(test_datasets) * 100.
    print('[Test Phase] Loss: {:.4f} Acc: {:.4f}% Time: {:.4f}s'.format(epoch_loss, epoch_acc, time.time() - start_time))

[예측 결과: 이병헌] (실제 정답: 이병헌)

[예측 결과: 김종국] (실제 정답: 김종국)

[예측 결과: 마동석] (실제 정답: 마동석)

[예측 결과: 이병헌] (실제 정답: 이병헌)

 

 

 

 

[출처]www.youtube.com/watch?v=Lu93Ah2h9XA

 

+ Recent posts