Примечание
Перейти в конец для скачивания полного кода примера. или для запуска этого примера в вашем браузере через 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 новостных групп, который включает около 18000 сообщений новостных групп по 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.222 s at 5.1 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.640 s at 9.8 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.713 s at 8.8 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.629 s at 9.9 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 более гибкий. В частности, он принимает различные шаблоны регулярных выражений через параметр 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.855 s at 7.3 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.637 s at 9.8 MB/s
Можно наблюдать, что это самая быстрая стратегия токенизации текста на данный момент, предполагая, что задача машинного обучения на последующем уровне может терпеть несколько коллизий.
TfidfVectorizer
В большом корпусе текста некоторые слова появляются с большей частотой (например, «the», «a», «is» на английском языке) и не несут значимой информации о фактическом содержании документа. Если мы будем подавать данные о количестве слов непосредственно в классификатор, эти очень распространенные термины затмят частоты более редких, но более информативных терминов. Для того, чтобы перевзвесить числовые признаки в значения с плавающей запятой, подходящие для использования классификатором, очень часто используется преобразование tf-idf, реализованное в TfidfTransformer. TF означает «частоту термина», а «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.751 s at 8.3 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.963 секунды)
Связанные примеры
© 2007–2025 The scikit-learn developers
Licensed under the 3-clause BSD License.
https://scikit-learn.org/1.6/auto_examples/text/plot_hashing_vs_dict_vectorizer.html