6.2. Извлечение признаков
Модуль sklearn.feature_extraction может использоваться для извлечения признаков в формате, поддерживаемом алгоритмами машинного обучения, из наборов данных, содержащих данные, такие как текст и изображения.
Примечание
Извлечение признаков сильно отличается от отбора признаков: первое заключается в преобразовании произвольных данных, таких как текст или изображения, в числовые признаки, пригодные для машинного обучения. Последнее — это метод машинного обучения, применяемый к этим признакам.
6.2.1. Загрузка признаков из словарей
Класс DictVectorizer может использоваться для преобразования массивов признаков, представленных в виде списков стандартных объектов Python dict в представление NumPy/SciPy, используемое оценщиками scikit-learn.
Хотя он не является особенно быстрым в обработке, Python’s dict имеет преимущества удобства использования, разреженности (отсутствующие признаки не нужно хранить) и хранения имен признаков в дополнение к значениям.
DictVectorizer реализует так называемое кодирование one-of-K или «one-hot» для категориальных (также известных как номинальные, дискретные) признаков. Категориальные признаки представляют собой пары «атрибут-значение», где значение ограничено списком дискретных возможностей без упорядочения (например, идентификаторы тем, типы объектов, теги, имена...).
В следующем примере «город» является категориальным атрибутом, а «температура» — традиционным числовым признаком:
>>> 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), которые мы хотим использовать в качестве дополнительных тегов для обучения последовательного классификатора (например, фразового разделителя). Следующий словарь может быть таким окном признаков, извлеченным вокруг слова «sat» в предложении «The cat sat on the mat.»:
>>> 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'], ...)
Как вы можете себе представить, если извлечь такой контекст вокруг каждого отдельного слова в корпусе документов, полученная матрица будет очень широкой (много признаков one-hot), при этом большинство из них будут иметь значение ноль большую часть времени. Для того, чтобы структура результирующих данных могла поместиться в памяти, класс DictVectorizer по умолчанию использует scipy.sparse матрицу вместо numpy.ndarray.
6.2.2. Хэширование признаков
Класс FeatureHasher — это высокоскоростной и малоемкостный векторизатор, использующий технику, известную как хэширование признаков или «хитрый хэш». Вместо построения таблицы хэшей встреченных в процессе обучения признаков, как это делают векторизаторы, экземпляры класса FeatureHasher применяют функцию хэширования к признакам, чтобы определить их индекс столбца в матрицах образцов напрямую. В результате увеличивается скорость и уменьшается потребление памяти, за счет потери инспекции; хэшировщик не запоминает, какими были входные признаки, и не имеет inverse_transform метода.
Поскольку функция хэширования может вызывать коллизии между (независимыми) признаками, используется функция хэширования со знаком, и знак значения хэша определяет знак значения, хранящегося в выходной матрице для признака. Таким образом, коллизии скорее всего будут отменяться, а не накапливать ошибку, и ожидаемое среднее значение любого выходного признака равно нулю. Этот механизм включен по умолчанию с alternate_sign=True и особенно полезен для небольших размеров таблицы хэшей (n_features < 10000). Для больших размеров таблицы хэшей его можно отключить, чтобы позволить выводу передаваться оценщикам, таким как MultinomialNB или chi2 селекторам признаков, которые ожидают неотрицательные входные данные.
FeatureHasher принимает либо отображения (например, dict Python и его аналоги в модуле 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 использует вариант MurmurHash3 со знаком и 32-битным представлением. В результате (и из-за ограничений в scipy.sparse) максимальное количество поддерживаемых признаков в настоящее время составляет \(2^{31} - 1\).
Оригинальная формулировка хэширования признаков Weinberger et al. использовала две отдельные функции хэширования \(h\) и \(\xi\) для определения индекса столбца и знака признака соответственно. Текущая реализация работает при предположении, что бит знака MurmurHash3 независим от его других битов.
Поскольку для преобразования функции хэширования в индекс столбца используется просто операция взятия остатка от деления, рекомендуется использовать степень двойки в качестве параметра n_features; в противном случае признаки не будут равномерно отображаться в столбцах.
Ссылки
Ссылки
- Kilian Weinberger, Anirban Dasgupta, John Langford, Alex Smola и Josh Attenberg (2009). Feature hashing for large scale multitask learning. Proc. ICML.
6.2.3. Извлечение текстовых признаков
6.2.3.1. Представление «мешок слов»
Анализ текста — важная область применения алгоритмов машинного обучения. Однако исходные данные, последовательность символов, не могут быть напрямую переданы алгоритмам, так как большинство из них ожидает числовые векторы признаков с фиксированной размерностью, а не текстовые документы с переменной длиной.
Для решения этой проблемы scikit-learn предоставляет инструменты для наиболее распространённых способов извлечения числовых признаков из текстового содержимого, а именно:
- разбиение строк на токены и присвоение целочисленного идентификатора каждому возможному токену, например, используя пробелы и знаки препинания в качестве разделителей токенов.
- подсчёт частоты появления токенов в каждом документе.
- нормализация и взвешивание с уменьшением важности токенов, которые встречаются в большинстве образцов/документов.
В этой схеме признаки и образцы определяются следующим образом:
- каждая частота появления индивидуального токена (нормализованная или нет) рассматривается как признак.
- вектор всех частот токенов для данного документа считается многомерным образцом.
Таким образом, корпус документов может быть представлен матрицей, где каждая строка соответствует документу, а каждый столбец — токену (например, слову), встречающемуся в корпусе.
Мы называем векторизацию общий процесс преобразования коллекции текстовых документов в числовые векторы признаков. Этот конкретный метод (разбиение на токены, подсчёт и нормализация) называется представлением «мешок слов» или «мешок n-грамм». Документы описываются по частоте встречаемости слов, полностью игнорируя информацию о взаимном расположении слов в документе.
6.2.3.2. Разреженность
Поскольку большинство документов, как правило, используют очень небольшую часть слов, используемых в корпусе, полученная матрица будет содержать много нулевых значений признаков (обычно более 99% из них).
Например, коллекция из 10 000 коротких текстовых документов (таких как электронные письма) будет использовать словарь размером порядка 100 000 уникальных слов в целом, в то время как каждый документ будет использовать от 100 до 1000 уникальных слов.
Для того, чтобы хранить такую матрицу в памяти, а также ускорить алгебраические операции матрица/вектор, в реализациях, как правило, используется разреженное представление, например, реализация, доступная в пакете scipy.sparse.
6.2.3.3. Типовое использование векторизатора
CountVectorizer реализует как разбиение на токены, так и подсчёт частоты в одном классе:
>>> from sklearn.feature_extraction.text import CountVectorizer
Эта модель имеет множество параметров, однако значения по умолчанию достаточно разумны (подробнее см. документацию):
>>> 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.vocabulary_.get('document')
1
Таким образом, слова, которые не встречались в обучающем корпусе, будут полностью игнорироваться в будущих вызовах метода transform:
>>> vectorizer.transform(['Something completely new.']).toarray() array([[0, 0, 0, 0, 0, 0, 0, 0, 0]]...)
Обратите внимание, что в предыдущем корпусе первые и последние документы содержат точно такие же слова, поэтому они закодированы в одинаковые векторы. В частности, мы теряем информацию о том, что последний документ имеет вопросительную форму. Для сохранения части локальной информации о порядке мы можем извлечь 2-граммы слов в дополнение к 1-граммам (отдельным словам):
>>> 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]...)
6.2.3.4. Использование стоп-слов
Стоп-слова — это слова вроде «и», «это», «он», которые, как предполагается, не несут полезной информации для представления содержимого текста и могут быть удалены, чтобы избежать их интерпретации как сигнала для прогнозирования. Однако иногда подобные слова полезны для прогнозирования, например, при классификации стиля письма или личности.
В нашем предоставленном списке стоп-слов для английского языка есть несколько известных проблем. Он не предназначен для универсального решения, так как некоторые задачи могут потребовать более индивидуального подхода. Для получения дополнительной информации см. [NQY18].
Обращайте внимание при выборе списка стоп-слов. Популярные списки стоп-слов могут включать слова, которые являются высокоинформативными для некоторых задач, например, компьютер.
Также необходимо убедиться, что список стоп-слов подвергся той же предобработке и разбиению на токены, что и используемый векторизатором. Слово we’ve разбивается на we и ve по умолчанию токенизатором CountVectorizer, поэтому, если we’ve есть в stop_words, но ve нет, ve сохранится из we’ve в преобразованном тексте. Наши векторизаторы будут пытаться выявить и предупредить о некоторых видах несоответствий.
Ссылки
J. Nothman, H. Qin и R. Yurchak (2018). “Списки стоп-слов в свободных программных пакетах с открытым исходным кодом”. В Proc. Workshop for NLP Open Source Software.
6.2.3.5. Взвешивание терминов Tf–idf
В большом корпусе текста некоторые слова (например, «the», «a», «is» на английском языке) встречаются очень часто, поэтому несут мало значимой информации о фактическом содержании документа. Если мы передадим прямые данные подсчета непосредственно классификатору, то очень частые термины затмят частоты более редких, но более интересных терминов.
Для того, чтобы перевзвесить количественные признаки в значения с плавающей точкой, подходящие для использования классификатором, очень часто используется преобразование tf–idf.
Tf означает частоту термина, а tf–idf означает частоту термина, умноженную на обратную частоту документа: \(\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-idf и как вычисления tf-idf в scikit-learn’s TfidfTransformer и TfidfVectorizer немного отличаются от стандартной обозначения в учебниках, где idf определяется как
\(\text{idf}(t) = \log{\frac{n}{1+\text{df}(t)}}.\)
В TfidfTransformer и TfidfVectorizer с smooth_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)
Обратитесь к документации по ссылке для получения подробной информации обо всех параметрах.
Числовой пример матрицы 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-idf:
\(\text{tf-idf}_{\text{raw}} = [3, 0, 2.0986].\)
Затем, применяя евклидову (L2) норму, мы получаем следующие tf-idf для документа 1:
\(\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\)
Используя это изменение, tf-idf третьего термина в документе 1 изменяется на 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, который объединяет все опции CountVectorizer и TfidfTransformer в одной модели:
>>> 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. В частности, некоторые оценщики, такие как Bernoulli Naive Bayes, явно моделируют дискретные булевы случайные переменные. Кроме того, очень короткие тексты, вероятно, будут иметь шумные значения tf-idf, в то время как информация о бинарном наличии более стабильна.
Как обычно, лучший способ настроить параметры извлечения признаков — использовать перекрестную проверку с помощью сетки, например, путем конвейеризации извлекателя признаков с классификатором:
6.2.3.6. Декодирование текстовых файлов
Текст состоит из символов, но файлы состоят из байтов. Эти байты представляют символы в соответствии с определённым кодированием. Для работы с текстовыми файлами в 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 (наберите help(bytes.decode) в командной строке Python).
Устранение неполадок с декодированием текста
Если у вас возникают проблемы с декодированием текста, попробуйте следующее:
- Выясните, какая кодировка используется на самом деле. В файле может быть заголовок или файл README, в котором указана кодировка, или, возможно, вы можете предположить стандартную кодировку на основе источника текста.
- Вы можете определить тип кодировки с помощью команды UNIX
file. Модуль Pythonchardetпоставляется со скриптомchardetect.py, который попытается угадать кодировку, но вы не можете полагаться на правильность его предположения. - Вы можете попробовать UTF-8 и проигнорировать ошибки. Вы можете декодировать байтовые строки с помощью
bytes.decode(errors='replace')для замены всех ошибок декодирования бессмысленным символом или установитьdecode_error='replace'в векторизаторе. Это может повлиять на полезность ваших признаков. - Реальный текст может поступать из различных источников, которые использовали разные кодировки или даже были некорректно декодированы в кодировке, отличной от той, в которой были закодированы. Это часто встречается в тексте, полученном из сети Интернет. Пакет Python ftfy может автоматически исправлять некоторые классы ошибок декодирования, поэтому вы можете попробовать декодировать неизвестный текст как
latin-1и затем использоватьftfyдля исправления ошибок. - Если текст содержит смесь кодировок, которую сложно разобрать (что характерно для набора данных 20 Newsgroups), вы можете использовать простую однобайтовую кодировку, такую как
latin-1. Некоторые тексты могут отображаться некорректно, но по крайней мере последовательность байтов всегда будет представлять один и тот же признак.
Например, следующий фрагмент кода использует chardet (не входит в состав 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 и кодировок символов в целом, см. статью Джоэла Сполки «Абсолюльный минимум, который должен знать каждый разработчик ПО о Unicode» https://www.joelonsoftware.com/articles/Unicode.html.
6.2.3.7. Примеры использования и приложения
Представление «мешок слов» довольно упрощенное, но на практике оказывается довольно полезным.
В частности, в обучающей настройке оно успешно сочетается с быстрыми и масштабируемыми линейными моделями для обучения классификаторам документов, например:
В безконтрольной настройке оно может использоваться для группировки похожих документов с помощью алгоритмов кластеризации, таких как K-means:
Наконец, можно выявить основные темы корпуса, ослабив жёсткое ограничение кластеризации, например, используя Разложение матрицы без отрицательных значений (NMF или NNMF):
6.2.3.8. Недостатки представления «мешок слов»
Совокупность униграмм (как в представлении «мешок слов») не может охватить фразы и многословные выражения, фактически игнорируя зависимость от порядка слов. Кроме того, модель «мешок слов» не учитывает потенциальные орфографические ошибки или производные слова.
N-граммы на помощь! Вместо построения простой совокупности униграмм (n=1) можно предпочесть совокупность биграмм (n=2), где подсчитываются случаи последовательности двух слов.
Альтернативно можно рассмотреть совокупность n-грамм символов, представление, устойчивое к орфографическим ошибкам и производным словам.
Например, предположим, что мы имеем дело с корпусом из двух документов: ['words', 'wprds']. Второй документ содержит орфографическую ошибку в слове «слова». Простое представление «мешок слов» рассматривало бы эти два документа как очень разные, отличающиеся по обоим возможным признакам. Представление с использованием 2-грамм символов, однако, обнаружило бы совпадение документов по 4 из 8 признаков, что может помочь предпочитаемому классификатору принять более обоснованное решение:
>>> 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-граммы, которые охватывают слова:
>>> 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-граммы вместо отдельных слов, «мешок слов» и «мешок n-грамм» разрушают большую часть внутренней структуры документа и, следовательно, большую часть смысла, содержащегося в этой внутренней структуре.
Для решения более широкой задачи понимания естественного языка необходимо учитывать локальную структуру предложений и абзацев. Многие такие модели будут построены как задачи «структурного вывода», которые в настоящее время не поддерживаются в scikit-learn.
6.2.3.9. Векторизация большого текстового корпуса с помощью хэширования
Приведенная выше схема векторизации проста, но тот факт, что она содержит отображение в памяти из строковых токенов в целочисленные индексы признаков (атрибут vocabulary_) вызывает несколько проблем при работе с большими наборами данных:
- чем больше корпус, тем больше будет увеличиваться словарь, а следовательно, и потребление памяти,
- для подгонки требуется выделение промежуточных структур данных, размер которых пропорционален размеру исходного набора данных.
- построение словаря соответствия требует полного прохода по набору данных, поэтому невозможно обучить текстовые классификаторы в строго онлайн-режиме.
- сериализация и десериализация векторизаторов с большим
vocabulary_может быть очень медленной (как правило, намного медленнее, чем сериализация/десериализация плоских структур данных, таких как массив NumPy того же размера), - невозможно легко разделить работу по векторизации на конкурирующие подзадачи, так как атрибут
vocabulary_должен представлять собой разделяемое состояние с барьером синхронизации с высокой точностью: отображение из строки токена в индекс признака зависит от порядка первого появления каждого токена, поэтому должно быть разделяемым, что потенциально может навредить производительности конкурирующих потоков до такой степени, что они станут медленнее, чем последовательная версия.
Преодолеть эти ограничения можно, объединив «хитрость хэширования» (Хэширование признаков), реализованную классом FeatureHasher, и функциями предобработки текста и токенизации класса CountVectorizer.
Это сочетание реализовано в HashingVectorizer, классе-преобразователе, совместимом по API в основном с 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, может помочь, не вызывая слишком много дополнительных коллизий на типичных задачах классификации текста.
Обратите внимание, что размерность не влияет на время обучения алгоритмов на CPU, которые работают с матрицами 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 — это возможность выполнять масштабирование вне основной памяти. Это означает, что мы можем обучаться на данных, которые не помещаются в оперативную память компьютера.
Стратегия реализации масштабирования вне рабочей области состоит в потоковой передаче данных в мини-пакетах в оценщик. Каждый мини-пакет векторизуется с помощью HashingVectorizer, чтобы гарантировать, что пространство ввода оценщика всегда имеет одинаковую размерность. Таким образом, объем используемой памяти в любой момент ограничен размером мини-пакета. Хотя нет предела количеству данных, которые можно получить таким образом, с практической точки зрения время обучения часто ограничено временем процессора, которое нужно потратить на задачу.
Полный пример масштабирования вне рабочей области в задаче классификации текста см. в Классификация текстовых документов вне рабочей области.
6.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-грамм и фильтрация стоп-слов происходят на уровне анализатора, поэтому пользовательский анализатор может должен воспроизвести эти шаги.
(Пользователи Lucene могут узнать эти имена, но имейте в виду, что понятия scikit-learn могут не соответствовать понятиям Lucene один к одному.)
Для того, чтобы предобработчик, токенизатор и анализаторы знали о параметрах модели, можно наследоваться от класса и переопределить фабричные методы build_preprocessor, build_tokenizer и build_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())
(Обратите внимание, что это не отфильтрует пунктуацию.)
Следующий пример преобразует, например, некоторые британские написания в американские написания:
>>> 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']
для других стилей предобработки; примеры включают стемминг, лемматизацию или нормализацию числовых токенов, последний пример показан в:
Настройка векторизатора также может быть полезна при работе с азиатскими языками, которые не используют явного разделителя слов, например, пробела.
6.2.4. Извлечение признаков изображения
6.2.4.1. Извлечение фрагментов
Функция extract_patches_2d извлекает фрагменты из изображения, представленного двумерным массивом или трёхмерным массивом с цветовой информацией по третьему измерению. Для восстановления изображения из всех его фрагментов используйте reconstruct_from_patches_2d. Например, давайте сгенерируем изображение размером 4x4 пикселя с 3 цветовыми каналами (например, в формате RGB):
>>> 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, поэтому может использоваться в конвейерах. См.:
>>> 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)
6.2.4.2. Граф связности изображения
Некоторые оценщики в scikit-learn могут использовать информацию о связности между признаками или образцами. Например, кластеризация Уорда (Иерархическая кластеризация) может объединять только соседние пиксели изображения, таким образом образуя сопряжённые фрагменты:
Для этой цели оценщики используют матрицу «связности», указывающую, какие образцы соединены.
Функция img_to_graph возвращает такую матрицу из 2D или 3D изображения. Аналогично, grid_to_graph строит матрицу связности для изображений, исходя из формы этих изображений.
Эти матрицы могут использоваться для наложения связности в оценщиках, использующих информацию о связности, таких как кластеризация Уорда (Иерархическая кластеризация), а также для построения предварительно вычисленных ядер или матриц сходства.
© 2007–2025 The scikit-learn developers
Licensed under the 3-clause BSD License.
https://scikit-learn.org/1.6/modules/feature_extraction.html