scikit-learn: FeatureUnion для включения функций, созданных вручную

Я выполняю многоуровневую классификацию текстовых данных. Я хочу использовать комбинированные функции tfidf и пользовательские лингвистические функции, аналогичные примеру здесь с помощью FeatureUnion< /а>.

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

custom_features_dict = {'contact':['contact details', 'e-mail'], 
                       'demographic':['gender', 'age', 'birth'],
                       'location':['location', 'geo']}

Структура обучающих данных выглядит следующим образом:

text                                            contact  demographic  location
---                                              ---      ---          ---
'provide us with your date of birth and e-mail'  1        1            0
'contact details and location will be stored'    1        0            1
'date of birth should be before 2004'            0        1            0

Как вышеупомянутое dict может быть включено в FeatureUnion? Насколько я понимаю, следует вызывать пользовательскую функцию, которая возвращает логические значения, соответствующие наличию или отсутствию строковых значений (из custom_features_dict) в обучающих данных.

Это дает следующие list из dict для заданных обучающих данных:

[
    {
       'contact':1,
       'demographic':1,
       'location':0
    },
    {
       'contact':1,
       'demographic':0,
       'location':1
    },
    {
       'contact':0,
       'demographic':1,
       'location':0
    },
] 

Как можно использовать приведенный выше list для реализации подгонки и преобразования?

Код приведен ниже:

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.feature_extraction import DictVectorizer
#from sklearn.metrics import accuracy_score
from sklearn.multiclass import OneVsRestClassifier
from nltk.corpus import stopwords
stop_words = set(stopwords.words('english'))
from sklearn.svm import LinearSVC
from sklearn.pipeline import Pipeline
from io import StringIO

data = StringIO(u'''text,contact,demographic,location
provide us with your date of birth and e-mail,1,1,0
contact details and location will be stored,0,1,1
date of birth should be before 2004,0,1,0''')

df = pd.read_csv(data)

custom_features_dict = {'contact':['contact details', 'e-mail'], 
                        'demographic':['gender', 'age', 'birth'],
                        'location':['location', 'geo']}

my_features = [
    {
       'contact':1,
       'demographic':1,
       'location':0
    },
    {
       'contact':1,
       'demographic':0,
       'location':1
    },
    {
       'contact':0,
       'demographic':1,
       'location':0
    },
]

bow_pipeline = Pipeline(
    steps=[
        ("tfidf", TfidfVectorizer(stop_words=stop_words)),
    ]
)

manual_pipeline = Pipeline(
    steps=[
        # This needs to be fixed
        ("custom_features", my_features),
        ("dict_vect", DictVectorizer()),
    ]
)

combined_features = FeatureUnion(
    transformer_list=[
        ("bow", bow_pipeline),
        ("manual", manual_pipeline),
    ]
)

final_pipeline = Pipeline([
            ('combined_features', combined_features),
            ('clf', OneVsRestClassifier(LinearSVC(), n_jobs=1)),
        ]
)

labels = ['contact', 'demographic', 'location']

for label in labels:
    final_pipeline.fit(df['text'], df[label]) 



Ответы (2)


Вы должны определить Transformer, который принимает ваш текст в качестве входных данных. Что-то такое:

from sklearn.base import BaseEstimator, TransformerMixin

custom_features_dict = {'contact':['contact details', 'e-mail'], 
                   'demographic':['gender', 'age', 'birth'],
                   'location':['location', 'geo']}

#helper function which returns 1, if one of the words occures in the text, else 0
#you can add more words or categories to custom_features_dict if you want
def is_words_present(text, listofwords):
  for word in listofwords:
    if word in text:
      return 1
  return 0

class CustomFeatureTransformer(BaseEstimator, TransformerMixin):
    def __init__(self, custom_feature_dict):
       self.custom_feature_dict = custom_feature_dict
    def fit(self, x, y=None):
        return self    
    def transform(self, data):
        result_arr = []
        for text in data:
          arr = []
          for key in self.custom_feature_dict:
            arr.append(is_words_present(text, self.custom_feature_dict[key]))
          result_arr.append(arr)
        return result_arr

Примечание. Этот Transformer генерирует массив, который выглядит следующим образом: [1, 0, 1], он не генерирует словарь, что позволяет нам сэкономить DictVectorizer.

Кроме того, я изменил способ обработки Multilabel-классификации, см. здесь:

#first, i generate a new column in the dataframe, with all the labels per row:
def create_textlabels_array(row):
  arr = []
  for label in ['contact', 'demographic', 'location']:
    if row[label]==1:
      arr.append(label)
  return arr

df['textlabels'] = df.apply(create_textlabels_array, 1) 

#then we generate the binarized Labels:
from sklearn.preprocessing import MultiLabelBinarizer
mlb = MultiLabelBinarizer().fit(df['textlabels'])
y = mlb.transform(df['textlabels'])

Теперь мы можем добавить все вместе в конвейер:

bow_pipeline = Pipeline(
    steps=[
        ("tfidf", TfidfVectorizer(stop_words=stop_words)),
    ]
)

manual_pipeline = Pipeline(
    steps=[
        ("costum_vect", CustomFeatureTransformer(custom_features_dict)),
    ]
)

combined_features = FeatureUnion(
    transformer_list=[
        ("bow", bow_pipeline),
        ("manual", manual_pipeline),
    ]
)

final_pipeline = Pipeline([
        ('combined_features', combined_features),
        ('clf', OneVsRestClassifier(LinearSVC(), n_jobs=1)),
    ]
)

#train your pipeline
final_pipeline.fit(df['text'], y) 

#let's predict something: (Note: of course training data is a bit low in that examplecase here)
pred = final_pipeline.predict(["write an e-mail to our location please"])
print(pred) #output: [0, 1, 1] 

#reverse the predicted array to the actual labels:
print(mlb.inverse_transform(pred)) #output: [('demographic', 'location')]
person chefhose    schedule 13.01.2020
comment
Я получаю следующую ошибку в def transform(self, data): result_arr NameError: глобальное имя «result_arr» не определено. При изменении на result_arr = [] я получаю следующую ошибку в строке arr.append(is_words_present(text, self.word_dict[key])) AttributeError: объект «CustomFeatureTransformer» не имеет атрибута «word_dict» - person SaadH; 15.01.2020
comment
да, были некоторые ошибки при переносе кода в stackoverflow. Я надеюсь, что я исправил это сейчас - person chefhose; 15.01.2020
comment
Теперь это работает! У меня есть только один последний вопрос: есть ли особая причина, по которой вы тренируете конвейер на всех метках вместе, а не по одной? Одним из плюсов обучения по одному является то, что мы можем получить оценку точности прогноза для каждой метки. Например, если у нас есть аналогичный фрейм данных для тестовых данных (df_test), то мы можем добавить следующие две строки в конце кода, указанного в вопросе: pred = final_pipeline.predict(df_test['text']) print (label, accuracy_score(df_test[label], pred)) - person SaadH; 16.01.2020
comment
потому что это по сути то же самое и стандартный способ сделать это. Вы используете OneVsRestClassifier, который обучает отдельный классификатор для каждой метки, даже если они обучаются вместе в одной строке. Вы также можете создавать отчеты о классификации с несколькими метками с помощью classification_report например. Вероятно, чтение этого поможет вам понять, как для оценки multilabel-clf-задач. - person chefhose; 16.01.2020

Если мы просто хотим исправить ту часть кода, которая помечена как исправленная, все, что нам нужно, — это реализовать новый оценщик, расширяющий класс sklearn.base.BaseEstimator (класс TemplateClassifier — хороший пример здесь).

Однако, похоже, здесь есть концептуальная ошибка. Информация в списке my_features, по-видимому, является самими метками (ну, можно утверждать, что это очень сильные функции...). Таким образом, мы не должны навешивать ярлыки на конвейер функций.

Как описано здесь,

Преобразователи обычно комбинируются с классификаторами, регрессорами или другими оценщиками для создания составной оценки. Наиболее распространенным инструментом является Pipeline. Pipeline часто используется в сочетании с FeatureUnion, который объединяет выходные данные преобразователей в составное пространство признаков. TransformedTargetRegressor имеет дело с преобразованием цели (т.е. логарифмическое преобразование y). Конвейеры, напротив, преобразуют только наблюдаемые данные (X).

Тем не менее, если вы все еще хотите поместить эту информацию списка в метод преобразования, это будет примерно так:

def transform_str(one_line_text: str) -> dict:
    """ Transforms one line of text to dict features using manually extracted information"""
    # manually extracted information
    custom_features_dict = {'contact': ['contact details', 'e-mail'],
                            'demographic': ['gender', 'age', 'birth'],
                            'location': ['location', 'geo']}
    # simple tokenization. it can be improved using some text pre-processing lib
    tokenized_text = one_line_text.split(" ")
    output = dict()
    for feature,tokens in custom_features_dict.items():
        output[feature] = False
        for word in tokenized_text:
            if word in tokens:
                output[feature] = True
    return output

def transform(text_list: list) -> list:
    output = list()
    for one_line_text in text_list:
        output.append(transform_str(one_line_text))
    return output

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

person user3357359    schedule 13.01.2020
comment
Как тогда можно реализовать FeatureUnion (если мы не используем конвейер), чтобы объединить наши ручные функции с векторизатором tfidf? - person SaadH; 15.01.2020
comment
Это может быть реализовано так, как показано в ответе @chefhose. Я просто указал, что если ваши ручные функции содержат ту же информацию, что и цель, которую вы прогнозируете, возможно, возникла концептуальная проблема. Но если информация, содержащаяся в ручных функциях, не совсем соответствует вашей цели, то вы можете продолжить эту формулировку. - person user3357359; 16.01.2020