self-attention与Bert

Transformer 架构

及其衍生模型 BERT、GPT-2 的对应关系

Transformer 的编码器 - 解码器(Encoder-Decoder)结构,这是现代自然语言处理(NLP)的核心基础架构,由 "注意力机制 + 前馈网络 + 残差连接 / 层归一化" 构成重复模块(标记为 "N×")。

编码器与 BERT 的对应:

  • 图左侧的编码器模块对应BERT(340M 参数),BERT 基于 Transformer 的编码器构建,采用双向的 Masked Multi-Head Attention,因此擅长文本理解类任务(如语义分类、实体识别)。

解码器与 GPT-2 的对应:

  • 图右侧的解码器模块对应GPT-2(1542M 参数),GPT-2 基于 Transformer 的解码器构建,采用自回归的 Masked Multi-Head Attention(仅关注前文 token),因此擅长文本生成类任务(如续写、创作)。

核心模块:

  • 编码器 / 解码器的重复单元均包含 "Multi-Head Attention(多头注意力,捕捉序列关联)""Feed Forward(前馈网络,特征变换)""Add & Norm(残差连接 + 层归一化,稳定训练)",是 Transformer 实现语义建模的关键组件。

这些模型是 NLP 领域的里程碑:BERT 开启了双向预训练的范式,GPT-2 则推动了大模型文本生成能力的普及。


Bert

"文本→模型输入" 的预处理流程

完成 "原始文本→分词→数值化输入" 的转换,是 BERT 等预训练语言模型的标准前置操作。

  1. 步骤分解:
    • 分词:将连续文本 "商务大床房" 拆分为基础单元(此处为单个汉字 "商、务、大、床、房")。
    • 数值化编码:分词结果被转换为模型可识别的格式:
      • Input ids:每个分词单元映射为数字编码(示例中101 是起始标记 [CLS]102 是结束标记 [SEP],其余为汉字对应的编码);
      • Mask:标记有效文本位置(1 表示有效,0 表示无效填充);
      • Seq_ids:区分不同输入序列(单文本任务中通常统一为 0)。

该流程是 NLP 任务(如文本分类、语义检索)的基础,通过将自然语言转换为数值格式,使模型能够进行语义建模。

  • Cls\]:序列开头的专用标记,用于后续任务(如文本分类)的全局语义聚合;

  • pad\]:序列长度不足时的填充标记,保证所有输入序列维度统一。

  • Input ids 是标记的数字映射

  • Mask 区分有效内容与填充内容

  • Seq_ids 用于单 / 双文本任务的序列区分

该流程通过标准化输入格式,使 BERT 能统一处理不同长度、内容的文本,为后续语义理解、分类等任务提供合规的输入数据。


Embedding结构

BERT 基于 Transformer 架构实现语义建模的核心输入层设计

BERT 的输入嵌入是三种嵌入的叠加 ,最终形成 Transformer 可处理的输入表示,这一设计让模型同时捕捉文本的语义、分段、位置信息

  1. 各嵌入的功能
    • Token Embeddings(词嵌入) :将输入的 token(包括普通词汇、特殊标记、子词如##ing)映射为语义向量,是文本语义信息的基础载体;
    • Segment Embeddings(段嵌入) :用于区分双文本任务中的不同文本段(如图中E_A对应第一段、E_B对应第二段),常见于问答、文本匹配等任务;
    • Position Embeddings(位置嵌入):由于 Transformer 本身无时序感知能力,通过位置嵌入为每个 token 添加位置信息,使模型捕捉文本的顺序关系。
  2. 特殊 Token 的作用
    • 输入序列中的[CLS]是全局语义聚合标记(用于文本分类等任务),[SEP]是文本段的分隔 / 结束标记,##开头的子词是 BPE 分词的结果(解决未登录词问题)。

这一嵌入结构是 BERT 实现双向语义理解的基础,确保模型能全面捕捉文本的多维度信息。


Bert结构

BERT 的结构从下到上分为四个核心部分:

  • 嵌入层(embedding):将预处理后的文本转换为初始向量表示,是模型的输入层;
  • BERT 层(bert layers):重复 N 次的 Transformer 编码器模块(图中 "N×" 表示重复),是 BERT 语义建模的核心;
  • 池化输出(pooler output):对 BERT 层的输出进行聚合(通常取 [CLS] 标记的向量),为下游任务提供全局语义表示;
  • 分类器(classifier):基于池化输出的下游任务分类层,用于文本分类等任务。

参数计算

BERT-base 模型的总参数数量:

维度 dim=768、词汇表大小 vocab=21128 、最大序列长度max_seq_len= 512 、忽略bias

嵌入层总参数:

  • Token Embedding 层的参数是 vocab × dim
  • Position Embedding 层的参数是 max_seq_len×dim
  • Segment Embedding 层的参数是 2×dim(支持 2 个文本段)

单个 BERT 层

  • 注意力参数:Multi-Head Attention 层的参数是 4×dim×dim(
    • Q/K/V 投影
    • 输出投影层:把拼接后的多头结果映射为最终输出(维度dim×dim,参数dim²);
  • 前馈网络参数:Feed Forward 层的参数是 2×dim×dff(输入 / 输出线性层)

所有 BERT 层

  • 12 层 BERT 层总参数

池化层(Pooler)

  • dim2(对 [CLS] 标记做向量投影)

dff(Feed Forward Dimension)是 BERT/Transformer 前馈网络中间隐藏层的维度,是架构设计中的核心超参数,它的取值直接决定了前馈网络的特征表达能力和参数规模。

《Attention Is All You Need》论文中明确将前馈网络的中间层维度dff设定为模型隐藏层维度dim4 倍 ,即 dff = 4 × dim

前馈网络的作用是对注意力层输出的特征做非线性变换和维度扩张 - 压缩 ------ 先把dim维的输入 "升维" 到dff维(扩大特征空间),通过激活函数增强非线性表达,再 "降维" 回dim维(保证和输入维度一致,满足残差连接)。4 倍的比例是作者通过大量实验验证的最优选择

BERT-base 配置中 dim=768,因此:dff = 768 × 4 = 3072,这也是 BERT-base/ Transformer-base 的标准dff值。


Bert的迁移学习流程

1. 预训练阶段(图上半部分)

  • 逻辑:由开发者 / 机构(图中 "大佬")在大规模无标注文本上完成预训练任务(如 Masked Language Model、Next Sentence Prediction),让 BERT 学习通用的语言语义规律;
  • 作用:预训练后的 BERT 是一个 "现成的特征提取器",具备理解文本语义的基础能力,可被不同下游任务直接复用,避免每个任务从头训练模型。

2. 微调阶段(图红色框内流程)

  • 输入:下游具体任务的文本(如图中示例 "我今天很高兴");
  • 流程:将文本输入预训练好的 BERT,由 BERT 提取其语义特征(图中虚线框),再在特征后添加任务专用的分类层;
  • 输出:完成特定任务的预测(如图中 "好 / 坏" 对应的情感分类任务);
  • 作用:通过少量下游任务的标注数据,调整模型参数(或仅调整分类层),让通用语义特征适配具体任务的需求。

这种 "预训练 - 微调" 模式是 BERT 普及的核心原因:既降低了下游任务的训练成本(数据、算力),也大幅提升了任务效果。


bert分类实战------酒店数据集

获取Bert模型

网址:

https://huggingface.co/bert-base-chinese/tree/main

点那个下载箭头就可以下 下载圈中的 放在bert-base-chinese文件夹。

放到我们的文件夹(bert-base-chinese)中


数据集(jiudian.txt):

读取文件

复制代码
def read_txt(path):
    label = []
    data = []
    with open(path, "r", encoding="utf-8") as f:
        for i, line in tqdm(enumerate(f)):
            if i == 0:           # 第一行数据是label,review
                continue
            if i > 200 and i < 7500:  # 取前面200行好评,和后200行差评
                continue
            line = line.strip('\n')  # 去掉换行符
            line = line.split(",", 1)  # 只分割第一个逗号
            label.append(line[0])  # 标签部分
            data.append(line[1])  # 评论内容部分 
    return data, label

数据集接口

复制代码
class JdDataset(Dataset):
    def __init__(self, data, lable):
        self.X = data                                # 特征数据(文本/评论)
        y = [int(i) for i in lable]                  # 将标签从字符串转为整数
        self.Y = torch.LongTensor(lable)             # Y必须是  PyTorch 的 LongTensor

    def __getitem__(self, item):
        return self.X[item], self.Y[item]            # 返回值:一个元组 (特征, 标签)

    def __len__(self):
        return len(self.Y)

数据加载器

复制代码
# batchsize:批量大小,默认为1(逐个样本处理)
# valSize:验证集比例,默认为0.2(20%作为验证集)
def get_dataloader(path, batchsize=1, valSize=0.2):
    x, y = read_txt(path)

    # 数据分割:用于将数据集分割成训练集和测试集
    train_x, val_x, train_y, val_y = train_test_split(x, y,test_size=valSize,shuffle=True, stratify=y) 

    train_set = JdDataset(train_x, train_y)
    val_set = JdDataset(val_x, val_y)

    train_loader = DataLoader(train_set, batch_size=batchsize)
    val_loader = DataLoader(val_set, batch_size=batchsize)

    return train_loader, val_loader

train_test_split 函数

train_test_split 是机器学习中最常用的数据分割函数,用于将数据集划分为训练集和测试集。

复制代码
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X,                     # 特征数据
    y,                     # 标签数据
    test_size=0.25,       # 测试集比例
    random_state=42,      # 随机种子
    shuffle=True,         # 是否洗牌
    stratify=None         # 是否分层采样
)

stratify=y 的重要性:

假设原始数据标签分布为:

  • 好评(1):800 个

  • 差评(0):200 个

  • 总计:1000 个

没有 stratify 的情况

  • 随机分割可能导致训练集和验证集标签比例不一致

  • 例如:训练集可能有 750好评/150差评,验证集 50好评/50差评

stratify=y 的情况

  • 保证两个集合的标签比例与原始数据一致

  • 训练集:640好评/160差评(80%)

  • 验证集:160好评/40差评(20%)

  • 保持了 80:20 的好评差评比例

Bert模型

复制代码
class myBertModel(nn.Module):
    def __init__(self, bert_path, num_class, device):
        super(myBertModel, self).__init__()
        self.device = device
        self.num_class = num_class

        # 加载BERT模型(有预训练权重)
        self.bert = BertModel.from_pretrained(bert_path)
        # 加载分词器
        self.tokenizer = BertTokenizer.from_pretrained(bert_path)
        # 创建顺序容器
        self.out = nn.Sequential(
            nn.Linear(768,num_class)                                  # 线性层:输入维度768(BERT输出维度),输出到分类数
        )

    # 将文本转换为BERT输入格式
    def get_bert_input(self, text):
        # 调用分词器处理文本
        Input = self.tokenizer(text,
                               return_tensors='pt',   # 返回PyTorch张量格式
                               padding='max_length',  # 填充到最大长度
                               truncation=True,       # 截断超过最大长度的文本
                               max_length=128)        # 设置最大长度为128个token

        input_ids = Input["input_ids"].to(self.device)            # token IDs,移动到指定设备
        attention_mask = Input["attention_mask"].to(self.device)  # 注意力掩码(区分真实token和填充)
        token_type_ids = Input["token_type_ids"].to(self.device)  # 句子类型ID(用于句子对任务)
        return input_ids, attention_mask, token_type_ids          # 返回三个张量

    # 前向传播
    def forward(self, text):
        # 1. 构建BERT输入:原始文本 → BERT要求的三大核心张量
        input_ids, attention_mask, token_type_ids = self.get_bert_input(text)

        # 通过 BERT 模型:self.bert(...)
        # 2. 通过BERT模型:输入张量 → 语义特征(序列特征+全局特征)
        sequence_out, pooled_output = self.bert(
            input_ids=input_ids,            # 文本的数字编码张量 [batch_size, seq_len]
            attention_mask=attention_mask,  # 掩码张量(区分有效token/填充)[batch_size, seq_len]
            token_type_ids=token_type_ids,  # 段标识张量(单文本全0)[batch_size, seq_len]
            return_dict=False               # 返回元组(而非字典),方便解包
        )

        # sequence_out:每个 token 的语义向量(768 维),适合 token 级任务(如实体识别)
        # pooled_output:[CLS] 标记经 Pooler 层投影后的全局语义向量(Pooler 层输出),适合文本级任务(如分类)

        # 分类头:self.out(pooled_output)
        # 3. 分类头:全局语义特征 → 分类预测结果
        pred= self.out(pooled_output)       # pooled_output: [batch_size, 768] → 映射到类别空间
        return pred                          # 最终输出形状: [batch_size, num_class]

训练

复制代码
def train_val(para): 
    model = para['model']
    train_loader =para['train_loader']
    val_loader = para['val_loader']
    scheduler = para['scheduler']
    optimizer = para['optimizer']
    loss = para['loss']
    epoch = para['epoch']
    device = para['device']
    save_path = para['save_path']
    max_acc = para['max_acc']
    val_epoch = para['val_epoch'] 
    plt_train_loss = []
    plt_train_acc = []
    plt_val_loss = []
    plt_val_acc = []
    val_rel = []

    for i in range(epoch):
        start_time = time.time()
        model.train()
        train_loss = 0.0
        train_acc = 0.0
        val_acc = 0.0
        val_loss = 0.0
        for batch in tqdm(train_loader):
            model.zero_grad()
            text, labels = batch[0], batch[1].to(device)
            pred = model(text)
            bat_loss = loss(pred, labels)
            bat_loss.backward()
            optimizer.step()
            scheduler.step()
            optimizer.zero_grad()
            torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
            train_loss += bat_loss.item()    #.detach 琛ㄧず鍘绘帀姊害
            train_acc += np.sum(np.argmax(pred.cpu().data.numpy(),axis=1)== labels.cpu().numpy())
        plt_train_loss . append(train_loss/train_loader.dataset.__len__())
        plt_train_acc.append(train_acc/train_loader.dataset.__len__())
        if i % val_epoch == 0:
            model.eval()
            with torch.no_grad():
                for batch in tqdm(val_loader):
                    val_text, val_labels = batch[0], batch[1].to(device)
                    val_pred = model(val_text)
                    val_bat_loss = loss(val_pred, val_labels)
                    val_loss += val_bat_loss.cpu().item()

                    val_acc += np.sum(np.argmax(val_pred.cpu().data.numpy(), axis=1) == val_labels.cpu().numpy())
                    val_rel.append(val_pred)

            if val_acc > max_acc:
                torch.save(model, save_path+str(epoch)+"ckpt")
                max_acc = val_acc
            plt_val_loss.append(val_loss/val_loader.dataset.__len__())
            plt_val_acc.append(val_acc/val_loader.dataset.__len__())
            print('[%03d/%03d] %2.2f sec(s) TrainAcc : %3.6f TrainLoss : %3.6f | valAcc: %3.6f valLoss: %3.6f  ' % \
                  (i, epoch, time.time()-start_time, plt_train_acc[-1], plt_train_loss[-1], plt_val_acc[-1], plt_val_loss[-1])
                  )
            if i % 50 == 0:
                torch.save(model, save_path+'-epoch:'+str(i)+ '-%.2f'%plt_val_acc[-1])
        else:
            plt_val_loss.append(plt_val_loss[-1])
            plt_val_acc.append(plt_val_acc[-1])
            print('[%03d/%03d] %2.2f sec(s) TrainAcc : %3.6f TrainLoss : %3.6f   ' % \
                  (i, epoch, time.time()-start_time, plt_train_acc[-1], plt_train_loss[-1])
                  )
    plt.plot(plt_train_loss)
    plt.plot(plt_val_loss)
    plt.title('loss')
    plt.legend(['train', 'val'])
    plt.show()

    plt.plot(plt_train_acc)
    plt.plot(plt_val_acc)
    plt.title('Accuracy')
    plt.legend(['train', 'val'])
    plt.savefig('acc.png')
    plt.show()

1. 梯度裁剪

复制代码
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm= 1.0)    

限制梯度范数不超过1.0,防止梯度爆炸

**第一个参数:model.parameters():**获取模型所有需要梯度更新的参数

第二个参数:max_norm=5.0 : 这是最大范数阈值,是梯度裁剪的核心参数

什么时候需要梯度裁剪?

1. 训练RNN/LSTM/GRU等循环神经网络

  • RNN容易出现梯度爆炸问题(梯度随时间步指数增长)

  • 特别是处理长序列时,梯度裁剪几乎是必需的

    RNN训练中的典型使用

    for batch in dataloader:
    optimizer.zero_grad()
    loss = train_rnn(batch)
    loss.backward()
    torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0) # 必须
    optimizer.step()

  1. 模型层数很深时
  • 深层网络(如Transformer、BERT、深层CNN)

  • 梯度在反向传播中可能累积变大

  • 深层Transformer模型

    BERT, GPT等预训练模型通常需要梯度裁剪

3. 学习率设置较大时

  • 高学习率 + 大梯度 = 参数更新过大 → 训练不稳定

  • 梯度裁剪可以稳定训练过程

2、优化器optimizer

复制代码
optimizer = torch.optim.AdamW(
    model.parameters(),  # 要优化的参数
    lr=learning_rate,    # 学习率
    weight_decay=0.0001  # 权重衰减系数
)
  • model.parameters():模型所有可训练参数

  • lr=learning_rate:初始学习率(如 1e-4, 2e-5)

  • weight_decay=0.0001

    • 作用:L2正则化,防止过拟合

    • :0.0001 = 1e-4(常用值)

    • 效果 :权重会以 w = w - lr * weight_decay * w 方式衰减

3、学习率调度器 scheduler

复制代码
scheduler = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(
    optimizer,      # 要调度的优化器
    T_0=20,        # 第一个周期的长度(epoch数)
    eta_min=1e-9   # 最小学习率
)

算法原理

1. 余弦退火(Cosine Annealing)

  • 学习率按余弦函数从初始值衰减到最小值

2. 热重启(Warm Restarts)

  • 在每个周期结束时,"重启"学习率

  • 但每个新周期的最大学习率可能不同

参数详解

T_0=20

第一个周期的长度

表示:每20个epoch完成一个余弦周期

然后重启开始新周期

复制代码
# 周期长度可能变化:
T_0 = 20      # 第一个周期:20个epoch
T_1 = T_0 * 2 = 40  # 第二个周期:40个epoch
T_2 = T_1 * 2 = 80  # 第三个周期:80个epoch
# 以此类推(默认倍增)

eta_min=1e-9

学习率的最小值

复制代码
# 学习率变化范围:
# 开始时:lr = learning_rate(如1e-4)
# 周期结束时:lr = eta_min = 1e-9
# 重启后:lr = 学习率 * 衰减因子(可能)

不用bert参数的备选方案:

复制代码
from transformers import BertConfig, BertModel

# 方案1:从配置文件加载配置
bert_config = BertConfig.from_pretrained(bert_path)  # 加载BERT配置
self.bert = BertModel(bert_config)                   # 用配置初始化BERT(随机权重)
  • BertConfig.from_pretrained(bert_path):只加载配置文件(如config.json),不加载预训练权重

  • BertModel(bert_config):根据配置创建新的BERT模型,所有权重随机初始化

缺点

  1. 需要更多数据:通常需要大量数据才能训练好

  2. 训练时间更长:需要更多epoch才能收敛

  3. 可能效果较差:特别是小数据集上

  4. 需要调参:学习率等超参数需要重新调整

  5. 资源消耗:从头训练计算成本高

相关推荐
格林威2 小时前
基于轮廓特征的工件分类识别:实现无模板快速分拣的 8 个核心算法,附 OpenCV+Halcon 实战代码!
人工智能·数码相机·opencv·算法·目标跟踪·分类·数据挖掘
村口曹大爷2 小时前
Aider-TUI: The Professional AI Pair Programming Shell
人工智能·ai·code·aider
乾元2 小时前
10 个可复制的企业级项目:从需求到交付的 AI 网络工程模板(深度实战版)
运维·网络·人工智能·网络协议·安全
深圳南柯电子2 小时前
南柯电子|EMI测试系统:5G时代新挑战,如何护航全行业电磁兼容
人工智能·汽车·互联网·实验室·emc
linmoo19862 小时前
Langchain4j 系列之十九 - RAG之Retrieval
人工智能·langchain·retrieval·rag·langchain4j
沛沛老爹2 小时前
Web开发者突围AI战场:Agent Skills元工具性能优化实战指南——像优化Spring Boot一样提升AI吞吐量
java·开发语言·人工智能·spring boot·性能优化·架构·企业开发
MM_MS2 小时前
Halcon小案例--->路由器散热口个数(两种方法)
人工智能·算法·目标检测·计算机视觉·视觉检测·智能路由器·视觉
SelectDB技术团队2 小时前
驾驭 CPU 与编译器:Apache Doris 实现极致性能的底层逻辑
数据库·数据仓库·人工智能·sql·apache