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