6.2. Извлечение признаков

Модуль sklearn.feature_extraction может быть использован для функций выписки в формате, поддерживаемом с помощью алгоритмов машинного обучения из наборов данных, состоящих из форматов , такие как текст и изображения.

Примечание

Извлечение функций сильно отличается от выбора признаков: первый состоит в преобразовании произвольных данных, таких как текст или изображения, в числовые функции, которые можно использовать для машинного обучения. Последний представляет собой метод машинного обучения, применяемый к этим функциям.

6.2.1. Загрузка функций из dicts

Класс DictVectorizer может использоваться для преобразования массивов функций, представленных в виде списков стандартных dict объектов Python, в представление NumPy / SciPy, используемое оценщиками scikit-learn.

Хотя 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()
['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() == ['category=animation', 'category=drama',
...                             'category=family', 'category=thriller',
...                             'year']
True
>>> vec.transform({'category': ['thriller'],
...                'unseen_feature': '3'}).toarray()
array([[0., 0., 0., 1., 0.]])

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

Например, предположим, что у нас есть первый алгоритм, который извлекает теги части речи (PoS), которые мы хотим использовать в качестве дополнительных тегов для обучения классификатора последовательности (например, фрагмента). Следующее изречение могло быть таким окном черт, выделенных вокруг слова «сидел» в предложении «Кот сел на циновку»:

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

Это описание может быть векторизовано в разреженную двумерную матрицу, подходящую для ввода в классификатор (возможно, после передачи в a TfidfTransformer для нормализации):

>>> vec = DictVectorizer()
>>> pos_vectorized = vec.fit_transform(pos_window)
>>> pos_vectorized
<1x6 sparse matrix of type '<... 'numpy.float64'>'
    with 6 stored elements in Compressed Sparse ... format>
>>> pos_vectorized.toarray()
array([[1., 1., 1., 1., 1., 1.]])
>>> vec.get_feature_names()
['pos+1=PP', 'pos-1=NN', 'pos-2=DT', 'word+1=on', 'word-1=cat', 'word-2=the']

Как вы можете себе представить, если выделить такой контекст вокруг каждого отдельного слова в корпусе документов, результирующая матрица будет очень широкой (много однозначных функций), и большинство из них большую часть времени будут иметь нулевые значения. Чтобы полученная структура данных могла уместиться в памяти, DictVectorizer класс scipy.sparse по умолчанию использует матрицу вместо numpy.ndarray.

6.2.2. Хеширование функций

Класс FeatureHasher представляет собой высокоскоростной векторизатор с низким объемом памяти, в котором используется метод, известный как хеширование функций или «трюк с хешированием». Вместо построения хэш-таблицы функций, встречающихся при обучении, как это делают векторизаторы, экземпляры FeatureHasher применяют хеш-функцию к функциям, чтобы напрямую определить их индекс столбца в образцах матриц. В результате повышается скорость и уменьшается использование памяти за счет возможности проверки; хешер не запоминает, как выглядели входные функции, и не имеет inverse_transform метода.

Поскольку хеш-функция может вызывать конфликты между (несвязанными) функциями, используется хеш-функция со знаком, и знак значения хеш-функции определяет знак значения, хранящегося в выходной матрице для функции. Таким образом, коллизии, скорее всего, будут отменять, а не накапливать ошибку, и ожидаемое среднее значение любого выходного значения функции равно нулю. Этот механизм включен по умолчанию с помощью alternate_sign=True и особенно полезен для небольших размеров хеш-таблиц (n_features < 10000). Для больших размеров хэш-таблицы, она может быть отключена, чтобы позволить выход должны быть переданы оценки, такие как MultinomialNB или chi2 выбор признаков , которые ожидают неотрицательные входы

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.

Функция хеширования может быть использовано в классификации документов, но в отличие от CountVectorizerFeatureHasher не делает слово расщеплению или любой другой предварительной обработки , кроме 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.

Обратите внимание на использование понимания генератора, которое вводит лень в извлечение функций: токены обрабатываются только по запросу от хешера.

6.2.2.1. Детали реализации

FeatureHasher использует подписанный 32-битный вариант MurmurHash3. В результате (и из-за ограничений scipy.sparse) максимальное количество поддерживаемых функций в настоящее время $2^{31}−1$.

Первоначальная формулировка трюка хеширования Weinberger et al. используются две отдельные хэш-функции $h$ а также $\xi$ для определения индекса столбца и знака функции соответственно. Настоящая реализация работает в предположении, что знаковый бит MurmurHash3 не зависит от других его битов.

Поскольку для преобразования хеш-функции в индекс столбца используется простой модуль по модулю, рекомендуется использовать степень двойки в качестве n_features параметра; в противном случае элементы не будут отображаться равномерно по столбцам.

Рекомендации

6.2.3. Извлечение текстовых признаков

6.2.3.1. Представление «Мешок слов»

Анализ текста — это основная область применения алгоритмов машинного обучения. Однако необработанные данные, последовательность символов не могут быть переданы непосредственно в сами алгоритмы, поскольку большинство из них ожидают числовых векторов признаков с фиксированным размером, а не необработанных текстовых документов с переменной длиной.

Чтобы решить эту проблему, scikit-learn предоставляет утилиты для наиболее распространенных способов извлечения числовых функций из текстового содержимого, а именно:

  • токенизация строк и предоставление целочисленного идентификатора для каждого возможного токена, например, с использованием пробелов и знаков препинания в качестве разделителей токенов.
  • подсчет вхождений токенов в каждый документ.
  • нормализация и взвешивание с помощью маркеров убывающей важности, которые встречаются в большинстве образцов / документов.

В этой схеме функции и образцы определены следующим образом:

  • частота появления каждого отдельного токена (нормализованная или нет) рассматривается как характеристика .
  • вектор всех частот токенов для данного документа считается многомерной выборкой .

Таким образом, корпус документов может быть представлен матрицей с одной строкой на документ и одним столбцом на токен (например, слово), встречающийся в корпусе.

Мы называем векторизацией общий процесс преобразования набора текстовых документов в числовые векторы признаков. Эта конкретная стратегия (токенизация, подсчет и нормализация) называется представлением « Мешок слов» или «Мешок н-граммов». Документы описываются вхождениями слов, при этом полностью игнорируется информация об относительном положении слов в документе.

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
<4x9 sparse matrix of type '<... 'numpy.int64'>'
    with 19 stored elements in Compressed Sparse ... format>

Конфигурация по умолчанию токенизирует строку, извлекая слова не менее чем из 2 букв. Конкретную функцию, выполняющую этот шаг, можно запросить явно:

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

Каждому члену, найденному анализатором во время подбора, присваивается уникальный целочисленный индекс, соответствующий столбцу в результирующей матрице. Эту интерпретацию столбцов можно получить следующим образом:

>>> vectorizer.get_feature_names() == (
...     ['and', 'document', 'first', 'is', 'one',
...      'second', 'the', 'third', 'this'])
True

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

Следовательно, слова, которые не были замечены в обучающем корпусе, будут полностью игнорироваться в будущих вызовах метода преобразования:

>>> 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.3.1. Использование стоп-слов

Стоп-слова — это такие слова, как «и», «тот», «он», которые считаются неинформативными при представлении содержания текста и которые могут быть удалены, чтобы не истолковывать их как сигнал для предсказания. Однако иногда похожие слова полезны для предсказаний, например, для классификации стиля письма или личности.

В предоставленном нами списке стоп-слов для английского языка есть несколько известных проблем. Он не ставит своей целью быть универсальным, универсальным решением, поскольку для некоторых задач может потребоваться более индивидуальное решение. См. [NQY18] для получения более подробной информации.

Пожалуйста, будьте осторожны при выборе списка стоп-слов. Популярные списки стоп-слов могут включать слова, которые очень информативны для некоторых задач, например для компьютера .

Вы также должны убедиться, что в списке стоп-слов были применены те же предварительная обработка и токенизация, что и в векторизаторе. Слово мы в расщепляется в нас и ве по умолчанию Tokenizer CountVectorizer, так что если мы в в stop_words, но ве не является, ве будет удерживаться от мы в в преобразованном тексте. Наши векторизаторы попытаются выявить некоторые несоответствия и предупредить о них.

Рекомендации

6.2.3.4. Взвешивание термов 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 — общее количество документов в наборе документов, и $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 и как tf-idfs вычисляются в scikit-learn, 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)

Опять же, пожалуйста, обратитесь к справочной документации для получения подробной информации обо всех параметрах.

Возьмем пример со следующими подсчетами. Первый член присутствует 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
<6x3 sparse matrix of type '<... 'numpy.float64'>'
    with 9 stored elements in Compressed Sparse ... format>

>>> 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) норму, мы получаем следующие 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)
<4x9 sparse matrix of type '<... 'numpy.float64'>'
    with 19 stored elements in Compressed Sparse ... format>

Хотя нормализация tf – idf часто бывает очень полезной, могут быть случаи, когда двоичные маркеры вхождения могут предложить лучшие функции. Этого можно добиться с помощью binary параметра CountVectorizer. В частности, некоторые оценки, такие как Bernoulli Naive Bayes, явно моделируют дискретные логические случайные величины. Кроме того, очень короткие тексты могут иметь зашумленные значения tf – idf, в то время как двоичная информация о вхождении более стабильна.

Как обычно, лучший способ настроить параметры извлечения признаков — использовать поиск по сетке с перекрестной проверкой, например, путем конвейерной обработки экстрактора признаков с помощью классификатора:

6.2.3.5. Расшифровка текстовых файлов

Текст состоит из символов, а файлы состоят из байтов. Эти байты представляют символы в соответствии с некоторой кодировкой . Для работы с текстовыми файлами в 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". Для bytes.decodeполучения дополнительных сведений см. Документацию по функции Python (введите help(bytes.decode)текст в командной строке Python).

Если у вас возникли проблемы с декодированием текста, попробуйте следующее:

  • Узнайте, какова фактическая кодировка текста. Файл может иметь заголовок или README, который сообщает вам кодировку, или может быть какая-то стандартная кодировка, которую вы можете принять в зависимости от того, откуда берется текст.
  • Вы можете узнать, что это за кодировка в целом, с помощью команды UNIX file. Модуль Python chardet поставляется со скриптом, 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.

6.2.3.6. Приложения и примеры

Мешок представления слов довольно упрощен, но удивительно полезен на практике.

В частности, при контролируемой настройке его можно успешно комбинировать с быстрыми и масштабируемыми линейными моделями для обучения классификаторов документов , например:

В неконтролируемой настройке его можно использовать для группировки похожих документов вместе, применяя алгоритмы кластеризации, такие как K-means :

Наконец, можно раскрыть основные темы корпуса, ослабив жесткое ограничение присваивания кластеризации, например, используя неотрицательную матричную факторизацию (NMF или NNMF) :

6.2.3.7. Ограничения представления Мешка слов

Набор униграмм (что такое мешок слов) не может улавливать фразы и многословные выражения, фактически игнорируя любую зависимость от порядка слов. Кроме того, модель набора слов не учитывает возможные орфографические ошибки или производные слова.

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() == (
...     [' w', 'ds', 'or', 'pr', 'rd', 's ', 'wo', 'wp'])
True
>>> 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'])
<1x4 sparse matrix of type '<... 'numpy.int64'>'
   with 4 stored elements in Compressed Sparse ... format>
>>> ngram_vectorizer.get_feature_names() == (
...     [' fox ', ' jump', 'jumpy', 'umpy '])
True

>>> ngram_vectorizer = CountVectorizer(analyzer='char', ngram_range=(5, 5))
>>> ngram_vectorizer.fit_transform(['jumpy fox'])
<1x5 sparse matrix of type '<... 'numpy.int64'>'
    with 5 stored elements in Compressed Sparse ... format>
>>> ngram_vectorizer.get_feature_names() == (
...     ['jumpy', 'mpy f', 'py fo', 'umpy ', 'y fox'])
True

Вариант с учетом границ слов char_wb особенно интересен для языков, которые используют пробелы для разделения слов, поскольку в этом случае он генерирует значительно меньше шумных функций, чем необработанный char вариант. Для таких языков это может повысить как точность прогнозов, так и скорость сходимости классификаторов, обученных с использованием таких функций, при сохранении устойчивости в отношении орфографических ошибок и производных слов.

В то время как некоторая информация о локальном местоположении может быть сохранена путем извлечения n-граммов вместо отдельных слов, набор слов и набор n-граммов разрушают большую часть внутренней структуры документа и, следовательно, большую часть смысла, который несет эта внутренняя структура.

Таким образом, для решения более широкой задачи понимания естественного языка необходимо учитывать локальную структуру предложений и абзацев. Таким образом, многие такие модели будут представлены как задачи «Структурированный вывод», которые в настоящее время выходят за рамки scikit-learn.

6.2.3.8. Векторизация большого текстового корпуса с помощью хеш-трюка

Вышеупомянутая схема векторизации проста, но тот факт, что она содержит отображение в памяти строковых токенов на целочисленные индексы объектов ( vocabulary_ атрибут), вызывает несколько проблем при работе с большими наборами данных :

  • чем больше корпус, тем больше будет увеличиваться словарный запас и, следовательно, использование памяти,
  • подгонка требует выделения промежуточных структур данных, размер которых пропорционален размеру исходного набора данных.
  • построение сопоставления слов требует полного прохождения по набору данных, поэтому невозможно подобрать текстовые классификаторы строго онлайн.
  • векторизаторы травления и распаковки с большим размером vocabulary_ могут быть очень медленными (обычно намного медленнее, чем травление / распаковка плоских структур данных, таких как массив NumPy того же размера),
  • нелегко разделить работу по векторизации на параллельные подзадачи, поскольку vocabulary_ атрибут должен быть общим состоянием с мелкозернистым барьером синхронизации: отображение строки токена в индекс функции зависит от порядка первого вхождения каждого токена следовательно, должен быть разделен, что может нанести ущерб производительности параллельных рабочих до такой степени, что они станут медленнее, чем последовательный вариант.

Эти ограничения можно преодолеть, объединив «трюк хеширования» ( хеширование функций ), реализованный FeatureHasher классом, и функции предварительной обработки текста и токенизации в CountVectorizer.

Эта комбинация реализуется в HashingVectorizer классе трансформатора, который в основном совместим с API CountVectorizerHashingVectorizer не имеет состояния. Это означает, что вам не нужно вызывать метод fit:

>>> from sklearn.feature_extraction.text import HashingVectorizer
>>> hv = HashingVectorizer(n_features=10)
>>> hv.transform(corpus)
<4x10 sparse matrix of type '<... 'numpy.float64'>'
    with 16 stored elements in Compressed Sparse ... format>

Вы можете видеть, что в векторных выходных данных было извлечено 16 ненулевых маркеров функций: это меньше, чем 19 ненулевых маркеров, извлеченных ранее в CountVectorizer в том же игрушечном корпусе. Расхождение возникает из-за конфликтов хеш-функций из-за низкого значения параметра n_features.

В реальных условиях для n_features параметра можно оставить значение по умолчанию $2^{20}$ (примерно один миллион возможных функций). Если размер памяти или последующих моделей является проблемой, выберите более низкое значение, например $2^{18}$, это может помочь, не создавая слишком много дополнительных конфликтов в типичных задачах классификации текста

Заметим , что размерность не влияет на время обучения CPU алгоритмов , которые работают над матрицами КСО ( LinearSVC(dual=True)PerceptronSGDClassifierPassiveAggressive) , но это делает для алгоритмов, работа с матрицами CSC ( LinearSVC(dual=False)Lasso()и т.д.).

Попробуем еще раз с настройкой по умолчанию:

>>> hv = HashingVectorizer()
>>> hv.transform(corpus)
<4x1048576 sparse matrix of type '<... 'numpy.float64'>'
    with 19 stored elements in Compressed Sparse ... format>

Мы больше не получаем коллизий, но это происходит за счет гораздо большей размерности выходного пространства. Конечно, другие термины, кроме 19, используемых здесь, могут противоречить друг другу.

HashingVectorizer также поставляется со следующими ограничениями:

  • невозможно ни инвертировать модель (без inverse_transform метода), ни получить доступ к исходному строковому представлению функций из-за одностороннего характера хэш-функции, выполняющей сопоставление.
  • он не обеспечивает взвешивание IDF, так как это привело бы к сохранению состояния в модели. При TfidfTransformer необходимости к нему можно добавить в конвейер.

6.2.3.9. Масштабирование вне ядра с помощью HashingVectorizer

Интересным усовершенствованием использования a 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_preprocessorbuild_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. Например, позвольте использовать создание изображения 4×4 пикселя с 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 только он поддерживает несколько изображений в качестве входных данных. Он реализован как оценщик, поэтому его можно использовать в конвейерах. Видеть:

>>> 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 постройте матрицу связности для изображений с учетом формы этих изображений.

Эти матрицы могут использоваться для наложения связности в оценщиках, которые используют информацию о связности, например кластеризацию Уорда ( иерархическая кластеризация ), а также для построения предварительно вычисленных ядер или матриц сходства.