Примечание
Перейти в конец, чтобы скачать полный код примера или запустить этот пример в браузере с помощью JupyterLite или Binder.
Сравнение FeatureHasher и DictVectorizer
В этом примере мы демонстрируем векторизацию текста — процесс представления нечисловых входных данных (например, словарей или текстовых документов) в виде векторов вещественных чисел.
Сначала мы сравним FeatureHasher и DictVectorizer, используя оба метода для векторизации текстовых документов, предварительно обработанных (токенизированных) с помощью пользовательской функции Python.
Далее мы рассмотрим и проанализируем специализированные для текста векторизаторы HashingVectorizer, CountVectorizer и TfidfVectorizer, которые выполняют и токенизацию, и формирование матрицы признаков в рамках одного класса.
Цель примера — продемонстрировать использование API для векторизации текста и сравнить время обработки. Примеры реального обучения на текстовых документах см. в сценариях Классификация текстовых документов с использованием разреженных признаков и Кластеризация текстовых документов с помощью k-means.
# Authors: The scikit-learn developers # SPDX-License-Identifier: BSD-3-Clause
Загрузка данных
Мы загружаем данные из текстового набора данных 20 newsgroups, содержащего около 18 000 публикаций в группах новостей на 20 тем, разделённых на две подвыборки: обучающую и тестовую. Для простоты и снижения вычислительных затрат мы выбираем подмножество из 7 тем и используем только обучающую выборку.
from sklearn.datasets import fetch_20newsgroups
categories = [
"alt.atheism",
"comp.graphics",
"comp.sys.ibm.pc.hardware",
"misc.forsale",
"rec.autos",
"sci.space",
"talk.religion.misc",
]
print("Loading 20 newsgroups training data")
raw_data, _ = fetch_20newsgroups(subset="train", categories=categories, return_X_y=True)
data_size_mb = sum(len(s.encode("utf-8")) for s in raw_data) / 1e6
print(f"{len(raw_data)} documents - {data_size_mb:.3f}MB")
Loading 20 newsgroups training data 3803 documents - 6.245MB
Определение функций предварительной обработки
Токеном может быть слово, часть слова или любая последовательность символов между пробелами или знаками в строке. Здесь мы определяем функцию, извлекающую токены с помощью простого регулярного выражения (regex), которое находит символы Unicode, используемые в словах. К ним относятся большинство символов, которые могут входить в состав слова на любом языке, а также цифры и символ подчёркивания:
import re
def tokenize(doc):
"""Extract tokens from doc.
This uses a simple regex that matches word characters to break strings
into tokens. For a more principled approach, see CountVectorizer or
TfidfVectorizer.
"""
return (tok.lower() for tok in re.findall(r"\w+", doc))
list(tokenize("This is a simple example, isn't it?"))
['this', 'is', 'a', 'simple', 'example', 'isn', 't', 'it']
Мы определяем дополнительную функцию, подсчитывающую частоту появления каждого токена в заданном документе. Она возвращает словарь частот, который будут использовать векторизаторы.
from collections import defaultdict
def token_freqs(doc):
"""Extract a dict mapping tokens from doc to their occurrences."""
freq = defaultdict(int)
for tok in tokenize(doc):
freq[tok] += 1
return freq
token_freqs("That is one example, but this is another one")
defaultdict(<class 'int'>, {'that': 1, 'is': 2, 'one': 2, 'example': 1, 'but': 1, 'this': 1, 'another': 1})
Обратите внимание: повторяющийся токен "is", например, учитывается дважды.
Разбиение текстового документа на словесные токены с возможной потерей информации о порядке слов в предложении часто называют представлением «мешок слов».
DictVectorizer
Сначала мы проводим сравнительный тест DictVectorizer, а затем сравниваем его с FeatureHasher, поскольку оба метода принимают на вход словари.
from time import time
from sklearn.feature_extraction import DictVectorizer
dict_count_vectorizers = defaultdict(list)
t0 = time()
vectorizer = DictVectorizer()
vectorizer.fit_transform(token_freqs(d) for d in raw_data)
duration = time() - t0
dict_count_vectorizers["vectorizer"].append(
vectorizer.__class__.__name__ + "\non freq dicts"
)
dict_count_vectorizers["speed"].append(data_size_mb / duration)
print(f"done in {duration:.3f} s at {data_size_mb / duration:.1f} MB/s")
print(f"Found {len(vectorizer.get_feature_names_out())} unique terms")
done in 1.051 s at 5.9 MB/s Found 47928 unique terms
Фактическое соответствие текстового токена индексу столбца явно хранится в атрибуте .vocabulary_, который представляет собой потенциально очень большой словарь Python:
type(vectorizer.vocabulary_)
len(vectorizer.vocabulary_)
47928
vectorizer.vocabulary_["example"]
19145
FeatureHasher
Словари занимают много места и увеличиваются по мере роста обучающей выборки. Вместо того чтобы увеличивать векторы вместе со словарём, хеширование признаков строит вектор заранее заданной длины, применяя к признакам (например, токенам) хеш-функцию h, а затем используя хеш-значения непосредственно как индексы признаков и обновляя вектор по этим индексам. Если пространство признаков недостаточно велико, хеш-функции склонны сопоставлять различные значения одному и тому же хеш-коду (коллизии хеширования). В результате невозможно определить, какой объект породил тот или иной хеш-код.
По этой причине невозможно восстановить исходные токены по матрице признаков. Лучше всего оценить количество уникальных терминов в исходном словаре, подсчитав число активных столбцов в закодированной матрице признаков. Для этого мы определяем следующую функцию:
import numpy as np
def n_nonzero_columns(X):
"""Number of columns with at least one non-zero value in a CSR matrix.
This is useful to count the number of features columns that are effectively
active when using the FeatureHasher.
"""
return len(np.unique(X.nonzero()[1]))
Число признаков по умолчанию для FeatureHasher равно 2**20. Здесь мы задаём n_features = 2**18, чтобы продемонстрировать коллизии хеширования.
FeatureHasher для словарей частот
from sklearn.feature_extraction import FeatureHasher
t0 = time()
hasher = FeatureHasher(n_features=2**18)
X = hasher.transform(token_freqs(d) for d in raw_data)
duration = time() - t0
dict_count_vectorizers["vectorizer"].append(
hasher.__class__.__name__ + "\non freq dicts"
)
dict_count_vectorizers["speed"].append(data_size_mb / duration)
print(f"done in {duration:.3f} s at {data_size_mb / duration:.1f} MB/s")
print(f"Found {n_nonzero_columns(X)} unique tokens")
done in 0.605 s at 10.3 MB/s Found 43873 unique tokens
При использовании FeatureHasher число уникальных токенов меньше, чем при использовании DictVectorizer. Это связано с коллизиями хеширования.
Количество коллизий можно уменьшить, увеличив пространство признаков. Обратите внимание, что скорость векторизатора существенно не меняется при задании большого числа признаков, однако это увеличивает размерность коэффициентов и, следовательно, требует больше памяти для их хранения, даже если большинство из них неактивны.
t0 = time()
hasher = FeatureHasher(n_features=2**22)
X = hasher.transform(token_freqs(d) for d in raw_data)
duration = time() - t0
print(f"done in {duration:.3f} s at {data_size_mb / duration:.1f} MB/s")
print(f"Found {n_nonzero_columns(X)} unique tokens")
done in 0.608 s at 10.3 MB/s Found 47668 unique tokens
Мы подтверждаем, что число уникальных токенов приближается к числу уникальных терминов, найденных с помощью DictVectorizer.
FeatureHasher для исходных токенов
Также можно задать input_type="string" в FeatureHasher, чтобы векторизовать строки, напрямую возвращаемые пользовательской функцией tokenize. Это эквивалентно передаче словаря, в котором каждому имени признака неявно присваивается частота 1.
t0 = time()
hasher = FeatureHasher(n_features=2**18, input_type="string")
X = hasher.transform(tokenize(d) for d in raw_data)
duration = time() - t0
dict_count_vectorizers["vectorizer"].append(
hasher.__class__.__name__ + "\non raw tokens"
)
dict_count_vectorizers["speed"].append(data_size_mb / duration)
print(f"done in {duration:.3f} s at {data_size_mb / duration:.1f} MB/s")
print(f"Found {n_nonzero_columns(X)} unique tokens")
done in 0.590 s at 10.6 MB/s Found 43873 unique tokens
Теперь построим график скорости описанных выше методов векторизации.
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(12, 6))
y_pos = np.arange(len(dict_count_vectorizers["vectorizer"]))
ax.barh(y_pos, dict_count_vectorizers["speed"], align="center")
ax.set_yticks(y_pos)
ax.set_yticklabels(dict_count_vectorizers["vectorizer"])
ax.invert_yaxis()
_ = ax.set_xlabel("speed (MB/s)")

В обоих случаях FeatureHasher работает примерно в два раза быстрее, чем DictVectorizer. Это удобно при работе с большими объёмами данных, но преобразование становится необратимым, что усложняет интерпретацию модели.
Вариант FeatureHeasher с input_type="string" немного быстрее варианта, работающего со словарём частот, поскольку он не подсчитывает повторяющиеся токены: каждый токен неявно учитывается один раз, даже если встречается повторно. В зависимости от последующей задачи машинного обучения это может быть как ограничением, так и нет.
Сравнение со специализированными векторизаторами текста
CountVectorizer принимает исходные данные, поскольку выполняет токенизацию и подсчёт вхождений внутри класса. Он похож на DictVectorizer при использовании вместе с пользовательской функцией token_freqs, как в предыдущем разделе. Разница в том, что CountVectorizer более гибок. В частности, он поддерживает различные шаблоны regex через параметр token_pattern.
from sklearn.feature_extraction.text import CountVectorizer
t0 = time()
vectorizer = CountVectorizer()
vectorizer.fit_transform(raw_data)
duration = time() - t0
dict_count_vectorizers["vectorizer"].append(vectorizer.__class__.__name__)
dict_count_vectorizers["speed"].append(data_size_mb / duration)
print(f"done in {duration:.3f} s at {data_size_mb / duration:.1f} MB/s")
print(f"Found {len(vectorizer.get_feature_names_out())} unique terms")
done in 0.793 s at 7.9 MB/s Found 47885 unique terms
Мы видим, что реализация CountVectorizer работает примерно в два раза быстрее, чем DictVectorizer вместе с простой функцией, которую мы определили для сопоставления токенов. Причина в том, что CountVectorizer оптимизирован за счёт повторного использования скомпилированного регулярного выражения для всего обучающего набора, вместо создания отдельного выражения для каждого документа, как это происходит в нашей простой функции токенизации.
Теперь проведём аналогичный эксперимент с HashingVectorizer, который сочетает «хеширование» класса FeatureHasher с предварительной обработкой текста и токенизацией класса CountVectorizer.
from sklearn.feature_extraction.text import HashingVectorizer
t0 = time()
vectorizer = HashingVectorizer(n_features=2**18)
vectorizer.fit_transform(raw_data)
duration = time() - t0
dict_count_vectorizers["vectorizer"].append(vectorizer.__class__.__name__)
dict_count_vectorizers["speed"].append(data_size_mb / duration)
print(f"done in {duration:.3f} s at {data_size_mb / duration:.1f} MB/s")
done in 0.581 s at 10.7 MB/s
Как видно, это самый быстрый из рассмотренных способов токенизации текста, если последующая задача машинного обучения допускает несколько коллизий.
TfidfVectorizer
В большом текстовом корпусе некоторые слова встречаются чаще (например, «the», «a», «is» в английском языке) и не несут значимой информации о содержании документа. Если напрямую передать классификатору данные о частоте слов, очень распространённые термины будут затенять частоты более редких, но информативных терминов. Чтобы преобразовать признаки-частоты в значения с плавающей точкой, подходящие для классификатора, часто используют преобразование tf-idf, реализованное в TfidfTransformer. TF означает «частота термина» (term-frequency), а tf-idf — это произведение частоты термина на обратную частоту документа.
Теперь мы проведём сравнительный тест TfidfVectorizer, который сочетает токенизацию и подсчёт вхождений из CountVectorizer с нормализацией и взвешиванием из TfidfTransformer.
from sklearn.feature_extraction.text import TfidfVectorizer
t0 = time()
vectorizer = TfidfVectorizer()
vectorizer.fit_transform(raw_data)
duration = time() - t0
dict_count_vectorizers["vectorizer"].append(vectorizer.__class__.__name__)
dict_count_vectorizers["speed"].append(data_size_mb / duration)
print(f"done in {duration:.3f} s at {data_size_mb / duration:.1f} MB/s")
print(f"Found {len(vectorizer.get_feature_names_out())} unique terms")
done in 0.795 s at 7.9 MB/s Found 47885 unique terms
Краткое описание
Завершим эту записную книжку, обобщив все записанные скорости обработки на одном графике:
fig, ax = plt.subplots(figsize=(12, 6))
y_pos = np.arange(len(dict_count_vectorizers["vectorizer"]))
ax.barh(y_pos, dict_count_vectorizers["speed"], align="center")
ax.set_yticks(y_pos)
ax.set_yticklabels(dict_count_vectorizers["vectorizer"])
ax.invert_yaxis()
_ = ax.set_xlabel("speed (MB/s)")

Обратите внимание, что TfidfVectorizer работает немного медленнее, чем CountVectorizer, из-за дополнительной операции, выполняемой TfidfTransformer.
Также обратите внимание, что при установке числа признаков n_features = 2**18 HashingVectorizer работает лучше, чем CountVectorizer, но преобразование становится необратимым из-за коллизий хешей.
Отметим, что CountVectorizer и HashingVectorizer работают лучше, чем соответствующие им DictVectorizer и FeatureHasher при обработке документов с токенизацией, выполненной вручную, поскольку этап внутренней токенизации первых векторизаторов один раз компилирует регулярное выражение, а затем использует его повторно для всех документов.
Общее время выполнения скрипта: (0 минут 5.642 секунды)
Связанные примеры
© 2007–2026 The scikit-learn developers
Licensed under the 3-clause BSD License.
https://scikit-learn.org/1.9/auto_examples/text/plot_hashing_vs_dict_vectorizer.html