我必须训练一个用于垃圾邮件检测的分类器。
我拥有的数据集
我手头有一个带有[text, class]
的标记电子邮件数据集。我也有很多没有分类标签的电子邮件。
我想做什么。
我想使用gridsearchcv()
函数来估计我的模型的最佳超参数。其中一个参数与字典创建有关(如1-gram或2-gram、最小频率等)。我希望gridsearchcv()
函数能做的是使用我的管道中CountVectorizer
的整个电子邮件数据集(带标签的电子邮件+无标签的电子邮件)来创建字典。但我希望它只在标记的电子邮件上测试结果。所以,基本上我想使用整个数据集来创建字典,并且我想只在标记的数据集上使用交叉验证来估计参数。
任何帮助都将被视为:)
更新:
重要:要解决@AndreasMueller的问题,答案是:结果会有所不同,因为我还调整了CountVectorizer的参数,并且我使用了相反的文档频率。因此,我正在寻找一种方法,通过同时包含未标记的数据,使我的分类器更通用。
这就是我现在所拥有的:
pipeline = Pipeline([
('features', FeatureUnion([
('words', Pipeline([
('vect', CountVectorizer()),
('frequency_transform', TfidfTransformer())
])),
('url_feature', Contains_URL_Transformer()),
('html_feature', Contains_HTML_Transformer()),
('length_feature', Text_Length_Transformer()),
('response_feature', Contains_Re_Transformer())
])),
('clf', SVC())
])
parameters = {
'features__words__vect__min_df': (1, 3, 5),
'features__words__vect__token_pattern': (r"b[^Wd_]+b",),
'features__words__vect__binary': (False,),
'features__words__frequency_transform__use_idf' : (True,),
#'vect__max_features': (None, 5000, 10000, 50000),
'features__words__vect__ngram_range': ((1, 1), (1, 2)), # unigrams or bigrams
'clf__C': (1, 5, 10),
'clf__kernel': ('linear', 'rbf')
#'tfidf__use_idf': (True, False)
#'tfidf__norm': ('l1', 'l2'),
#'clf__alpha': (0.00001, 0.000001),
#'clf__penalty': ('l2', 'elasticnet'),
#'clf__n_iter': (10, 50, 80),
}
grid_search = GridSearchCV(pipeline, parameters, n_jobs=-1, verbose=1)
data_column = numpy.asarray(data['text'])
data_column = numpy.append(data_column, ['test'])
grid_search.fit(data_column, numpy.asarray(data['class']))
best_parameters = grid_search.best_estimator_.get_params()
for param_name in sorted(parameters.keys()):
print("t%s: %r" % (param_name, best_parameters[param_name]))
但我也有unlabled_data['text']
。我如何将data['text']
和unlabled_data['text']
的组合添加到管道中,以便根据该组合创建字典(并估计参数),但在标记的数据上进行测试。问题是,当我做grid_search.fit()
时,它使用提供的数据集来创建字典,我看不出有什么办法可以把所有的电子邮件都放在那里。
一个简单的解决方案迫使拟合数据保持不变,而不考虑交叉验证数据:
X_all = full dataset
class MyVectorizer(sklearn.feature_extraction.text.TfidfVectorizer):
def fit(self, X, y=None):
return super(MyVectorizer, self).fit(X_all)
def fit_transform(self, X, y=None):
return super(MyVectorizer, self).fit(X_all).transform(X)
使用它来代替上面的'words'
子管道。
一个可以说不那么棘手但更复杂的解决方案是:
- 连接已标记和未标记的数据,设置的标签后者对
-1
的实例 - 使用自定义交叉验证生成器,该生成器始终将未标记的实例保留在训练集中
- 在管道的特征提取后部分(此处为SVC)周围使用包装器来移除未标记的数据(注意,不能仅将其实现为CCD_ 11)。(也许从SVC扩展更简单,有点像
MyVectorizer
上面所做的,但不使用全局数据破解。)
这种方法的一个优点是,无论GridSearchCV
输入如何,它都适用(与通过全局变量注入完整数据的方法相反)。
示例代码:
def semisupervised_stratified_kfold(y, *args, **kwargs):
labeled_idx = np.flatnonzero(y != -1)
unlabeled_idx = np.flatnonzero(y == -1)
for train, test in StratifiedKFold(y[labelled_idx], *args, **kwargs):
train = np.concatenate([unlabeled_idx, labeled_idx.take(train)])
test = labeled_idx.take(test)
yield train, test
from sklearn.utils.metaestimators import if_delegate_has_method
class StripUnlabelled(sklearn.base.BaseEstimator):
def __init__(self, estimator):
self.estimator = sklearn.base.clone(estimator)
def fit(self, X, y, **kwargs):
return self.estimator.fit()
@if_delegate_has_method(delegate='estimator')
def predict(self, X):
return self.estimator.predict(X)
# and similar for decision_function, predict_proba, score, etc.
然后将GridSearchCV
的cv
参数设置为自定义生成器,将StripUnlabeled
包裹在SVC
实例周围,并在SVC参数名称前面加上estimator__
这实际上不会在所有数据上构建TFIDF模型,而是使用所有未标记的数据加上标记数据的所有训练折叠。
此外,请注意,所有使用Pipeline
的类似解决方案都将非常低效,因为在下游更改参数时不会缓存重复的工作,尽管已经提出了用于缓存管道部分的通用解决方案。
您可以使用预先指定的字典来完成此操作。然而,这并不重要。如果单词没有出现在训练数据中,它们的系数将为零,所以将它们添加到词汇表中不会有任何作用。