在职前端Leader学习/转行 AI Agent -DAY73

2026.10.9 09:51

学习路线

rust 复制代码
Python -> Python数据分析 -> LangChain -> LangGraph -> FastAPI + SQLAIchemy -> 机器学习 -> 深度学习 -> NLP -> Coze -> Dify -> 大模型应用基础 -> 大模型微调 -> 多模态 -> vibeCoding -> Hermes -> Docker

复习巩固总结

1. RNN

张量 形状 各维含义
输入 (x) [5, 32, 128],即 [seq_len, batch, input_size] 句子长度 5,一批 32 条,每个词的向量 128 维
初始隐藏状态 (h_0) [1, 32, 256],即 [num_layers, batch, hidden_size] 隐藏层 1 层,一批 32 条,隐藏状态 256 维。对应前 14 集的概要
最终隐藏状态 (h_n) [1, 32, 256],和 (h_0) 相同 当前时刻的隐藏状态,前 15 集的概要
输出 output [5, 32, 256],即 [seq_len, batch, hidden_size] 句子长度 5,一批 32 条,每个时间步的输出向量 256 维
结构 内部有什么
传统 RNN 输入层,隐藏层(词嵌入层、循环网络层),输出层
LSTM 遗忘门、输入门、细胞状态、输出门
Bi-LSTM 双向 LSTM。句子从前到后走一次,从后到前再走一次,两次结果合并
GRU 重置门、更新门
Bi-GRU 双向 GRU,同样是两个方向各走一遍再合并

1.1 基础版RNN

python 复制代码
import torch
import torch.nn as nn
# todo 1. 定义函数, 演示RNN的基础版代码.
def dm_rnn_for_base():
    rnn = nn.RNN(5, 6, 1)
    input = torch.randn(1, 3, 5)
    h0 = torch.randn(1, 3, 6)
    output, hn = rnn(input, h0)
    print(f'output: {output}, output.shape: {output.shape}')    # shape: (1, 3, 6)
    print(f'hidden: {hn}, hidden.shape: {hn.shape}')            # shape: (1, 3, 6)
    print(f'rnn模型: {rnn}')                                    # RNN(5, 6)
# todo 2. 定义函数, 修改RNN模型(句子)的长度
def dm_rnn_for_sequence_len():
    rnn = nn.RNN(5, 6, 1)
    input = torch.randn(20, 3, 5)
    h0 = torch.randn(1, 3, 6)
    output, hn = rnn(input, h0)
    print(f'output: {output}, output.shape: {output.shape}')    # shape: (20, 3, 6)
    print(f'hidden: {hn}, hidden.shape: {hn.shape}')            # shape: (1, 3, 6)
    print(f'rnn模型: {rnn}')                                    # RNN(5, 6)
# todo 3. 定义函数, 修改RNN模型 隐藏层层数
def dm_rnn_for_hidden_layers():
    rnn = nn.RNN(5, 6, 2)
    input = torch.randn(1, 3, 5)
    h0 = torch.randn(2, 3, 6)
    output, hn = rnn(input, h0)
    print(f'output: {output}, output.shape: {output.shape}')    # shape: (1, 3, 6)
    print(f'hidden: {hn}, hidden.shape: {hn.shape}')            # shape: (2, 3, 6)
    print(f'rnn模型: {rnn}')                                    # RNN(5, 6, num_layers=2)

1.2 LSTM

1.2.1 基础版

LSTM 全称 Long Short Term Memory,长短时记忆结构。它是 RNN 的一种变体,用来抓住较长序列里前后词的语义关系,缓解梯度消失和梯度爆炸,但这种现象仍然可能发生。序列更长时,它的表现优于传统 RNN,但同等算力下,训练效率比传统 RNN 低很多。

遗忘门、输入门、输出门的激活函数是 sigmoid,把数变到 0 和 1 之间。接近 1 表示通过,接近 0 表示挡住。候选记忆和最终读出记忆时用 tanh,把数压到 -1 和 1 之间。下面公式里的 (*) 都是按元素相乘。

符号 谁 公式 中文 激活 作用
(f_t) 遗忘门 (\sigma(W_f h_{t-1}, x_t + b_f)) 遗忘门门值 = 激活函数(W @ 上一隐藏状态, 当前输入 + 偏置) sigmoid 筛选旧记忆,丢掉不要的。乘上 (C_{t-1}),决定旧记忆留多少,用来更新长期记忆本
(i_t) 输入门 (\sigma(W_i h_{t-1}, x_t + b_i)) 输入门门值 = 激活函数(W @ 上一隐藏状态, 当前输入 + 偏置) sigmoid 筛选新信息,留下值得记的。乘上 (\tilde{C}_t),决定新信息写多少,加进长期记忆本
(\tilde{C}_t) 候选记忆 (\tanh(W_c h_{t-1}, x_t + b_c)) 候选记忆 = tanh(W @ 上一隐藏状态, 当前输入 + 偏置) tanh 这一步准备好的新内容
(C_t) 记忆细胞 (f_t * C_{t-1} + i_t * \tilde{C}_t) 新的细胞状态 = 遗忘门门值 * 旧记忆细胞状态 + 输入门门值 * 候选记忆 加法更新 长期保存关键信息,记在小本本上,传给下一步
(o_t) 输出门 (\sigma(W_o h_{t-1}, x_t + b_o)) 输出门门值 = 激活函数(W @ 上一隐藏状态, 当前输入 + 偏置) sigmoid 筛选这一步要传出去的内容。乘上 (\tanh(C_t)),决定说出多少
(h_t) 隐藏输出 (o_t * \tanh(C_t)) 本次隐藏状态 = 输出门门值 * tanh 之后的新记忆细胞状态 tanh 后再乘 当前隐藏输出,交给下一个时间步
1.2.2 Bi-LSTM

双向 LSTM 让每个词同时用上左边和右边的上下文。每个方向用的仍是原来的 LSTM 单元,遗忘门、输入门、记忆细胞和输出门都不改。变化只在外面:同一句话走两遍,再把两个方向的隐藏状态拼在一起。

正向和反向各有一套参数,参数量和计算量都大约变成单向 LSTM 的两倍。

以「我爱中国」为例。

从左往右走一遍,顺序是「我 → 爱 → 中国」。正向 LSTM(LSTM_L)依次得到 (h_{L0})、(h_{L1})、(h_{L2})。走到「爱」时,它已经看过左边的「我」。

从右往左再走一遍,顺序是「中国 → 爱 → 我」。反向 LSTM(LSTM_R)依次得到 (h_{R0})、(h_{R1})、(h_{R2})。走到「爱」时,它已经看过右边的「中国」。

词 从左往右 从右往左 最终输出
我 (h_{L0}) (h_{R2}) (h_0):(h_{L0}) 与 (h_{R2}) 拼接
爱 (h_{L1}) (h_{R1}) (h_1):(h_{L1}) 与 (h_{R1}) 拼接
中国 (h_{L2}) (h_{R0}) (h_2):(h_{L2}) 与 (h_{R0}) 拼接
1.2.3 代码示例
python 复制代码
"""
案例:
    演示LSTM入门代码

LSTM简介:
    概述:
        Long Short Term Memory, 长短时记忆结构, 能够解决传统RNN的弊端 → 处理长序列数据效果差的问题.
    组成:
        遗忘门, 输入门, 细胞状态, 输出门
    优点:
        1. 能够解决长时依赖.
        2. 缓解梯度消失, 爆炸的问题 → 训练更稳定.
        3. 能够抓取复杂的特征.
    缺点:
        1. 计算慢, 消耗资源大.
        2. 调参相对较难.
        3. 可解释性稍差.
"""

# 导包
import torch
import torch.nn as nn

# todo 1. 定义函数, 演示LSTM模型的用法.
def lstm_demo():
    # 1. 创建LSTM模型对象.
    # 参1: 词向量维度(输入的维度), 参2: 隐藏层的维度(输出的维度), 参3: LSTM的层数.  参4: 是否双向.
    lstm = nn.LSTM(input_size=5, hidden_size=6, num_layers=1, bidirectional=False)

    # 2. 构建输入张量.
    # 参1: 句子的长度(seq_len), 参2: 批次大小(batch_size), 参3: 词向量维度(input_size).
    input = torch.randn(4, 3, 5)

    # 3. 初始化隐藏层 和 细胞状态.
    # 参1: LSTM层数(隐藏层层数), 参2: 批次大小(batch_size), 参3: 隐藏层的维度(hidden_size).
    h0 = torch.randn(1, 3, 6)
    c0 = torch.randn(1, 3, 6)

    # 4. 模型计算.
    # 写法1: 手动传入 h0 和 c0
    # 实参列表: 参1(input) → 输入张量,  参2 → 隐藏状态 和 细胞状态的元组形式.
    # 返回值:   output → 本次的输出结果, (hn,cn) → 最后1个时间步的隐藏状态和细胞状态.
    output, (hn, cn) = lstm(input, (h0, c0))

    # 写法2: 不传入 h0和c0 → 底层会自动创建1个 全0的 h0(上一时刻隐藏状态) 和 c0(上一时刻细胞状态)
    # output, (hn, cn) = lstm(input)

    # 5. 打印结果.
    print(f'output: {output}, {output.shape}')        # shape: (4, 3, 6)
    print(f'hn: {hn}, {hn.shape}')                    # shape: (1, 3, 6)
    print(f'cn: {cn}, {cn.shape}')                    # shape: (1, 3, 6)


# todo 2. 测试代码
if __name__ == '__main__':
    lstm_demo()

1.3 GRU

GRU 全称 Gated Recurrent Unit,门控循环单元。它也是传统 RNN 的变体,和 LSTM 一样能抓住较长序列里前后词的语义关系,缓解梯度消失和梯度爆炸。结构比 LSTM 简单,比 RNN 复杂。

也带着 RNN 结构本身的一个弊端:不能并行计算。数据量和模型体量逐步变大之后,这是 RNN 发展的关键瓶颈。

符号 谁 公式 中文 激活 作用
(r_t) 重置门 (\sigma(W_r h_{t-1}, x_t)) 重置门门值 = 激活函数(W @ 上一隐藏状态, 当前输入) sigmoid 决定生成新记忆时要不要擦掉上一份隐藏状态。乘上 (h_{t-1}),决定生成候选时旧记忆留多少
(\tilde{h}_t) 候选隐藏状态 (\tanh(W r_t \* h_{t-1}, x_t)) 候选隐藏状态 = tanh(W @ 重置门门值 \* 上一隐藏状态, 当前输入) tanh 这一步准备好的新记忆。先用重置门筛过旧隐藏状态,再和当前输入拼在一起
(z_t) 更新门 (\sigma(W_z h_{t-1}, x_t)) 更新门门值 = 激活函数(W @ 上一隐藏状态, 当前输入) sigmoid 决定新旧记忆怎么融合,最终这份隐藏状态里旧的占多少、新候选占多少
(h_t) 隐藏状态 ((1 - z_t) * h_{t-1} + z_t * \tilde{h}_t) 本次隐藏状态 = (1 - 更新门门值) * 上一隐藏状态 + 更新门门值 * 候选隐藏状态 加权相加 融合后的隐藏状态,既是这一步的输出,也传给下一步
python 复制代码
"""
案例:
    演示GRU代码

GRU简介:
    概述:
        Gated Recurrent Unit, 门控循环单元结构, 可以理解为是一个 简化版的智能管家,
        即能像LSTM那样处理长序列信息, 计算上也更简单, 更节省资源.
    核心设计思路:
        重置门, 更新门.        就是在LSTM的基础上, 对部分门做合并.
    优点:
        和LSTM一样, 既可以处理长序列语义关联, 能够有效缓解梯度消失和爆炸问题.
        关于长序列的处理效果: 优于传统RNN
        关于计算复杂度: 比LSTM要小.
    缺点:
        不能完全解决梯度消失, 爆炸问题, 不能并行计算.
        在数据量和模型体量逐步增大的未来, 是RNN发展的关键瓶颈.
"""

# 导包
import torch
import torch.nn as nn

# todo 1. 定义函数, 演示: GRU代码实现.
def gru_demo():
    # 1. 创建GRU模型对象.
    # 参1: 输入特征维度(词向量维度), 参2: 隐藏层维度(输出维度), 参3: 层数
    gru = nn.GRU(input_size=5, hidden_size=6, num_layers=1)

    # 2. 创建输入数据.
    # 参1: 句子长度(seq_len), 参2: 批次大小(batch_size), 参3: 输入特征维度(input_size)
    input = torch.randn(2, 3, 5)

    # 3. 创建初始隐藏状态.
    # 参1: 层数(num_layers), 参2: 批次大小(batch_size), 参3: 隐藏层维度(hidden_size)
    h0 = torch.zeros(1, 3, 6)

    # 4. 运行GRU模型.
    output, hn = gru(input, h0)

    # 5. 输出结果.
    print(f'output: {output}, output.shape: {output.shape}')  # shape: (2, 3, 6)
    print(f'hidden: {hn}, hidden.shape: {hn.shape}')          # shape: (1, 3, 6)
    print(f'gru模型: {gru}')                                  # RNN(5, 6)


# todo 2. 测试代码
if __name__ == '__main__':
    gru_demo()

1.4 RNN人名分类案例

python 复制代码
"""
案例:
    代码实现RNN 全球人名分类案例, 录入人名, 预测其国家.

回顾: 深度学习, NLP项目研发流程:
    1. 导包
    2. 数据的预处理.
        文件加载 → 封装成Tensor → 数据集对象(TensorDataset) → 数据加载器(DataLoader)
    3. 构建模型.
        RNN, LSTM, GRU
    4. 模型训练.
        绘图 → 对比三种模型的效果.
    5. 模型测试.
        RNN, LSTM, GRU

细节:
    1. 本案例是(课程中, NLP阶段)为数不多的 用one-hot编码来处理的案例.
    2. 本案例是(课程中, NLP阶段)为数不多的 用RNN, LSTM, GRU三种模型全演示的案例.
    3. 代码层次上, 优先掌握: LSTM, RNN, 因为GRU写法和RNN几乎一致, 简单改改即可.
"""
import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'

# 导包
import torch                                                                # 张量计算相关
import torch.nn as nn                                                       # 神经网络模块, 各种模型的层, 组件...
import torch.nn.functional as F                                             # 常用的函数库...
import torch.optim as optim                                                 # 优化器模块
from torch.utils.data import Dataset, DataLoader                           # 数据集对象, 数据加载器
import string                                                               # 字符串处理模块.
import time                                                                 # 时间模块.
import matplotlib.pyplot as plt                                             # 绘图模块.
from tqdm import tqdm                                                       # 进度条

# 解决绘图时, 中文乱码问题.
plt.rcParams['font.sans-serif'] = ['SimHei']                               # Mac本换成: 'Arial Unicode  MS'
plt.rcParams['axes.unicode_minus'] = False

# todo 1. 定义遍历, 获取常用的字符数量.
# 1. 获取所有的常用字符 → 包括 字母 + 符号.
all_letters = string.ascii_letters + " .,;'"                               # 52个字母(大小写形式) + '空格 点 逗号 分号 单引号'
# 2. 获取常用的字符的数量
n_letters = len(all_letters)

# print('所有常用字符: ', all_letters)                                     # abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ .,;'
# print('常用字符数量: ', n_letters)                                       # 57

# todo 2. 定义遍历, 获取常用国家名 种类数 和 个数.
# 1. 国家名 种类数.
categories = ['Italian', 'English', 'Arabic', 'Spanish', 'Scottish', 'Irish', 'Chinese', 'Vietnamese', 'Japanese', 'French', 'Greek', 'Dutch', 'Korean', 'Polish', 'Portuguese', 'Russian', 'Czech', 'German']
# 2. 国家名 个数.
category_num = len(categories)

print('国家名: ', categories)
print('国家名种类数: ', category_num)                                          # 18个国家名

# todo 3. 定义函数, 实现: 读取源数据到内存.
def read_data(file_path):
    """
    读取源数据到内存中, 并把 特征(人名) 和 标签(国家) 分别存储到两个列表中.
    :param file_path: 源数据文件的路径
    :return: my_list_x: 存储的人名(特征), my_list_y: 存储的国家名(标签)
    """
    # 1. 创建两个列表, 分别存储: 人名(特征), 国家名(标签)
    my_list_x, my_list_y = [], []

    # 2. 关联文件, 并读取其内容(逐行读取)
    with open(file_path, 'r', encoding='utf-8') as f:
        # 3. 遍历, 获取到每一行的数据.
        for line in f.readlines():
            # 4. 过滤无效数据, 假设(整行的)长度 小于等于5, 就过滤掉.  整行长度 = 人名 + '\t' + 国家名
            if len(line) <= 5:
                continue
            # 5. 走到这里, 说明该行数据是有效数据(即: 行长度 > 5), 处理后, 添加到对应的列表中.
            x, y = line.strip().split('\t')
            # 6. 添加到对应的列表中.
            my_list_x.append(x)
            my_list_y.append(y)

    # 扩展: 查看下数据集的长度, 即: 判断数据是否正确.
    print(f'my_list_x: {len(my_list_x)}')        # 20074
    print(f'my_list_y: {len(my_list_y)}')        # 20074

    # 7. 返回解析后的 样本 和 标签.
    return my_list_x, my_list_y


# todo 4. 创建数据集对象, 即: 原始数据 → 数据集对象TensorDataset → 数据加载器DataLoader
class NameClassDataset(Dataset):
    # 1. 初始化函数, 接收: 样本和标签数据, 初始化数据集基本属性.
    def __init__(self, my_list_x, my_list_y):
        self.my_list_x = my_list_x          # 存储样本数据列表
        self.my_list_y = my_list_y          # 存储标签数据列表
        self.sample_len = len(my_list_x)    # 计算样本总数并存储, 20074

    # 2. 定义函数, 用于获取样本总数.      外界用 len(NameClassDataset对象) 的时候, 自动触发.
    def __len__(self):
        return self.sample_len

    # 3. 定义函数, 实现根据指定索引, 获取其对应的样本.
    def __getitem__(self, index):
        """
        根据指定的索引, 获取其对应的样本, 并进行 one-hot编码 和 张量转换.
        :param index: 样本索引
        :return: tensor_x: 人名(特征)的one-hot编码, tensor_y: 国家(标签)的张量表示
        """
        # 1. 索引边界校验, 确保索引在合法范围.    [0, self.sample_len - 1]
        index = min(max(index, 0), self.sample_len - 1)

        # 2. 按照索引获取原始样本 和 标签.
        x = self.my_list_x[index]          # 例如: Ding     → (4, 57)
        y = self.my_list_y[index]          # 例如: Chinese  → 18个国家中的某个索引, 例如: 6

        # 3. 人名数据转换为 one-hot编码.
        # 3.1 生成全0张量
        tensor_x = torch.zeros(len(x), n_letters)          # 例如: [4, 57]
        # 3.2 遍历人名, 获取每个字母, 生成one-hot张量.
        for li, letter in enumerate(x):
            # 3.2.1 获取字母在 全局字母表中的索引位置, 例如: 字母'D' 在 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ .,;'" 中的位置
            letter_index = all_letters.find(letter)
            # 3.2.2 在对应位置设置为1 → 即: one-hot编码
            tensor_x[li][letter_index] = 1

        # 4. 国家数据转换为 张量.
        tensor_y = torch.tensor(categories.index(y), dtype=torch.long)

        # 5. 返回结果
        return tensor_x, tensor_y


# todo 5. 定义函数, 获取数据加载器对象 → 思路: Tensor → TensorDataset → DataLoader
def get_dataloader():
    # 1. 读取数据文件, 获取: 样本(人名)列表 和 标签(国家名)列表.
    my_list_x, my_list_y = read_data(os.path.join(os.path.dirname(__file__), 'data', 'name_classfication.txt'))

    # 2. 创建数据集对象
    name_class_dataset = NameClassDataset(my_list_x, my_list_y)

    # 3. 创建数据加载器对象, 用于批量加载和处理数据.
    # 参1: 数据集对象(20074个人名 和 国家名),  参2: 批次大小, 参3: 是否打乱数据(训练集打乱, 测试集不打乱)
    my_dataloader = DataLoader(name_class_dataset, batch_size=1, shuffle=True)

    # 4. 测试数据加载器, 打印第一批数据(某一个样本的) 形状 和 内容
    for x, y in my_dataloader:
        print(f'x.shape: {x.shape}, x: {x}')        # 人名的张量形状和内容.
        print(f'y.shape: {y.shape}, y: {y}')        # 国家张量形状和内容.
        break                                       # 仅打印第1批次数据, 用于查看, 避免全部输出.

    # 5. 优化1: 可以把上述的数据加载器给返回, 后续直接调用.
    # return my_dataloader


# todo n. 测试代码
if __name__ == '__main__':
    # 1. 读取数据
    # my_list_x, my_list_y = read_data(os.path.join(os.path.dirname(__file__), 'data', 'name_classfication.txt'))

    # 2. 测试: 数据加载器.
    get_dataloader()

NLP 学习

56. 全球人名分类案例_搭建RNN模型(掌握)

python 复制代码
# todo 6. 搭建神经网络模型.
# todo 6.1 搭建RNN网络
class My_RNN(nn.Module):
    # 1. 初始化函数:输入特征维度,隐藏层维度,输出维度,层数.
    def __init__(self, input_size, hidden_size, output_size, n_layers=1):
        # 1.1 初始化父类成员.
        super().__init__()
        # 1.2 输出特征维度(对应字母表大小,即:57个字符)
        self.input_size = input_size
        # 1.3 隐藏层维度,决定模型的表示能力.
        self.hidden_size = hidden_size
        # 1.4 输出维度(对应国家名数量,即:18个国家名)
        self.output_size = output_size
        # 1.5 层数,默认为1.
        self.n_layers = n_layers

        # 1.6 定义RNN层,接收输入特征 和 输出隐藏状态.
        self.rnn = nn.RNN(self.input_size, self.hidden_size, self.n_layers)

        # 1.7 定义全连接层,将RNN的隐藏状态转换成输出.
        self.linear = nn.Linear(self.hidden_size, self.output_size)

        # 1.8 定义激活函数,将输出类别 → 类别的概率分布.
        # 大白话解释:多分类交叉熵损失函数CrossEntropyLoss(新版写法) = NLLLoss损失函数 + LogSoftmax(dim=-1) 旧版写法
        self.softmax = nn.LogSoftmax(dim=-1)        # 优化2: 如果用CrossEntropyLoss损失函数,这行代码可以省略不写.

    # 2. 前向传播函数.
    # 参1: input输入张量,形状为:[seq_len, input_size] → [seq_len, batch_size, input_size]
    # 参2: hidden(隐藏状态),形状为:[n_layers, batch_size, hidden_size]
    def forward(self, input, hidden):
        # 2.1 调整输入张量,添加:batch_size
        input = input.unsqueeze(1)

        # 2.2 通过RNN计算.
        # output: 所有时间步的隐藏状态    hn: 最后1个时间步的隐藏状态
        output, hn = self.rnn(input, hidden)
        # 2.3 提取最后1个时间步的隐藏状态.
        tmp_output = output[-1]              # 形状为:[batch_size, hidden_size]
        # 2.4 通过全连接层,获取输出.
        tmp_output = self.linear(tmp_output)

        # 2.5 数据通过激活函数,映射到概率分布,并返回.
        return self.softmax(tmp_output), hn

    # 3. 初始化隐藏状态,创建全0的初始化隐藏状态.
    def init_hidden(self):
        # 参1: 隐藏层层数,参2: 批次大小,参3: 隐藏层维度.
        return torch.zeros(self.n_layers, 1, self.hidden_size)


# todo 6.2 搭建LSTM网络

# todo 6.3 搭建GRU网络

# todo 7. 测试神经网络模型.
# todo 7.1 测试RNN网络模型

# todo 7.2 测试RNN, LSTM, GRU网络模型

拓展QA:

1. 第 416 行如果改用 CrossEntropyLoss,代码具体怎么改?

全连接层交出原始分数(logits),模型里删掉 LogSoftmax。CrossEntropyLoss 内部就是 LogSoftmax 加 NLLLoss,分数再过一遍 LogSoftmax 会算重。

__init__ 里删掉激活函数这三行:

python 复制代码
# 1.8 定义激活函数,将输出类别 → 类别的概率分布.
# 大白话解释:多分类交叉熵损失函数CrossEntropyLoss(新版写法) = NLLLoss损失函数 + LogSoftmax(dim=-1) 旧版写法
self.softmax = nn.LogSoftmax(dim=-1)

forward 的返回改成直接交全连接的结果:

python 复制代码
# 2.5 返回原始分数(logits)。训练时用 CrossEntropyLoss,它内部会做 LogSoftmax.
return tmp_output, hn

训练里损失函数写成:

python 复制代码
criterion = nn.CrossEntropyLoss()
# output: [batch_size, 18] 的原始分数
# y: 国家下标,形状 [batch_size],dtype 为 long
loss = criterion(output, y)

现在这版配套的是旧写法:模型里 LogSoftmax,训练时用 NLLLoss。两种写法二选一。标签一直是国家下标,不用改成 one-hot。

2. 为什么 n_layers 默认是 1?改成别的数之后,unsqueeze(1) 还能用吗?

n_layers=1 是 nn.RNN 的默认层数,这个案例沿用它。人名很短,一层已经把整个人名收进最后一个隐藏状态。层数只在创建模型时传入,例如 My_RNN(57, 128, 18, n_layers=2)。

input.unsqueeze(1) 在层数改成 2、3 之后照常使用。它在第 1 维插入长度 1,把 [seq_len, input_size] 变成 [seq_len, 1, input_size]。这里的 1 是批次大小,和隐藏层层数无关。

层数变了,跟着变的是隐藏状态的第 0 维。init_hidden 已经写成 torch.zeros(self.n_layers, 1, self.hidden_size),所以不用再改。

以 seq_len 为句子长度、hidden_size 记为 (H):

张量 n_layers=1 n_layers=2
unsqueeze 之后的 input [seq_len, 1, 57] 相同
init_hidden() [1, 1, H] [2, 1, H]
output [seq_len, 1, H] 仍是 [seq_len, 1, H],每个时间步只保留最后一层
hn [1, 1, H] [2, 1, H],两层各一份最终状态
output[-1] [1, H] 仍是 [1, H]
self.linear Linear(H, 18) 不用改

3. a.shape 为 [3, 3, 3] 时,a[-1] 取到的是什么?

a[-1] 只切第 0 维,取最后一片。这一维长度是 3,所以 a[-1] 就是 a[2],结果形状从 [3, 3, 3] 变成 [3, 3]。后面两维原样留下。

把 0 到 26 按顺序填进这个张量,第 0 维是叠在一起的 3 块,每块 3 行 3 列:

text 复制代码
a = torch.arange(27).reshape(3, 3, 3)

a[0]                 a[1]                 a[-1]  也就是 a[2]
┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│  0    1    2 │    │  9   10   11 │    │ 18   19   20 │
│  3    4    5 │    │ 12   13   14 │    │ 21   22   23 │
│  6    7    8 │    │ 15   16   17 │    │ 24   25   26 │
└──────────────┘    └──────────────┘    └──────────────┘
   形状 [3, 3]          形状 [3, 3]          形状 [3, 3]

中括号里写几个下标,就从第 0 维开始连续切几刀。负号表示从该维的末尾数。

写法 切哪一维 结果形状 拿到的数
a[-1] 第 0 维最后一块 [3, 3] 18 到 26 那一块
a[:, -1] 第 1 维最后一行 [3, 3] 每块的最后一行:[6,7,8]、[15,16,17]、[24,25,26]
a[:, :, -1] 第 2 维最后一列 [3, 3] 每行的最后一个数:2、5、8、11、14、17、20、23、26
a[-1, -1, -1] 三维都取最后一个 一个数 26

回到人名分类:output 的形状是 [seq_len, batch_size, hidden_size]。output[-1] 切的是第 0 维,也就是最后一个字母对应的隐藏状态,形状变成 [batch_size, hidden_size]。人名 Li、批次 1、隐藏维度 3 时,output 是两片,output[-1] 是字母 i 那一片:

text 复制代码
output[0]  字母 L     output[-1]  字母 i
┌─────────┐          ┌─────────┐
│ h0 h1 h2│          │ h0 h1 h2│
└─────────┘          └─────────┘
形状 [1, 3]           形状 [1, 3]

4. forward 的 input 为什么是 [seq_len, input_size]?RNN 要的不是 [seq_len, batch, input_size] 吗?

nn.RNN 收到的输入一直是三维,批次维会留到 output 和 hn。forward 的形参写成二维,是因为这次送进来的是一条人名;批次维由 unsqueeze(1) 补上,然后才交给 self.rnn。

__getitem__ 里一条人名做成 [人名长度, 57],例如 Ding 是 [4, 57]。注释里的 [seq_len, input_size] 说的就是这个形参。

text 复制代码
__getitem__          forward 形参              unsqueeze(1) 之后           self.rnn 的输出
[seq_len, 57]   →    [seq_len, 57]        →    [seq_len, 1, 57]      →    output: [seq_len, 1, hidden_size]
一条人名,没有批次        原样接收                  在第 1 维插入 batch=1         hn:     [n_layers, 1, hidden_size]

nn.RNN 默认 batch_first=False,所以三维的顺序是句子长度、批次、特征。批次写在中间,unsqueeze(1) 插的就是这个位置。

数据加载器还会再包一层。batch_size=1 时,for x, y in my_dataloader 拿到的 x 是 [1, seq_len, 57],这个 1 在第 0 维,和 RNN 要的位置不同。训练时先 x = x.squeeze(0) 变回 [seq_len, 57],再送进 forward。直接把加载器的 x 送进去再 unsqueeze(1),会变成四维 [1, 1, seq_len, 57],self.rnn 接不住。

5. 为什么批次直接用 unsqueeze 插成 1?批次不能大于 1 吗?

unsqueeze 插入的新维度长度固定是 1。这里用它,表示这一条人名单独成一批。nn.RNN 的批次维可以大于 1,写成 8 就是 8 条一起算。当前插 1,是因为每个人名长度不同,默认的 DataLoader 叠不成一批。

__getitem__ 按名字本身的长度建张量。Ding 是 [4, 57],Li 是 [2, 57]。DataLoader 组批时用 torch.stack,这一批里每条的形状必须相同。batch_size 大于 1 时,4 和 2 对不齐,加载器直接报错。所以 get_dataloader 把 batch_size 写成 1,每批只有一条,形状总能对齐。

一批里要放多条人名,先把短名字补到这一批里最长的长度,补出来的位置一般填 0。补完之后输入已经是 [最大长度, batch_size, 57],forward 里就不再 unsqueeze。init_hidden 的第 2 维也改成这批的条数:

python 复制代码
def init_hidden(self, batch_size):
    return torch.zeros(self.n_layers, batch_size, self.hidden_size)

补出来的 0 仍会被 RNN 当成字母往下算。还要用 pack_padded_sequence 告诉它每条的真实长度,算到名字结束就停。这一步本案例还没写,所以批次先固定为 1。

6. forward 要手动调用吗?既然是手动调用,直接传入已经带批次的张量不就行了吗?

训练循环里自己写调用,写成 output, hn = model(x, hidden)。model(...) 进入 nn.Module 的 __call__,再执行这个类里的 forward。通常不写 model.forward(...),那样会跳过模块上的钩子。数据加载完之后,PyTorch 不会自己去跑 forward,循环里的这一行就是调用。

当前 forward 约定入参是一条人名,形状 [seq_len, 57],函数内部固定执行 unsqueeze(1)。调用方已经拿着 [seq_len, batch, 57] 再传进来,会再被插入一维,变成四维,self.rnn 接不住。外面自己带好批次,和里面再插一维,只能留一种。

加载器给出的批次还在第 0 维。batch_size=1 时,x 是 [1, seq_len, 57]。nn.RNN 默认要 [seq_len, batch, 57],批次在中间。把这个 x 原样传进当前的 forward,维度顺序对不上。现在的做法是先 squeeze(0),回到 [seq_len, 57],由 forward 用 unsqueeze(1) 把批次放到中间。

调用方如果负责整理成 [seq_len, batch, 57],forward 就改成直接交给 RNN,同时 init_hidden 接收真实的 batch_size:

python 复制代码
def forward(self, input, hidden):
    # input 已经是 [seq_len, batch_size, input_size]
    output, hn = self.rnn(input, hidden)
    tmp_output = output[-1]                  # [batch_size, hidden_size]
    tmp_output = self.linear(tmp_output)
    return self.softmax(tmp_output), hn

这个案例把「批次等于 1」写进模型里面,调用时只传一条人名。

57. 全球人名分类案例_测试RNN模型(理解)

python 复制代码
# todo 7. 测试神经网络模型.
# todo 7.1 测试RNN网络模型
def dm_test_myrnn():
    # 1. 实例化RNN对象
    my_rnn = My_RNN(input_size=57, hidden_size=128, output_size=18)
    # print(f'my_rnn: {my_rnn}')        # my_rnn: My_RNN( (rnn): RNN(57, 128) (linear): Linear(in_features=128, out_features=18, bias=True) (softmax): LogSoftmax(dim=-1) )

    # 2. 准备测试数据,创建一个随机张量,形状为:[seq_len人名长度, input_size词向量维度]
    input = torch.randn(6, 57)         # 例如: ouyang 欧阳
    print(f'input(输入的张量维度): {input.shape}')    # torch.Size([6, 57])

    # 3. 初始化隐藏状态
    # h0 = torch.zeros(1, 1, 128)
    h0 = my_rnn.init_hidden()          # 效果同上.

    # 4. 测试一次性输入完整的一个样本(序列数据)
    output, hn = my_rnn(input, h0)

    # 5. 打印结果.
    print(f'输出的形状: {output.shape}, 输出的内容: {output}')        # [1, 18]
    print(f'隐藏状态的形状: {hn.shape}, 隐藏状态的内容: {hn}')        # [1, 1, 128]

# todo 7.2 测试RNN, LSTM, GRU网络模型


# todo n. 测试代码
if __name__ == '__main__':
    # 1. 读取数据
    # my_list_x, my_list_y = read_data(os.path.join(os.path.dirname(__file__), 'data', 'name_classfication.txt'))

    # 2. 测试: 数据加载器.
    # get_dataloader()

    # 3. 测试RNN模型
    dm_test_myrnn()

58. 全球人名分类案例_搭建LSTM和GRU模型(掌握)

python 复制代码
# todo 6.2 搭建LSTM网络
class My_LSTM(nn.Module):
    # 1. 初始化函数:输入特征维度,隐藏层维度,输出维度,层数.
    def __init__(self, input_size, hidden_size, output_size, n_layers=1):
        # 1.1 初始化父类成员.
        super().__init__()
        # 1.2 输出特征维度(对应字母表大小,即:57个字符)
        self.input_size = input_size
        # 1.3 隐藏层维度,决定模型的表示能力.
        self.hidden_size = hidden_size
        # 1.4 输出维度(对应国家名数量,即:18个国家名)
        self.output_size = output_size
        # 1.5 层数,默认为1.
        self.n_layers = n_layers

        # 1.6 定义LSTM层,接收输入特征 和 输出隐藏状态.
        self.rnn = nn.LSTM(self.input_size, self.hidden_size, self.n_layers)

        # 1.7 定义全连接层,将RNN的隐藏状态转换成输出.
        self.linear = nn.Linear(self.hidden_size, self.output_size)

        # 1.8 定义激活函数,将输出类别 → 类别的概率分布.
        # 大白话解释:多分类交叉熵损失函数CrossEntropyLoss(新版写法) = NLLLoss损失函数 + LogSoftmax(dim=-1) 旧版写法
        self.softmax = nn.LogSoftmax(dim=-1)        # 优化2: 如果用CrossEntropyLoss损失函数,这行代码可以省略不写.

    # 2. 前向传播函数.
    # 参1: input输入张量,形状为:[seq_len, input_size] → [seq_len, batch_size, input_size]
    # 参2: hidden(隐藏状态),形状为:[n_layers, batch_size, hidden_size]
    def forward(self, input, hidden, c):
        # 2.1 调整输入张量,添加:batch_size
        input = input.unsqueeze(1)

        # 2.2 通过LSTM计算.
        # output: 所有时间步的隐藏状态    hidden: 最后1个时间步的隐藏状态
        output, (hn, cn) = self.rnn(input, (hidden, c))
        # 2.3 提取最后1个时间步的隐藏状态.
        tmp_output = output[-1]              # 形状为:[batch_size, hidden_size]
        # 2.4 通过全连接层,获取输出.
        tmp_output = self.linear(tmp_output)

        # 2.5 数据通过激活函数,映射到概率分布,并返回.
        # 返回值1: 预测的类别概率分布,形状:[batch_size, output_size]
        # 返回值2: (最后一个时间步的)隐藏状态张量,形状:[n_layers, batch_size, hidden_size]
        # 返回值3: (最后一个时间步的)细胞状态张量,形状:[n_layers, batch_size, hidden_size]
        return self.softmax(tmp_output), hn, cn

    # 3. 初始化隐藏状态,创建全0的初始化隐藏状态.
    def init_hidden(self):
        # 参1: 隐藏层层数,参2: 批次大小,参3: 隐藏层维度.
        hidden = c = torch.zeros(self.n_layers, 1, self.hidden_size)
        return hidden, c


# todo 6.3 搭建GRU网络
class My_GRU(nn.Module):
    # 1. 初始化函数:输入特征维度,隐藏层维度,输出维度,层数.
    def __init__(self, input_size, hidden_size, output_size, n_layers=1):
        # 1.1 初始化父类成员.
        super().__init__()
        # 1.2 输出特征维度(对应字母表大小,即:57个字符)
        self.input_size = input_size
        # 1.3 隐藏层维度,决定模型的表示能力.
        self.hidden_size = hidden_size
        # 1.4 输出维度(对应国家名数量,即:18个国家名)
        self.output_size = output_size
        # 1.5 层数,默认为1.
        self.n_layers = n_layers

        # 1.6 定义GRU层,接收输入特征 和 输出隐藏状态.
        self.rnn = nn.GRU(self.input_size, self.hidden_size, self.n_layers)

        # 1.7 定义全连接层,将RNN的隐藏状态转换成输出.
        self.linear = nn.Linear(self.hidden_size, self.output_size)

        # 1.8 定义激活函数,将输出类别 → 类别的概率分布.
        # 大白话解释:多分类交叉熵损失函数CrossEntropyLoss(新版写法) = NLLLoss损失函数 + LogSoftmax(dim=-1) 旧版写法
        self.softmax = nn.LogSoftmax(dim=-1)        # 优化2: 如果用CrossEntropyLoss损失函数,这行代码可以省略不写.

    # 2. 前向传播函数.
    # 参1: input输入张量,形状为:[seq_len, input_size] → [seq_len, batch_size, input_size]
    # 参2: hidden(隐藏状态),形状为:[n_layers, batch_size, hidden_size]
    def forward(self, input, hidden):
        # 2.1 调整输入张量,添加:batch_size
        input = input.unsqueeze(1)

        # 2.2 通过RNN计算.
        # output: 所有时间步的隐藏状态    hidden: 最后1个时间步的隐藏状态
        output, hn = self.rnn(input, hidden)
        # 2.3 提取最后1个时间步的隐藏状态.
        tmp_output = output[-1]              # 形状为:[batch_size, hidden_size]
        # 2.4 通过全连接层,获取输出.
        tmp_output = self.linear(tmp_output)

        # 2.5 数据通过激活函数,映射到概率分布,并返回.
        return self.softmax(tmp_output), hn

    # 3. 初始化隐藏状态,创建全0的初始化隐藏状态.
    def init_hidden(self):
        # 参1: 隐藏层层数,参2: 批次大小,参3: 隐藏层维度.
        return torch.zeros(self.n_layers, 1, self.hidden_size)

2026.10.09 13:14

59. 全球人名分类案例_测试三种模型(理解)

python 复制代码
# todo 7. 测试神经网络模型(了解)
# todo 7.2 测试RNN, LSTM, GRU网络模型
def dm_test_rnn_lstm_gru():
    # 1. 定义变量,记录: 输入维度(词向量维度: 57),隐藏层维度(128),输出维度(18,国家数量)
    input_size, n_hidden, output_size = n_letters, 128, category_num
    # input_size, n_hidden, output_size = 57, 128, 18        # 效果同上.

    # 2. 加载数据
    my_list_x, my_list_y = read_data(os.path.join(os.path.dirname(__file__), 'data', 'name_classfication.txt'))
    # 3. 创建数据集对象
    name_class_dataset = NameClassDataset(my_list_x, my_list_y)
    # 4. 创建数据加载器.
    my_dataloader = DataLoader(name_class_dataset, batch_size=1, shuffle=True)
    # 5. 模型初始化.
    my_rnn = My_RNN(input_size, n_hidden, output_size)
    my_lstm = My_LSTM(input_size, n_hidden, output_size)
    my_gru = My_GRU(input_size, n_hidden, output_size)

    # 6. 模型结构可视化.
    print(f'RNN模型结构: {my_rnn}')
    print(f'LSTM模型结构: {my_lstm}')
    print(f'GRU模型结构: {my_gru}')

    # 7. 测试上述的3个模型
    # 7.1 测试RNN模型
    for i, (x, y) in enumerate(my_dataloader):
        print(f'i: {i}')                                          # 编号,第i条数据
        print(f'x: {x}, x.shape: {x.shape}')                      # 输入数据的词向量形式,例如: x.shape: torch.Size([1, 10, 57])
        print(f'y: {y}, y.shape: {y.shape}')                      # 输出数据(国家的编号),例如: y: tensor([15]), y.shape: torch.Size([1])

        # 7.2 初始化隐藏状态.
        hidden = my_rnn.init_hidden()                              # 形状:[1, 1, 128]

        # 7.3 前向传播.
        output, hidden = my_rnn(x[0], hidden)                      # x[0] 等价于:[10, 57]
        print(f'RNN输出形状: {output.shape}, 预测结果: {output}')

        # 扩展: 只训练1个样本,不然太多了,这里看看即可.
        if i == 0:
            break

    # 7.2 测试LSTM模型
    for i, (x, y) in enumerate(my_dataloader):
        print(f'i: {i}')                                          # 编号,第i条数据
        print(f'x: {x}, x.shape: {x.shape}')                      # 输入数据的词向量形式,例如: x.shape: torch.Size([1, 10, 57])
        print(f'y: {y}, y.shape: {y.shape}')                      # 输出数据(国家的编号),例如: y: tensor([15]), y.shape: torch.Size([1])

        # 7.2 初始化隐藏状态.
        hidden, c = my_lstm.init_hidden()                         # 形状:[1, 1, 128]

        # 7.3 前向传播.
        output, hidden, c = my_lstm(x[0], hidden, c)              # x[0] 等价于:[10, 57]
        print(f'LSTM输出形状: {output.shape}, 预测结果: {output}')

        # 扩展: 只训练1个样本,不然太多了,这里看看即可.
        if i == 0:
            break

    # 7.3 测试GRU模型
    for i, (x, y) in enumerate(my_dataloader):
        # print(f'i: {i}')                                          # 编号,第i条数据
        # print(f'x: {x}, x.shape: {x.shape}')                      # 输入数据的词向量形式,例如: x.shape: torch.Size([1, 10, 57])
        # print(f'y: {y}, y.shape: {y.shape}')                      # 输出数据(国家的编号),例如: y: tensor([15]), y.shape: torch.Size([1])

        # 7.2 初始化隐藏状态.
        hidden = my_gru.init_hidden()                              # 形状:[1, 1, 128]

        # 7.3 前向传播.
        output, hidden = my_gru(x[0], hidden)                      # x[0] 等价于:[10, 57]
        print(f'RNN输出形状: {output.shape}, 预测结果: {output}')

        # 扩展: 只训练1个样本,不然太多了,这里看看即可.
        if i == 0:
            break


if __name__ == '__main__':
    # 1. 读取数据
    # my_list_x, my_list_y = read_data(os.path.join(os.path.dirname(__file__), 'data', 'name_classfication.txt'))

    # 2. 测试: 数据加载器.
    # get_dataloader()

    # 3. 测试RNN模型
    # dm_test_myrnn()

    # 4. 测试RNN, LSTM, GRU模型
    dm_test_rnn_lstm_gru()

60. 全球人名分类案例_RNN模型训练(掌握)

python 复制代码
# todo 8. 模型训练.
# 定义变量,记录:学习率,训练的轮数.
my_lr, epochs = 1e-3, 1

# todo 8.1 RNN模型训练.
def train_rnn():
    # 1. 数据准备动作.
    # 1.1 读取数据
    my_list_x, my_list_y = read_data(os.path.join(os.path.dirname(__file__), 'data', 'name_classfication.txt'))
    # 1.2 构建数据集对象.
    name_class_dataset = NameClassDataset(my_list_x, my_list_y)

    # 2. 模型与优化器初始化.
    # 2.1 定义模型参数.
    # 参1: 输入维度(字符表大小),参2: 隐藏层维度,参3: 输出维度(国家数量)
    input_size, n_hidden, output_size = n_letters, 128, category_num        # 等价于: 57, 128, 18
    # 2.2 创建模型对象.
    my_rnn = My_RNN(input_size, n_hidden, output_size)

    # 2.3 定义损失函数和优化器.
    criterion = nn.NLLLoss()        # 如果你用了CrossEntropyLoss(),则它 = NLLLoss() + LogSoftmax()
    optimizer = optim.Adam(my_rnn.parameters(), lr=my_lr)

    # 3. 训练过程 → 参数初始化
    start_time = time.time()          # 模型开始训练时间.
    total_iter_num = 0                 # 已训练的样本数.
    total_loss = 0.0                   # 已训练的损失和
    total_loss_list = []               # 每100个样本求一次平均损失,形成:损失列表.
    total_acc_num = 0                  # 已训练的样本,预测正确的总数
    total_acc_list = []                # 每100个样本求一次平均准确率,形成:准确率列表.

    # 4. 具体的训练过程,按轮数遍历数据集.
    for epoch in range(epochs):        # epoch: 第几轮
        print(f'\n开始第{epoch + 1}/{epochs} 轮训练...')
        # 4.1 创建数据集加载器对象,随机打乱数据集.
        train_dataloader = DataLoader(name_class_dataset, batch_size=1, shuffle=True)
        # 4.2 样本迭代训练,即: 本轮具体的每批次训练
        for i, (x, y) in enumerate(tqdm(train_dataloader)):        # 优化点3: 这里加入进度条.
            # 4.3 前向传播,计算结果.
            output, hidden = my_rnn(x[0], my_rnn.init_hidden())
            # 4.4 计算损失.
            my_loss = criterion(output, y)
            # 4.5 三剑客 → 梯度清零,反向传播,优化器更新参数.
            optimizer.zero_grad()
            my_loss.backward()
            optimizer.step()

            # 4.6 统计训练结果(指标统计)
            total_iter_num += 1                 # 已训练的样本数 + 1
            total_loss += my_loss.item()        # 累计损失值

            # 4.7 计算当前样本预测准确率
            pred_tag = torch.argmax(output).item()
            total_acc_num += (1 if pred_tag == y else 0)        # 统计: 预测正确的样本数

            # 4.8 统计: 每100个样本求一次平均损失,准确率 形成:损失列表,准确率列表.
            if total_iter_num % 100 == 0:
                # 走这里,说明100步了,计算:平均损失.
                avg_loss = total_loss / total_iter_num          # 总损失 / 总样本数
                # 把上述的平均损失,添加到:损失列表.
                total_loss_list.append(avg_loss)

                # 计算准确率,即: 预测正确的 / 总样本数,并添加到:准确率列表.
                avg_acc = total_acc_num / total_iter_num
                total_acc_list.append(avg_acc)

            # 4.9 每2000步(个样本),打印训练日志.
            if total_iter_num % 2000 == 0:
                # 计算平均损失.
                avg_loss = total_loss / total_iter_num
                # 计算模型训练耗时
                end_time = int(time.time() - start_time)
                # 输出训练日志.
                print(f'轮次: {epoch + 1}, 训练的样本数: {total_iter_num}, 平均损失: {avg_loss:.4f}, 耗时: {end_time}s, 准确率: {avg_acc:.4f}')

        # 4.10 走到这里,说明一轮训练完毕 → 保存模型.
        os.makedirs('./model', exist_ok=True)
        torch.save(my_rnn.state_dict(), f'./model/my_rnn_wh02_{epoch + 1}.bin')

    # 5. 走到这里,训练结束,返回统计结果.
    total_time = int(time.time() - start_time)
    print(f'训练完成,总耗时: {total_time}s, 总训练了 {total_iter_num}个样本!!')

    # 6. 优化4: 你可以把下述返回的三个值(损失列表,训练总耗时,准确率列表),存储到文件中.
    #           因为一会儿我们会 可视化3个模型的训练结果,如果没有存储的话,会把 训练动作从新跑一次.

    # 7. 返回结果: 损失列表,训练总耗时,准确率列表.
    return total_loss_list, total_time, total_acc_list

# todo 8.2 LSTM模型训练.

# todo 8.3 GRU模型训练.


if __name__ == '__main__':
    # 1. 读取数据
    # my_list_x, my_list_y = read_data(os.path.join(os.path.dirname(__file__), 'data', 'name_classfication.txt'))

    # 2. 测试: 数据加载器.
    # get_dataloader()

    # 3. 测试RNN模型
    # dm_test_myrnn()

    # 4. 测试RNN, LSTM, GRU模型
    # dm_test_rnn_lstm_gru()

    # 5. 测试: 模型训练.
    train_rnn()

61. 全球人名分类案例_LSTM模型训练(掌握)

python 复制代码
# todo 8.2 LSTM模型训练.
def train_lstm():
    # 1. 数据准备动作.
    # 1.1 读取数据
    my_list_x, my_list_y = read_data(os.path.join(os.path.dirname(__file__), 'data', 'name_classfication.txt'))
    # 1.2 构建数据集对象.
    name_class_dataset = NameClassDataset(my_list_x, my_list_y)

    # 2. 模型与优化器初始化.
    # 2.1 定义模型参数.
    # 参1: 输入维度(字符表大小),参2: 隐藏层维度,参3: 输出维度(国家数量)
    input_size, n_hidden, output_size = n_letters, 128, category_num        # 等价于: 57, 128, 18
    # 2.2 创建模型对象.
    my_rnn = My_LSTM(input_size, n_hidden, output_size)

    # 2.3 定义损失函数和优化器.
    criterion = nn.NLLLoss()        # 如果你用了CrossEntropyLoss(),则它 = NLLLoss() + LogSoftmax()
    optimizer = optim.Adam(my_rnn.parameters(), lr=my_lr)

    # 3. 训练过程 → 参数初始化
    start_time = time.time()          # 模型开始训练时间.
    total_iter_num = 0                 # 已训练的样本数.
    total_loss = 0.0                   # 已训练的损失和
    total_loss_list = []               # 每100个样本求一次平均损失,形成:损失列表.
    total_acc_num = 0                  # 已训练的样本,预测正确的总数
    total_acc_list = []                # 每100个样本求一次平均准确率,形成:准确率列表.

    # 4. 具体的训练过程,按轮数遍历数据集.
    for epoch in range(epochs):        # epoch: 第几轮
        print(f'\n开始第{epoch + 1}/{epochs} 轮训练...')
        # 4.1 创建数据集加载器对象,随机打乱数据集.
        train_dataloader = DataLoader(name_class_dataset, batch_size=1, shuffle=True)
        # 4.2 样本迭代训练,即: 本轮具体的每批次训练
        for i, (x, y) in enumerate(tqdm(train_dataloader)):        # 优化点3: 这里加入进度条.
            # 4.3 前向传播,计算结果.
            hidden, c = my_rnn.init_hidden()
            output, hidden, c = my_rnn(x[0], hidden, c)
            # 4.4 计算损失.
            my_loss = criterion(output, y)
            # 4.5 三剑客 → 梯度清零,反向传播,优化器更新参数.
            optimizer.zero_grad()
            my_loss.backward()
            optimizer.step()

            # 4.6 统计训练结果(指标统计)
            total_iter_num += 1                 # 已训练的样本数 + 1
            total_loss += my_loss.item()        # 累计损失值

            # 4.7 计算当前样本预测准确率
            pred_tag = torch.argmax(output).item()
            total_acc_num += (1 if pred_tag == y else 0)        # 统计: 预测正确的样本数

            # 4.8 统计: 每100个样本求一次平均损失,准确率 形成:损失列表,准确率列表.
            if total_iter_num % 100 == 0:
                # 走这里,说明100步了,计算:平均损失.
                avg_loss = total_loss / total_iter_num          # 总损失 / 总样本数
                # 把上述的平均损失,添加到:损失列表.
                total_loss_list.append(avg_loss)

                # 计算准确率,即: 预测正确的 / 总样本数,并添加到:准确率列表.
                avg_acc = total_acc_num / total_iter_num
                total_acc_list.append(avg_acc)

            # 4.9 每2000步(个样本),打印训练日志.
            if total_iter_num % 2000 == 0:
                # 计算平均损失.
                avg_loss = total_loss / total_iter_num
                # 计算模型训练耗时
                end_time = int(time.time() - start_time)
                # 输出训练日志.
                print(f'轮次: {epoch + 1}, 训练的样本数: {total_iter_num}, 平均损失: {avg_loss:.4f}, 耗时: {end_time}s, 准确率: {avg_acc:.4f}')

        # 4.10 走到这里,说明一轮训练完毕 → 保存模型.
        model_path = os.path.join(os.path.dirname(__file__), 'model', f'my_lstm_wh02_{epoch + 1}.bin')
        os.makedirs(os.path.dirname(model_path), exist_ok=True)
        torch.save(my_rnn.state_dict(), model_path)

    # 5. 走到这里,训练结束,返回统计结果.
    total_time = int(time.time() - start_time)
    print(f'训练完成,总耗时: {total_time}s, 总训练了 {total_iter_num}个样本!!')

    # 6. 优化4: 你可以把下述返回的三个值(损失列表,训练总耗时,准确率列表),存储到文件中.
    #           因为一会儿我们会 可视化3个模型的训练结果,如果没有存储的话,会把 训练动作从新跑一次.

    # 7. 返回结果: 损失列表,训练总耗时,准确率列表.
    return total_loss_list, total_time, total_acc_list

# todo 8.3 GRU模型训练.


if __name__ == '__main__':
    # 1. 读取数据
    # my_list_x, my_list_y = read_data(os.path.join(os.path.dirname(__file__), 'data', 'name_classfication.txt'))

    # 2. 测试: 数据加载器.
    # get_dataloader()

    # 3. 测试RNN模型
    # dm_test_myrnn()

    # 4. 测试RNN, LSTM, GRU模型
    # dm_test_rnn_lstm_gru()

    # 5. 测试: 模型训练.
    # train_rnn()        # 51s
    train_lstm()         # 训练完成,总耗时: 52s, 总训练了 20074个样本!!

62. 全球人名分类案例_GRU模型训练(掌握)

python 复制代码
# todo 8.3 GRU模型训练.
def train_gru():
    # 1. 数据准备动作.
    # 1.1 读取数据
    my_list_x, my_list_y = read_data(os.path.join(os.path.dirname(__file__), 'data', 'name_classfication.txt'))
    # 1.2 构建数据集对象.
    name_class_dataset = NameClassDataset(my_list_x, my_list_y)

    # 2. 模型与优化器初始化.
    # 2.1 定义模型参数.
    # 参1: 输入维度(字符表大小),参2: 隐藏层维度,参3: 输出维度(国家数量)
    input_size, n_hidden, output_size = n_letters, 128, category_num        # 等价于: 57, 128, 18
    # 2.2 创建模型对象.
    my_rnn = My_GRU(input_size, n_hidden, output_size)

    # 2.3 定义损失函数和优化器.
    criterion = nn.NLLLoss()        # 如果你用了CrossEntropyLoss(),则它 = NLLLoss() + LogSoftmax()
    optimizer = optim.Adam(my_rnn.parameters(), lr=my_lr)

    # 3. 训练过程 → 参数初始化
    start_time = time.time()          # 模型开始训练时间.
    total_iter_num = 0                 # 已训练的样本数.
    total_loss = 0.0                   # 已训练的损失和
    total_loss_list = []               # 每100个样本求一次平均损失,形成:损失列表.
    total_acc_num = 0                  # 已训练的样本,预测正确的总数
    total_acc_list = []                # 每100个样本求一次平均准确率,形成:准确率列表.

    # 4. 具体的训练过程,按轮数遍历数据集.
    for epoch in range(epochs):        # epoch: 第几轮
        print(f'\n开始第{epoch + 1}/{epochs} 轮训练...')
        # 4.1 创建数据集加载器对象,随机打乱数据集.
        train_dataloader = DataLoader(name_class_dataset, batch_size=1, shuffle=True)
        # 4.2 样本迭代训练,即: 本轮具体的每批次训练
        for i, (x, y) in enumerate(tqdm(train_dataloader)):        # 优化点3: 这里加入进度条.
            # 4.3 前向传播,计算结果.
            output, hidden = my_rnn(x[0], my_rnn.init_hidden())
            # 4.4 计算损失.
            my_loss = criterion(output, y)
            # 4.5 三剑客 → 梯度清零,反向传播,优化器更新参数.
            optimizer.zero_grad()
            my_loss.backward()
            optimizer.step()

            # 4.6 统计训练结果(指标统计)
            total_iter_num += 1                 # 已训练的样本数 + 1
            total_loss += my_loss.item()        # 累计损失值

            # 4.7 计算当前样本预测准确率
            pred_tag = torch.argmax(output).item()
            total_acc_num += (1 if pred_tag == y else 0)        # 统计: 预测正确的样本数

            # 4.8 统计: 每100个样本求一次平均损失,准确率 形成:损失列表,准确率列表.
            if total_iter_num % 100 == 0:
                # 走这里,说明100步了,计算:平均损失.
                avg_loss = total_loss / total_iter_num          # 总损失 / 总样本数
                # 把上述的平均损失,添加到:损失列表.
                total_loss_list.append(avg_loss)

                # 计算准确率,即: 预测正确的 / 总样本数,并添加到:准确率列表.
                avg_acc = total_acc_num / total_iter_num
                total_acc_list.append(avg_acc)

            # 4.9 每2000步(个样本),打印训练日志.
            if total_iter_num % 2000 == 0:
                # 计算平均损失.
                avg_loss = total_loss / total_iter_num
                # 计算模型训练耗时
                end_time = int(time.time() - start_time)
                # 输出训练日志.
                print(f'轮次: {epoch + 1}, 训练的样本数: {total_iter_num}, 平均损失: {avg_loss:.4f}, 耗时: {end_time}s, 准确率: {avg_acc:.4f}')

        # 4.10 走到这里,说明一轮训练完毕 → 保存模型.
        model_path = os.path.join(os.path.dirname(__file__), 'model', f'my_gru_wh02_{epoch + 1}.bin')
        os.makedirs(os.path.dirname(model_path), exist_ok=True)
        torch.save(my_rnn.state_dict(), model_path)

    # 5. 走到这里,训练结束,返回统计结果.
    total_time = int(time.time() - start_time)
    print(f'训练完成,总耗时: {total_time}s, 总训练了 {total_iter_num}个样本!!')

    # 6. 优化4: 你可以把下述返回的三个值(损失列表,训练总耗时,准确率列表),存储到文件中.
    #           因为一会儿我们会 可视化3个模型的训练结果,如果没有存储的话,会把 训练动作从新跑一次.

    # 7. 返回结果: 损失列表,训练总耗时,准确率列表.
    return total_loss_list, total_time, total_acc_list


if __name__ == '__main__':
    # 1. 读取数据
    # my_list_x, my_list_y = read_data(os.path.join(os.path.dirname(__file__), 'data', 'name_classfication.txt'))

    # 2. 测试: 数据加载器.
    # get_dataloader()

    # 3. 测试RNN模型
    # dm_test_myrnn()

    # 4. 测试RNN, LSTM, GRU模型
    # dm_test_rnn_lstm_gru()

    # 5. 测试: 模型训练.
    # train_rnn()        # 训练完成,总耗时: 51s, 总训练了 20074个样本!!
    # train_lstm()       # 训练完成,总耗时: 52s, 总训练了 20074个样本!!
    train_gru()          # 训练完成,总耗时: 82s, 总训练了 20074个样本!!

63. 全球人名分类案例_三种模型可视化(了解)

python 复制代码
# (作业)
# todo 9. 模型训练绘图 → 这个函数的可视化代码你可以不写,但是模型训练等代码要写出来,出3张图.
def dm_test_train_rnn_lstm_gru():
    # 1. 训练3种模型,并获取性能指标.
    # 参1: 损失列表,参2: 训练总耗时,参3: 准确率列表.
    total_loss_list_rnn, total_time_rnn, total_acc_list_rnn = train_rnn()
    total_loss_list_lstm, total_time_lstm, total_acc_list_lstm = train_lstm()
    total_loss_list_gru, total_time_gru, total_acc_list_gru = train_gru()

    # 2. 绘制 损失对比曲线(评估: 模型收敛速度)
    # 2.1 创建画布.     0:图1
    plt.figure(num=0, figsize=(10, 5))
    # 2.2 绘制各模型损失曲线.
    plt.plot(total_loss_list_rnn, label='RNN')
    plt.plot(total_loss_list_lstm, label='LSTM')
    plt.plot(total_loss_list_gru, label='GRU')
    # 2.3 设置图表属性
    plt.title('模型损失对比曲线')
    plt.xlabel('训练步数(每100步)')
    plt.ylabel('平均损失值')
    plt.grid(visible=True, linestyle='--', alpha=0.7)
    plt.legend(loc='upper left')
    img_dir = os.path.join(os.path.dirname(__file__), 'img')
    os.makedirs(img_dir, exist_ok=True)
    plt.savefig(os.path.join(img_dir, 'loss_comparison.png'))
    plt.show()

    # 3. 绘制 训练耗时对比柱状图(评估: 模型计算效率)
    # 3.1 创建画布.     1: 图2
    plt.figure(num=1, figsize=(10, 5))
    # 3.2 准备x轴 和 y轴标签内容.
    x_data = ['RNN', 'LSTM', 'GRU']
    y_data = [total_time_rnn, total_time_lstm, total_time_gru]

    # 3.3 绘制柱状图
    plt.bar(range(len(x_data)), y_data, tick_label=x_data)

    # 3.4 设置图表属性
    plt.title('模型耗时对比柱状图')
    plt.savefig(os.path.join(img_dir, 'RNN_LSTM_GRU_time.png'))
    plt.show()

    # 4. 绘制 训练准确率对比曲线(评估: 模型效果)
    # 4.1 创建画布.     2: 图3
    plt.figure(num=2, figsize=(10, 5))
    # 4.2 绘制各模型准确率曲线.
    plt.plot(total_acc_list_rnn, label='RNN', color='red')
    plt.plot(total_acc_list_lstm, label='LSTM', color='green')
    plt.plot(total_acc_list_gru, label='GRU', color='orange')
    # 4.3 绘制图表属性.
    plt.title('模型准确率对比曲线')
    plt.legend(loc='upper left')
    plt.savefig(os.path.join(img_dir, 'RNN_LSTM_GRU_acc.png'))
    plt.show()

if __name__ == '__main__':
    # 1. 读取数据
    # my_list_x, my_list_y = read_data(os.path.join(os.path.dirname(__file__), 'data', 'name_classfication.txt'))

    # 2. 测试: 数据加载器.
    # get_dataloader()

    # 3. 测试RNN模型
    # dm_test_myrnn()

    # 4. 测试RNN, LSTM, GRU模型
    # dm_test_rnn_lstm_gru()

    # 5. 测试: 模型训练.
    # train_rnn()        # 训练完成,总耗时: 49s, 总训练了 20074个样本!!
    # train_lstm()       # 训练完成,总耗时: 48s, 总训练了 20074个样本!!
    # train_gru()        # 训练完成,总耗时: 78s, 总训练了 20074个样本!!

    # 6. 测试: 模型训练绘图(即: 效果对比)
    dm_test_train_rnn_lstm_gru()

损失每 100 条记一个点,20074 条人名一轮大约 200 个点。代码里 epochs = 1,所以自己跑出来的损失图横轴停在 200 附近,对应课件左图。课件右图和准确率图横轴到 800,是同一套代码把轮数改成 4 之后的结果。

63.1 损失对比曲线

损失降得快,说明这一段收敛快。

阶段 谁降得快 原因
第 1 轮 RNN,然后是 GRU,最后是 LSTM RNN 参数少,一开始见效快
训到 4 轮 GRU 最低,LSTM 次之,RNN 停在最高 数据变多之后,门控模型把长一点的人名记得更稳

第 1 轮结束时,三条线都从 2 左右降到 1.1 到 1.3。训满 4 轮后,GRU、LSTM 降到 0.7 上下,RNN 还在 1.0 附近。模型怎么选,要看这次任务的曲线,不能按模型名字直接定。

63.2 训练耗时

耗时长短对应计算量。课件的柱状图里,RNN 大约是后两者的一半,GRU 次之,LSTM 最高。LSTM 每个时间步要维护隐藏状态和细胞状态,GRU 少一个细胞状态,传统 RNN 只有一套隐藏状态。

本机这一轮的注释是另一组数:RNN 49 秒,LSTM 48 秒,GRU 78 秒。排名会随机器和这一轮的数据顺序变化,和课件柱状图不必逐根对齐。

63.3 准确率对比曲线

4 轮之后,GRU 最高,大约 0.80;LSTM 紧挨着,大约 0.79;RNN 大约 0.72。三条线开头都在 0.45 附近,分开发生在后面的几百步。

看哪张图 本案例的顺序
第 1 轮损失 RNN 收敛最快,其次 GRU,最后 LSTM
多轮损失和准确率 GRU 最好,其次 LSTM,RNN 最差
课件上的耗时 RNN 最短,其次 GRU,LSTM 最长

综合这三张图,这个人名分类任务选 GRU。它在多轮之后损失更低、准确率更高,耗时又低于 LSTM。

QA:

以 RNN 为例,模型训练按什么顺序走?

和 train_rnn 的书写顺序一致。

  1. 数据三步。read_data 把人名和国家读进内存,NameClassDataset 做成数据集,每一轮里的 DataLoader 再按 batch_size=1 打乱取出。
  2. 模型三件。My_RNN、NLLLoss、Adam。课件把损失写成 CrossEntropyLoss。本案例模型里已经有 LogSoftmax,所以损失用 NLLLoss。改成 CrossEntropyLoss 时,模型里就不再做 LogSoftmax。
  3. 辅助量。start_time、total_iter_num、total_loss、total_loss_list、total_acc_num、total_acc_list。
  4. 外层 for epoch in range(epochs) 控制训几轮。
  5. 内层 for i, (x, y) in enumerate(...) 逐条人名。一次循环里是:x[0] 送进模型,criterion 算损失,梯度清零,反向传播,optimizer.step()。每 100 条把平均损失和准确率放进列表,每 2000 条打印一行日志。
  6. 这一轮的内层循环结束后,torch.save 保存这一轮的 state_dict。
  7. 全部轮次结束后返回 total_loss_list、total_time、total_acc_list。

这个案例选哪一个模型?

选 GRU。

第 1 轮的损失曲线上,RNN 降得最快;把轮数拉长之后,GRU 的损失更低,LSTM 次之。耗时上,课件里 RNN 最短,GRU 居中,LSTM 最长。准确率上,GRU 最高,LSTM 次之,RNN 最低。对着这三张图,GRU 兼顾效果和计算量。

64. 全球人名分类案例_RNN模型预测(掌握)

python 复制代码
# todo 10. 模型预测.
# todo 10.1 RNN模型预测
# todo 10.1.1. 定义变量,记录模型的参数的路径.
my_rnn_path = os.path.join(os.path.dirname(__file__), 'model', 'my_rnn_wh02_1.bin')
my_lstm_path = os.path.join(os.path.dirname(__file__), 'model', 'my_lstm_wh02_1.bin')
my_gru_path = os.path.join(os.path.dirname(__file__), 'model', 'my_gru_wh02_1.bin')

# todo 10.1.2 定义函数,将要预测的人名 转成 one-hot编码,例如: 'zhang' → [5, 57]
def lineToTensor(line):
    # 1. 初始化张量,[文本长度,字符表长度]
    tensor_x = torch.zeros(len(line), n_letters)

    # 2. 遍历文本,获取到每个字符及其索引.
    for i, letter in enumerate(line):
        # 3. 查看字符在全局字母表中的位置(索引)
        letter_index = all_letters.find(letter)
        # 4. 在张量的对应位置改为1,完成: one-hot编码
        tensor_x[i][letter_index] = 1

    # 5. 返回结果.
    return tensor_x        # 即: 'zhang' → [5, 57]

# todo 10.1.3 定义函数,实现: RNN预测.
def dm_predict_rnn(x):
    # 1. 定义变量,记录模型相关参数.
    n_letters, n_hidden, n_categories = 57, 128, 18
    # 2. 把输入的文字转成 one-hot编码.
    x_tensor = lineToTensor(x)
    # 3. 创建模型对象.
    my_rnn = My_RNN(n_letters, n_hidden, n_categories)
    # 4. 加载模型参数.
    my_rnn.load_state_dict(torch.load(my_rnn_path))
    # 5. 进行预测,不计算梯度. → 节省内存和计算机资源.
    with torch.no_grad():
        # 5.1 模型预测
        output, hidden = my_rnn(x_tensor, my_rnn.init_hidden())
        # 5.2 从预测结果中,获取前3个最大的元素.
        # 参1(k):      取前3个最大的元素.
        # 参2(dim):    获取概率最大的元素所在的维度.
        # 参3(largest): 获取概率最大的元素.
        topv, topi = output.topk(3, 1, True)
        # 5.3 打印待预测文本.
        print(f'rnn(待预测文本): {x}')

        # 5.4 解析预测结果.
        for i in range(3):
            value = topv[0][i].item()                  # 概率值 → Python的标量
            category_idx = topi[0][i].item()           # 类别索引
            category = categories[category_idx]        # 类别名称.
            print(f'value: {value}, category: {category}')

# todo 10.2 LSTM模型预测(自己写)
def dm_predict_lstm(x):
    # 1. 定义变量,记录模型相关参数.
    n_letters, n_hidden, n_categories = 57, 128, 18
    # 2. 把输入的文字转成 one-hot编码.
    x_tensor = lineToTensor(x)
    # 3. 创建模型对象.
    my_lstm = My_LSTM(n_letters, n_hidden, n_categories)
    # 4. 加载模型参数.
    my_lstm.load_state_dict(torch.load(my_lstm_path))
    # 5. 切到评估状态. 这个模型没有 Dropout、BatchNorm,数值和训练状态相同.
    my_lstm.eval()
    # 6. 预测,不记录梯度. LSTM 要同时传入隐藏状态和细胞状态.
    with torch.no_grad():
        hidden, c = my_lstm.init_hidden()
        output, hidden, c = my_lstm(x_tensor, hidden, c)
        # 7. 从预测结果中,获取前3个最大的元素.
        topv, topi = output.topk(3, 1, True)
        # 8. 打印待预测文本.
        print(f'lstm(待预测文本): {x}')
        # 9. 解析预测结果.
        for i in range(3):
            value = topv[0][i].item()                  # 对数概率 → Python的标量
            category_idx = topi[0][i].item()           # 类别索引
            category = categories[category_idx]        # 类别名称.
            print(f'value: {value}, category: {category}')

# todo 10.3 GRU模型预测(自己写)
def dm_predict_gru(x):
    # 1. 定义变量,记录模型相关参数.
    n_letters, n_hidden, n_categories = 57, 128, 18
    # 2. 把输入的文字转成 one-hot编码.
    x_tensor = lineToTensor(x)
    # 3. 创建模型对象.
    my_gru = My_GRU(n_letters, n_hidden, n_categories)
    # 4. 加载模型参数.
    my_gru.load_state_dict(torch.load(my_gru_path))
    # 5. 切到评估状态. 这个模型没有 Dropout、BatchNorm,数值和训练状态相同.
    my_gru.eval()
    # 6. 预测,不记录梯度. GRU 和 RNN 一样,只维护一份隐藏状态.
    with torch.no_grad():
        output, hidden = my_gru(x_tensor, my_gru.init_hidden())
        # 7. 从预测结果中,获取前3个最大的元素.
        topv, topi = output.topk(3, 1, True)
        # 8. 打印待预测文本.
        print(f'gru(待预测文本): {x}')
        # 9. 解析预测结果.
        for i in range(3):
            value = topv[0][i].item()                  # 对数概率 → Python的标量
            category_idx = topi[0][i].item()           # 类别索引
            category = categories[category_idx]        # 类别名称.
            print(f'value: {value}, category: {category}')


if __name__ == '__main__':
    # 1. 读取数据
    # my_list_x, my_list_y = read_data(os.path.join(os.path.dirname(__file__), 'data', 'name_classfication.txt'))

    # 2. 测试: 数据加载器.
    # get_dataloader()

    # 3. 测试RNN模型
    # dm_test_myrnn()

    # 4. 测试RNN, LSTM, GRU模型
    # dm_test_rnn_lstm_gru()

    # 5. 测试: 模型训练.
    # train_rnn()        # 训练完成,总耗时: 49s, 总训练了 20074个样本!!
    # train_lstm()       # 训练完成,总耗时: 48s, 总训练了 20074个样本!!
    # train_gru()        # 训练完成,总耗时: 78s, 总训练了 20074个样本!!

    # 6. 测试: 模型训练绘图(即: 效果对比)
    # dm_test_train_rnn_lstm_gru()

    # 7. 测试: 模型预测.
    dm_predict_rnn('zhang')

以 dm_predict_rnn 为例,'zhang' 按这四步预测。

  1. 输入文本张量化。lineToTensor('zhang') 得到 x_tensor,形状 [5, 57]。5 是人名长度,57 是字母表大小,每个字符在自己那一行写成 1。
  2. 实例化模型,加载已训练参数。先 My_RNN(57, 128, 18) 建一个结构相同的空模型,再 load_state_dict(torch.load(my_rnn_path)) 读入 my_rnn_wh02_1.bin。路径用脚本旁边的 model 目录。
  3. 在 with torch.no_grad(): 里做前向。output, hidden = my_rnn(x_tensor, my_rnn.init_hidden())。output 是 [1, 18] 的对数概率,这一段不建计算图。
  4. 取出前 3 名并打印。topv, topi = output.topk(3, 1, True)。topi[0][i] 是国家下标,categories[category_idx] 换成国家名,和 topv 里的对数概率一起打印。

拓展QA:

第 1333 行 with torch.no_grad(): 是干什么的?以前讲过吗?

这行打开一段「不记录梯度」的区域。里面的前向、topk、打印都在这个区域里。预测不调用 loss.backward(),也不调用 optimizer.step(),计算图留着没有用处。关掉记录之后,中间结果不必再挂在图上,内存和记账开销都少一些。离开 with 之后,梯度记录恢复原样。

前向本身照常算。output 仍是 [1, 18] 的对数概率,hidden 仍是最后的隐藏状态。差别只在这些张量没有为反向传播准备一份历史。

笔记里前面没有讲过 torch.no_grad。上一行注释只写了用途:预测时不计算梯度,省内存和计算。深度学习阶段讲过的是 model.train() 和 model.eval(),那一对管 Dropout、BatchNorm 在训练和推理时走哪套算法。这个 RNN 没有这两层,eval() 不改变这次前向的数值。这里要关的是梯度记录,所以用 no_grad。

训练循环里不能套这个区域。my_loss.backward() 要顺着前向留下来的计算图走回每个参数;图没记下来,反向传播就没有路径。

预测和训练的代码正常怎么写?

预测和训练各写一套。dm_predict_rnn 已经把前向放在 torch.no_grad() 里,这个写法对这个 RNN 是对的。习惯上再在前面加一行 eval()。

预测时先切到评估状态,再关掉梯度记录:

python 复制代码
my_rnn.load_state_dict(torch.load(my_rnn_path))
my_rnn.eval()
with torch.no_grad():
    output, hidden = my_rnn(x_tensor, my_rnn.init_hidden())
    topv, topi = output.topk(3, 1, True)

eval() 管 Dropout、BatchNorm 走推理用的算法。这个 My_RNN 里没有这两层,加不加 eval(),算出来的 18 个对数概率一样。no_grad() 才是这里真正起作用的:前向照常算,只是不留下给 backward() 走的计算图。

训练时保持梯度记录,开头用 train(),循环里仍是现在这三步:

python 复制代码
my_rnn.train()
output, hidden = my_rnn(x[0], my_rnn.init_hidden())
my_loss = criterion(output, y)
optimizer.zero_grad()
my_loss.backward()
optimizer.step()

train_rnn 里已经是后三行。外面包上 no_grad() 之后,backward() 就没有计算图可走。

跳过梯度是怎么实现的?不就是不反向传播了吗?

从结果上看,就是没有做反向传播,权重不会变。torch.no_grad() 做的是更早的一步:前向计算时不建计算图,所以后面也没有反向可跑。

PyTorch 里有一个线程上的开关,torch.is_grad_enabled() 能读到它。with torch.no_grad(): 进入时把开关拨到关,离开时拨回原来的状态。前向里每一次乘法、加法、激活都会看这个开关。开关开着,这一步的结果挂上 grad_fn,并留下反向要用的中间值。开关关着,这一步只算出数,output.grad_fn 是空的,中间值不保存。

不写 backward(),反向确实没跑,但前向时图已经记过了,用完再丢。no_grad 是前向当时就不记这张图。

前向建图 调用 backward() 权重
训练 建 调用,梯度写进参数的 .grad,再 step() 更新
预测时只是不写 backward() 仍然建,算完图被丢掉 不调用 不更新,但前向已经花了建图的内存
with torch.no_grad() 不建 没图可走 不更新,前向也不记账

预测函数里两件事叠在一起:不建图,也不调用 backward()。只去掉 backward() 已经不会改权重。

预测时不做反向传播,正确率不就会变差吗?

预测阶段梯度正常就是不需要的,因为它不影响这次的预测结果。前向只用已经训好的权重算出 18 个对数概率,topk 再取出前 3 个国家。梯度是给训练时的 backward() 和 optimizer.step() 用的,用来改权重。预测不改权重,所以这份记录可以不建。torch.no_grad() 就是把这件事关掉。

正确率是训练阶段攒出来的。每一条人名走完前向、backward()、optimizer.step(),权重才挪一点。训完写进 my_rnn_wh02_1.bin 的就是这份权重。预测时 load_state_dict 把它读回来,再只做前向。这一步不改权重,所以有没有 backward(),打印出来的国家和对数概率都一样。

torch.no_grad() 也不改这些数。它只是前向时不建计算图,乘法和 LogSoftmax 的结果与建图时相同。

正确率会差,发生在训练阶段就关掉反向传播的时候。权重停在随机初始化,模型没有从 20074 条人名里学到东西,预测就接近乱猜。

65. 全球人名分类案例_思路总结(理解)

自己联系代码如下:

python 复制代码
"""
晚上练习,在 dm01 的基础上做这几件事:
    1. 先用 RNN 跑一版: 导包 → 数据预处理 → 构建模型 → 训练 → 预测
    2. 再用 LSTM 跑一版
    3. 再用 GRU 跑一版
    4. 最终版: 三个模型合在一起,训练结果绘图
    5. 优化:
        1. 损失改用 CrossEntropyLoss,模型不再写 LogSoftmax
        2. get_dataloader 直接返回数据加载器
        3. 损失、耗时、准确率列表存到文件,绘图时直接加载
"""
import os

os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'

import time
import string

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import matplotlib.pyplot as plt
from tqdm import tqdm

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_PATH = os.path.join(BASE_DIR, '..', 'data', 'name_classfication.txt')
MODEL_DIR = os.path.join(BASE_DIR, 'model')
RESULT_DIR = os.path.join(BASE_DIR, 'result')
IMG_DIR = os.path.join(BASE_DIR, 'img')

all_letters = string.ascii_letters + " .,;'"
n_letters = len(all_letters)
categories = ['Italian', 'English', 'Arabic', 'Spanish', 'Scottish', 'Irish', 'Chinese', 'Vietnamese', 'Japanese', 'French', 'Greek', 'Dutch', 'Korean', 'Polish', 'Portuguese', 'Russian', 'Czech', 'German']
category_num = len(categories)

my_lr, epochs = 1e-3, 1
HIDDEN_SIZE = 128


def read_data(file_path):
    my_list_x, my_list_y = [], []
    with open(file_path, 'r', encoding='utf-8') as f:
        for line in f.readlines():
            if len(line) <= 5:
                continue
            x, y = line.strip().split('\t')
            my_list_x.append(x)
            my_list_y.append(y)
    print(f'my_list_x: {len(my_list_x)}')
    print(f'my_list_y: {len(my_list_y)}')
    return my_list_x, my_list_y


class NameClassDataset(Dataset):
    def __init__(self, my_list_x, my_list_y):
        self.my_list_x = my_list_x
        self.my_list_y = my_list_y
        self.sample_len = len(my_list_x)

    def __len__(self):
        return self.sample_len

    def __getitem__(self, index):
        index = min(max(index, 0), self.sample_len - 1)
        x = self.my_list_x[index]
        y = self.my_list_y[index]
        tensor_x = torch.zeros(len(x), n_letters)
        for li, letter in enumerate(x):
            tensor_x[li][all_letters.find(letter)] = 1
        tensor_y = torch.tensor(categories.index(y), dtype=torch.long)
        return tensor_x, tensor_y


def get_dataloader():
    """读取数据并直接返回 DataLoader。每轮训练再用同一份数据集重新打乱。"""
    my_list_x, my_list_y = read_data(DATA_PATH)
    dataset = NameClassDataset(my_list_x, my_list_y)
    return DataLoader(dataset, batch_size=1, shuffle=True)


class My_RNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size, n_layers=1):
        super().__init__()
        self.n_layers = n_layers
        self.hidden_size = hidden_size
        self.rnn = nn.RNN(input_size, hidden_size, n_layers)
        self.linear = nn.Linear(hidden_size, output_size)

    def forward(self, input, hidden):
        input = input.unsqueeze(1)
        output, hn = self.rnn(input, hidden)
        # 返回原始分数。CrossEntropyLoss 内部会做 LogSoftmax。
        return self.linear(output[-1]), hn

    def init_hidden(self):
        return torch.zeros(self.n_layers, 1, self.hidden_size)


class My_LSTM(nn.Module):
    def __init__(self, input_size, hidden_size, output_size, n_layers=1):
        super().__init__()
        self.n_layers = n_layers
        self.hidden_size = hidden_size
        self.rnn = nn.LSTM(input_size, hidden_size, n_layers)
        self.linear = nn.Linear(hidden_size, output_size)

    def forward(self, input, hidden, c):
        input = input.unsqueeze(1)
        output, (hn, cn) = self.rnn(input, (hidden, c))
        return self.linear(output[-1]), hn, cn

    def init_hidden(self):
        hidden = torch.zeros(self.n_layers, 1, self.hidden_size)
        c = torch.zeros(self.n_layers, 1, self.hidden_size)
        return hidden, c


class My_GRU(nn.Module):
    def __init__(self, input_size, hidden_size, output_size, n_layers=1):
        super().__init__()
        self.n_layers = n_layers
        self.hidden_size = hidden_size
        self.rnn = nn.GRU(input_size, hidden_size, n_layers)
        self.linear = nn.Linear(hidden_size, output_size)

    def forward(self, input, hidden):
        input = input.unsqueeze(1)
        output, hn = self.rnn(input, hidden)
        return self.linear(output[-1]), hn

    def init_hidden(self):
        return torch.zeros(self.n_layers, 1, self.hidden_size)


MODEL_MAP = {
    'rnn': My_RNN,
    'lstm': My_LSTM,
    'gru': My_GRU,
}


def build_model(model_name):
    return MODEL_MAP[model_name](n_letters, HIDDEN_SIZE, category_num)


def forward_one(model, model_name, x):
    if model_name == 'lstm':
        hidden, c = model.init_hidden()
        output, hidden, c = model(x, hidden, c)
        return output
    output, hidden = model(x, model.init_hidden())
    return output


def record_path(model_name):
    return os.path.join(RESULT_DIR, f'{model_name}_record.pt')


def weight_path(model_name):
    return os.path.join(MODEL_DIR, f'my_{model_name}_1.bin')


def save_record(model_name, loss_list, total_time, acc_list):
    os.makedirs(RESULT_DIR, exist_ok=True)
    torch.save({
        'loss': loss_list,
        'time': total_time,
        'acc': acc_list,
    }, record_path(model_name))
    print(f'记录已保存: {record_path(model_name)}')


def load_record(model_name):
    path = record_path(model_name)
    if not os.path.exists(path):
        return None
    record = torch.load(path)
    print(f'记录已加载: {path}')
    return record


def train_one(model_name):
    """训练一种模型,并把损失、耗时、准确率列表存到文件。"""
    loader = get_dataloader()
    dataset = loader.dataset
    model = build_model(model_name)
    model.train()
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=my_lr)

    start_time = time.time()
    total_iter_num = 0
    total_loss = 0.0
    total_loss_list = []
    total_acc_num = 0
    total_acc_list = []

    for epoch in range(epochs):
        print(f'\n开始第{epoch + 1}/{epochs} 轮 {model_name.upper()} 训练...')
        train_dataloader = DataLoader(dataset, batch_size=1, shuffle=True)
        for x, y in tqdm(train_dataloader):
            output = forward_one(model, model_name, x[0])
            my_loss = criterion(output, y)
            optimizer.zero_grad()
            my_loss.backward()
            optimizer.step()

            total_iter_num += 1
            total_loss += my_loss.item()
            pred_tag = torch.argmax(output).item()
            total_acc_num += (1 if pred_tag == y.item() else 0)

            if total_iter_num % 100 == 0:
                total_loss_list.append(total_loss / total_iter_num)
                total_acc_list.append(total_acc_num / total_iter_num)
            if total_iter_num % 2000 == 0:
                avg_loss = total_loss / total_iter_num
                avg_acc = total_acc_num / total_iter_num
                used = int(time.time() - start_time)
                print(f'轮次: {epoch + 1}, 训练的样本数: {total_iter_num}, 平均损失: {avg_loss:.4f}, 耗时: {used}s, 准确率: {avg_acc:.4f}')

        os.makedirs(MODEL_DIR, exist_ok=True)
        torch.save(model.state_dict(), weight_path(model_name))

    total_time = int(time.time() - start_time)
    print(f'{model_name.upper()} 训练完成,总耗时: {total_time}s, 总训练了 {total_iter_num}个样本!!')
    save_record(model_name, total_loss_list, total_time, total_acc_list)
    return total_loss_list, total_time, total_acc_list


def line_to_tensor(line):
    tensor_x = torch.zeros(len(line), n_letters)
    for i, letter in enumerate(line):
        tensor_x[i][all_letters.find(letter)] = 1
    return tensor_x


def predict_one(model_name, text):
    model = build_model(model_name)
    model.load_state_dict(torch.load(weight_path(model_name)))
    model.eval()
    x_tensor = line_to_tensor(text)
    with torch.no_grad():
        output = forward_one(model, model_name, x_tensor)
        topv, topi = output.topk(3, 1, True)
        print(f'{model_name}(待预测文本): {text}')
        for i in range(3):
            value = topv[0][i].item()
            category = categories[topi[0][i].item()]
            print(f'value: {value}, category: {category}')


def train_and_predict(model_name, text='zhang'):
    train_one(model_name)
    predict_one(model_name, text)


def load_three_records():
    records = {}
    for name in ('rnn', 'lstm', 'gru'):
        record = load_record(name)
        if record is None:
            return None
        records[name] = record
    return records


def plot_three(records):
    os.makedirs(IMG_DIR, exist_ok=True)
    names = ('rnn', 'lstm', 'gru')
    labels = ('RNN', 'LSTM', 'GRU')

    plt.figure(num=0, figsize=(10, 5))
    for name, label in zip(names, labels):
        plt.plot(records[name]['loss'], label=label)
    plt.title('模型损失对比曲线')
    plt.xlabel('训练步数(每100步)')
    plt.ylabel('平均损失值')
    plt.grid(visible=True, linestyle='--', alpha=0.7)
    plt.legend(loc='upper left')
    plt.savefig(os.path.join(IMG_DIR, 'loss_comparison.png'))
    plt.show()

    plt.figure(num=1, figsize=(10, 5))
    x_data = list(labels)
    y_data = [records[name]['time'] for name in names]
    plt.bar(range(len(x_data)), y_data, tick_label=x_data)
    plt.title('模型耗时对比柱状图')
    plt.savefig(os.path.join(IMG_DIR, 'RNN_LSTM_GRU_time.png'))
    plt.show()

    colors = ('red', 'green', 'orange')
    plt.figure(num=2, figsize=(10, 5))
    for name, label, color in zip(names, labels, colors):
        plt.plot(records[name]['acc'], label=label, color=color)
    plt.title('模型准确率对比曲线')
    plt.legend(loc='upper left')
    plt.savefig(os.path.join(IMG_DIR, 'RNN_LSTM_GRU_acc.png'))
    plt.show()


def run_final():
    """最终版: 三个记录都在就直接加载绘图,缺哪个就先训练哪个。"""
    records = {}
    for name in ('rnn', 'lstm', 'gru'):
        record = load_record(name)
        if record is None:
            loss_list, total_time, acc_list = train_one(name)
            record = {'loss': loss_list, 'time': total_time, 'acc': acc_list}
        records[name] = record
    plot_three(records)


if __name__ == '__main__':
    # 1. 先用 RNN 把案例跑一版.
    # train_and_predict('rnn')

    # 2. 再用 LSTM 把案例跑一版.
    # train_and_predict('lstm')

    # 3. 再用 GRU 做一版.
    # train_and_predict('gru')

    # 4. 最终版: 三个模型合并,训练结果绘图。已有记录就直接加载。
    run_final()

66. 注意力机制介绍(了解)

注意力机制的本质:用有限的精力,从海量信息里快速筛出高价值信息。人脑天生会抓重点、放过次要信息,这样才能处理世界上那么多信息。

66.1 从人眼看注意力

人类视觉在长期进化里形成了一套注意力机制,它是大脑的信号处理方式。看一张图时,眼睛先快速扫一遍全局,再把精力放到需要重点看的区域。

右图是热力图。颜色越亮,注意力放得越多。人的注意力资源有限,会从大量信息里挑出高价值的部分,例如人脸、文本标题、文章首句。

这套做法可以引进深度神经网络:让模型也把算力花在输入里更重要的位置。

66.2 注意力机制是什么

观察一件事物时能很快判断它是什么,是因为大脑把注意力放在最有辨识度的地方。判断来自这个重点,不必先把事物从头到尾看完。注意力机制就是按这个想法做出来的。

它是深度学习里的常用技术。处理序列时,模型可以按需要去看输入的不同部分。

用在哪 具体任务
自然语言处理 机器翻译(例如 Transformer)、文本摘要、问答
图像 图像描述生成、图像分类
多模态 图像和文本之间的对应关系

它帮助模型理解和利用输入里真正有用的信息。

66.3 为什么要在 RNN 之外再引入注意力

人名分类里,RNN、LSTM、GRU 都是一个时间步接一个时间步往前走,最后用最后一个时间步的隐藏状态做预测。和这种提特征的方式比,注意力解决的是下面两件事。

RNN 怎么提特征 带注意力的模型怎么提特征
计算方式 必须按时间步顺序处理,前一步算完才能算下一步,无法并行,效率低 各位置可以并行提取特征
长序列 前面词的特征会慢慢忘掉,序列里没有重点,抓不住全局关键信息 能注意到关键点,动态关注输入里重要的部分,减轻长信息遗忘
结果 整句压成最后一个隐藏状态再往下用 提特征更灵活

课堂上的翻译例子:英文 Welcome to WuHan! 译成「欢迎」时,注意力权重可以是 0.8, 0.1, 0.1。「欢迎」主要对齐 Welcome,其余两个词只分到很少的权重。模型不必把整句平均看完,也不必只靠最后一个词的隐藏状态。

下一问留到实现:特征张量怎么送进注意力机制,注意力又按哪几步把事物特征提出来。

67. 注意力机制_QKV简介(理解)

QKV 是注意力的输入写法。三者分工明确,一起完成「聚焦关键信息」:Query 是问题,Key 是索引,Value 是内容。模型用 Query 去和每个 Key 比相似度,再按这个权重从 Value 里取出实际内容。

名字 是什么 在计算里干什么
Query(Q,查询向量) 当前要解决的问题,触发注意力计算的需求向量 表达这次的任务需求
Key(K,键向量) 被关注内容的索引,也就是关键字 和 Q 算相似度,标出最相关的位置
Value(V,值向量) 被关注的实际内容 按相似度权重加权,得到最终结果

三步合在一起:

  1. 用 Query 写出当前要做什么。
  2. 用 Key 当索引,和 Query 算相似度,找到最相关的信息在哪。
  3. 用 Value 当实际内容,按相似度权重输出最终结果。

Q、K、V 是注意力的核心组件。它们配合起来,模型就能按需要去看输入的不同部分,表达能力也跟着变强。

67.1 档案柜里找文件

档案柜 对应谁
Q 正在研究的课题,写在便利贴上 查询向量
K 文件夹上的标签,例如 key#1、key#2 索引
V 文件夹里的文档、书籍 要查的内容

便利贴先和各个标签比相关程度,对上的那个文件夹打开,里面的资料才是要拿的内容。Q 和 K 比的是相似度,关心的内容从 V 里取。

67.2 三个任务里的 QKV

任务 Query Key Value 怎么配合
新闻问答 这个新闻的主题是关于什么的? 这个新闻报道了当地政府的新政策。 这个新闻说了政府的新政策是什么。 Q 在问主题。Q 和 K 一对,对上「新政策」这条。V 再给出报道里的具体内容
文本生成 请给我生成一段关于环保的短文。 环保。 我们应该采取一系列措施来保护环境。 Q 和 K 一对,确定要写的是环保。V 给出一条具体建议,用来生成短文
文本摘要 这篇文章的主要内容是什么? 这篇文章介绍了最新的科学发现。 最新的科学发现证实了某种假设。 Q 和 K 一对,确定主要内容是科学发现。V 提供发现的具体内容,用来写摘要

翻译例子接上一节:Welcome to WuHan! 译成「欢迎 来 武汉」,相似度权重是 0.8, 0.1, 0.1。权重最大的位置提供主要的 Value,其余位置只占很少一份。

下一问留到实现:QKV 这种输入已经清楚了,注意力具体按哪几步算出来。

68. 注意力机制_实现步骤介绍(理解)

注意力的计算就是两步:先算权重,再加权求和。attention 是关于 Q、K、V 的函数,普通的 q 经过这次运算,升级成一个融合了相关内容的、更强的 q。

attention(Q, K, V) \\rightarrow attention_weights,\\ attention_q

步 在算什么 得到什么
第 1 步 查询张量 q 和每个 key 做相似度运算。点积、余弦相似度都可以 注意力权重分布 attention_weights,一组权重系数
第 2 步 用这组权重去乘内容 v,再加起来 注意力的结果表示 attention_q

第 1 步是拿当前问题(Query)和所有索引(Key)比,算出每个索引和这个问题有多相关,得到权重数组。第 2 步把上一步的重要程度系数乘到对应的 value 上,全部加起来,就是最终聚出来的向量。

it 指的是谁

句子来自机器人第二定律:

A robot must obey the orders given it by human beings except where such orders would conflict with the First Law.

机器人必须服从人类给它的命令,除非这些命令与第一定律相冲突。第一定律是:机器人不能伤害人类。

阅读理解要问的是:句中的 it 指代谁。这句话拆成 QKV:

这个例子里是谁
q 要查的问题,就是 it 的词向量
k 每个单词的索引
v 每个单词的词向量

文本大约 21 个词,开头是 <s> a robot must obey the orders given it ...。q 是 it 这一个词,所以它和 21 个词各算一个相似度,权重分布的形状是 [1, 21]。课件表格里靠前的几行是:

词 Q 和 K 的相似度(权重)
<s> 0.001
a 0.3
robot 0.5
must 0.002
obey 0.001
the 0.0003
orders 0.005
given 0.002
it 0.19

权重大的三处:robot 占 50%,a 占 30%,it 自己占 19%。其余词接近 0。

第 2 步按这个权重把词向量加起来。课件假设每个词向量是 3 维,加权之后仍然是 3 维:

attention_q = 0.001 \\cdot v_{} + 0.3 \\cdot v_{a} + 0.5 \\cdot v_{robot} + \\cdots + 0.19 \\cdot v_{it}

attention_q 已经不是孤立的 it 向量。它把整句里最相关的信息融了进去,一半以上来自 robot。模型据此判断 it 最可能指 robot,指代就消掉了。

这就是「普通的 q 升级成一个更加强大的 q」:输出仍是一个向量,里面动态聚合了和当前任务最相关的内容。

总结QA:

1. 说一说什么是注意力机制?

深度学习里提特征时用的一种技术。模型把精力放在事物最有辨识度的地方,从而更好地利用输入里的信息。近几年它在计算机视觉、自然语言处理、多模态里都在用。

2. 与 RNN 提取事物特征对比,说一说为什么要引入注意力机制?

RNN 一个时间步接一个时间步提特征,串行,效率低。序列一长,前面单词的特征会遗忘,没有重点。带注意力的模型可以并行提特征,效率高,而且能注意到关键点。提特征因此更灵活。

3. 说一说注意力机制的 QKV 代表什么?

Q 是查询张量,相当于正在研究的课题,比如写在便利贴上。K 是索引张量,相当于文件夹上贴的标签,比如 key#1、key#2。V 是内容张量,相当于正在查找的内容,比如文档、书籍资料。Q 和 K 比较相关性(相似度),得到注意力的权重分布,再用这组权重对 V 加权求和,得到新的注意力结果,也就是一个新的、更加强大的 q。

4. 说一说注意力机制的实现步骤?

第 1 步:用查询张量 q 与 key 做相似度运算,得到注意力的权重分布,也就是一组权重系数。第 2 步:用权重分布乘以内容,得到注意力的结果表示。最终目标是 attention(Q, K, V) --> attention_weights, attention_q。attention 是关于 QKV 的函数,通过这次运算,普通的 q 升级成一个更加强大的 q。

单选题:

1. 在自然语言处理中,注意力机制的作用是()。

  • A. 提升计算速度
  • B. 提取文本特征
  • C. 帮助模型集中关注输入的不同部分
  • D. 减少模型的复杂度

答案:C。帮助模型集中关注输入的不同部分。

2. 在注意力机制中,Q、K、V 分别代表什么意思?

  • A. Query、Key、Value
  • B. Question、Knowledge、Verification
  • C. Qualify、Knowledge、Value
  • D. Query、Keyword、Variable

答案:A。Query、Key、Value。

3. 注意力机制的一个重要应用是在机器翻译中,它如何帮助提升翻译质量?

  • A. 增加模型的参数
  • B. 使模型能够根据源语言和目标语言的不同部分动态调整关注度
  • C. 缩短训练时间
  • D. 增加模型的深度

答案:B。使模型能够根据源语言和目标语言的不同部分动态调整关注度。课件解析写的是:就是给模型增加注意力机制。

69. Seq2Seq架构_任务介绍(理解)

Seq2Seq(Sequence-to-Sequence)把一段输入序列先压成一个中间表示,再从这个中间表示生成目标序列。机器翻译、文本摘要、对话系统都是这种「序列进、序列出」的任务。

架构分成三部分:编码器(Encoder)、解码器(Decoder)、中间语义张量 (C)。课件里把 (C) 叫信息包:编码器把整句收成这一个包,交给解码器去生成。

部分 干什么
Encoder 理解输入。一个时间步接一个时间步编码,每步都有隐藏层输出,最后收成中间语义张量 (C)
(C) 固定长度的向量,装着整句的语义
Decoder 生成输出。一个时间步接一个时间步解码,每步都用到 (C)

69.1 编码器

编码器把输入序列(例如一段文本)映射成中间语义张量 (C),通常是一个固定长度的向量。常用结构是 RNN 或 LSTM,近年来也包括 Transformer。

输入文本的每个时间步都产生一个隐藏状态。这个隐藏状态抓住当前步的信息,再传给下一步。整句处理完后,最后一个时刻的隐藏状态当作解码器的初始隐藏状态。

以「欢迎 来 北京」为例,三个词先过 Embedding,得到词向量 (v_0, v_1, v_2)。编码器每步的输入是这次的词向量,再加上一时刻的隐藏状态:

步 本次输入 上一时刻隐藏状态 本步输出
欢迎 (v_0) (h_0) (h_1)
来 (v_1) (h_1) (h_2)
北京 (v_2) (h_2) 再往下收成 (C)

图上是三个 GRU:(h_0 \rightarrow h_1 \rightarrow h_2),最后合成信息包 (C),传给解码器。

69.2 解码器

解码器也是 RNN 系列模型。它收下编码器传来的初始隐藏状态,再一步步生成目标序列。生成过程里通常还会加注意力,让它在每个词上动态去看输入序列的不同部分。

解码按自回归进行:上一时间步的输出,当作下一时间步的输入。每个时间步的输入是当前 input 和上一隐藏状态 (h_{t-1}),输出是这一步的 output 和 (h_t)。output 再接一个全连接层加 softmax 做分类,从分类结果里取出预测词。每一步都会用到中间语义张量 (C)。

「欢迎 来 北京」译成英文时,解码从 _GO 开始,拿到 (C),结合解码器自己的 Embedding,用 GRU 生成英文。每步都送入 (C),上一时刻的隐藏状态是 (s_0, s_1, s_2)。生成顺序是:

_GO → welcome → to → BeiJing → _EOS

_EOS 表示句子生成结束。

69.3 (C) 的长度问题和注意力

原生 Seq2Seq 的 (C) 长度是固定的。输入一长,前面的信息会丢,翻译和生成的效果就下降。

改进是加上注意力。解码器每生成一个词,都能动态关注输入序列里对应的那一部分,长序列也好处理。注意力解决的是「信息利用不够细」:一个信息包把整句揉在一起,生成时对不上原词。

正在生成 多看编码器里的哪一部分
welcome 「欢迎」
to 「来」
BeiJing 「北京」

这样比只靠一个信息包更细。生成每个词时都能回到原文里找对应信息,翻译更准,也更自然。

70. Seq2Seq架构_加入注意力机制(理解)

注意力加在解码里。解码仍是一个时间步接一个时间步,但每一步都要引入 QKV,把这一步普通的查询向量 q 升级成更强的 (q^{\wedge}),再送去解码。

70.1 每个解码步做什么

不加注意力时,把当前要生成的词的查询张量 q 送给 GRU 解码,再经全连接做分类,得到这个词的词向量表示。例如生成 welcome,送进去的就是 welcome 的 q。

加上注意力之后,这一步先把 q 升级成 (q^{\wedge})。生成 welcome 时,先看它和「欢迎」「来」「北京」的权重分布,再用这组权重对中间语义张量 (C) 加权求和,得到新的 (q^{\wedge})。(q^{\wedge}) 才送给 GRU,后面仍是全连接加分类。

图上两次生成的权重是:

正在生成 对「欢迎」「来」「北京」的权重
welcome 0.8,0.1,0.1
to 0.3,0.5,0.2

welcome 的大头在「欢迎」上,to 的大头在「来」上。

70.2 这里的 q、k、v

在这次解码里是谁
q 词嵌入之后的张量。查询目标是谁,q 就是谁的查询张量。生成 welcome 时,q 就是 welcome 的查询张量。这个目标要先定下来
k 上一时间步的隐藏层输出。用来和 q 算相似度,得到对「欢迎」「来」「北京」的权重分布
v 中间语义张量 (C)。权重分布乘上 (C),加权求和,得到 (q^{\wedge})

70.3 权重一开始不准怎么办

生成 to 时,假如算出的权重是 [0.1, 0.8, 0.1],分布不准,不用在这一步里手工改。

注意力要放在整个网络的三件套里看:前向计算、损失函数、反向传播。它只是前向路径里的一小段。每一步先照常算出权重和 (q^{\wedge})。权重参数准不准,靠损失函数和反向传播去更新注意力层,模型从数据里把「该看哪个词」学出来。

70.4 练习

1. 添加注意力机制后,Seq2Seq 模型在生成输出时的特点是()。

  • A. 模型会根据输入的不同部分动态调整关注度
  • B. 模型的生成过程与没有注意力机制时无区别
  • C. 模型会忽略输入的不同部分,只关注整体信息
  • D. 注意力机制只会影响模型的训练速度,不会影响生成结果的准确性

答案:A。模型会根据输入的不同部分动态调整关注度。

2. 有关注意力机制的下列说法正确的是?(多选)

  • A. seq2seq 架构中添加注意力机制,一般是在解码的时候添加
  • B. seq2seq 架构编码的时候,可以使用 RNN 提取文本序列特征,在解码的时候可以使用 LSTM 提取事物特征
  • C. 注意力机制只是前向计算中的一个小的计算路径。一开始每个时间步产生的注意力权重分布可能不准确,需要通过反向传播来学习事物特征,进而让模型能从数据中快速获取事物特征
  • D. 注意力机制很重要,但是不要忘记整个神经网络三件事(前向计算、损失函数、反向传播)组合在一起,相互配合才行

答案:ABCD。要从更高、更深的角度理解注意力机制。

71. 扩展1_普通的Encoder_Decoder框架(理解)

普通 Encoder-Decoder 是由一个句子(或篇章)生成另一个句子(或篇章)的通用模型。给定输入句子 Source,经过这个框架生成目标句子 Target。Source 和 Target 可以是同一种语言,也可以是两种语言,各自都是单词序列。图上是 (X_1, X_2, X_3, X_4) 进编码器,语义编码 (c) 再进解码器,出来 (Y_1, Y_2, Y_3)。输入长度和输出长度可以不一样。

Source = \\langle X_1, X_2 \\cdots X_m \\rangle

Target = \\langle y_1, y_2 \\cdots y_n \\rangle

71.1 编码和解码各算什么

编码器对 Source 做非线性变换,得到中间语义表示 (C)。课件里这个变换写成 GRU 那一类:

C = F(X_1, X_2 \\cdots X_m)

解码器的任务是:根据 (C),再加上已经生成的历史 (y_1, y_2 \cdots y_{i-1}),生成第 (i) 个词 (y_i)。

y_i = G(C, y_1, y_2 \\cdots y_{i-1})

拆开看,目标句前三个词是:

y_1 = f(C)

y_2 = f(C, y_1)

y_3 = f(C, y_1, y_2)

(f) 是解码器的非线性变换。生成 (y_1)、(y_2)、(y_3) 时,用到的 Source 语义编码都是同一个 (C)。

71.2 为什么说它注意力不集中

上面这张图里没有注意力,可以看成注意力不集中的分心模型。(C) 是编码器对整个 Source 编出来的。目标句里任意一个词 (y_i),看到的都是这同一个 (C),所以 Source 里每个单词对 (y_i) 的影响力都相同。生成「welcome」和生成「to」时,模型对「欢迎」「来」「北京」没有区别对待。这就是图 1 没有体现注意力的原因。

71.3 注意力的三类

给模型的每一个输入项分配一个权重,输入项可以是图片的一块,也可以是句子里的一个词。权重大小就是希望模型对这一块的关注程度。用权重大小模拟人处理信息时的侧重,模型效果会好一些,计算量也会降一些。

深度学习里的注意力通常分三类:

类型 权重怎么给 特点
软注意(Soft / Global Attention,全局注意) 每个输入项的权重在 0 和 1 之间。有的部分多看一点,有的少看一点 大部分信息都考虑了,只是程度不同,计算量相对大
硬注意(Hard / Local Attention,局部注意,了解即可) 每个输入项的权重非 0 即 1。要看的留下,不看的直接丢掉 能省一些时间和计算,也可能丢掉本该注意的信息
自注意力(Self / Intra Attention,内注意) 每个输入项的权重由输入项之间的相互作用决定,靠输入项内部的「表决」决定该看哪些项 处理很长的输入时,可以并行计算

2026.10.09 21:12

72. 扩展2_加入注意力机制的Encoder_Decoder框架(理解)

target 里任意一个词,都有一份对 source 各词的注意力分配概率。生成这个词时的中间语义按这份概率重算,所以每一步用的 (C) 各不相同。

编码器仍读入 (X_1, X_2, X_3, X_4)。解码器生成 (Y_1, Y_2, Y_3) 时,分别拿 (C_1, C_2, C_3):(C_1) 进第一步,(C_2) 进第二步,(C_3) 进第三步。

72.1 为什么每个目标词要单独分配注意力

机器翻译里,source 是 Tom chase Jerry,target 是「汤姆」「追逐」「杰瑞」。

生成「杰瑞」时,普通框架里 Tom、Chase、Jerry 对这个中文词的贡献一样大。译「杰瑞」时,英文里的 Jerry 更该被看重。

加上注意力之后,生成「杰瑞」要体现出每个英文词的影响不一样。可以给出这样一组概率:

英文词 生成「杰瑞」时的注意力
Tom 0.3
Chase 0.2
Jerry 0.5

每个数是注意力模型在翻译当前词「杰瑞」时,分给对应英文词的注意力大小。Jerry 分到 0.5,比另外两个词都大。

因此,target 中任意一个单词,都对应一套 source 单词的注意力分配概率。生成该词时的中间语义,跟着这组概率变化。

72.2 生成公式

(f_1) 根据这一步的信息包 (C_i),再加上已经生成的历史词,推出新词。

y_1 = f_1(C_1)

y_2 = f_1(C_2, y_1)

y_3 = f_1(C_3, y_1, y_2)

(y_1) 只看 (C_1)。从 (y_2) 起,还要带上前面已经生成的词。

72.3 每个 (C_i) 对应一套权重

每个 (C_i) 可以对应不同的源语单词注意力分配。上面这句英译中,三份中间语义可以是:

C_{Tom} = g(0.6 \\cdot f_2(Tom),\\ 0.2 \\cdot f_2(Chase),\\ 0.2 \\cdot f_2(Jerry))

C_{Chase} = g(0.2 \\cdot f_2(Tom),\\ 0.7 \\cdot f_2(Chase),\\ 0.1 \\cdot f_2(Jerry))

C_{Jerry} = g(0.3 \\cdot f_2(Tom),\\ 0.2 \\cdot f_2(Chase),\\ 0.5 \\cdot f_2(Jerry))

(f_2) 是编码器对每个输入词做的变换。编码器如果是 RNN,(f_2) 的结果往往是该词输入之后隐藏层的状态,也可以先理解成这个词经过基础处理后的表示。(g) 把这些结果按注意力系数加权求和,合成这一步的整句中间语义。权重就是注意力分配系数。

一般写成:

C_i = \\sum_{j=1}\^{L_x} a_{ij} h_j

符号 含义
(L_x) source 的长度。Tom、Chase、Jerry 就是 3 个词,(L_x = 3)
(a_{ij}) 输出 target 第 (i) 个词时,source 第 (j) 个词的注意力系数。也就是上面的 0.6、0.2、0.2 这一类数,表示第 (j) 个输入词对生成第 (i) 个目标词有多重要
(h_j) source 第 (j) 个词的语义编码,即这个词经过 (f_2) 之后的结果。例如 (h_1 = f_2(Tom)),(h_2 = f_2(Chase)),(h_3 = f_2(Jerry))

(g) 本质上就是这个加权求和。

72.4 生成「汤姆」时 (C) 怎么拼出来

生成「汤姆」时,三个输入词先各自过 (f_2),得到 (f_2(Tom))、(f_2(Chase))、(f_2(Jerry))。注意力系数是:

系数 词 值
(a_{11}) Tom 0.6
(a_{12}) Chase 0.2
(a_{13}) Jerry 0.2

(g) 把这三项加权加起来:

C_{汤姆} = g(0.6 \\cdot f_2(Tom),\\ 0.2 \\cdot f_2(Chase),\\ 0.2 \\cdot f_2(Jerry))

这份 (C) 就是解码器生成「汤姆」时用的中间语义张量,也就是这一步的总信息包。下一步生成「追逐」时换用 (C_{Chase}) 那一组权重(0.2、0.7、0.1),生成「杰瑞」时换用 0.3、0.2、0.5。

相关推荐
用户1733598075372 小时前
纯前端 PDF 压平避坑指南:压平后表单字段变了?
前端·javascript·vue.js
nyaomaru2 小时前
将一个真实的 TypeScript OSS 库从 tsup 迁移到 tsdown
前端·typescript
胡写代码2 小时前
雪花 ID 传到前端就变了个数?我用全局 Long 转 String 一次收口
前端·后端
沐言人生3 小时前
82.4k 星!把十几万行代码变成知识图谱,新人终于不用硬啃了
前端·后端·github
溪语流沙3 小时前
【Web全栈进阶】JWT无状态认证:签发、校验、刷新
前端·git·python·github
在繁华处3 小时前
2.1 上下文:决定 Agent 能力上限的关键
前端·人工智能·microsoft
ndsc_d3 小时前
2026年有哪些好用的AI UI设计工具?主流工具功能和适用场景对比
前端·人工智能·ui·ai·设计师·ai ui·ai ui工具
溪语流沙3 小时前
【每天一个CSS | Day18】会摆动的拟物怀表:黄铜、皮革与催眠摆
前端·css3·html5
Lvan的前端笔记3 小时前
docker:基于docker的前端部署方案
前端·docker·状态模式