有时,机器学习模型的可能配置即使没有上千种,也有数百种,这使得手工找到最佳配置的可能性变得不可能,因此自动化是必不可少的。在处理复合特征空间时尤其如此,在复合特征空间中,我们希望对数据集中的不同特征应用不同的转换。一个很好的例子是将文本文档与数字数据相结合,然而,在scikit-learn中,我找不到关于如何自动建模这种类型的特征空间的信息。
使用scikit-learn管道可以更有效地工作,而不是手动将文本转换成词袋,然后再手动添加一些数字列。这篇文章将告诉你如何去做。
使用管道允许你将一系列转换步骤和评估器(分类器或回归器)视为单个模型,称为复合评估器。这不仅使你的代码保持整洁并防止训练集和测试集之间的信息泄漏,而且还允许你将转换步骤视为模型的超参数,然后通过网格搜索在超参数空间中优化模型。这意味着你可以在文本数据的同时试验不同的数值特征组合,以及不同的文本处理方法,等等。
在接下来的内容中,你将看到如何构建这样一个系统:将带标签的文本文档集合作为输入;自动生成一些数值特征;转换不同的数据类型;将数据传递给分类器;然后搜索特征和转换的不同组合,以找到性能最佳的模型。
模型构建
我使用的是垃圾短信数据集,可以从UCI机器学习库下载,它包含两列:一列短信文本和一个相应的标签列,包含字符串' Spam '和' ham ',这是我们必须预测的。和往常一样,整个项目代码都可以在GitHub上找到(https://github.com/job9931/Blog-notebooks/tree/main/automated_model_selection)。
第一步是定义要应用于数据集的转换。要在scikit-learn管道中包含数据转换,我们必须把它写成类,而不是普通的Python函数;一开始这可能听起来令人生畏,但它很简单。另一种方法是简单地定义一个普通的Python函数,并将其传递给FunctionTransformer类,从而将其转换为一个scikit-learn transformer对象。然而,在这里,我将向你展示更多的手工方法,这样你就可以看到实际发生了什么,因为我认为它有助于理解scikit-learn是如何工作的。
你创建一个类,它继承了scikit-learn提供的BaseEstimator和TransformerMixin类,它们提供了创建与scikit-learn管道兼容的对象所需的属性和方法。然后,在init()方法中包含函数参数作为类属性,并使用将用于转换数据集的函数体覆盖transform()方法。我在下面提供了三个例子。
fromsklearn.baseimportBaseEstimator, TransformerMixinclassCountWords(BaseEstimator,TransformerMixin): #createsadataframefromaseriesoftextdocumentsbycreatinganewcolumnnamedn_words, #whichcontainsthenumberofwordsineachdocumentdef__init__(self,new_col_name): self.new_col_name=new_col_namedeffit(self,series,y=None): returnselfdeftransform(self,series): n_words_col=series.apply(lambdax: len(x.split(' '))).rename(self.new_col_name) returnpd.concat([series, n_words_col], axis=1) classMeanWordLength(BaseEstimator,TransformerMixin): #createsacolumnmeanlengthofwordsinmessagedef__init__(self,text_column): self.text_column=text_columndeffit(self,dataframe,y=None): returnselfdeftransform(self,dataframe): dataframe['mean_word_length'] =dataframe[self.text_column].apply(lambdax: sum(map(len,x.split(' ') ))/len(x.split(' '))) returndataframeclassFeatureSelector(BaseEstimator,TransformerMixin): #createsanewdataframeusingonlycolumnslistedinattribute_namesdef__init__(self,attribute_names): self.attribute_names=attribute_namesdeffit(self, dataframe, y=None): returnselfdeftransform(self, dataframe): returndataframe[self.attribute_names].values
管道中使用的自定义转换器对象。在每个示例中,fit()方法不执行任何操作,所有工作都体现在transform()方法中。
前两个转换符用于创建新的数字特征,这里我选择使用文档中的单词数量和文档中单词的平均长度作为特征。由于我们的数据集只包含两列,文本和标签,我们的文本在分离标签列之后被存储为熊猫系列,我们应该在项目的一开始就这样做。因此,CountWords.transform()被设计为接受一个序列并返回一个数据流,因为我将使用它作为管道中的第一个转换器。
final transformer FeatureSelector将允许我们将各种特性作为模型的超参数。它的transform()方法接受列名列表,并返回一个仅包含这些列的DataFrame;通过向它传递不同的列名列表,我们可以在不同的特征空间中搜索以找到最佳的一个。这三个转换器提供了我们构建管道所需的所有附加功能。
构建管道
最终的管道由三个组件构成:初始管道对象、ColumnTransformer对象和估计器。第二个组件ColumnTransformer是0.20版本中引入的一个方便的类,它允许你对数据集的指定列应用单独的转换。在这里,我们将使用它将CountVectorizer应用到文本列,并将另一个管道num_pipeline应用到数值列,该管道包含FeatureSelector和scikit-learn的SimpleImputer类。整个管道结构如图所示:
管道示意图。整个对象(称为复合估计器)可以用作模型;所有的转换器和估计器对象以及它们的参数,都成为我们模型的超参数。
工作流程如下
- 一系列文档进入管道,CountWords和MeanWordLength在管道中创建两个名为n_words和mean_word_length的数字列。
- 文本列被传递给CountVectorizer,而n_words和mean_word_length首先通过FeatureSelector,然后是SimpleImputer。
- 转换后的数据集被传递给估计器对象。
fromsklearn.pipelineimportPipelinefromsklearn.composeimportColumnTransformerfromsklearn.imputeimportSimpleImputerfromsklearn.feature_extraction.textimportCountVectorizerfromsklearn.svmimportSVC#Definethenamesofthetextandnumericalfeaturestext_features='text'numerical_features= ['n_words','mean_word_length'] #Createtheinitialpipelinewhichgeneratesnumericalcolumnspipeline_1=Pipeline([('n_words',CountWords('n_words')), ('mean_length',MeanWordLength('text'))]) #ThenuseColumnTransformertoprocessthenumericalcolumnsandthetextcolumnseparately. #Wedefineandapplynum_pipelinetothenumericalcolumnsandCountVectorizertothetextcolumnnum_pipeline=Pipeline([('selector',FeatureSelector(numerical_features)), ('imp',SimpleImputer())]) pipeline_2=ColumnTransformer([ ("txt", CountVectorizer(), 'text'), ("num", num_pipeline,['n_words','mean_word_length']), ]) #Buildthefinalpipelineusingpipeline_1andpipeline_2andanestimator, inthiscaseSVC() pipeline=Pipeline([('add_numerical',pipeline_1), ('transform',pipeline_2), ('clf',SVC())])
最终的管道由初始化对象、ColumnTransformer对象和估计器对象组成。注意,ColumnTransformer可以将整个管道应用于某些列。
在上面的代码示例中,我们使用CountVectorizer和SimpleImputer的默认参数,同时保留数字列,并使用支持向量分类器作为估计器。这最后一个管道是我们的复合估计器,它里面的每个对象,以及这些对象的参数,都是一个超参数,我们可以自由地改变它。这意味着我们可以搜索不同的特征空间、不同的向量化设置和不同的估计器对象。
通过网格搜索选择最佳模型
使用复合估计器设置,很容易找到最佳执行模型;你所需要做的就是创建一个字典,指定想要改变的超参数和想要测试的值。然后将其传递给scikit-learn的GridSearchCV类,该类对每个超参数值组合使用交叉验证来评估模型,然后返回最好的。
fromsklearn.model_selectionimportGridSearchCV#paramsisadictionary, thekeysarethehyperparameterandthevaulesarealistofvalues#tosearchover. params= [ {'transform__txt__max_features':[None,100,10], 'transform__num__selector__attribute_names': [['n_words'], ['mean_word_length'], ['n_words','mean_word_length']]} ] #GridSearchCVbydefaultstratifiesourcross-validation#andretrainsmodelonthebestsetofhyperparametersmodel=GridSearchCV(pipeline,params,scoring='balanced_accuracy',cv=5) model.fit(X_train, y_train) #displayalloftheresultsofthegridsearchprint(model.cv_results_) #displaythemeanscoresforeachcombinationofhyperparametersprint(model.cv_results_[model.cv_results_['mean_test_score']])
参数网格被定义为一个字典,键是超参数,值是要搜索的值的列表。然后将其与复合估计数器一起传递给GridSearchCV,并将其与训练数据相匹配。
我们的复合估计器总共有65个可调参数,但是,这里只改变了两个参数:使用的数字列和CountVectorizer的max_features参数,该参数设置词汇表中使用的单词的最大数量。在代码中,你可以看到如何获得所有可用超参数的列表。下面是绘制在超参数空间上的平均平衡精度的可视化图。
当我们只使用一个数字列n_words并使用词汇表中的所有单词(即max_features = None)时,可以获得最佳性能。在交叉验证期间,该模型的平衡精度为0.94,在测试集上评估时为0.93。注意,如果你自己运行笔记本,确切的数字可能会改变。
在超参数网格上绘制了平衡精度图,显示了模型性能如何在超参数空间上变化。
总结
我们已经讨论了很多,特别是,如何通过设置一个复合评估器来自动化整个建模过程,复合评估器是包含在单个管道中的一系列转换和评估器。这不仅是一个很好的实践,而且是搜索大型超参数空间的唯一可行方法,在处理复合特征空间时经常出现这种情况。我们看到了将文本数据与数字数据组合在一起的示例,但是对于任何数据类型都可以很容易地遵循相同的过程,从而使你能够更快、更有效地工作。