Сентимент-анализ отзывов на товары (простая версия)

Выполним классификацию отзывов по тональности на готовой обучающей выборке


In [4]:
from nltk.corpus import stopwords as nltk_stop_words
from nltk.corpus import words
from sklearn.feature_extraction.text import CountVectorizer 
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
#from sklearn.model_selection import cross_val_score
from sklearn.cross_validation import cross_val_score
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.svm import LinearSVC
from sklearn.linear_model import SGDClassifier
import pandas as pd
import datetime
import numpy as np
from time import time
from sklearn.grid_search import GridSearchCV

Определим необходимые функции. Чтение данных и разделение на обучающую и тестовую выборки.


In [5]:
def read_data():
    X_train = []
    X_test = []
    id_test = []
    y_train = []
    with open('products_sentiment_train.tsv') as f:
        for line in f:
            parts = line.rsplit('\t', 1)

            X_train.append(parts[0].strip())
            y_train.append(parts[1].strip())
    
    with open('products_sentiment_test.tsv') as f:
        f.readline()
        for line in f:
            parts = line.split('\t', 1)
            id_test.append(parts[0].strip())
            X_test.append(parts[1].strip())                    

    return X_train, y_train, id_test, X_test

Обучение и классификация выбранным классификатором, сохранение результатов в файл с временной меткой в названии для удобства.


In [6]:
def predict(predictor, data_train, y, id_test, data_test, cv_score=None):
    predictor.fit(data_train, y)
    prediction = predictor.predict(data_test)
    #print predictor
    timestamp = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
    filepath_prediction = 'data/prediction-%s-data.csv' % timestamp
    filepath_description = 'data/prediction-%s-estimator.txt' % timestamp

    # Create a dataframe with predictions and write it to CSV file   
    predictions_df = pd.DataFrame(data=prediction, columns=['y'])
    predictions_df.to_csv(filepath_prediction, sep=',', index_label='Id')

    # Write a short description of the classifier that was used
    f = open(filepath_description, 'w')
    f.write(str(predictor))
    score = '\nCross-validation score %.8f' % cv_score    
    f.write(score)
    f.close()

Далее описаны различные конфигурации для выделения признаков и классификации, использовавшиеся в экспериментах. Для каждой конфигурации также указывается набор параметров и перечень значений, использовавшихся для подбора наилучшего сочетания.

Вариант-1 с признаками на основе счетчика слов и классификация логистической регрессией.


In [37]:
def get_pipeline_and_params_1():
    pipeline = Pipeline([
        ('vect', CountVectorizer()),
        ('logreg', LogisticRegression()),
    ])
    parameters = {
        'vect__max_df': (0.6, 0.8, 1.0),
        'vect__min_df': (0, 1, 2, 5),
        'vect__stop_words': ('english', None),
        'vect__ngram_range': ((1, 1), (1, 2)),  # unigrams or bigrams        
        'logreg__C': (0.0001, 0.01, 1),
        'logreg__penalty': ('l2', 'l1'),        
    }
    return pipeline, parameters

Вариант-2 с признаками на основе частотности слов (TF-IDF) и классификация логистической регрессией.


In [41]:
def get_pipeline_and_params_2():
    pipeline = Pipeline([
        ('tfidf', TfidfVectorizer()),
        ('logreg', LogisticRegression()),
    ])
    parameters = {
        'tfidf__max_df': (0.6, 0.8, 1.0),
        'tfidf__min_df': (0, 5, 10, 15),
        'tfidf__ngram_range': ((1, 1), (1, 2), (1,3), (2,3)),  # unigrams or bigrams
        #'tfidf__use_idf': (True, False),
        'tfidf__norm': ('l1', 'l2'),   
        'logreg__C': (0.0001, 0.01, 1),
        'logreg__penalty': ('l2', 'l1'),  
    }
    return pipeline, parameters

Вариант-3 с признаками на основе частотности слов (TF-IDF) и классификатор на основе стохастического градиентного спуска.


In [28]:
def get_pipeline_and_params_3():
    # gives 0.7895 on cross-validation and 0.835 on Kaggle. done in 108.629s
    pipeline = Pipeline([
        ('vect', CountVectorizer()),
        ('tfidf', TfidfTransformer()),
        ('clf', SGDClassifier()),
    ])
    parameters = {
        'vect__max_df': (0.5, 0.75, 1.0),
        #'vect__max_features': (None, 5000, 10000, 50000),
        'vect__ngram_range': ((1, 3), (1, 2)),  # unigrams or bigrams
        #'tfidf__use_idf': (True, False),
        'tfidf__norm': ('l1', 'l2'),
        'clf__alpha': (0.00001, 0.000001),
        'clf__penalty': ('l2', 'elasticnet'),
        'clf__n_iter': (10, 50, 80),
    }
    return pipeline, parameters

Вариант-4 с признаками на основе частотности слов (TF-IDF) и классификация методом опорных векторов.


In [51]:
def get_pipeline_and_params_4():
    nltk_sw = nltk_stop_words.words('english')
    pipeline = Pipeline([
        ('vect', CountVectorizer()),
        ('tfidf', TfidfTransformer()),
        ('clf', LinearSVC()),
    ])
    parameters = {
        #'vect__max_df': (0.8, 0.9, 1),
        'vect__min_df': (0, 5, 10),
        #'vect__vocabulary': (None),
        'vect__stop_words': ('english', nltk_sw, None),
        'vect__max_features': (None, 5000, 10000, 50000),
        #'vect__analyzer' : ('word', 'char', 'char_wb',), 
        'vect__ngram_range': ((1,1), (1, 4), (1, 3)),  # unigrams or bigrams
        #'tfidf__use_idf': (True, False),
        #'tfidf__smooth_idf': (True, False),
        #'tfidf__norm': ('l1', 'l2'),
        #'tfidf__sublinear_tf': (True, False),
        #'clf__C': (0.00001, 0.001, 0.1, 1),        
    }
    return pipeline, parameters

Вариант-5 с признаками на основе частотности слов (TF-IDF) и классификатор на основе стохастического градиентного спуска с использванием корпуса английских слов, стоп-слов и анализатора частей слов (параметр "char_wb").


In [87]:
def get_pipeline_and_params_5():
    wordlist = set(words.words())  
    nltk_sw = nltk_stop_words.words('english')
    pipeline = Pipeline([
        ('vect', CountVectorizer(vocabulary=wordlist)),
        ('tfidf', TfidfTransformer()),
        ('clf', SGDClassifier()),
    ])
    parameters = {
        'vect__strip_accents': ('ascii', None),
        'vect__analyzer': ('word', 'char_wb'),
        'vect__max_df': (0.8, 1.0),
        #'vect__min_df': (0, 1, 2),
        #'vect__max_features': (100, 200, 500, 1000, 2000, 5000),
        'vect__vocabulary': (None, wordlist),
        'vect__stop_words': ('english', nltk_sw, None),
        'vect__ngram_range': ((1, 3), (1, 1), (1, 2)),
        'tfidf__use_idf': (True,),
        'tfidf__norm': ('l1',),
        'clf__alpha': (0.000001,),
        'clf__penalty': ('l1',),
        #'clf__n_iter': (10, 50, 80),
    }
    return pipeline, parameters

Функция перебора комбинаций параметров и определения наилучшей конфигурации с помощью GridSearchCV


In [30]:
def do_grid_search(pipeline, parameters, X_train, y_train):
    grid_search = GridSearchCV(pipeline, parameters, scoring='accuracy')
    t0 = time()
    grid_search.fit(X_train, y_train)
    print "done in %0.3fs" % (time() - t0)
    
    print("Best score: %.4f" % grid_search.best_score_)
    print("Best parameters set:")
    best_parameters = grid_search.best_estimator_.get_params()
    for param_name in sorted(parameters.keys()):
        print("\t%s: %r" % (param_name, best_parameters[param_name]))
    return grid_search

Основная фунция, описывающая отдельный эксперимент: сформировать тестируюмую конфигирацию и наборы параметров, выполнить перебор для определения наилучших и выполнить классификацию тестовых данных с помощью лучшего классификатора.


In [34]:
def do_experiment(X_train, y_train, id_test, X_test, get_pipeline_and_params):      
    pipeline, parameters = get_pipeline_and_params()
    gs = do_grid_search(pipeline, parameters, X_train, y_train)
    predict(gs.best_estimator_, X_train, y_train, id_test, X_test, gs.best_score_)

Чтение исходных данных


In [35]:
X_train, y_train, id_test, X_test = read_data()

Выполнение серии экспериментов, их результаты по точности при кросс-валидации и подобранные значения параметров.


In [38]:
do_experiment(X_train, y_train, id_test, X_test, get_pipeline_and_params_1)


done in 91.633s
Best score: 0.7740
Best parameters set:
	logreg__C: 1
	logreg__penalty: 'l2'
	vect__max_df: 0.8
	vect__min_df: 0
	vect__ngram_range: (1, 1)
	vect__stop_words: None

In [43]:
do_experiment(X_train, y_train, id_test, X_test, get_pipeline_and_params_2)


done in 303.685s
Best score: 0.7580
Best parameters set:
	logreg__C: 1
	logreg__penalty: 'l2'
	tfidf__max_df: 0.6
	tfidf__min_df: 0
	tfidf__ngram_range: (1, 1)
	tfidf__norm: 'l2'

In [64]:
do_experiment(X_train, y_train, id_test, X_test, get_pipeline_and_params_3)


done in 108.629s
Best score: 0.7895
Best parameters set:
	clf__alpha: 1e-06
	clf__n_iter: 10
	clf__penalty: 'l2'
	tfidf__norm: 'l1'
	vect__max_df: 0.5
	vect__ngram_range: (1, 3)

In [52]:
do_experiment(X_train, y_train, id_test, X_test, get_pipeline_and_params_4)


done in 54.368s
Best score: 0.7840
Best parameters set:
	vect__max_features: 10000
	vect__min_df: 0
	vect__ngram_range: (1, 3)
	vect__stop_words: None

In [89]:
do_experiment(X_train, y_train, id_test, X_test, get_pipeline_and_params_5)


done in 654.527s
Best score: 0.7850
Best parameters set:
	clf__alpha: 1e-06
	clf__penalty: 'l1'
	tfidf__norm: 'l1'
	tfidf__use_idf: True
	vect__analyzer: 'word'
	vect__max_df: 1.0
	vect__ngram_range: (1, 3)
	vect__stop_words: None
	vect__strip_accents: 'ascii'
	vect__vocabulary: None

Результаты при кросс-валидации на обучающей выборке, разумеется, отличались от результатов на тестовой выборке, полученных на сайте Kaggle.

На тестовых данных лучший результат показала конфигурация, описанная в варианте-3 - признаки по TF-IDF и классификация с помощью SGDClassifier. Результат на Kaggle составил 0.835.


In [ ]: