Вступление
В машинном обучении производительность модели только выигрывает от большего количества функций до определенного момента. Чем больше объектов вводится в модель, тем больше увеличивается размерность данных. По мере увеличения размерности становится все более вероятным переоснащение.
Существует множество методов , которые можно использовать для борьбы с переобучением , но уменьшение размерности является одним из самых эффективных методов. Уменьшение размерности выбирает наиболее важные компоненты пространства объектов, сохраняя их и отбрасывая другие компоненты.
Зачем Нужно Уменьшение Размерности?
Существует несколько причин, по которым уменьшение размерности используется в машинном обучении: для борьбы с вычислительными затратами, для контроля переоснащения, а также для визуализации и интерпретации многомерных наборов данных.
Часто в машинном обучении, чем больше функций присутствует в наборе данных, тем лучше классификатор может учиться. Однако большее количество функций также означает более высокие вычислительные затраты. Мало того, что высокая размерность может привести к длительному времени обучения, большее количество функций часто приводит к переоснащению алгоритма, когда он пытается создать модель, объясняющую все функции в данных.
Поскольку уменьшение размерности уменьшает общее количество функций, это может уменьшить вычислительные требования, связанные с обучением модели, но также помогает бороться с переобучением, сохраняя функции, которые будут подаваться в модель, достаточно простыми.
Уменьшение размерности может быть использовано как в контролируемых, так и в неконтролируемых контекстах обучения . В случае неконтролируемого обучения уменьшение размерности часто используется для предварительной обработки данных путем выбора или извлечения признаков.
Основными алгоритмами, используемыми для уменьшения размерности при неконтролируемом обучении, являются Анализ главных компонент (PCA) и Сингулярная декомпозиция (SVD).
В случае контролируемого обучения уменьшение размерности может быть использовано для упрощения функций, подаваемых в классификатор машинного обучения. Наиболее распространенными методами, используемыми для уменьшения размерности задач контролируемого обучения, являются линейный дискриминантный анализ (LDA) и PCA, и они могут быть использованы для прогнозирования новых случаев.
Обратите внимание, что случаи использования, описанные выше, являются общими случаями использования, а не единственными условиями, в которых используются эти методы. В конце концов, методы уменьшения размерности являются статистическими методами, и их использование не ограничивается моделями машинного обучения.
Давайте уделим некоторое время объяснению идей, лежащих в основе каждого из наиболее распространенных методов уменьшения размерности.
Анализ основных компонентов
Анализ главных компонент (PCA) – это статистический метод, который создает новые объекты или характеристики данных путем анализа характеристик набора данных. По существу, характеристики данных суммируются или объединяются вместе. Вы также можете представить себе анализ главных компонент как “сдавливание” данных всего в несколько измерений из пространства гораздо более высоких измерений.
Чтобы быть более конкретным, напиток может быть описан многими признаками, но многие из этих признаков будут избыточными и относительно бесполезными для идентификации рассматриваемого напитка. Вместо того, чтобы описывать вино с такими характеристиками, как аэрация, уровень С02 и т. Д., их легче было бы описать цветом, вкусом и возрастом.
Анализ основных компонентов выбирает “основные” или наиболее влиятельные характеристики набора данных и создает на их основе объекты. При выборе только тех объектов, которые оказывают наибольшее влияние на набор данных, размерность уменьшается.
PCA сохраняет корреляции между переменными при создании новых объектов. Основные компоненты, создаваемые методом, представляют собой линейные комбинации исходных переменных, вычисляемые с помощью понятий, называемых собственными векторами .
Предполагается, что новые компоненты ортогональны или не связаны друг с другом.
Пример реализации PCA
Давайте посмотрим, как PCA может быть реализован в Scikit-Learn . Для этого мы будем использовать набор данных классификации грибов .
Во-первых, нам нужно импортировать все необходимые модули, включая PCA, train_test_split
, а также инструменты маркировки и масштабирования:
import pandas as pd import matplotlib.pyplot as plt from sklearn.preprocessing import LabelEncoder, StandardScaler from sklearn.decomposition import PCA from sklearn.model_selection import train_test_split import warnings warnings.filterwarnings("ignore")
После загрузки данных мы проверим наличие нулевых значений. Мы также закодируем данные с помощью LabelEncoder
. Объект класса – это первый столбец в наборе данных, поэтому мы разделяем объекты и метки соответственно:
m_data = pd.read_csv('mushrooms.csv') # Machine learning systems work with integers, we need to encode these # string characters into ints encoder = LabelEncoder() # Now apply the transformation to all the columns: for col in m_data.columns: m_data[col] = encoder.fit_transform(m_data[col]) X_features = m_data.iloc[:,1:23] y_label = m_data.iloc[:, 0]
Теперь мы будем масштабировать объекты с помощью стандартного скаляра. Это необязательно, так как на самом деле мы не запускаем классификатор, но это может повлиять на то, как наши данные анализируются PCA:
# Scale the features scaler = StandardScaler() X_features = scaler.fit_transform(X_features)
Теперь мы будем использовать PCA, чтобы получить список функций и построить график, какие функции имеют наибольшую объяснительную силу или имеют наибольшую дисперсию. Это основные компоненты. Похоже, что около 17 или 18 функций объясняют большинство, почти 95% наших данных:
# Visualize pca = PCA() pca.fit_transform(X_features) pca_variance = pca.explained_variance_ plt.figure(figsize=(8, 6)) plt.bar(range(22), pca_variance, alpha=0.5, align='center', label='individual variance') plt.legend() plt.ylabel('Variance ratio') plt.xlabel('Principal components') plt.show()
Давайте преобразуем эти функции в 17 лучших функций. Затем мы построим точечную диаграмму классификации точек данных на основе этих 17 признаков:
pca2 = PCA(n_components=17) pca2.fit(X_features) x_3d = pca2.transform(X_features) plt.figure(figsize=(8,6)) plt.scatter(x_3d[:,0], x_3d[:,5], c=m_data['class']) plt.show()
Давайте также сделаем это для топ-2 признаков и посмотрим, как изменится классификация:
pca3 = PCA(n_components=2) pca3.fit(X_features) x_3d = pca3.transform(X_features) plt.figure(figsize=(8,6)) plt.scatter(x_3d[:,0], x_3d[:,1], c=m_data['class']) plt.show()
Сингулярная декомпозиция
Цель сингулярной декомпозиции состоит в том, чтобы упростить матрицу и облегчить выполнение вычислений с ней. Матрица сводится к ее составным частям, подобным цели СПС. Понимание всех тонкостей SVD не является полностью необходимым для реализации его в ваших моделях машинного обучения, но наличие интуиции относительно того, как он работает, даст вам лучшее представление о том, когда его использовать.
SVD может выполняться как на комплексных, так и на вещественных матрицах, но чтобы сделать это объяснение более понятным, мы рассмотрим метод разложения вещественной матрицы.
При выполнении SVD у нас есть матрица, заполненная данными, и мы хотим уменьшить количество столбцов в матрице. Это уменьшает размерность матрицы, сохраняя при этом как можно большую вариабельность данных.
Можно сказать, что матрица A равна транспозиции матрицы V:
$$ * D * V^t $$
Предполагая, что у нас есть некоторая матрица A, мы можем представить эту матрицу в виде трех других матриц, называемых U , V и D . Матрица A имеет исходные x*y элементы, в то время как матрица U является ортогональной матрицей, содержащей x*x элементы, а матрица V является другой ортогональной матрицей, содержащей y*y элементы. Наконец, D – это диагональная матрица, содержащая элементы x*y .
Разложение значений для матрицы включает преобразование сингулярных значений в исходной матрице в диагональные значения новой матрицы. Ортогональные матрицы не изменяют своих свойств, если их умножить на другие числа, и мы можем воспользоваться этим свойством, чтобы получить аппроксимацию матрицы A . При умножении ортогональной матрицы вместе взятых при транспозиции матрицы V мы получаем матрицу , эквивалентную исходной матрице A .
Когда мы разбиваем/разлагаем матрицу A вниз на U , D и V , то у нас есть три различные матрицы , которые содержат информацию матрицы A .
Оказывается, что самые левые столбцы матриц содержат большую часть наших данных, и мы можем выбрать только эти несколько столбцов, чтобы иметь хорошее приближение матрицы A . Эта новая матрица намного проще и проще в работе, так как она имеет гораздо меньше измерений.
Пример реализации SVD
Одним из наиболее распространенных способов использования SVD является сжатие изображений. В конце концов, значения пикселей, которые составляют красный, зеленый и синий каналы в изображении, могут быть просто уменьшены, и в результате получится изображение, которое является менее сложным, но все еще содержит то же самое содержание изображения. Давайте попробуем использовать SVD для сжатия изображения и его рендеринга.
Мы будем использовать несколько функций для обработки сжатия изображения. Для этого нам действительно понадобится только Numpy и функция Image
из библиотеки PIL , поскольку у Numpy есть метод для выполнения вычисления SVD:
import numpy from PIL import Image
Сначала мы просто напишем функцию для загрузки изображения и превратим ее в массив Numpy. Затем мы хотим выбрать красный, зеленый и синий цветовые каналы из изображения:
def load_image(image): image = Image.open(image) im_array = numpy.array(image) red = im_array[:, :, 0] green = im_array[:, :, 1] blue = im_array[:, :, 2] return red, green, blue
Теперь, когда у нас есть цвета, нам нужно сжать цветовые каналы. Мы можем начать с вызова функции SVD Numpy на нужном нам цветовом канале. Затем мы создадим массив нулей, который будем заполнять после завершения умножения матрицы. Затем мы указываем предел сингулярного значения, который мы хотим использовать при выполнении вычислений:
def channel_compress(color_channel, singular_value_limit): u, s, v = numpy.linalg.svd(color_channel) compressed = numpy.zeros((color_channel.shape[0], color_channel.shape[1])) n = singular_value_limit left_matrix = numpy.matmul(u[:, 0:n], numpy.diag(s)[0:n, 0:n]) inner_compressed = numpy.matmul(left_matrix, v[0:n, :]) compressed = inner_compressed.astype('uint8') return compressed red, green, blue = load_image("dog3.jpg") singular_val_lim = 350
После этого мы делаем матричное умножение по диагонали и пределы значений в U-матрице, как описано выше. Это дает нам левую матрицу, а затем мы умножаем ее на V-матрицу. Это должно привести нас к сжатым значениям, которые мы преобразуем в тип ‘uint8’:
def compress_image(red, green, blue, singular_val_lim): compressed_red = channel_compress(red, singular_val_lim) compressed_green = channel_compress(green, singular_val_lim) compressed_blue = channel_compress(blue, singular_val_lim) im_red = Image.fromarray(compressed_red) im_blue = Image.fromarray(compressed_blue) im_green = Image.fromarray(compressed_green) new_image = Image.merge("RGB", (im_red, im_green, im_blue)) new_image.show() new_image.save("dog3-edited.jpg") compress_image(red, green, blue, singular_val_lim)
Мы будем использовать это изображение собаки, чтобы проверить наше сжатие SVD на:
Нам также нужно установить предел сингулярного значения, который мы будем использовать, давайте пока начнем с 600:
red, green, blue = load_image("dog.jpg") singular_val_lim = 350
Наконец, мы можем получить сжатые значения для трех цветовых каналов и преобразовать их из массивов Numpy в компоненты изображения с помощью PIL. Затем мы просто должны соединить три канала вместе и показать изображение. Это изображение должно быть немного меньше и проще, чем исходное изображение:
Действительно, если вы посмотрите на размер изображений, вы заметите, что сжатые изображения меньше, хотя у нас также было немного сжатия с потерями. Вы также можете увидеть некоторый шум на изображении.
Вы можете поиграть с регулировкой предела сингулярного значения. Чем ниже выбранный предел, тем больше будет сжатие, но в определенный момент появится искусственное изображение, и качество изображения ухудшится:
def compress_image(red, green, blue, singular_val_lim): compressed_red = channel_compress(red, singular_val_lim) compressed_green = channel_compress(green, singular_val_lim) compressed_blue = channel_compress(blue, singular_val_lim) im_red = Image.fromarray(compressed_red) im_blue = Image.fromarray(compressed_blue) im_green = Image.fromarray(compressed_green) new_image = Image.merge("RGB", (im_red, im_green, im_blue)) new_image.show() compress_image(red, green, blue, singular_val_lim)
Линейный дискриминантный анализ
Линейный дискриминантный анализ работает путем проецирования данных из многомерного графа на линейный граф. Проще всего представить себе это с помощью графика, заполненного точками данных двух различных классов. Предполагая, что нет линии, которая аккуратно разделит данные на два класса, двумерный граф можно свести к 1D-графу. Затем этот 1D-график можно использовать для достижения наилучшего разделения точек данных.
Когда LDA выполняется, есть две основные цели: минимизация дисперсии двух классов и максимизация расстояния между средними значениями двух классов данных.
Для этого на двумерном графике будет построена новая ось. Эта новая ось должна разделять две точки данных на основе ранее упомянутых критериев. После создания новой оси точки данных в 2D-графике перерисовываются вдоль новой оси.
LDA выполняет три различных шага для перемещения исходного графика на новую ось. Во-первых, должна быть рассчитана разделимость между классами, и это основано на расстоянии между средними классами или дисперсии между классами. На следующем этапе необходимо вычислить дисперсию внутри класса, которая представляет собой расстояние между средним значением и выборкой для различных классов. Наконец, должно быть построено пространство более низкой размерности, которое максимизирует дисперсию между классами.
LDA работает лучше всего, когда средства классов находятся далеко друг от друга. Если средства распределения будут общими, то LDA не сможет разделить классы с помощью новой линейной оси.
Пример реализации LDA
Наконец, давайте посмотрим, как LDA можно использовать для уменьшения размерности. Заметим, что LDA может быть использован в качестве алгоритма классификации в дополнение к проведению редукции размерности.
В следующем примере мы будем использовать набор данных Titanic .
Давайте начнем с того, что сделаем весь наш необходимый импорт:
import pandas as pd import numpy as np from sklearn.metrics import accuracy_score, f1_score from sklearn.preprocessing import LabelEncoder, StandardScaler from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression
Теперь мы загрузим наши обучающие данные, которые разделим на обучающие и проверочные наборы.
Однако сначала нам нужно сделать небольшую предварительную обработку данных. Давайте опустим столбцы Name
, Cabin
и Ticket
, поскольку они не несут много полезной информации. Нам также нужно заполнить любые недостающие данные, которые мы заменим медианными значениями в случае функции Age
и S
в случае функции Embarked
:
training_data = pd.read_csv("train.csv") # Let's drop the cabin and ticket columns training_data.drop(labels=['Cabin', 'Ticket'], axis=1, inplace=True) training_data["Age"].fillna(training_data["Age"].median(), inplace=True) training_data["Embarked"].fillna("S", inplace=True)
Нам также нужно кодировать нечисловые признаки. Мы закодируем оба столбца Sex
и Started
. Давайте также отбросим столбец Name
, поскольку он вряд ли будет полезен при классификации:
encoder_1 = LabelEncoder() # Fit the encoder on the data encoder_1.fit(training_data["Sex"]) # Transform and replace the training data training_sex_encoded = encoder_1.transform(training_data["Sex"]) training_data["Sex"] = training_sex_encoded encoder_2 = LabelEncoder() encoder_2.fit(training_data["Embarked"]) training_embarked_encoded = encoder_2.transform(training_data["Embarked"]) training_data["Embarked"] = training_embarked_encoded # Assume the name is going to be useless and drop it training_data.drop("Name", axis=1, inplace=True)
Нам нужно масштабировать значения, но инструмент Scaler
принимает массивы, поэтому значения, которые мы хотим изменить, сначала должны быть превращены в массивы. После этого мы можем масштабировать данные:
# Remember that the scaler takes arrays ages_train = np.array(training_data["Age"]).reshape(-1, 1) fares_train = np.array(training_data["Fare"]).reshape(-1, 1) scaler = StandardScaler() training_data["Age"] = scaler.fit_transform(ages_train) training_data["Fare"] = scaler.fit_transform(fares_train) # Now to select our training and testing data features = training_data.drop(labels=['PassengerId', 'Survived'], axis=1) labels = training_data['Survived']
Теперь мы можем выбрать обучающие функции и метки и использовать train_test_split
для создания наших обучающих и проверочных данных. Классификацию с помощью LDA сделать легко, вы справляетесь с ней точно так же, как с любым другим классификатором в Scikit-Learn.
Просто установите функцию на обучающие данные и попросите ее предсказать результаты валидации/тестирования. Затем мы можем напечатать метрики для прогнозов относительно фактических значений:
X_train, X_val, y_train, y_val = train_test_split(features, labels, test_size=0.2, random_state=27) model = LDA() model.fit(X_train, y_train) preds = model.predict(X_val) acc = accuracy_score(y_val, preds) f1 = f1_score(y_val, preds) print("Accuracy: {}".format(acc)) print("F1 Score: {}".format(f1))
Вот распечатка:
Accuracy: 0.8100558659217877 F1 Score: 0.734375
Когда дело доходит до преобразования данных и уменьшения размерности, давайте сначала запустим классификатор логистической регрессии на данных, чтобы мы могли увидеть, какова наша производительность до уменьшения размерности:
logreg_clf = LogisticRegression() logreg_clf.fit(X_train, y_train) preds = logreg_clf.predict(X_val) acc = accuracy_score(y_val, preds) f1 = f1_score(y_val, preds) print("Accuracy: {}".format(acc)) print("F1 Score: {}".format(f1))
Вот результаты:
Accuracy: 0.8100558659217877 F1 Score: 0.734375
Теперь мы преобразуем объекты данных, указав ряд необходимых компонентов для LDA и установив модель на объекты и метки. Затем мы просто преобразуем объекты и сохраняем их в новую переменную. Давайте распечатаем исходное и уменьшенное количество функций:
LDA_transform = LDA(n_components=1) LDA_transform.fit(features, labels) features_new = LDA_transform.transform(features) # Print the number of features print('Original feature #:', features.shape[1]) print('Reduced feature #:', features_new.shape[1]) # Print the ratio of explained variance print(LDA_transform.explained_variance_ratio_)
Вот распечатка для приведенного выше кода:
Original feature #: 7 Reduced feature #: 1 [1.]
Теперь нам просто нужно снова сделать разделение train/test с новыми функциями и снова запустить классификатор, чтобы увидеть, как изменилась производительность:
X_train, X_val, y_train, y_val = train_test_split(features_new, labels, test_size=0.2, random_state=27) logreg_clf = LogisticRegression() logreg_clf.fit(X_train, y_train) preds = logreg_clf.predict(X_val) acc = accuracy_score(y_val, preds) f1 = f1_score(y_val, preds) print("Accuracy: {}".format(acc)) print("F1 Score: {}".format(f1))
Accuracy: 0.8212290502793296 F1 Score: 0.7500000000000001
Вывод
Мы рассмотрели основные методы сокращения размерности: Анализ главных компонент, Сингулярное разложение и Линейный дискриминантный анализ. Это статистические методы, которые вы можете использовать, чтобы помочь вашим моделям машинного обучения работать лучше, бороться с переобучением и помогать в анализе данных.
Хотя эти три метода являются наиболее часто используемыми методами уменьшения размерности, существуют и другие. Другие методы размерности включают аппроксимацию ядра и isomap спектральное вложение.