Spec-Zone.ru › scikit-learn

Примечание

Перейти в конец, чтобы скачать полный код примера или запустить этот пример в браузере с помощью 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)")
plot hashing vs dict vectorizer

В обоих случаях 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)")
plot hashing vs dict vectorizer

Обратите внимание, что TfidfVectorizer работает немного медленнее, чем CountVectorizer, из-за дополнительной операции, выполняемой TfidfTransformer.

Также обратите внимание, что при установке числа признаков n_features = 2**18 HashingVectorizer работает лучше, чем CountVectorizer, но преобразование становится необратимым из-за коллизий хешей.

Отметим, что CountVectorizer и HashingVectorizer работают лучше, чем соответствующие им DictVectorizer и FeatureHasher при обработке документов с токенизацией, выполненной вручную, поскольку этап внутренней токенизации первых векторизаторов один раз компилирует регулярное выражение, а затем использует его повторно для всех документов.

Общее время выполнения скрипта: (0 минут 5.642 секунды)

Launch binder
Launch JupyterLite

Download Jupyter notebook: plot_hashing_vs_dict_vectorizer.ipynb

Download Python source code: plot_hashing_vs_dict_vectorizer.py

Download zipped: plot_hashing_vs_dict_vectorizer.zip

Связанные примеры

Кластеризация текстовых документов методом k-средних

Классификация текстовых документов с использованием разреженных признаков

Бикластеризация документов с помощью алгоритма Spectral Co-clustering

Преобразователь столбцов для разнородных источников данных

© 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API