Метод мешка слов в задачах обработки естественного языка

Раздел: Data Science -> NLP

Методы реализации 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.

Мешок слов (Bag of Words) в Python - comments

En
Bag of words python (python)