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 真正走向大众。

让我们用一张表格总结这段演进史:

年份模型核心思想历史地位
2013Word2Vec用词周围的词定义它的含义词向量革命,语义的向量化表示
2018ELMo上下文相关的词向量首次实现一词多义的向量表示
2018GPT-1Transformer 解码器,单向预训练证明 Transformer 的潜力
2018BERTTransformer 编码器,双向预训练NLP 进入预训练模型时代
2020GPT-31750 亿参数,涌现能力展示大模型的无限可能
2022ChatGPTRLHF + 对话能力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

让我们用一张表格对比这三种架构:

特性BERTGPTT5
架构Encoder-onlyDecoder-onlyEncoder-Decoder
注意力双向(看前后文)单向(只看前文)编码器双向,解码器单向
预训练任务MLM(掩码预测)自回归语言建模Span Corruption
擅长任务理解类:分类、NER、问答生成类:续写、对话转换类:翻译、摘要、改写
代表模型BERT、RoBERTa、ALBERTGPT-1/2/3/4、ChatGPTT5、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()