В рамках исследовательского проекта я хочу выбрать комбинацию Лучший методов предварительной обработки и текстовых функций, которые оптимизируют результаты задачи классификации текста. Для этого я использую Python 3.6.
Существует ряд методов для объединения функций и алгоритмов, но я хочу в полной мере использовать преимущества конвейеров sklearn и протестировать все различные (допустимые) возможности, используя поиск по сетке для поиска окончательной комбинации функций.
Моим первым шагом было построить конвейер, который выглядит следующим образом:
# Run a vectorizer with a predefined tweet tokenizer and a Naive Bayes
pipeline = Pipeline([
('vectorizer', CountVectorizer(tokenizer = tweet_tokenizer)),
('nb', MultinomialNB())
])
parameters = {
'vectorizer__preprocessor': (None, preprocessor)
}
gs = GridSearchCV(pipeline, parameters, cv=5, n_jobs=-1, verbose=1)
В этом простом примере векторизатор токенизирует данные с помощью tweet_tokenizer, а затем проверяет, какой вариант предварительной обработки («Нет» или предопределенная функция) дает лучший результат.
Это кажется неплохим началом, но сейчас я изо всех сил пытаюсь найти способ проверить все различные возможности в функции препроцессора, определенной ниже:
def preprocessor(tweet):
# Data cleaning
tweet = URL_remover(tweet) # Removing URLs
tweet = mentions_remover(tweet) # Removing mentions
tweet = email_remover(tweet) # Removing emails
tweet = irrelev_chars_remover(tweet) # Removing invalid chars
tweet = emojies_converter(tweet) # Translating emojies
tweet = to_lowercase(tweet) # Converting words to lowercase
# Others
tweet = hashtag_decomposer(tweet) # Hashtag decomposition
# Punctuation may only be removed after hashtag decomposition
# because it considers "#" as punctuation
tweet = punct_remover(tweet) # Punctuation
return tweet
«Простым» решением для объединения всех различных методов обработки было бы создание отдельной функции для каждой возможности (например, funcA: proc1, funcB: proc1 + proc2, funcC: proc1 + proc3 и т. д.) И установка параметра сетки следующим образом :
parameters = {
'vectorizer__preprocessor': (None, funcA, funcB, funcC, ...)
}
Хотя это, скорее всего, сработает, это не жизнеспособное или разумное решение для этой задачи, тем более что существуют различные комбинации 2^n_features
и, следовательно, функции.
Конечная цель - объединить в конвейере как методы предварительной обработки, так и функции, чтобы оптимизировать результаты классификации с помощью gridsearch:
pipeline = Pipeline([
('vectorizer', CountVectorizer(tokenizer = tweet_tokenizer)),
('feat_extractor' , feat_extractor)
('nb', MultinomialNB())
])
parameters = {
'vectorizer__preprocessor': (None, funcA, funcB, funcC, ...)
'feat_extractor': (None, func_A, func_B, func_C, ...)
}
Есть ли более простой способ получить это?
@VivekKumar feat_extractor должен работать только с исходным необработанным текстом. Я знаю, что это требует вывода CountVectorizer, но это была неудачная попытка показать, что я пытаюсь сделать. CountVectorizer позволяет использовать только определенный набор функций (например, n-граммы), и я хочу выполнить извлечение дополнительных функций (например, анализ тональности).
Если вам нужны обе эти вещи (vectorizer
и feat_extractor
) для одних и тех же данных, FeatureUnion
может помочь. Итак, теперь о func_A
, func_B
и т. д. Они одинаковы для vectorizer__preprocessor
и feat_extractor
?
Нет, у них должны быть разные функции. Для vectorizer__preprocessor funcA будет сочетать предварительную обработку 1 и 2 (например, строчные буквы + конвертер смайлов), а func_A feat_extractor будет сочетать функции 1 и 2 или любую другую возможную комбинацию (например, n-граммы + анализ настроений + длина твита). Функции предварительной обработки будут содержать только комбинации функций очистки данных, в то время как функции извлечения функций будут содержать только комбинации текстовых функций.
Для простоты я хочу объединить набор функций (которые либо предварительно обрабатывают, либо извлекают функции из данных). Для каждой возможной комбинации функций у меня был бы другой конвейер, объединяющий функции (функции) вместе, и классификатор в конце, чтобы классифицировать производительность комбинации и, в конце концов, получить комбинацию, которая дает лучший результат. Это жизнеспособно / понятно?
да. Я опубликую ответ, в котором вкратце расскажу об этом.
Я ценю. Я читал об исчерпывающем выборе функций, но он работает с каждым отдельным столбцом, а не с функцией в целом. На объединение 10 тыс. Столбцов уйдет, вероятно, неделя вычислительного времени.
Вы говорите, что "The feat_extractor should only work on the original raw text."
. Как вы планируете проводить анализ настроений на основе необработанных данных? Потребовалось бы, чтобы вы тренировались на настроениях данных (которые отличаются от фактических классов)?
На самом деле я еще не думал об этом. Это возможно, но я, скорее всего, воспользуюсь чем-то вроде TextBlob и вычислю тональность, например "tweet.sentiment.polarity"
Это очень приблизительное решение, основанное на вашем описании и конкретное для ответа в зависимости от типа используемых данных. Прежде чем создавать конвейер, давайте разберемся, как CountVectorizer
работает с переданными в него raw_documents
. По сути, это линия, который преобразует строковые документы в токены,
return lambda doc: self._word_ngrams(tokenize(preprocess(self.decode(doc))), stop_words)
которые затем просто подсчитываются и преобразуются в матрицу подсчета.
Итак, вот что происходит:
decode
: Просто решите, как читать данные из файла (если он указан). Бесполезно для нас, где у нас уже есть данные в списке.preprocess
: выполняет следующие действия, если 'strip_accents'
и 'lowercase'
являются True
в CountVectorizer
. Больше ничего
strip_accents(x.lower())
Опять же, бесполезно, потому что мы переносим функции нижнего регистра в наш собственный препроцессор и не нужно убирать акценты, потому что у нас уже есть данные в списке строк.
tokenize
: удалит все знаки препинания и сохранит только буквенно-цифровые слова длиной 2 или более, а также вернет список токенов для одного документа (элемент списка)
lambda doc: token_pattern.findall(doc)
Об этом следует помнить. Если вы хотите самостоятельно обрабатывать знаки препинания и другие символы (решив оставить одни и удалить другие), то лучше также изменить значение token_pattern=’(?u)\b\w\w+\b’
по умолчанию для CountVectorizer
.
_word_ngrams
: этот метод сначала удалит стоп-слова (предоставленные как параметр выше) из списка токенов из предыдущего шага, а затем вычислит n_grams, как определено параметром ngram_range
в CountVectorizer
. Это также следует иметь в виду, если вы хотите обращаться с "n_grams"
по-своему.Примечание: Если для анализатора задано значение 'char'
, то шаг tokenizer
выполняться не будет и n_grams будут строиться из символов.
Итак, теперь перейдем к нашему конвейеру. Я думаю, вот такая структура может работать здесь:
X --> combined_pipeline, Pipeline
|
| Raw data is passed to Preprocessor
|
/
Preprocessor
|
| Cleaned data (still raw texts) is passed to FeatureUnion
|
/
FeatureUnion
|
| Data is duplicated and passed to both parts
_______________|__________________
| |
| |
/ /
CountVectorizer FeatureExtractor
| |
| Converts raw to | Extracts numerical features
| count-matrix | from raw data
/________________________________/
|
| FeatureUnion combines both the matrices
|
/
Classifier
Теперь перейдем к коду. Вот как выглядит конвейер:
# Imports
from sklearn.svm import SVC
from sklearn.pipeline import FeatureUnion, Pipeline
# Pipeline
pipe = Pipeline([('preprocessor', CustomPreprocessor()),
('features', FeatureUnion([("vectorizer", CountVectorizer()),
("extractor", CustomFeatureExtractor())
]))
('classifier', SVC())
])
Где CustomPreprocessor
и CustomFeatureExtractor
определены как:
from sklearn.base import TransformerMixin, BaseEstimator
class CustomPreprocessor(BaseEstimator, TransformerMixin):
def __init__(self, remove_urls=True, remove_mentions=True,
remove_emails=True, remove_invalid_chars=True,
convert_emojis=True, lowercase=True,
decompose_hashtags=True, remove_punctuations=True):
self.remove_urls=remove_urls
self.remove_mentions=remove_mentions
self.remove_emails=remove_emails
self.remove_invalid_chars=remove_invalid_chars
self.convert_emojis=convert_emojis
self.lowercase=lowercase
self.decompose_hashtags=decompose_hashtags
self.remove_punctuations=remove_punctuations
# You Need to have all the functions ready
# This method works on single tweets
def preprocessor(self, tweet):
# Data cleaning
if self.remove_urls:
tweet = URL_remover(tweet) # Removing URLs
if self.remove_mentions:
tweet = mentions_remover(tweet) # Removing mentions
if self.remove_emails:
tweet = email_remover(tweet) # Removing emails
if self.remove_invalid_chars:
tweet = irrelev_chars_remover(tweet) # Removing invalid chars
if self.convert_emojis:
tweet = emojies_converter(tweet) # Translating emojies
if self.lowercase:
tweet = to_lowercase(tweet) # Converting words to lowercase
if self.decompose_hashtags:
# Others
tweet = hashtag_decomposer(tweet) # Hashtag decomposition
# Punctuation may only be removed after hashtag decomposition
# because it considers "#" as punctuation
if self.remove_punctuations:
tweet = punct_remover(tweet) # Punctuation
return tweet
def fit(self, raw_docs, y=None):
# Noop - We dont learn anything about the data
return self
def transform(self, raw_docs):
return [self.preprocessor(tweet) for tweet in raw_docs]
from textblob import TextBlob
import numpy as np
# Same thing for feature extraction
class CustomFeatureExtractor(BaseEstimator, TransformerMixin):
def __init__(self, sentiment_analysis=True, tweet_length=True):
self.sentiment_analysis=sentiment_analysis
self.tweet_length=tweet_length
# This method works on single tweets
def extractor(self, tweet):
features = []
if self.sentiment_analysis:
blob = TextBlob(tweet)
features.append(blob.sentiment.polarity)
if self.tweet_length:
features.append(len(tweet))
# Do for other features you want.
return np.array(features)
def fit(self, raw_docs, y):
# Noop - Again I am assuming that We dont learn anything about the data
# Definitely not for tweet length, and also not for sentiment analysis
# Or any other thing you might have here.
return self
def transform(self, raw_docs):
# I am returning a numpy array so that the FeatureUnion can handle that correctly
return np.vstack(tuple([self.extractor(tweet) for tweet in raw_docs]))
Наконец, сетку параметров теперь можно легко сделать, например:
param_grid = ['preprocessor__remove_urls':[True, False],
'preprocessor__remove_mentions':[True, False],
...
...
# No need to search for lowercase or preprocessor in CountVectorizer
'features__vectorizer__max_df':[0.1, 0.2, 0.3],
...
...
'features__extractor__sentiment_analysis':[True, False],
'features__extractor__tweet_length':[True, False],
...
...
'classifier__C':[0.01, 0.1, 1.0]
]
Приведенный выше код предназначен для исключения "to create a different function for each possibility (e.g. funcA: proc1, funcB: proc1 + proc2, funcC: proc1 + proc3, etc.)
". Просто сделайте True, False и GridSearchCV справится с этим.
Обновлять:
Если вы не хотите иметь CountVectorizer
, вы можете удалить его из конвейера и сетки параметров, и новый конвейер будет:
pipe = Pipeline([('preprocessor', CustomPreprocessor()),
("extractor", CustomFeatureExtractor()),
('classifier', SVC())
])
Затем убедитесь, что в CustomFeatureExtractor
реализованы все необходимые функции. Если это станет слишком сложным, вы всегда можете сделать более простые экстракторы и объединить их вместе в FeatureUnion вместо CountVectorizer
.
Мне нравится представленная архитектура, но я не понимаю, как можно протестировать различные наборы функций. Конвейер для каждой возможной итерации? Или настроить параметры gridsearch? например 'featureExtractor__ngram': (Нет, MyNgram)
Кроме того, вместо использования предварительно созданного CountVectorizer я решил сам реализовать его функции для большей гибкости. Единственные функции CountVectorizer, которые мне нужны, - это в значительной степени ngram, токенизатор и строчные буквы, которые я уже реализовал сам. Возможно, благодаря этому я смогу использовать FeatureExtractor только в конвейере?
Величественный ответ, мне он очень нравится. Вы бы порекомендовали оставить CountVectorizer вместо того, чтобы писать свои собственные функции, кроме проблем со сложностью?
@Khabz Как я описал выше, единственное, что вы не делаете, а CountVectorizer
не обрабатывает, - это токенизация и генерация функций ngram. Вы можете попробовать свои собственные методы для этого.
@Khabz Также помните, что когда вы делаете это по-своему, вам нужно будет выполнить обучающую часть в fit()
, потому что вам нужно сохранить найденные функции, а затем в transform()
использовать эти функции для преобразования новых данных.
Эквивалентно ли преобразование fit + извлечению функций и одной горячей кодировки (или чего-то еще) в матрицу?
@Khabz fit()
предназначен только для изучения данных (например, слов или цифр, которые вы хотели бы сделать в качестве функций в тексте, или нахождения максимальных и минимальных значений данных в числах и т. д.). Вы сохраняете такие значения во время fit
. Теперь во время transform()
вы используете эти изученные значения для значимого изменения данных (например, находите те же слова или числа в новых данных и создавайте из них функции).
Не могли бы вы привести пример реализации экстрактора ngram?
@Khabz Это зависит от того, чего вы хотите. Вы хотите извлечь из текста все нграммы требуемого размера или только те, которые были просмотрены во время обучения? Что ты будешь делать после этого?
Моя цель состояла бы в том, чтобы собрать только нграммы обучающих данных и превратить их в функции (1 - содержит нграмму, 0 - не содержит).
Ну, CountVectorizer просто делает это. Проверьте параметр binary
на странице документации] (scikit-learn.org/stable/modules/generated/…). Вы можете рассмотреть мою более раннюю установку, содержащую CountVectorizer
, для этого.
Что должен делать
('feat_extractor' , feat_extractor)
послеCounteVectorizer
? Конвейер будет передавать данные черезCountVectorizer
, затем новые данные (матрица подсчета, а не слова) будут переданы вfeat_extractor
. Это то, что вы хотите? Или вы хотите, чтобыfeat_extractor
был включен вpreprocesser
, как вы описали выше?