ЛР4
Очистка: превращаем слова в одинаковую форму

Разобраться с нуля

Очистка: превращаем слова в одинаковую форму

The mice were running in 2025 → mouse run. Так компьютер не считает каждое окончание новым словом.

Токен
Отдельная единица текста, здесь слово.
Токенизация
Разделение текста на слова.
Лемматизация
Замена слова начальной формой: mice → mouse.
Стоп-слова
Служебные слова вроде the, and, is.
Часть речи / POS
Существительное, глагол и т.д., помогает найти лемму.
Регулярное выражение
Правило поиска символов, например [a-z]+ ищет английские буквы.
На пальцах. В каталоге магазина «красная чашка» и «красные чашки» должны находиться рядом. Очистка приводит варианты к общей форме.
  1. lower(): делаем буквы маленькими.
  2. re.findall('[a-z]+', ...): оставляем последовательности букв, пунктуация и числа исчезают.
  3. pos_tag: определяем часть речи.
  4. lemmatize: ищем начальную форму с учётом части речи.
  5. Фильтруем стоп-слова и соединяем через один пробел.

Заголовок и аннотацию соединяем через пробел. Нельзя склеить слова на границе без него.

Учебные примеры с заранее проверенными шагами; настоящий NLTK выполняется в ноутбуке.

Ловушка. У слова studies без части речи может получиться неверная лемма. Удаление not и чисел может терять смысл; здесь это требование задания.

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

Код исходной ячейки 10: import sys, subprocess
import sys, subprocess
subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', 'nltk', 'gensim>=4.4'])
import nltk
from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer
import zipfile, io
nltk_dir = DATA / 'nltk_data'
nltk.data.path.insert(0, str(nltk_dir.resolve()))
for group, resource in [('corpora','wordnet'), ('corpora','stopwords'), ('taggers','averaged_perceptron_tagger_eng')]:
    target = nltk_dir / group / resource
    if not target.exists():
        url = f'https://raw.githubusercontent.com/nltk/nltk_data/gh-pages/packages/{group}/{resource}.zip'
        response = requests.get(url, timeout=120)
        response.raise_for_status()
        with zipfile.ZipFile(io.BytesIO(response.content)) as archive:
            archive.extractall(nltk_dir / group)
stop_words = set(stopwords.words('english'))
lemmatizer = WordNetLemmatizer()
from functools import lru_cache
@lru_cache(maxsize=200000)
def lemma(word, pos):
    return lemmatizer.lemmatize(word, pos)

def preprocess(text):
    tokens = re.findall(r'[a-z]+', text.lower())
    tagged = nltk.pos_tag(tokens)
    pos_map = {'J': 'a', 'V': 'v', 'N': 'n', 'R': 'r'}
    lemmas = [lemma(word, pos_map.get(tag[0], 'n')) for word, tag in tagged]
    return ' '.join(word for word in lemmas if word not in stop_words)

start = perf_counter()
import hashlib
cache_key = hashlib.sha256((DATA / 'openalex.json').read_bytes()).hexdigest()[:16]
cache = DATA / f'preprocessed_{cache_key}.json'
if cache.exists():
    df['title_abstract_preprocessed'] = json.loads(cache.read_text(encoding='utf-8'))
else:
    df['title_abstract_preprocessed'] = (df['title'] + ' ' + df['abstract']).map(preprocess)
    cache.write_text(json.dumps(df['title_abstract_preprocessed'].tolist()), encoding='utf-8')
print('Предобработка, с:', round(perf_counter() - start, 2))
print('Пример:', preprocess('The mice were running in 2025, and the studies were better!'))
print('Пустых текстов после очистки:', df['title_abstract_preprocessed'].str.len().eq(0).sum())
df.head()
  1. 1: import sys, subprocess
    Подключает готовые инструменты; import сам по себе модель не обучает.
  2. 2: subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', 'nltk', 'gensim>=4.4'])
    Вычисляет правую часть и сохраняет результат в subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', 'nltk', 'gensim>.
  3. 3: import nltk
    Подключает готовые инструменты; import сам по себе модель не обучает.
  4. 4: from nltk.corpus import stopwords
    Подключает готовые инструменты; import сам по себе модель не обучает.
  5. 5: from nltk.stem import WordNetLemmatizer
    Подключает готовые инструменты; import сам по себе модель не обучает.
  6. 6: import zipfile, io
    Подключает готовые инструменты; import сам по себе модель не обучает.
  7. 7: nltk_dir = DATA / 'nltk_data'
    Вычисляет правую часть и сохраняет результат в nltk_dir.
  8. 8: nltk.data.path.insert(0, str(nltk_dir.resolve()))
    Продолжает выражение предыдущей строки: здесь заданы данные или аргументы вызова.
  9. 9: for group, resource in [('corpora','wordnet'), ('corpora','stopwords'), ('taggers','averaged_perceptron_tagger_eng')]:
    Повторяет вложенный блок для каждого элемента указанной последовательности.
  10. 10: target = nltk_dir / group / resource
    Вычисляет правую часть и сохраняет результат в target.
  11. 11: if not target.exists():
    Выполняет вложенный блок только при истинном условии.
  12. 12: url = f'https://raw.githubusercontent.com/nltk/nltk_data/gh-pages/packages/{group}/{resource}.zip'
    Вычисляет правую часть и сохраняет результат в url.
  13. 13: response = requests.get(url, timeout=120)
    Посылает HTTP-запрос к API или скачивает официальный словарь.
  14. 14: response.raise_for_status()
    Проверяет код ответа сервера и не даёт продолжать с неудачной загрузкой.
  15. 15: with zipfile.ZipFile(io.BytesIO(response.content)) as archive:
    Управляет ресурсом: открытым архивом или больше не нужной матрицей.
  16. 16: archive.extractall(nltk_dir / group)
    Продолжает выражение предыдущей строки: здесь заданы данные или аргументы вызова.
  17. 17: stop_words = set(stopwords.words('english'))
    Вычисляет правую часть и сохраняет результат в stop_words.
  18. 18: lemmatizer = WordNetLemmatizer()
    Вычисляет правую часть и сохраняет результат в lemmatizer.
  19. 19: from functools import lru_cache
    Подключает готовые инструменты; import сам по себе модель не обучает.
  20. 20: @lru_cache(maxsize=200000)
    Вычисляет правую часть и сохраняет результат в @lru_cache(maxsize.
  21. 21: def lemma(word, pos):
    Объявляет функцию: код внутри выполняется при её вызове.
  22. 22: return lemmatizer.lemmatize(word, pos)
    Возвращает результат функции вызывающему коду.
  23. 24: def preprocess(text):
    Объявляет функцию: код внутри выполняется при её вызове.
  24. 25: tokens = re.findall(r'[a-z]+', text.lower())
    Вычисляет правую часть и сохраняет результат в tokens.
  25. 26: tagged = nltk.pos_tag(tokens)
    Вычисляет правую часть и сохраняет результат в tagged.
  26. 27: pos_map = {'J': 'a', 'V': 'v', 'N': 'n', 'R': 'r'}
    Вычисляет правую часть и сохраняет результат в pos_map.
  27. 28: lemmas = [lemma(word, pos_map.get(tag[0], 'n')) for word, tag in tagged]
    Повторяет вложенный блок для каждого элемента указанной последовательности.
  28. 29: return ' '.join(word for word in lemmas if word not in stop_words)
    Возвращает результат функции вызывающему коду.
  29. 31: start = perf_counter()
    Замеряет время, чтобы отдельно оценить затраты на этот этап.
  30. 32: import hashlib
    Подключает готовые инструменты; import сам по себе модель не обучает.
  31. 33: cache_key = hashlib.sha256((DATA / 'openalex.json').read_bytes()).hexdigest()[:16]
    Вычисляет правую часть и сохраняет результат в cache_key.
  32. 34: cache = DATA / f'preprocessed_{cache_key}.json'
    Вычисляет правую часть и сохраняет результат в cache.
  33. 35: if cache.exists():
    Выполняет вложенный блок только при истинном условии.
  34. 36: df['title_abstract_preprocessed'] = json.loads(cache.read_text(encoding='utf-8'))
    Вычисляет правую часть и сохраняет результат в df['title_abstract_preprocessed'].
  35. 37: else:
    Ветвь для случая, когда предыдущее условие не выполнено.
  36. 38: df['title_abstract_preprocessed'] = (df['title'] + ' ' + df['abstract']).map(preprocess)
    Вычисляет правую часть и сохраняет результат в df['title_abstract_preprocessed'].
  37. 39: cache.write_text(json.dumps(df['title_abstract_preprocessed'].tolist()), encoding='utf-8')
    Вычисляет правую часть и сохраняет результат в cache.write_text(json.dumps(df['title_abstract_preprocessed'].tolist()), encoding.
  38. 40: print('Предобработка, с:', round(perf_counter() - start, 2))
    Замеряет время, чтобы отдельно оценить затраты на этот этап.
  39. 41: print('Пример:', preprocess('The mice were running in 2025, and the studies were better!'))
    Печатает число или пояснение, чтобы проверить результат выполнения.
  40. 42: print('Пустых текстов после очистки:', df['title_abstract_preprocessed'].str.len().eq(0).sum())
    Печатает число или пояснение, чтобы проверить результат выполнения.
  41. 43: df.head()
    Продолжает выражение предыдущей строки: здесь заданы данные или аргументы вызова.

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

Лемматизация и стемминг одинаковы?

Нет. Лемма это словарная форма; стемминг механически обрезает окончания.

Почему до split это можно?

Обработка каждой статьи независима и использует готовые внешние словари NLTK.

Зачем join?

Получить одну строку с одиночными пробелами вместо списка слов.

Одна фразаСнижаю число разных написаний одного слова и удаляю служебный шум.