ЛР4
BoW: текст как счётчик слов

BoW · 3 балла

BoW: текст как счётчик слов

Выбираем словарь и считаем, сколько раз каждое слово встречается в статье.

Корпус
Все тексты, с которыми работаем.
Словарь / vocabulary
Слово → номер столбца.
Вектор
Список чисел для одного документа.
Матрица
Таблица: строка = документ, столбец = признак.
N-грамма
n соседних слов: machine learning это биграмма.
max_features
Максимальное число столбцов, отбираемых по частоте.
CSR
Разреженный формат: хранит ненулевые значения и их координаты.
fit / transform
Выучить словарь / посчитать векторы по готовому словарю.
На пальцах. Забыл порядок покупок, но сохранил чек: хлеб 2, молоко 1. Это мешок слов. «кот ест рыбу» и «рыба ест кота» дадут одинаковые счётчики слов.

Словарь учится по «cat cat dog» и «dog bird». Новый текст проходит только transform.

Как выглядит вектор

Словарь [bird, cat, dog], текст cat cat dog: [0,2,1]. Весь корпус из двух текстов: [[0,2,1],[1,0,1]]. Слово fish на test не создаёт новый столбец.

Что делает мой класс

  1. tokenize: выделяет слова длиной от 2 символов, собирает n-граммы.
  2. fit: Counter считает суммарную частоту в train.
  3. Сортировка: сначала частота по убыванию, при равенстве алфавит.
  4. Отбор max_features, затем фиксируем номера столбцов.
  5. transform: Counter для каждой статьи и заполнение CSR.
Ловушка. Словарь хранит индексы, а не частоты из всего корпуса. При 10 известных словах и max_features=15 будет 10 столбцов. Циклы по текстам здесь разрешены и нужны для построения счётчиков.

После fit в test встретилось fish, которого нет в словаре. Что делать?

Код и построчный разбор

Код исходной ячейки 18: from collections import Counter
from collections import Counter
from scipy.sparse import csr_matrix

class BagOfWords:
    def __init__(self, ngram_range=(1, 1), max_features=None):
        self.ngram_range = ngram_range
        self.max_features = max_features
        self.vocabulary = {}

    def tokenize(self, text):
        words = re.findall(r'(?u)\b\w\w+\b', text.lower())
        return [' '.join(words[i:i+n]) for n in range(self.ngram_range[0], self.ngram_range[1]+1)
                for i in range(len(words)-n+1)]

    def fit(self, documents):
        counts = Counter(token for document in documents for token in self.tokenize(document))
        ranked = sorted(counts, key=lambda word: (-counts[word], word))
        selected = sorted(ranked[:self.max_features])
        self.vocabulary = {word: index for index, word in enumerate(selected)}
        if not selected:
            raise ValueError('Пустой словарь')
        return self

    def transform(self, documents):
        if not self.vocabulary:
            raise ValueError('Сначала вызовите fit')
        rows, columns, values = [], [], []
        for row, document in enumerate(documents):
            counts = Counter(self.tokenize(document))
            for word, count in counts.items():
                if word in self.vocabulary:
                    rows.append(row); columns.append(self.vocabulary[word]); values.append(count)
        return csr_matrix((values, (rows, columns)), shape=(len(documents), len(self.vocabulary)), dtype=np.float64)

    def fit_transform(self, documents):
        return self.fit(documents).transform(documents)
  1. 1: from collections import Counter
    Подключает готовые инструменты; import сам по себе модель не обучает.
  2. 2: from scipy.sparse import csr_matrix
    Подключает готовые инструменты; import сам по себе модель не обучает.
  3. 4: class BagOfWords:
    Объявляет класс, который хранит настройки, словарь и методы преобразования.
  4. 5: def __init__(self, ngram_range=(1, 1), max_features=None):
    Объявляет функцию: код внутри выполняется при её вызове.
  5. 6: self.ngram_range = ngram_range
    Сохраняет настройку или промежуточный результат внутри объекта.
  6. 7: self.max_features = max_features
    Сохраняет настройку или промежуточный результат внутри объекта.
  7. 8: self.vocabulary = {}
    Фиксирует соответствие признака номеру столбца.
  8. 10: def tokenize(self, text):
    Объявляет функцию: код внутри выполняется при её вызове.
  9. 11: words = re.findall(r'(?u)\b\w\w+\b', text.lower())
    Вычисляет правую часть и сохраняет результат в words.
  10. 12: return [' '.join(words[i:i+n]) for n in range(self.ngram_range[0], self.ngram_range[1]+1)
    Возвращает результат функции вызывающему коду.
  11. 13: for i in range(len(words)-n+1)]
    Повторяет вложенный блок для каждого элемента указанной последовательности.
  12. 15: def fit(self, documents):
    Объявляет функцию: код внутри выполняется при её вызове.
  13. 16: counts = Counter(token for document in documents for token in self.tokenize(document))
    Повторяет вложенный блок для каждого элемента указанной последовательности.
  14. 17: ranked = sorted(counts, key=lambda word: (-counts[word], word))
    Сортирует значения, чтобы порядок слов и столбцов был воспроизводимым.
  15. 18: selected = sorted(ranked[:self.max_features])
    Сохраняет настройку или промежуточный результат внутри объекта.
  16. 19: self.vocabulary = {word: index for index, word in enumerate(selected)}
    Повторяет вложенный блок для каждого элемента указанной последовательности.
  17. 20: if not selected:
    Выполняет вложенный блок только при истинном условии.
  18. 21: raise ValueError('Пустой словарь')
    Сообщает об ошибке и останавливает этот путь выполнения.
  19. 22: return self
    Возвращает результат функции вызывающему коду.
  20. 24: def transform(self, documents):
    Объявляет функцию: код внутри выполняется при её вызове.
  21. 25: if not self.vocabulary:
    Выполняет вложенный блок только при истинном условии.
  22. 26: raise ValueError('Сначала вызовите fit')
    Сообщает об ошибке и останавливает этот путь выполнения.
  23. 27: rows, columns, values = [], [], []
    Вычисляет правую часть и сохраняет результат в rows, columns, values.
  24. 28: for row, document in enumerate(documents):
    Повторяет вложенный блок для каждого элемента указанной последовательности.
  25. 29: counts = Counter(self.tokenize(document))
    Подсчитывает частоты токенов или пар.
  26. 30: for word, count in counts.items():
    Повторяет вложенный блок для каждого элемента указанной последовательности.
  27. 31: if word in self.vocabulary:
    Выполняет вложенный блок только при истинном условии.
  28. 32: rows.append(row); columns.append(self.vocabulary[word]); values.append(count)
    Сохраняет настройку или промежуточный результат внутри объекта.
  29. 33: return csr_matrix((values, (rows, columns)), shape=(len(documents), len(self.vocabulary)), dtype=np.float64)
    Возвращает результат функции вызывающему коду.
  30. 35: def fit_transform(self, documents):
    Объявляет функцию: код внутри выполняется при её вызове.
  31. 36: return self.fit(documents).transform(documents)
    Возвращает результат функции вызывающему коду.
Код исходной ячейки 19: bow = BagOfWords(ngram_range=(1, 2), max_features=15)
bow = BagOfWords(ngram_range=(1, 2), max_features=15)
test_sentences = [
    "BoW transforms text into a frequency vector based on words occurrence",
    "in BoW each word's count creates a unique feature",
    "BoW ignores word order focusing on word occurrence frequency",
]
vectors = bow.fit_transform(test_sentences)
pd.DataFrame(vectors.toarray(), columns=list(bow.vocabulary))
  1. 1: bow = BagOfWords(ngram_range=(1, 2), max_features=15)
    Вычисляет правую часть и сохраняет результат в bow.
  2. 2: test_sentences = [
    Вычисляет правую часть и сохраняет результат в test_sentences.
  3. 3: "BoW transforms text into a frequency vector based on words occurrence",
    Продолжает выражение предыдущей строки: здесь заданы данные или аргументы вызова.
  4. 4: "in BoW each word's count creates a unique feature",
    Продолжает выражение предыдущей строки: здесь заданы данные или аргументы вызова.
  5. 5: "BoW ignores word order focusing on word occurrence frequency",
    Продолжает выражение предыдущей строки: здесь заданы данные или аргументы вызова.
  6. 6: ]
    Завершает многострочное выражение или набор настроек.
  7. 7: vectors = bow.fit_transform(test_sentences)
    Выучивает словарь и статистику по обучающим текстам и сразу строит их матрицу.
  8. 8: pd.DataFrame(vectors.toarray(), columns=list(bow.vocabulary))
    Разворачивает маленький пример в плотную таблицу; большой корпус так не храним.
Код исходной ячейки 21: for ngrams, limit in [((1, 1), 5000), ((1, 1), 15000), ((1, 2), 15000)]:
for ngrams, limit in [((1, 1), 5000), ((1, 1), 15000), ((1, 2), 15000)]:
    evaluate(f'own BoW {ngrams} {limit}', BagOfWords(ngrams, limit))
display(pd.DataFrame(results).round(4))
  1. 1: for ngrams, limit in [((1, 1), 5000), ((1, 1), 15000), ((1, 2), 15000)]:
    Повторяет вложенный блок для каждого элемента указанной последовательности.
  2. 2: evaluate(f'own BoW {ngrams} {limit}', BagOfWords(ngrams, limit))
    Продолжает выражение предыдущей строки: здесь заданы данные или аргументы вызова.
  3. 3: display(pd.DataFrame(results).round(4))
    Показывает таблицу в ноутбуке.
Одна из матриц ошибок собственного BoW: строка настоящая, столбец предсказанная область.
Одна из матриц ошибок собственного BoW: строка настоящая, столбец предсказанная область.

Вопросы защиты

Почему CSR?

8000 × 15000 float64 занимают около 960 МБ в плотном виде. Большинство элементов нули, их хранить незачем.

Что теряет BoW?

Порядок слов. Биграммы частично возвращают локальный порядок.

Как проверил со sklearn?

При одинаковом vocabulary сравнил каждую ячейку train- и test-матриц, число различий равно нулю.

Можно тут без цикла?

Подсчёт переменных строковых токенов естественно использует цикл. Это не MSE или градиент, которые нужно считать матрично.

Одна фразаBoW фиксирует словарь на train и переводит статью в счётчики слов.