scikit-learn 特征提取

2023-02-20 14:35 更新

sklearn.feature_extraction模块可用于从机器学习算法支持的格式中提取特征,这些特征由包含文本和图像等格式的数据集构成。

**注意:**特征提取与特征选择有很大不同:前者在于将任意数据(例如文本或图像)转换成可用于机器学习的数字特征。后者是应用于这些功能的机器学习技术。

6.2.1 从字典加载特征

DictVectorizer类可用于将以标准Python dict对象列表为表示形式的要素数组转换为scikit-learn估计器使用的NumPy / SciPy形式。

尽管处理速度不是特别快,但是Python dict具有以下优点:易于使用,稀疏(缺少的特征不需要存储)以及除了特征值之外还存储特征名称。

DictVectorizer通过实现one-of-K或者“独热”编码来分类(又称虚设,离散)特征。类别特征是成对的“属性--值”形式,其中值被限制为不定序的符合要求的列表(例如主题标识符,对象类型,标签,名称等)。

以下示例中,“city”是分类属性,而“temperature”是传统的数字特征:

>>> measurements = [
...     {'city': 'Dubai', 'temperature': 33.},
...     {'city': 'London', 'temperature': 12.},
...     {'city': 'San Francisco', 'temperature': 18.},
... ]

>>> from sklearn.feature_extraction import DictVectorizer
>>> vec = DictVectorizer()

>>> vec.fit_transform(measurements).toarray()
array([[ 1.,  0.,  0., 33.],
       [ 0.,  1.,  0., 12.],
       [ 0.,  0.,  1., 18.]])

>>> vec.get_feature_names()
['city=Dubai', 'city=London', 'city=San Francisco', 'temperature']

自然语言处理模型进行训练序列分类器的过程中,DictVectorizer 类在表示形式转换时很有用,通常通过提取特定感兴趣词周围的特征窗口来起作用。

例如,假设有第一个算法用来提取词性(PoS)标签,我们希望将其用作训练序列分类器(例如分块器)的补充标签。以下命令可能是这样的窗口:在 ‘The cat sat on the mat.’句子中的“ sat”一词周围提取特征:

>>> pos_window = [
...     {
...         'word-2': 'the',
...         'pos-2': 'DT',
...         'word-1': 'cat',
...         'pos-1': 'NN',
...         'word+1': 'on',
...         'pos+1': 'PP',
...     },
...     # in a real application one would extract many such dictionaries
... ]

可以将该描述矢量化为适合呈递给分类器的稀疏二维矩阵(可能在通过管道传递到 text.TfidfTransformer中进行归一化之后):

>>> vec = DictVectorizer()
>>> pos_vectorized = vec.fit_transform(pos_window)
>>> pos_vectorized
<1x6 sparse matrix of type '<... 'numpy.float64'>'
    with 6 stored elements in Compressed Sparse ... format>
>>> pos_vectorized.toarray()
array([[1., 1., 1., 1., 1., 1.]])
>>> vec.get_feature_names()
['pos+1=PP', 'pos-1=NN', 'pos-2=DT', 'word+1=on', 'word-1=cat', 'word-2=the']

可以想象,如果围绕文档语料库的每个单词提取这样的上下文,则生成的矩阵将非常宽(许多“one-hot-features"),并且大多数情况下大多数时间该值都为零。为了使生成的数据结构能够适合内存,DictVectorizer类默认使用scipy.sparse矩阵而不是numpy.ndarray

6.2.2 特征哈希

FeatureHasher类是一种高速,低内存消耗的矢量化程序,它使用一种被称为 特征哈希(或称为“哈希技巧”)的技术。像矢量化程序那样,与其构建训练中遇到的特征的哈希表,不如让FeatureHasher 实例将哈希函数应用于特征,以直接确定其在样本矩阵中的列索引。结果是以可检查性为代价提高了速度,减少了内存使用;哈希器无法记住输入特征的外观,并且没有inverse_transform方法。

由于散列函数可能导致(不相关)特征之间的冲突,因此使用带符号散列函数,并且存储在输出矩阵中的特征值的符号是由散列值的符号确定。这样,冲突可能会抵消而不是累积误差,并且任何输出要素的值的预期均值为零。alternate_sign=True时,此机制默认已启用,并且对于较小的哈希表(n_features < 10000)特别有用。对于较大的哈希表,可以将其禁用,以允许将输出传递到要求非负输入的估计器,例如 sklearn.naive_bayes.MultinomialNB 或者 sklearn.feature_selection.chi2

FeatureHasher根据构造函数参数 input_type,接受映射(如Python中的dict 及其在collections模块中的变体),以及成对出现的(feature, value)或字符串。映射被视为 (feature, value) 对的列表,而单个字符串的隐含值为1,因此 ['feat1', 'feat2', 'feat3'] 被解释为 [('feat1', 1), ('feat2', 1), ('feat3', 1)] 。如果一个特征在一个样本中多次出现,则相关的值将被求和(('feat', 2)('feat', 3.5)成为('feat', 5.5))。 FeatureHasher 的输出始终是CSR格式的scipy.sparse 矩阵。

特征散列可以用于文档分类,但是与text.CountVectorizer不同,FeatureHasher不会进行字符分割或除Unicode-to-UTF-8编码外的任何其他预处理; 请参阅下面的 使用散列技巧对大型文本语料库进行矢量化处理,以获取组合的标记器/哈希器。

例如,有一个词级别的自然语言处理任务,需要从 (token, part_of_speech)键值对中提取特征。可以使用 Python 生成器函数来提取功能:

def token_features(token, part_of_speech):
    if token.isdigit():
        yield "numeric"
    else:
        yield "token={}".format(token.lower())
        yield "token,pos={},{}".format(token, part_of_speech)
    if token[0].isupper():
        yield "uppercase_initial"
    if token.isupper():
        yield "all_uppercase"
    yield "pos={}".format(part_of_speech)

然后, 可以使用以下命令构造要传入FeatureHasher.transformraw_X

raw_X = (token_features(tok, pos_tagger(tok)) for tok in corpus)

并通过以下方式传入哈希器:

hasher = FeatureHasher(input_type='string')
X = hasher.transform(raw_X)

得到一个scipy.sparse矩阵X

注意生成器使用的理解,在特征提取中引入了惰性:标记(tokens)仅根据哈希器的要求进行处理。

6.2.2.1 实施细节

FeatureHasher 使用的是MurmurHash3的32位变体,(由于scipy.sparse的限制)致使当前支持的最大特征数量为

哈希技巧的原始形式源自Weinberger等人用两个单独的哈希函数 分别确定特征的列索引和标记。当前实现基于假设:MurmurHash3的符号位与其他位独立。

由于使用简单的模数将哈希函数转换为列索引,因此建议使用2的幂次方作为n_features参数。否则特征不会均匀地映射到列中。

参考文献:

6.2.3 文本特征提取

6.2.3.1 词袋表示法

文本分析是机器学习算法的主要应用领域。但是,原始数据,符号序列不能直接输入到算法本身,因为大多数算法期望的是具有固定大小的数值特征向量,而不是具有可变长度的原始文本文档。

为了解决这个问题,scikit-learn提供了从文本内容中提取数字特征的最常用方法,即:

  • 标记字符串并为每个可能的标记提供整数ID,例如通过使用空格和标点符号作为标记分隔符。
  • 统计每个文档中标记出现的频次。
  • 大多数样本/文档中采用通过减少重要性标记来进行标准化和加权。

在此方案中,特征和样本被定义如下:

  • 每个单独的标记出现频率(已标准化或未标准化)都被视为特征
  • 给定文档中的所有标记频率的向量被认为是一个多元样本

因此,文档语料库可以由矩阵表示,每行对应一个文档,每列对应语料库中出现的标记(如单个词)。

我们将向量化称为将文本文档集合转换为数字特征向量的一般过程。这种特定的策略(标记化,计数和归一化)称为“ 词袋” 或“ n-gram袋”表示。通过单词出现来描述文档,而完全忽略文档中单词的相对位置信息。

6.2.3.2 稀疏性

由于大多数文档通常只会使用文档语料库中很小一部分的单词,因此生成的矩阵中很多特征值为零(通常超过总特征值的99%)。

例如,一个包含10,000个短文本文档(例如电子邮件)的集合将使用一个词汇表,该词汇表的总大小约包含100,000个不重复的单词,而每个文档将单独使用100至1000个不重复的单词。

为了能够将这样的矩阵存储在内存中并且还可以加快矩阵/向量的代数运算,通常使用例如scipy.sparse包中的稀疏表示来实现。

6.2.3.3 Vectorizer的常见用法

CountVectorizer 在单个类中实现标记化和出现计数:

>>> from sklearn.feature_extraction.text import CountVectorizer

该模型具有许多参数,但是参数默认值是相当合理的(有关详细信息,请参见参考文档):

>>> vectorizer = CountVectorizer()
>>> vectorizer
CountVectorizer()

让我们用它来对一个由文本文档组成的简约文集进行标记和词频统计:

>>> corpus = [
...     'This is the first document.',
...     'This is the second second document.',
...     'And the third one.',
...     'Is this the first document?',
... ]
>>> X = vectorizer.fit_transform(corpus)
>>> X
<4x9 sparse matrix of type '<... 'numpy.int64'>'
    with 19 stored elements in Compressed Sparse ... format>

默认配置通过提取至少包含2个字母的单词来提取字符串。执行这一步的特定功能可以被显式调用:

>>> analyze = vectorizer.build_analyzer()
>>> analyze("This is a text document to analyze.") == (
...     ['this', 'is', 'text', 'document', 'to', 'analyze'])
True

在拟合过程中,分析器会找到并分配一个唯一的整数索引给每个词语,该索引对应于所得矩阵中的一列。可以按以下方式检索这些列的解释:

>>> vectorizer.get_feature_names() == (
...     ['and', 'document', 'first', 'is', 'one',
...      'second', 'the', 'third', 'this'])
True

>>> X.toarray()
array([[0, 1, 1, 1, 0, 0, 1, 0, 1],
       [0, 1, 0, 1, 0, 2, 1, 0, 1],
       [1, 0, 0, 0, 1, 0, 1, 1, 0],
       [0, 1, 1, 1, 0, 0, 1, 0, 1]]...)

从特征名称到列索引的逆映射存储在矢量化器的vocabulary_属性中:

>>> vectorizer.vocabulary_.get('document')
1

因此,将来在对transform方法进行调用时,训练语料库中未出现的单词将被完全忽略:

>>> vectorizer.transform(['Something completely new.']).toarray()
array([[0, 0, 0, 0, 0, 0, 0, 0, 0]]...)

注意,在前一语料库中,第一个文档和最后一个文档恰好具有相同的词,因此被编码为相等的向量。特别是,我们失去了最后一个文档是疑问形式的信息。为了保留本地指令信息,除了提取 1-grams(个别词)之外,我们还可以提取 2-grams 的单词:

>>> bigram_vectorizer = CountVectorizer(ngram_range=(1, 2),
...                                     token_pattern=r'\b\w+\b', min_df=1)
>>> analyze = bigram_vectorizer.build_analyzer()
>>> analyze('Bi-grams are cool!') == (
...     ['bi', 'grams', 'are', 'cool', 'bi grams', 'grams are', 'are cool'])
True

因此,此矢量化器提取的词汇量要大得多,同时还可以解决本地定位模式中编码的歧义:

>>> X_2 = bigram_vectorizer.fit_transform(corpus).toarray()
>>> X_2
array([[0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0],
       [0, 0, 1, 0, 0, 1, 1, 0, 0, 2, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0],
       [1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0],
       [0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1]]...)

特别是“Is this”的疑问句形式仅出现在最后的文档中:

>>> feature_index = bigram_vectorizer.vocabulary_.get('is this')
>>> X_2[:, feature_index]     
array([0, 0, 0, 1]...)

6.2.3.3.1 停用词的使用

停用词是指诸如“和”,“这”,“他”之类的词,它们被认为在表示文本内容方面没有提供任何信息,可以将其删除以避免将其理解为参与预测的信息。然而有时候,类似的词对于预测很有用,例如在对写作风格或性格进行分类时。

我们提供的“英语”停用词列表中有几个已知问题。它并非旨在成为通用的“一刀切”解决方案,因为某些任务可能需要定制的解决方案。有关更多详细信息,请参见[NQY18]。

请谨慎选择停用词列表。流行的停用词列表可能包含对某些任务非常有用的词(例如计算机)

您还应该确保停用词列表具有与矢量化器中使用的是相同的预处理和标记。单词CountVectorizer的默认标记分配器分割成,所以如果在停止词列表中,但不在,会被保留在转换后的文本中。我们的向量化器将尝试识别并警告某些不一致之处。

参考文献

6.2.3.4 TF–IDF术语权重

在一个大型文本语料库中,有些高频出现的词(例如英语中“the”, “a”, “is” )几乎没有携带任何与文档内容相关的有用信息。 如果我们将统计数据直接提供给分类器,那么这些高频出现的词会掩盖住那些我们关注但出现次数较少的词。

为了重新加权特征计数为适合分类器使用的浮点值,通常使用tf–idf变换。

Tf表示词频,而tf–idf表示词频乘以 逆文档频率

使用TfidfTransformer的默认设置, TfidfTransformer(norm='l2', use_idf=True, smooth_idf=True, sublinear_tf=False) 词频,词语在给定文档中出现的次数乘以idf分量,其计算公式为:

其中是文档集中的文档总数,是文档集中包含词语的文档数量,然后将所得的tf-idf向量通过欧几里得范数归一化:

这最初是为信息检索(作为搜索引擎结果的排名函数)开发的术语加权方案,也已在文档分类和聚类中找到了很好的用途。

以下部分包含进一步的说明和示例,表现了如何精确计算tf-idfs,以及如何在scikit-learn中计算tf-idfs,TfidfTransformerTfidfVectorizer与将idf定义为以下内容的标准教科书符号略有不同

TfidfTransformer和设置了smooth_idf=FalseTfidfVectorizer 中,将“ 1”计数添加到idf中,而不是idf的分母中:

该归一化由TfidfTransformer 类实现:

>>> from sklearn.feature_extraction.text import TfidfTransformer
>>> transformer = TfidfTransformer(smooth_idf=False)
>>> transformer
TfidfTransformer(smooth_idf=False)

所有参数的详细信息请参阅参考文档

以下方统计为例。第一项100%的时间都出现,因此不是很有重要。另外两个特征只占不到50%的时间出现,因此可能更能代表文档的内容:

>>> counts = [[3, 0, 1],
...           [2, 0, 0],
...           [3, 0, 0],
...           [4, 0, 0],
...           [3, 2, 0],
...           [3, 0, 2]]
...
>>> tfidf = transformer.fit_transform(counts)
>>> tfidf
<6x3 sparse matrix of type '<... 'numpy.float64'>'
    with 9 stored elements in Compressed Sparse ... format>

>>> tfidf.toarray()
array([[0.81940995, 0.        , 0.57320793],
       [1.        , 0.        , 0.        ],
       [1.        , 0.        , 0.        ],
       [1.        , 0.        , 0.        ],
       [0.47330339, 0.88089948, 0.        ],
       [0.58149261, 0.        , 0.81355169]])

每行都被正则化,使其适应欧几里得标准:

例如,我们可以如下计算counts数组中第一个文档中第一项的tf-idf :

-

现在,如果我们对文档中剩余的2个词语重复此计算,我们将得到:

-

-

以及原始tf-idfs的向量:

-

然后,应用欧几里得(L2)范数,我们为文档1获得以下tf-idfs:

此外,默认参数smooth_idf=True将“ 1”添加到分子和分母,类似于通过一个包含集合中的每个词语的附加文档从而避免除零错误。

使用此修改,文档1中第三项的tf-idf更改为1.8473:

-

并且L2归一化的tf-idf变为:

>>> transformer = TfidfTransformer()
>>> transformer.fit_transform(counts).toarray()
array([[0.85151335, 0.        , 0.52433293],
       [1.        , 0.        , 0.        ],
       [1.        , 0.        , 0.        ],
       [1.        , 0.        , 0.        ],
       [0.55422893, 0.83236428, 0.        ],
       [0.63035731, 0.        , 0.77630514]])

通过调用fit方法计算的每个特征的权重并存储在模型属性中:

>>> transformer.idf_
array([1. ..., 2.25..., 1.84...])

由于tf–idf通常用于文本特征,因此还有一个被称为TfidfVectorizer的类,它在一个模型中结合了CountVectorizerTfidfTransformer的所有选项:

>>> from sklearn.feature_extraction.text import TfidfVectorizer
>>> vectorizer = TfidfVectorizer()
>>> vectorizer.fit_transform(corpus)
<4x9 sparse matrix of type '<... 'numpy.float64'>'
    with 19 stored elements in Compressed Sparse ... format>

尽管tf–idf归一化通常非常有用,但是在某些情况下,二进制标记可能会提供更好的特征。这可以通过使用CountVectorizerbinary参数来实现。 特别是一些估计器(例如 Bernoulli Naive Bayes)显式的使用离散的布尔随机变量。还有,很短的文本可能带有嘈杂的tf–idf值,而二进制标志信息则更稳定。

通常,调整特征选取参数的最佳方法是使用带交叉验证的网格搜索,例如通过将特征提取器与分类器进行流水线化:

用于文本特征提取和评估的示例管道

6.2.3.5 解码文本文件

文本由字符组成,但文件由字节组成。字节依照一定的编码方式表示字符。要使用Python处理文本文件,必须将其字节解码为称为Unicode的字符集。常见编码为ASCII,Latin-1(西欧),KOI8-R(俄语)以及通用编码UTF-8和UTF-16。也存在许多其他的编码。

**注意:**编码也可以称为“字符集”,但这个术语的不太准确:单个字符集可以存在多种编码。

scikit-learn中的文本特征提取器知道如何解码文本文件,但是需要提前告知文件的编码方式。

为此,CountVectorizer有一个encoding参数。 对于现在的文本文件,正确的编码可能是UTF-8,因此这是默认编码(encoding="utf-8")。

但是,如果您要加载的文本实际上未使用UTF-8编码,则会得到一个UnicodeDecodeError。通过将decode_error参数设置为"ignore""replace",可以使矢量化器对解码错误保持沉默以避免抛出解码错误。有关bytes.decode更多详细信息,请参见Python函数的文档 (在Python提示符下键入help(bytes.decode))。

如果您在解码文本时遇到问题,请尝试以下操作:

  • 找出文本的实际编码是什么。该文件可能带有标头或自述文件来告诉您编码,或者您可以根据文本的来源推断使用一些标准编码。
  • 使用UNIX命令,您也许可以找到一般的编码方式file。Python的 chardet模块带有一个名为chardetect.py的脚本,它将猜测特定的编码,尽管它的猜测不一定是正确的。
  • 您可以尝试使用UTF-8并且忽略错误。您可以使用bytes.decode(errors='replace')解码字节字符串以将所有解码错误替换为无意义的字符,或者在矢量化程序中设置decode_error='replace' 。这个操作可能会破坏特征的效用。
  • 真实文本可能来自各种使用不同编码的来源,或者甚至采用与编码时所用的编码不同的解码方式进行草率解码。这在从Web检索的文本中很常见。Python软件包ftfy可以自动分类出一些解码错误,因此您可以尝试将未知文本解码为latin-1 ,然后使用ftfy来修复错误。
  • 如果文本采用混合编码,很难整理出来(20个新闻组数据集就是这种情况),则可以使用简单的单字节编码,例如latin-1。某些文本可能显示不准确,但是至少相同的字节序列将始终表示相同的特征。

例如,以下代码片段使用chardet (scikit-learn不附带,必须单独安装)来确定三个文本的编码。然后对文本进行矢量化处理,并显示学习到的词汇。输出未在此处显示。

>>> import chardet    # doctest: +SKIP
>>> text1 = b"Sei mir gegr\xc3\xbc\xc3\x9ft mein Sauerkraut"
>>> text2 = b"holdselig sind deine Ger\xfcche"
>>> text3 = b"\xff\xfeA\x00u\x00f\x00 \x00F\x00l\x00\xfc\x00g\x00e\x00l\x00n\x00 \x00d\x00e\x00s\x00 \x00G\x00e\x00s\x00a\x00n\x00g\x00e\x00s\x00,\x00 \x00H\x00e\x00r\x00z\x00l\x00i\x00e\x00b\x00c\x00h\x00e\x00n\x00,\x00 \x00t\x00r\x00a\x00g\x00 \x00i\x00c\x00h\x00 \x00d\x00i\x00c\x00h\x00 \x00f\x00o\x00r\x00t\x00"
>>> decoded = [x.decode(chardet.detect(x)['encoding'])
...            for x in (text1, text2, text3)]        # doctest: +SKIP
>>> v = CountVectorizer().fit(decoded).vocabulary_    # doctest: +SKIP
>>> for term in v: print(v)                           # doctest: +SKIP

(根据 chardet的版本,可能会返回第一个值错误的结果。)

有关Unicode和字符编码的一般介绍,请参阅Joel Spolsky的 Absolute Minimum Every Software Developer Must Know About Unicode.

6.2.3.6 应用与实例

词袋表示法非常简单,但实际上却很有用。

特别是在有监督的环境中,它可以与快速且可扩展的线性模型成功地组合以训练文档分类器,例如:

无监督的环境中,可以通过应用诸如K-means之类的聚类算法将类似的文档分组在一起:

最后,可以通过放宽聚类的硬性约束条件,例如通过使用非负矩阵分解(NMF或NNMF)来发现语料库的主要主题:

6.2.3.7 词袋表示法的局限性

字母组合的集合(即单词)无法捕获短语和多单词表达,很大程度上忽略了任何单词顺序依赖性。此外,单词袋模型不会考虑潜在的拼写错误或单词派生。

N-grams 可以拯救我们!与其建立简单的字母组合(n = 1),不如选择对成对的连续单词进行计数的二元组(n = 2)。

还可以考虑选择n-grams字符的集合,一种可以抵抗拼写错误和派生的表示法。

例如,假设我们要处理两个文档的语料库: ['words', 'wprds']. 第二个文档中包含的一个单词“ words”拼写错误。一个简单的词袋表示法会将这两个文档视为差异很大的文档,可能有两个特征存在差异。然而,2-gram字符表示法可以找到匹配文档的8个特征中的4个,这可能有助于首选分类器做出更优决定:

>>> ngram_vectorizer = CountVectorizer(analyzer='char_wb', ngram_range=(2, 2))
>>> counts = ngram_vectorizer.fit_transform(['words', 'wprds'])
>>> ngram_vectorizer.get_feature_names() == (
...     [' w', 'ds', 'or', 'pr', 'rd', 's ', 'wo', 'wp'])
True
>>> counts.toarray().astype(int)
array([[1, 1, 1, 0, 1, 1, 1, 0],
       [1, 1, 0, 1, 1, 1, 0, 1]])

在上面的示例中,使用了char_wb分析器,该分析器仅从单词边界内的字符(每边都用空格填充)创建n-gram。char分析仪还可以创建跨单词的n-gram:

>>> ngram_vectorizer = CountVectorizer(analyzer='char_wb', ngram_range=(5, 5))
>>> ngram_vectorizer.fit_transform(['jumpy fox'])
<1x4 sparse matrix of type '<... 'numpy.int64'>'
   with 4 stored elements in Compressed Sparse ... format>
>>> ngram_vectorizer.get_feature_names() == (
...     [' fox ', ' jump', 'jumpy', 'umpy '])
True

>>> ngram_vectorizer = CountVectorizer(analyzer='char', ngram_range=(5, 5))
>>> ngram_vectorizer.fit_transform(['jumpy fox'])
<1x5 sparse matrix of type '<... 'numpy.int64'>'
    with 5 stored elements in Compressed Sparse ... format>
>>> ngram_vectorizer.get_feature_names() == (
...     ['jumpy', 'mpy f', 'py fo', 'umpy ', 'y fox'])
True

单词边界感知变体char_wb对于使用空格进行单词分离的语言特别有用,因为在这种情况下,它产生的噪声特征比原始char变体少得多。对于这样的语言,它可以增加使用这些特征训练的分类器的预测精度和收敛速度,同时保留关于拼写错误和单词派生的稳健性。

虽然可以通过提取n-gram而不是单个单词来保留一些局部信息,但词袋和n-gram袋会破坏文档的大部分内部结构,从而破坏该内部结构所携带的大部分含义。

为了处理自然语言理解的更广泛的任务,应该考虑句子和段落的局部结构。于是,很多这样的模型将被视为目前不在 scikit-learn范围内的“结构化输出”问题。

6.2.3.8 使用哈希技巧对大型文本语料库进行矢量化处理

上述向量化方案很简单,但是它有从字符串标记到整数特征索引vocabulary_属性)的内存映射,这一事实在处理大型数据集时会引起一些问题

  • 语料库越大,词汇量就越大,内存使用也越多,
  • 拟合需要根据原始数据集的大小成比例分配中间数据结构的大小.
  • 建立单词映射需要对数据集进行全面遍历,因此不可能以严格的在线方式拟合文本分类器。
  • vocabulary_大的pickling 和 un-pickling的矢量化器程序可能非常慢(通常比pickling / un-pickling 例如相同大小NumPy的数组平面数据结构慢得多)
  • 将矢量化任务分成多个并行子任务是不容易的,因为该vocabulary_属性必须是具有细粒度同步障碍的共享状态:从标记字符串到特征索引的映射取决于每个标记首次出现的顺序,因此必须共享这些资源,这有可能损害并行子任务的性能,使其比顺序变体慢。

通过组合由 sklearn.feature_extraction.FeatureHasher类实现的“哈希技巧”(Feature hashing)和CountVectorizer的文本预处理与标记化功能,可以克服这些限制。

这种组合是在HashingVectorizer类中实现的,一个与CountVectorizer大部分API兼容的转换器类。 HashingVectorizer是无状态的,这意味着您不必对它调用fit

>>> from sklearn.feature_extraction.text import HashingVectorizer
>>> hv = HashingVectorizer(n_features=10)
>>> hv.transform(corpus)
<4x10 sparse matrix of type '<... 'numpy.float64'>'
    with 16 stored elements in Compressed Sparse ... format>

能够看到在向量输出中提取了16个非零特征标记:这比之前CountVectorizer在同一样本语料库中提取的19个非零特征标记少 。差异来自由于n_features参数值较低引起的哈希函数冲突。

在实际设置中,该n_features参数可以保留为其默认值2 ** 20(大约一百万个可能的特征)。如果内存或下游模型的大小是一个问题,则选择一个较低的值2 ** 18可能会有所帮助,避免在典型的文本分类任务上引入过多的额外冲突。

请注意,维度并不影响操作CSR矩阵(LinearSVC(dual=True), Perceptron, SGDClassifier)算法训练时CPU的运行时间,但是它会影响与CSC矩阵(LinearSVC(dual=False), Lasso(), etc)一起使用的算法。

让我们使用默认设置再试一次:

>>> hv = HashingVectorizer()
>>> hv.transform(corpus)
<4x1048576 sparse matrix of type '<... 'numpy.float64'>'
    with 19 stored elements in Compressed Sparse ... format>

我们没有再遇到冲突,但这是以输出空间更大的纬度值为代价的。当然,使用的这19个词语之外的其他词语可能仍会相互冲突。

HashingVectorizer还带有以下限制:

  • 无法反转模型(没有 inverse_transform 方法),也不访问特征的原始字符串表示形式,因为执行映射的哈希函数具有单向性。
  • 不提供IDF加权,因为这会在模型中引入有状态性。如果需要,可以在管道中为它附加一个TfidfTransformer

6.2.3.9 使用HashingVectorizer执行核外缩放

使用HashingVectorizer的一个有用的进步是执行核外扩展的能力。这意味着我们可以从不适合放入计算机主内存的数据中学习。

实施核外扩展的策略是以小批量方式将数据流传输到估计器。使用HashingVectorizer 对其进行矢量化处理,以确保估计器的输入空间始终具有相同的维数。因此,任何时候使用的内存量都受微型批处理大小的限制。尽管使用这种方法对可以摄取的数据量没有限制,但是从实际的角度来看,学习时间通常会受到在这个任务上要花费的CPU时间的限制。

有关文本分类任务中核外缩放的完整示例,请参见文本文档的核外分类

6.2.3.10 自定义矢量化器类

通过将一个callable传递给vectorizer构造函数,可以自定义行为:

>>> def my_tokenizer(s):
...     return s.split()
...
>>> vectorizer = CountVectorizer(tokenizer=my_tokenizer)
>>> vectorizer.build_analyzer()(u"Some... punctuation!") == (
...     ['some...', 'punctuation!'])
True

特别是命名

  • preprocessor:一个将整个文档作为输入(作为单个字符串)的可调用的方法,,并仍然作为整个字符串返回文档转换后的可能的版本,。这可用于删除HTML标记,将整个文档小写等。
  • tokenizer:一个从预处理器获取输出并将其拆分为标记的可调用方法,然后返回包含这些标记的列表。
  • analyzer:一个可替换预处理器和标记生成器的可调用程序。默认分析器都调用预处理器和标记器,但是自定义分析器将跳过此过程。N-gram提取和停用词过滤是在分析器级别进行的,因此自定义分析器可能必须重现这些步骤。

(Lucene用户可能会识别出这些名称,但是请注意,scikit-learn概念可能不会一对一映射到Lucene概念上。)

为了使预处理,分词器和分析器意识到模型参数可以从类派生并重写 build_preprocessorbuild_tokenizerbuild_analyzer 工厂方法,而不是传递自定义函数。

一些提示和技巧:

  • 如果文档是由外部程序包预先标记的,则将它们存储在文件(或字符串)中,并用空格分隔标记并传递参数 analyzer=str.split
  • scikit-learn代码库中不包含花式标记级分析,例如词干,词组去除,复合分割,基于词性的过滤等,但可以通过自定义标签生成器或分析器来添加。这是一个使用NLTK的标记器和词条 分解器的CountVectorizer
>>> from nltk import word_tokenize          
>>> from nltk.stem import WordNetLemmatizer 
>>> class LemmaTokenizer:
...     def __init__(self):
...         self.wnl = WordNetLemmatizer()
...     def __call__(self, doc):
...         return [self.wnl.lemmatize(t) for t in word_tokenize(doc)]
...
>>> vect = CountVectorizer(tokenizer=LemmaTokenizer())  

(请注意,这不会过滤掉标点符号。)

例如,以下示例将某些英国拼写转换为美国拼写:

>>> import re
>>> def to_british(tokens):
...     for t in tokens:
...         t = re.sub(r"(...)our$", r"\1or", t)
...         t = re.sub(r"([bt])re$", r"\1er", t)
...         t = re.sub(r"([iy])s(e$|ing|ation)", r"\1z\2", t)
...         t = re.sub(r"ogue$", "og", t)
...         yield t
...
>>> class CustomVectorizer(CountVectorizer):
...     def build_tokenizer(self):
...         tokenize = super().build_tokenizer()
...         return lambda doc: list(to_british(tokenize(doc)))
...
>>> print(CustomVectorizer().build_analyzer()(u"color colour"))
[...'color', ...'color']

用于其他样式的预处理;示例包括词干,词形化或规范化数字标记,后者说明如下:

使用“光谱共聚类”算法对文档进行聚类

在处理不使用显式单词分隔符(例如空格)的亚洲语言时,自定义矢量化程序也很有用。

6.2.4 图像特征提取

6.2.4.1 补丁提取

extract_patches_2d函数从存储为二维数组或沿第三轴显示颜色信息的三维数组的图像中提取色块。为了用所有修补程序重建图像,请使用 reconstruct_from_patches_2d。例如,让我们使用3个颜色通道(例如RGB格式)生成一个4x4像素图片:

>>> import numpy as np
>>> from sklearn.feature_extraction import image

>>> one_image = np.arange(4 * 4 * 3).reshape((4, 4, 3))
>>> one_image[:, :, 0]  # R channel of a fake RGB picture
array([[ 0,  3,  6,  9],
       [12, 15, 18, 21],
       [24, 27, 30, 33],
       [36, 39, 42, 45]])

>>> patches = image.extract_patches_2d(one_image, (2, 2), max_patches=2,
...     random_state=0)
>>> patches.shape
(2, 2, 2, 3)
>>> patches[:, :, :, 0]
array([[[ 0,  3],
        [12, 15]],

       [[15, 18],
        [27, 30]]])
>>> patches = image.extract_patches_2d(one_image, (2, 2))
>>> patches.shape
(9, 2, 2, 3)
>>> patches[4, :, :, 0]
array([[15, 18],
       [27, 30]])

现在让我们尝试通过对重叠区域求平均来从补丁中重建原始图像:

>>> reconstructed = image.reconstruct_from_patches_2d(patches, (4, 4, 3))
>>> np.testing.assert_array_equal(one_image, reconstructed)

PatchExtractor类的工作方式与 extract_patches_2d相同,只是它支持多图像输入。由于被实现为一个估计器,因此它可以在管道中使用。如下:

>>> five_images = np.arange(5 * 4 * 4 * 3).reshape(5, 4, 4, 3)
>>> patches = image.PatchExtractor(patch_size=(2, 2)).transform(five_images)
>>> patches.shape
(45, 2, 2, 3)

6.2.4.2 图像的连接图

scikit-learn 中的多个估计器可以使用特征或样本之间的连接信息。例如Ward聚类(Hierarchical clustering)只能将图像的相邻像素聚类在一起,从而形成连续的补丁:

为此,估计器使用“连接性”矩阵,给出了要连接的样本。 img_to_graph函数从2D或3D图像返回这样一个矩阵。同样,grid_to_graph为给定图像形状的图像建立连接矩阵。

这些矩阵可用于在使用连通性信息的估计器中强加连接,例如Ward聚类(层次聚类),而且还以构建预计算的内核或相似性矩阵。

示例:


以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号