파이썬 표준라이브러리만으로 코드 구현하기
이 노트북은 아래에 있는 MLWave에 올라온 글을 한국어로 번역하고 아래 출처의 소스코드를 더해 몇 가지 내용을 추가한 것입니다.
Online Learning Perceptron From MLWave
소스코드 출처
- https://github.com/MLWave/online-learning-perceptron
- https://github.com/MLWave/online-learning-perceptron/blob/master/perceptron.py
- 위 소스코드를 Python3에서 실행되도록 일부 수정하였다.
import re # 정규표현식을 사용해 텍스트 데이터를 정제하기 위해
import random # 랜덤 숫자를 생성하기 위해
from math import exp, log # 지수함수와 로그함수를 사용하기 위해
from datetime import datetime # 시간을 계산하기 위해
from operator import itemgetter # 키가 아닌 값으로 max, min 값을 구할 때 사용
def clean(s):
"""
Returns a cleaned, lowercased string
텍스트 데이터를 정제하고 소문자로 변환해 준다.
"""
return " ".join(re.findall(r'\w+', s, flags = re.UNICODE)).lower()
def get_data_tsv(loc_dataset,opts):
"""
Running through data in an online manner
Parses a tsv file for this competition
and yields label, identifier and features
output:
label: int, The label / target (set to "1" if test set)
id: string, the sample identifier
features: list of tuples,
in the form [(hashed_feature_index,feature_value)]
온라인 학습 방법을 통해 데이터를 실행한다.
tsv파일을 통해 레이블, identifier, 피처(특성)를 파싱한다.
결과물:
label : int, 레이블 / 대상 (테스트 집합 인 경우 "1"로 설정)
id : 문자열, 샘플 식별자
features : [(hashed_feature_index, feature_value)]
형식의 튜플 목록
"""
for e, line in enumerate(open(loc_dataset,"rb")):
if e > 0:
r = line.decode('utf-8').strip().split("\t")
id = r[0]
if opts["clean"]:
try:
r[2] = clean(r[2])
except:
r[1] = clean(r[1])
# opts["D"] = 2 ** 25 = 33554432
# Vowpal Wabbit의 해싱트릭을 사용한다.
# 해싱트릭은 큰 규모의 feature공간을
# 고정크기의 표현을 사용해 저장할 수 있게 한다.
if len(r) == 3: # train set
features = [(hash(f)%opts["D"],1) for f in r[2].split()]
label = int(r[1])
else: #test set
features = [(hash(f)%opts["D"],1) for f in r[1].split()]
label = 1
# bigram을 사용하면 해당 피처[i]와 다음피처[i+1]를 함께 해싱한다.
if opts["2grams"]:
for i in range(len(features)-1):
features.append(
(hash(str(features[i][0])+str(features[i+1][0]))%opts["D"],1))
yield label, id, features
def dot_product(features,weights):
"""
Calculate dot product from features and weights
input:
features: A list of tuples [(feature_index,feature_value)]
weights: the hashing trick weights filter,
note: length is max(feature_index)
output:
dotp: the dot product
피처(특성)과 가중치로부터 내적을 구한다.
"""
dotp = 0
for f in features:
dotp += weights[f[0]] * f[1]
return dotp
def train_tron(loc_dataset,opts):
start = datetime.now()
print("\nPass\t\tErrors\t\tAverage\t\tNr. Samples\tSince Start")
# 가중치 초기화
if opts["random_init"]:
random.seed(3003)
weights = [random.random()] * opts["D"]
else:
weights = [0.] * opts["D"]
# Running training passes
# 학습 실행
for pass_nr in range(opts["n_passes"]):
error_counter = 0
for e, (label, id, features) in enumerate( \
get_data_tsv(loc_dataset,opts) ):
# 퍼셉트론은 지도학습 분류기의 일종이다.
# 이전 값에 대한 학습으로 예측을 한다.
# 내적(dotproduct) 값이 임계 값보다 높거나 낮은지에 따라
# 초과하면 "1"을 예측하고 미만이면 "0"을 예측한다.
dp = dot_product(features, weights) > 0.5
# 다음 perceptron은 샘플의 레이블을 본다.
# 실제 레이블 데이터에서 위 퍼셉트론으로 구한 dp값을 빼준다.
# 예측이 정확하다면, error 값은 "0"이며, 가중치만 남겨 둔다.
# 예측이 틀린 경우 error 값은 "1" 또는 "-1"이고 다음과 같이 가중치를 업데이트 한다.
# weights[feature_index] += learning_rate * error * feature_value
error = label - dp
# 예측이 틀린 경우 퍼셉트론은 다음과 같이 가중치를 업데이트한다.
if error != 0:
error_counter += 1
# Updating the weights
for index, value in features:
weights[index] += opts["learning_rate"] * error * log(1.+value)
#Reporting stuff
print("%s\t\t%s\t\t%s\t\t%s\t\t%s" % ( \
pass_nr+1,
error_counter,
round(1 - error_counter /float(e+1),5),
e+1,datetime.now()-start))
#Oh heh, we have overfit :)
if error_counter == 0 or error_counter < opts["errors_satisfied"]:
print("%s errors found during training, halting"%error_counter)
break
return weights
def test_tron(loc_dataset,weights,opts):
"""
output:
preds: list, a list with
[id,prediction,dotproduct,0-1normalized dotproduct]
"""
start = datetime.now()
print("\nTesting online\nErrors\t\tAverage\t\tNr. Samples\tSince Start")
preds = []
error_counter = 0
for e, (label, id, features) in enumerate( \
get_data_tsv(loc_dataset,opts) ):
dotp = dot_product(features, weights)
# 내적이 0.5보다 크다면 긍정으로 예측한다.
dp = dotp > 0.5
if dp > 0.5: # we predict positive class
preds.append( [id, 1, dotp ] )
else:
preds.append( [id, 0, dotp ] )
# get_data_tsv에서 테스트 데이터의 레이블을 1로 초기화 해주었음
if label - dp != 0:
error_counter += 1
print("%s\t\t%s\t\t%s\t\t%s" % (
error_counter,
round(1 - error_counter /float(e+1),5),
e+1,
datetime.now()-start))
# normalizing dotproducts between 0 and 1
# 내적을 구해 0과 1로 일반화 한다.
# TODO: proper probability (bounded sigmoid?),
# online normalization
max_dotp = max(preds,key=itemgetter(2))[2]
min_dotp = min(preds,key=itemgetter(2))[2]
for p in preds:
# appending normalized to predictions
# 정규화 된 값을 마지막에 추가해 준다.
# (피처와 가중치에 대한 내적값 - 최소 내적값) / 최대 내적값 - 최소 내적값
# 이 값이 캐글에서 0.95의 AUC를 얻을 수 있는 값이다.
p.append((p[2]-min_dotp)/float(max_dotp-min_dotp))
#Reporting stuff
print("Done testing in %s"%str(datetime.now()-start))
return preds
#Setting options
opts = {}
opts["D"] = 2 ** 25
opts["learning_rate"] = 0.1
opts["n_passes"] = 80 # Maximum number of passes to run before halting
opts["errors_satisfied"] = 0 # Halt when training errors < errors_satisfied
opts["random_init"] = False # set random weights, else set all 0
opts["clean"] = True # clean the text a little
opts["2grams"] = True # add 2grams
#training and saving model into weights
%time weights = train_tron("data/labeledTrainData.tsv",opts)
Pass Errors Average Nr. Samples Since Start
1 5648 0.77408 25000 0:00:19.187437
2 3161 0.87356 25000 0:00:38.114154
3 2218 0.91128 25000 0:00:57.910578
4 1643 0.93428 25000 0:01:13.906263
5 1254 0.94984 25000 0:01:29.959013
6 1038 0.95848 25000 0:01:44.738919
7 805 0.9678 25000 0:01:59.575742
8 579 0.97684 25000 0:02:14.277759
9 513 0.97948 25000 0:02:29.356437
10 464 0.98144 25000 0:02:44.361049
11 367 0.98532 25000 0:02:59.225589
12 363 0.98548 25000 0:03:19.239056
13 231 0.99076 25000 0:03:35.903281
14 203 0.99188 25000 0:03:53.428992
15 160 0.9936 25000 0:04:08.202330
16 163 0.99348 25000 0:04:27.039214
17 144 0.99424 25000 0:04:43.598832
18 168 0.99328 25000 0:04:59.653083
19 99 0.99604 25000 0:05:16.492206
20 98 0.99608 25000 0:05:31.565497
21 127 0.99492 25000 0:05:46.293131
22 81 0.99676 25000 0:06:01.460219
23 73 0.99708 25000 0:06:17.186924
24 92 0.99632 25000 0:06:33.383587
25 96 0.99616 25000 0:06:50.064784
26 82 0.99672 25000 0:07:06.048313
27 41 0.99836 25000 0:07:21.237198
28 84 0.99664 25000 0:07:36.076427
29 75 0.997 25000 0:07:52.291855
30 66 0.99736 25000 0:08:08.568775
31 20 0.9992 25000 0:08:25.085804
32 27 0.99892 25000 0:08:41.198909
33 7 0.99972 25000 0:08:57.871054
34 46 0.99816 25000 0:09:13.094634
35 23 0.99908 25000 0:09:27.876432
36 7 0.99972 25000 0:09:42.681918
37 61 0.99756 25000 0:09:57.770814
38 7 0.99972 25000 0:10:15.091846
39 0 1.0 25000 0:10:30.445496
0 errors found during training, halting
CPU times: user 10min 17s, sys: 3.64 s, total: 10min 21s
Wall time: 10min 30s
# testing and saving predictions into preds
%time preds = test_tron("data/testData.tsv",weights,opts)
Testing online
Errors Average Nr. Samples Since Start
12731 0.49076 25000 0:00:17.228997
Done testing in 0:00:17.255413
CPU times: user 16.7 s, sys: 143 ms, total: 16.9 s
Wall time: 17.3 s
preds[:10]
[['"1231110"', 1, 77.70179894076985, 0.6557850948318409],
['"83482"', 0, -99.60524984646412, 0.4754283296904748],
['"58284"', 0, -0.2079441541680142, 0.5765352887259396],
['"71862"', 0, -3.2577917486317594, 0.5734329831488402],
['"121287"', 1, 22.66591280431018, 0.5998025805541847],
['"29138"', 1, 68.48294143932256, 0.6464076711556087],
['"43961"', 0, -36.2515975432851, 0.5398716773602201],
['"3952"', 0, -1.3169796430639447, 0.5754071776069943],
['"106161"', 0, -87.47517418666499, 0.48776704505393825],
['"90749"', 0, -23.497689420982187, 0.5528449552280901]]
# writing kaggle submission
# 캐글 점수 제출을 위한 서브미션 파일을 작성한다.
with open("data/submit_perceptron.csv","wb") as outfile:
outfile.write('"id","sentiment"\n'.encode('utf-8'))
for p in sorted(preds):
outfile.write("{},{}\n".format(p[0],p[3]).encode('utf-8'))
# 캐글 스코어 0.95338
129/578
0.2231833910034602
# 캐글에 제출할 때 바이너리 타입으로 제출하라고 되어 있는데 여기에서는 바이너리로 변환하지 않은 채 제출을 했다.
# 그런데 캐글에서 점수가 평가되고 심지어 높은 점수로!
# 그래서 다시 바이너리 형태의 예측값을 제출했더니 점수가 낮아졌다.
# 바이너리로 제출한 스코어가 훨씬 낮은 0.89132 가 나왔지만 튜토리얼에 있는 방법을 사용했을 때보다 훨씬 높은 스코어다.
# 머신러닝 패키지를 사용하지 않고 파이썬의 내장 기능을 통해서 이런 점수가 나올 수 있다는 것에 놀랐다.
import pandas as pd
presult = pd.DataFrame(preds)
presult.head()
0 | 1 | 2 | 3 | |
---|---|---|---|---|
0 | "12311_10" | 1 | 77.701799 | 0.655785 |
1 | "8348_2" | 0 | -99.605250 | 0.475428 |
2 | "5828_4" | 0 | -0.207944 | 0.576535 |
3 | "7186_2" | 0 | -3.257792 | 0.573433 |
4 | "12128_7" | 1 | 22.665913 | 0.599803 |
output_sentiment = presult[1].value_counts()
print(output_sentiment[0] - output_sentiment[1])
output_sentiment
462
0 12731
1 12269
Name: 1, dtype: int64
상위 벤치 마크
포럼에서 현재 가장 좋은 벤치 마크 는 Abhishek 의 로지스틱 회귀 스크립트다. 리더 보드에서 0.95 AUC 를 얻기 위해 학습과 테스트 세트의 메모리 안에서 tfidf-fit_transform을 사용한다. 여기에서는 이 변환을 건너 뛰고 온라인 해시 벡터 라이저를 사용한다.
Abhishek의 스크립트처럼 기능에서 2-gram("좋지 않은"등)을 생성한다. 2-gram을 단순히 해쉬하고 샘플 벡터에 추가한다.
또, 문자를 소문자로 바꾸고 영숫자가 아닌 것을 제거함으로써 텍스트를 조금 더 빠르게 정제한다.
예측
AUC를 최적화하기 위해 "1"또는 "0"예측만 제출하지 않는다. 이것은 약 0.88의 AUC를 줄 것이다. 여기에서는 0과 1 사이에서 정규화된 dotproduct를 제출한다.
이 스크립트는 Abhishek의 솔루션 0.952 점수와 비교해 본다. 하지만 이 코드는 단지 몇 MB의 메모리를 필요로하며, tfidf 변환을 수행하지 않고 단지 2분 만에 0의 학습오류에 수렴한다다.
텍스트 정제와 2-gram이 없으면 스크립트는 60초 이내에 실행되고 0.93의 점수를 산출한다.
결론
"절대적인 지식은 없다. 모든 것은 확률에 의해서만 이루어진다 " - Gödel (1961)
0-1 임계 값 활성화가 적용된 매우 기본적인 퍼셉트론은 NLP 경진대회에서 잘 동작한다. 여기에서는 1957 알고리즘을 전혀 변경하지 않았다. 해싱 트릭을 없애고 비슷하거나 약간 더 좋은 점수를 받을 수 있다.
이 스크립트는 확률을 출력 할 수 없다. dotproduct에 bounded sigmoid를 사용하여 출력의 온라인 정규화 형태를 얻거나 tinrtgu의 (Vowpal Wabbit)스크립트를 더 보완해 볼 수 있을 것이다.
단일 노드 단일 레이어 퍼셉트론은 비선형 함수를 모델링 할 수 없으므로 더 나은 NLP 출력을 얻으려면 FFN(feedforward nets), recurrent nets, self-organizing maps, MLP(Multi-layer Perceptrons), word2vec 및 extreme learning machine (백프로파게이션이 없는 fast ffnets)을 봐야할 것이다.