NLP 进阶技术
我们其实每天都在与 NLP 打交道,用手机输入法打字时的联想词、邮件应用自动过滤的垃圾邮件、翻译软件帮你看懂的外文网站——这些都是自然语言处理(Natural Language Processing,NLP)的应用。
在 ChatGPT 出现之前,NLP 已经发展了几十年,但技术门槛很高。你需要了解分词、词性标注、句法分析、语义角色标注等一长串概念,还要手工设计特征,才能让机器"懂"一点语言。
今天,大语言模型让这一切变得简单。但理解 NLP 的技术脉络,能让你更透彻地理解为什么大模型能做到这些事,以及它的局限性在哪里。
本模块将带你从词向量开始,一路走到今天的大语言模型,建立完整的 NLP 知识体系。

学习路径:词向量 → 预训练模型 → BERT/GPT/T5 三大范式 → 下游任务(分类、NER、翻译、摘要)。每一步都有对应的代码示例,帮助你把理论落地。
预训练语言模型演进史
NLP 的发展可以清晰地划分为几个阶段,每个阶段都有标志性的技术突破。
Word2Vec:词向量的革命
在 2013 年之前,计算机处理文字的方式很原始。
通常用"独热编码"(One-Hot Encoding):每个词对应一个超长的向量,只有一个位置是 1,其他都是 0。比如词表有 10 万个词,每个词就是一个 10 万维的向量。
这种方式的问题显而易见:向量里没有语义信息,"猫"和"狗"的距离,跟"猫"和"桌子"的距离一样远。
Word2Vec 的核心思想是:一个词的含义,由它周围的词来定义。
实例
# Word2Vec 基本概念演示
# 使用 gensim 库训练一个简单的词向量模型
# ============================================
# 首先安装 gensim:pip install gensim
from gensim.models import Word2Vec
import numpy as np
# 准备训练数据:一些简单的句子
sentences = [
["我", "喜欢", "吃", "苹果"],
["我", "喜欢", "吃", "香蕉"],
["猫", "喜欢", "吃", "鱼"],
["狗", "喜欢", "吃", "肉"],
["苹果", "是", "一种", "水果"],
["香蕉", "是", "一种", "水果"],
["猫", "是", "一种", "动物"],
["狗", "是", "一种", "动物"],
["RUNOOB", "是", "一个", "编程", "网站"],
["学习", "编程", "去", "RUNOOB"],
]
# 训练 Word2Vec 模型
# vector_size: 词向量的维度
# window: 上下文窗口大小(看前后几个词)
# min_count: 忽略出现次数少于这个值的词
# workers: 并行训练的线程数
model = Word2Vec(
sentences=sentences,
vector_size=50, # 每个词用 50 维向量表示
window=3, # 看前后 3 个词
min_count=1, # 所有词都保留
workers=4,
epochs=100 # 训练 100 轮
)
# 获取词向量
apple_vector = model.wv["苹果"]
print(f"'苹果' 的词向量(前 10 维):{apple_vector[:10]}")
print(f"词向量维度:{len(apple_vector)}")
# 计算词之间的相似度
similarity = model.wv.similarity("苹果", "香蕉")
print(f"'苹果' 和 '香蕉' 的相似度:{similarity:.4f}")
similarity = model.wv.similarity("苹果", "猫")
print(f"'苹果' 和 '猫' 的相似度:{similarity:.4f}")
# 找出最相似的词
print("\n与 '猫' 最相似的词:")
for word, score in model.wv.most_similar("猫", topn=3):
print(f" {word}: {score:.4f}")
print("\n与 'RUNOOB' 最相似的词:")
for word, score in model.wv.most_similar("RUNOOB", topn=3):
print(f" {word}: {score:.4f}")
# 经典的词向量运算:国王 - 男人 + 女人 ≈ 女王
# 在我们的小语料里试试:水果 - 苹果 + 鱼 ≈ ?
if "苹果" in model.wv and "鱼" in model.wv and "水果" in model.wv:
result = model.wv.most_similar(positive=["水果", "鱼"], negative=["苹果"], topn=3)
print("\n'水果' - '苹果' + '鱼' ≈")
for word, score in result:
print(f" {word}: {score:.4f}")
Word2Vec 的成功证明了一点:语义可以用向量空间来表示。
但它有一个局限:每个词只有一个固定的向量,不管上下文是什么。比如"打"在"打电话"和"打游戏"里是不同的意思,但 Word2Vec 给的是同一个向量。
ELMo:上下文相关词向量
2018 年出现的 ELMo(Embeddings from Language Models)解决了这个问题。
ELMo 的思路是:不预先给每个词一个固定向量,而是看整个句子,再给这个词生成向量。
同样是"打"字,在"我打电话"里是一个向量,在"我打游戏"里是另一个向量。
ELMo 使用双向 LSTM(长短期记忆网络)来建模上下文,这是第一次大规模使用"预训练 + 微调"的范式。
GPT-1:单向预训练
同样是 2018 年,OpenAI 发布了 GPT-1(Generative Pre-training Transformer)。
它的特点是:
1. 使用 Transformer 解码器,而不是 LSTM
2. 单向:只看前面的词,预测下一个词
3. 生成式:可以续写文本
GPT-1 证明了 Transformer 在 NLP 任务上的巨大潜力。
BERT:双向预训练
2018 年底,Google 发布 BERT(Bidirectional Encoder Representations from Transformers),彻底改变了 NLP 领域。
BERT 的核心创新是:
1. 双向:同时看前后文
2. MLM(Masked Language Model):随机遮住一些词,让模型预测
3. NSP(Next Sentence Prediction):判断两个句子是不是连续的
BERT 在 11 个 NLP 任务上取得了当时最好的成绩,标志着 NLP 进入"预训练模型时代"。
GPT-3 到 ChatGPT 的跃升
2020 年,GPT-3 发布,参数量达到 1750 亿。
人们发现,当模型足够大、数据足够多时,会出现"涌现"(Emergence)现象——模型突然具备了小模型没有的能力,比如少样本学习、复杂推理。
2022 年底,ChatGPT 发布,通过 RLHF(人类反馈强化学习)让模型的输出更符合人类偏好,AI 真正走向大众。
让我们用一张表格总结这段演进史:
| 年份 | 模型 | 核心思想 | 历史地位 |
|---|---|---|---|
| 2013 | Word2Vec | 用词周围的词定义它的含义 | 词向量革命,语义的向量化表示 |
| 2018 | ELMo | 上下文相关的词向量 | 首次实现一词多义的向量表示 |
| 2018 | GPT-1 | Transformer 解码器,单向预训练 | 证明 Transformer 的潜力 |
| 2018 | BERT | Transformer 编码器,双向预训练 | NLP 进入预训练模型时代 |
| 2020 | GPT-3 | 1750 亿参数,涌现能力 | 展示大模型的无限可能 |
| 2022 | ChatGPT | RLHF + 对话能力 | AI 真正走向大众 |
BERT 深度解析
BERT 是 NLP 发展史上的里程碑,值得我们深入理解。
MLM(掩码语言模型)任务
BERT 的核心预训练任务是 MLM:随机把句子里 15% 的词替换成 [MASK],让模型预测原来的词是什么。
比如句子:"我喜欢吃苹果",可能变成:"我 [MASK] 吃苹果",模型要预测 [MASK] 是"喜欢"。
为什么这样做?因为这迫使模型同时利用前后文的信息,而不只是看左边或右边。
实际操作中,这 15% 的词不是全换成 [MASK],而是:
1. 80% 的时间换成 [MASK]
2. 10% 的时间换成一个随机词
3. 10% 的时间保持原词不变
这样做是为了让模型更健壮——它不能确定某个词是不是被替换了,必须真正理解上下文。
NSP(下句预测)任务
BERT 的第二个预训练任务是 NSP:给两个句子 A 和 B,判断 B 是不是 A 后面真正的下一句。
正例:A="我喜欢吃苹果",B="苹果是一种水果"
负例:A="我喜欢吃苹果",B="今天天气真好"
这个任务帮助模型理解句子之间的关系,对问答、自然语言推理等任务很有帮助。
BERT 的适用场景
BERT 是"编码器"架构,擅长理解语言,适合做:
文本分类、情感分析、命名实体识别、问答、自然语言推理等。
它不太擅长生成文本,这是 GPT 的强项。
微调 BERT 的标准流程
让我们用 Hugging Face Transformers 库,实战微调 BERT 做文本分类:
实例
# 使用 Hugging Face 微调 BERT 做文本分类
# 任务:判断句子是积极还是消极情感
# ============================================
# 首先安装依赖:
# pip install transformers datasets torch scikit-learn
import torch
from transformers import (
BertTokenizer,
BertForSequenceClassification,
TrainingArguments,
Trainer,
)
from datasets import Dataset, load_metric
import numpy as np
from sklearn.model_selection import train_test_split
# ============================================
# 1. 准备数据
# ============================================
# 构造一个简单的情感分类数据集
texts = [
"这个产品真的很好用,我很喜欢",
"质量太差了,非常失望",
"RUNOOB 教程写得很清晰,学习起来很轻松",
"物流很慢,包装也破损了",
"这款手机拍照效果很棒",
"服务态度不好,不会再来了",
"书的内容很精彩,推荐购买",
"电影很无聊,看得睡着了",
"这家餐厅的菜很好吃",
"游戏 bug 太多,体验很差",
"RUNOOB 的 NLP 教程帮助很大",
"这个软件界面设计得很好",
"快递员态度很友好",
"衣服尺码不准,颜色也不对",
"酒店环境很安静,睡得很好",
"客服回复很慢,问题没解决",
]
labels = [
1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0
] # 1=积极,0=消极
# 划分训练集和验证集
train_texts, val_texts, train_labels, val_labels = train_test_split(
texts, labels, test_size=0.25, random_state=42
)
# 创建 Dataset 对象
train_dataset = Dataset.from_dict({"text": train_texts, "label": train_labels})
val_dataset = Dataset.from_dict({"text": val_texts, "label": val_labels})
# ============================================
# 2. 加载 tokenizer 和模型
# ============================================
# 使用中文 BERT 模型
model_name = "bert-base-chinese"
tokenizer = BertTokenizer.from_pretrained(model_name)
model = BertForSequenceClassification.from_pretrained(
model_name,
num_labels=2 # 二分类任务
)
# ============================================
# 3. 数据预处理:把文本转换成模型输入
# ============================================
def tokenize_function(examples):
return tokenizer(
examples["text"],
padding="max_length",
truncation=True,
max_length=64
)
# 应用预处理
tokenized_train = train_dataset.map(tokenize_function, batched=True)
tokenized_val = val_dataset.map(tokenize_function, batched=True)
# ============================================
# 4. 定义评估指标
# ============================================
metric = load_metric("accuracy")
def compute_metrics(eval_pred):
logits, labels = eval_pred
predictions = np.argmax(logits, axis=-1)
return metric.compute(predictions=predictions, references=labels)
# ============================================
# 5. 配置训练参数
# ============================================
training_args = TrainingArguments(
output_dir="./runoob-bert-sentiment", # 输出目录
num_train_epochs=3, # 训练轮数
per_device_train_batch_size=4, # 训练批次大小
per_device_eval_batch_size=4, # 评估批次大小
warmup_steps=5, # 预热步数
weight_decay=0.01, # 权重衰减
logging_dir="./logs", # 日志目录
logging_steps=10,
evaluation_strategy="epoch", # 每个 epoch 评估一次
save_strategy="epoch",
learning_rate=2e-5,
load_best_model_at_end=True,
)
# ============================================
# 6. 创建 Trainer 并开始训练
# ============================================
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_train,
eval_dataset=tokenized_val,
compute_metrics=compute_metrics,
)
# 开始训练(注释掉,因为这需要较长时间和 GPU)
# print("开始训练...")
# trainer.train()
# ============================================
# 7. 使用训练好的模型进行预测
# ============================================
# 这里我们直接用原始模型演示预测流程
# 实际使用时,应该用 trainer 训练后保存的模型
def predict_sentiment(text, model, tokenizer):
"""预测句子情感"""
inputs = tokenizer(
text,
return_tensors="pt",
padding=True,
truncation=True,
max_length=64
)
model.eval()
with torch.no_grad():
outputs = model(**inputs)
logits = outputs.logits
probabilities = torch.softmax(logits, dim=-1)
prediction = torch.argmax(probabilities, dim=-1).item()
label_map = {0: "消极", 1: "积极"}
confidence = probabilities[0][prediction].item()
return {
"text": text,
"sentiment": label_map[prediction],
"confidence": confidence,
"negative_prob": probabilities[0][0].item(),
"positive_prob": probabilities[0][1].item(),
}
# 测试几个句子
test_texts = [
"RUNOOB 教程真的很棒!",
"这个产品质量太差了",
"今天天气真好",
"我不太喜欢这个电影",
]
print("测试模型预测(使用预训练的 bert-base-chinese):")
print("-" * 50)
for text in test_texts:
# 注意:这里直接用原始模型,没有微调,效果仅供演示
result = predict_sentiment(text, model, tokenizer)
print(f"文本:{result['text']}")
print(f"情感:{result['sentiment']}")
print(f"置信度:{result['confidence']:.4f}")
print(f"积极概率:{result['positive_prob']:.4f}")
print(f"消极概率:{result['negative_prob']:.4f}")
print("-" * 50)
注意:上面的代码演示了完整流程,但实际微调 BERT 需要 GPU 和较长时间。在生产环境中,你也可以考虑使用更轻量的模型(如 distilbert),或者直接使用大模型的 API。
GPT 系列深度解析
GPT(Generative Pre-trained Transformer)走了一条与 BERT 不同的路。
自回归语言模型
GPT 是"自回归"的:一个词一个词地生成,每一步都用之前生成的所有词来预测下一个词。
比如要生成"我喜欢吃苹果",过程是:
1. 输入"我",预测下一个词是"喜欢"
2. 输入"我喜欢",预测下一个词是"吃"
3. 输入"我喜欢吃",预测下一个词是"苹果"
这种方式天生适合文本生成。
Decoder-Only 架构
GPT 只用 Transformer 的解码器,而 BERT 只用编码器。
解码器的特点是有"掩码自注意力"(Masked Self-Attention)——每个位置只能看到它左边的位置,不能看到右边。
这很合理,因为生成文本时是从左到右的,你不能偷看还没生成的词。
Scaling Law 的发现
GPT 系列最重要的发现是 Scaling Law(缩放定律):模型性能与模型大小、数据量、计算量呈幂律关系。
简单说就是:模型越大、数据越多、训练越久,效果就越好,而且这种提升是可预测的,没有明显的平台期。
这就是为什么 GPT 系列一直在"做大":GPT-1(1.17 亿)→ GPT-2(15 亿)→ GPT-3(1750 亿)。
T5 与 Seq2Seq 范式
还有第三种范式:编码器-解码器(Encoder-Decoder)结构,T5 是其中的代表。
文本到文本的统一框架
T5(Text-to-Text Transfer Transformer)的核心思想是:把所有 NLP 任务都统一成"文本到文本"的格式。
比如:
文本分类:输入"分类:这电影很好看" → 输出"积极"
翻译:输入"翻译英文到中文:Hello world" → 输出"你好世界"
摘要:输入"摘要:这是一篇关于...的文章" → 输出"本文主要讲了..."
这种统一框架的好处是:同一个模型可以做各种任务,不需要为每个任务改模型结构。
编码器-解码器的工作方式
编码器负责理解输入文本,解码器负责生成输出文本。
以翻译为例:
1. 编码器读取"Hello world",生成一个"理解"后的表示
2. 解码器根据这个表示,一个词一个词地生成"你好世界"
解码器可以"关注"编码器的不同位置——比如生成"你好"时,更多关注"Hello",生成"世界"时,更多关注"world"。
三大范式对比:BERT vs GPT vs T5
让我们用一张表格对比这三种架构:
| 特性 | BERT | GPT | T5 |
|---|---|---|---|
| 架构 | Encoder-only | Decoder-only | Encoder-Decoder |
| 注意力 | 双向(看前后文) | 单向(只看前文) | 编码器双向,解码器单向 |
| 预训练任务 | MLM(掩码预测) | 自回归语言建模 | Span Corruption |
| 擅长任务 | 理解类:分类、NER、问答 | 生成类:续写、对话 | 转换类:翻译、摘要、改写 |
| 代表模型 | BERT、RoBERTa、ALBERT | GPT-1/2/3/4、ChatGPT | T5、BART、mT5 |
| 典型应用 | 情感分析、垃圾邮件过滤 | 写作助手、对话机器人 | 机器翻译、文档摘要 |
今天的大模型大多采用 Decoder-only 架构(GPT 路线),因为它在生成方面表现最好,而且通过 Scaling Law 可以持续提升。但在特定任务上,另外两种架构仍有优势。
文本分类
文本分类是最常见的 NLP 任务之一,也是初学者最好的入门任务。
多类别分类实战
多类别分类就是给文本分配一个标签,标签有多个选项。
比如:新闻分类(科技、体育、娱乐、财经)、客服工单分类、产品评论分类等。
实例
# 文本分类实战:多种方案对比
# ============================================
from typing import List, Dict, Any
# ============================================
# 方案一:传统机器学习 + TF-IDF
# 适合小数据集,可解释性强
# ============================================
def traditional_text_classification_demo():
"""使用传统方法做文本分类"""
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import Pipeline
# 训练数据
texts = [
"我今天买了一支股票,涨了很多",
"基金定投是不错的理财方式",
"这个球队的前锋进球了",
"NBA 季后赛真精彩",
"这款手机的相机拍照很棒",
"RUNOOB 的编程教程很清晰",
"这个电影的剧情很感人",
"那张专辑的歌曲很好听",
]
labels = ["财经", "财经", "体育", "体育", "科技", "科技", "娱乐", "娱乐"]
# 创建流水线:TF-IDF + 朴素贝叶斯
pipeline = Pipeline([
("tfidf", TfidfVectorizer()), # 把文本转换成 TF-IDF 特征
("classifier", MultinomialNB()), # 朴素贝叶斯分类器
])
# 训练
pipeline.fit(texts, labels)
# 测试
test_texts = [
"股票跌了,心情很不好",
"篮球比赛最后一秒绝杀",
"新出的笔记本电脑性能很强",
"RUNOOB 新出的 NLP 教程",
]
predictions = pipeline.predict(test_texts)
probabilities = pipeline.predict_proba(test_texts)
print("方案一:传统机器学习 + TF-IDF")
print("-" * 50)
for text, pred, probs in zip(test_texts, predictions, probabilities):
print(f"文本:{text}")
print(f"预测:{pred}")
print(f"各类概率:{dict(zip(pipeline.classes_, probs))}")
print()
# ============================================
# 方案二:使用 Hugging Face pipeline
# 适合快速原型,无需训练
# ============================================
def huggingface_pipeline_demo():
"""使用 Hugging Face 的预训练 pipeline"""
print("方案二:Hugging Face Pipeline")
print("-" * 50)
try:
from transformers import pipeline
# 使用情感分析 pipeline(英文)
classifier = pipeline("sentiment-analysis")
test_texts = [
"I love RUNOOB tutorials!",
"This movie is terrible.",
]
results = classifier(test_texts)
for text, result in zip(test_texts, results):
print(f"文本:{text}")
print(f"情感:{result['label']}")
print(f"置信度:{result['score']:.4f}")
print()
except Exception as e:
print(f"需要先安装 transformers:pip install transformers")
print(f"错误:{e}")
# ============================================
# 方案三:使用大模型 API(推荐)
# 适合生产环境,效果最好
# ============================================
def llm_based_classification_demo():
"""使用大模型做文本分类(模拟)"""
print("方案三:大模型分类(演示思路)")
print("-" * 50)
# 实际使用时,调用 OpenAI/Anthropic 等 API
# 这里演示分类的 prompt 设计
def classify_with_llm(text: str, categories: List[str]) -> Dict[str, Any]:
"""用 LLM 做分类的模拟函数"""
# 真实场景:调用 LLM API
# prompt = f"""请对以下文本进行分类,只返回类别名称。
# 文本:{text}
# 可选类别:{', '.join(categories)}"""
# 这里做一个简单的关键词匹配来模拟
category_map = {
"财经": ["股票", "基金", "理财", "投资"],
"体育": ["篮球", "足球", "比赛", "进球"],
"科技": ["手机", "电脑", "编程", "教程"],
"娱乐": ["电影", "音乐", "专辑", "歌曲"],
}
for category, keywords in category_map.items():
if any(keyword in text for keyword in keywords):
return {
"category": category,
"confidence": 0.9,
"reasoning": f"包含关键词:{', '.join([k for k in keywords if k in text])}"
}
return {"category": "其他", "confidence": 0.5}
test_texts = [
"股票涨了很开心",
"足球世界杯开幕了",
"RUNOOB 新出的编程教程",
]
categories = ["财经", "体育", "科技", "娱乐", "其他"]
for text in test_texts:
result = classify_with_llm(text, categories)
print(f"文本:{text}")
print(f"分类:{result['category']}")
print(f"置信度:{result['confidence']:.4f}")
if "reasoning" in result:
print(f"理由:{result['reasoning']}")
print()
# ============================================
# 运行演示
# ============================================
if __name__ == "__main__":
traditional_text_classification_demo()
print("=" * 50)
llm_based_classification_demo()
少样本分类方法
少样本学习(Few-Shot Learning)是大模型带来的新能力——只需要给几个例子,模型就能学会分类。
比如:
文本:这个产品很好用 → 标签:积极
文本:质量太差了 → 标签:消极
文本:[你的新文本] → 标签:?
大模型看了这两个例子,就能理解任务,对新文本做出合理分类。
大模型 vs 小模型的选择
什么时候用什么方案?这里有一个决策指南:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 数据量小(< 1000 条) | 大模型少样本学习 | 小模型学不出来,大模型泛化能力强 |
| 数据量大(> 10000 条) | 微调小模型(如 BERT) | 成本更低,推理更快 |
| 需要快速验证 | 大模型 API | 不用训练,立即可用 |
| 生产环境,高吞吐 | 微调小模型 + 蒸馏 | 推理速度快,成本可控 |
| 类别经常变化 | 大模型 Prompt 工程 | 不用重新训练,改 prompt 即可 |
命名实体识别(NER)
命名实体识别(Named Entity Recognition,NER)就是从文本里找出特定的实体,比如人名、地名、组织机构名。
序列标注框架
NER 通常建模为"序列标注"任务:给每个词打一个标签,说明它是不是实体的一部分。
比如句子:"张三在 Google 工作",标注可能是:
张三 → B-PER(人名开始)
在 → O(非实体)
Google → B-ORG(机构开始)
工作 → O
BIO 标注格式
最常用的标注格式是 BIO:
B-XXX:实体开始
I-XXX:实体内部
O:非实体
举个例子:"王小明在北京的微软公司上班",标注为:
王 → B-PER
小 → I-PER
明 → I-PER
在 → O
北 → B-LOC
京 → I-LOC
的 → O
微 → B-ORG
软 → I-ORG
公 → I-ORG
司 → I-ORG
上 → O
班 → O
实战应用
让我们用 Hugging Face 做 NER:
实例
# NER 实战:使用 Hugging Face 做命名实体识别
# ============================================
def ner_with_huggingface():
"""使用预训练 NER 模型"""
print("NER 实战:Hugging Face")
print("-" * 50)
try:
from transformers import pipeline
# 加载 NER pipeline
# 英文 NER
ner_pipeline = pipeline(
"ner",
model="dbmdz/bert-large-cased-finetuned-conll03-english",
grouped_entities=True # 把同一个实体的多个 token 合并
)
test_text = "John Smith works at Google in New York City. He loves RUNOOB tutorials."
results = ner_pipeline(test_text)
print(f"文本:{test_text}")
print("识别到的实体:")
for entity in results:
print(f" - {entity['word']}: {entity['entity_group']} (置信度: {entity['score']:.4f})")
print()
except Exception as e:
print(f"需要先安装 transformers:pip install transformers")
print(f"错误:{e}")
# ============================================
# 中文 NER:使用正则 + 关键词(简单场景)
# ============================================
def simple_chinese_ner():
"""简单的中文 NER:使用正则表达式和词典"""
import re
print("简单中文 NER:正则 + 词典")
print("-" * 50)
# 实体词典(实际场景中会更大)
person_names = ["张三", "李四", "王小明", "刘德华"]
organizations = ["Google", "微软", "阿里巴巴", "腾讯", "RUNOOB"]
locations = ["北京", "上海", "深圳", "纽约", "伦敦"]
# 正则模式
patterns = {
"PERSON": "|".join(re.escape(name) for name in person_names),
"ORG": "|".join(re.escape(org) for org in organizations),
"LOC": "|".join(re.escape(loc) for loc in locations),
"PHONE": r"1[3-9]\d{9}", # 手机号
"EMAIL": r"\w+@\w+\.\w+", # 邮箱
}
def extract_entities(text: str):
"""从文本中提取实体"""
entities = []
for entity_type, pattern in patterns.items():
for match in re.finditer(pattern, text):
entities.append({
"text": match.group(),
"type": entity_type,
"start": match.start(),
"end": match.end(),
})
# 按位置排序
entities.sort(key=lambda x: x["start"])
return entities
# 测试
test_texts = [
"张三在 Google 工作,手机号是 13800138000",
"王小明去北京的微软公司出差",
"有问题联系 [email protected]",
"RUNOOB 是一个很好的学习网站",
]
for text in test_texts:
entities = extract_entities(text)
print(f"文本:{text}")
if entities:
print("识别到的实体:")
for ent in entities:
print(f" - {ent['text']}: {ent['type']}")
else:
print("未识别到实体")
print()
# ============================================
# 大模型做 NER
# ============================================
def ner_with_llm():
"""使用大模型做 NER 的思路演示"""
print("大模型做 NER(演示思路)")
print("-" * 50)
def extract_entities_with_llm(text: str):
"""用 LLM 做 NER(模拟)"""
# 真实场景:构造 prompt 调用 LLM
# prompt = f"""请从以下文本中提取命名实体。
# 只返回 JSON 格式,包含实体类型:PERSON(人名)、ORG(机构)、LOC(地点)。
# 文本:{text}"""
# 这里做简单模拟
entities = []
# 简单的关键词匹配来模拟
if "张三" in text:
entities.append({"text": "张三", "type": "PERSON"})
if "Google" in text:
entities.append({"text": "Google", "type": "ORG"})
if "北京" in text:
entities.append({"text": "北京", "type": "LOC"})
if "RUNOOB" in text:
entities.append({"text": "RUNOOB", "type": "ORG"})
return entities
test_text = "张三在北京的 Google 公司工作,他经常上 RUNOOB 学习"
entities = extract_entities_with_llm(test_text)
print(f"文本:{test_text}")
print("识别到的实体:")
for ent in entities:
print(f" - {ent['text']}: {ent['type']}")
# ============================================
# 运行演示
# ============================================
if __name__ == "__main__":
simple_chinese_ner()
print("=" * 50)
ner_with_llm()
机器翻译
机器翻译是 NLP 最经典的应用之一,也是最早商业化的应用。
神经机器翻译原理
早期的机器翻译是规则的,后来是统计的,现在都是神经的。
神经机器翻译(Neural Machine Translation,NMT)通常是 Encoder-Decoder 架构:
1. Encoder 读取源语言句子,生成语义表示
2. Decoder 根据语义表示,生成目标语言句子
Attention 机制让解码器在生成每个词时,可以"关注"源语言句子的不同位置——这是翻译质量提升的关键。
评估指标:BLEU Score
怎么衡量翻译质量?BLEU(Bilingual Evaluation Understudy)是最常用的指标。
BLEU 的核心思想是:机器翻译结果与人工翻译结果的 n-gram 重合度越高,质量越好。
BLEU 分数范围是 0 到 1,越高越好。一般来说:
0.1 以下:基本不通
0.1-0.3:能看懂大概意思
0.3-0.5:翻译质量不错
0.5 以上:接近人工翻译
实例
# BLEU 分数计算演示
# ============================================
def bleu_score_demo():
"""演示 BLEU 分数的计算"""
print("BLEU 分数演示")
print("-" * 50)
# 注意:实际使用时建议用 sacrebleu 库
# 这里做概念演示
from collections import Counter
import math
def compute_ngrams(tokens, n):
"""计算 n-gram"""
return [tuple(tokens[i:i+n]) for i in range(len(tokens)-n+1)]
def simple_bleu(reference: str, candidate: str, max_n: int = 4):
"""简单的 BLEU 计算(概念演示)"""
ref_tokens = reference.split()
cand_tokens = candidate.split()
# 计算 brevity penalty(短句惩罚)
ref_len = len(ref_tokens)
cand_len = len(cand_tokens)
if cand_len == 0:
return 0.0
if cand_len <= ref_len:
bp = math.exp(1 - ref_len / cand_len)
else:
bp = 1.0
# 计算各个 n-gram 的精确率
precisions = []
for n in range(1, max_n + 1):
ref_ngrams = Counter(compute_ngrams(ref_tokens, n))
cand_ngrams = Counter(compute_ngrams(cand_tokens, n))
if not cand_ngrams:
precisions.append(0.0)
continue
# 统计匹配的数量
matches = 0
for ngram, count in cand_ngrams.items():
matches += min(count, ref_ngrams.get(ngram, 0))
total = sum(cand_ngrams.values())
precisions.append(matches / total if total > 0 else 0.0)
# 几何平均
if all(p == 0 for p in precisions):
geo_mean = 0.0
else:
# 避免 log(0)
log_sum = sum(math.log(p + 1e-10) for p in precisions) / max_n
geo_mean = math.exp(log_sum)
bleu = bp * geo_mean
return bleu
# 测试
reference = "the cat sat on the mat"
candidates = [
"the cat sat on the mat", # 完全匹配
"the cat was on the mat", # 一个词不同
"the cat sat on a mat", # 冠词不同
"a cat sat on the mat",
"the dog sat on the mat", # 一个词错误
"mat the on sat cat the", # 顺序全乱
"hello world", # 完全不相关
]
print(f"参考翻译:{reference}")
print()
for candidate in candidates:
bleu = simple_bleu(reference, candidate)
print(f"候选翻译:{candidate}")
print(f"BLEU 分数:{bleu:.4f}")
print()
# 中文例子
print("中文翻译示例:")
print("-" * 30)
ref = "我 喜欢 吃 苹果"
cand1 = "我 喜欢 吃 苹果"
cand2 = "我 爱 吃 苹果"
cand3 = "苹果 我 喜欢 吃"
print(f"参考:{ref}")
print(f"候选 1:{cand1}, BLEU: {simple_bleu(ref, cand1):.4f}")
print(f"候选 2:{cand2}, BLEU: {simple_bleu(ref, cand2):.4f}")
print(f"候选 3:{cand3}, BLEU: {simple_bleu(ref, cand3):.4f}")
if __name__ == "__main__":
bleu_score_demo()
LLM 时代的翻译质量
大模型出现后,机器翻译质量又上了一个台阶。
传统翻译模型是双语平行数据训练的,而大模型用海量文本训练,对语言的理解更深刻。
大模型在翻译上的优势:
1. 上下文理解更好:能处理歧义、多义词
2. 风格控制:可以指定翻译的语气、风格
3. 术语一致性:可以给术语表,保证专有名词翻译一致
4. 多语言能力:一个模型能做几十上百种语言的互译
情感分析
情感分析就是判断文本的情感倾向——积极、消极,还是中性。
细粒度情感分析
简单的情感分析是二分类(积极/消极),更细粒度的可以是:
1. 情感打分:1-5 星,1 分最消极,5 分最积极
2. 情感分类:愤怒、喜悦、悲伤、惊讶等
3. 方面级情感分析(Aspect-Based Sentiment Analysis):不仅判断整体情感,还分析对具体方面的情感。
比如:"这家餐厅菜很好吃,但服务太慢了"
整体情感:中性偏积极
方面级情感:
菜品 → 积极
服务 → 消极
多语言情感模型
现在的情感模型可以处理多种语言,不需要为每种语言单独训练。
常用的多语言模型:
XLM-RoBERTa、mBERT(多语言 BERT)、Llama、Claude 等大模型。
文本摘要
文本摘要就是把长文本缩短,保留核心信息。
抽取式 vs 生成式摘要
有两种主要的摘要方式:
抽取式摘要:从原文中挑选最重要的句子,拼在一起。
优点:信息准确,不会编造
缺点:不够流畅,可能包含冗余信息
生成式摘要:让模型"理解"原文,然后用自己的话写摘要。
优点:流畅、简洁,可以概括原文
缺点:可能"幻觉",编造原文没有的信息
大模型时代,生成式摘要成为主流,但使用时需要注意幻觉问题。
ROUGE 评估指标
摘要质量怎么评估?ROUGE 是最常用的指标。
ROUGE 有几个变种:
ROUGE-1:基于 unigram(单个词)的匹配
ROUGE-2:基于 bigram(两个词)的匹配
ROUGE-L:基于最长公共子序列
和 BLEU 类似,ROUGE 也是看机器生成的摘要和人工参考摘要的重合度。
实例
# 文本摘要:ROUGE 指标 + 简单抽取式摘要
# ============================================
def rouge_demo():
"""ROUGE 指标演示"""
print("ROUGE 指标演示")
print("-" * 50)
from collections import Counter
def compute_rouge_1(reference: str, candidate: str) -> float:
"""计算 ROUGE-1(unigram F1)"""
ref_tokens = reference.split()
cand_tokens = candidate.split()
if not ref_tokens or not cand_tokens:
return 0.0
ref_counts = Counter(ref_tokens)
cand_counts = Counter(cand_tokens)
# 计算匹配的数量
matches = 0
for token, count in cand_counts.items():
matches += min(count, ref_counts.get(token, 0))
precision = matches / len(cand_tokens)
recall = matches / len(ref_tokens)
if precision + recall == 0:
return 0.0
f1 = 2 * precision * recall / (precision + recall)
return f1
def compute_lcs(a: list, b: list) -> int:
"""计算最长公共子序列长度"""
m, n = len(a), len(b)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if a[i-1] == b[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
def compute_rouge_l(reference: str, candidate: str) -> float:
"""计算 ROUGE-L(最长公共子序列 F1)"""
ref_tokens = reference.split()
cand_tokens = candidate.split()
if not ref_tokens or not cand_tokens:
return 0.0
lcs_len = compute_lcs(ref_tokens, cand_tokens)
precision = lcs_len / len(cand_tokens)
recall = lcs_len / len(ref_tokens)
if precision + recall == 0:
return 0.0
f1 = 2 * precision * recall / (precision + recall)
return f1
# 测试
reference = "自然语言处理是人工智能的重要分支,研究计算机与人类语言的交互。"
candidates = [
"自然语言处理是人工智能的重要分支,研究计算机与人类语言的交互。", # 完全匹配
"自然语言处理研究计算机与人类语言的交互,是人工智能的重要分支。", # 顺序调整
"自然语言处理是人工智能的分支,研究计算机与语言的交互。", # 删减版
"RUNOOB 是一个编程学习网站。", # 不相关
]
print(f"参考摘要:{reference}")
print()
for candidate in candidates:
rouge1 = compute_rouge_1(reference, candidate)
rougel = compute_rouge_l(reference, candidate)
print(f"候选摘要:{candidate}")
print(f"ROUGE-1: {rouge1:.4f}")
print(f"ROUGE-L: {rougel:.4f}")
print()
def simple_extractive_summarization():
"""简单的抽取式摘要:基于词频选重要句子"""
print("简单抽取式摘要演示")
print("-" * 50)
import re
from collections import Counter
def split_sentences(text: str) -> list:
"""简单的中文分句"""
# 按句号、感叹号、问号分句
sentences = re.split(r'[。!?]', text)
# 过滤空字符串
sentences = [s.strip() for s in sentences if s.strip()]
return sentences
def get_word_frequency(text: str) -> Counter:
"""计算词频(简单演示:用字符级 n-gram 模拟)"""
# 实际场景应该用分词工具,如 jieba
# 这里简单用 uni-gram
chars = [c for c in text if c.strip()]
return Counter(chars)
def sentence_score(sentence: str, word_freq: Counter) -> float:
"""计算句子得分"""
if not sentence:
return 0.0
# 简单的得分:句子中词的频率之和 / 句子长度
total_score = 0.0
for char in sentence:
total_score += word_freq.get(char, 0)
# 归一化
return total_score / (len(sentence) + 1)
def extractive_summary(text: str, num_sentences: int = 2) -> str:
"""抽取式摘要"""
sentences = split_sentences(text)
if len(sentences) <= num_sentences:
return text
# 计算词频
word_freq = get_word_frequency(text)
# 计算每个句子的得分
scores = [(i, sentence_score(s, word_freq)) for i, s in enumerate(sentences)]
# 按得分排序,选最高的几个
scores.sort(key=lambda x: x[1], reverse=True)
top_indices = [i for i, score in scores[:num_sentences]]
top_indices.sort() # 保持原文顺序
# 拼接结果
summary = "。".join(sentences[i] for i in top_indices) + "。"
return summary
# 测试文本
test_text = """
自然语言处理是人工智能的重要分支。它研究如何让计算机理解和生成人类语言。
这项技术有很多应用,包括机器翻译、情感分析、问答系统等。
近年来,大语言模型的出现让自然语言处理取得了重大突破。
RUNOOB 网站提供了很多优秀的编程教程,帮助开发者学习新技能。
"""
print("原文:")
print(test_text.strip())
print()
summary = extractive_summary(test_text, num_sentences=2)
print("抽取式摘要:")
print(summary)
if __name__ == "__main__":
rouge_demo()
print("=" * 50)
simple_extractive_summarization()
点我分享笔记