目录

10.1 BERT:双向理解的语言大师

10.1.1 BERT 的使用方式

10.1.2 BERT 有用的原因

10.1.3 BERT 的变体

10.2 GPT:生成式的语言艺术家

完整可运行的Python示例程序

程序说明与运行指南

程序特点:

运行说明:


10.1 BERT:双向理解的语言大师

在自然语言处理的世界里,BERT(Bidirectional Encoder Representations from Transformers)的出现可以说是一场革命。要理解BERT为什么如此重要,我们首先要明白传统语言模型的局限性。

传统语言模型的困境

在BERT之前,大多数语言模型都是单向的。想象一下,你在阅读一篇文章,但只能从左到右阅读,永远不能回头看已经读过的内容。这就是传统语言模型的工作方式。比如,在预测句子"今天天气很好,我们去__"中的空白处时,模型只能基于前面的内容来猜测,而无法利用后面的信息。

这种单向性带来了很大的限制。在真实的语言理解中,我们人类总是同时利用前后文的信息来理解每个词的含义。比如在句子"苹果很甜"和"苹果发布了新手机"中,"苹果"的含义完全不同,但我们能准确理解,就是因为同时考虑了前后文。

BERT的革命性突破

BERT的核心突破在于它的"双向性"。它能够同时考虑一个词左边和右边的所有上下文信息。这就像让你在阅读时能够随意前后翻阅,充分理解每个词在完整语境中的含义。

BERT是如何实现这一点的呢?答案在于它的训练方法——掩码语言模型(Masked Language Model, MLM)。在训练过程中,BERT会随机遮盖输入句子中15%的单词,然后尝试预测这些被遮盖的单词。比如,对于句子"今天天气很好,我们去公园散步",BERT可能会看到"今天天气很好,我们去[MASK]散步",然后它需要根据所有可见的上下文来预测被遮盖的词是"公园"。

BERT的架构解析

BERT基于Transformer的编码器部分构建。让我们深入理解BERT的各个组成部分:

输入表示:

BERT的输入非常精巧,它由三个部分拼接而成:

  • 词嵌入:将每个单词转换为向量表示

  • 位置嵌入:记录每个单词在句子中的位置信息

  • 段落嵌入:区分句子对中的两个句子

这种设计让BERT能够理解词义、词序以及句子关系,为深层次的语言理解奠定了基础。

注意力机制:

BERT使用自注意力机制,这让它能够为每个词分配不同的"注意力权重"。比如在理解"它"这个代词时,BERT会关注句子中所有可能的名词,然后确定"它"具体指代哪一个。

层归一化和前馈网络:

每个Transformer层都包含层归一化和前馈网络,这些组件帮助模型稳定训练并增强表达能力。

BERT的训练过程

BERT的训练包含两个主要任务:掩码语言模型和下一句预测。

掩码语言模型(MLM):

在这个任务中,BERT学习理解词语在上下文中的含义。训练时:

  1. 随机选择输入中15%的单词进行遮盖

  2. 其中80%替换为[MASK]标记

  3. 10%替换为随机单词

  4. 10%保持不变

这种设计很巧妙,因为它迫使模型不仅要预测被遮盖的词,还要判断当前词是否被修改过。这增强了模型的鲁棒性。

下一句预测(NSP):

这个任务让BERT理解句子之间的关系。训练时,BERT会接收两个句子,然后判断第二个句子是否是第一个句子的下一句。这帮助模型学习篇章级别的连贯性。

10.1.1 BERT 的使用方式

BERT的真正威力在于它的预训练+微调范式。这种范式彻底改变了自然语言处理的发展路径。

预训练:知识的积累

预训练阶段就像是让BERT参加一个"语言通识教育"课程。在这个阶段,BERT在海量的无标签文本数据上学习,包括维基百科、书籍、新闻文章等。它不针对任何特定任务,而是学习语言的通用规律:语法结构、语义关系、常识推理等。

这个过程的计算成本很高,需要大量的GPU和训练时间。但一旦完成,我们就获得了一个拥有丰富语言知识的"基础模型"。

微调:专业的精修

微调阶段就像是让经过通识教育的BERT参加"专业培训"。在这个阶段,我们在特定任务的数据集上继续训练BERT,让它适应具体的应用场景。

微调的美妙之处在于:

  1. 高效性:只需要相对较少的有标签数据

  2. 快速性:训练时间远远少于预训练

  3. 灵活性:同一个预训练模型可以微调到各种不同任务

具体使用场景

文本分类:

对于情感分析、主题分类等任务,我们通常在BERT的输出上添加一个简单的分类层。比如:

# 伪代码示例
inputs = [CLS] + tokens + [SEP]  # [CLS]标记用于分类
outputs = bert_model(inputs)
class_logits = classifier(outputs[CLS_position])

命名实体识别:

对于识别文本中的人名、地名、组织机构名等任务,我们对每个词的表征进行分类:

# 伪代码示例
outputs = bert_model(tokens)
entity_logits = [classifier(output) for output in outputs]

问答系统:

对于抽取式问答,我们使用两个分类器分别预测答案的开始位置和结束位置:

# 伪代码示例
outputs = bert_model([CLS] + question + [SEP] + context + [SEP])
start_logits = start_classifier(outputs)
end_logits = end_classifier(outputs)

句子对任务:

对于语义相似度、自然语言推理等任务,我们输入两个句子,让BERT学习它们之间的关系。

实践中的技巧

学习率调整:

微调时通常使用较小的学习率,因为模型已经具备很好的初始权重。常见策略包括:

  • 分层学习率:底层使用较小学习率,顶层使用较大学习率

  • 学习率预热:逐步增加学习率到目标值

  • 余弦退火:周期性地调整学习率

批量大小选择:

由于BERT模型较大,选择合适的批量大小很重要。通常会在计算资源允许的情况下选择较大的批量大小。

训练轮数控制:

BERT微调通常很快收敛,需要小心监控验证集性能,避免过拟合。

10.1.2 BERT 有用的原因

BERT之所以能在自然语言处理领域引起如此大的轰动,是因为它解决了长期以来困扰研究者的几个核心问题。

深层的双向理解

传统语言模型的最大限制就是单向性。想象一下,如果让你只通过前半句话来理解整个句子的含义,你肯定会错过很多重要信息。BERT的双向注意力机制让它能够同时考虑每个词的所有上下文信息。

这种双向性在理解歧义、指代消解等复杂语言现象时特别有用。比如在句子"小明告诉小红他赢了比赛"中,要理解"他"指代谁,需要同时考虑前后信息。BERT的这种能力让它在这类任务上表现卓越。

上下文相关的词表示

在BERT之前,大多数词嵌入方法(如Word2Vec、GloVe)都是上下文无关的。也就是说,同一个词在不同语境中会有相同的向量表示。这显然不符合语言的实际使用情况。

BERT生成的词表示是高度上下文相关的。同一个词在不同的句子中会有不同的向量表示,这准确地反映了词义随语境变化的现象。比如"银行"在"我去银行存钱"和"我们坐在河岸边的银行上"中会有完全不同的表示。

迁移学习的威力

BERT证明了大规模预训练+任务微调范式的有效性。这种方法的优势在于:

知识复用: 模型在预训练阶段学到的语言知识可以迁移到各种下游任务中
数据效率: 微调所需的有标签数据量大大减少
快速适应: 新的NLP任务可以快速部署,无需从头训练

多任务学习的内在优势

BERT在预训练阶段同时学习MLM和NSP任务,这实际上是一种多任务学习。研究表明,多任务学习能够促使模型学习更通用、更鲁棒的特征表示。

规模效应的体现

BERT的成功也证明了模型规模的重要性。更大的模型、更多的数据、更长的训练时间确实能够带来性能的提升。这推动了整个领域向更大规模模型的方向发展。

10.1.3 BERT 的变体

BERT的成功催生了一系列改进模型,每个变体都在某些方面对原始BERT进行了优化。

RoBERTa:优化的BERT

RoBERTa(Robustly Optimized BERT Pretraining Approach)通过对BERT训练过程的细致优化,在不改变模型架构的情况下显著提升了性能。

主要改进包括:

  • 移除NSP任务,只使用MLM任务

  • 使用更大的批量大小

  • 更长的训练时间

  • 更多的训练数据

  • 动态掩码模式

这些优化让RoBERTa在多项基准测试中超越了BERT。

DistilBERT:轻量化的BERT

DistilBERT通过知识蒸馏技术,将BERT模型压缩到原来大小的40%,同时保持97%的性能。

关键技术:

  • 知识蒸馏:让小模型学习大模型的输出分布

  • 三重损失:结合蒸馏损失、掩码语言模型损失和余弦嵌入损失

  • 移除token-type embeddings和pooler

DistilBERT在资源受限的环境中特别有用。

ALBERT:参数高效的BERT

ALBERT(A Lite BERT)通过两种主要技术大幅减少了模型参数量:

因子化参数: 将词嵌入矩阵分解为两个较小的矩阵
跨层参数共享: 所有Transformer层共享参数

这些改进让ALBERT在保持性能的同时显著减少了内存消耗。

ELECTRA:高效的预训练方法

ELECTRA提出了一种新的预训练任务——替换token检测。相比MLM的15%训练效率,ELECTRA能够利用所有输入token进行训练,大大提高了训练效率。

多语言BERT

Google发布了多语言BERT,在104种语言的维基百科数据上训练,能够处理多种语言的NLP任务。

领域特定BERT

针对特定领域的需求,出现了各种领域特定的BERT变体:

  • BioBERT:生物医学领域

  • SciBERT:科学文献领域

  • ClinicalBERT:临床文本领域

  • LegalBERT:法律文档领域

10.2 GPT:生成式的语言艺术家

如果说BERT是语言理解的大师,那么GPT(Generative Pre-trained Transformer)就是语言生成的艺术家。GPT系列模型展示了自回归语言模型在文本生成方面的惊人能力。

自回归生成的核心思想

GPT采用自回归的方式生成文本,这意味着它逐个生成单词,每个新单词的生成都依赖于之前生成的所有单词。这就像人类写作或说话的过程——我们也是一个词一个词地构建句子,每个新词都基于前面已经说出的内容。

GPT与BERT的关键区别

虽然GPT和BERT都基于Transformer架构,但它们有几个根本性的不同:

架构差异:

  • BERT使用Transformer编码器,能够双向理解上下文

  • GPT使用Transformer解码器,只能单向生成文本

训练目标:

  • BERT训练目标是理解语言(掩码语言模型)

  • GPT训练目标是生成语言(自回归语言模型)

应用场景:

  • BERT更适合理解类任务:分类、问答、信息抽取

  • GPT更适合生成类任务:文本创作、对话、摘要

GPT的训练哲学

GPT的训练基于一个简单而强大的思想:通过预测下一个词来学习语言的规律。给定一个文本序列,GPT尝试预测序列中每个位置的下一个词。通过在这个任务上的大规模训练,GPT学会了语言的语法、语义甚至一定程度的世界知识。

GPT的进化历程

GPT-1:开创先河

最初的GPT模型证明了在大规模无标签文本上预训练,然后在特定任务上微调的有效性。虽然规模相对较小,但它为后续发展奠定了基础。

GPT-2:规模的力量

GPT-2通过大幅增加模型参数(最大15亿参数)和训练数据,展示了生成质量随规模提升的规律。GPT-2能够生成连贯、多样且上下文相关的文本。

GPT-3:少样本学习的突破

GPT-3将模型规模推向了新的高度(1750亿参数),并展示了令人印象深刻的少样本学习能力。GPT-3能够在只有几个示例的情况下适应新任务,这大大降低了应用门槛。

GPT系列的核心技术

自注意力机制:

GPT使用掩码自注意力,确保在生成每个词时只能关注到前面的词。这保证了生成过程的自回归特性。

位置编码:

由于Transformer本身不包含位置信息,GPT使用学习得到的位置编码来理解词序。

层归一化:

每个子层后面都有层归一化,帮助稳定训练过程。

GPT的应用场景

文本生成:

GPT能够生成各种类型的文本,包括故事、新闻、诗歌、代码等。用户只需要提供开头或提示,GPT就能续写完整的内容。

对话系统:

GPT能够进行多轮对话,理解上下文并生成合适的回复。虽然有时会生成不合逻辑的内容,但整体表现令人印象深刻。

内容摘要:

GPT能够理解长文档的核心内容,并生成简洁的摘要。

代码生成:

专门训练的代码GPT模型能够根据自然语言描述生成相应的代码。

GPT的局限性

尽管GPT系列模型表现惊人,但它们也存在一些局限性:

事实准确性: GPT可能会生成看似合理但实际上不正确的内容
一致性: 在生成长文本时可能失去一致性
可控性: 很难精确控制生成内容的具体属性
偏见问题: 可能放大训练数据中的社会偏见


完整可运行的Python示例程序

以下是一个完整的自监督学习示例,我们将实现一个简化的BERT模型,并在情感分析任务上进行微调:

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import numpy as np
import random
import math
from collections import Counter
import matplotlib.pyplot as plt
import os

# 设置设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用设备: {device}")

# 创建目录保存结果
os.makedirs("self_supervised_results", exist_ok=True)

# 超参数配置
batch_size = 32
max_length = 128
d_model = 256  # 模型维度
n_heads = 8    # 注意力头数
n_layers = 6   # Transformer层数
d_ff = 512     # 前馈网络维度
lr = 1e-4
num_epochs = 20
vocab_size = 10000  # 词汇表大小

# 1. 构建简化的BERT模型组件

class PositionalEncoding(nn.Module):
    """位置编码"""
    def __init__(self, d_model, max_len=5000):
        super(PositionalEncoding, self).__init__()

        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * 
                           (-math.log(10000.0) / d_model))

        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        pe = pe.unsqueeze(0).transpose(0, 1)
        self.register_buffer('pe', pe)

    def forward(self, x):
        return x + self.pe[:x.size(0), :]

class MultiHeadAttention(nn.Module):
    """多头注意力机制"""
    def __init__(self, d_model, n_heads):
        super(MultiHeadAttention, self).__init__()
        self.d_model = d_model
        self.n_heads = n_heads
        self.d_k = d_model // n_heads

        self.W_q = nn.Linear(d_model, d_model)
        self.W_k = nn.Linear(d_model, d_model)
        self.W_v = nn.Linear(d_model, d_model)
        self.W_o = nn.Linear(d_model, d_model)

        self.scale = math.sqrt(self.d_k)

    def forward(self, query, key, value, mask=None):
        batch_size = query.size(1)

        Q = self.W_q(query).view(-1, batch_size, self.n_heads, self.d_k).transpose(0, 1)
        K = self.W_k(key).view(-1, batch_size, self.n_heads, self.d_k).transpose(0, 1)
        V = self.W_v(value).view(-1, batch_size, self.n_heads, self.d_k).transpose(0, 1)

        scores = torch.matmul(Q, K.transpose(-2, -1)) / self.scale

        if mask is not None:
            scores = scores.masked_fill(mask == 0, -1e9)

        attn_weights = torch.softmax(scores, dim=-1)
        attn_output = torch.matmul(attn_weights, V)

        attn_output = attn_output.transpose(0, 1).contiguous().view(
            -1, batch_size, self.d_model)

        return self.W_o(attn_output), attn_weights

class PositionwiseFeedForward(nn.Module):
    """位置式前馈网络"""
    def __init__(self, d_model, d_ff):
        super(PositionwiseFeedForward, self).__init__()
        self.linear1 = nn.Linear(d_model, d_ff)
        self.linear2 = nn.Linear(d_ff, d_model)
        self.relu = nn.ReLU()

    def forward(self, x):
        return self.linear2(self.relu(self.linear1(x)))

class TransformerEncoderLayer(nn.Module):
    """Transformer编码器层"""
    def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
        super(TransformerEncoderLayer, self).__init__()
        self.self_attn = MultiHeadAttention(d_model, n_heads)
        self.feed_forward = PositionwiseFeedForward(d_model, d_ff)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, mask=None):
        # 自注意力 + 残差连接 + 层归一化
        attn_output, _ = self.self_attn(x, x, x, mask)
        x = self.norm1(x + self.dropout(attn_output))

        # 前馈网络 + 残差连接 + 层归一化
        ff_output = self.feed_forward(x)
        x = self.norm2(x + self.dropout(ff_output))

        return x

class SimpleBERT(nn.Module):
    """简化的BERT模型"""
    def __init__(self, vocab_size, d_model, n_heads, n_layers, d_ff, max_length):
        super(SimpleBERT, self).__init__()

        self.token_embedding = nn.Embedding(vocab_size, d_model)
        self.position_encoding = PositionalEncoding(d_model, max_length)
        self.segment_embedding = nn.Embedding(2, d_model)  # 用于句子对任务

        self.layers = nn.ModuleList([
            TransformerEncoderLayer(d_model, n_heads, d_ff) 
            for _ in range(n_layers)
        ])

        self.dropout = nn.Dropout(0.1)

    def forward(self, input_ids, segment_ids=None, attention_mask=None):
        # 词嵌入
        token_embeddings = self.token_embedding(input_ids)

        # 位置编码
        position_embeddings = self.position_encoding(
            torch.zeros_like(token_embeddings).transpose(0, 1)
        ).transpose(0, 1)

        # 组合嵌入
        embeddings = token_embeddings + position_embeddings

        # 添加段落嵌入(如果有)
        if segment_ids is not None:
            segment_embeddings = self.segment_embedding(segment_ids)
            embeddings += segment_embeddings

        embeddings = self.dropout(embeddings)

        # 转换维度: [batch_size, seq_len, d_model] -> [seq_len, batch_size, d_model]
        x = embeddings.transpose(0, 1)

        # 创建注意力掩码
        if attention_mask is not None:
            mask = attention_mask.unsqueeze(1).unsqueeze(2)
        else:
            mask = None

        # 通过Transformer层
        for layer in self.layers:
            x = layer(x, mask)

        # 转换回: [seq_len, batch_size, d_model] -> [batch_size, seq_len, d_model]
        x = x.transpose(0, 1)

        return x

class BERTForSequenceClassification(nn.Module):
    """用于序列分类的BERT模型"""
    def __init__(self, bert_model, num_classes):
        super(BERTForSequenceClassification, self).__init__()
        self.bert = bert_model
        self.classifier = nn.Linear(d_model, num_classes)
        self.dropout = nn.Dropout(0.1)

    def forward(self, input_ids, attention_mask=None):
        # 获取BERT输出
        outputs = self.bert(input_ids, attention_mask=attention_mask)

        # 使用[CLS]标记的输出进行分类
        cls_output = outputs[:, 0, :]  # [CLS]标记在第一个位置
        cls_output = self.dropout(cls_output)
        logits = self.classifier(cls_output)

        return logits

# 2. 创建模拟数据集

class TextDataset(Dataset):
    """模拟文本数据集"""
    def __init__(self, num_samples=1000, max_length=128, vocab_size=10000):
        self.num_samples = num_samples
        self.max_length = max_length
        self.vocab_size = vocab_size

        # 生成模拟数据
        self.texts = []
        self.labels = []

        # 正面评论模板
        positive_templates = [
            "这部电影非常精彩,演员表演出色,剧情扣人心弦。",
            "产品质量很好,使用体验非常满意,会推荐给朋友。",
            "服务态度很棒,环境优雅,价格合理,值得再次光临。",
            "内容很有价值,讲解清晰易懂,收获很大。"
        ]

        # 负面评论模板
        negative_templates = [
            "这部电影很糟糕,剧情无聊,演员表演生硬。",
            "产品质量差,使用不久就出现故障,非常失望。",
            "服务态度恶劣,环境嘈杂,价格偏高,不会再来。",
            "内容质量低下,讲解混乱,浪费时间。"
        ]

        # 生成样本
        for i in range(num_samples):
            if random.random() > 0.5:
                template = random.choice(positive_templates)
                label = 1  # 正面
            else:
                template = random.choice(negative_templates)
                label = 0  # 负面

            # 添加一些随机变化
            words = template.split()
            if len(words) > 5:
                # 随机替换一些词
                idx = random.randint(0, len(words)-1)
                words[idx] = f"词{random.randint(1000, 9999)}"
                text = " ".join(words)
            else:
                text = template

            self.texts.append(text)
            self.labels.append(label)

        # 构建词汇表
        self.build_vocab()

    def build_vocab(self):
        """构建词汇表"""
        all_text = " ".join(self.texts)
        words = all_text.split()
        word_counts = Counter(words)

        # 选择最常见的词
        most_common = word_counts.most_common(self.vocab_size - 2)

        self.vocab = {
            '[PAD]': 0,
            '[UNK]': 1,
        }

        for i, (word, count) in enumerate(most_common):
            self.vocab[word] = i + 2

        self.inv_vocab = {v: k for k, v in self.vocab.items()}

    def text_to_ids(self, text):
        """将文本转换为ID序列"""
        words = text.split()
        ids = [self.vocab.get(word, 1) for word in words]  # 1是[UNK]

        # 截断或填充
        if len(ids) > self.max_length - 2:  # 为[CLS]和[SEP]留位置
            ids = ids[:self.max_length - 2]
        else:
            ids = ids + [0] * (self.max_length - 2 - len(ids))

        # 添加[CLS]和[SEP]
        ids = [2] + ids + [3]  # 假设2是[CLS],3是[SEP]

        return ids

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

    def __getitem__(self, idx):
        text = self.texts[idx]
        label = self.labels[idx]

        input_ids = self.text_to_ids(text)
        attention_mask = [1 if token_id != 0 else 0 for token_id in input_ids]

        return {
            'input_ids': torch.tensor(input_ids, dtype=torch.long),
            'attention_mask': torch.tensor(attention_mask, dtype=torch.long),
            'labels': torch.tensor(label, dtype=torch.long)
        }

# 3. 模拟预训练过程(简化的MLM任务)

def pretrain_bert(model, dataloader, optimizer, device):
    """简化的BERT预训练(掩码语言模型)"""
    model.train()
    total_loss = 0
    criterion = nn.CrossEntropyLoss()

    for batch in dataloader:
        input_ids = batch['input_ids'].to(device)
        attention_mask = batch['attention_mask'].to(device)

        batch_size, seq_len = input_ids.shape

        # 创建掩码:随机选择15%的token进行掩码
        mask_prob = torch.rand(input_ids.shape, device=device) < 0.15
        # 确保特殊token不被掩码
        special_tokens = (input_ids < 4)  # [PAD], [UNK], [CLS], [SEP]
        mask_prob = mask_prob & ~special_tokens

        # 创建掩码标签
        masked_labels = input_ids.clone()
        masked_labels[~mask_prob] = -100  # 忽略未掩码的token

        # 创建掩码输入
        masked_input = input_ids.clone()
        # 80%替换为[MASK],10%替换为随机词,10%保持不变
        mask_token = 4  # 假设4是[MASK]token

        # 80%替换为[MASK]
        mask_mask = mask_prob & (torch.rand(input_ids.shape, device=device) < 0.8)
        masked_input[mask_mask] = mask_token

        # 10%替换为随机词
        random_mask = mask_prob & ~mask_mask & (torch.rand(input_ids.shape, device=device) < 0.5)
        random_tokens = torch.randint(5, vocab_size, input_ids.shape, device=device)
        masked_input[random_mask] = random_tokens[random_mask]

        # 10%保持不变

        # 前向传播
        outputs = model(masked_input, attention_mask=attention_mask)

        # 预测掩码token
        prediction_scores = nn.Linear(d_model, vocab_size).to(device)(outputs)

        # 计算损失
        loss = criterion(
            prediction_scores.view(-1, vocab_size), 
            masked_labels.view(-1)
        )

        # 反向传播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        total_loss += loss.item()

    return total_loss / len(dataloader)

# 4. 微调过程(情感分析)

def finetune_bert(model, train_loader, val_loader, optimizer, device, num_epochs):
    """在情感分析任务上微调BERT"""
    model.train()
    criterion = nn.CrossEntropyLoss()

    train_losses = []
    val_accuracies = []

    for epoch in range(num_epochs):
        # 训练阶段
        model.train()
        epoch_loss = 0

        for batch in train_loader:
            input_ids = batch['input_ids'].to(device)
            attention_mask = batch['attention_mask'].to(device)
            labels = batch['labels'].to(device)

            # 前向传播
            logits = model(input_ids, attention_mask)
            loss = criterion(logits, labels)

            # 反向传播
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            epoch_loss += loss.item()

        avg_train_loss = epoch_loss / len(train_loader)
        train_losses.append(avg_train_loss)

        # 验证阶段
        model.eval()
        correct = 0
        total = 0

        with torch.no_grad():
            for batch in val_loader:
                input_ids = batch['input_ids'].to(device)
                attention_mask = batch['attention_mask'].to(device)
                labels = batch['labels'].to(device)

                logits = model(input_ids, attention_mask)
                predictions = torch.argmax(logits, dim=1)

                correct += (predictions == labels).sum().item()
                total += labels.size(0)

        val_accuracy = correct / total
        val_accuracies.append(val_accuracy)

        print(f'Epoch {epoch+1}/{num_epochs}:')
        print(f'  训练损失: {avg_train_loss:.4f}')
        print(f'  验证准确率: {val_accuracy:.4f}')

    return train_losses, val_accuracies

# 5. 主程序

def main():
    print("开始自监督学习示例...")

    # 创建数据集
    print("创建模拟数据集...")
    full_dataset = TextDataset(num_samples=2000, max_length=max_length, vocab_size=vocab_size)

    # 分割训练集和验证集
    train_size = int(0.8 * len(full_dataset))
    val_size = len(full_dataset) - train_size
    train_dataset, val_dataset = torch.utils.data.random_split(full_dataset, [train_size, val_size])

    train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
    val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)

    # 创建BERT模型
    print("创建BERT模型...")
    bert_model = SimpleBERT(
        vocab_size=vocab_size,
        d_model=d_model,
        n_heads=n_heads,
        n_layers=n_layers,
        d_ff=d_ff,
        max_length=max_length
    ).to(device)

    # 模拟预训练(在实际应用中,这需要大量数据和计算资源)
    print("开始模拟预训练...")
    pretrain_optimizer = optim.Adam(bert_model.parameters(), lr=lr)

    # 简化的预训练(在实际应用中需要更多轮次)
    for epoch in range(3):  # 实际预训练需要数千轮
        pretrain_loss = pretrain_bert(bert_model, train_loader, pretrain_optimizer, device)
        print(f'预训练轮次 {epoch+1}/3, 损失: {pretrain_loss:.4f}')

    # 创建分类模型
    print("创建分类模型...")
    classification_model = BERTForSequenceClassification(bert_model, num_classes=2).to(device)

    # 微调
    print("开始微调...")
    finetune_optimizer = optim.Adam(classification_model.parameters(), lr=lr/10)  # 更小的学习率

    train_losses, val_accuracies = finetune_bert(
        classification_model, train_loader, val_loader, 
        finetune_optimizer, device, num_epochs
    )

    # 绘制结果
    plt.figure(figsize=(12, 5))

    plt.subplot(1, 2, 1)
    plt.plot(train_losses)
    plt.title('训练损失')
    plt.xlabel('轮次')
    plt.ylabel('损失')
    plt.grid(True)

    plt.subplot(1, 2, 2)
    plt.plot(val_accuracies)
    plt.title('验证准确率')
    plt.xlabel('轮次')
    plt.ylabel('准确率')
    plt.grid(True)

    plt.tight_layout()
    plt.savefig('self_supervised_results/training_results.png', dpi=150)
    plt.show()

    # 测试模型
    print("测试模型...")
    classification_model.eval()

    test_sentences = [
        "这部电影非常精彩,演员表演出色。",
        "产品质量很差,使用体验糟糕。",
        "服务态度很好,环境优雅舒适。",
        "内容质量低下,讲解混乱不清。"
    ]

    print("\n模型预测结果:")
    for sentence in test_sentences:
        # 预处理
        input_ids = torch.tensor([full_dataset.text_to_ids(sentence)]).to(device)
        attention_mask = torch.tensor([[1 if i != 0 else 0 for i in input_ids[0]]]).to(device)

        # 预测
        with torch.no_grad():
            logits = classification_model(input_ids, attention_mask)
            prediction = torch.argmax(logits, dim=1).item()
            confidence = torch.softmax(logits, dim=1)[0][prediction].item()

        sentiment = "正面" if prediction == 1 else "负面"
        print(f'  句子: "{sentence}"')
        print(f'  情感: {sentiment} (置信度: {confidence:.4f})')
        print()

    # 保存模型
    torch.save({
        'bert_state_dict': bert_model.state_dict(),
        'classifier_state_dict': classification_model.state_dict(),
        'vocab': full_dataset.vocab
    }, 'self_supervised_model.pth')

    print("模型已保存!")
    print("程序运行完成!")

if __name__ == "__main__":
    main()

程序说明与运行指南

这个完整的自监督学习示例展示了BERT模型的核心概念和实际应用:

程序特点:

  1. 完整的BERT实现

    • 多头自注意力机制

    • 位置编码

    • Transformer编码器层

    • 掩码语言模型预训练

  2. 自监督学习流程

    • 模拟预训练过程

    • 下游任务微调

    • 迁移学习演示

  3. 情感分析应用

    • 文本分类任务

    • 模型评估和测试

    • 结果可视化

运行说明:

  1. 环境要求

    pip install torch matplotlib
    
  2. 运行程序

    python self_supervised_learning.py
    
  3. 程序输出

    • 显示预训练和微调过程

    • 绘制训练损失和准确率曲线

    • 测试模型在示例句子上的表现

    • 保存训练好的模型

  4. 自定义调整

    • 修改num_epochs调整训练轮数

    • 修改d_model调整模型维度

    • 修改n_layers调整Transformer层数

这个程序完整地展示了自监督学习的核心思想:通过无监督的预训练学习通用语言表示,然后通过有监督的微调适应特定任务。虽然这是一个简化版本,但它清晰地展示了BERT的工作原理和应用方法。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐