파이썬 표준라이브러리만으로 코드 구현하기

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]

[['12311_10', 1, 77.70179894076985, 0.6557850948318409],
['8348_2', 0, -99.60524984646412, 0.4754283296904748],
['5828_4', 0, -0.2079441541680142, 0.5765352887259396],
['7186_2', 0, -3.2577917486317594, 0.5734329831488402],
['12128_7', 1, 22.66591280431018, 0.5998025805541847],
['2913_8', 1, 68.48294143932256, 0.6464076711556087],
['4396_1', 0, -36.2515975432851, 0.5398716773602201],
['395_2', 0, -1.3169796430639447, 0.5754071776069943],
['10616_1', 0, -87.47517418666499, 0.48776704505393825],
['9074_9', 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)을 봐야할 것이다.

강의에 등록된 질문이 없습니다. 궁금한 부분이 있으면 주저하지 말고 무엇이든 물어보세요.