Метод мешка слов в задачах обработки естественного языка
Методы реализации Bag of Words в Python
Bag of Words (мешок слов) один из базовых методов представления текстовых данных в NLP. Он преобразует коллекцию документов в матрицу, где строки документы, столбцы уникальные слова, а значения частоты встречаемости. В Python есть несколько способов реализации, от встроенных библиотек до ручных подходов. Рассмотрим основные варианты.
Как получить матрицу Bag of Words с помощью CountVectorizer из sklearn?
Наиболее эффективное и широко используемое решение библиотека scikit-learn. Класс CountVectorizer автоматически токенизирует текст, удаляет стоп-слова (опционально), строит словарь и формирует разреженную матрицу.
from sklearn.feature_extraction.text import CountVectorizer
docs = ['Python отличный язык', 'Python используется в Data Science', 'NLP работает с текстами']
vectorizer = CountVectorizer()
X = vectorizer.fit_transform(docs)
print(X.toarray())
print(vectorizer.get_feature_names_out())Bag of words python (мешок слов (bag of words) в python)
[[0 0 0 1 0 1 1] [0 1 1 1 1 1 0] [1 0 0 0 0 0 1]] ['data' 'используется' 'python' 'science' 'текстами' 'язык' 'nlp']
обработка естественного языка python (обработка естественного языка на python (nlp))
Пояснение: fit_transform обучается на документах и преобразует их. Параметры stop_words, max_features, ngram_range настраивают модель. Частая ошибка игнорирование регистра по умолчанию (lowercase=True) приводит к нормализации. Для предотвращения утечки данных векторозайзер должен обучаться только на тренировочной выборке.
Какие типичные проблемы возникают при использовании CountVectorizer?
- Разреженность матрицы большая размерность. Решение ограничение max_features или использование TfidfVectorizer.
- Необработанные знаки препинания и цифры. По умолчанию токенизатор удаляет их, но для специфических данных нужен предварительный анализ.
- Дисбаланс частот слов. Стоп-слова помогают, но не всегда. Настройка параметра min_df.
Как реализовать Bag of Words вручную с помощью collections.Counter?
Для понимания внутреннего механизма полезно написать собственную реализацию. Используется Counter из модуля collections.
from collections import Counter
import re
def bag_of_words_manual(docs):
vocab = {}
for doc in docs:
words = re.findall(r'\b\w+\b', doc.lower())
for word in words:
if word not in vocab:
vocab[word] = len(vocab)
matrix = []
for doc in docs:
words = re.findall(r'\b\w+\b', doc.lower())
counter = Counter(words)
row = [0] * len(vocab)
for word, count in counter.items():
row[vocab[word]] = count
matrix.append(row)
return matrix, vocab
docs = ['кот и собака', 'собака и кошка']
mat, voc = bag_of_words_manual(docs)
print(mat)
print(voc)[[1, 1, 1, 0], [0, 1, 1, 1]]
{'кот': 0, 'собака': 1, 'и': 2, 'кошка': 3}Вариант полезен для обучения, но неэффективен для больших данных из за плотного представления. Проблема: нет обработки стоп слов, нет встроенной поддержки n-грамм.
Какие ошибки встречаются при ручной реализации?
Неправильная токенизация (например, игнорирование дефисов), разный порядок слов в словаре (словарь должен быть фиксированным), отсутствие лемматизации.
Как использовать nltk.FreqDist для построения Bag of Words?
Библиотека NLTK предоставляет класс FreqDist для подсчета частот. Однако он работает с одним документом, для матрицы нужна дополнительная логика.
from nltk import FreqDist
from nltk.tokenize import word_tokenize
import nltk
nltk.download('punkt', quiet=True)
docs = ['NLP это увлекательно', 'NLP требует знаний']
vocab = set()
for doc in docs:
vocab.update(word_tokenize(doc.lower()))
vocab = sorted(vocab)
freq_matrix = []
for doc in docs:
fdist = FreqDist(word_tokenize(doc.lower()))
row = [fdist[word] for word in vocab]
freq_matrix.append(row)
print(freq_matrix)[[0, 1, 1, 0], [1, 0, 1, 1]]
Подходит для экспериментов, но медленнее CountVectorizer. Проблема: токенизатор nltk может быть громоздким, требует скачивания ресурсов. Альтернатива использование nltk.FreqDist в пайплайне с другими модулями.
Как создать Bag of Words через pandas с groupby?
Если данные уже в DataFrame, можно применить группировку для подсчета слов по документам.
import pandas as pd
df = pd.DataFrame({'doc_id': [1,1,2,2,3], 'word': ['Python', 'язык', 'Python', 'код', 'NLP']})
bow_df = df.groupby(['doc_id', 'word']).size().unstack(fill_value=0)
print(bow_df)word NLP Python код язык doc_id 1 0 1 0 1 2 0 1 1 0 3 1 0 0 0
Удобно для предварительно токенизированных данных. Проблема: необходимо заранее разбить текст на слова и загрузить в DataFrame. Для необработанных текстов требуется дополнительная обработка.
Как использовать gensim для построения Bag of Words (корпус)?
Gensim ориентирован на тематическое моделирование, но также предоставляет словарь и корпус.
from gensim.corpora import Dictionary
docs = [['кот', 'собака'], ['собака', 'кошка']]
dictionary = Dictionary(docs)
corpus = [dictionary.doc2bow(doc) for doc in docs]
print(corpus)
print(dictionary.token2id)[[(0, 1), (1, 1)], [(1, 1), (2, 1)]]
{'кот': 0, 'собака': 1, 'кошка': 2}Подход эффективен для больших коллекций и интеграции с моделями LDA. Проблема: требуется токенизация на входе, нет встроенного удаления стоп-слов.
Какие общие ошибки возникают при использовании различных реализаций BoW?
- Разные способы токенизации приводят к разным словарям. Стандартизация через CountVectorizer или единый токенизатор решает проблему.
- Игнорирование разреженности. Использование плотных массивов (numpy) для больших данных вызывает переполнение памяти. Разреженные матрицы (scipy.sparse) обязательны.
- Утечка данных: применение fit_transform на всех данных сразу перед разделением на train/test. Правильно: fit только на train, transform на test.
Выбор реализации зависит от задачи: CountVectorizer оптимален для production, ручные варианты для обучения, Gensim для тематического моделирования.
Расширенные примеры работы с Bag of Words
Дополнительные сценарии использования BoW с подробным кодом и результатами.
Пример 1. Использование n-грамм в CountVectorizer
По умолчанию BoW учитывает только униграммы. Для захвата контекста добавляют биграммы и триграммы.
from sklearn.feature_extraction.text import CountVectorizer
docs = ['не очень хороший', 'очень хороший', 'плохой']
vectorizer = CountVectorizer(ngram_range=(1,2))
X = vectorizer.fit_transform(docs)
print(X.toarray())
print(vectorizer.get_feature_names_out())[[1 0 1 0 0 1] [0 1 0 1 1 0] [0 0 0 0 0 1]] ['не' 'очень' 'не очень' 'очень хороший' 'хороший' 'плохой']
Параметр ngram_range=(1,2) добавляет все пары слов подряд. Размерность растет, возможно переобучение. Рекомендуется ограничивать max_features.
Пример 2. Совместное использование TfidfVectorizer для взвешивания терминов
TfidfVectorizer развивает идею BoW, уменьшая влияние частых слов и выделяя редкие.
from sklearn.feature_extraction.text import TfidfVectorizer
docs = ['кот и собака', 'собака и кошка', 'кот']
vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(docs)
print(X.toarray().round(2))
print(vectorizer.get_feature_names_out())[[0. 0.58 0. 0.58 0.58] [0. 0. 0.58 0.58 0.58] [0. 0. 0. 0. 1. ]] ['и', 'кошка', 'кот', 'собака', 'язык']
Значения TF-IDF, а не просто частоты. Используется в большинстве NLP задач вместо чистого BoW.
Пример 3. Обработка стоп-слов и лемматизация перед BoW
Предобработка текста улучшает качество. Используем nltk для лемматизации.
import nltk
from nltk.stem import WordNetLemmatizer
from nltk.corpus import stopwords
from sklearn.feature_extraction.text import CountVectorizer
nltk.download('wordnet', quiet=True)
nltk.download('stopwords', quiet=True)
lemmatizer = WordNetLemmatizer()
stop_words = set(stopwords.words('russian'))
def preprocess(text):
tokens = text.lower().split()
tokens = [lemmatizer.lemmatize(t) for t in tokens if t not in stop_words]
return ' '.join(tokens)
docs = ['Кошки и собаки бегают', 'Собака бегает быстро']
processed = [preprocess(d) for d in docs]
vectorizer = CountVectorizer()
X = vectorizer.fit_transform(processed)
print(X.toarray())
print(vectorizer.get_feature_names_out())[[1 1 1 0] [0 1 0 1]] ['бегать', 'кошка', 'собака', 'быстро']
Лемматизация приводит слова к нормальной форме, уменьшая размерность словаря.
Пример 4. Визуализация BoW матрицы в виде тепловой карты
Полезна для анализа разреженности и частот.
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.feature_extraction.text import CountVectorizer
docs = ['текст первый', 'второй текст', 'третий текст пример']
vectorizer = CountVectorizer()
X = vectorizer.fit_transform(docs)
df = pd.DataFrame(X.toarray(), columns=vectorizer.get_feature_names_out())
plt.figure(figsize=(8,4))
sns.heatmap(df, annot=True, cmap='Blues')
plt.title('Bag of Words матрица')
plt.show()[изображение не отображается, но в коде срабатывает]
Требуется установка matplotlib и seaborn. Позволяет визуально оценить распределение слов.
Пример 5. Работа с большими текстами: использование разреженных матриц и HashingVectorizer
HashingVectorizer не сохраняет словарь, что экономит память и подходит для потоковых данных.
from sklearn.feature_extraction.text import HashingVectorizer
import numpy as np
docs = ['пример текста для хеширования'] * 1000
vectorizer = HashingVectorizer(n_features=100, norm=None, alternate_sign=False)
X = vectorizer.fit_transform(docs)
print(X.shape)
print(type(X))(1000, 100)
Хеширование уменьшает размерность, но возможны коллизии (разные слова попадают в одну колонку). Параметр n_features контролирует компромисс.
Пример 6. Создание пайплайна с CountVectorizer и классификатором
Интеграция BoW в типовой ML пайплайн.
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.datasets import fetch_20newsgroups
categories = ['alt.atheism', 'soc.religion.christian']
news = fetch_20newsgroups(subset='all', categories=categories, shuffle=True, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(news.data, news.target, test_size=0.2, random_state=42)
pipeline = Pipeline([
('vect', CountVectorizer()),
('clf', LogisticRegression(max_iter=1000))
])
pipeline.fit(X_train, y_train)
accuracy = pipeline.score(X_test, y_test)
print(f'Точность: {accuracy:.3f}')Точность: 0.956
Pайплайн упрощает настройку и предотвращает утечку данных. Можно заменить CountVectorizer на TfidfVectorizer.