使用管道和网格搜索执行特征选择

Posted

技术标签:

【中文标题】使用管道和网格搜索执行特征选择【英文标题】:Perform feature selection using pipeline and gridsearch 【发布时间】:2019-05-19 09:29:24 【问题描述】:

作为研究项目的一部分,我想选择优化文本分类任务结果的预处理技术和文本特征的最佳组合。为此,我使用的是 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 不同的组合,因此还有不同的功能。

最终目标是在管道中结合预处理技术和特征,以便使用网格搜索优化分类结果:

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 中? @VivekKumar feat_extractor 应该只适用于原始原始文本。我知道它需要 CountVectorizer 的输出,但这只是一个糟糕的尝试来展示我正在尝试做的事情。 CountVectorizer 只允许一组特定的特征(例如 n-gram),我想执行更多的特征提取(例如情感分析) 如果你想在相同的数据上同时使用这两个东西(vectorizerfeat_extractor),FeatureUnion 可以提供帮助。那么现在关于func_Afunc_B 等:vectorizer__preprocessorfeat_extractor 是否相同? 不,它们应该是不同的功能。对于 vectorizer__preprocessor,funcA 将结合预处理 1 和 2(例如小写字母 + emojies 转换器),而 feat_extractor 的 func_A 将结合特征 1 和 2 或任何其他可能的组合(例如 n-grams + 情感分析 + 推文长度)。预处理函数只包含数据清洗特征的组合,而特征提取函数只包含文本特征的组合。 是的。我将发布一个简短的回答,大致描述一下。 【参考方案1】:

根据您的描述,此解决方案非常粗略,具体答案取决于所使用的数据类型。在制作管道之前,让我们了解CountVectorizer 如何作用于其中传递的raw_documents。本质上,this is the line 将字符串文档处理成标记,

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

然后将其计数并转换为计数矩阵。

所以这里发生的是:

    decode:只需决定如何从文件中读取数据(如果指定)。对我们没用,我们已经将数据放入列表中。

    preprocess:如果'strip_accents''lowercase'CountVectorizer 中是True,它会执行以下操作。其他什么都没有

    strip_accents(x.lower())
    

    同样,没有用,因为我们正在将小写功能转移到我们自己的预处理器中,并且不需要去除重音,因为我们已经在字符串列表中拥有数据。

    tokenize:将删除所有标点符号并仅保留长度为 2 或以上的字母数字单词,并返回单个文档的标记列表(列表元素)

    lambda doc: token_pattern.findall(doc)
    

    请记住这一点。如果您想自己处理标点符号和其他符号(决定保留一些并删除其他符号),那么最好也更改默认的 token_pattern=’(?u)\b\w\w+\b’CountVectorizer

      _word_ngrams:此方法将首先从上一步的标记列表中删除停用词(作为上述参数提供),然后计算由 CountVectorizer 中的 ngram_range 参数定义的 n_grams。如果您想以自己的方式处理"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())
                ])

其中CustomPreprocessorCustomFeatureExtractor 定义为:

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) 另外,我没有使用预构建的 CountVectorizer,而是考虑自己实现它的功能以获得更大的灵活性。我需要的唯一 CountVectorizer 功能几乎是 ngrams、tokenizer 和小写字母,反正我自己已经实现了。也许这样做我只能在管道中使用 FeatureExtractor? 雄伟的回答,我非常喜欢。除了复杂性问题,您是否建议保留 CountVectorizer 而不是编写我自己的函数? @Khabz 正如我上面所描述的,您唯一没有做的事情和CountVectorizer 正在处理的是标记化和 ngram 特征生成。您可以尝试自己的方法。 @Khabz fit() 仅用于学习数据(例如您想作为文本中的特征的单词或 ngram,或查找数字中数据的最大值和最小值等)。您在fit 期间存储这些值。现在在transform() 期间,您可以使用这些学习值来有意义地更改数据(例如在新数据中找到相同的单词或 ngram 并从中制作特征)。

以上是关于使用管道和网格搜索执行特征选择的主要内容,如果未能解决你的问题,请参考以下文章

情感分析管道,使用特征选择时获取正确特征名称的问题

网格搜索 SVM-anova 的超参数并在 Sklearn 中获得选择的特征

使用 scikit-learn 对 SVR 进行递归特征消除和网格搜索

管道中的python特征选择:如何确定特征名称?

在 scikit-learn 中结合递归特征消除和网格搜索

使用 scikit-learn 进行递归特征消除和网格搜索