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

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

Существует ряд методов для объединения функций и алгоритмов, но я хочу в полной мере использовать преимущества конвейеров sklearn и протестировать все различные (допустимые) возможности, используя поиск по сетке для поиска окончательной комбинации функций.

Моим первым шагом было построить конвейер, который выглядит следующим образом:

# Run a vectorizer with a predefined tweet tokenizer and a Naive Bayes

pipeline = Pipeline([
    ('vectorizer', CountVectorizer(tokenizer = tweet_tokenizer)),
    ('nb', MultinomialNB())
])

parameters = {
'vectorizer__preprocessor': (None, preprocessor)
}

gs =  GridSearchCV(pipeline, parameters, cv=5, n_jobs=-1, verbose=1)

В этом простом примере векторизатор токенизирует данные с помощью tweet_tokenizer, а затем проверяет, какой вариант предварительной обработки («Нет» или предопределенная функция) дает лучший результат.

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

def preprocessor(tweet):
    # Data cleaning
    tweet = URL_remover(tweet) # Removing URLs
    tweet = mentions_remover(tweet) # Removing mentions
    tweet = email_remover(tweet) # Removing emails
    tweet = irrelev_chars_remover(tweet) # Removing invalid chars
    tweet = emojies_converter(tweet) # Translating emojies
    tweet = to_lowercase(tweet) # Converting words to lowercase
    # Others
    tweet = hashtag_decomposer(tweet) # Hashtag decomposition
    # Punctuation may only be removed after hashtag decomposition  
    # because it considers "#" as punctuation
    tweet = punct_remover(tweet) # Punctuation 
    return tweet

«Простым» решением для объединения всех различных методов обработки было бы создание отдельной функции для каждой возможности (например, funcA: proc1, funcB: proc1 + proc2, funcC: proc1 + proc3 и т. д.) И установка параметра сетки следующим образом :

parameters = {
   'vectorizer__preprocessor': (None, funcA, funcB, funcC, ...)
}

Хотя это, скорее всего, сработает, это не жизнеспособное или разумное решение для этой задачи, тем более что существуют различные комбинации 2^n_features и, следовательно, функции.

Конечная цель - объединить в конвейере как методы предварительной обработки, так и функции, чтобы оптимизировать результаты классификации с помощью gridsearch:

pipeline = Pipeline([
    ('vectorizer', CountVectorizer(tokenizer = tweet_tokenizer)),
    ('feat_extractor' , feat_extractor)
    ('nb', MultinomialNB())
])

 parameters = {
   'vectorizer__preprocessor': (None, funcA, funcB, funcC, ...)
   'feat_extractor': (None, func_A, func_B, func_C, ...)
 }

Есть ли более простой способ получить это?

Что должен делать ('feat_extractor' , feat_extractor) после CounteVectorizer? Конвейер будет передавать данные через CountVectorizer, затем новые данные (матрица подсчета, а не слова) будут переданы в feat_extractor. Это то, что вы хотите? Или вы хотите, чтобы feat_extractor был включен в preprocesser, как вы описали выше?

Vivek Kumar 19.12.2018 07:55

@VivekKumar feat_extractor должен работать только с исходным необработанным текстом. Я знаю, что это требует вывода CountVectorizer, но это была неудачная попытка показать, что я пытаюсь сделать. CountVectorizer позволяет использовать только определенный набор функций (например, n-граммы), и я хочу выполнить извлечение дополнительных функций (например, анализ тональности).

GRoutar 19.12.2018 13:08

Если вам нужны обе эти вещи (vectorizer и feat_extractor) для одних и тех же данных, FeatureUnion может помочь. Итак, теперь о func_A, func_B и т. д. Они одинаковы для vectorizer__preprocessor и feat_extractor?

Vivek Kumar 19.12.2018 13:23

Нет, у них должны быть разные функции. Для vectorizer__preprocessor funcA будет сочетать предварительную обработку 1 и 2 (например, строчные буквы + конвертер смайлов), а func_A feat_extractor будет сочетать функции 1 и 2 или любую другую возможную комбинацию (например, n-граммы + анализ настроений + длина твита). Функции предварительной обработки будут содержать только комбинации функций очистки данных, в то время как функции извлечения функций будут содержать только комбинации текстовых функций.

GRoutar 19.12.2018 13:32

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

GRoutar 19.12.2018 13:40

да. Я опубликую ответ, в котором вкратце расскажу об этом.

Vivek Kumar 19.12.2018 13:59

Я ценю. Я читал об исчерпывающем выборе функций, но он работает с каждым отдельным столбцом, а не с функцией в целом. На объединение 10 тыс. Столбцов уйдет, вероятно, неделя вычислительного времени.

GRoutar 19.12.2018 14:03

Вы говорите, что "The feat_extractor should only work on the original raw text.". Как вы планируете проводить анализ настроений на основе необработанных данных? Потребовалось бы, чтобы вы тренировались на настроениях данных (которые отличаются от фактических классов)?

Vivek Kumar 19.12.2018 15:23

На самом деле я еще не думал об этом. Это возможно, но я, скорее всего, воспользуюсь чем-то вроде TextBlob и вычислю тональность, например "tweet.sentiment.polarity"

GRoutar 19.12.2018 15:37
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
2
9
958
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

return lambda doc: self._word_ngrams(tokenize(preprocess(self.decode(doc))), stop_words)

которые затем просто подсчитываются и преобразуются в матрицу подсчета.

Итак, вот что происходит:

  1. decode: Просто решите, как читать данные из файла (если он указан). Бесполезно для нас, где у нас уже есть данные в списке.
  2. preprocess: выполняет следующие действия, если 'strip_accents' и 'lowercase' являются True в CountVectorizer. Больше ничего

    strip_accents(x.lower())
    

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

  3. tokenize: удалит все знаки препинания и сохранит только буквенно-цифровые слова длиной 2 или более, а также вернет список токенов для одного документа (элемент списка)

    lambda doc: token_pattern.findall(doc)
    

    Об этом следует помнить. Если вы хотите самостоятельно обрабатывать знаки препинания и другие символы (решив оставить одни и удалить другие), то лучше также изменить значение token_pattern=’(?u)\b\w\w+\b’ по умолчанию для CountVectorizer.

    1. _word_ngrams: этот метод сначала удалит стоп-слова (предоставленные как параметр выше) из списка токенов из предыдущего шага, а затем вычислит n_grams, как определено параметром ngram_range в CountVectorizer. Это также следует иметь в виду, если вы хотите обращаться с "n_grams" по-своему.

Примечание: Если для анализатора задано значение 'char', то шаг tokenizer выполняться не будет и n_grams будут строиться из символов.

Итак, теперь перейдем к нашему конвейеру. Я думаю, вот такая структура может работать здесь:

X --> combined_pipeline, Pipeline
            |
            |  Raw data is passed to Preprocessor
            |
            /
         Preprocessor 
                 |
                 |  Cleaned data (still raw texts) is passed to FeatureUnion
                 |
                 /
              FeatureUnion
                      |
                      |  Data is duplicated and passed to both parts
       _______________|__________________
      |                                  |
      |                                  |                         
      /                                /
   CountVectorizer                  FeatureExtractor
           |                                  |   
           |   Converts raw to                |   Extracts numerical features
           |   count-matrix                   |   from raw data
           /________________________________/
                             |
                             | FeatureUnion combines both the matrices
                             |
                             /
                          Classifier

Теперь перейдем к коду. Вот как выглядит конвейер:

# Imports
from sklearn.svm import SVC
from sklearn.pipeline import FeatureUnion, Pipeline

# Pipeline
pipe = Pipeline([('preprocessor', CustomPreprocessor()), 
                 ('features', FeatureUnion([("vectorizer", CountVectorizer()),
                                            ("extractor", CustomFeatureExtractor())
                                            ]))
                 ('classifier', SVC())
                ])

Где CustomPreprocessor и CustomFeatureExtractor определены как:

from sklearn.base import TransformerMixin, BaseEstimator

class CustomPreprocessor(BaseEstimator, TransformerMixin):
    def __init__(self, remove_urls=True, remove_mentions=True, 
                 remove_emails=True, remove_invalid_chars=True, 
                 convert_emojis=True, lowercase=True, 
                 decompose_hashtags=True, remove_punctuations=True):
        self.remove_urls=remove_urls
        self.remove_mentions=remove_mentions
        self.remove_emails=remove_emails
        self.remove_invalid_chars=remove_invalid_chars
        self.convert_emojis=convert_emojis
        self.lowercase=lowercase
        self.decompose_hashtags=decompose_hashtags
        self.remove_punctuations=remove_punctuations

    # You Need to have all the functions ready
    # This method works on single tweets
    def preprocessor(self, tweet):
        # Data cleaning
        if self.remove_urls:
            tweet = URL_remover(tweet) # Removing URLs

        if self.remove_mentions:
            tweet = mentions_remover(tweet) # Removing mentions

        if self.remove_emails:
            tweet = email_remover(tweet) # Removing emails

        if self.remove_invalid_chars:
            tweet = irrelev_chars_remover(tweet) # Removing invalid chars

        if self.convert_emojis:
            tweet = emojies_converter(tweet) # Translating emojies

        if self.lowercase:
            tweet = to_lowercase(tweet) # Converting words to lowercase

        if self.decompose_hashtags:
            # Others
            tweet = hashtag_decomposer(tweet) # Hashtag decomposition

        # Punctuation may only be removed after hashtag decomposition  
        # because it considers "#" as punctuation
        if self.remove_punctuations:
            tweet = punct_remover(tweet) # Punctuation 

        return tweet

    def fit(self, raw_docs, y=None):
        # Noop - We dont learn anything about the data
        return self

    def transform(self, raw_docs):
        return [self.preprocessor(tweet) for tweet in raw_docs]

from textblob import TextBlob
import numpy as np
# Same thing for feature extraction
class CustomFeatureExtractor(BaseEstimator, TransformerMixin):
    def __init__(self, sentiment_analysis=True, tweet_length=True):
        self.sentiment_analysis=sentiment_analysis
        self.tweet_length=tweet_length

    # This method works on single tweets
    def extractor(self, tweet):
        features = []

        if self.sentiment_analysis:
            blob = TextBlob(tweet)
            features.append(blob.sentiment.polarity)

        if self.tweet_length:
            features.append(len(tweet))

        # Do for other features you want.

        return np.array(features)

    def fit(self, raw_docs, y):
        # Noop - Again I am assuming that We dont learn anything about the data
        # Definitely not for tweet length, and also not for sentiment analysis
        # Or any other thing you might have here.
        return self

    def transform(self, raw_docs):
        # I am returning a numpy array so that the FeatureUnion can handle that correctly
        return np.vstack(tuple([self.extractor(tweet) for tweet in raw_docs]))

Наконец, сетку параметров теперь можно легко сделать, например:

param_grid = ['preprocessor__remove_urls':[True, False],
              'preprocessor__remove_mentions':[True, False],
              ...
              ...
              # No need to search for lowercase or preprocessor in CountVectorizer 
              'features__vectorizer__max_df':[0.1, 0.2, 0.3],
              ...
              ...
              'features__extractor__sentiment_analysis':[True, False],
              'features__extractor__tweet_length':[True, False],
              ...
              ...
              'classifier__C':[0.01, 0.1, 1.0]
            ]

Приведенный выше код предназначен для исключения "to create a different function for each possibility (e.g. funcA: proc1, funcB: proc1 + proc2, funcC: proc1 + proc3, etc.)". Просто сделайте True, False и GridSearchCV справится с этим.

Обновлять: Если вы не хотите иметь CountVectorizer, вы можете удалить его из конвейера и сетки параметров, и новый конвейер будет:

pipe = Pipeline([('preprocessor', CustomPreprocessor()), 
                 ("extractor", CustomFeatureExtractor()),
                 ('classifier', SVC())
                ])

Затем убедитесь, что в CustomFeatureExtractor реализованы все необходимые функции. Если это станет слишком сложным, вы всегда можете сделать более простые экстракторы и объединить их вместе в FeatureUnion вместо CountVectorizer.

Мне нравится представленная архитектура, но я не понимаю, как можно протестировать различные наборы функций. Конвейер для каждой возможной итерации? Или настроить параметры gridsearch? например 'featureExtractor__ngram': (Нет, MyNgram)

GRoutar 19.12.2018 16:40

Кроме того, вместо использования предварительно созданного CountVectorizer я решил сам реализовать его функции для большей гибкости. Единственные функции CountVectorizer, которые мне нужны, - это в значительной степени ngram, токенизатор и строчные буквы, которые я уже реализовал сам. Возможно, благодаря этому я смогу использовать FeatureExtractor только в конвейере?

GRoutar 19.12.2018 16:43

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

GRoutar 19.12.2018 16:54

@Khabz Как я описал выше, единственное, что вы не делаете, а CountVectorizer не обрабатывает, - это токенизация и генерация функций ngram. Вы можете попробовать свои собственные методы для этого.

Vivek Kumar 19.12.2018 16:59

@Khabz Также помните, что когда вы делаете это по-своему, вам нужно будет выполнить обучающую часть в fit(), потому что вам нужно сохранить найденные функции, а затем в transform() использовать эти функции для преобразования новых данных.

Vivek Kumar 19.12.2018 17:13

Эквивалентно ли преобразование fit + извлечению функций и одной горячей кодировки (или чего-то еще) в матрицу?

GRoutar 19.12.2018 18:09

@Khabz fit() предназначен только для изучения данных (например, слов или цифр, которые вы хотели бы сделать в качестве функций в тексте, или нахождения максимальных и минимальных значений данных в числах и т. д.). Вы сохраняете такие значения во время fit. Теперь во время transform() вы используете эти изученные значения для значимого изменения данных (например, находите те же слова или числа в новых данных и создавайте из них функции).

Vivek Kumar 20.12.2018 07:25

Не могли бы вы привести пример реализации экстрактора ngram?

GRoutar 24.12.2018 18:43

@Khabz Это зависит от того, чего вы хотите. Вы хотите извлечь из текста все нграммы требуемого размера или только те, которые были просмотрены во время обучения? Что ты будешь делать после этого?

Vivek Kumar 26.12.2018 10:41

Моя цель состояла бы в том, чтобы собрать только нграммы обучающих данных и превратить их в функции (1 - содержит нграмму, 0 - не содержит).

GRoutar 26.12.2018 22:32

Ну, CountVectorizer просто делает это. Проверьте параметр binary на странице документации] (scikit-learn.org/stable/modules/generated/…). Вы можете рассмотреть мою более раннюю установку, содержащую CountVectorizer, для этого.

Vivek Kumar 27.12.2018 08:01

Другие вопросы по теме