入门
NLTK 与 spaCy 安装
NLTK 适合教学与研究;spaCy 面向生产环境且速度快。spaCy 模型返回 Doc 对象,包含分词文本、词性标签、依存关系等。用 'python -m spacy download <model>' 安装语言模型——en_core_web_sm 是小型英文模型。
# install NLTK and spaCy
pip install nltk spacy
python -m spacy download en_core_web_sm
import nltk
import spacy
# download NLTK data
nltk.download('punkt')
nltk.download('stopwords')
# load a spaCy model
nlp = spacy.load("en_core_web_sm")
doc = nlp("Apple is looking at buying a U.K. startup.")
for token in doc:
print(token.text, token.pos_, token.dep_)spaCy 流水线基础
spaCy 的流水线是模块化的:分词器总是最先运行,然后是可选组件如标注器、解析器、NER。禁用未使用的组件可大幅提速(例如提取嵌入时只需要 tok2vec)。使用 nlp.pipe() 批量处理——比循环调用 nlp() 快得多。
import spacy
# blank pipeline (no pretrained components)
nlp_blank = spacy.blank("en")
# full pipeline: tokenizer -> tagger -> parser -> ner -> ...
nlp = spacy.load("en_core_web_sm")
print(nlp.pipe_names) # ['tok2vec', 'tagger', 'parser', 'ner', ...]
# disable components you don't need (faster)
nlp_fast = spacy.load("en_core_web_sm", disable=["parser", "ner"])
# process many texts efficiently
docs = list(nlp.pipe(["Hello world.", "SpaCy is fast."]))
# stream from file with nlp.pipe(texts, batch_size=1000)NLTK 语料库与数据
NLTK 封装了许多经典语料库——Gutenberg(书籍)、Brown(分类文本)、Reuters(新闻)、WordNet(词典)。下载是一次性的并会缓存。适合教程和基准测试,但生产环境建议使用 Hugging Face 'datasets' 中的现代数据集。
import nltk
# one-time downloads (cached in ~/nltk_data)
nltk.download('gutenberg') # Project Gutenberg books
nltk.download('reuters') # Reuters news corpus
nltk.download('brown') # Brown corpus (categorized)
nltk.download('wordnet') # lexical database
nltk.download('averaged_perceptron_tagger')
from nltk.corpus import gutenberg, brown
# first sentences of Moby Dick
print(gutenberg.sents('melville-moby_dick.txt')[0][:8])
# Brown corpus categories
print(brown.categories()) # ['news', 'editorial', 'reviews', ...]
news_words = brown.words(categories='news')第一个 NLP 脚本(文本统计)
快速分析文本的方法:词元数、句子数、唯一词形数和内容词频率(移除停用词后)。spaCy Doc 是可迭代的,并通过 doc.sents 暴露句子。collections 中的 Counter 是最简单的频率统计工具。
import spacy
nlp = spacy.load("en_core_web_sm")
text = "The quick brown fox jumps over the lazy dog. The dog barks."
doc = nlp(text)
# basic stats
print("Tokens:", len(doc))
print("Sentences:", len(list(doc.sents)))
print("Unique words:", len({t.lemma_.lower() for t in doc if t.is_alpha}))
# word frequency
from collections import Counter
words = [t.text.lower() for t in doc if not t.is_stop and t.is_alpha]
print(Counter(words).most_common(5))读取与清洗文本文件
真实文本很杂乱:编码不一致、HTML 标签、智能引号、破折号。始终用显式 utf-8 读取。在分词前规范化空白字符并去除 HTML。小写化会丢失信息(US vs us)——仅在进行词袋模型类任务时使用,NER 和词性标注不要小写化。
import re
from pathlib import Path
# read with proper encoding
raw = Path("doc.txt").read_text(encoding="utf-8")
# normalize whitespace
text = re.sub(r"\s+", " ", raw).strip()
# remove HTML tags
text = re.sub(r"<[^>]+>", " ", text)
# fix curly quotes / dashes to ASCII
text = text.replace("\u2019", "'").replace("\u201c", '"')
text = text.replace("\u2013", "-").replace("\u2014", "-")
# lowercase only if case-insensitive task
clean = text.lower()分词
NLTK 词与句子分词
NLTK 的 punkt 分词器使用训练好的模型拆分句子,能处理 'Mr.' 等缩写。word_tokenize 采用 Treebank 规范,会拆分缩写('don't' -> 'do' + "n't")。选择与下游模型训练方式匹配的分词器——不匹配的分词会悄悄降低性能。
from nltk.tokenize import word_tokenize, sent_tokenize
text = "Hello there! How are you? I'm fine, thanks."
# sentence split (handles abbreviations)
sents = sent_tokenize(text)
# ['Hello there!', 'How are you?', "I'm fine, thanks."]
# word split (handles contractions, punctuation)
tokens = word_tokenize(text)
# ['Hello', 'there', '!', 'How', 'are', 'you', '?',
# 'I', "'m", 'fine', ',', 'thanks', '.']
# treebank tokenizer: 'don't' -> ['do', "n't"]
from nltk.tokenize import TreebankWordTokenizer
print(TreebankWordTokenizer().tokenize("don't stop"))spaCy 分词
spaCy 的分词器是非破坏性的:它不会丢失词元与原始文本之间的对齐关系(tok.idx 给出字符偏移)。这对 NER 和高亮很重要。自定义 infix/exception 规则允许你调整拆分方式而无需重写整个流水线。
import spacy
nlp = spacy.load("en_core_web_sm")
doc = nlp("Let's go to N.Y.C. on 03/14/2023 for $5.50!")
# tokens keep their position in the original text
for tok in doc:
print(f"{tok.text!r:12} start={tok.idx} end={tok.idx + len(tok.text)}")
# spans: contiguous slices of the Doc
span = doc[2:5] # 'go to N.Y.C.'
print(span.text)
# spaCy never loses alignment to the original string
print(doc.text == "".join(t.text_with_ws for t in doc)) # True正则表达式分词器
正则分词器快速且确定性强——适合你掌控文本格式的场景。\w+ 会丢弃标点和数字词;需根据任务谨慎搭配。WhitespaceTokenizer 是最简单的拆分,但标点会粘在词上,通常需要二次清理。
from nltk.tokenize import RegexpTokenizer
# words only (drop punctuation)
tok = RegexpTokenizer(r"\w+")
print(tok.tokenize("Hello, world! 123")) # ['Hello', 'world', '123']
# keep contractions: \w+|'\w+
tok2 = RegexpTokenizer(r"\w+|'\w+")
print(tok2.tokenize("don't stop")) # ['do', "n't", 'stop']
# whitespace tokenizer (keeps punctuation attached)
from nltk.tokenize import WhitespaceTokenizer
print(WhitespaceTokenizer().tokenize("Hi! Bye.")) # ['Hi!', 'Bye.']子词分词(BPE / WordPiece)
子词分词器(BPE、WordPiece、Unigram、SentencePiece)将稀有词拆分为常见片段,平衡词表大小和覆盖率。它们是现代 Transformer 的标准——BERT 使用 WordPiece,GPT-2 使用 BPE,T5 使用 SentencePiece。在自己的语料上训练以匹配目标领域。
from tokenizers import Tokenizer
from tokenizers.models import BPE
from tokenizers.trainers import BpeTrainer
from tokenizers.pre_tokenizers import Whitespace
# train a Byte-Pair-Encoding tokenizer
tokenizer = Tokenizer(BPE(unk_token="[UNK]"))
tokenizer.pre_tokenizer = Whitespace()
trainer = BpeTrainer(
special_tokens=["[UNK]", "[PAD]", "[CLS]", "[SEP]"],
vocab_size=8000,
)
tokenizer.train(files=["corpus.txt"], trainer=trainer)
enc = tokenizer.encode("Tokenization is fun.")
print(enc.tokens) # ['Tok', 'eni', 'zer', ...]
print(enc.ids) # [34, 912, ...]自定义 spaCy 分词器规则
对默认分词器处理不好的领域术语(URL、话题标签、药品名)使用 add_special_case。修改 infix/prefix/suffix 模式会改变全局拆分行为——先在样本上测试。优先使用 special cases 而非完全重写;它们能与流水线的其他部分干净组合。
import spacy
from spacy.tokenizer import Tokenizer
from spacy.util import compile_infix_regex, compile_prefix_regex
nlp = spacy.load("en_core_web_sm")
# add a special-case rule
specials = {"gimme": [{ORTH: "gim"}, {ORTH: "me"}]}
for text, pattern in specials.items():
nlp.tokenizer.add_special_case(text, pattern)
print([t.text for t in nlp("gimme that")]) # ['gim', 'me', 'that']
# customize infixes (e.g. split on hyphens)
infixes = compile_infix_regex([r"-"])
nlp.tokenizer.infix_finditer = infixes.finditer
print([t.text for t in nlp("state-of-the-art")])Transformer 分词器(Hugging Face)
始终将模型与其专用分词器配对——词表不匹配会悄悄破坏一切。AutoTokenizer.from_pretrained 自动选择正确的类(BertTokenizer、GPT2Tokenizer 等)。bert-base-uncased 会小写化并添加 [CLS]/[SEP];cased 变体保留大小写。设置 truncation/max_length 以避免长输入报错。
from transformers import AutoTokenizer
# load the tokenizer matching a pretrained model
tok = AutoTokenizer.from_pretrained("bert-base-uncased")
enc = tok("Tokenization with BERT.", return_tensors="pt")
print(enc.keys()) # dict_keys(['input_ids', 'token_type_ids', 'attention_mask'])
print(enc["input_ids"]) # tensor([[101, 19204, 3989, 102]])
print(tok.convert_ids_to_tokens(enc["input_ids"][0]))
# ['[CLS]', 'tokenization', 'with', 'bert', '.', '[SEP]']
# handle long inputs with sliding window
print(tok("Very long text", truncation=True, max_length=512))词性标注
NLTK 词性标注
默认的感知器标注器使用 Penn Treebank 标签集(NN=名词、VB=动词、JJ=形容词、PRP=代词...)。它是上下文敏感的——'permit' 作为动词(VB)还是名词(NN)由周围词消歧。词性标签是廉价特征,能改善 NER、分块和基于规则的抽取。
import nltk
from nltk.tokenize import word_tokenize
nltk.download('averaged_perceptron_tagger_eng')
tokens = word_tokenize("They refuse to permit us to obtain the permit.")
tags = nltk.pos_tag(tokens)
# [('They', 'PRP'), ('refuse', 'VBP'), ('to', 'TO'),
# ('permit', 'VB'), ..., ('permit', 'NN'), ('.', '.')]
# note: the two 'permit' tokens get different tags (VB verb vs NN noun)
for word, tag in tags:
print(f"{word:10} {tag}")spaCy 词性与形态学
spaCy 同时提供粗粒度通用词性(pos_,语言无关)和细粒度 Treebank 标签(tag_)。morph 对象携带时态、数、人称等特征。通用标签最适合跨语言工作;Treebank 标签为英语特定规则提供更多细节。
import spacy
nlp = spacy.load("en_core_web_sm")
doc = nlp("The children were playing happily.")
for tok in doc:
print(f"{tok.text:12} POS={tok.pos_:6} TAG={tok.tag_:5} "
f"lemma={tok.lemma_:10} morph={tok.morph}")
# tok.pos_ -> coarse Universal POS (NOUN, VERB, ADJ, ...)
# tok.tag_ -> fine-grained Treebank tag (NN, VBD, JJ, ...)
# tok.morph -> morphological features (Tense=Past, Number=Plur, ...)通用词性标签
通用词性标签(来自 Universal Dependencies)在所有语言中使用相同的 17 个标签——对多语言流水线极有价值。PROPN(专有名词)与 NOUN 的区分对 NER 很重要。统计词性比例(词 汇密度 = NOUN+VERB+ADJ / 总数)是简单的文本风格特征。
# Universal Dependencies tagset (17 tags, same across languages)
# ADJ - adjective | ADP - adposition (prep/post)
# ADV - adverb | AUX - auxiliary (is, has)
# CCONJ - coordinating conj | DET - determiner (the, a)
# INTJ - interjection | NOUN - noun
# NUM - numeral | PART - particle ('s, not)
# PRON - pronoun | PROPN - proper noun
# PUNCT - punctuation | SCONJ - subordinating conj
# SYM - symbol | VERB - verb
# X - other
import spacy
nlp = spacy.load("en_core_web_sm")
doc = nlp("She quickly ran to the store.")
nouns = [t.text for t in doc if t.pos_ in ("NOUN", "PROPN")]
verbs = [t.text for t in doc if t.pos_ == "VERB"]读取已标注语料库
已标注语料库是训练或评估标注器以及进行语言学分析的黄金标准。Brown 使用丰富的标签集(带后缀如 -TL 表示标题、-NC 表示引用)。比较 Brown 各类别(新闻 vs 小说)的标签分布可揭示词性使用的风格差异。
import nltk
from nltk.corpus import brown, treebank
# Brown: each word pre-tagged
sent = brown.tagged_sents(categories='news')[0]
print(sent) # [('The', 'AT'), ('Fulton', 'NP-TL'), ...]
# Penn Treebank ( Wall Street Journal)
for s in treebank.tagged_sents()[:1]:
print(s)
# count tag distribution
from collections import Counter
tags = [tag for w, tag in brown.tagged_words(categories='news')]
print(Counter(tags).most_common(5))基于规则与查找的标注
对于小规模或领域特定数据,带回退链的简单标注器效果出奇地好:先尝试最具体的标注器,对未知词回退到更宽泛的。UnigramTagger 学习每个词最常见的标签;DefaultTagger('NN')处理未见词。RegexTagger 捕获数字、日期和大小写模式。
from nltk.tag import UnigramTagger, DefaultTagger
from nltk.corpus import brown
# fallback: tag everything as NN
default = DefaultTagger('NN')
# unigram: most-likely tag per word (from training data)
train = brown.tagged_sents(categories='news')[:3000]
uni = UnigramTagger(train, backoff=default)
# backoff chain: unigram -> default
test = brown.tagged_sents(categories='news')[3000:3100]
print(f"Accuracy: {uni.evaluate(test):.3f}")
# regex tagger for patterns
from nltk.tag import RegexpTagger
regex = RegexpTagger([(r'^-?[0-9]+(\.[0-9]+)?$', 'CD')], backoff=default)命名实体识别
spaCy 命名实体识别
spaCy 的预训练 NER 识别 ORG、PERSON、GPE(地缘政治实体)、DATE、MONEY 等。doc.ents 是与分词器对齐的跨度。displacy.render/serve 提供漂亮的 HTML 可视化。在一般新闻文本上准确率不错;对专业领域(医疗、法律)需要微调。
import spacy
nlp = spacy.load("en_core_web_sm")
doc = nlp("Apple is buying a U.K. startup for $1 billion in New York.")
for ent in doc.ents:
print(f"{ent.text:20} {ent.label_:10} {spacy.explain(ent.label_)}")
# Apple ORG Companies, agencies, institutions
# U.K. GPE Countries, cities, states
# $1 billion MONEY Monetary values
# New York GPE Countries, cities, states
# visualize
from spacy import displacy
displacy.serve(doc, style="ent") # opens a browserNLTK 命名实体分块
NLTK 的 ne_chunk 结合词性标注和基于语法的分块器。输出是一棵树,命名实体是标记为 PERSON、ORGANIZATION、GPE 等的子树。比 spaCy 慢且准确率较低,但适合学习分块概念和基于规则的扩展。
import nltk
from nltk.tokenize import word_tokenize, sent_tokenize
text = "Barack Obama was born in Hawaii and led the USA."
sents = sent_tokenize(text)
for s in sents:
tokens = word_tokenize(s)
tags = nltk.pos_tag(tokens)
# ne_chunk returns a nested tree of named entities
tree = nltk.ne_chunk(tags)
print(tree)
# (S
# (PERSON Barack/NNP Obama/NNP)
# was/VBD born/VBN in/IN
# (GPE Hawaii/NNP) ...)
# extract just the named entities
for node in tree:
if hasattr(node, 'label'):
name = " ".join(w for w, t in node)
print(node.label(), name)实体类型与标签
spaCy 的英文 NER 使用 OntoNotes 标签集——广泛覆盖人、组织、地点、日期、货币、数量。用 spacy.explain(label) 获取人类可读的描述。如需更细粒度的类型(如区分 CEO 和 COMPANY),训练自定义模型或使用外部知识库。
import spacy
nlp = spacy.load("en_core_web_sm")
# list all labels the model knows
for label in nlp.get_pipe("ner").labels:
print(f"{label:12} {spacy.explain(label)}")
# PERSON People, including fictional
# NORP Nationalities or religious/political groups
# FAC Buildings, airports, highways
# ORG Companies, agencies, institutions
# GPE Countries, cities, states
# LOC Non-GPE locations: mountain ranges, bodies of water
# PRODUCT Vehicles, food, clothing (not services)
# EVENT Battles, wars, sports events
# DATE Absolute or relative dates
# TIME Times smaller than a day
# MONEY Monetary values
# QUANTITY Measurements (weight, distance)
# ORDINAL First, second, ...
# CARDINAL Numerals that are not dates or amounts训练自定义 NER 模型
当预训练标签不适合你的领域时微调 NER。以 (text, {entities: [(start, end, label)]}) 格式提供训练数据,使用字符偏移——spaCy 需要精确跨度。从 en_core_web_sm 而非空白模型开始以利用已有特征。每种实体类型 30-50 个样本是合理的最低数量;使用 use_eval 进行早停。
import spacy
from spacy.training import Example
# start from a blank model (or fine-tune a pretrained one)
nlp = spacy.blank("en")
ner = nlp.add_pipe("ner")
ner.add_label("DRUG")
# training data: text + character offsets of entities
TRAIN = [
("Take aspirin daily.", {"entities": [(5, 11, "DRUG")]}),
("Ibuprofen reduces fever.", {"entities": [(0, 9, "DRUG")]}),
]
optimizer = nlp.initialize()
for epoch in range(30):
for text, ann in TRAIN:
doc = nlp.make_doc(text)
nlp.update([Example.from_dict(doc, ann)], sgd=optimizer)
# save and use
nlp.to_disk("custom_ner")
doc = nlp("Give metformin 500mg.")
print([(e.text, e.label_) for e in doc.ents]) # [('metformin', 'DRUG')]实体链接与维基化
NER 找到提及;实体链接将它们消歧到知识库条目(哪个'苹果'?公司还是水果)。完整链接需要知识库加上候选生成器和消歧器。spaCy 内置的 EntityLinker 需要你构建 KB;生产环境可考虑 REL、mGENRE 或商业 API(Google KG、Bing Entity Search)。
# Entity linking maps a mention to a unique ID in a knowledge base
# (e.g. "Apple" -> Apple Inc. the company, not the fruit)
import spacy
nlp = spacy.load("en_core_web_sm")
# spaCy's EntityLinker requires a knowledge base (KB).
# Building one from Wikipedia/Wikidata is involved; in practice use:
# Option 1: spaCy + Wikipedia-based KB (spacy-entity-linker)
# pip install spacy-entity-linker
# from spacy_entity_linker import EntityLinker
# linker = EntityLinker()
# nlp.add_pipe("entityLinker", last=True)
# Option 2: a dedicated library
# import entitylinking # or use a Wikidata API directly
# Example query via Wikidata SPARQL
import requests
url = "https://www.wikidata.org/w/api.php"
params = {"action": "wbsearchentities", "search": "Apple",
"language": "en", "format": "json"}
print(requests.get(url, params=params).json())词干提取与词形还原
Porter 词干提取器
PorterStemmer 应用固定序列的后缀剥离规则。它快速且确定性强,但会产生非词('easili'、'studi')且忽略上下文——'ran' 保持 'ran' 因为没有后缀可剥离。适合搜索引擎和主题模型等注重词干一致性而非正确性的场景。
from nltk.stem import PorterStemmer
stemmer = PorterStemmer()
for w in ["running", "runs", "ran", "easily", "fairness", "studies"]:
print(f"{w:10} -> {stemmer.stem(w)}")
# running -> run
# runs -> run
# ran -> ran (Porter doesn't handle irregulars)
# easily -> easili (sometimes ugly)
# fairness -> fair
# studies -> studi
# fast, rule-based, deterministic
print([stemmer.stem(t) for t in "the cats are running".split()])Snowball 词干提取器
SnowballStemmer 是 Porter 的现代化版本('English' 版本),并支持 15+ 种其他语言。它略微更准确且开箱即用支持非英文文本。如果需要无词典的多语言词干提取,Snowball 是实用的默认选择;如需更高质量的结果,切换到词形还原器。
from nltk.stem import SnowballStemmer
# Snowball = improved Porter, supports many languages
print(SnowballStemmer.languages)
# ['arabic', 'danish', 'dutch', 'english', 'finnish', 'french',
# 'german', 'hungarian', 'italian', 'norwegian', 'porter',
# 'portuguese', 'romanian', 'russian', 'spanish', 'swedish']
en = SnowballStemmer("english")
print(en.stem("running"), en.stem("fairly"), en.stem("studies"))
# run fair studi
# compare two languages
fr = SnowballStemmer("french")
print(fr.stem("mangerons")) # mangWordNet 词形还原器
词形还原返回真实的词典词('better' -> 'good'、'mice' -> 'mouse'),通过查找 WordNet 实现。它需要词性来消歧('running' 是名词还是动词)。当下游特征(BoW、TF-IDF)受益于规范形式时,词性标注的开销是值得的。由于词典查找,比词干提取慢。
import nltk
from nltk.stem import WordNetLemmatizer
nltk.download('wordnet')
lem = WordNetLemmatizer()
# without POS, defaults to NOUN
print(lem.lemmatize("running")) # running (treated as noun)
print(lem.lemmatize("running", pos='v')) # run (verb)
print(lem.lemmatize("better", pos='a')) # good (adjective)
print(lem.lemmatize("mice")) # mouse (irregular plural)
# pipeline: tokenize -> POS tag -> lemmatize with correct POS
from nltk.tokenize import word_tokenize
from nltk import pos_tag
words = word_tokenize("The cats were running quickly.")
treebank_to_wordnet = {'N': 'n', 'V': 'v', 'J': 'a', 'R': 'r'}
for w, t in pos_tag(words):
wn_pos = treebank_to_wordnet.get(t[0], 'n')
print(w, '->', lem.lemmatize(w, pos=wn_pos))spaCy 词形还原
spaCy 在流水线内进行词形还原——词性感知且无需额外代码即可处理不规则形式('ate'->'eat'、'were'->'be')。使用 lemma_ 代替 text 会将 'cat'/'cats'/'cats' 合并为一个特征,减小词表大小。对于多语言模型,词形还原质量各异;请查看模型的准确率报告。
import spacy
nlp = spacy.load("en_core_web_sm")
doc = nlp("The cats were running faster. I ate two apples.")
for tok in doc:
if tok.text.lower() != tok.lemma_:
print(f"{tok.text:10} -> {tok.lemma_}")
# cats -> cat
# were -> be
# running -> run
# faster -> fast
# ate -> eat
# apples -> apple
# context-aware, handles irregulars automatically
# use lemma_ instead of text for bag-of-words features
docs_tokens = [[t.lemma_.lower() for t in nlp(d) if not t.is_stop]
for d in ["Cats run.", "The cat ran."]]词干提取与词形还原对比
词干提取更快但会产生非词,且对某些用途合并过于激进。词形还原返回有效词并尊重词性,代价是词典查找。对于词袋特征,两者都能减小词表;对于面向用户的输出(如关键词列表),始终用词形还原。对于大规模搜索,词干提取是务实的选择。
import spacy
from nltk.stem import PorterStemmer
nlp = spacy.load("en_core_web_sm")
stem = PorterStemmer()
words = ["studies", "studying", "better", "feet", "running", "happily"]
print(f"{'word':12}{'stem':12}{'lemma':12}")
for w in words:
lemma = nlp(w)[0].lemma_
print(f"{w:12}{stem.stem(w):12}{lemma:12}")
# studies studi study
# studying studi study
# better better good <- lemma handles irregular
# feet feet foot
# running run run
# happily happili happily
# rule of thumb:
# search/indexing/topic-models -> stemming (fast)
# interpretation/visualization -> lemmatization (accurate)停用词
NLTK 停用词
NLTK 为每种语言提供紧凑的停用词列表(英文约 180 个词)。使用 set 实现 O(1) 查找。检查前始终小写化,因为列表是小写的。过滤会缩小词表并移除低信息量词——对主题模型和搜索索引有用,但对情感分析有害("not good" 会丢失否定)。
import nltk
from nltk.corpus import stopwords
nltk.download('stopwords')
# 18 languages available
print(stopwords.fileids())
# ['arabic', 'english', 'french', 'german', 'spanish', ...]
en = set(stopwords.words('english'))
print(len(en)) # 179
print("the" in en) # True
print("python" in en) # False
# remove from a token list
from nltk.tokenize import word_tokenize
text = "The quick brown fox is very lazy indeed"
tokens = [w for w in word_tokenize(text.lower()) if w.isalpha()]
filtered = [w for w in tokens if w not in en]
print(filtered) # ['quick', 'brown', 'fox', 'lazy']spaCy 停用词
spaCy 为每个词元标记 is_stop,包括缩写('isn't')。默认列表比 NLTK 的大(326 个词)。将 is_stop 与 is_punct 和 is_alpha 一起使用以提取干净的内容词。列表位于 nlp.Defaults.stop_words,可在运行时按流水线编辑。
import spacy
nlp = spacy.load("en_core_web_sm")
# access the stopword set
print(len(nlp.Defaults.stop_words)) # 326
print("the" in nlp.Defaults.stop_words) # True
# tokens expose is_stop directly
doc = nlp("The quick fox isn't lazy")
for tok in doc:
print(f"{tok.text:8} is_stop={tok.is_stop}")
# The is_stop=True
# quick is_stop=False
# isn't is_stop=True (contraction included)
# content words only
content = [t.text for t in doc if not t.is_stop and not t.is_punct]安全移除停用词
盲目移除停用词会破坏信息:否定('not good' -> 'good')、多词实体('New York' 没问题但 'out of' 可能重要)和问题结构('What is X?' -> 'X?')。情感分析始终保留否定词。NER 在实体提取前绝不要移除停用词。
import spacy
nlp = spacy.load("en_core_web_sm")
def remove_stopwords(text, keep_negations=True):
doc = nlp(text)
out = []
for tok in doc:
if tok.is_stop or tok.is_punct:
# keep negations for sentiment tasks
if keep_negations and tok.lemma_ in ("not", "no", "never"):
out.append(tok.text)
continue
continue
out.append(tok.text)
return " ".join(out)
print(remove_stopwords("I do not like green eggs and ham."))
# 'not like green eggs ham .'
# bigrams preserve phrases that stopwords broke:
# 'New York' should NOT lose its inner stopwords...自定义停用词列表
通用列表会遗漏领域噪声('reuters'、'inc'、新闻中的'said')。挖掘你的语料库中频率极高、信息量极低的词并添加它们。反之,移除你的任务关心的词(立场检测中的否定词、情态动词)。保存修改后的流水线使自定义列表随模型一起传输。
import spacy
nlp = spacy.load("en_core_web_sm")
# add domain-specific stopwords
domain_stops = {"said", "says", "say", "according", "reuters", "inc", "ltd"}
for w in domain_stops:
nlp.Defaults.stop_words.add(w)
# also set the lexeme attribute
lex = nlp.vocab[w]
lex.is_stop = True
# remove a word that you DO want to keep
nlp.Defaults.stop_words.discard("not")
nlp.vocab["not"].is_stop = False
# persist by saving the modified model
# nlp.to_disk("my_model_with_stops")何时保留停用词
深度模型(BERT、GPT)在包含停用词的自然文本上训练——预先剥离会降低性能,因为模型依赖功能词来理解语法。停用词移除是浅层模型的特征工程技巧;两种方式都评估,保留在验证指标上得分更高的那种。
# Stopword removal HELPS:
# - bag-of-words / TF-IDF (smaller vocab, less noise)
# - topic modeling (LDA wants content words)
# - search indexing (precision up, index smaller)
# Stopword removal HURTS:
# - sentiment analysis ("not good" != "good")
# - NER / entity extraction (alignment breaks)
# - syntax parsing (function words are syntactic glue)
# - transformer models (they were pretrained WITH stopwords)
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("bert-base-uncased")
# NEVER strip stopwords before feeding BERT — the model
# expects natural text and uses function words syntactically.
# rule: only remove stopwords for shallow features (BoW/TF-IDF),
# and only if you've checked it doesn't hurt your metric.词袋模型与向量化
CountVectorizer 基础
CountVectorizer 将字符串列表转为稀疏的文档-词矩阵。fit_transform 一步完成学习词表和转换;transform 在新数据上复用已学词表(关键——绝不在测试数据上重新 fit)。矩阵是稀疏的以高效处理大词表。默认 token 模式 \b\w\w+\b 会丢弃单字符词元。
from sklearn.feature_extraction.text import CountVectorizer
docs = ["The cat sat on the mat.", "The dog sat on the log."]
# default: lowercase, word tokenizer (\b\w\w+\b), unigrams
vec = CountVectorizer()
X = vec.fit_transform(docs)
print(X.shape) # (2, 6) -> 2 docs, 6 unique words
print(vec.get_feature_names_out())
# ['cat' 'dog' 'log' 'mat' 'on' 'sat' 'the']
print(X.toarray())
# [[1 0 0 1 1 1 2] 'the' appears twice in doc 0
# [0 1 1 0 1 1 2]]
# transform new text with the fitted vocabulary
new = vec.transform(["The cat and the dog."])N-gram
N-gram 捕获 unigram 丢失的局部词序——'not good' vs 'good'。Bigram 会急剧增加词表(二次增长),所以搭配 min_df(丢弃稀有)或 max_features(限制 top-K)。对于词袋模型,trigram 很少有回报;如需更长上下文,切换到嵌入。
from sklearn.feature_extraction.text import CountVectorizer
docs = ["I love this movie", "I hate this movie"]
# unigrams + bigrams
vec = CountVectorizer(ngram_range=(1, 2))
X = vec.fit_transform(docs)
print(vec.get_feature_names_out())
# ['hate', 'hate this', 'love', 'love this', 'movie',
# 'this', 'this movie']
# bigrams capture phrases like "not good", "New York"
# but explode the vocabulary -> pair with min_df / max_features
vec2 = CountVectorizer(ngram_range=(2, 2), min_df=2)
# (2,2) = bigrams only; min_df=2 keeps bigrams seen >= 2 timesHashingVectorizer
HashingVectorizer 是无状态的——它将每个词元哈希到一列索引,因此没有词表需要存储。适合流式或内存受限场景。代价是:你无法检查某列代表哪个词,且冲突会合并特征。设置 alternate_sign=False 以避免符号翻转在线性模型中抵消计数。
from sklearn.feature_extraction.text import HashingVectorizer
# stateless: no vocabulary kept, uses feature hashing
vec = HashingVectorizer(n_features=2**18, alternate_sign=False)
X = vec.transform(["doc one", "doc two"])
print(X.shape) # (2, 262144)
# pros: no memory for vocabulary, streamable, fit_transform = transform
# cons: hashes are not interpretable, possible collisions,
# no inverse_transform (can't map feature -> word)
# use for: very large / streaming corpora where you don't need
# to know which word a column corresponds to词表控制
min_df 和 max_df 是最有用的旋钮:分别丢弃稀有拼写错误和全语料停用词,缩小矩阵并减少噪声。max_features 限制词表以节省内存。自定义 token_pattern 按长度或字符类过滤。固定词表在需要跨运行保持列稳定时很方便。
from sklearn.feature_extraction.text import CountVectorizer
docs = ["a a a b b c rareword commontoken"] * 5
vec = CountVectorizer(
min_df=2, # ignore terms in <2 docs
max_df=0.9, # ignore terms in >90% of docs (corpus-specific stops)
max_features=1000, # keep only top-1000 by frequency
stop_words='english',
token_pattern=r"\b[a-z]{2,}\b", # only 2+ letter words
)
X = vec.fit_transform(docs)
print(vec.get_feature_names_out())
# fix a custom vocabulary (only these words are counted)
vec_fixed = CountVectorizer(vocabulary=["cat", "dog", "fish"])
print(vec_fixed.get_feature_names_out())稀疏矩阵操作
文档-词矩阵是稀疏的(大部分为零)——保持稀疏以避免内存爆炸(一个 10万×5万 的密集 float64 矩阵需要 40GB)。使用 .sum(axis=0)、.multiply()、在稀疏对象上切片。只在小的切片上调用 .toarray()。CSR 格式优化了行切片;转换为 CSC 用于列切片。
from sklearn.feature_extraction.text import CountVectorizer
import numpy as np
from scipy.sparse import csr_matrix
docs = ["the cat sat", "the dog ran", "the cat ran"]
vec = CountVectorizer()
X = vec.fit_transform(docs) # CSR sparse matrix
print(type(X)) # <class 'scipy.sparse.csr_matrix'>
print(X.shape, X.nnz) # (3, 5) 9 non-zero entries
# NEVER call .toarray() on huge matrices — they explode in memory.
# operate on sparse directly:
col_sums = np.asarray(X.sum(axis=0)).ravel() # document frequency
print(dict(zip(vec.get_feature_names_out(), col_sums)))
# find docs containing a word
word_idx = vec.vocabulary_["cat"]
rows_with_cat = X[:, word_idx].nonzero()[0]
print("docs with 'cat':", rows_with_cat) # [0, 2]TF-IDF
TfidfVectorizer 基础
TF-IDF 降低出现在许多文档中的词的权重(低区分力),提高稀有且有信息量的词的权重。TfidfVectorizer 结合了 CountVectorizer + TfidfTransformer 并进行 L2 行归一化,使文档长度不占主导。它是线性模型文本分类的默认特征。
from sklearn.feature_extraction.text import TfidfVectorizer
docs = ["the cat sat on the mat",
"the dog sat on the log",
"cats and dogs are pets"]
vec = TfidfVectorizer() # default: L2-normalized rows
X = vec.fit_transform(docs)
print(vec.get_feature_names_out())
# tf-idf = term_freq * log(N / df) * (variations exist)
# down-weights words common across all docs ('the', 'sat')
print(X.toarray().round(3))
# each row is L2-normalized to length 1
import numpy as np
print(np.linalg.norm(X.toarray(), axis=1)) # [1. 1. 1.]TfidfTransformer 与公式
smooth_idf 加 +1 以避免未见词的除零错误。sublinear_tf 用 1+log(count) 替换原始计数,限制文档内极高频词的影响——对某词可能重复数十次的长文档有用。norm='l2' 是标准的;如需原始量级则用 None。
from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer
from sklearn.pipeline import Pipeline
# equivalent to TfidfVectorizer but in two steps
pipe = Pipeline([
("counts", CountVectorizer()),
("tfidf", TfidfTransformer(
norm='l2',
use_idf=True,
smooth_idf=True, # idf = ln((1+N)/(1+df)) + 1 (avoids div-by-zero)
sublinear_tf=False, # if True: tf = 1 + log(count)
)),
])
# sublinear_tf compresses high counts:
# count 5 -> tf 5 (linear) vs tf 1+log(5)≈2.6 (sublinear)
# useful when a word repeating 100x isn't 100x more important
pipe2 = Pipeline([
("counts", CountVectorizer()),
("tfidf", TfidfTransformer(sublinear_tf=True)),
])解读 TF-IDF 权重
每个文档的 top TF-IDF 特征是其独特内容的快速摘要。出现在每个文档中的词获得约 1 的 idf(低权重);出现在一个文档中的词获得最高 idf。检查 vec.idf_ 可查看学习到的全局权重。这是训练分类器前很好的健全性检查。
from sklearn.feature_extraction.text import TfidfVectorizer
import numpy as np
docs = ["machine learning models", "deep learning networks",
"natural language processing"]
vec = TfidfVectorizer()
X = vec.fit_transform(docs)
names = vec.get_feature_names_out()
# top features per document
for i, doc in enumerate(docs):
row = X[i].toarray().ravel()
top = np.argsort(row)[::-1][:3]
print(f"doc {i}: {[(names[j], round(row[j], 3)) for j in top]}")
# doc 0: [('machine', 0.58), ('models', 0.58), ('learning', 0.41)]
# doc 1: [('deep', 0.5), ('networks', 0.5), ...]
# doc 2: [('natural', 0.41), ('processing', 0.41), ('language', 0.41)]
# inspect the learned idf weights
idf = dict(zip(names, vec.idf_))
print(sorted(idf.items(), key=lambda x: x[1])[:3]) # lowest idf = most common结合 N-gram 与停用词的 TF-IDF
将 TF-IDF 与 bigram 搭配可保留 unigram 丢失的否定('not good')。stop_words='english' 加 min_df 可缩小词表。这个 TfidfVectorizer + LinearSVC/LogisticRegression 组合是出奇强的基线——只有当你有足够标注数据时才用 BERT 超越它。
from sklearn.feature_extraction.text import TfidfVectorizer
docs = ["not good at all", "very good indeed", "not bad actually"]
vec = TfidfVectorizer(
stop_words='english',
ngram_range=(1, 2), # unigrams + bigrams
min_df=1,
sublinear_tf=True,
max_features=5000,
)
X = vec.fit_transform(docs)
print(vec.get_feature_names_out())
# bigrams like 'not good', 'not bad' preserve negation
# this combination is a strong baseline for text classification
# with LogisticRegression or LinearSVC用于相似度与搜索的 TF-IDF
TF-IDF 向量上的余弦相似度是经典的关键词搜索基线:查询和文档变为向量,按余弦角排序。它无需训练数据,但只能匹配共享词表——语义相似但词汇不同的文本('ML' vs 'machine learning')不会匹配。语义搜索请使用嵌入(sentence-transformers)。
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
corpus = [
"Machine learning is fun",
"Python is great for ML",
"I love cooking pasta",
]
query = ["learning machine"]
vec = TfidfVectorizer()
X = vec.fit_transform(corpus)
q = vec.transform(query)
# cosine similarity between query and each doc
sims = cosine_similarity(q, X).ravel()
for doc, score in sorted(zip(corpus, sims), key=lambda x: -x[1]):
print(f"{score:.3f} {doc}")
# 0.580 Machine learning is fun
# 0.000 I love cooking pasta
# this is essentially a tiny search engine词嵌入(Word2Vec / GloVe / FastText)
gensim Word2Vec
Word2Vec 通过预测上下文(skip-gram)或从上下文预测词(CBOW)来学习密集向量。skip-gram(sg=1)在小数据和稀有词上效果更好;CBOW 更快。min_count 过滤稀有词。真实模型需要数百万词元——在 Wikipedia 上训练或对大多数应用使用预训练向量。
from gensim.models import Word2Vec
from gensim.utils import simple_preprocess
sentences = [
"the cat sat on the mat".split(),
"the dog sat on the log".split(),
"cats and dogs are pets".split(),
]
# simple_preprocess lowercases and tokenizes
sents = [simple_preprocess(" ".join(s)) for s in sentences]
model = Word2Vec(
sentences=sents,
vector_size=100, # embedding dimension
window=5, # context window size
min_count=1, # ignore words with freq < min_count
workers=4,
sg=1, # 1 = skip-gram, 0 = CBOW
epochs=10,
)
print(model.wv["cat"].shape) # (100,)
print(model.wv.most_similar("cat", topn=3))训练自定义嵌入
使用生成器(SentenceIterator)使语料从磁盘流式读取——gensim 为流式而建,从不把所有内容放在内存中。min_count=5 移除拼写错误和稀有词。negative=10 是 skip-gram 的标准。当有更多数据时可保存并恢复训练。部署前用类比和相似度任务评估。
from gensim.models import Word2Vec
from gensim.utils import simple_preprocess
from pathlib import Path
# read a large corpus line by line (don't load all into memory)
class SentenceIterator:
def __init__(self, path):
self.path = Path(path)
def __iter__(self):
for line in self.path.open(encoding="utf-8"):
yield simple_preprocess(line)
sentences = SentenceIterator("corpus.txt")
model = Word2Vec(
sentences,
vector_size=200,
window=8,
min_count=5, # drop very rare words
workers=8,
epochs=5,
negative=10, # negative samples per positive (skip-gram)
)
model.save("w2v.model")
# resume training later
# model = Word2Vec.load("w2v.model")
# model.train(more_sentences, total_examples=len(more), epochs=5)相似度与类比
Word2Vec 著名地捕获类比关系(king - man + woman 约等于 queen),因为相似的上下文产生相似的向量。similarity 返回 [-1, 1] 范围的余弦值。doesnt_match 是有趣的异常值检测器。预训练的 Google News 向量覆盖 300 万词但体积 3GB;GloVe 或 fastText 通常更实用。
from gensim.models import KeyedVectors
# load pretrained Google News vectors (3GB, 3M words)
# Download: https://code.google.com/archive/p/word2vec/
model = KeyedVectors.load_word2vec_format("GoogleNews-vectors-negative300.bin",
binary=True)
# similarity
print(model.similarity("cat", "dog")) # ~0.86
print(model.similarity("cat", "car")) # ~0.28
# analogy: king - man + woman = queen
print(model.most_similar(positive=["king", "woman"],
negative=["man"], topn=1))
# [('queen', 0.71)]
# odd one out
print(model.doesnt_match("cat dog car horse".split())) # 'car'
# which word doesn't fit
print(model.most_similar("python", topn=5))FastText
FastText 通过学习字符 n-gram(子词)向量来扩展 Word2Vec,因此它可以嵌入未见词('catting' 由 'cat'+'ing' 组成)。这对形态丰富的语言和拼写错误大有帮助。由于子词查找,比 Word2Vec 稍慢,但 OOV 处理对大多数真实文本是值得的。
from gensim.models import FastText
sentences = [
"the cat sat on the mat".split(),
"dogs and cats are friends".split(),
]
# FastText learns subword (character n-gram) embeddings
model = FastText(
sentences,
vector_size=100,
window=5,
min_count=1,
min_n=3, # min char n-gram
max_n=6, # max char n-gram
epochs=10,
)
# KEY advantage: handles out-of-vocabulary words
print("catting" in model.wv) # False (not seen in training)
print(model.wv["catting"].shape) # (100,) -> still gets a vector!
# vector is averaged from subword n-grams of 'catting'
print(model.wv.most_similar("cat", topn=3))加载 GloVe 向量
GloVe(全局向量)基于共现计数而非预测训练,产生与 Word2Vec 可比的向量。Stanford 提供 50/100/200/300 维版本,在 60 亿词上训练。转换为 gensim 格式以复用 Word2Vec API。300 维向量在内存中约 1.5GB。
# GloVe: pretrained on Wikipedia+Gigaword
# Download: https://nlp.stanford.edu/projects/glove/
# Files: glove.6B.50d.txt, glove.6B.100d.txt, glove.6B.200d.txt, glove.6B.300d.txt
from gensim.scripts.glove2word2vec import glove2word2vec
from gensim.models import KeyedVectors
# convert GloVe format to word2vec format (one-time)
glove2word2vec("glove.6B.100d.txt", "glove.6B.100d.w2v.txt")
# load
glove = KeyedVectors.load_word2vec_format("glove.6B.100d.w2v.txt")
print(glove["king"].shape) # (100,)
print(glove.most_similar("king", topn=3))
# fast alternative: load directly into a dict
import numpy as np
def load_glove(path, dim=100):
embeddings = {}
with open(path, encoding="utf-8") as f:
for line in f:
parts = line.split()
embeddings[parts[0]] = np.asarray(parts[1:], dtype="float32")
return embeddings用 t-SNE 可视化嵌入
t-SNE 将高维向量投影到 2D 用于可视化——图中相近的点在嵌入空间中相似。根据点数将 perplexity 设在 5 到 50 之间。注意:t-SNE 会扭曲全局距离,所以用于探索而非测量。UMAP 通常比 t-SNE 更好地保留结构。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE
from gensim.models import KeyedVectors
model = KeyedVectors.load_word2vec_format("glove.6B.100d.w2v.txt")
# pick a small set of related words
words = ["king", "queen", "man", "woman", "paris", "france",
"london", "england", "dog", "cat", "horse", "cow"]
vectors = np.array([model[w] for w in words])
# reduce 100d -> 2d with t-SNE
tsne = TSNE(n_components=2, random_state=42, perplexity=5)
coords = tsne.fit_transform(vectors)
plt.figure(figsize=(8, 6))
for w, (x, y) in zip(words, coords):
plt.scatter(x, y)
plt.annotate(w, (x, y), xytext=(5, 5), textcoords="offset points")
plt.savefig("embeddings_tsne.png", dpi=120)
plt.show()上下文嵌入(BERT)
Hugging Face Transformers 基础
AutoTokenizer/AutoModel 从 Hugging Face Hub 上的模型名自动选择正确的类——无需知道是 BERT、RoBERTa 还是 DistilBERT。last_hidden_state 每个词元有一个 768 维向量;pooler_output 是 [CLS] 表示。对于句子嵌入,单独的 [CLS] 通常较弱——改用 sentence-transformer。
from transformers import AutoTokenizer, AutoModel
import torch
# load any pretrained model by name from the Hub
model_name = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)
inputs = tokenizer("Hello, BERT!", return_tensors="pt")
print(inputs.keys()) # input_ids, token_type_ids, attention_mask
with torch.no_grad():
outputs = model(**inputs)
# last hidden state: (batch, seq_len, hidden_size)
print(outputs.last_hidden_state.shape) # torch.Size([1, 6, 768])
# pooled output: [CLS] token passed through a linear+tanh
print(outputs.pooler_output.shape) # torch.Size([1, 768])BERT 词元嵌入
BERT 的决定性特征:'bank' 在 'river bank' 和 'bank account' 中的向量不同,因为注意力融入了上下文。这解决了多义性,即 Word2Vec/GloVe 最大的弱点。每个词元的向量是由整个序列构建的 768 维上下文表示。
from transformers import AutoTokenizer, AutoModel
import torch
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
model = AutoModel.from_pretrained("bert-base-uncased")
# contextual: 'bank' embeds differently in river vs money context
texts = ["I sat by the river bank.", "I deposited cash at the bank."]
for t in texts:
inputs = tokenizer(t, return_tensors="pt")
with torch.no_grad():
out = model(**inputs)
# find the index of 'bank'
idx = (inputs["input_ids"][0] == tokenizer.convert_tokens_to_ids("bank")).nonzero()[0]
vec = out.last_hidden_state[0, idx[0]]
print(f"{t[:25]:25} bank-vec[:3] = {vec[:3].tolist()}")
# the two 'bank' vectors DIFFER -> this is the whole point of BERT
# (static embeddings like Word2Vec would give the same vector both times)句子 Transformer
普通 BERT 的 [CLS] 是较差的句子嵌入(相似度分数不可靠)。sentence-transformers 用对比损失微调模型,使余弦相似度反映语义接近度。all-MiniLM-L6-v2 是默认选择:384 维、快速,且与大模型竞争。非常适合语义搜索、聚类和去重。
# pip install sentence-transformers
from sentence_transformers import SentenceTransformer, util
# models tuned to produce good sentence-level embeddings
model = SentenceTransformer("all-MiniLM-L6-v2") # 384d, fast & strong
sentences = [
"The cat sits on the mat.",
"A feline is resting on a rug.",
"Machine learning is fascinating.",
]
emb = model.encode(sentences, convert_to_tensor=True, normalize_embeddings=True)
# semantic similarity via cosine
print(util.cos_sim(emb, emb).round(3))
# [[1. 0.678 0.089]
# [0.678 1. 0.107]
# [0.089 0.107 1. ]]
# semantic search: rank corpus by similarity to a query
query = model.encode("where is the cat?", convert_to_tensor=True)
hits = util.semantic_search(query, emb)[0]
print(hits) # ranks 'cat sits on the mat' highest池化嵌入与词元嵌入对比
不同层编码不同信息:低层是句法的,高层是语义的。[CLS] 适合分类(微调后),但平均池化通常是更好的免费句子向量。拼接最后 4 层是原始 BERT 特征提取配方,对 NER 等词元级任务仍然有效。
from transformers import AutoTokenizer, AutoModel
import torch
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
model = AutoModel.from_pretrained("bert-base-uncased", output_hidden_states=True)
inputs = tokenizer("I love NLP", return_tensors="pt")
with torch.no_grad():
out = model(**inputs)
# all 13 layers (embedding + 12 transformer layers)
hidden = out.hidden_states # tuple of (1, seq, 768), len 13
print(len(hidden), hidden[0].shape)
# common pooling strategies for a sentence vector:
# 1. [CLS] token of the last layer
cls = hidden[-1][:, 0, :]
# 2. mean-pool over tokens (often better than [CLS])
mask = inputs["attention_mask"].unsqueeze(-1).float()
mean = (hidden[-1] * mask).sum(1) / mask.sum(1)
# 3. concat last 4 layers (rich feature, used in early BERT papers)
cat = torch.cat([hidden[-i][:, 0, :] for i in range(1, 5)], dim=-1)
print(cls.shape, mean.shape, cat.shape) # all (1, 768) except cat (1, 3072)静态与上下文嵌入对比
静态嵌入比 BERT 小 100-300 倍且更快,但无法区分词义。对于上下文重要的任务(情感、NER、QA),上下文嵌入胜出。对于纯词级相似度、大词表聚类或 CPU 大规模运行,静态嵌入仍然有竞争力——根据任务和预算选择。
import numpy as np
from gensim.models import KeyedVectors
from sentence_transformers import SentenceTransformer
# STATIC (Word2Vec/GloVe): same vector regardless of context
w2v = KeyedVectors.load_word2vec_format("glove.6B.100d.w2v.txt")
v1 = w2v["bank"]
v2 = w2v["bank"]
print("static cosine (always 1.0):", np.dot(v1, v2) / (np.linalg.norm(v1)*np.linalg.norm(v2)))
# CONTEXTUAL (BERT): vector depends on surrounding text
bert = SentenceTransformer("all-MiniLM-L6-v2")
from sklearn.metrics.pairwise import cosine_similarity
e_river = bert.encode("sat by the river bank")
e_money = bert.encode("deposited at the bank")
print("contextual cosine of two sentences:", cosine_similarity([e_river], [e_money])[0, 0].round(3))
# trade-offs:
# static: small, fast, fixed vocabulary, OOV-friendly only with FastText
# contextual: large, slower, handles polysemy, needs GPU for speed文本分类
sklearn 文本流水线
这个 TfidfVectorizer + LogisticRegression 流水线是标准的文本分类基线——快速、可解释,没有深度学习难以超越。Pipeline 封装预处理 + 模型,使相同的转换在预测时应用。class_weight='balanced' 有助于不平衡类别。始终报告每类精确率/召回率,而非仅准确率。
from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
# X: list of strings, y: labels
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2,
stratify=y, random_state=42)
clf = Pipeline([
("tfidf", TfidfVectorizer(ngram_range=(1, 2), min_df=2,
sublinear_tf=True, stop_words="english")),
("clf", LogisticRegression(max_iter=1000, class_weight="balanced")),
])
clf.fit(X_train, y_train)
print(classification_report(y_test, clf.predict(X_test)))
# predict on new text
print(clf.predict(["this is a great movie"])) # ['positive']朴素贝叶斯文本分类
朴素贝叶斯是最简单的文本分类器——快速、在极小数据集上有效,'朴素'的词独立假设虽被违反但在实践中仍效果好。MultinomialNB 接受计数(或 TF-IDF);BernoulliNB 接受二值存在。ComplementNB 专为不平衡类别设计,在文本上经常优于 Multinomial。
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import Pipeline
clf = Pipeline([
("vec", CountVectorizer()),
("clf", MultinomialNB(alpha=1.0)), # alpha = Laplace smoothing
])
clf.fit(X_train, y_train)
# MultinomialNB assumes word counts -> use CountVectorizer (not TF-IDF)
# alpha=1 (default) adds one to every count -> avoids zero probabilities
# lower alpha (0.1) for less smoothing when you have lots of data
# variants:
# MultinomialNB - counts/TF-IDF (most common for text)
# BernoulliNB - binary features (word present/absent)
# ComplementNB - better on imbalanced text classes线性 SVM 文本分类
LinearSVC 经常是最佳的浅层文本分类器——快速、稀疏友好,在 TF-IDF 特征上准确率高。缺点:没有原生的 predict_proba。如需概率分数(如阈值调优),用 CalibratedClassifierCV 包装。用网格搜索调 C;0.1-10 附近的值通常有效。
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.svm import LinearSVC
from sklearn.pipeline import Pipeline
from sklearn.calibration import CalibratedClassifierCV
# LinearSVC is much faster than SVC(kernel='linear') on sparse text
clf = Pipeline([
("tfidf", TfidfVectorizer(sublinear_tf=True)),
("clf", LinearSVC(C=1.0)), # C = inverse regularization
])
# LinearSVC has no predict_proba by default; wrap if you need probabilities
calibrated = CalibratedClassifierCV(LinearSVC(), cv=3)
clf_prob = Pipeline([("tfidf", TfidfVectorizer()),
("clf", calibrated)])
# tune C: smaller C = more regularization (helps with noisy text)
# LinearSVC is often the strongest shallow text classifierBERT 微调分类
在分类头上微调预训练 BERT/DistilBERT,当你有数千标注样本时可获得最先进结果。DistilBERT 比 BERT 小约 40% 且快,质量约 95%——很好的默认选择。使用低学习率(2e-5)和 2-4 个 epoch;更多 epoch 会过拟合。Trainer 处理批处理、混合精度和日志。
from transformers import (AutoTokenizer, AutoModelForSequenceClassification,
TrainingArguments, Trainer)
import torch
model_name = "distilbert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(
model_name, num_labels=3
)
# tokenize the dataset
def tokenize(batch):
return tokenizer(batch["text"], padding="max_length",
truncation=True, max_length=128)
train_ds = dataset["train"].map(tokenize, batched=True)
train_ds.set_format("torch", columns=["input_ids", "attention_mask", "label"])
args = TrainingArguments(
output_dir="./clf",
num_train_epochs=3,
per_device_train_batch_size=16,
learning_rate=2e-5,
eval_strategy="epoch",
)
trainer = Trainer(model=model, args=args,
train_dataset=train_ds, eval_dataset=dataset["test"])
trainer.train()多标签分类
多标签不同于多分类:每个实例可以有零个、一个或多个标签。OneVsRestClassifier 为每个标签训练一个二分类器。使用 micro/macro F1(而非准确率)——100 标签空间上的准确率有误导性。对于 BERT,用每标签 sigmoid 代替 softmax 并使用 BCEWithLogitsLoss。
# multi-label: each document can have MULTIPLE tags
# (e.g. a news article is both 'politics' AND 'economy')
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.multiclass import OneVsRestClassifier
from sklearn.pipeline import Pipeline
from sklearn.metrics import f1_score
# y is a (n_samples, n_labels) binary matrix, NOT a 1-d array
clf = Pipeline([
("tfidf", TfidfVectorizer()),
("clf", OneVsRestClassifier(LogisticRegression(max_iter=1000))),
])
clf.fit(X_train, Y_train) # Y_train: 2D binary array
# predict returns a 2D matrix
pred = clf.predict(X_test)
print(f1_score(Y_test, pred, average="micro"))
# alternatives:
# MultiLabelBinarizer to convert list-of-labels -> 2D matrix
# BinaryRelevance / ClassifierChains from scikit-multilearn
# BERT with sigmoid + BCEWithLogitsLoss (one head per label)评估指标
准确率在不平衡数据上会说谎——99% 负样本的垃圾邮件检测器通过总是预测'非垃圾'获得 99%。macro F1 平等对待每个类(最适合不平衡),micro F1 受多数类主导,weighted F1 是折中。部署前始终查看每类精确率/召回率以发现弱类。
from sklearn.metrics import (classification_report, confusion_matrix,
f1_score, precision_recall_fscore_support)
y_true = [0, 0, 1, 1, 2, 2]
y_pred = [0, 1, 1, 1, 2, 0]
# per-class precision / recall / F1
print(classification_report(y_true, y_pred, digits=3))
# F1 averaging strategies (multi-class):
# macro : average F1 over classes (treats all classes equally)
# micro : aggregate then compute (dominated by majority class)
# weighted: average weighted by class support
print("macro F1:", f1_score(y_true, y_pred, average="macro"))
print("micro F1:", f1_score(y_true, y_pred, average="micro"))
# confusion matrix
print(confusion_matrix(y_true, y_pred))
# ROC-AUC needs probabilities; for multi-class use roc_auc_score with multi_class='ovr'情感分析
VADER(基于词典)
VADER 是基于规则的,无需训练数据——非常适合社交媒体(处理表情符号、大写、'kinda'、'!')。compound 是推荐的一维分数。它在讽刺和上下文依赖情感上表现不佳。VADER 仅支持英文;其他语言请用 Transformer 模型或先翻译。
import nltk
from nltk.sentiment.vader import SentimentIntensityAnalyzer
nltk.download('vader_lexicon')
sia = SentimentIntensityAnalyzer()
for text in ["I love this!", "This is terrible :(",
"It's okay, not great."]:
scores = sia.polarity_scores(text)
print(f"{text:25} {scores}")
# {'neg': 0.0, 'neu': 0.308, 'pos': 0.692, 'compound': 0.636}
# {'neg': 0.679, 'neu': 0.321, 'pos': 0.0, 'compound': -0.476}
# {'neg': 0.323, 'neu': 0.677, 'pos': 0.0, 'compound': -0.226}
# compound: normalized score in [-1, 1]
# common thresholds: >0.05 positive, <-0.05 negative, else neutralTextBlob 情感分析
TextBlob 封装了基于 Pattern 的词典,一次调用同时给出极性和主观性。主观性便于过滤事实与观点文本。TextBlob 在社交媒体上不如 VADER 稳健(无表情符号处理)但更简单。两者都是基线——只要有标注数据,微调的 Transformer 就能超越它们。
from textblob import TextBlob # pip install textblob
for text in ["I love NLP.", "This is the worst experience.",
"The movie was okay."]:
blob = TextBlob(text)
print(f"{text:30} polarity={blob.sentiment.polarity:.2f} "
f"subjectivity={blob.sentiment.subjectivity:.2f}")
# I love NLP. polarity=0.50 subjectivity=0.60
# This is the worst experience. polarity=-0.60 subjectivity=0.75
# The movie was okay. polarity=0.00 subjectivity=0.00
# polarity: [-1, 1] negative to positive
# subjectivity: [0, 1] objective (fact) to subjective (opinion)
# also gives noun phrases, translation, etc.
print(blob.noun_phrases)基于 Transformer 的情感分析
从 Hub 获取的预训练情感模型无需训练数据即可给出生产级结果。零样本流水线对快速原型来说很神奇:传入任何候选标签,它通过 NLI 排序。零样本不如微调模型准确,但在标签未知或频繁变化时极有价值。
from transformers import pipeline
# zero-shot: pick any labels you want, no fine-tuning
clf = pipeline("sentiment-analysis",
model="distilbert-base-uncased-finetuned-sst-2-english")
print(clf(["I love this product!", "Worst purchase ever."]))
# [{'label': 'POSITIVE', 'score': 0.9998}, {'label': 'POSITIVE'...}]
# zero-shot classification: works on ANY label set
zero = pipeline("zero-shot-classification",
model="facebook/bart-large-mnli")
print(zero("The battery lasts only an hour.",
candidate_labels=["positive", "negative", "neutral"]))
# {'labels': ['negative', 'neutral', 'positive'], 'scores': [0.95, ...]}微调情感模型
在自己的标注数据上微调可超越任何现成情感模型,因为标签定义不同(5 星 vs 3 类 vs 基于方面)。使用小学习率(2e-5)、2-4 个 epoch,关注验证损失以防过拟合。'evaluate' 库捆绑了标准指标。用 trainer.save_model() 保存模型。
from transformers import (AutoTokenizer, AutoModelForSequenceClassification,
Trainer, TrainingArguments)
import evaluate
model_name = "distilbert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=2)
def tokenize(batch):
return tokenizer(batch["text"], truncation=True, padding="max_length")
train_ds = dataset["train"].map(tokenize, batched=True)
train_ds = train_ds.rename_column("label", "labels")
train_ds.set_format("torch")
accuracy = evaluate.load("accuracy")
def compute_metrics(eval_pred):
logits, labels = eval_pred
preds = logits.argmax(axis=-1)
return accuracy.compute(predictions=preds, references=labels)
args = TrainingArguments("sentiment", num_train_epochs=3,
per_device_train_batch_size=16,
eval_strategy="epoch", learning_rate=2e-5)
trainer = Trainer(model=model, args=args, train_dataset=train_ds,
eval_dataset=dataset["test"], compute_metrics=compute_metrics)
trainer.train()基于方面的情感分析
基于方面的情感分析(ABSA)将评论分解为按方面的意见——对产品反馈远比单一文档级分数有用。用 NLI 零样本可作为快速原型;生产环境中微调序列对分类器(方面, 文本) -> 情感,或使用 pyabsa 等专用库。带结构化提示的 LLM 也具竞争力。
# Aspect-based sentiment: sentiment TOWARD a specific aspect
# "The food was great but the service was slow."
# food -> positive, service -> negative
from transformers import pipeline
# approach 1: zero-shot on each aspect
zero = pipeline("zero-shot-classification", model="facebook/bart-large-mnli")
text = "The food was great but the service was slow."
for aspect in ["food", "service"]:
result = zero(text, candidate_labels=["positive", "negative", "neutral"])
print(f"{aspect}: {result['labels'][0]} ({result['scores'][0]:.2f})")
# approach 2: pyabsa library (specialized for ABSA)
# pip install pyabsa
# from pyabsa import AspectTermExtraction as ATE
# analyzer = ATE.AspectExtractor('english', auto_device=True)
# print(analyzer.predict(text))
# approach 3: prompt an LLM with aspect + text -> label主题模型
gensim LDA 主题模型
LDA(潜在狄利克雷分配)将每篇文档建模为主题的混合,每个主题建模为词的分布。gensim 的 LdaModel 是标准实现。预处理很重要:拟合前移除停用词、词形还原并过滤极端值(极常见/极稀有词)。num_topics 是关键超参数——用一致性来选择。
from gensim.corpora import Dictionary
from gensim.models import LdaModel
docs = [["cat", "dog", "pet"],
["dog", "bark", "pet"],
["python", "code", "program"],
["code", "bug", "python"]]
dictionary = Dictionary(docs)
corpus = [dictionary.doc2bow(d) for d in docs] # bag-of-words
lda = LdaModel(
corpus=corpus,
id2word=dictionary,
num_topics=2,
passes=10, # iterations over the corpus
random_state=42,
alpha="auto", # document-topic prior
eta="auto", # topic-word prior
)
# top words per topic
for topic_id, words in lda.print_topics(num_words=4):
print(f"Topic {topic_id}: {words}")sklearn LDA 主题模型
sklearn 的 LDA 与 CountVectorizer 流水线集成,但在大语料上比 gensim 慢且内存占用更多。使用 learning_method='online' 进行流式处理。components_ 给出主题-词分布;transform(X) 给出文档-主题分布。设置 random_state 以保证可复现——LDA 对初始化敏感。
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation
docs = ["cat dog pet", "dog bark pet", "python code program", "code bug python"]
vec = CountVectorizer(max_df=0.9, min_df=2, stop_words="english")
X = vec.fit_transform(docs)
lda = LatentDirichletAllocation(
n_components=3,
learning_method="batch", # 'online' for large corpora
max_iter=20,
random_state=42,
)
lda.fit(X)
# top words per topic
words = vec.get_feature_names_out()
for i, topic in enumerate(lda.components_):
top = [words[j] for j in topic.argsort()[:-6:-1]]
print(f"Topic {i}: {top}")NMF(非负矩阵分解)
NMF 将 TF-IDF 矩阵分解为非负的 W(文档-主题)和 H(主题-词)。它比 LDA 快,且在短文本上经常产生更连贯的主题,因为它不做 LDA 的概率假设。LDA 需要计数数据(TF-IDF 会破坏其生成假设);NMF 适用于 TF-IDF 并从中受益。
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.decomposition import NMF
docs = ["machine learning models", "deep learning networks",
"natural language processing", "cooking recipes food"]
vec = TfidfVectorizer(max_features=1000, stop_words="english")
X = vec.fit_transform(docs)
nmf = NMF(n_components=2, random_state=42, l1_ratio=0.5, alpha_W=0.1)
W = nmf.fit_transform(X) # document-topic
H = nmf.components_ # topic-word
words = vec.get_feature_names_out()
for i, topic in enumerate(H):
print(f"Topic {i}: {[words[j] for j in topic.argsort()[:-6:-1]]}")
# NMF works with TF-IDF (LDA needs counts); topics are often cleaner
# on short documents主题一致性与 K 值选择
一致性衡量每个主题 top 词之间的语义相关性——越高越好。c_v 是最可靠的指标。绘制一致性 vs k 并寻找'肘点'或峰值。没有真实 K,所以还需人工检查主题:如果两个主题近乎重复,减小 K。含通用词('get'、'make')的主题暗示 K 过大。
from gensim.models import CoherenceModel, LdaModel
from gensim.corpora import Dictionary
# compare LDA models with different numbers of topics
dictionary = Dictionary(docs)
corpus = [dictionary.doc2bow(d) for d in docs]
scores = []
for k in range(2, 11):
lda = LdaModel(corpus=corpus, id2word=dictionary,
num_topics=k, passes=10, random_state=42)
cm = CoherenceModel(model=lda, texts=docs,
dictionary=dictionary, coherence="c_v")
score = cm.get_coherence()
scores.append((k, score))
print(f"k={k}: coherence={score:.3f}")
# pick the k with highest coherence (or where scores plateau)
# c_v in [0,1]; >0.5 is usually interpretablepyLDAvis 可视化
pyLDAvis 是事实上的主题模型可视化工具:每个气泡是一个主题,按其流行度大小排列,按与其他主题的距离定位(重叠 = 相似主题)。右侧面板显示每个主题最相关的词,带 lambda 滑块——较低的 lambda 突出独特词而非仅仅是高频词。非常适合向利益相关者解释主题。
# pip install pyldavis
import pyLDAvis
import pyLDAvis.gensim_models as gensimvis
# (assuming lda, corpus, dictionary from above)
vis = gensimvis.prepare(lda, corpus, dictionary)
# save as standalone HTML
pyLDAvis.save_html(vis, "lda_vis.html")
# open in a browser: bubbles = topics, sized by prevalence,
# positioned by inter-topic distance (PCoA on topic-word distances)
#pyLDAvis.enable_notebook() # inline in Jupyter
#vis
# lambda slider: 1 = pure relevance by frequency,
# 0.6 = lift (rare-but-distinctive words rise)BERTopic(神经主题模型)
BERTopic 是现代神经流水线:句子嵌入(默认为 Transformer) -> UMAP 降维 -> HDBSCAN 聚类 -> c-TF-IDF 主题表示。它不需要 num_topics(HDBSCAN 自动发现)且处理短文本远好于 LDA。比 LDA 慢但产生更连贯的主题;为你的领域调整嵌入模型。
# pip install bertopic
from bertopic import BERTopic
from sklearn.datasets import fetch_20newsgroups
docs = fetch_20newsgroups(subset="all")["data"][:1000]
# BERTopic: embeddings -> dimensionality reduction -> clustering
topic_model = BERTopic(
language="english",
calculate_probabilities=True,
verbose=True,
)
topics, probs = topic_model.fit_transform(docs)
print(topic_model.get_topic_info().head())
print(topic_model.get_topic(0)) # top words for topic 0
# visualize
# topic_model.visualize_topics()
# topic_model.visualize_barchart()
# BERTopic finds the number of topics automatically (via HDBSCAN clusters)文本生成
N-gram 语言模型
N-gram 语言模型统计每个词跟在前 n-1 个词之后的频率,通过从该分布中采样来预测下一个词。Bigram 简单但上下文有限;trigram/4-gram 提升连贯性但需要更多数据。平滑(add-k、Kneser-Ney)处理未见上下文。适合自动补全和简单文本生成的基线。
import nltk
from nltk.util import ngrams
from collections import defaultdict, Counter
text = "the cat sat on the mat the dog sat on the log".split()
n = 2 # bigram model
# count bigram occurrences
model = defaultdict(Counter)
for w1, w2 in ngrams(text, n):
model[w1][w2] += 1
# convert counts to probabilities
for w1 in model:
total = sum(model[w1].values())
for w2 in model[w1]:
model[w1][w2] /= total
# generate next word given a context
import random
context = ("the",)
candidates = model[context[0]]
print(candidates) # Counter({'cat': 0.25, 'dog': 0.25, 'mat': 0.25, 'log': 0.25})
print(random.choices(list(candidates), weights=candidates.values())[0])马尔可夫链文本生成器
马尔可夫链是最简单的生成模型:状态 = 最后 N 个词,下一个词从观测到的转移中采样。它捕获局部风格(常见短语)但没有长程连贯性——输出几个词后就会漂移。增加阶数可改善连贯性但需要指数级更多的数据。适合风格化仿写;不适合生产级生成。
import random
from collections import defaultdict
def build_chain(text, order=2):
chain = defaultdict(list)
words = text.split()
for i in range(len(words) - order):
state = tuple(words[i:i + order])
chain[state].append(words[i + order])
return chain
def generate(chain, length=30, seed=None):
start = seed or random.choice(list(chain))
out = list(start)
state = start
for _ in range(length):
nxt = chain.get(state)
if not nxt:
break
word = random.choice(nxt)
out.append(word)
state = tuple(out[-len(state):])
return " ".join(out)
text = "the cat sat on the mat the cat ran the dog barked"
chain = build_chain(text, order=2)
print(generate(chain, length=20))GPT-2 文本生成
GPT-2(及后续模型)自回归地预测下一个词元,生成连贯的段落。generate() 方法统一了各种解码策略:do_sample=True 启用随机生成(创造性),False 给出贪婪/束搜索(确定性)。对于现代生成,偏好指令微调模型(GPT-NeoX、Llama、Mistral)而非基础 GPT-2。
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch
model_name = "gpt2" # 'gpt2-medium', 'gpt2-large' for bigger
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)
prompt = "In a world where machines could think,"
input_ids = tokenizer.encode(prompt, return_tensors="pt")
# greedy / beam / sampling all live in generate()
output = model.generate(
input_ids,
max_new_tokens=80,
do_sample=True,
top_k=50,
top_p=0.95,
temperature=0.9,
repetition_penalty=1.2,
pad_token_id=tokenizer.eos_token_id,
)
print(tokenizer.decode(output[0], skip_special_tokens=True))解码策略(temperature / top-k / top-p)
解码策略控制创造性与连贯性。贪婪/束搜索安全但乏味。Temperature 在 softmax 前缩放 logits(<1 更锐利,>1 更平坦)。Top-k 限制为 k 个最可能的词元;top-p(核采样)限制为覆盖概率 p 的最小集合——更具自适应性。常见配方:temperature=0.7、top_p=0.9、repetition_penalty=1.1。
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch
tokenizer = AutoTokenizer.from_pretrained("gpt2")
model = AutoModelForCausalLM.from_pretrained("gpt2")
input_ids = tokenizer.encode("Once upon a time", return_tensors="pt")
# 1. GREEDY: always pick the argmax token. Deterministic, repetitive.
# 2. BEAM SEARCH: keep top-k sequences; deterministic, more coherent
# but still bland.
beam = model.generate(input_ids, num_beams=5, max_new_tokens=50)
# 3. TEMPERATURE: sharpen (<1) or flatten (>1) the distribution.
# temp=0.1 = nearly greedy; temp=1.5 = chaotic
sampler = model.generate(input_ids, do_sample=True, temperature=0.7,
top_k=0, max_new_tokens=50)
# 4. TOP-K: sample only from the k most likely tokens (k=50 typical)
topk = model.generate(input_ids, do_sample=True, top_k=50, max_new_tokens=50)
# 5. TOP-P (nucleus): sample from the smallest set whose cumulative
# probability >= p (p=0.9 typical). Adapts to the distribution shape.
topp = model.generate(input_ids, do_sample=True, top_p=0.9, max_new_tokens=50)提示工程
基础模型需要精心提示:简单任务用零样本,模式学习用少样本(3-5 个示例),并给出明确的格式指令。指令微调模型(InstructGPT、Llama-2-Chat、Mistral-Instruct)更好地遵循自然语言指令且需要更少示例。思维链('逐步思考')提升数学和逻辑推理。系统性地迭代提示。
from transformers import pipeline
generator = pipeline("text-generation", model="gpt2")
# zero-shot: just ask
zero = generator("Translate English to French: cheese ->", max_new_tokens=5)
# few-shot: show examples in the prompt, then ask for the next
prompt = """Translate English to French:
cheese -> fromage
bread -> pain
water ->"""
few = generator(prompt, max_new_tokens=3)
# instruction prompt (works best with instruction-tuned models)
instruction = """Summarize the following text in one sentence:
Text: The cat sat on the mat. It was a lazy afternoon...
Summary:"""
summary = generator(instruction, max_new_tokens=30)
# tips:
# - be specific about format ("Output JSON", "Answer in one word")
# - provide examples for non-obvious tasks
# - chain-of-thought: "Let's think step by step."机器翻译
使用 googletrans 翻译
googletrans 封装了 Google 免费的网页翻译端点——适合原型开发但非官方且有速率限制。生产环境请使用官方 API(Google Cloud Translation、DeepL、Azure Translator)或通过 Hugging Face 自托管 MarianMT/NLLB。DeepL 通常为欧洲语言提供最流畅的翻译。
# pip install googletrans==4.0.0-rc1
from googletrans import Translator
translator = Translator()
# single sentence
result = translator.translate("Hello, world!", src="en", dest="fr")
print(result.text) # Bonjour le monde !
print(result.pronunciation)
# batch
results = translator.translate(
["Good morning", "Thank you"], src="en", dest="es")
for r in results:
print(r.text) # Buenos días / Gracias
# auto-detect source language
detected = translator.translate("Guten Tag")
print(detected.src) # 'de'
# caveat: googletrans uses the free web API and may rate-limit / break.
# For production, use official Google / DeepL / Azure APIs.MarianMT(Hugging Face)
MarianMT(Helsinki-NLP opus 模型)为约 100 种语言对提供免费、离线、质量不错的翻译。每对是一个单独的模型——精确选择你需要的。多语言翻译可用 Meta 的 NLLB-200,一个模型覆盖 200 种语言。质量落后于商业 API(Google/DeepL)但对许多用例足够,且可在单 GPU 上运行。
from transformers import MarianTokenizer, MarianMTModel
# many language pairs available: 'Helsinki-NLP/opus-mt-{src}-{tgt}'
model_name = "Helsinki-NLP/opus-mt-en-fr"
tokenizer = MarianTokenizer.from_pretrained(model_name)
model = MarianMTModel.from_pretrained(model_name)
texts = ["Hello, how are you?", "I love natural language processing."]
batch = tokenizer(texts, return_tensors="pt", padding=True, truncation=True)
generated = model.generate(**batch, max_length=128, num_beams=4)
print(tokenizer.batch_decode(generated, skip_special_tokens=True))
# ['Bonjour, comment allez-vous ?', "J'aime le traitement du langage naturel."]
# Helsinki-NLP shipped hundreds of language pairs; for many-to-many use
# 'facebook/nllb-200-distilled-600M' (200 languages, one model)BLEU 分数
BLEU 比较系统输出和参考翻译之间的 n-gram 重叠,并对短输出施加简洁惩罚。分数为 0-100;30+ 为合理,60+ 为高质量。BLEU 便宜且标准但不完美——它遗漏同义和重排。始终用多个参考评估,并补充 chrF 或 COMET 以获得更好的人类相关性。
from sacrebleu import corpus_bleu # pip install sacrebleu
refs = [["The cat is on the mat."]] # list of reference(s) per sentence
hyp = ["The cat is on the mat."] # system output
bleu = corpus_bleu(hyp, refs)
print(bleu.score) # 0-100 (0=worst, 100=perfect match)
# multiple references for one sentence
refs = [["The cat is on the mat.", "There is a cat on the mat."]]
hyp = ["A cat is on the mat."]
print(corpus_bleu(hyp, refs).score)
# BLEU = geometric mean of modified n-gram precision * brevity penalty
# - precision: how many n-grams in hyp appear in references
# - brevity penalty: penalizes too-short outputs
# caveats: BLEU ignores semantics, struggles on short sentences,
# correlates only moderately with human judgment翻译的分词处理
翻译模型使用 SentencePiece 或 BPE 分词器,可处理任何 Unicode(包括货币符号、破折号、emoji)而不会出现未知词元。词边界标记表示 SentencePiece 中的词首——去除它以重建表面文本。绝不要将原始空白分词文本喂给 MarianMT/NLLB 模型;始终通过其分词器获取匹配的输入 ID。
from transformers import AutoTokenizer
# translation models often use SentencePiece (BPE/unigram) tokenizers
tok = AutoTokenizer.from_pretrained("Helsinki-NLP/opus-mt-en-de")
text = "Don't go there — it's 5€!"
enc = tok(text)
print(tok.convert_ids_to_tokens(enc["input_ids"]))
# ['▁Don', "'", 't', '▁go', '▁there', '▁—', '▁it', "'", 's', '▁5', '€', '!', '</s>']
# the leading '▁' (U+2581) marks a word start in SentencePiece.
# Strip it when reconstructing words:
pieces = [p.replace("▁", " ") for p in tok.convert_ids_to_tokens(enc["input_ids"])]
print("".join(pieces).strip())
# always use the model's own tokenizer, never a generic one —
# vocabulary and subword rules differ across translation models回译(数据增强)
回译创建合成平行数据:将单语言目标文本翻译为源语言,然后与原始目标配对。这就是 Google、Facebook 等大规模 NMT 系统在平行语料稀缺时的训练方式。回译文本比真实平行数据噪声更大,但增加了多样性并提高了鲁棒性,尤其对低资源语言对。
from transformers import MarianMTModel, MarianTokenizer
# augment monolingual source data with back-translated paraphrases
src_model_name = "Helsinki-NLP/opus-mt-en-de" # EN -> DE
bt_model_name = "Helsinki-NLP/opus-mt-de-en" # DE -> EN (back)
mono_en = ["The weather is nice today.", "I am learning NLP."]
# forward: EN -> DE
fwd_tok = MarianTokenizer.from_pretrained(src_model_name)
fwd_model = MarianMTModel.from_pretrained(src_model_name)
de = fwd_model.generate(**fwd_tok(mono_en, return_tensors="pt", padding=True))
# back: DE -> EN (paraphrase of the original)
bt_tok = MarianTokenizer.from_pretrained(bt_model_name)
bt_model = MarianMTModel.from_pretrained(bt_model_name)
en_bt = bt_model.generate(**bt_tok(
bt_tok.batch_decode(de, skip_special_tokens=True),
return_tensors="pt", padding=True))
paraphrases = bt_tok.batch_decode(en_bt, skip_special_tokens=True)
print(paraphrases) # ~same meaning, different wording序列到序列
编码器-解码器基础
Seq2Seq 将变长输入序列映射到变长输出——翻译、摘要和许多其他任务的基础。经典的 RNN 编码器-解码器将整个输入压缩为固定上下文向量,这对长序列成为瓶颈。注意力(下一节)正是为解决此问题而发明的。
# Seq2Seq: an encoder maps the input sequence to a context vector,
# a decoder generates the output sequence from that context.
#
# input -> [Encoder] -> context -> [Decoder] -> output
#
# Classic architecture (Sutskever et al. 2014):
# - Encoder: RNN/LSTM/GRU processes input tokens, final hidden state
# becomes the context vector.
# - Decoder: another RNN initialized with the context, generates
# output tokens one at a time.
#
# Used for: translation, summarization, QA, parsing, code generation.
import torch
import torch.nn as nn
class Encoder(nn.Module):
def __init__(self, vocab_size, emb_dim, hid_dim):
super().__init__()
self.emb = nn.Embedding(vocab_size, emb_dim)
self.rnn = nn.LSTM(emb_dim, hid_dim)
def forward(self, src):
embedded = self.emb(src)
outputs, (hidden, cell) = self.rnn(embedded)
return hidden, cell # contextRNN/LSTM 序列到序列
解码器每次生成一个词元,将自己的前一个输出作为下一个输入(自回归)。用编码器的上下文初始化解码器的隐藏状态。贪婪解码(每步取 argmax)简单但可能陷入循环;束搜索保留 top-k 假设。固定上下文向量在长输入上挣扎——注意力解决了这个问题。
import torch
import torch.nn as nn
class Decoder(nn.Module):
def __init__(self, vocab_size, emb_dim, hid_dim):
super().__init__()
self.emb = nn.Embedding(vocab_size, emb_dim)
self.rnn = nn.LSTM(emb_dim, hid_dim)
self.fc = nn.Linear(hid_dim, vocab_size)
def forward(self, input_token, hidden, cell):
# input_token: (1,) the previous output token
emb = self.emb(input_token).unsqueeze(0) # (1, 1, emb_dim)
out, (hidden, cell) = self.rnn(emb, (hidden, cell))
logits = self.fc(out.squeeze(0)) # (1, vocab_size)
return logits, hidden, cell
# full generation loop (greedy decoding)
def generate(decoder, context_hidden, context_cell, sos_idx, eos_idx, max_len=20):
hidden, cell = context_hidden, context_cell
token = torch.tensor([sos_idx])
output = []
for _ in range(max_len):
logits, hidden, cell = decoder(token, hidden, cell)
token = logits.argmax(dim=-1)
if token.item() == eos_idx:
break
output.append(token.item())
return output教师强制
教师强制通过喂入真实前一个词而非模型自己的(通常错误的)预测来大幅加速训练。纯教师强制会导致'暴露偏差'——推理时模型从未见过自己的错误。计划采样(混合教师强制和自有预测)可缓解但增加复杂性;现代 Transformer 依赖并行的教师强制训练。
import torch
import torch.nn as nn
# During training, feed the GROUND TRUTH previous token (not the model's
# prediction) as the next input. This is 'teacher forcing'.
def train_step(encoder, decoder, src, trg, criterion, teacher_ratio=0.5):
# src: (src_len, batch), trg: (trg_len, batch)
hidden, cell = encoder(src)
batch_size = src.size(1)
trg_len = trg.size(0)
vocab_size = decoder.fc.out_features
outputs = torch.zeros(trg_len, batch_size, vocab_size)
input_token = trg[0, :] # <sos> token
for t in range(1, trg_len):
logits, hidden, cell = decoder(input_token, hidden, cell)
outputs[t] = logits
# teacher forcing: use real target as next input
teacher_force = torch.rand(1).item() < teacher_ratio
input_token = trg[t, :] if teacher_force else logits.argmax(dim=-1)
return criterion(outputs[1:].reshape(-1, vocab_size),
trg[1:].reshape(-1))注意力机制
注意力将解码器从单一固定上下文向量中解放出来:每步它计算所有编码器输出的加权平均,学习当前哪些源词重要。这修复了长序列瓶颈并大幅提升质量。Bahdanau(加性)和 Luong(乘性)是两种经典变体;两者都是 Transformer 自注意力的前身。
import torch
import torch.nn as nn
import torch.nn.functional as F
# Bahdanau-style additive attention:
# the decoder looks at ALL encoder outputs (not just the final state)
# and learns which to focus on at each step.
class Attention(nn.Module):
def __init__(self, hid_dim):
super().__init__()
self.attn = nn.Linear(hid_dim * 2, hid_dim)
self.v = nn.Linear(hid_dim, 1, bias=False)
def forward(self, hidden, encoder_outputs):
# hidden: (1, batch, hid), encoder_outputs: (src_len, batch, hid)
src_len = encoder_outputs.size(0)
hidden_exp = hidden.repeat(src_len, 1, 1)
energy = torch.tanh(self.attn(
torch.cat((hidden_exp, encoder_outputs), dim=2)))
attention = self.v(energy).squeeze(2) # (src_len, batch)
return F.softmax(attention, dim=0) # weights over src
# context = sum(attention_weights * encoder_outputs)
# then concat context with decoder input embedding束搜索
束搜索保留 k 个候选序列并扩展最有前景的,平衡探索与利用。宽度 4-8 是典型的。束搜索比贪婪产生更流畅的输出,但可能偏好通用翻译——length_penalty 和 no_repeat_ngram_size 可抵消。对于开放式生成,采样经常胜过束搜索;对于翻译/摘要,束搜索通常胜出。
# Greedy decoding picks the single most likely token each step.
# Beam search keeps the top-k candidate SEQUENCES, expanding each.
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
model = AutoModelForSeq2SeqLM.from_pretrained("Helsinki-NLP/opus-mt-en-fr")
tokenizer = AutoTokenizer.from_pretrained("Helsinki-NLP/opus-mt-en-fr")
input_ids = tokenizer("Hello, how are you?", return_tensors="pt").input_ids
# beam search: explore k hypotheses in parallel
output = model.generate(
input_ids,
num_beams=5, # beam width
max_length=50,
length_penalty=0.6, # <1 prefers shorter, >1 prefers longer
early_stopping=True,
)
print(tokenizer.decode(output[0], skip_special_tokens=True))
# trade-off: larger beam = better quality but slower (O(k * steps))
# and a tendency toward generic / shorter outputs (length penalty helps)注意力与 Transformer
自注意力直觉
自注意力是 Transformer 的核心:每个词元关注所有词元(包括自身),并按查询-键相似度加权聚合它们的值。这在单次操作中提供全局上下文,无需循环,允许大规模并行。1/sqrt(d_k) 缩放保持 softmax 梯度稳定。这一单一机制取代了 RNN 和卷积。
# Self-attention lets each token look at every other token in the
# sequence to build a context-aware representation.
#
# For token i: context_i = sum_j attention(i, j) * value_j
#
# attention(i, j) = softmax( q_i . k_j / sqrt(d) )
# q_i = query of token i (what am I looking for?)
# k_j = key of token j (what do I contain?)
# v_j = value of token j (what info do I pass on?)
#
# Each token is projected to Q, K, V by learned weight matrices.
import torch
import torch.nn.functional as F
import math
def self_attention(x, W_q, W_k, W_v):
# x: (seq_len, d_model)
Q, K, V = x @ W_q, x @ W_k, x @ W_v
d_k = K.size(-1)
scores = Q @ K.transpose(-2, -1) / math.sqrt(d_k)
weights = F.softmax(scores, dim=-1)
return weights @ V # (seq_len, d_model)多头注意力
多头注意力在输入的不同投影上并行运行多个注意力层,然后拼接。每个头学习不同的关系(句法、共指、语义...)。8-16 个头是典型的。更多头 = 更多容量但收益递减。mask 参数是因果(解码器)和填充掩码的应用方式。
import torch
import torch.nn as nn
import math
import torch.nn.functional as F
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, n_heads):
super().__init__()
assert d_model % n_heads == 0
self.d_k = d_model // n_heads
self.n_heads = n_heads
self.q = nn.Linear(d_model, d_model)
self.k = nn.Linear(d_model, d_model)
self.v = nn.Linear(d_model, d_model)
self.out = nn.Linear(d_model, d_model)
def forward(self, x, mask=None):
B, L, D = x.shape
# project to (B, n_heads, L, d_k)
Q = self.q(x).view(B, L, self.n_heads, self.d_k).transpose(1, 2)
K = self.k(x).view(B, L, self.n_heads, self.d_k).transpose(1, 2)
V = self.v(x).view(B, L, self.n_heads, self.d_k).transpose(1, 2)
scores = (Q @ K.transpose(-2, -1)) / math.sqrt(self.d_k)
if mask is not None:
scores = scores.masked_fill(mask == 0, float("-inf"))
attn = F.softmax(scores, dim=-1)
ctx = attn @ V # (B, h, L, d_k)
ctx = ctx.transpose(1, 2).contiguous().view(B, L, D)
return self.out(ctx)Transformer 架构
一个 Transformer 块 = 多头注意力 + 位置前馈网络,各自用残差连接和 LayerNorm 包裹。残差让梯度流经深层堆叠;LayerNorm 稳定训练。前馈(通常比 d_model 宽 4 倍)是大多数参数所在。堆叠 6-24 个块构成完整模型。内存按序列长度 O(L^2) 增长——长上下文挑战的根源。
# The full Transformer block (Vaswani et al. 2017):
#
# x -> MultiHeadAttention -> Add & LayerNorm -> FeedForward -> Add & LayerNorm
# (Add = residual connection: out = x + Sublayer(x))
#
# Encoder: stacks N of these blocks (N=6 in base, 12 in BERT-base).
# Decoder: same but with masked self-attention + cross-attention
# to the encoder output.
#
# Properties:
# - no recurrence -> fully parallelizable, trains much faster than RNNs
# - global receptive field from layer 1
# - position-agnostic -> needs positional encoding
# - quadratic memory in seq_len (O(L^2)) due to full attention
import torch.nn as nn
class TransformerBlock(nn.Module):
def __init__(self, d_model=512, n_heads=8, d_ff=2048, dropout=0.1):
super().__init__()
self.attn = nn.MultiheadAttention(d_model, n_heads, dropout=dropout,
batch_first=True)
self.ff = nn.Sequential(
nn.Linear(d_model, d_ff), nn.GELU(), nn.Linear(d_ff, d_model))
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.drop = nn.Dropout(dropout)
def forward(self, x, mask=None):
a, _ = self.attn(x, x, x, attn_mask=mask, need_weights=False)
x = self.norm1(x + self.drop(a))
f = self.ff(x)
x = self.norm2(x + self.drop(f))
return x位置编码
因为注意力将输入视为集合,Transformer 需要显式位置信号。原论文使用固定正弦(可外推超出训练长度)。BERT/GPT 使用学习的嵌入(更简单、略好,但不能外推)。现代 LLM 偏好 RoPE(旋转)或 ALiBi,它们能泛化到训练中未见过的更长上下文——对长文档任务至关重要。
import torch
import math
import torch.nn as nn
# Self-attention is permutation-invariant, so we ADD positional info
# to the embeddings so the model knows token order.
class SinusoidalPositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=5000):
super().__init__()
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1).float()
div_term = torch.exp(
torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
self.register_buffer("pe", pe.unsqueeze(0)) # (1, max_len, d_model)
def forward(self, x):
# x: (batch, seq_len, d_model)
return x + self.pe[:, :x.size(1)]
# alternatives:
# - learned positional embeddings (BERT, GPT): nn.Embedding(max_len, d_model)
# - relative positional encoding (T5, Transformer-XL)
# - RoPE (Rotary Position Embedding, used in LLaMA) -> handles long contextBERT vs GPT vs T5 对比
BERT(编码器)双向阅读文本——最适合理解任务。GPT(解码器)从左到右阅读——最适合生成。T5/BART(编码器-解码器)——最适合翻译和摘要等序列到序列任务。现代仅解码器 LLM(LLaMA、Mistral)通过指令调优缩小了理解任务的差距,但在数据有限时简单分类上小型 BERT 仍然胜出。
# Three Transformer families, three pretraining objectives:
#
# BERT (encoder-only, bidirectional):
# - Masked Language Modeling (predict [MASK] tokens)
# - Great for: classification, NER, QA (extractive), embeddings
# - example: bert-base-uncased, RoBERTa, DistilBERT, DeBERTa
#
# GPT (decoder-only, autoregressive):
# - Causal Language Modeling (predict next token)
# - Great for: generation, chat, few-shot, code
# - example: gpt2, gpt-neo, LLaMA, Mistral, Qwen
#
# T5 (encoder-decoder, text-to-text):
# - Span corruption (mask spans, predict them)
# - One model for EVERY task by framing as text->text
# - example: t5-base, flan-t5, BART (similar, denoising autoencoder)
from transformers import AutoModel
bert = AutoModel.from_pretrained("bert-base-uncased") # encoder
print(bert.config.model_type) # bert
# pick the family that matches your task:
# classification/NER -> encoder (BERT)
# generation -> decoder (GPT)
# translation/summarization -> encoder-decoder (T5/BART)问答系统
BERT 抽取式问答
抽取式问答将问答框架化为跨度预测:模型输出上下文中的起始和结束词元位置。在 SQuAD 上微调的 DistilBERT 是快速、准确的默认选择。当答案字面出现在上下文中时效果最好。对于生成式答案(改写、综合),使用 T5 等 seq2seq 模型或带检索的 LLM。
from transformers import pipeline
# Extractive QA: pick a SPAN of the context as the answer.
qa = pipeline("question-answering",
model="distilbert-base-cased-distilled-squad")
context = "Transformers were introduced in 2017. The model uses self-attention."
question = "When were transformers introduced?"
result = qa(question=question, context=context)
print(result)
# {'score': 0.97, 'start': 32, 'end': 36, 'answer': '2017'}
# start/end are character offsets into the context —
# you can highlight the answer in the original text:
print(context[:result['start']] + '[' + result['answer'] + ']' + context[result['end']:])生成式问答(LLM)
生成式问答产生自由形式的答案(非跨度),因此可以综合、改写或说'我不知道'。FLAN-T5 是小型、免费的指令微调模型,能很好地遵循问答提示。对于没有提供上下文的开放领域问题,你需要带有内部知识的大型 LLM 或检索增强生成(RAG)来提供新事实。
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch
model_name = "google/flan-t5-base" # instruction-tuned seq2seq
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
prompt = """context: The Eiffel Tower is located in Paris and was built in 1889.
question: Where is the Eiffel Tower?
answer:"""
input_ids = tokenizer(prompt, return_tensors="pt").input_ids
out = model.generate(input_ids, max_new_tokens=20)
print(tokenizer.decode(out[0], skip_special_tokens=True))
# 'Paris'
# for open-ended QA without a fixed context, use a large LLM
# (Llama-2-Chat, Mistral-Instruct) and prompt with the question directly检索增强生成(RAG)
RAG 使 LLM 答案扎根于检索到的证据——对 LLM 未见过的最新或私有数据至关重要。步骤:嵌入文档、按余弦相似度找到 top-k、将它们塞入提示、让模型生成。生产环境使用向量数据库(FAISS、Chroma、Qdrant)并分块长文档。RAG 减少幻觉并让你引用来源。
# RAG: retrieve relevant documents, then let an LLM answer
# using those documents as context. Combines search + generation.
from sentence_transformers import SentenceTransformer, util
# 1. Build a vector index of your documents
docs = ["Paris is the capital of France.",
"Python is a programming language.",
"The Eiffel Tower was built in 1889."]
encoder = SentenceTransformer("all-MiniLM-L6-v2")
doc_emb = encoder.encode(docs, convert_to_tensor=True, normalize_embeddings=True)
# 2. Retrieve top-k for a query
query = encoder.encode("What is the capital of France?",
convert_to_tensor=True, normalize_embeddings=True)
hits = util.semantic_search(query, doc_emb, top_k=2)[0]
context = "\n".join(docs[h['corpus_id']] for h in hits)
# 3. Generate answer conditioned on retrieved context
prompt = f"Context:\n{context}\n\nQuestion: What is the capital of France?\nAnswer:"
# -> feed to an LLM; it reads the context and answers 'Paris'
print(prompt)SQuAD 数据集
SQuAD 是抽取式问答的标杆基准——每个 QA 模型都报告 SQuAD F1。答案是跨度,所以模型输出起始/结束位置。SQuAD v2 添加了不可回答的问题以测试弃权。现代模型得分 >90 F1(在该指标上超人类),但 SQuAD 是单跳且英文——更难的情况用 Natural Questions、HotpotQA(多跳)或 TyDiQA(多语言)。
# SQuAD (Stanford Question Answering Dataset):
# - 100k+ questions on Wikipedia paragraphs
# - each answer is a SPAN in the paragraph
# - the standard benchmark for extractive QA
#
# Load with Hugging Face datasets:
from datasets import load_dataset
squad = load_dataset("squad")
print(squad)
# DatasetDict({ train: 87599 rows, validation: 10570 rows })
ex = squad["train"][0]
print(ex.keys())
# dict_keys(['id', 'title', 'context', 'question', 'answers'])
print(ex["question"]) # 'Which NFL team represented the AFC...'
print(ex["answers"]["text"]) # ['Denver Broncos'] (sometimes multiple)
# SQuAD v2 adds unanswerable questions (model must abstain)
squad_v2 = load_dataset("squad_v2")问答评估(F1 / EM)
SQuAD 评分在比较前规范化两个答案(小写、去除冠词/标点/多余空白)。EM 是二值的;F1 是词元级的,对改写更宽容。两者都忽略语义——'canine' vs 'dog' 得 0 分。对于生成式问答,使用语义指标(BERTScore、BARTScore)或人工评估。报告 EM 和 F1,在所有问题上(包括不可回答的)平均。
# Two standard metrics for extractive QA:
# Exact Match (EM): 1 if prediction == gold answer exactly, else 0
# F1: token-level F1 between prediction and gold (after normalization)
import re
from collections import Counter
def normalize(text):
# SQuAD normalization: lowercase, remove articles/punct/extra space
text = text.lower()
text = re.sub(r"\b(a|an|the)\b", " ", text)
text = " ".join(text.split())
return re.sub(r"[^\w\s]", "", text)
def f1_score(pred, gold):
p, g = normalize(pred).split(), normalize(gold).split()
common = Counter(p) & Counter(g)
n_common = sum(common.values())
if n_common == 0:
return 0.0
precision = n_common / len(p)
recall = n_common / len(g)
return 2 * precision * recall / (precision + recall)
def exact_match(pred, gold):
return float(normalize(pred) == normalize(gold))
print(f1_score("the cat", "a cat")) # 1.0 (articles removed)
print(exact_match("Paris.", "paris")) # 1.0 (case/punct normalized)文本摘要
抽取式摘要(TextRank)
TextRank 构建句子相似度图并运行 PageRank 以找到最中心(最具代表性)的句子。抽取方法快速、忠实(无幻觉),适合新闻。缺点:无法改写或合并跨句信息。生产就绪的实现可用 sumy、gensim 的 summarize 或 pytextrank。
# Extractive: pick the most important SENTENCES from the source
# (no new text generated). TextRank ranks sentences by graph centrality.
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
def textrank_summary(text, n=3):
sentences = text.split(". ") # naive sentence split
if len(sentences) <= n:
return text
# similarity graph: sentence vs sentence
tfidf = TfidfVectorizer(stop_words="english").fit_transform(sentences)
sim = cosine_similarity(tfidf)
np.fill_diagonal(sim, 0)
# PageRank-style power iteration
scores = np.ones(len(sentences)) / len(sentences)
for _ in range(50):
scores = 0.85 * sim.dot(scores) / sim.sum(axis=0) + 0.15 / len(sentences)
top = np.argsort(scores)[-n:]
return ". ".join(sentences[i] for i in sorted(top))
text = "Sentence one. Sentence two is longer. Third sentence here. Fourth."
print(textrank_summary(text, n=2))gensim 摘要
gensim 4 移除了其 summarize 模块;抽取式摘要请使用 sumy 库。sumy 提供多种算法(TextRank、LexRank、LSA、Luhn)——LexRank 在新闻上经常胜过 TextRank。所有抽取方法都有相同局限:无法改写。如需更高质量的摘要,切换到 BART 或 T5 等生成式模型。
from gensim.summarization import summarize # older gensim
# (removed in gensim 4.x; use 'sumy' or 'pytextrank' instead)
# modern alternative: sumy
# pip install sumy
from sumy.parsers.plaintext import PlaintextParser
from sumy.nlp.tokenizers import Tokenizer
from sumy.summarizers.text_rank import TextRankSummarizer
parser = PlaintextParser.from_string("Your long text here. Multiple "
"sentences. The summarizer picks the most important ones.",
Tokenizer("english"))
summarizer = TextRankSummarizer()
for sentence in summarizer(parser.document, sentences_count=2):
print(sentence)
# sumy also offers: LuhnSummarizer, LsaSummarizer, LexRankSummarizer,
# EdmundsonSummarizer (heuristic-based)生成式摘要(BART / T5)
BART-large-CNN 是标准的新闻预训练摘要器:在 CNN/DailyMail 上训练以生成 3 句摘要。生成式摘要比抽取式阅读体验更好(它们改写和压缩)但可能产生源文档中没有的事实幻觉——始终在你的领域上验证。Pegasus 为极短摘要调优;T5 是多任务且灵活的。
from transformers import pipeline
# Abstractive: GENERATE a summary (may use words not in the source)
summarizer = pipeline("summarization",
model="facebook/bart-large-cnn")
article = """\
Scientists at CERN have announced the discovery of a new subatomic
particle. The finding, published in Nature, confirms a prediction made
decades ago and opens new avenues for research into dark matter. The
team analyzed data from the Large Hadron Collider over three years."""
summary = summarizer(article, max_length=50, min_length=20,
do_sample=False, truncation=True)
print(summary[0]["summary_text"])
# alternative models:
# t5-base / flan-t5-base (text-to-text, multitask)
# google/pegasus-xsum (very short, one-sentence summaries)ROUGE 评估
ROUGE 衡量生成摘要和参考摘要之间的 n-gram 重叠——面向召回(你覆盖了参考吗?)。ROUGE-1 和 ROUGE-2 F1 是标题数字;ROUGE-L 使用最长公共子序列来捕获顺序。与 BLEU 一样,ROUGE 遗漏语义等价('canine' vs 'dog')。补充 BERTScore 或人工评估以进行生产质量检查。
# ROUGE (Recall-Oriented Understudy for Gisting Evaluation):
# standard metric for summarization. Compares n-gram overlap.
# pip install rouge-score
from rouge_score import rouge_scorer
scorer = rouge_scorer.RougeScorer(["rouge1", "rouge2", "rougeL"],
use_stemmer=True)
reference = "The cat sat on the mat."
hypothesis = "A cat is sitting on a mat."
scores = scorer.score(reference, hypothesis)
print(scores)
# rouge1: unigram overlap, rouge2: bigram, rougeL: longest common subsequence
# each gives (precision, recall, fmeasure)
# tips:
# - ROUGE-1/ROUGE-2 F1 are the most reported
# - ROUGE-L captures sentence-level order via LCS
# - use multiple references for a fairer comparison
# - ROUGE correlates with human judgment but ignores semantics长文档摘要
标准 BART/T5 输入上限为 512-1024 个词元。对于长文档使用长上下文模型(LongT5、Longformer-Encoder-Decoder、BigBird),它们将近线性地扩展注意力。Map-reduce 分块(分别摘要每块,再摘要摘要)是稳健的回退方案,且让你按节控制。层次化摘要为报告和会议保留结构。
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
import torch
# Problem: most summarizers have a 512/1024-token input limit.
# Long documents (papers, meetings) need special handling.
model_name = "google/long-t5-tglobal-base" # handles long inputs
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
# option 1: use a long-context model (LongT5, LED, BigBird)
inputs = tokenizer(long_text, return_tensors="pt", truncation=True,
max_length=4096)
out = model.generate(**inputs, max_length=256)
# option 2: chunk + summarize + combine (map-reduce)
def chunk_text(text, max_tokens=512):
# split into overlapping windows, summarize each, then summarize
# the concatenation of partial summaries
pass
# option 3: hierarchical attention (summarize sections, then summarize
# section summaries)Hugging Face Transformers
Pipeline API
pipeline() 是最简单的入口点:选一个任务、获取模型、一行运行推理。它处理分词、模型分发和后处理。用任何 Hub 名称覆盖 'model' 以替换架构。适合原型开发;生产环境你会想要对批处理、设备放置和分词的显式控制以提速。
from transformers import pipeline
# one-line inference for common tasks (downloads a default model)
clf = pipeline("sentiment-analysis")
print(clf("I love transformers!"))
qa = pipeline("question-answering")
print(qa(question="Who built BERT?", context="BERT was built by Google."))
ner = pipeline("ner", aggregation_strategy="simple")
print(ner("Barack Obama was born in Hawaii."))
summarizer = pipeline("summarization")
print(summarizer("Long text...", max_length=50))
generator = pipeline("text-generation", model="gpt2")
print(generator("Once upon a time", max_new_tokens=30))
# tasks available: sentiment-analysis, ner, question-answering,
# summarization, translation, text-generation, zero-shot-classification,
# fill-mask, feature-extraction, text2text-generation, image-classification...AutoModel 与 AutoTokenizer
AutoModel 加载基础编码器(输出隐藏状态);特定任务的 Auto 类添加正确的预测头(分类、NER、QA、生成)。AutoTokenizer 选择匹配的分词器。用于微调或自定义推理流水线。每个 'AutoModelFor*' 都内置了正确的损失函数——只需将 labels 传给 forward() 即可获得损失。
from transformers import AutoTokenizer, AutoModel, AutoModelForSequenceClassification
import torch
model_name = "bert-base-uncased"
# Auto* classes pick the right implementation from the model's config
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name) # base encoder
clf_model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=2)
inputs = tokenizer(["Hello", "World"], padding=True, return_tensors="pt")
with torch.no_grad():
embeddings = model(**inputs).last_hidden_state # (batch, seq, 768)
# task-specific Auto classes:
# AutoModelForTokenClassification (NER)
# AutoModelForQuestionAnswering (QA)
# AutoModelForCausalLM (GPT-style generation)
# AutoModelForSeq2SeqLM (T5/BART translation, summarization)
# AutoModelForMaskedLM (BERT-style MLM)使用 Trainer 微调
Trainer 处理训练循环(批处理、梯度累积、混合精度、评估、检查点),使你编写最少的样板代码。传入 TrainingArguments 控制 LR、批大小、epoch、评估/保存策略。load_best_model_at_end=True 保留最佳检查点。完全控制(自定义损失、多任务)时仍可写纯 PyTorch 循环——Trainer 是便利,不是约束。
from transformers import (AutoTokenizer, AutoModelForSequenceClassification,
TrainingArguments, Trainer)
from datasets import load_dataset
import evaluate
model_name = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=2)
dataset = load_dataset("imdb")
def tokenize(batch):
return tokenizer(batch["text"], truncation=True, padding="max_length", max_length=256)
train_ds = dataset["train"].map(tokenize, batched=True).rename_column("label", "labels")
train_ds.set_format("torch", columns=["input_ids", "attention_mask", "labels"])
accuracy = evaluate.load("accuracy")
def compute_metrics(eval_pred):
logits, labels = eval_pred
return accuracy.compute(predictions=logits.argmax(-1), references=labels)
args = TrainingArguments(
output_dir="imdb-bert",
eval_strategy="epoch",
save_strategy="epoch",
learning_rate=2e-5,
per_device_train_batch_size=16,
num_train_epochs=3,
load_best_model_at_end=True,
)
trainer = Trainer(model=model, args=args,
train_dataset=train_ds,
eval_dataset=dataset["test"].map(tokenize, batched=True),
compute_metrics=compute_metrics)
trainer.train()Datasets 库
datasets 为数千个 NLP 数据集提供统一 API,具有智能缓存、内存映射(大数据无 OOM)和流式模式。map(batched=True) 用多进程并行应用预处理。内置方法(filter、shuffle、train_test_split)镜像 pandas/sklearn。Hub 还通过 'evaluate' 库托管指标。
from datasets import load_dataset, Dataset
# thousands of datasets, one API, streaming for huge ones
squad = load_dataset("squad", split="train[:1%]")
print(squad.column_names) # ['id', 'title', 'context', 'question', 'answers']
# map over examples (batched for speed)
def preprocess(batch):
return tokenizer(batch["context"], truncation=True)
squad = squad.map(preprocess, batched=True)
# filter / shuffle / select
squad = squad.filter(lambda ex: len(ex["context"]) > 100).shuffle(seed=42)
# create your own from a dict or CSV/JSON
ds = Dataset.from_dict({"text": ["a", "b"], "label": [0, 1]})
ds = Dataset.from_csv("data.csv")
# stream a huge dataset without downloading fully
wiki = load_dataset("wikipedia", "20220301.en", streaming=True)
for ex in wiki: break # first example onlyTokenizers 库
tokenizers 库(Rust 核心、Python 绑定)让你快速训练自定义 BPE/WordPiece/Unigram 分词器——纯 Python 需数小时,这里只需数分钟。Normalizer 处理 Unicode(NFD、小写、去重音)。Decoder 从 ID 重建表面文本。将结果包装为 PreTrainedTokenizerFast 以插入 transformers Trainer。镜像模型的精确分词以避免静默精度损失。
from tokenizers import Tokenizer
from tokenizers.models import BPE
from tokenizers.trainers import BpeTrainer
from tokenizers.pre_tokenizers import Whitespace, Metaspace
from tokenizers.decoders import Metaspace as MetaspaceDecoder
from tokenizers.normalizers import NFD, Lowercase, StripAccents, Sequence
# build a tokenizer from scratch (fast, Rust-backed)
tokenizer = Tokenizer(BPE(unk_token="[UNK]"))
tokenizer.normalizer = Sequence([NFD(), Lowercase(), StripAccents()])
tokenizer.pre_tokenizer = Whitespace()
trainer = BpeTrainer(vocab_size=30000,
special_tokens=["[UNK]", "[PAD]", "[CLS]", "[SEP]", "[MASK]"])
tokenizer.train(files=["corpus.txt"], trainer=trainer)
# encode/decode
enc = tokenizer.encode("Hello, world!")
print(enc.ids, enc.tokens)
print(tokenizer.decode(enc.ids))
# save / load
tokenizer.save("my_tokenizer.json")
tok = Tokenizer.from_file("my_tokenizer.json")
# use with transformers: wrap as PreTrainedTokenizerFast模型中心与分享
Hub 是生态系统的核心:预训练模型、数据集和演示汇聚一处。用 trainer.push_to_hub() 推送你微调的模型,让队友(或未来的你)按名加载。添加模型卡描述训练数据、预期用途和局限——这是良好实践,且越来越成为负责任 AI 的要求。私有仓库需要令牌;通过组织账户分享。
# The Hugging Face Hub hosts 100k+ models and datasets.
# Browse: https://huggingface.co/models
# load any model by its repo id
from transformers import AutoModel, AutoTokenizer
model = AutoModel.from_pretrained("bert-base-uncased")
# private/org models need an access token:
# AutoModel.from_pretrained("org/private-model", token="hf_xxx")
# share your own fine-tuned model
from huggingface_hub import HfApi
# 1. huggingface-cli login (one-time, paste a token)
# 2. push from a Trainer:
trainer.push_to_hub("my-imdb-bert")
# 3. or push manually:
# api = HfApi()
# api.create_repo(repo_id="my-imdb-bert", repo_type="model")
# api.upload_folder(folder_path="imdb-bert", repo_id="my-imdb-bert")
# now anyone (or your team) can load it:
# AutoModel.from_pretrained("your-username/my-imdb-bert")相关 NLP 代码片段
Copy-paste ready code for common tasks.
Tokenization
Tokenize text with NLTK, spaCy, and regex.
Stopwords
Remove common words with NLTK and spaCy.
Stemming and Lemmatization
Reduce words to roots with Porter, Snowball, and lemmatizers.
TF-IDF
Vectorize text with TF-IDF and n-grams.
Word2Vec
Train and use word embeddings with gensim.
Named Entity Recognition
Extract entities with spaCy and transformers.
Sentiment Analysis
Score text polarity with VADER and transformers.
Text Classification
Train a TF-IDF + LogisticRegression text classifier.
这篇内容对您有帮助吗?