sklearn.feature_extraction
模块可用于以机器学习算法支持的格式从原始数据集(如文本和图像)中提取特征。
**注意:**特征提取与
特征选择
有很大不同:前者是将任意数据(例如文本或图像)转换为可用于机器学习的数字特征。后者是一种应用在这些特征上的机器学习技术。
1. 从字典加载特征(Loading features from dicts)
DictVectorizer
可用于将以标准Python的
dict
对象的列表形式表示的特征数组转换为scikit-learn估计器使用的NumPy/SciPy表示形式。
虽然处理速度不是特别快,但Python的
dict
对象的优点:易于使用,稀疏(缺失的特征不需要存储), 并且除了值之外还存储特征名称。
DictVectorizer
实现了对标称型特征(分类特征(categorical),nominal特征,离散特征)的one-of-K或“one-hot”编码。标称型特征是“属性-值(attribute-value)”对,其中值(value)的取值被限制在一个不排序的可能性离散列表中。(例如,话题标识符,对象类型,标签,名称。)
在下文中,“城市”是分类属性,而“温度”是传统的数字特征:
>>> 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)标签的一个算法。以下字典(dict)可以是在“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',... },... # 在真实的应用中会提取想这样的一个字典。... ]
上述描述可以被矢量化为适合于传递给分类器的稀疏二维矩阵(可能在通过管道之后进行
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特征),其中大部分通常将会是0。为了使产生的数据结构能够适应内存,
DictVectorizer
类默认使用
scipy.sparse
矩阵而不是
numpy.ndarray
。
2. 特征哈希(散列)化(Feature hashing)
FeatureHasher
类是一种高速,低内存消耗的矢量化方法,它使用一种称为
特征哈希化
(或称为“散列法(hashing trick)”)的技术,不是像矢量化方法那样构建训练中遇到的特征哈希表。
FeatureHasher
将哈希函数应用于特征以便直接在样本矩阵中确定它们的列索引。结果是以牺牲可检测性(inspectability)为代价,带来速度的提高和内存使用的减少。哈希器(hasher)不记得输入特征是什么样的,也没有
inverse_transform
办法。
由于散列函数可能导致(不相关)特征之间的冲突,因此使用带符号散列函数,并且散列值的符号确定存储在特征的输出矩阵中值的符号。这样,碰撞可能会抵消而不是累积错误,并且任何输出特征的值的预期平均值为零。默认情况下,此机制将使用
alternate_sign=True
启用,尤其对小型哈希表的大小(
n_features < 10000
)特别有用。对于大哈希表的大小,可以禁用它,以便将输出传递给估计器,如
sklearn.naive_bayes.MultinomialNB
或
sklearn.feature_selection.chi2
特征选择器,这些特征选项器希望输入是非负的。
FeatureHasher
接受三种类型的输入:mappings,
(feature, value)
pairs,或 strings。其中mappings就像是python的
dict
或在
collections
模块中的字典变体。到底使用哪种参数依赖于构造器的
input_type
参数。Mapping被当作是由
(feature, value)
组成的列表(list),而单个字符串有一个内在的值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.transform
方法中的
raw_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
。
这里需要注意的是,由于我们使用了Python的生成器,导致在特征抽取过程中引入了懒惰性:只有在哈希器有需求的时候tokens才会被处理。
2.1. 实现细节
FeatureHasher
使用带符号的MurmurHash3的32比特变体。作为其结果(也因为
scipy.sparse
的限制),当前支持的特征最大数量为。
散列技巧(hashing trick)的原始形式源于Weinberger et al。使用两个分开的哈希函数,和ξ分别确定特征的列索引和符号。现有的实现是基于假设:MurmurHash3的符号位与其他位独立。
由于使用简单的模数将哈希函数转换为列索引,建议使用2次幂作为
n_features
参数,否则特征不会被均匀的分布到列中。 参考文献:
- Kilian Weinberger, Anirban Dasgupta, John Langford, Alex Smola and Josh Attenberg (2009). Feature hashing for large scale multitask learning. Proc. ICML.
- MurmurHash3.
3. 文本特征提取
3.1. 词袋表示法
文本分析是机器学习算法的主要应用领域,然而原始数据的符号文字序列不能直接传递给算法,因为它们大多数要求具有固定长度的数字矩阵特征向量,而不是具有可变长度的原始文本文档。
为解决这个问题,scikit-learn提供了从文本内容中提取数字特征的最常见方法,即:
- tokenizing令牌化,即对每个可能的词令牌(token)分成字符串并赋予整型id,例如通过使用空格和标点符号作为令牌分隔符。
- counting统计计数,即数出每个文档中令牌的出现次数。
- normalizing标准化,即对大多数样本/文档中出现的重要性递减的token进行归一化和加权
在这个机制中, 特征和样本是如下定义的:
- 每个单独的令牌发生频率(归一化或不归一化)被视为一个特征 。
- 给定文档中所有的令牌频率向量被看做一个多元样本
因此,文档语料库(corpus of documents)可被表示为矩阵形式,每行对应一个文本文档,每列对应文集中出现的词令牌(如单个词)。
我们将**向量化(vectorization)**称为是将文本文档集合转换为数字集合特征向量的通用方法。这种特定的策略(令牌化,计数和归一化)被称为 **词袋(Bag of Words)**或“Bag of n-grams” 表示法。文档由单词的出现与否和出现频率来描述,同时完全忽略文档中单词的相对位置信息。
3.2. 稀疏性
由于大多数文本文档通常只使用语料库的词向量全集中的一个小子集,所以得到的矩阵将具有许多零特征值(通常大于99%)。
例如,10,000个短文本文档(如电子邮件)的集合将使用总共100,000个独特词大小的词汇,而每个文档将单独使用100到1000个独特的单词。
为了能够将这样的矩阵存储在存储器中,并且还可以加速代数的矩阵/向量运算,实现通常将使用诸如
scipy.sparse
包中的稀疏实现。
3.3. 常见Vectorizer的用法
CountVectorizer
在单个类中实现词语切分(tokenization)和出现频数统计(occurrence counting):
>>> from sklearn.feature_extraction.text import CountVectorizer
该模型具有许多参数,但参数的默认初始值是相当合理的(有关详细信息,请参见
参考文档
):
>>> vectorizer = CountVectorizer()>>> vectorizer
CountVectorizer()
我们用该类对一个简约的文本语料库进行分词(tokenize)和统计单词出现频数:
>>> 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
在拟合过程中,分析器(analyzer)找到的每个项都会被分配一个唯一的整数索引,对应于结果矩阵(resulting matrix)中的一列。此列的一些说明可以被检索如下:
>>> 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
由上述向量化器(vectorizer)提取的词汇量会变得更大,同时可以在局部定位模式时消除歧义:
>>> 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]...)
3.3.1.使用停用词(stop words)
停用词是指诸如“和(and)”,“该(the)”,“他(him)”之类的词,它们被认为在表示文本内容方面没有提供任何信息,可以将其删除,以避免将其理解为对预测有用的信息。但是,有时候,这些类似的单词可用于预测,例如在对写作风格或性格进行分类时。 我们提供的“英语”停用词列表中有几个已知问题。它并非是通用的“一刀切”的解决方案,因为某些任务可能需要一个定制的解决方案。有关更多详细信息,请参见 [NQY18] 。 在选择停用词列表时请多加注意。流行的停用词列表可能包含对某些任务(例如计算机)具有高度信息意义的词。
您还应该确保停用词列表的预处理和令牌化和矢量化器中使用的预处理和令牌化相同。例如,这个词we’ve被CountVectorizer的默认词令牌生成器分成we和ve,所以当we’ve在进行文本转化时,如果we’ve在停用词
stop_words
中,但ve不在的话,ve将被会保留。我们的向量化器将尝试识别并警告某些不一致的地方。
参考文献
[NQY18] J. Nothman, H. Qin and R. Yurchak (2018). “Stop Word Lists in Free Open-source Software Packages” . In Proc. Workshop for NLP Open Source Software
3.4. Tf–idf 术语权重(term weighting)
在一个大的文本语料库中,一些单词将出现很多次(例如,英文的“the”,“a”,“is”),因此对文档的实际内容没有什么有意义的信息。如果我们直接把计数数据提供给分类器,那么这些非常频繁的词组会掩盖住那些我们感兴趣但却很少出现的词。 为了重新计算特征权重,并将其转化为适合分类器使用的浮点值,使用tf-idf变换是非常常见的。 Tf表示术语频率(term-frequency),而tf–idf表示术语频率(term-frequency)乘以逆文档频率(inverse document-frequency):
使用
TfidfTransformer
的默认设置,
TfidfTransformer(norm='l2', use_idf=True, smooth_idf=True,sublinear_tf=False)
,术语的频率(term frequency),一个术语(term)出现在给定文档的次数,被乘以idf分量,计算如下
其中是文档的总数量,是包含某个术语的文档的数量。然后计算出的tf-idf向量用欧式范数归一化,如下所示:
上面所介绍的就是用于信息检索领域的原始的术语(term)加权机制。该术语(term)加权机制在文档分类和聚类中的表现也比较好。
接下来的各小节包含了对tf-idfs的进一步解释以及实验案例来说明tf-idfs是如何准确计算出来的,以及scikit-learn的
TfidfTransformer
是如何计算tf-idfs的。还有
TfidfVectorizer
与标准的idf的定义的细微差别。标准的idf的定义如下所示:
在
TfidfTransformer
和
TfidfVectorizer
,如果设置了
smooth_idf=False
的话,那么数量“1”就被加到idf上而不是idf的分母上:
该标准化(归一化)由
TfidfTransformer
类实现的:
>>> from sklearn.feature_extraction.text import TfidfTransformer>>> transformer = TfidfTransformer(smooth_idf=False)>>> transformer
TfidfTransformer(smooth_idf=False)
同样,请参阅
参考文档
以获取所有参数的详细信息。
让我们以下面的数量来举个例子。第一个术语(term)出现次数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数组中的第一个文档中第一个术语(term)的tf-idf:
现在,如果我们重复上述计算过程去计算文档中剩余的2个术语(terms),我们可以得到:
原始tf-idfs的向量:
然后,应用欧几里得(L2)范数,我们可以在文档1上得到以下tf-idfs:
更进一步,默认参数
smooth_idf=True
会把添加“1”到分子和分母上,就好像又看到了另一篇文档而这篇文档恰好包含了所有的术语(term)仅仅一次,这么做就可以避免除零的异常发生了:
使用这个修改版本, 文档1中第三个术语(term)的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
方法计算出的每个特征的权重被保存在模型
idf_
属性中:
>>> transformer.idf_
array([1. ..., 2.25..., 1.84...])
由于tf–idf在文本特征提取中被经常使用,我们还提供了另一个类
TfidfVectorizer
来组合
CountVectorizer
和
TfidfTransformer
的所有的选项到一个单一模型中去:
>>> 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标准化(归一化)通常非常有用,但是在某些情况下,二元出现标记( binary occurrence markers)可能会提供更好的特征。这可以通过
CountVectorizer
的
binary
参数来实现,特别是某些估计器(例如
Bernoulli Naive Bayes
)显式的使用离散的布尔随机变量,而且非常短的文本很可能影响tf-idf值,而二元出现信息(binary occurrence info)更稳定。
通常情况下,调整特征提取参数的最佳方法是使用基于网格搜索的交叉验证,例如通过将特征提取器与分类器进行流水线化:
- 用于文本特征提取和评估管道(pipeline)案例
3.5. 解码文本文件(Decoding text files)
文本由字符组成,但文件由字节组成。字节转化成字符依照一定的编码(encoding)方式。为了在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"
来避免抛出解码错误。有关详细信息,请参阅Python函数
bytes.decode
的文档(在Python提示符下输入
help(bytes.decode)
)。
如果您在解码文本时遇到问题,请尝试以下操作:
- 找出文本的实际编码是什么。该文件可能带有标题或自述文件(README),来告诉您编码,或者您可以根据文本的来源来推断编码方式。
- 您可能可以使用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的“
每个软件开发人员最低必须要了解的Unicode的知识”
。
3.6. 应用和案例
词汇表达方式相当简单,但在实践中却非常有用。 特别是在有监督的应用中,它能够把快速和可扩展的线性模型组合来训练文档分类器,例如:
- 使用稀疏特征对文本文档进行分类
在无监督的应用中,应用聚类算法(例如
K-means
)将相似的文档分组在一起:
- 使用k-means聚类文本文档
最后,可以通过松弛聚类的约束条件来发现语料库的主要主题,例如通过使用
非负矩阵分解(NMF或NNMF)
:
- 使用非负矩阵分解和潜在狄利克雷分配进行主题提取
3.7. 词袋表示法的局限性
单个单词(unigrams)的集合无法捕获短语和多字表达,有效地忽略了单词顺序依赖。另外,这个单词模型不包含潜在的拼写错误或词汇推倒。
N-grams 可以来救场!不去构建一个简单的单个单词(unigrams)集合 (n=1),而是使用二元单词(bigrams)集合 (n=2),计算连续单词。
还可以考虑n-gram的集合,这是一种对拼写错误和派生有弹性的表示。
例如,假设我们正在处理两个文档的语料库:
['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的范围之内。
3.8. 用哈希技巧矢量化大型语料库
上述向量化方案是简单的,但是它存在从字符串令牌到整数特征索引的内存映射(
vocabulary_
属性),在处理大型数据集时会引起几个问题:
- 语料库越大,词汇量越大,使用的内存也越大。
- 拟合(fitting)需要根据原始数据集的大小等比例分配中间数据结构的大小。
- 构建词映射需要完整的传递数据集,因此不可能以严格在线的方式拟合文本分类器。
- pickling和un-pickling的
vocabulary
的向量器会非常慢(通常比pickling/un-pickling平面数据结构(比如同等大小的Numpy数组))还要慢。 - 将向量化任务分隔成并行的子任务很不容易实现,因为
vocabulary_
属性要共享状态有一个细颗粒度的同步障碍:从标记字符串中映射特征索引与每个标记的首次出现顺序是独立的, 因此应该被共享,在这点上并行工作器(worker)的性能受到了损害,使他们比串行更慢。
通过组合由
sklearn.feature_extraction.FeatureHasher
类实现的“哈希技巧”(
特征哈希(散列)化
) 和
CountVectorizer
的文本预处理和标记化功能,可以克服这些限制。
此组合是在
HashingVectorizer
中实现的,该类是与
CountVectorizer
类的大部分API兼容的变换器(transformer)类。
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个非0特征标记:与之前由
CountVectorizer
在同一个样本语料库抽取的19个非0特征要少。差异是来自哈希方法的冲突,因为
n_features
参数的值比较低。
在真实世界的环境下,
n_features
参数可以使用默认值
2 ** 20
(将近100万可能的特征)。如果内存或者下游模型的大小是一个问题,那么选择一个较小的值比如
2 ** 18
可能有一些帮助,而不需要为典型的文本分类任务引入太多额外的冲突。
请注意,维度并不影响CPU的算法训练时间,训练是在操作CSR矩阵(
LinearSVC(dual=True)
,
Perceptron
,
SGDClassifier
,
PassiveAggressive
),但是,它对CSC度量(matrices)(
LinearSVC(dual=False)
,
Lasso()
等)算法有效。
让我们使用默认设置再试一次:
>>> 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
添加到管道中。
3.9. 使用 HashingVectorizer执行核外scaling
使用
HashingVectorizer
的一个有趣的开发是执行
核外(out-of-core)
缩放的能力。这意味着我们可以从无法放入电脑主内存的数据中进行学习。
实现核外扩展的一个策略是将数据以流的方式以一小批提交给估计器。每批的向量化都是用
HashingVectorizer
。这样来保证估计器的输入空间的维度是相等的,因此任何时间使用的内存数都限定在小频次的大小。尽管用这种方法可以处理的数据没有限制,但是从实用角度学习时间受到想要在这个任务上花费的CPU时间的限制。
有关文本分类任务中核心外缩放的完整示例,请参见
文本文档的核外分类
。
3.10. 自定义向量化器类
通过将一个可调用对象传递给向量化器的构造函数,可以自定义其行为:
>>> 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
:一个可替代预处理程序和标记器的可调用程序。默认analyzer都会调用预处理器,但是自定义analyzer将会跳过这个。N-gram提取和停止字过滤在analyzer级进行,因此定制analyzer可能必须重现这些步骤。
(Lucene用户可能会识别这些名称,但请注意,scikit-learn概念可能无法一对一对应到Lucene概念上。)
为了使预处理器,标记器和分析器了解模型参数,可以从类派生并覆盖
build_preprocessor
,
build_tokenizer
和
build_analyzer
工厂方法, 而不是传递自定义函数。
一些提示和技巧:
- 如果文档是由外部程序包预先标记的,则将它们存储在文件(或字符串)中,并用空格分隔,传递
analyzer=str.split
- scikit-learn代码库中不包含花式词令牌分析,例如词干,词根分解,化合物分解,基于词性的过滤等,但可以通过自定义词令牌化器或分析器来添加。这是一个
CountVectorizer
使用NLTK的标记器(tokenizer)和词条分解器(lemmatizer):
>>> 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']
用于其他样式的预处理;例子包括词干(stemming),词性还原(lemmatization)或规范化数字令牌(normalizing numerical tokens),后者在以下示例中进行说明:
- 使用“光谱共聚类(Spectral Co-clustering algorithm)”算法对文档进行聚类
在处理不使用显式字分隔符(例如空格)的亚洲语言时,自定义向量化器也是有用的。
4. 图像特征提取
4.1. 图像块提取
extract_patches_2d
函数从存储为二维数组的灰度图像或三维数组的彩色图像中提取图像块(patches)。彩色图像的颜色信息在第三个维度中存放。如果要从所有的图像块(patches)中重建图像,请使用
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] # 一张假的RGB图片的R通道。
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
函数相同,只是它支持多幅图像作为输入。它被实现为一个估计器(estimator),因此它可以在管道(pipelines)中使用。请看:
>>> five_images = np.arange(5 * 4 * 4 * 3).reshape(5, 4, 4, 3)>>> patches = image.PatchExtractor((2, 2)).transform(five_images)>>> patches.shape
(45, 2, 2, 3)
4.2. 图像的连接图
scikit-learn中有几个估计器(estimators)可以使用特征或样本之间的连接信息(connectivity information)。例如Ward clustering (
层次聚类(Hierarchical clustering)
)可以只把相邻像素聚集在一起,从而形成连续的斑块:
sphx_glr_plot_coin_ward_segmentation_1
由于这个目的,这些估计器使用一个连接性矩阵,给出哪些样本是连接着的。
函数
img_to_graph
从2D或3D图像返回这样的矩阵。同样,
grid_to_graph
函数为给定形状的图像构建连接矩阵。 这些矩阵可用于在使用连接信息的估计器中强加连接,例如Ward clustering ( 层次聚类(Hierarchical clustering) ),而且还要构建预计算的内核或相似矩阵。 注意:例子
- 硬币图像上的结构化Ward层次聚类演示
- 光谱聚类(Spectral clustering)用于图像分割
- 特征集聚与单变量选择