7.2. 特征提取#

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

备注

特征提取与 特征选择 :前者包括将文本或图像等任意数据转换为可用于机器学习的数字特征。后者是应用于这些功能的机器学习技术。

7.2.1. 从文本加载功能#

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

虽然处理起来不是特别快,但Python的 dict 具有使用方便、稀疏(缺少的特征不需要存储)、特征名与值并存的优点。

DictVectorizer 为类别(又名名义性、离散性)特征实现所谓的K之一或“一热”编码。类别特征是“属性-值”对,其中值仅限于一系列不经过排序的离散可能性(例如主题标识符、对象类型、标签、名称.)。

在下文中,“城市”是一个范畴属性,而“温度”是一个传统的数值功能::

>>> 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_out()
array(['city=Dubai', 'city=London', 'city=San Francisco', 'temperature'], ...)

DictVectorizer 接受一个功能的多个字符串值,例如,一部电影的多个类别。

假设数据库使用某些类别(非强制性)及其上映年份对每部电影进行分类。

>>> movie_entry = [{'category': ['thriller', 'drama'], 'year': 2003},
...                {'category': ['animation', 'family'], 'year': 2011},
...                {'year': 1974}]
>>> vec.fit_transform(movie_entry).toarray()
array([[0.000e+00, 1.000e+00, 0.000e+00, 1.000e+00, 2.003e+03],
       [1.000e+00, 0.000e+00, 1.000e+00, 0.000e+00, 2.011e+03],
       [0.000e+00, 0.000e+00, 0.000e+00, 0.000e+00, 1.974e+03]])
>>> vec.get_feature_names_out()
array(['category=animation', 'category=drama', 'category=family',
       'category=thriller', 'year'], ...)
>>> vec.transform({'category': ['thriller'],
...                'unseen_feature': '3'}).toarray()
array([[0., 0., 0., 1., 0.]])

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',
...     },
...     # in a real application one would extract many such dictionaries
... ]

该描述可以被载体化为适合输入分类器的稀疏二维矩阵(可能在被管道传输到分类器之后 TfidfTransformer 用于正常化)::

>>> vec = DictVectorizer()
>>> pos_vectorized = vec.fit_transform(pos_window)
>>> pos_vectorized
<Compressed Sparse...dtype 'float64'
  with 6 stored elements and shape (1, 6)>
>>> pos_vectorized.toarray()
array([[1., 1., 1., 1., 1., 1.]])
>>> vec.get_feature_names_out()
array(['pos+1=PP', 'pos-1=NN', 'pos-2=DT', 'word+1=on', 'word-1=cat',
       'word-2=the'], ...)

正如您可以想象的那样,如果围绕文档库的每个单词提取这样的上下文,那么产生的矩阵将非常宽(许多单一特征),其中大多数特征在大多数时候都被值为零。以便使生成的数据结构能够容纳内存 DictVectorizer 类使用 scipy.sparse 默认情况下, numpy.ndarray .

7.2.2. 特征哈希#

FeatureHasher 是一个高速、低内存的载体转换器,使用称为 feature hashing ,或者“哈希技巧”。与像向量器那样构建训练中遇到的特征的哈希表不同, FeatureHasher 对要素应用哈希函数,以直接确定其在样本矩阵中的列索引。结果是提高了速度并减少了内存使用,但牺牲了可检查性;哈希器不记得输入功能是什么样子,也没有 inverse_transform

由于散列函数可能导致(不相关的)特征之间的冲突,因此使用带符号的散列函数,并且散列值的符号确定存储在输出矩阵中的特征的值的符号。这样,冲突可能会抵消而不是累积误差,并且任何输出特征值的期望均值为零。默认情况下, alternate_sign=True 并且对于小哈希表大小特别有用 (n_features < 10000 ).对于较大的哈希表大小,可以禁用它,以允许将输出传递给估计器,例如 MultinomialNBchi2 期望非负输入的功能选择器。

FeatureHasher 接受任何一个映射(例如Python的 dict 及其变体, collections 模块), (feature, value) 对或字符串,具体取决于构造函数参数 input_type .映射被视为 (feature, value) 对,而单个字符串的隐式值为1,所以 ['feat1', 'feat2', 'feat3'] 被解释为 [('feat1', 1), ('feat2', 1), ('feat3', 1)] .如果单个要素在样本中出现多次,则关联值将被相加(因此 ('feat', 2)('feat', 3.5) 成为 ('feat', 5.5) ).的输出 FeatureHasher 总是一个 scipy.sparse CSR格式的矩阵。

特征哈希可以用于文档分类,但与 CountVectorizer , FeatureHasher 不进行单词拆分或任何其他预处理,Unicode到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)

然后 raw_X 要被供给至 FeatureHasher.transform 可以使用::

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 .

请注意使用了生成器理解,它将惰性引入到特征提取中:令牌只根据散列器的需求进行处理。

实现细节#

FeatureHasher 使用MurmurHas3的签名32位变体。因此(而且由于限制 scipy.sparse ),目前支持的最大功能数为 \(2^{31} - 1\) .

Weinberger等人最初的哈希技巧公式使用了两个独立的哈希函数 \(h\)\(\xi\) 分别确定要素的列索引和符号。当前实现的工作假设是Murmur哈希3的符号位独立于其其他位。

由于使用简单的模将哈希函数转换为列索引,因此建议使用二的乘势作为 n_features 参数;否则要素将不会均匀地映射到列。

引用

  • MurmurHash3 <https://github.com/aappleby/smhasher> _.

引用

7.2.3. 文本特征提取#

7.2.3.1. 词袋代表#

文本分析是机器学习算法的一个主要应用领域。然而,原始数据(一个符号序列)无法直接输入到算法本身,因为大多数算法期望具有固定大小的数字特征载体,而不是具有可变长度的原始文本文档。

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

  • tokenizing 字符串,并为每个可能的标记提供一个整元id,例如通过使用空白和标点符号作为标记分隔符。

  • counting 每个文档中标记的出现次数。

  • normalizing 并以大多数样本/文档中出现的重要性递减标记进行加权。

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

  • 每个 individual token occurrence frequency (规范化与否)被视为 feature .

  • 给定的所有令牌频率的载体 document 被认为是多元的 sample .

因此,文档库可以用矩阵来表示,其中每个文档有一行,每个文档有一列出现在该库中的每个标记(例如单词)有一列。

我们称 vectorization 将文本文档集合转化为数字特征载体的一般过程。这种特定策略(标记化、计数和标准化)称为 Bag of Words 或“n元语法袋”表示。文档是通过词的出现来描述的,而完全忽略了词在文档中的相对位置信息。

7.2.3.2. 稀疏性#

由于大多数文档通常会使用文集中使用的单词的一小部分子集,因此生成的矩阵将具有许多为零的特征值(通常超过99%)。

例如,10,000个短文本文档(例如电子邮件)的集合将使用大小为总共100,000个唯一单词的词汇表,而每个文档将单独使用100到1000个唯一单词。

为了能够将这样的矩阵存储在存储器中,而且为了加速代数运算矩阵/载体,实现通常将使用稀疏表示,例如 scipy.sparse 包.

7.2.3.3. 常见Vectorizer使用#

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

>>> from sklearn.feature_extraction.text import CountVectorizer

该模型有很多参数,但默认值相当合理(请参阅 reference documentation 详情)::

>>> 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
<Compressed Sparse...dtype 'int64'
  with 19 stored elements and shape (4, 9)>

默认配置通过提取至少2个字母的单词来标记字符串。执行此步骤的特定函数可以显式请求:

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

分析器在匹配过程中找到的每个项都被分配一个与结果矩阵中的列相对应的唯一整指数。这些列的解释可以如下检索:

>>> vectorizer.get_feature_names_out()
array(['and', 'document', 'first', 'is', 'one', 'second', 'the',
       'third', 'this'], ...)

>>> 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的属性::

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

因此,在未来对转换方法的调用中,训练数据库中没有看到的单词将被完全忽略::

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

请注意,在之前的文集中,第一个和最后一个文档具有完全相同的单词,因此以相等的载体编码。特别是,我们会丢失最后一个文件是疑问形式的信息。为了保留一些本地排序信息,除了1克(单个单词)之外,我们还可以提取2克单词::

>>> 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]...)

7.2.3.4. 使用停止词#

停止词是像“and”、“the”、“him”这样的词,这些词被认为在代表文本内容时没有信息性,并且可以删除这些词以避免它们被解释为预测的信息性。然而,有时,相似的词对于预测很有用,例如对写作风格或性格进行分类。

我们提供的“英语”停止词列表中有几个已知问题。它的目标不是成为通用的“一刀切”解决方案,因为某些任务可能需要更定制的解决方案。看到 [NQY18] 了解更多详细信息。

请小心选择停止词列表。流行的停止词列表可能包括对某些任务具有高度信息性的词,例如 computer .

您还应该确保停止词列表已应用与向量器中使用的相同的预处理和标记化。这个词 we've 被分成 weve 由CountVectorizer的默认代币化器执行,因此如果 we've 处于 stop_words ,但是 ve 不是, ve 将保留自 we've 在转换后的文本中。 我们的vectorizer将尝试识别并警告某些不一致。

引用

[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.

7.2.3.5. Tf-idf项加权#

在一个大型的文本语料库中,有些词会非常常见(例如英语中的“the”,“a”,“is”),因此关于文档的实际内容的有意义的信息很少。如果我们将直接计数数据直接馈送到分类器,那么这些非常频繁的术语将遮蔽更罕见但更有趣的术语的频率。

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

Tf means term-frequency while tf–idf means term-frequency times inverse document-frequency: \(\text{tf-idf(t,d)}=\text{tf(t,d)} \times \text{idf(t)}\).

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

\(\text{idf}(t) = \log{\frac{1 + n}{1+\text{df}(t)}} + 1\),

哪里 \(n\) 是文档集中的文档总数,并且 \(\text{df}(t)\) 是文档集中包含术语的文档数 \(t\) .然后通过欧几里得规范对所得tf-idf载体进行规格化:

\(v_{norm} = \frac{v}{||v||_2} = \frac{v}{\sqrt{v{_1}^2 + v{_2}^2 + \dots + v{_n}^2}}\).

这最初是为信息检索(作为搜索引擎结果的排名功能)开发的术语加权方案,在文档分类和集群中也得到了很好的应用。

以下部分包含进一步的解释和示例,说明如何准确计算tf-idfs以及如何在scikit-learn的中计算tf-idfs TfidfTransformerTfidfVectorizer 与定义idf为

\(\text{idf}(t) = \log{\frac{n}{1+\text{df}(t)}}.\)

TfidfTransformerTfidfVectorizersmooth_idf=False ,将“1”计数添加到idf,而不是idf的分母:

\(\text{idf}(t) = \log{\frac{n}{\text{df}(t)}} + 1\)

这种正常化由 TfidfTransformer 类别::

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

再次请参阅 reference documentation 了解所有参数的详细信息。

tf-idf矩阵的数字示例#

让我们举一个例子,包含以下计数。第一项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
<Compressed Sparse...dtype 'float64'
  with 9 stored elements and shape (6, 3)>

>>> 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]])

每一行都被规范化为具有单位欧几里得规范:

\(v_{norm} = \frac{v}{||v||_2} = \frac{v}{\sqrt{v{_1}^2 + v{_2}^2 + \dots + v{_n}^2}}\)

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

\(n = 6\)

\(\text{df}(t)_{\text{term1}} = 6\)

\(\text{idf}(t)_{\text{term1}} = \log \frac{n}{\text{df}(t)} + 1 = \log(1)+1 = 1\)

\(\text{tf-idf}_{\text{term1}} = \text{tf} \times \text{idf} = 3 \times 1 = 3\)

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

\(\text{tf-idf}_{\text{term2}} = 0 \times (\log(6/1)+1) = 0\)

\(\text{tf-idf}_{\text{term3}} = 1 \times (\log(6/2)+1) \approx 2.0986\)

以及原始tf-idfs的载体:

\(\text{tf-idf}_{\text{raw}} = [3, 0, 2.0986].\)

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

\(\frac{[3, 0, 2.0986]}{\sqrt{\big(3^2 + 0^2 + 2.0986^2\big)}} = [ 0.819, 0, 0.573].\)

此外,默认参数 smooth_idf=True 将“1”添加到分子和分母中,就像看到一个包含集合中每个术语的额外文档恰好一次一样,这可以防止零除:

\(\text{idf}(t) = \log{\frac{1 + n}{1+\text{df}(t)}} + 1\)

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

\(\text{tf-idf}_{\text{term3}} = 1 \times \log(7/3)+1 \approx 1.8473\)

L2规格化的tf-idf更改为

\(\frac{[3, 0, 1.8473]}{\sqrt{\big(3^2 + 0^2 + 1.8473^2\big)}} = [0.8515, 0, 0.5243]\):

>>> 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)
<Compressed Sparse...dtype 'float64'
  with 19 stored elements and shape (4, 9)>

虽然tf-idf规范化通常非常有用,但在某些情况下,二进制出现标记可能会提供更好的功能。这可以通过使用 binary 参数 CountVectorizer .特别是,一些估计器,例如 伯努里天真的贝耶斯 显式建模离散布尔随机变量。此外,非常短的文本可能具有噪声tf-idf值,而二进制出现信息更稳定。

与往常一样,调整特征提取参数的最佳方法是使用交叉验证的网格搜索,例如通过将特征提取器与分类器管道化:

7.2.3.6. 解码文本文件#

文本由字符组成,但文件由字节组成。这些字节表示根据一些 encoding .要在Python中处理文本文件,其字节必须 decoded 到名为Unicode的字符集。常见的编码有ASC、Latin-1(西欧)、KOI 8-R(俄语)以及通用编码UTF-8和UTF-16。还有许多其他的。

备注

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

scikit-learn中的文本特征提取器知道如何解码文本文件,但前提是您告诉他们文件的编码方式。的 CountVectorizer 需要一个 encoding 参数为此。对于现代文本文件,正确的编码可能是UTF-8,因此这是默认值 (encoding="utf-8" ).

但是,如果您正在加载的文本实际上没有使用UTF-8编码,那么您将得到 UnicodeDecodeError .通过设置 decode_error 参数为 "ignore""replace" .请参阅Python函数的文档 bytes.decode 有关更多详细信息(类型 help(bytes.decode) 在Python提示符处)。

Troubleshooting decoding text#

如果您在解码文本时遇到困难,请尝试以下一些方法:

  • 找出文本的实际编码是什么。该文件可能带有一个标题或REAUTE,告诉您编码,或者您可能可以根据文本的来源假设一些标准编码。

  • 您可能可以使用UNix命令了解它通常是哪种编码 file .蟒蛇 chardet 模块附带一个名为 chardetect.py 它将猜测特定的编码,尽管你不能指望它的猜测是正确的。

  • 您可以尝试UTF-8并忽略错误。可以使用 bytes.decode(errors='replace') 用无意义的字符或集合替换所有解码错误 decode_error='replace' 在向量器中。这可能会损害您功能的有用性。

  • 真正的文本可能来自于不同的来源,这些来源可能使用了不同的编码,甚至可能被草率地解码为与其编码的编码不同的编码。这在从Web检索的文本中很常见。python包 ftfy 可以自动整理某些类别的解码错误,因此您可以尝试将未知文本解码为 latin-1 并且然后使用 ftfy 修复错误。

  • 如果文本位于难以分类的编码混合体中(20个新闻组数据集的情况就是如此),则可以使用简单的单字节编码,例如 latin-1 .某些文本可能会显示不正确,但至少相同的字节序列始终代表相同的特征。

例如,以下代码段使用 chardet (not随scikit-learn一起提供,必须单独安装)才能计算出三个文本的编码。然后,它将文本进行载体化并打印学习的词汇。此处未显示输出。

>>> import chardet
>>> 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)]
>>> v = CountVectorizer().fit(decoded).vocabulary_
>>> for term in v: print(v)

(取决于版本 chardet ,它可能会把第一个弄错。)

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

7.2.3.7. 应用和示例#

词袋表示相当简单,但在实践中却出奇地有用。

特别是以 supervised setting 它可以成功地与快速且可扩展的线性模型结合进行训练 document classifiers ,例如:

unsupervised setting 它可以通过应用集群算法(例如 K-means :

最后,可以通过放松集群的硬分配约束来发现文集的主要主题,例如使用 非负矩阵分解(NMF或NNMF) :

7.2.3.8. 词袋表示的局限性#

一词集(单词包是什么)无法捕获短语和多词表达,从而有效地忽视了任何词序依赖性。此外,词袋模型不考虑潜在的拼写错误或单词衍生。

N-grams来救援!与构建简单的一元语法集合(n=1),人们可能更喜欢二元语法集合(n=2),其中对连续单词对的出现进行计数。

或者可以考虑字符n元语法的集合,这是一种能够抵御拼写错误和派生的表示。

例如,假设我们正在处理由两个文档组成的文集: ['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_out()
array([' w', 'ds', 'or', 'pr', 'rd', 's ', 'wo', 'wp'], ...)
>>> counts.toarray().astype(int)
array([[1, 1, 1, 0, 1, 1, 1, 0],
       [1, 1, 0, 1, 1, 1, 0, 1]])

在上面的例子中, char_wb 使用分析器,该分析器仅从单词边界内的字符创建n元语法(在每边填充空间)。的 char 或者,分析器创建跨单词的n-gram::

>>> ngram_vectorizer = CountVectorizer(analyzer='char_wb', ngram_range=(5, 5))
>>> ngram_vectorizer.fit_transform(['jumpy fox'])
<Compressed Sparse...dtype 'int64'
  with 4 stored elements and shape (1, 4)>

>>> ngram_vectorizer.get_feature_names_out()
array([' fox ', ' jump', 'jumpy', 'umpy '], ...)

>>> ngram_vectorizer = CountVectorizer(analyzer='char', ngram_range=(5, 5))
>>> ngram_vectorizer.fit_transform(['jumpy fox'])
<Compressed Sparse...dtype 'int64'
  with 5 stored elements and shape (1, 5)>
>>> ngram_vectorizer.get_feature_names_out()
array(['jumpy', 'mpy f', 'py fo', 'umpy ', 'y fox'], ...)

边界感知这个词变体 char_wb 对于使用空白进行单词分离的语言来说尤其有趣,因为它生成的噪音特征比原始特征明显少 char 变种在这种情况下对于这些语言,它可以提高使用这些特征训练的分类器的预测准确性和收敛速度,同时保持拼写错误和单词推导的鲁棒性。

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

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

7.2.3.9. 使用哈希技巧对大型文本库进行载体化#

上面的载体化方案很简单,但事实上它具有 in-memory mapping from the string tokens to the integer feature indices (The vocabulary_ 属性)导致多个 problems when dealing with large datasets :

  • 数据库越大,词汇量就会增加越多,因此记忆的使用也会增加,

  • 匹配需要分配大小与原始数据集成比例的中间数据结构。

  • 构建单词映射需要完全通过数据集,因此不可能以严格在线的方式适应文本分类器。

  • 带有大尺寸的腌制和去腌制转向器 vocabulary_ 可能非常慢(通常比picking/unpicking扁平数据结构(例如相同大小的NumPy数组)慢得多),

  • 不太可能将载体化工作拆分为并发的子任务,因为 vocabulary_ 属性必须是具有细粒度同步障碍的共享状态:从令牌串到特征索引的映射取决于每个令牌第一次出现的顺序,因此必须共享,这可能会损害并发工作器的性能,以至于使它们比顺序变体慢。

通过结合“哈希技巧”可以克服这些限制 (特征哈希 )由 FeatureHasher 类以及的文本预处理和标记化功能 CountVectorizer .

此组合实施于 HashingVectorizer ,一个主要与API兼容的Transformer类 CountVectorizer . HashingVectorizer 是无状态的,这意味着您不必打电话 fit 在上面::

>>> from sklearn.feature_extraction.text import HashingVectorizer
>>> hv = HashingVectorizer(n_features=10)
>>> hv.transform(corpus)
<Compressed Sparse...dtype 'float64'
  with 16 stored elements and shape (4, 10)>

您可以看到在向输出中提取了16个非零特征标记:这比之前提取的19个非零特征标记少 CountVectorizer 在同一个玩具库上。差异来自哈希函数冲突,因为 n_features 参数.

在现实世界中, n_features 参数可以保留为其默认值 2 ** 20 (大约一百万个可能的功能)。如果内存或下游模型大小存在问题,请选择较低的值,例如 2 ** 18 可能会有所帮助,而不会在典型的文本分类任务中引入太多额外的冲突。

请注意,维度不会影响在CSR矩阵上操作的算法的中央训练时间 (LinearSVC(dual=True) , Perceptron , SGDClassifier , PassiveAggressive ),但对于使用CSC矩阵的算法, (LinearSVC(dual=False) , Lasso() 等)。

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

>>> hv = HashingVectorizer()
>>> hv.transform(corpus)
<Compressed Sparse...dtype 'float64'
  with 19 stored elements and shape (4, 1048576)>

我们不再得到碰撞,但这是以牺牲输出空间的更大维度为代价的。当然,除了这里使用的19个术语之外,其他术语仍然可能会相互冲突。

HashingVectorizer 还具有以下限制:

  • 无法倒置模型(否 inverse_transform 方法),也不能访问要素的原始字符串表示,因为执行映射的哈希函数的单向性质。

  • 它不提供IDF加权,因为这会在模型中引入状态性。一 TfidfTransformer 如果需要,可以在管道中添加到它。

使用HashingVectorizer执行核心外扩展#

使用一个有趣的发展 HashingVectorizer 就是有能力 out-of-core 缩放。这意味着我们可以从不适合计算机主存储器的数据中学习。

实现核外扩展的策略是以小批量形式将数据流式传输到估计器。每个小批都使用 HashingVectorizer 以保证估计器的输入空间始终具有相同的维度。因此,任何时候使用的内存量都受到迷你批处理的大小的限制。尽管使用这种方法可以吸收的数据量没有限制,但从实际角度来看,学习时间通常受到人们想要在任务上花费的中央处理器时间的限制。

有关文本分类任务中超出核心扩展的完整示例,请参阅 sphx_glr_auto_examples_applications_plot_out_of_core_classification.py .

7.2.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 :取代预处理器和标记化器的可调用对象。默认分析器都调用预处理器和标记化器,但自定义分析器将跳过这一点。N-gram提取和停止词过滤在分析器级别进行,因此自定义分析器可能必须重现这些步骤。

(Lucene用户可能认识这些名称,但要注意scikit-learn概念可能不会一对一地映射到Lucene概念上。

为了使预处理器、标记化器和分析器了解模型参数,可以从类派生并重写 build_preprocessor , build_tokenizerbuild_analyzer 工厂方法而不是传递自定义函数。

提示和技巧#
  • 如果文档已由外部包预先标记化,则将它们存储在文件(或字符串)中,标记用空白分隔并传递 analyzer=str.split

  • Scikit-learn代码库中不包括花哨的标记级分析,例如词干、引理、复合拆分、基于词性的过滤等,但可以通过自定义标记化器或分析器来添加。这里有一个 CountVectorizer 使用一个分词器和词形化器, NLTK

    >>> 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())
    

    (Note这不会过滤掉标点符号。)

    例如,下面的示例将将一些英式拼写转换为美式拼写::

    >>> 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']
    

    用于其他类型的预处理;示例包括词干、词元化或规范化数字标记,后者在:

当处理不使用显式单词分隔符(例如空白)的亚洲语言时,自定义向量器也很有用。

7.2.4. 图像特征提取#

7.2.4.1. 贴片提取#

extract_patches_2d 功能从存储为二维阵列或具有沿着第三轴的颜色信息的三维图像中提取补丁。要从所有补丁重建图像,请使用 reconstruct_from_patches_2d .例如,让我们生成具有3个颜色通道的4x 4像素图片(例如,以RB格式)::

>>> 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 ,只不过它支持多个图像作为输入。它作为scikit-learn Transformer实现,因此可以在管道中使用。请参阅::

>>> 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)

7.2.4.2. 图像的连通图#

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

../_images/sphx_glr_plot_coin_ward_segmentation_001.png

为此,估计器使用“连通性”矩阵,给出哪些样本是连通的。

功能 img_to_graph 从2D或3D图像返回这样的矩阵。同样, grid_to_graph 给定这些图像的形状,为图像构建连接性矩阵。

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