Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第三十七 自然语言处理(NLP, Natural Language Processing)

第三十七 自然语言处理
├── 什么是自然语言处理(NLP)
├── 文本预处理
│   ├── 分词
│   ├── 清洗与标准化
│   └── 词干提取与词形还原
├── 文本表示
│   ├── 词袋模型(BoW)
│   ├── TF-IDF
│   └── 词嵌入(Word2Vec)
├── 语言模型
│   ├── 统计语言模型(N-gram)
│   ├── 神经网络语言模型
│   └── Transformer 与自注意力机制
├── 大语言模型(LLM)
│   ├── Token 与词嵌入
│   ├── AI Agent(智能体)
│   └── 典型大模型介绍
├── Rust NLP 生态
│   ├── rust-bert / candle / burn
│   └── 实战:用 Rust 实现 TF-IDF
└── 总结与练习

自然语言处理(Natural Language Processing,简称 NLP)是人工智能领域中最具魅力的方向之一。它的目标是让计算机理解、生成和处理人类自然语言——从搜索引擎的关键词匹配,到手机里的语音助手,再到如今能写代码、做推理的大语言模型,NLP 已经深刻地改变了我们的日常生活。

$$\text{自然语言处理} = \text{语言学} + \text{计算机科学} + \text{数学(概率论/线性代数)}$$


一、什么是自然语言处理

1.1 NLP 的定义

自然语言处理是计算机科学、人工智能和语言学的交叉学科,研究如何让计算机有效地处理和理解人类自然语言。NLP 的核心任务是弥合人类语言(模糊、上下文依赖、高度灵活)与计算机表示(精确、结构化、形式化)之间的鸿沟。

1.2 NLP 的发展历程

时代时间段代表方法特点
规则时代1950s–1980s专家系统、正则匹配依赖人工编写语法规则,泛化能力差
统计时代1990s–2010sN-gram、HMM、CRF基于概率和统计,数据驱动
深度学习时代2013–2017Word2Vec、RNN/LSTM、Seq2Seq神经网络自动学习特征
预训练时代2018–至今BERT、GPT、Transformer大规模预训练 + 微调范式
大模型时代2022–至今ChatGPT、Claude、DeepSeek生成式AI、Agent、多模态

1.3 NLP 的核心任务

NLP 的任务可以大致分为以下几类:

  • 文本分类:情感分析、垃圾邮件检测、新闻分类
  • 序列标注:命名实体识别(NER)、词性标注
  • 文本生成:机器翻译、文本摘要、对话系统
  • 信息抽取:关系抽取、事件抽取、知识图谱构建
  • 语义理解:问答系统、阅读理解、语义相似度
/// NLP 任务枚举——用 Rust 类型系统表达领域知识
#[derive(Debug, Clone, PartialEq)]
enum NlpTask {
    TextClassification,   // 文本分类
    NamedEntityRecognition, // 命名实体识别
    MachineTranslation,    // 机器翻译
    TextSummarization,     // 文本摘要
    QuestionAnswering,      // 问答系统
    SentimentAnalysis,     // 情感分析
}

fn describe_task(task: &NlpTask) -> &str {
    match task {
        NlpTask::TextClassification => "将文本归入预定义类别",
        NlpTask::NamedEntityRecognition => "从文本中识别人名、地名、组织名等实体",
        NlpTask::MachineTranslation => "将文本从一种语言翻译为另一种语言",
        NlpTask::TextSummarization => "生成文本的简洁摘要",
        NlpTask::QuestionAnswering => "根据上下文回答问题",
        NlpTask::SentimentAnalysis => "判断文本的情感倾向(正面/负面/中性)",
    }
}

fn main() {
    let tasks = vec![
        NlpTask::TextClassification,
        NlpTask::SentimentAnalysis,
        NlpTask::MachineTranslation,
    ];
    for task in &tasks {
        println!("{:?}: {}", task, describe_task(task));
    }
}

二、文本预处理

文本预处理是 NLP 管道的第一步,也是最关键的一步。原始文本往往包含噪声——标点、特殊字符、大小写不一致、停用词等,需要经过清洗和标准化才能用于后续分析。

2.1 分词(Tokenization)

分词是将连续的文本切分为有意义的单元(词、子词或字符)的过程。中文没有天然的空格分隔符,分词尤为关键。

/// 简单的英文分词器
fn tokenize_english(text: &str) -> Vec<&str> {
    text.split_whitespace()
        .map(|word| word.trim_matches(|c: char| !c.is_alphanumeric()))
        .filter(|w| !w.is_empty())
        .collect()
}

/// 简单的中文分词器(基于字符级别 + 常用词匹配)
fn tokenize_chinese(text: &str) -> Vec<String> {
    // 常用双字词词典(示例)
    let dictionary = vec![
        "自然", "语言", "处理", "人工", "智能", "机器", "学习",
        "深度", "模型", "计算", "科学", "技术", "方法", "数据",
    ];
    let chars: Vec<char> = text.chars().filter(|c| !c.is_whitespace()).collect();
    let mut tokens = Vec::new();
    let mut i = 0;
    while i < chars.len() {
        // 优先尝试匹配双字词
        if i + 1 < chars.len() {
            let bigram: String = chars[i..=i + 1].iter().collect();
            if dictionary.contains(&bigram.as_str()) {
                tokens.push(bigram);
                i += 2;
                continue;
            }
        }
        // 单字回退
        tokens.push(chars[i].to_string());
        i += 1;
    }
    tokens
}

fn main() {
    let en_text = "Natural Language Processing is fascinating!";
    let en_tokens = tokenize_english(en_text);
    println!("英文分词: {:?}", en_tokens);

    let zh_text = "自然语言处理是人工智能的核心技术";
    let zh_tokens = tokenize_chinese(zh_text);
    println!("中文分词: {:?}", zh_tokens);
}

2.2 文本清洗与标准化

use std::collections::HashSet;

/// 文本清洗管道
struct TextCleaner {
    stopwords: HashSet<String>,
}

impl TextCleaner {
    fn new() -> Self {
        let stopwords: HashSet<String> = [
            "的", "了", "在", "是", "我", "有", "和", "就",
            "不", "人", "都", "一", "一个", "上", "也", "很",
            "the", "a", "an", "is", "are", "was", "were",
            "and", "or", "but", "in", "on", "at", "to",
        ]
        .iter()
        .map(|s| s.to_string())
        .collect();
        TextCleaner { stopwords }
    }

    /// 转小写(英文)
    fn to_lowercase(&self, text: &str) -> String {
        text.to_lowercase()
    }

    /// 去除标点和特殊字符
    fn remove_punctuation(&self, text: &str) -> String {
        text.chars()
            .filter(|c| c.is_alphanumeric() || c.is_whitespace() || *c == '\u{4e00}..='\u{9fff}')
            .collect()
    }

    /// 去除停用词
    fn remove_stopwords(&self, tokens: &[String]) -> Vec<String> {
        tokens.iter().filter(|t| !self.stopwords.contains(*t)).cloned().collect()
    }

    /// 完整清洗管道
    fn clean(&self, text: &str) -> Vec<String> {
        let lowered = self.to_lowercase(text);
        let tokens: Vec<String> = lowered
            .split_whitespace()
            .map(|s| s.to_string())
            .collect();
        self.remove_stopwords(&tokens)
    }
}

fn main() {
    let cleaner = TextCleaner::new();
    let text = "Natural Language Processing is a fascinating field of study";
    let cleaned = cleaner.clean(text);
    println!("清洗前: {}", text);
    println!("清洗后: {:?}", cleaned);
}

2.3 词干提取与词形还原

词干提取(Stemming)是将词还原为词干的过程,例如 “running” -> “run”。词形还原(Lemmatization)则是将词还原为词典中的原形,例如 “better” -> “good”。

/// 简易英文词干提取器(Porter 算法简化版)
fn stem(word: &str) -> String {
    let mut result = word.to_string();

    // 去除常见后缀
    let suffixes = [
        "ing", "tion", "ness", "ment", "able", "ible",
        "ful", "less", "ous", "ive", "ed", "er", "ly", "es", "s",
    ];

    for suffix in &suffixes {
        if result.ends_with(suffix) && result.len() > suffix.len() + 2 {
            result = result[..result.len() - suffix.len()].to_string();
            break;
        }
    }

    result
}

fn main() {
    let words = ["running", "happiness", "computing", "played", "easily"];
    for word in &words {
        println!("{} -> {}", word, stem(word));
    }
    // running -> runn
    // happiness -> happi
    // computing -> comput
    // played -> play
    // easily -> eas
}

三、文本表示

计算机无法直接处理文本字符串,需要将文本转换为数值向量。文本表示方法经历了从离散到稠密、从人工设计到自动学习的演进。

3.1 词袋模型(Bag of Words, BoW)

词袋模型是最简单的文本表示方法。它忽略词序,仅统计每个词在文档中出现的次数。

给定文档集合 $D = {d_1, d_2, \ldots, d_n}$ 和词汇表 $V = {w_1, w_2, \ldots, w_m}$,文档 $d_i$ 的 BoW 向量为:

$$\text{BoW}(d_i) = [c_{i1}, c_{i2}, \ldots, c_{im}]$$

其中 $c_{ij}$ 表示词 $w_j$ 在文档 $d_i$ 中出现的次数。

use std::collections::HashMap;

/// 词袋模型
struct BagOfWords {
    vocabulary: Vec<String>,
    word_to_idx: HashMap<String, usize>,
}

impl BagOfWords {
    /// 从文档集合构建词汇表
    fn fit(documents: &[&str]) -> Self {
        let mut word_set = std::collections::BTreeSet::new();
        for doc in documents {
            for word in doc.split_whitespace() {
                word_set.insert(word.to_string());
            }
        }
        let vocabulary: Vec<String> = word_set.into_iter().collect();
        let word_to_idx: HashMap<String, usize> = vocabulary
            .iter()
            .enumerate()
            .map(|(i, w)| (w.clone(), i))
            .collect();
        BagOfWords { vocabulary, word_to_idx }
    }

    /// 将文档转换为 BoW 向量
    fn transform(&self, document: &str) -> Vec<usize> {
        let mut vector = vec![0usize; self.vocabulary.len()];
        for word in document.split_whitespace() {
            if let Some(&idx) = self.word_to_idx.get(word) {
                vector[idx] += 1;
            }
        }
        vector
    }

    /// 获取词汇表
    fn vocabulary(&self) -> &[String] {
        &self.vocabulary
    }
}

fn main() {
    let docs = [
        "rust language system safety",
        "rust programming memory safety",
        "python programming language",
    ];

    let bow = BagOfWords::fit(&docs);
    println!("词汇表: {:?}", bow.vocabulary());

    for doc in &docs {
        let vec = bow.transform(doc);
        println!("文档 \"{}\" -> {:?}", doc, vec);
    }
}

3.2 TF-IDF

TF-IDF(Term Frequency - Inverse Document Frequency)在词频的基础上引入了逆文档频率,降低了常见词的权重,提升了区分度高的词的权重。

$$\text{TF-IDF}(t, d, D) = \text{TF}(t, d) \times \text{IDF}(t, D)$$

其中:

$$\text{TF}(t, d) = \frac{f_{t,d}}{\sum_{t’ \in d} f_{t’,d}}$$

$$\text{IDF}(t, D) = \log \frac{|D|}{|{d \in D : t \in d}| + 1}$$

use std::collections::HashMap;

/// TF-IDF 计算器
struct TfIdfCalculator {
    vocabulary: Vec<String>,
    word_to_idx: HashMap<String, usize>,
    idf: Vec<f64>, // 每个词的 IDF 值
}

impl TfIdfCalculator {
    fn fit(documents: &[&str]) -> Self {
        // 构建词汇表
        let mut word_set = std::collections::BTreeSet::new();
        for doc in documents {
            for word in doc.split_whitespace() {
                word_set.insert(word.to_string());
            }
        }
        let vocabulary: Vec<String> = word_set.into_iter().collect();
        let word_to_idx: HashMap<String, usize> = vocabulary
            .iter()
            .enumerate()
            .map(|(i, w)| (w.clone(), i))
            .collect();

        // 计算 IDF
        let n_docs = documents.len() as f64;
        let mut doc_freq = vec![0usize; vocabulary.len()];
        for doc in documents {
            let mut seen = std::collections::HashSet::new();
            for word in doc.split_whitespace() {
                if let Some(&idx) = word_to_idx.get(word) {
                    if seen.insert(idx) {
                        doc_freq[idx] += 1;
                    }
                }
            }
        }
        let idf: Vec<f64> = doc_freq
            .iter()
            .map(|&df| (n_docs / (df as f64 + 1.0)).ln())
            .collect();

        TfIdfCalculator { vocabulary, word_to_idx, idf }
    }

    fn transform(&self, document: &str) -> Vec<f64> {
        let mut tf = vec![0.0f64; self.vocabulary.len()];
        let mut total = 0usize;
        for word in document.split_whitespace() {
            if let Some(&idx) = self.word_to_idx.get(word) {
                tf[idx] += 1.0;
                total += 1;
            }
        }
        // 归一化 TF
        if total > 0 {
            for v in &mut tf {
                *v /= total as f64;
            }
        }
        // TF * IDF
        tf.iter().zip(self.idf.iter()).map(|(&t, &i)| t * i).collect()
    }
}

fn main() {
    let docs = [
        "rust is a systems programming language",
        "rust focuses on safety and performance",
        "python is a high level programming language",
    ];

    let tfidf = TfIdfCalculator::fit(&docs);
    println!("词汇表: {:?}", tfidf.vocabulary);

    for doc in &docs {
        let vec = tfidf.transform(doc);
        println!("文档: \"{}\"", doc);
        for (word, val) in tfidf.vocabulary.iter().zip(vec.iter()) {
            if *val > 0.0 {
                println!("  {} => {:.4}", word, val);
            }
        }
    }
}

3.3 词嵌入(Word2Vec)

词嵌入将每个词映射为一个低维稠密向量,使得语义相近的词在向量空间中距离更近。Word2Vec 是最经典的词嵌入方法,包含两种训练模式:

  • CBOW(Continuous Bag of Words):用上下文预测中心词
  • Skip-gram:用中心词预测上下文

Word2Vec 的核心思想可以用一个简单的神经网络来理解。给定中心词 $w_t$,其上下文窗口为 $C$,Skip-gram 模型的目标函数为:

$$\mathcal{L} = \sum_{t=1}^{T} \sum_{-c \le j \le c, j \ne 0} \log P(w_{t+j} | w_t)$$

其中条件概率通过 softmax 计算:

$$P(w_{t+j} | w_t) = \frac{\exp(\mathbf{v}‘{w{t+j}} \cdot \mathbf{v}{w_t})}{\sum{w=1}^{V} \exp(\mathbf{v}’w \cdot \mathbf{v}{w_t})}$$

/// 简化的 Skip-gram 词嵌入训练器
use std::collections::HashMap;

struct WordEmbedding {
    word_to_idx: HashMap<String, usize>,
    idx_to_word: Vec<String>,
    // 词向量矩阵 W: vocab_size x embedding_dim
    w: Vec<Vec<f64>>,
    embedding_dim: usize,
    learning_rate: f64,
}

impl WordEmbedding {
    fn new(vocabulary: Vec<String>, embedding_dim: usize, lr: f64) -> Self {
        let vocab_size = vocabulary.len();
        let word_to_idx: HashMap<String, usize> = vocabulary
            .iter()
            .enumerate()
            .map(|(i, w)| (w.clone(), i))
            .collect();
        let idx_to_word = vocabulary.clone();

        // 随机初始化词向量
        let mut rng = rand_simple();
        let w: Vec<Vec<f64>> = (0..vocab_size)
            .map(|_| (0..embedding_dim).map(|_| rng.next_f64() * 0.1 - 0.05).collect())
            .collect();

        WordEmbedding { word_to_idx, idx_to_word, w, embedding_dim, learning_rate: lr }
    }

    /// 简化的 Skip-gram 训练步骤(负采样省略,使用朴素梯度)
    fn train_step(&mut self, center: &str, context: &str) {
        if let (Some(&ci), Some(&oi)) = (self.word_to_idx.get(center), self.word_to_idx.get(context)) {
            let dim = self.embedding_dim;
            let lr = self.learning_rate;

            // 简化:直接拉近中心词和上下文词的向量
            let mut grad = vec![0.0; dim];
            for d in 0..dim {
                grad[d] = self.w[oi][d] - self.w[ci][d];
            }
            for d in 0..dim {
                self.w[ci][d] += lr * grad[d] * 0.01;
                self.w[oi][d] -= lr * grad[d] * 0.01;
            }
        }
    }

    fn get_vector(&self, word: &str) -> Option<&Vec<f64>> {
        self.word_to_idx.get(word).map(|&idx| &self.w[idx])
    }

    /// 余弦相似度
    fn cosine_similarity(a: &[f64], b: &[f64]) -> f64 {
        let dot: f64 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
        let norm_a: f64 = a.iter().map(|x| x * x).sum::<f64>().sqrt();
        let norm_b: f64 = b.iter().map(|x| x * x).sum::<f64>().sqrt();
        if norm_a == 0.0 || norm_b == 0.0 { return 0.0; }
        dot / (norm_a * norm_b)
    }
}

/// 简易伪随机数生成器
struct SimpleRng { state: u64 }
fn rand_simple() -> SimpleRng { SimpleRng { state: 42 } }
impl SimpleRng {
    fn next_f64(&mut self) -> f64 {
        self.state = self.state.wrapping_mul(6364136223846793005).wrapping_add(1);
        (self.state >> 33) as f64 / u32::MAX as f64
    }
}

fn main() {
    let vocab = vec![
        "king".into(), "queen".into(), "man".into(), "woman".into(),
        "prince".into(), "princess".into(),
    ];
    let mut emb = WordEmbedding::new(vocab, 10, 0.01);

    // 模拟训练数据(中心词, 上下文词)
    let pairs = [
        ("king", "queen"), ("king", "man"), ("queen", "woman"),
        ("man", "woman"), ("prince", "princess"), ("prince", "king"),
        ("queen", "king"), ("woman", "man"), ("princess", "queen"),
    ];

    for _ in 0..100 {
        for (c, o) in &pairs {
            emb.train_step(c, o);
        }
    }

    // 计算相似度
    if let (Some(v1), Some(v2)) = (emb.get_vector("king"), emb.get_vector("queen")) {
        let sim = WordEmbedding::cosine_similarity(v1, v2);
        println!("king 与 queen 的余弦相似度: {:.4}", sim);
    }
}

四、语言模型

语言模型是 NLP 的基石,它为一段文本分配概率,衡量该文本在语言中出现的合理性。

4.1 统计语言模型(N-gram)

N-gram 模型基于马尔可夫假设:一个词的出现仅依赖于它前面的 $n-1$ 个词。

$$P(w_1, w_2, \ldots, w_T) = \prod_{t=1}^{T} P(w_t | w_{t-n+1}, \ldots, w_{t-1})$$

最常用的是 bigram($n=2$)和 trigram($n=3$)模型:

$$P(w_t | w_{t-1}) = \frac{C(w_{t-1}, w_t)}{C(w_{t-1})}$$

use std::collections::HashMap;

/// Bigram 语言模型
struct BigramModel {
    /// 单词计数
    unigram_counts: HashMap<String, usize>,
    /// 相邻词对计数
    bigram_counts: HashMap<(String, String), usize>,
    /// 总词数
    total_words: usize,
}

impl BigramModel {
    fn new() -> Self {
        BigramModel {
            unigram_counts: HashMap::new(),
            bigram_counts: HashMap::new(),
            total_words: 0,
        }
    }

    /// 从文本训练模型
    fn train(&mut self, text: &str) {
        let words: Vec<&str> = text.split_whitespace().collect();
        for i in 0..words.len() {
            *self.unigram_counts.entry(words[i].to_string()).or_insert(0) += 1;
            self.total_words += 1;
            if i > 0 {
                let key = (words[i - 1].to_string(), words[i].to_string());
                *self.bigram_counts.entry(key).or_insert(0) += 1;
            }
        }
    }

    /// 计算句子概率(加1平滑)
    fn sentence_probability(&self, sentence: &str) -> f64 {
        let words: Vec<&str> = sentence.split_whitespace().collect();
        let vocab_size = self.unigram_counts.len();
        let mut log_prob = 0.0f64;

        for i in 0..words.len() {
            let unigram_count = self.unigram_counts.get(words[i]).unwrap_or(&0) + 1;
            if i > 0 {
                let bigram_count = self.bigram_counts
                    .get(&(words[i - 1].to_string(), words[i].to_string()))
                    .unwrap_or(&0) + 1;
                let prob = (bigram_count as f64) / ((unigram_count + vocab_size) as f64);
                log_prob += prob.ln();
            } else {
                let prob = unigram_count as f64 / (self.total_words + vocab_size) as f64;
                log_prob += prob.ln();
            }
        }
        log_prob
    }

    /// 预测下一个词
    fn predict_next(&self, word: &str) -> Option<String> {
        let prefix = word.to_string();
        let candidates: Vec<_> = self.bigram_counts
            .iter()
            .filter(|((w, _), _)| w == &prefix)
            .collect();

        candidates.into_iter().max_by_key(|(_, &count)| count).map(|((_, w), _)| w.clone())
    }
}

fn main() {
    let corpus = "I love Rust programming . Rust is a systems language . I love programming in Rust .";

    let mut model = BigramModel::new();
    model.train(corpus);

    println!("'I love' 后面最可能的词: {:?}", model.predict_next("love"));
    println!("'Rust is' 后面最可能的词: {:?}", model.predict_next("is"));

    let prob = model.sentence_probability("I love Rust");
    println!("句子概率 (log): {:.4}", prob);
}

4.2 神经网络语言模型

神经网络语言模型用低维稠密向量替代了 N-gram 中的离散计数,能够更好地捕捉语义关系。从 Word2Vec 到 LSTM,再到 Transformer,神经网络语言模型逐步解决了长距离依赖和语义理解的问题。

4.3 Transformer 与自注意力机制

Transformer 是 2017 年由 Vaswani 等人提出的革命性架构,完全基于注意力机制,抛弃了循环和卷积结构。它是当今所有主流大语言模型(GPT、BERT、Claude、DeepSeek 等)的基础。

自注意力机制

自注意力的核心公式为:

$$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right) V$$

其中:

  • $Q$(Query):查询矩阵,表示“我在找什么“
  • $K$(Key):键矩阵,表示“我有什么“
  • $V$(Value):值矩阵,表示“我的内容是什么“
  • $d_k$:键向量的维度,用于缩放以防止梯度消失
/// 自注意力机制的 Rust 实现
struct Attention {
    /// Q, K, V 权重矩阵 (d_model x d_k)
    w_q: Vec<Vec<f64>>,
    w_k: Vec<Vec<f64>>,
    w_v: Vec<Vec<f64>>,
    d_k: usize,
    d_model: usize,
}

impl Attention {
    fn new(d_model: usize, d_k: usize) -> Self {
        let mut rng = rand_simple();
        let random_matrix = |rows, cols| -> Vec<Vec<f64>> {
            (0..rows)
                .map(|_| (0..cols).map(|_| rng.next_f64() * 0.1 - 0.05).collect())
                .collect()
        };
        Attention {
            w_q: random_matrix(d_model, d_k),
            w_k: random_matrix(d_model, d_k),
            w_v: random_matrix(d_model, d_k),
            d_k,
            d_model,
        }
    }

    /// 矩阵乘法 C = A * B^T
    fn mat_mul(a: &[Vec<f64>], b_t: &[Vec<f64>]) -> Vec<Vec<f64>> {
        let m = a.len();
        let n = b_t.len();
        let k = a[0].len();
        let mut c = vec![vec![0.0; n]; m];
        for i in 0..m {
            for j in 0..n {
                for l in 0..k {
                    c[i][j] += a[i][l] * b_t[j][l];
                }
            }
        }
        c
    }

    /// 矩阵乘以向量
    fn mat_vec_mul(mat: &[Vec<f64>], vec: &[f64]) -> Vec<f64> {
        mat.iter()
            .map(|row| row.iter().zip(vec.iter()).map(|(a, b)| a * b).sum())
            .collect()
    }

    /// Softmax 函数
    fn softmax(row: &[f64]) -> Vec<f64> {
        let max_val = row.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
        let exps: Vec<f64> = row.iter().map(|x| (x - max_val).exp()).collect();
        let sum: f64 = exps.iter().sum();
        exps.iter().map(|x| x / sum).collect()
    }

    /// 计算自注意力
    fn forward(&self, input: &[Vec<f64>]) -> Vec<Vec<f64>> {
        let seq_len = input.len();

        // 计算 Q, K, V
        let q: Vec<Vec<f64>> = input.iter().map(|x| Self::mat_vec_mul(&self.w_q, x)).collect();
        let k: Vec<Vec<f64>> = input.iter().map(|x| Self::mat_vec_mul(&self.w_k, x)).collect();
        let v: Vec<Vec<f64>> = input.iter().map(|x| Self::mat_vec_mul(&self.w_v, x)).collect();

        // QK^T / sqrt(d_k)
        let scale = 1.0 / (self.d_k as f64).sqrt();
        let mut scores = Self::mat_mul(&q, &k);
        for row in &mut scores {
            for val in row {
                *val *= scale;
            }
        }

        // Softmax
        let attention_weights: Vec<Vec<f64>> = scores.iter().map(|row| Self::softmax(row)).collect();

        // Attention * V
        let mut output = vec![vec![0.0; self.d_k]; seq_len];
        for i in 0..seq_len {
            for j in 0..self.d_k {
                for l in 0..seq_len {
                    output[i][j] += attention_weights[i][l] * v[l][j];
                }
            }
        }
        output
    }
}

fn main() {
    let d_model = 4;
    let d_k = 4;
    let attention = Attention::new(d_model, d_k);

    // 输入:3个token,每个4维
    let input = vec![
        vec![1.0, 0.0, 1.0, 0.0],
        vec![0.0, 1.0, 0.0, 1.0],
        vec![1.0, 1.0, 0.0, 0.0],
    ];

    let output = attention.forward(&input);
    println!("自注意力输出:");
    for (i, row) in output.iter().enumerate() {
        println!("  token {}: {:?}", i, row.iter().map(|x| format!("{:.4}", x)).collect::<Vec<_>>());
    }
}

Transformer 的整体结构

Transformer 由编码器(Encoder)和解码器(Decoder)两部分组成:

组件功能
多头注意力(Multi-Head Attention)从多个子空间捕捉不同层面的语义关系
前馈神经网络(FFN)对每个位置进行非线性变换
层归一化(Layer Normalization)稳定训练过程
残差连接(Residual Connection)缓解梯度消失,促进信息流动
位置编码(Positional Encoding)为序列注入位置信息

多头注意力的计算方式:

$$\text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, \ldots, \text{head}_h) W^O$$

$$\text{head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)$$


五、大语言模型(LLM)

5.1 Token 与词嵌入

大语言模型处理文本的基本单位是 Token(标记)。Token 不等于单词——它可以是单词、子词或字符。现代 LLM 普遍使用 BPE(Byte Pair Encoding)算法进行分词。

BPE 的核心思想是:反复合并语料中出现频率最高的相邻字符对,逐步构建子词词汇表。

use std::collections::HashMap;

/// 简化的 BPE(Byte Pair Encoding)分词器
struct BpeTokenizer {
    merges: Vec<(String, String)>,  // 合并规则
    vocab: Vec<String>,              // 词汇表
}

impl BpeTokenizer {
    fn new() -> Self {
        BpeTokenizer { merges: Vec::new(), vocab: Vec::new() }
    }

    /// 从语料中学习合并规则
    fn train(&mut self, text: &str, num_merges: usize) {
        // 初始:按字符分词
        let mut word_freq: HashMap<Vec<String>, usize> = HashMap::new();
        for word in text.split_whitespace() {
            let chars: Vec<String> = word.chars().map(|c| c.to_string()).collect();
            *word_freq.entry(chars).or_insert(0) += 1;
        }

        for _ in 0..num_merges {
            // 统计相邻符号对频率
            let mut pair_freq: HashMap<(String, String), usize> = HashMap::new();
            for (word, freq) in &word_freq {
                for i in 0..word.len().saturating_sub(1) {
                    let pair = (word[i].clone(), word[i + 1].clone());
                    *pair_freq.entry(pair).or_insert(0) += freq;
                }
            }

            // 找到最高频的符号对
            if let Some((&best_pair, _)) = pair_freq.iter().max_by_key(|(_, &f)| f) {
                self.merges.push(best_pair.clone());

                // 合并所有词中的该符号对
                let mut new_word_freq = HashMap::new();
                for (word, freq) in &word_freq {
                    let mut new_word = word.clone();
                    let merged = format!("{}{}", best_pair.0, best_pair.1);
                    let mut i = 0;
                    while i < new_word.len().saturating_sub(1) {
                        if new_word[i] == best_pair.0 && new_word[i + 1] == best_pair.1 {
                            new_word[i] = merged.clone();
                            new_word.remove(i + 1);
                        } else {
                            i += 1;
                        }
                    }
                    *new_word_freq.entry(new_word).or_insert(0) += freq;
                }
                word_freq = new_word_freq;
            } else {
                break;
            }
        }

        // 构建词汇表
        let mut vocab_set = std::collections::BTreeSet::new();
        for word in word_freq.keys() {
            for token in word {
                vocab_set.insert(token.clone());
            }
        }
        self.vocab = vocab_set.into_iter().collect();
    }

    /// 编码文本为 token 序列
    fn encode(&self, text: &str) -> Vec<String> {
        let mut tokens: Vec<String> = text.chars().map(|c| c.to_string()).collect();
        for (a, b) in &self.merges {
            let merged = format!("{}{}", a, b);
            let mut i = 0;
            while i < tokens.len().saturating_sub(1) {
                if tokens[i] == *a && tokens[i + 1] == *b {
                    tokens[i] = merged.clone();
                    tokens.remove(i + 1);
                } else {
                    i += 1;
                }
            }
        }
        tokens
    }
}

fn main() {
    let corpus = "low lower newest widest";
    let mut tokenizer = BpeTokenizer::new();
    tokenizer.train(corpus, 10);

    println!("合并规则: {:?}", tokenizer.merges);
    println!("词汇表: {:?}", tokenizer.vocab);

    let test = "lower newest";
    let encoded = tokenizer.encode(test);
    println!("'{}' => {:?}", test, encoded);
}

5.2 AI Agent(智能体)

AI Agent 是以大语言模型为“大脑“,能够自主感知环境、规划任务、调用工具并执行动作的智能系统。

$$\text{Agent} = \text{LLM(大脑)} + \text{Memory(记忆)} + \text{Tools(工具)} + \text{Planning(规划)}$$

/// 简化的 AI Agent 框架
use std::collections::HashMap;

/// 工具 trait:所有 Agent 可调用的工具都实现此 trait
trait Tool {
    fn name(&self) -> &str;
    fn description(&self) -> &str;
    fn execute(&self, input: &str) -> String;
}

/// 计算器工具
struct CalculatorTool;
impl Tool for CalculatorTool {
    fn name(&self) -> &str { "calculator" }
    fn description(&self) -> &str { "执行数学计算,输入为数学表达式" }
    fn execute(&self, input: &str) -> String {
        // 简化:仅支持两个数的四则运算
        let parts: Vec<&str> = input.split_whitespace().collect();
        if parts.len() == 3 {
            let a: f64 = parts[0].parse().unwrap_or(0.0);
            let b: f64 = parts[2].parse().unwrap_or(0.0);
            let result = match parts[1] {
                "+" => a + b, "-" => a - b,
                "*" => a * b, "/" => if b != 0.0 { a / b } else { f64::NAN },
                _ => f64::NAN,
            };
            format!("{} {} {} = {}", a, parts[1], b, result)
        } else {
            "无法解析表达式".to_string()
        }
    }
}

/// 搜索工具
struct SearchTool;
impl Tool for SearchTool {
    fn name(&self) -> &str { "search" }
    fn description(&self) -> &str { "搜索信息,输入为查询关键词" }
    fn execute(&self, input: &str) -> String {
        format!("搜索结果:关于 '{}' 的信息...", input)
    }
}

/// Agent 结构体
struct Agent {
    tools: HashMap<String, Box<dyn Tool>>,
    memory: Vec<String>,
}

impl Agent {
    fn new() -> Self {
        let mut tools: HashMap<String, Box<dyn Tool>> = HashMap::new();
        tools.insert("calculator".to_string(), Box::new(CalculatorTool));
        tools.insert("search".to_string(), Box::new(SearchTool));
        Agent { tools, memory: Vec::new() }
    }

    /// 模拟 Agent 的推理和工具调用过程
    fn process(&mut self, user_input: &str) -> String {
        self.memory.push(user_input.to_string());

        // 简化的意图识别
        if user_input.contains("计算") || user_input.contains("+") || user_input.contains("*") {
            let tool = self.tools.get("calculator").unwrap();
            let expr = user_input
                .replace("计算", "")
                .replace("请", "")
                .trim()
                .to_string();
            let result = tool.execute(&expr);
            self.memory.push(result.clone());
            format!("[Agent] 调用工具 {} => {}", tool.name(), result)
        } else if user_input.contains("搜索") || user_input.contains("查找") {
            let tool = self.tools.get("search").unwrap();
            let query = user_input
                .replace("搜索", "")
                .replace("查找", "")
                .replace("请", "")
                .trim()
                .to_string();
            let result = tool.execute(&query);
            self.memory.push(result.clone());
            format!("[Agent] 调用工具 {} => {}", tool.name(), result)
        } else {
            "[Agent] 我理解了你的问题,但目前没有合适的工具来处理。".to_string()
        }
    }
}

fn main() {
    let mut agent = Agent::new();

    let queries = vec![
        "请计算 123 + 456",
        "搜索 Rust 语言特性",
    ];

    for q in &queries {
        println!("用户: {}", q);
        println!("{}", agent.process(q));
        println!();
    }
}

5.3 典型大模型介绍

模型开发者参数量特点
GPT-4oOpenAI未公开多模态、推理能力强
ClaudeAnthropic未公开安全对齐、长上下文、代码能力突出
DeepSeekDeepSeek671B (MoE)开源、MoE 架构、性价比极高
Qwen阿里云72B+中文能力强、开源生态完善
LLaMAMeta8B–405B开源标杆、社区生态丰富

六、Rust NLP 生态

Rust 在 NLP 领域的生态正在快速发展,以下是一些重要的库和框架。

6.1 主要库一览

库名说明适用场景
rust-bertHuggingFace Transformers 的 Rust 移植预训练模型推理(BERT、GPT-2 等)
candleHuggingFace 出品的轻量级 ML 框架模型训练与推理、Transformer 实现
burnRust 原生深度学习框架灵活的模型构建与训练
rust-tokenizersHuggingFace Tokenizers 的 Rust 原生实现高性能分词(BPE、WordPiece 等)
finalfusionRust 词嵌入库Word2Vec、FastText、GloVe 格式
rust-stemmers多语言词干提取文本预处理

6.2 实战:用 Rust + candle 加载预训练模型

// Cargo.toml 依赖:
// [dependencies]
// candle-core = "0.8"
// candle-nn = "0.8"
// candle-transformers = "0.8"

use candle_core::{Device, Tensor};
use candle_transformers::models::bert::{BertModel, Config};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 加载预训练 BERT 模型配置
    let config = Config::bert_base_uncased();

    // 创建随机初始化的模型(实际使用时加载权重文件)
    let device = Device::Cpu;
    let vb = candle_nn::VarBuilder::new_null(); // 实际使用 candle_nn::VarBuilder::from_pth()
    let _model = BertModel::new(vb, &config)?;

    // 模拟输入
    let input_ids = Tensor::new(&[[101u32, 2023, 2003, 1037, 3376, 102]], &device)?;
    let attention_mask = Tensor::new(&[[1u32; 7]], &device)?;

    println!("输入 token IDs: {:?}", input_ids.shape());
    println!("注意力掩码: {:?}", attention_mask.shape());
    println!("BERT 模型已准备就绪");

    Ok(())
}

6.3 实战:用 Rust 实现完整的文本分类管道

use std::collections::HashMap;

/// 文本分类器(基于 TF-IDF + 余弦相似度)
struct TextClassifier {
    tfidf: TfIdfCalculator,
    /// 每个类别的 TF-IDF 质心向量
    centroids: HashMap<String, Vec<f64>>,
}

impl TextClassifier {
    fn train(documents: &[(&str, &str)]) -> Self {
        let all_texts: Vec<&str> = documents.iter().map(|(t, _)| *t).collect();
        let tfidf = TfIdfCalculator::fit(&all_texts);

        // 按类别计算质心
        let mut class_docs: HashMap<String, Vec<Vec<f64>>> = HashMap::new();
        for (text, label) in documents {
            let vec = tfidf.transform(text);
            class_docs.entry(label.to_string()).or_default().push(vec);
        }

        let mut centroids = HashMap::new();
        for (label, vecs) in class_docs {
            let dim = vecs[0].len();
            let n = vecs.len() as f64;
            let centroid: Vec<f64> = (0..dim)
                .map(|d| vecs.iter().map(|v| v[d]).sum::<f64>() / n)
                .collect();
            centroids.insert(label, centroid);
        }

        TextClassifier { tfidf, centroids }
    }

    fn predict(&self, text: &str) -> String {
        let vec = self.tfidf.transform(text);
        let mut best_label = String::new();
        let mut best_sim = f64::NEG_INFINITY;

        for (label, centroid) in &self.centroids {
            let sim = cosine_similarity(&vec, centroid);
            if sim > best_sim {
                best_sim = sim;
                best_label = label.clone();
            }
        }
        best_label
    }
}

fn cosine_similarity(a: &[f64], b: &[f64]) -> f64 {
    let dot: f64 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
    let norm_a: f64 = a.iter().map(|x| x * x).sum::<f64>().sqrt();
    let norm_b: f64 = b.iter().map(|x| x * x).sum::<f64>().sqrt();
    if norm_a == 0.0 || norm_b == 0.0 { return 0.0; }
    dot / (norm_a * norm_b)
}

fn main() {
    let training_data: Vec<(&str, &str)> = vec![
        ("rust is a great programming language", "tech"),
        ("python is popular for data science", "tech"),
        ("machine learning transforms industries", "tech"),
        ("the stock market rose today", "finance"),
        ("interest rates are increasing", "finance"),
        ("investment portfolio diversification", "finance"),
    ];

    let classifier = TextClassifier::train(&training_data);

    let test_cases = vec![
        "I love programming in rust",
        "The economy is growing fast",
    ];

    for text in &test_cases {
        let label = classifier.predict(text);
        println!("'{}' => 分类: {}", text, label);
    }
}

七、总结

7.1 NLP 核心概念速查表

概念说明关键公式/方法
分词将文本切分为 token字符级、词级别、BPE
词袋模型忽略词序,统计词频$\text{BoW}(d) = [c_1, c_2, \ldots, c_m]$
TF-IDF词频 x 逆文档频率$\text{TF-IDF} = \text{TF} \times \log\frac{N}{df+1}$
Word2Vec词嵌入,语义向量化Skip-gram / CBOW
N-gram统计语言模型$P(w_t | w_{t-1})$
Transformer自注意力架构$\text{softmax}(\frac{QK^T}{\sqrt{d_k}})V$
BPE子词分词算法迭代合并最高频字符对
LLM大规模预训练语言模型GPT、BERT、Claude、DeepSeek
AI AgentLLM + 工具 + 规划Agent = LLM + Memory + Tools + Planning

7.2 练习建议

难度练习内容涉及知识点
入门实现一个英文文本清洗管道(去停用词、转小写、去标点)文本预处理
入门用 Rust 实现 BoW 模型,计算两篇文档的余弦相似度词袋模型、向量运算
进阶实现 TF-IDF 计算器,完成一个简单的新闻分类器TF-IDF、文本分类
进阶用 Rust 从零实现 Bigram 语言模型,支持句子概率计算和下一个词预测N-gram、概率计算
进阶实现简化版 BPE 分词器,对比不同合并次数对词汇表大小的影响BPE、Tokenization
挑战用 Rust 从零实现多头自注意力机制,支持可变序列长度Transformer、矩阵运算
挑战基于 candle 框架加载预训练 BERT 模型,完成情感分析推理任务rust-bert/candle、预训练模型
挑战构建一个简易 AI Agent,支持多轮对话、工具调用和记忆管理Agent 架构、工具系统