"Начиная"

Как получить значение функций из любого конвейера Sklearn

Навигация по конвейерам может быть сложной, вот некоторый код, который в целом работает.

Вступление

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

Трубопроводы

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

from datasets import list_datasets, load_dataset, list_metrics
from sklearn.pipeline import FeatureUnion, Pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn import svm
# Load a dataset and print the first examples in the training set
imdb_data = load_dataset('imdb')
classifier = svm.LinearSVC(C=1.0, class_weight="balanced")
model = Pipeline(
    [
        ("vectorizer", TfidfVectorizer()),
        ("classifier", classifier),
    ]
)
x_train = [x["text"]for x in imdb_data["train"]]
y_train = [x["label"]for x in imdb_data["train"]]
model.fit(x_train, y_train)

Здесь мы используем отличный пакет python datasets для быстрого доступа к данным настроений imdb. Этот пакет, созданный HuggingFace, содержит массу отличных наборов данных, и все они готовы к работе, так что вы можете сразу приступить к созданию интересной модели.

Вышеупомянутый конвейер определяет два шага в списке. Сначала он принимает ввод и передает его через TfidfVectorizer, который принимает текст и возвращает функции текста TF-IDF в виде вектора. Затем он передает этот вектор классификатору SVM.

Обратите внимание, как это происходит по порядку, шаг TF-IDF, затем классификатор. Вы можете связать столько этапов изменения, сколько захотите. Например, приведенный выше конвейер эквивалентен:

model = Pipeline(
    [
        ("vectorizer", CountVectorizer()),
        ("transformer", TfidfTransformer()),
        ("classifier", classifier),
    ]
)

Здесь мы делаем еще больше вручную. Во-первых, мы получаем количество каждого слова, во-вторых, мы применяем преобразование TF-IDF и, наконец, передаем этот вектор признаков классификатору. TfidfVectorizer выполняет эти два действия за один шаг. Но это иллюстрирует суть. В необработанном конвейере все выполняется по порядку. Мы обсудим, как складывать элементы вместе, чуть позже. А пока давайте поработаем над определением важности функции для нашего первого примера модели.

Важность функций

Трубопроводы упрощают доступ к отдельным элементам. Если распечатать модель после обучения, вы увидите:

Pipeline(memory=None,
         steps=[('vectorizer',
                 TfidfVectorizer(...)
                ('classifier',
                 LinearSVC(...))],
         verbose=False)

Это означает, что есть два шага: один называется vectorizer, другой - classifier. Мы можем получить к ним доступ, посмотрев на параметр named_steps конвейера следующим образом:

model.named_steps["vectorizer"]

Это вернет наш подогнанный TfidfVectorizer. Довольно аккуратно! Большинство шагов по настройке в Sklearn также реализуют метод get_feature_names(), который мы можем использовать для получения имен каждой функции, запустив:

# Get the names of each feature
feature_names = model.named_steps["vectorizer"].get_feature_names()

Это даст нам список имен всех функций в нашем векторизаторе. Тогда нам просто нужно получить коэффициенты из классификатора. Для большинства классификаторов в Sklearn это так же просто, как получить параметр .coef_. (Методы ансамбля немного отличаются, вместо них используется параметр feature_importances_)

# Get the coefficients of each feature
coefs = model.named_steps["classifier"].coef_.flatten()

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

import pandas as pd
# Zip coefficients and names together and make a DataFrame
zipped = zip(feature_names, coefs)
df = pd.DataFrame(zipped, columns=["feature", "value"])
# Sort the features by the absolute value of their coefficient
df["abs_value"] = df["value"].apply(lambda x: abs(x))
df["colors"] = df["value"].apply(lambda x: "green" if x > 0 else "red")
df = df.sort_values("abs_value", ascending=False)

И визуализируйте:

import seaborn as sns
fig, ax = plt.subplots(1, 1, figsize=(12, 7))
sns.barplot(x="feature",
            y="value",
            data=df.head(20),
           palette=df.head(20)["colors"])
ax.set_xticklabels(ax.get_xticklabels(), rotation=90, fontsize=20)
ax.set_title("Top 20 Features", fontsize=25)
ax.set_ylabel("Coef", fontsize=22)
ax.set_xlabel("Feature Name", fontsize=22)

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

Получите важность функций от FeatureUnion

Я обнаружил, что в большинстве реальных приложений я сложным образом комбинирую множество функций вместе. Ранее мы видели, как конвейер выполняет каждый шаг по порядку. Как мы справляемся с несколькими одновременными шагами? Ответ - класс FeatureUnion. Допустим, мы хотим построить модель, в которой мы берем на себя функции биграмм TF-IDF, но также имеем несколько вручную отобранных униграмм. (См. Мое сообщение в блоге об использовании моделей, чтобы найти здесь хорошие униграммы.) Мы можем определить этот конвейер с помощью FeatureUnion. FeatureUnion принимает transformer_list, который может быть списком преобразователей, конвейеров, классификаторов и т. Д., А затем объединяет их результаты.

classifier = svm.LinearSVC(C=1.0, class_weight="balanced")
vocab = {"worst": 0, "awful": 1, "waste": 2,
         "boring": 3, "excellent": 4}
model = Pipeline([
    ("union", FeatureUnion(transformer_list=[
        ("handpicked", TfidfVectorizer(vocabulary=vocab)),
        ("bigrams", TfidfVectorizer(ngram_range=(2, 2)))])
    ),
    ("classifier", classifier),
    ])

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

Извлечь особенности из этой модели немного сложнее. Мы должны войти в союз, а затем получить все индивидуальные черты. Давайте попробуем сделать это вручную, а затем посмотрим, сможем ли мы обобщить его на любой произвольный конвейер. Мы уже знаем, как получить доступ к элементам конвейера, это named_steps.. Чтобы попасть внутрь FeatureUnion, мы можем смотреть прямо на transformer_list и проходить через каждый элемент. Таким образом, код будет выглядеть примерно так.

handpicked = (model
              .named_steps["union"]
              .transformer_list[0][1]
              .get_feature_names())
bigrams = (model
           .named_steps["union"]
           .transformer_list[1][1]
           .get_feature_names())
feature_names = bigrams + handpicked

Поскольку классификатор - это SVM, которая работает с одним вектором, коэффициенты будут поступать из одного места и располагаться в одном порядке. Мы снова можем визуализировать наши результаты.

Похоже, наши биграммы были намного информативнее, чем юниграммы, отобранные нами вручную.

Общий случай

Итак, мы сделали несколько простых примеров, но теперь нам нужен способ сделать это для любой (примерно любой) комбинации Pipeline и FeatureUnion. Для этого мы обращаемся к нашему старому другу Depth First Search (DFS). Мы собираемся рассматривать конвейер как дерево. Каждый слой может иметь произвольное количество FeatureUnions, но в конечном итоге все они будут складываться в один вектор признаков. При перемещении необходимо учитывать примерно три случая. Первый - это базовый случай, когда мы находимся в реальном преобразователе или классификаторе, который будет генерировать наши функции. Второй - если мы находимся в конвейере. Третий и последний случай - это когда мы находимся внутри FeatureUnion. Давайте поговорим об этом немного подробнее.

Случай 1: этап модификации

Здесь мы хотим написать функцию, которая при заданном некотором типе функции будет возвращать имена функций. Это базовый случай в нашей DFS. В Sklearn есть несколько разных типов вещей, которые можно использовать для создания функций. Некоторыми примерами являются методы кластеризации, методы уменьшения размерности, традиционные классификаторы и препроцессоры, и это лишь некоторые из них. Каждый из них позволяет вам получить доступ к именам функций по-разному. Например, текстовый препроцессор TfidfVectorizer реализует метод get_feature_names, как мы видели выше. Однако у большинства методов кластеризации нет именованных функций, это произвольные кластеры, но у них есть фиксированное количество кластеров. Давайте напишем вспомогательную функцию, которая при заданном методе характеристики Sklearn будет возвращать список функций.

Здесь мы пытаемся перечислить ряд потенциальных случаев, которые могут произойти внутри Sklearn. Мы используем hasattr, чтобы проверить, имеет ли предоставленная модель данный атрибут, и если да, мы вызываем его, чтобы получить имена функций. Если метод похож на кластеризацию и не включает в себя фактические именованные функции, мы создаем собственные имена функций, используя предоставленное имя. Например, предположим, что мы применяем этот метод к PCA с двумя компонентами, и мы назвали этап pca, тогда возвращаемые результирующие имена функций будут [pca_0, pca_1].

DFS

Теперь мы можем реализовать DFS.

Давайте вместе пройдем через это. Эта функция потребует трех вещей. Первая - это модель, которую мы хотим проанализировать. Эта модель должна быть конвейером. Второй - это список всех именованных шагов по изменению характеристик, которые мы хотим вывести. В нашем последнем примере это были bigrams и handpicked.. Это названия отдельных шагов, которые мы использовали в нашей модели. Последний параметр - это текущее имя, на которое мы смотрим. Это необходимо для рекурсии и не имеет значения при первом проходе. (Я должен создать вспомогательный метод, чтобы скрыть это от конечного пользователя, но пока это меньше кода, который нужно объяснять).

  • Строки 19–25 образуют базовый вариант. Они справляются с ситуацией, когда имя шага совпадает с именем в нашем списке желаемых имен. Это соответствует листовому узлу, который на самом деле выполняет присвоение характеристик, и мы хотим получить имена от него.
  • Строки 26–30 управляют экземплярами, когда мы находимся на конвейере. Когда это происходит, мы хотим получить имена каждого шага, обратившись к параметру named_steps, а затем рекурсивно просмотреть их, чтобы собрать функции. Мы проходим каждый именованный шаг в конвейере и получаем все имена функций, объединяя их в список.
  • Строки 31–35 управляют экземплярами, когда мы находимся в FeatureUnion. Когда это происходит, мы хотим получить имена каждого субтрансформатора из параметра transformer_list, а затем повторно просмотреть их, чтобы собрать функции.

Имея это в руках, теперь мы можем взять произвольно вложенный конвейер, скажем, например, приведенный ниже код, и получить имена функций в правильном порядке!

from sklearn.decomposition import TruncatedSVD
classifier = svm.LinearSVC(C=1.0, class_weight="balanced")
vocab = {"worst": 0, "awful": 1, "waste": 2,
         "boring": 3, "excellent": 4}
model = Pipeline([
    ("union", FeatureUnion(transformer_list=[
        ("h1", TfidfVectorizer(vocabulary={"worst": 0})),
        ("h2", TfidfVectorizer(vocabulary={"best": 0})),
        ("h3", TfidfVectorizer(vocabulary={"awful": 0})),
        ("tfidf_cls", Pipeline([
            ("vectorizer", CountVectorizer()),
            ("transformer", TfidfTransformer()),
            ("tsvd", TruncatedSVD(n_components=2))
        ]
        ))
    ])
     ),
    ("classifier", classifier),
])

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

get_feature_names(model, ["h1", "h2", "h3", "tsvd"], None)

Который вернется

['worst', 'best', 'awful', 'tsvd_0', 'tsvd_1']

В точности то, что мы ожидали.

Заключение

Есть много способов смешивать и сопоставлять шаги в конвейере, и получение имен функций может быть довольно сложной задачей. Если мы используем DFS, мы можем извлечь их все в правильном порядке. Этот метод будет работать в большинстве случаев в экосистеме SciKit-Learn, но я еще не все протестировал. Чтобы расширить его, вам просто нужно просмотреть документацию к тому классу, из которого вы пытаетесь получить имена, и обновить метод extract_feature_names, добавив новую условную проверку наличия нужного атрибута. Надеюсь, это поможет упростить использование и изучение конвейеров :). Вы можете найти блокнот Jupyter с некоторыми примерами кода для этого фрагмента здесь. Как и в случае со всеми моими сообщениями, если вы застряли, прокомментируйте здесь или напишите мне в LinkedIn. Мне всегда интересно услышать мнение людей. Удачного кодирования!