Skip to content

NLP 速查表

用于文本分析的自然语言处理技术与工具。

01

入门

NLTK 与 spaCy 安装

NLTK 适合教学与研究;spaCy 面向生产环境且速度快。spaCy 模型返回 Doc 对象,包含分词文本、词性标签、依存关系等。用 'python -m spacy download <model>' 安装语言模型——en_core_web_sm 是小型英文模型。

nlp
# 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() 快得多。

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' 中的现代数据集。

nlp
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 是最简单的频率统计工具。

nlp
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 和词性标注不要小写化。

nlp
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()
02

分词

NLTK 词与句子分词

NLTK 的 punkt 分词器使用训练好的模型拆分句子,能处理 'Mr.' 等缩写。word_tokenize 采用 Treebank 规范,会拆分缩写('don't' -> 'do' + "n't")。选择与下游模型训练方式匹配的分词器——不匹配的分词会悄悄降低性能。

nlp
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 规则允许你调整拆分方式而无需重写整个流水线。

nlp
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 是最简单的拆分,但标点会粘在词上,通常需要二次清理。

nlp
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。在自己的语料上训练以匹配目标领域。

nlp
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 而非完全重写;它们能与流水线的其他部分干净组合。

nlp
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 以避免长输入报错。

nlp
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))
03

词性标注

NLTK 词性标注

默认的感知器标注器使用 Penn Treebank 标签集(NN=名词、VB=动词、JJ=形容词、PRP=代词...)。它是上下文敏感的——'permit' 作为动词(VB)还是名词(NN)由周围词消歧。词性标签是廉价特征,能改善 NER、分块和基于规则的抽取。

nlp
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 标签为英语特定规则提供更多细节。

nlp
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 / 总数)是简单的文本风格特征。

nlp
# 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 小说)的标签分布可揭示词性使用的风格差异。

nlp
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 捕获数字、日期和大小写模式。

nlp
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)
04

命名实体识别

spaCy 命名实体识别

spaCy 的预训练 NER 识别 ORG、PERSON、GPE(地缘政治实体)、DATE、MONEY 等。doc.ents 是与分词器对齐的跨度。displacy.render/serve 提供漂亮的 HTML 可视化。在一般新闻文本上准确率不错;对专业领域(医疗、法律)需要微调。

nlp
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 browser

NLTK 命名实体分块

NLTK 的 ne_chunk 结合词性标注和基于语法的分块器。输出是一棵树,命名实体是标记为 PERSON、ORGANIZATION、GPE 等的子树。比 spaCy 慢且准确率较低,但适合学习分块概念和基于规则的扩展。

nlp
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),训练自定义模型或使用外部知识库。

nlp
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 进行早停。

nlp
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)。

nlp
# 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())
05

词干提取与词形还原

Porter 词干提取器

PorterStemmer 应用固定序列的后缀剥离规则。它快速且确定性强,但会产生非词('easili'、'studi')且忽略上下文——'ran' 保持 'ran' 因为没有后缀可剥离。适合搜索引擎和主题模型等注重词干一致性而非正确性的场景。

nlp
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 是实用的默认选择;如需更高质量的结果,切换到词形还原器。

nlp
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"))   # mang

WordNet 词形还原器

词形还原返回真实的词典词('better' -> 'good'、'mice' -> 'mouse'),通过查找 WordNet 实现。它需要词性来消歧('running' 是名词还是动词)。当下游特征(BoW、TF-IDF)受益于规范形式时,词性标注的开销是值得的。由于词典查找,比词干提取慢。

nlp
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' 合并为一个特征,减小词表大小。对于多语言模型,词形还原质量各异;请查看模型的准确率报告。

nlp
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."]]

词干提取与词形还原对比

词干提取更快但会产生非词,且对某些用途合并过于激进。词形还原返回有效词并尊重词性,代价是词典查找。对于词袋特征,两者都能减小词表;对于面向用户的输出(如关键词列表),始终用词形还原。对于大规模搜索,词干提取是务实的选择。

nlp
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)
06

停用词

NLTK 停用词

NLTK 为每种语言提供紧凑的停用词列表(英文约 180 个词)。使用 set 实现 O(1) 查找。检查前始终小写化,因为列表是小写的。过滤会缩小词表并移除低信息量词——对主题模型和搜索索引有用,但对情感分析有害("not good" 会丢失否定)。

nlp
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,可在运行时按流水线编辑。

nlp
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 在实体提取前绝不要移除停用词。

nlp
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')。挖掘你的语料库中频率极高、信息量极低的词并添加它们。反之,移除你的任务关心的词(立场检测中的否定词、情态动词)。保存修改后的流水线使自定义列表随模型一起传输。

nlp
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)在包含停用词的自然文本上训练——预先剥离会降低性能,因为模型依赖功能词来理解语法。停用词移除是浅层模型的特征工程技巧;两种方式都评估,保留在验证指标上得分更高的那种。

nlp
# 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.
07

词袋模型与向量化

CountVectorizer 基础

CountVectorizer 将字符串列表转为稀疏的文档-词矩阵。fit_transform 一步完成学习词表和转换;transform 在新数据上复用已学词表(关键——绝不在测试数据上重新 fit)。矩阵是稀疏的以高效处理大词表。默认 token 模式 \b\w\w+\b 会丢弃单字符词元。

nlp
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 很少有回报;如需更长上下文,切换到嵌入。

nlp
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 times

HashingVectorizer

HashingVectorizer 是无状态的——它将每个词元哈希到一列索引,因此没有词表需要存储。适合流式或内存受限场景。代价是:你无法检查某列代表哪个词,且冲突会合并特征。设置 alternate_sign=False 以避免符号翻转在线性模型中抵消计数。

nlp
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 按长度或字符类过滤。固定词表在需要跨运行保持列稳定时很方便。

nlp
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 用于列切片。

nlp
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]
08

TF-IDF

TfidfVectorizer 基础

TF-IDF 降低出现在许多文档中的词的权重(低区分力),提高稀有且有信息量的词的权重。TfidfVectorizer 结合了 CountVectorizer + TfidfTransformer 并进行 L2 行归一化,使文档长度不占主导。它是线性模型文本分类的默认特征。

nlp
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。

nlp
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_ 可查看学习到的全局权重。这是训练分类器前很好的健全性检查。

nlp
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 超越它。

nlp
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)。

nlp
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
09

词嵌入(Word2Vec / GloVe / FastText)

gensim Word2Vec

Word2Vec 通过预测上下文(skip-gram)或从上下文预测词(CBOW)来学习密集向量。skip-gram(sg=1)在小数据和稀有词上效果更好;CBOW 更快。min_count 过滤稀有词。真实模型需要数百万词元——在 Wikipedia 上训练或对大多数应用使用预训练向量。

nlp
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 的标准。当有更多数据时可保存并恢复训练。部署前用类比和相似度任务评估。

nlp
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 通常更实用。

nlp
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 处理对大多数真实文本是值得的。

nlp
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。

nlp
# 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 更好地保留结构。

nlp
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()
10

上下文嵌入(BERT)

Hugging Face Transformers 基础

AutoTokenizer/AutoModel 从 Hugging Face Hub 上的模型名自动选择正确的类——无需知道是 BERT、RoBERTa 还是 DistilBERT。last_hidden_state 每个词元有一个 768 维向量;pooler_output 是 [CLS] 表示。对于句子嵌入,单独的 [CLS] 通常较弱——改用 sentence-transformer。

nlp
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 维上下文表示。

nlp
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 维、快速,且与大模型竞争。非常适合语义搜索、聚类和去重。

nlp
# 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 等词元级任务仍然有效。

nlp
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 大规模运行,静态嵌入仍然有竞争力——根据任务和预算选择。

nlp
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
11

文本分类

sklearn 文本流水线

这个 TfidfVectorizer + LogisticRegression 流水线是标准的文本分类基线——快速、可解释,没有深度学习难以超越。Pipeline 封装预处理 + 模型,使相同的转换在预测时应用。class_weight='balanced' 有助于不平衡类别。始终报告每类精确率/召回率,而非仅准确率。

nlp
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。

nlp
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 附近的值通常有效。

nlp
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 classifier

BERT 微调分类

在分类头上微调预训练 BERT/DistilBERT,当你有数千标注样本时可获得最先进结果。DistilBERT 比 BERT 小约 40% 且快,质量约 95%——很好的默认选择。使用低学习率(2e-5)和 2-4 个 epoch;更多 epoch 会过拟合。Trainer 处理批处理、混合精度和日志。

nlp
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。

nlp
# 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 是折中。部署前始终查看每类精确率/召回率以发现弱类。

nlp
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'
12

情感分析

VADER(基于词典)

VADER 是基于规则的,无需训练数据——非常适合社交媒体(处理表情符号、大写、'kinda'、'!')。compound 是推荐的一维分数。它在讽刺和上下文依赖情感上表现不佳。VADER 仅支持英文;其他语言请用 Transformer 模型或先翻译。

nlp
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 neutral

TextBlob 情感分析

TextBlob 封装了基于 Pattern 的词典,一次调用同时给出极性和主观性。主观性便于过滤事实与观点文本。TextBlob 在社交媒体上不如 VADER 稳健(无表情符号处理)但更简单。两者都是基线——只要有标注数据,微调的 Transformer 就能超越它们。

nlp
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 排序。零样本不如微调模型准确,但在标签未知或频繁变化时极有价值。

nlp
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() 保存模型。

nlp
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 也具竞争力。

nlp
# 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
13

主题模型

gensim LDA 主题模型

LDA(潜在狄利克雷分配)将每篇文档建模为主题的混合,每个主题建模为词的分布。gensim 的 LdaModel 是标准实现。预处理很重要:拟合前移除停用词、词形还原并过滤极端值(极常见/极稀有词)。num_topics 是关键超参数——用一致性来选择。

nlp
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 对初始化敏感。

nlp
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 并从中受益。

nlp
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 过大。

nlp
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 interpretable

pyLDAvis 可视化

pyLDAvis 是事实上的主题模型可视化工具:每个气泡是一个主题,按其流行度大小排列,按与其他主题的距离定位(重叠 = 相似主题)。右侧面板显示每个主题最相关的词,带 lambda 滑块——较低的 lambda 突出独特词而非仅仅是高频词。非常适合向利益相关者解释主题。

nlp
# 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 慢但产生更连贯的主题;为你的领域调整嵌入模型。

nlp
# 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)
14

文本生成

N-gram 语言模型

N-gram 语言模型统计每个词跟在前 n-1 个词之后的频率,通过从该分布中采样来预测下一个词。Bigram 简单但上下文有限;trigram/4-gram 提升连贯性但需要更多数据。平滑(add-k、Kneser-Ney)处理未见上下文。适合自动补全和简单文本生成的基线。

nlp
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 个词,下一个词从观测到的转移中采样。它捕获局部风格(常见短语)但没有长程连贯性——输出几个词后就会漂移。增加阶数可改善连贯性但需要指数级更多的数据。适合风格化仿写;不适合生产级生成。

nlp
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。

nlp
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。

nlp
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)更好地遵循自然语言指令且需要更少示例。思维链('逐步思考')提升数学和逻辑推理。系统性地迭代提示。

nlp
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."
15

机器翻译

使用 googletrans 翻译

googletrans 封装了 Google 免费的网页翻译端点——适合原型开发但非官方且有速率限制。生产环境请使用官方 API(Google Cloud Translation、DeepL、Azure Translator)或通过 Hugging Face 自托管 MarianMT/NLLB。DeepL 通常为欧洲语言提供最流畅的翻译。

nlp
# 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 上运行。

nlp
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 以获得更好的人类相关性。

nlp
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。

nlp
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 系统在平行语料稀缺时的训练方式。回译文本比真实平行数据噪声更大,但增加了多样性并提高了鲁棒性,尤其对低资源语言对。

nlp
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
16

序列到序列

编码器-解码器基础

Seq2Seq 将变长输入序列映射到变长输出——翻译、摘要和许多其他任务的基础。经典的 RNN 编码器-解码器将整个输入压缩为固定上下文向量,这对长序列成为瓶颈。注意力(下一节)正是为解决此问题而发明的。

nlp
# 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   # context

RNN/LSTM 序列到序列

解码器每次生成一个词元,将自己的前一个输出作为下一个输入(自回归)。用编码器的上下文初始化解码器的隐藏状态。贪婪解码(每步取 argmax)简单但可能陷入循环;束搜索保留 top-k 假设。固定上下文向量在长输入上挣扎——注意力解决了这个问题。

nlp
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 依赖并行的教师强制训练。

nlp
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 自注意力的前身。

nlp
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 可抵消。对于开放式生成,采样经常胜过束搜索;对于翻译/摘要,束搜索通常胜出。

nlp
# 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)
17

注意力与 Transformer

自注意力直觉

自注意力是 Transformer 的核心:每个词元关注所有词元(包括自身),并按查询-键相似度加权聚合它们的值。这在单次操作中提供全局上下文,无需循环,允许大规模并行。1/sqrt(d_k) 缩放保持 softmax 梯度稳定。这一单一机制取代了 RNN 和卷积。

nlp
# 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 参数是因果(解码器)和填充掩码的应用方式。

nlp
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) 增长——长上下文挑战的根源。

nlp
# 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,它们能泛化到训练中未见过的更长上下文——对长文档任务至关重要。

nlp
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 context

BERT vs GPT vs T5 对比

BERT(编码器)双向阅读文本——最适合理解任务。GPT(解码器)从左到右阅读——最适合生成。T5/BART(编码器-解码器)——最适合翻译和摘要等序列到序列任务。现代仅解码器 LLM(LLaMA、Mistral)通过指令调优缩小了理解任务的差距,但在数据有限时简单分类上小型 BERT 仍然胜出。

nlp
# 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)
18

问答系统

BERT 抽取式问答

抽取式问答将问答框架化为跨度预测:模型输出上下文中的起始和结束词元位置。在 SQuAD 上微调的 DistilBERT 是快速、准确的默认选择。当答案字面出现在上下文中时效果最好。对于生成式答案(改写、综合),使用 T5 等 seq2seq 模型或带检索的 LLM。

nlp
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)来提供新事实。

nlp
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 减少幻觉并让你引用来源。

nlp
# 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(多语言)。

nlp
# 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,在所有问题上(包括不可回答的)平均。

nlp
# 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)
19

文本摘要

抽取式摘要(TextRank)

TextRank 构建句子相似度图并运行 PageRank 以找到最中心(最具代表性)的句子。抽取方法快速、忠实(无幻觉),适合新闻。缺点:无法改写或合并跨句信息。生产就绪的实现可用 sumy、gensim 的 summarize 或 pytextrank。

nlp
# 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 等生成式模型。

nlp
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 是多任务且灵活的。

nlp
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 或人工评估以进行生产质量检查。

nlp
# 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 分块(分别摘要每块,再摘要摘要)是稳健的回退方案,且让你按节控制。层次化摘要为报告和会议保留结构。

nlp
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)
20

Hugging Face Transformers

Pipeline API

pipeline() 是最简单的入口点:选一个任务、获取模型、一行运行推理。它处理分词、模型分发和后处理。用任何 Hub 名称覆盖 'model' 以替换架构。适合原型开发;生产环境你会想要对批处理、设备放置和分词的显式控制以提速。

nlp
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() 即可获得损失。

nlp
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 是便利,不是约束。

nlp
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' 库托管指标。

nlp
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 only

Tokenizers 库

tokenizers 库(Rust 核心、Python 绑定)让你快速训练自定义 BPE/WordPiece/Unigram 分词器——纯 Python 需数小时,这里只需数分钟。Normalizer 处理 Unicode(NFD、小写、去重音)。Decoder 从 ID 重建表面文本。将结果包装为 PreTrainedTokenizerFast 以插入 transformers Trainer。镜像模型的精确分词以避免静默精度损失。

nlp
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 的要求。私有仓库需要令牌;通过组织账户分享。

nlp
# 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")

这篇内容对您有帮助吗?

学习路径

从零开始学习

通过结构化课程从头学习这个语言。