갈루아의 반서재

텍스트 분류를 위한 나이브 베이즈 (2) - 분류기 훈련 및 성능평가


나이브 베이즈 분류기 훈련


sklearn.naive_bayes 모듈의 MultinomialNB 클래스와 3개의 벡터라이저를 각각 복합해 서로 다른 3개의 분류기를 만들고 기본 매개변수를 사용해 어떤 것이 더 낫게 수행하는지 비교한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> from sklearn.naive_bayes import MultinomialNB
>>> from sklearn.pipeline import Pipeline
>>> from sklearn.feature_extraction.text import TfidfVectorizer, >>> HashingVectorizer, CountVectorizer
>>>
>>> clf_1 = Pipeline([
>>>     ('vect', CountVectorizer()),
>>>     ('clf', MultinomialNB()),
>>> ])
>>>
>>> clf_2 = Pipeline([
>>>     ('vect', HashingVectorizer(non_negative=True)),
>>>     ('clf', MultinomialNB()),
>>> ])
>>>
>>> clf_3 = Pipeline([
>>>     ('vect', TfidfVectorizer()),
>>>     ('clf', MultinomialNB()),
>>> ])
cs



분류기와 특화된 X, y 값을 입력받아 K-중첩 교차 검증을 수행하는 함수를 정의한다.

1
2
3
4
5
6
7
8
9
10
>>> from sklearn.cross_validation import cross_val_score, KFold
>>> from scipy.stats import sem
>>> 
>>> def evaluate_cross_validation(clf, X, y, K):
>>>     # k=5 인 중첨 교차 검증 생성기를 만든다
>>>     cv = KFold(len(y), K, shuffle=True, random_state=0)
>>>     # 기본적으로 점수 함수는 에스터메이터의 점수 함수로 반환된 함수를 사용한다(정확도).
>>>     scores = cross_val_score(clf, X, y, cv=cv)
>>>     print scores
>>>     print ("Mean score: {0:.3f} (+/-{1:.3f})").format(np.mean(scores), sem(scores))
cs


각 분류기에 5-중첩 교차 검증을 수행한다. 결과는 다음과 같다. 결과에서 보는대로 CountVectorizer 와 TfidfVectorizer 는 HashingVectorizer 보다 뛰어나며 서로 비슷한 성능을 보이고 있다.

1
2
3
4
5
6
7
8
9
10
>>> clfs = [clf_1, clf_2, clf_3]
>>> for clf in clfs:
>>>     evaluate_cross_validation(clf, news.data, news.target, 5)
 
0.85782493  0.85725657  0.84664367  0.85911382  0.8458477 ]
Mean score: 0.853 (+/-0.003)
0.75543767  0.77659857  0.77049615  0.78508888  0.76200584]
Mean score: 0.770 (+/-0.005)
0.84482759  0.85990979  0.84558238  0.85990979  0.84213319]
Mean score: 0.850 (+/-0.004)
cs



그럼 TfidfVectorizer 을 가지고 정규표현식으로 텍스트를 파싱해 결과를 향상시켜보자. 기본 정규표현식: ur"\b\w\w+\b" 는 알파벳 문자와 _ 를 고려한다. / 와 . 에 대한 고려는 토큰화를 향상시킬 수 있고, Wi-Fi 와 site.com 과 같은 토큰을 고려하기 시작한다. 새로운 정규표현식은 ur"\b[a-z0-9_\-\.]+[a-z][a-z0-9_\-\.]+\b" 가 된다. 정규표현식 정의에 대한 질문이 있다면 파이썬 re 모듈 문서를 참고한다. 새로운 분류기로 시도해보자.

1
2
3
4
5
6
7
8
9
10
11
>>> clf_4 = Pipeline([
>>>     ('vect', TfidfVectorizer(
>>>        token_pattern=ur"\b[a-z0-9_\-\.]+[a-z][a-z0->>> 9_\-\.]+\b",
>>>     )),
>>>     ('clf', MultinomialNB()),
>>> ])
>>>
>>> evaluate_cross_validation(clf_4, news.data, news.target, 5)
 
0.80450928  0.81029451  0.81082515  0.82143805  0.80817193]
Mean score: 0.811 (+/-0.003)
cs


사용할 수 있는 또 다른 매개변수는 stop_words 이다. 이 값은 너무 빈도가 높은 단어이거나 특정 주제의 정보를 제공하지 않는 단어와 같은 고려할 필요가 없는 단어의 리스트를 전달한다. 다음과 같이 텍스트 파일에서 stop_words 를 얻는 함수를 정의한다.

1
2
3
4
5
>>> def get_stop_words():
>>>     result = set()
>>>     for line in open('stopwords_en.txt''r').readlines():
>>>         result.add(line.strip())
>>>     return result
cs

Stop Word List 는 아래의 링크 등을 참고해서 생성한다. 

http://www.lextek.com/manuals/onix/stopwords2.html


다음과 같이 새로운 매개변수로 새로운 분류기를 생성한다. 0.89 로 결과치가 향상되었다.

1
2
3
4
5
6
7
8
9
10
11
12
>>> clf_5 = Pipeline([
>>>     ('vect', TfidfVectorizer(
>>>                 stop_words= get_stop_words(),
>>>                 token_pattern=ur"\b[a-z0-9_\-\.]+[a-z][a-z0-9_\-\.]+\b",    
>>>     )),
>>>     ('clf', MultinomialNB()),
>>> ])
 
evaluate_cross_validation(clf_5, news.data, news.target, 5)
 
0.88222812  0.89599363  0.88591138  0.89599363  0.88485009]
Mean score: 0.889 (+/-0.003)
cs


이 벡터라이저를 가지고 MultinomialNB 의 매개변수를 확인하자. 이 분류기는 변경할 수 있는 일부 매개변수를 가진다. 가장 중요한 것은 평활화를 조절하는 매개변수인 alpha 이다. 우선은 낮은 값인 0.01 로 설정한다. 결과는 0.89 에서 0.92 로 더 높아졌음을 볼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
>>> clf_7 = Pipeline([
>>>     ('vect', TfidfVectorizer(
>>>                 stop_words= get_stop_words(),
>>>                 token_pattern=ur"\b[a-z0-9_\-\.]+[a-z][a-z0-9_\-\.]+\b",         
>>>     )),
>>>     ('clf', MultinomialNB(alpha=0.01)),
>>> ])
>>> 
>>> evaluate_cross_validation(clf_7, news.data, news.target, 5)
 
0.92175066  0.92040329  0.91907668  0.92491377  0.91881136]
Mean score: 0.921 (+/-0.001)
cs


이제 테스트 데이터에 대해 모델의 성능을 평가해보자. 전체 훈련 데이터로 모델을 훈련하고, 훈련 데이터와 테스트 데이터로 정확도를 평가하는 도움 함수를 만든다. 이 함수는 분류 보고서와 해당 혼돈 매트릭스를 출력한다. 최적의 분류기를 평가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> from sklearn import metrics
>>> 
>>> def train_and_evaluate(clf, X_train, X_test, y_train, y_test):
>>>     
>>>     clf.fit(X_train, y_train)
>>>     
>>>     print "Accuracy on training set:"
>>>     print clf.score(X_train, y_train)
>>>     print "Accuracy on testing set:"
>>>     print clf.score(X_test, y_test)    
>>>     y_pred = clf.predict(X_test)
>>>     
>>>     print "Classification Report:"
>>>     print metrics.classification_report(y_test, y_pred)
>>>     print "Confusion Matrix:"
>>>     print metrics.confusion_matrix(y_test, y_pred)
cs


1
2
3
4
5
6
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
>>> train_and_evaluate(clf_7, X_train, X_test, y_train, y_test)
 
Accuracy on training set:
0.996957690675
Accuracy on testing set:
0.91935483871
Classification Report:
             precision    recall  f1-score   support
 
          0       0.95      0.88      0.91       216
          1       0.86      0.85      0.85       246
          2       0.91      0.84      0.87       274
          3       0.82      0.86      0.84       235
          4       0.88      0.90      0.89       231
          5       0.89      0.92      0.90       225
          6       0.88      0.80      0.84       248
          7       0.92      0.93      0.93       275
          8       0.96      0.98      0.97       226
          9       0.97      0.94      0.96       250
         10       0.97      1.00      0.98       257
         11       0.96      0.97      0.97       261
         12       0.92      0.91      0.91       216
         13       0.94      0.96      0.95       257
         14       0.94      0.97      0.96       246
         15       0.91      0.97      0.94       234
         16       0.91      0.97      0.94       218
         17       0.97      0.99      0.98       236
         18       0.95      0.91      0.93       213
         19       0.87      0.80      0.83       148
 
avg / total       0.92      0.92      0.92      4712
 
Confusion Matrix:
[[190   0   0   0   1   0   0   0   0   1   0   0   0   1   0   9   2   0
    0  12]
 [  0 209   5   3   2  13   3   0   0   0   0   2   3   2   3   0   0   1
    0   0]
 [  0  11 230  22   1   5   1   0   1   0   0   0   0   0   1   0   1   0
    1   0]
 [  0   6   7 201  11   3   4   0   0   0   0   0   2   0   1   0   0   0
    0   0]
 [  0   2   3   3 209   1   5   0   0   0   2   0   5   0   1   0   0   0
    0   0]
 [  0   8   2   1   1 207   0   1   1   0   0   0   0   2   1   0   0   1
    0   0]
 [  0   2   4   9   6   0 199  14   1   2   1   1   4   2   2   0   0   1
    0   0]
 [  0   1   1   1   1   0   6 257   4   1   0   0   0   1   0   0   2   0
    0   0]
 [  0   0   0   0   0   1   1   2 221   0   0   0   0   1   0   0   0   0
    0   0]
 [  0   0   0   0   0   0   1   0   2 236   5   0   1   3   0   1   1   0
    0   0]
 [  0   0   0   1   0   0   0   0   0   0 256   0   0   0   0   0   0   0
    0   0]
 [  0   0   0   0   0   1   0   1   0   0   0 254   0   1   0   0   3   0
    1   0]
 [  0   1   0   2   5   1   3   1   0   2   1   1 196   1   2   0   0   0
    0   0]
 [  0   1   0   1   1   0   0   0   0   0   0   2   1 246   3   0   1   0
    0   1]
 [  0   2   0   0   0   0   0   1   0   0   0   0   0   1 239   0   1   0
    1   1]
 [  1   0   1   2   0   0   0   1   0   0   0   1   0   0   1 226   0   1
    0   0]
 [  0   0   1   0   0   0   1   0   1   0   0   1   0   0   0   0 211   0
    3   0]
 [  0   1   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0 234
    1   0]
 [  1   0   0   0   0   0   1   0   0   0   0   2   1   1   0   1   7   3
  193   3]
 [  8   0   0   0   0   1   0   0   0   1   0   0   0   0   0  11   4   1
    4 118]]
cs

벡터라이저 내부를 보면 딕셔너리를 만든 토큰을 볼 수 있다.


딕셔너리는 145,598개의 토큰으로 이루어져 있다. 속성 이름을 출력해보자.

1
2
3
>>> print len(clf_7.named_steps['vect'].get_feature_names())
 
145598
cs


의미적으로 같은 일부 단어를 볼 수 있다. 이를테면, sand 와 sands, sanctuaries 와 sanctuary 이다. 복수형 단어와 단수형 단어를 같은 단어로 처리한다면 문서를 좀 더 좋게 대표할 수 있을 것이다. 이 작업은 같은 어근을 갖는 두 단어의 어근 추출stemming을 사용해 해결한다(stemming 에 관해서는 http://nltk.org 를 참조한다).

1
>>> clf_7.named_steps['vect'].get_feature_names()
cs


1
2
3
4
5
6
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
36
37
38
39
40
41
42
43
44
45
46
47
[u'0-.66d8wt',
 u'0-04g55',
 u'0-100mph',
 u'0-13-117441-x--or',
 u'0-3mb',
 u'0-40mb',
 u'0-40volts',
 u'0-5mb',
 u'0-60mph',
 u'0-8.3mb',
 u'0-a00138',
 u'0-byte',
 u'0-defects',
 u'0-e8',
 u'0-for-4',
 u'0-hc',
 u'0-ii',
 u'0-uw',
 u'0-uw0',
 u'0-uw2',
 u'0-uwa',
 u'0-uwt',
 u'0-uwt7',
 u'0-uww',
 u'0-uww7',
 u'0.-w0',
 u'0..x-1',
 u'0.00...nice',
 u'0.02cents',
 u'0.0cb',
 u'0.1-ports',
 u'0.15mb',
 u'0.2d-_',
 u'0.5db',
 u'0.6-micron',
 u'0.65mb',
 u'0.97pl4',
 u'0.b34s_',
 u'0.c0rgo5kj7pp0',
 u'0.c4',
 u'0.jy',
 u'0.s_',
 u'0.tprv6ekj7r',
 u'0.tt',
 u'0.txa_',
 u'0.txc',
 u'0.vpp',
cs