Я компьютерный ученый с большим опытом создания веб-сайтов, приложений и тому подобного; Однако мои навыки в машинном обучении и работе с данными оставляют желать лучшего. Поэтому, исследуя различные наборы данных и документируя свои методы, я надеюсь улучшить свои навыки работы с данными и научить других, кто может оказаться в таком же положении, как я! :)

Набор данных, который мы рассмотрим сегодня, представляет собой небольшой игрушечный набор, называемый набором Balance Scale. Это набор классификации, описывающий веса на шкале и предсказывающий, сбалансирована ли шкала. Причина, по которой я выбрал это: из-за своего небольшого размера и упрощенного характера, это хорошее начало для практики анализа и рассуждений. Кроме того, из-за зависимости класса от функций должно быть возможно легко достичь 100% точности (или близкой).

Набор данных: https://archive.ics.uci.edu/ml/datasets/Balance+Scale

Мы будем использовать следующие инструменты:

  • Python 3.4 (Анаконда)
  • Панды
  • Numpy
  • Scikit-Learn
  • Сиборн
  • Матплотлиб PyPlot

Подготовка

Данные доступны в формате CSV с пятью столбцами. Я решил добавить заголовки к каждому столбцу в первой строке файла, чтобы Pandas играл немного лучше. Итак, первые несколько рядов выглядят так:

 balance,left_weight,left_distance,right_weight,right_distance
 B,1,1,1,1
 R,1,1,1,2
 R,1,1,1,3
 …

Единственное, что нам нужно сделать, это векторизовать класс. Вот код для этого:

 import pandas as pd
 
 def load_data():
     """
     loading csv to pandas dataframe
 
     :return: dataframe
     """
     return pd.read_csv('data/data.txt', sep=',', header=0)
 
 def prepare_data(data):
     """
     vectorize balance

     :return: data with vectorized classes
     """
     data['balance'] = LabelEncoder().fit_transform(data['balance'])
     return data
 
 
 if __name__ == '__main__':
     raw_data = load_data()
     prepared_data = prepare_data(raw_data)

Примечание: альтернативный подход заключается в применении однократного кодирования с использованием классификатора «один против всех», ожидается аналогичная производительность.

Изучение

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

Сразу же мы замечаем несколько очевидных вещей

Number of rows: 625
Number of class 0: 288 | 46.08%
Number of class 1: 49 | 7.84%
Number of class 2: 288 | 46.08%

Все функции представляют собой целые числа от 1 до 5 и имеют следующий состав.

Таким образом, все они равномерно распределены по своим ценностям. Посмотрим, как они взаимодействуют с классом.

Теперь это говорит нам, что функции являются достойными предикторами класса, что подтверждает наше предыдущее предположение. Теперь давайте проверим, есть ли какие-либо основные структуры в данных, проверив графики рассеяния T-SNE и PCA.

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

P.S. код для выполнения всего этого исследования выглядит следующим образом:

def explore_data(data):
    """
    perform various forms of data visualisation and analysis

    :param data:
    :return: None
    """
    [m, n] = data.shape
    attributes = data.columns.values

    def data_summary():
        """
        Prints summary of data

        :return: None
        """
        print('Number of rows: {}'.format(m))
        for i in range(0, 3):
            [m_class, _] = data[data['balance'] == i].shape
            print('Number of class {}: {} | {}%'.format(i, m_class, m_class/m * 100))
        print(data.info())
        print(data.describe())

    def occurrence_histogram():
        """
        displays histogram of feature value occurrences.

        :return: None
        """
        nrows = n // 2
        fig, ax = plt.subplots(ncols=2, nrows=nrows)
        for i in range(1, len(attributes)):
            row, col = (i-1)//2, 1 if i % 2 == 0 else 0
            ax_ref = ax[col] if nrows <= 1 else ax[row, col]
            sns.distplot(data[attributes[i]], kde=False, vertical=True, ax=ax_ref)
            ax_ref.set(xlabel='# of occurrences', ylabel='value', title=attributes[i])
        fig.subplots_adjust(hspace=0.75, wspace=0.75)
        plt.show()

    def correlation_matrix():
        """
        Show matrix of correlations

        :return: None
        """
        plt.subplots(figsize=(10, 10))
        sns.heatmap(data[attributes].corr(), annot=True, fmt=".2f", cmap="coolwarm")
        plt.yticks(rotation=0)
        plt.xticks(rotation=90)
        plt.title('Correlation Matrix')
        plt.show()

    def covariance_matrix():
        """
        Show matrix of covariances between attributes.

        :return: None
        """
        plt.subplots(figsize=(10, 10))
        sns.heatmap(data[attributes].cov(), annot=True, fmt=".2f", cmap="coolwarm")
        plt.yticks(rotation=0)
        plt.xticks(rotation=90)
        plt.title('Covariance Matrix')
        plt.show()

    def t_sne_scatter():
        """
        Shows a scatter plot of T-SNE embed

        :return: None
        """
        features = data.drop(['balance'], axis=1)
        labels = data['balance']

        t_sne = TSNE(n_components=2)
        embed = t_sne.fit_transform(features)
        x1, y1, x2, y2, x3, y3 = [], [], [], [], [], []
        for i, e in enumerate(embed):
            if labels[i] == 1:
                x1 += [e[0]]
                y1 += [e[1]]
            elif labels[i] == 2:
                x2 += [e[0]]
                y2 += [e[1]]
            else:
                x3 += [e[0]]
                y3 += [e[1]]
        plt.scatter(x1, y1, c='r')
        plt.scatter(x2, y2, c='b')
        plt.scatter(x3, y3, c='g')
        plt.title('T-SNE Embedding')
        plt.show()

    def pca_scatter():
        """
        Shows a scatter plot of PCA decomposition

        :return: None
        """
        features = data.drop(['balance'], axis=1)
        labels = data['balance']

        pca = PCA(n_components=2)
        decomposition = pca.fit_transform(features)
        x1, y1, x2, y2, x3, y3 = [], [], [], [], [], []
        for i, e in enumerate(decomposition):
            if labels[i] == 1:
                x1 += [e[0]]
                y1 += [e[1]]
            elif labels[i] == 2:
                x2 += [e[0]]
                y2 += [e[1]]
            else:
                x3 += [e[0]]
                y3 += [e[1]]
        plt.scatter(x1, y1, c='r')
        plt.scatter(x2, y2, c='b')
        plt.scatter(x3, y3, c='g')
        plt.title('PCA Decomposition')
        plt.show()

    data_summary()
    occurrence_histogram()
    correlation_matrix()
    covariance_matrix()
    t_sne_scatter()
    pca_scatter()

Функциональная инженерия

Давайте на секунду задумаемся над проблемой. У нас есть весы с двумя грузами, чтобы определить баланс весов, мы должны учитывать как вес, так и расстояние от середины. Итак, давайте просто создадим две новые функции, умножив вес и расстояние каждой стороны на следующий код:

def engineer_data(data):
    """
    Returns modified version of data with left and right aggregate features while dropping weight and distance features

    :param data: data to work with
    :return: modified dataframe
    """
    data['left'] = data['left_weight'] * data['left_distance']
    data['right'] = data['right_weight'] * data['right_distance']
    data = data.drop(['left_weight', 'left_distance', 'right_weight', 'right_distance'], axis=1)
    return data

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

(Изменить: впоследствии я понял, что умножение уже упоминалось в описании набора данных. Извлеченный урок: внимательно прочитайте описания).

Теперь давайте посмотрим на встроенную структуру и структуру декомпозиции.

Намного лучше!

Моделирование

Мы начинаем процесс моделирования, сначала откладывая тестовый набор. Это набор, который мы не трогаем до самого конца нашего процесса. Это сделано для того, чтобы гарантировать, что мы не настраиваем наши параметры для набора тестов и сохраняем обобщаемость модели. Из оставшегося рабочего набора мы снова разделяем его между обучающим и проверочным набором. Мы используем набор поездов для обучения наших моделей и набор для проверки для тестирования производительности на удерживаемых данных и точной настройки наших моделей.

Впоследствии это немного более свободная форма. Это процесс экспериментирования с разными моделями и поиска наиболее эффективных. Процесс, который мне нравится использовать, состоит в том, чтобы сначала выбрать модели, которые логически имели бы смысл хорошо работать на этом наборе (например, SVM, KNN и т. Д.), И применить их стандартные формы (без настройки) к набору данных. Если эти модели работают «достаточно» хорошо, я их оставляю, в противном случае я их бросаю. В итоге у меня есть несколько хорошо работающих моделей, которые я настраиваю с помощью GridSearch. Оказывается, SVM (SVC в SKLearn) работает отлично. Более того, если я объединю набор более слабых настроенных моделей (MLP, KNN и Random Forest) в ансамбль голосования. Мы также можем получить довольно хорошую производительность.

Вот код для всего этого:

def model_data(data):
    features = data.drop(['balance'], axis=1)
    labels = data['balance']
    X_work, X_test, y_work, y_test = train_test_split(features, labels)
    # we don't touch the test set until the end.

    def grid_search(estimator, grid, X, y):
        gs = GridSearchCV(estimator, cv=5, n_jobs=-1, param_grid=grid)
        gs.fit(X, y)
        print(gs.best_params_)
        return gs.best_estimator_

    def support_vector(X, y):
        svc = SVC()
        grid = {
            'kernel': ['linear', 'poly', 'rbf', 'sigmoid']
        }
        return grid_search(svc, grid, X, y)

    def random_forest(X, y):
        rfc = RandomForestClassifier(n_jobs=-1)
        grid = {
            'n_estimators': np.arange(5, 15),
            'criterion': ['gini', 'entropy']
        }
        return grid_search(rfc, grid, X, y)

    def knn(X, y):
        knc = KNeighborsClassifier()
        grid = {
            'n_neighbors': np.arange(1, 10)
        }
        return grid_search(knc, grid, X, y)

    def perceptron(X, y):
        mlp = MLPClassifier()
        grid = {
            'activation': ['identity', 'logistic', 'tanh', 'relu']
        }
        return grid_search(mlp, grid, X, y)

    def vote(X, y):
        estimators = [
            ('random_forest', random_forest(X, y)),
            ('knn', knn(X, y)),
            ('perceptron', perceptron(X, y))
        ]
        vc = VotingClassifier(estimators, n_jobs=-1)
        vc.fit(X, y)
        return vc

    avg_train, avg_validate = 0, 0
    skf = StratifiedKFold(n_splits=5)
    for train_idx, test_idx in skf.split(X_work, y_work):
        X_train, X_test, y_train, y_test = \
            X_work.iloc[train_idx], X_work.iloc[test_idx], y_work.iloc[train_idx], y_work.iloc[test_idx]
        model = vote(X_train, y_train)
        avg_train += accuracy_score(y_train, model.predict(X_train))
        avg_validate += accuracy_score(y_test, model.predict(X_test))
    print('train: {}'.format(avg_train/5))
    print('validate: {}'.format(avg_validate/5))

Оценка

Теперь давайте посмотрим, как наша модель работает на том тестовом наборе, который мы намеренно игнорировали до сих пор. Мы оцениваем производительность, определяя точность как на тренировочном, так и на тестовом наборах. Это необходимо для обнаружения переобучения, если оно произойдет. Если точность обучающего набора очень высока, а набор тестов работает плохо, то это сильный индикатор переобучения.

В итоге для классификатора голосования находим следующие результаты:

  • поезд: 1.0
  • тест: 0.9872611464968153

и для машинного классификатора опорных векторов:

  • поезд: 1.0
  • тест: 1.0

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

Резюме

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

Код: https://github.com/andrew-x/DataPlay/tree/master/BalanceScale

Набор данных: https://archive.ics.uci.edu/ml/datasets/Balance+Scale