当前位置: 首页 > article >正文

PyTorch深度学习框架进阶学习计划 - 第21天:自然语言处理基础

PyTorch深度学习框架进阶学习计划 - 第21天

自然语言处理基础

今天我们将深入学习自然语言处理(NLP)的基础概念,重点关注词嵌入技术、序列建模原理以及主流模型之间的区别和优缺点。通过理解这些基础知识,你将能够更好地应用PyTorch构建NLP应用。

1. 词嵌入原理与实现

词嵌入(Word Embeddings) 是NLP中的核心概念,它将单词映射到连续向量空间,使得语义相似的词在向量空间中距离较近。

为什么需要词嵌入?

传统的独热编码(One-Hot Encoding)存在维度灾难和语义鸿沟问题:

  • 对于拥有百万级词汇的语言,独热向量非常稀疏且维度巨大
  • 独热编码无法表达单词之间的语义关系

词嵌入解决了这些问题,它能够:

  • 将单词表示为低维稠密向量(通常30-300维)
  • 捕捉词之间的语义和句法关系
  • 支持词向量的算术运算(如 king - man + woman ≈ queen)

让我们使用PyTorch实现简单的词嵌入:

import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE

# 定义一个简单的数据集
sentences = [
    "I love deep learning",
    "I love PyTorch",
    "I enjoy natural language processing",
    "I like neural networks",
    "Neural networks are fascinating"
]

# 数据预处理
def preprocess(sentences):
    # 构建词汇表
    word_list = " ".join(sentences).lower().split()
    vocab = sorted(set(word_list))
    word_to_idx = {word: idx for idx, word in enumerate(vocab)}
    idx_to_word = {idx: word for idx, word in enumerate(vocab)}
    
    # 构建训练样本 (上下文词, 目标词)
    window_size = 2
    data = []
    
    for sentence in sentences:
        words = sentence.lower().split()
        for center_idx, center_word in enumerate(words):
            # 上下文窗口内的词
            context_words = []
            for i in range(max(0, center_idx - window_size), min(len(words), center_idx + window_size + 1)):
                if i != center_idx:
                    context_words.append(words[i])
            
            # 创建样本对
            for context_word in context_words:
                data.append((word_to_idx[center_word], word_to_idx[context_word]))
    
    return data, word_to_idx, idx_to_word, vocab

# 准备数据
data, word_to_idx, idx_to_word, vocab = preprocess(sentences)
vocab_size = len(vocab)

# 定义Skip-Gram模型
class SkipGramModel(nn.Module):
    def __init__(self, vocab_size, embedding_dim):
        super(SkipGramModel, self).__init__()
        self.embeddings = nn.Embedding(vocab_size, embedding_dim)
        self.linear = nn.Linear(embedding_dim, vocab_size)
        
    def forward(self, inputs):
        embeds = self.embeddings(inputs)
        output = self.linear(embeds)
        return output

# 训练模型
def train_skipgram(data, vocab_size, embedding_dim=10, epochs=1000, lr=0.01):
    model = SkipGramModel(vocab_size, embedding_dim)
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=lr)
    
    for epoch in range(epochs):
        total_loss = 0
        for center_word, context_word in data:
            center_word = torch.tensor([center_word], dtype=torch.long)
            context_word = torch.tensor([context_word], dtype=torch.long)
            
            # 前向传播
            optimizer.zero_grad()
            log_probs = model(center_word)
            loss = criterion(log_probs, context_word)
            
            # 反向传播
            loss.backward()
            optimizer.step()
            
            total_loss += loss.item()
        
        if (epoch + 1) % 100 == 0:
            print(f'Epoch {epoch+1}/{epochs}, Loss: {total_loss/len(data):.4f}')
    
    return model

# 训练模型并可视化词嵌入
embedding_dim = 10
model = train_skipgram(data, vocab_size, embedding_dim, epochs=1000, lr=0.01)

# 提取词嵌入
embeddings = model.embeddings.weight.detach().numpy()

# 使用t-SNE可视化词嵌入
def visualize_embeddings(embeddings, idx_to_word):
    tsne = TSNE(n_components=2, random_state=42)
    embeddings_tsne = tsne.fit_transform(embeddings)
    
    plt.figure(figsize=(10, 8))
    for i, (x, y) in enumerate(embeddings_tsne):
        plt.scatter(x, y)
        plt.annotate(idx_to_word[i], (x, y), fontsize=12)
    plt.title("Word Embeddings Visualization")
    plt.grid(True)
    plt.show()

# 可视化词嵌入
visualize_embeddings(embeddings, idx_to_word)

# 展示一些词向量之间的相似度
def cosine_similarity(v1, v2):
    return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))

# 计算"pytorch"和"learning"的相似度
word1, word2 = "pytorch", "neural"
if word1 in word_to_idx and word2 in word_to_idx:
    idx1, idx2 = word_to_idx[word1], word_to_idx[word2]
    similarity = cosine_similarity(embeddings[idx1], embeddings[idx2])
    print(f"Cosine similarity between '{word1}' and '{word2}': {similarity:.4f}")

# 查找与给定词最相似的词
def find_most_similar(word, word_to_idx, idx_to_word, embeddings, top_k=3):
    if word not in word_to_idx:
        return []
    
    word_idx = word_to_idx[word]
    word_vec = embeddings[word_idx]
    
    similarities = []
    for i, vec in enumerate(embeddings):
        if i != word_idx:
            similarity = cosine_similarity(word_vec, vec)
            similarities.append((idx_to_word[i], similarity))
    
    return sorted(similarities, key=lambda x: x[1], reverse=True)[:top_k]

# 查找与"deep"最相似的三个词
word = "deep"
if word in word_to_idx:
    most_similar = find_most_similar(word, word_to_idx, idx_to_word, embeddings)
    print(f"Words most similar to '{word}':")
    for similar_word, similarity in most_similar:
        print(f"  {similar_word}: {similarity:.4f}")

上面的代码实现了一个简单的Skip-Gram模型,它是Word2Vec的一种实现方式。此模型通过预测上下文词来学习中心词的向量表示。在实际应用中,我们通常会使用预训练好的词嵌入,如GloVe、FastText或Word2Vec。

2. Word2Vec与BERT语义表示对比

Word2Vec和BERT代表了两代不同的词嵌入技术,它们在语义捕获能力上有显著差异。

Word2Vec特点:
  1. 静态词嵌入:每个词只有一个固定的向量表示
  2. 上下文无关:无法处理一词多义的问题
  3. 浅层模型:基于简单的神经网络架构
  4. 训练方式:基于词的共现统计,使用Skip-Gram或CBOW模型
  5. 无监督学习:利用大规模非标注文本进行训练
BERT特点:
  1. 动态词嵌入:根据上下文生成不同的词表示
  2. 上下文相关:能够处理一词多义的问题
  3. 深层模型:基于Transformer的多层双向架构
  4. 训练方式:使用掩码语言模型(MLM)和下一句预测(NSP)任务
  5. 迁移学习:预训练+微调的范式

让我们通过代码实现,对比这两种模型的语义表示能力:

import torch
import numpy as np
from gensim.models import Word2Vec
from transformers import BertTokenizer, BertModel
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA

# 准备测试句子展示一词多义问题
sentences = [
    "The bank by the river is eroding.",
    "I deposited money in the bank yesterday.",
    "The bank approved my loan application."
]

# 1. 使用Word2Vec训练静态词嵌入
# 准备训练数据
training_data = [s.lower().split() for s in sentences]

# 训练Word2Vec模型
word2vec_model = Word2Vec(sentences=training_data, vector_size=100, window=5, min_count=1, workers=4)

# 2. 使用BERT获取上下文相关的词嵌入
# 加载预训练的BERT模型和分词器
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
bert_model = BertModel.from_pretrained('bert-base-uncased')

# 设置为评估模式
bert_model.eval()

# 函数: 使用BERT获取词在特定上下文中的嵌入
def get_bert_embedding(sentence, target_word):
    # 标记化句子
    tokens = tokenizer.tokenize(sentence.lower())
    
    # 找到目标词的索引
    try:
        target_idx = tokens.index(target_word)
    except ValueError:
        # 处理分词器可能将词分成子词的情况
        sub_tokens = tokenizer.tokenize(target_word)
        if sub_tokens[0] in tokens:
            target_idx = tokens.index(sub_tokens[0])
        else:
            print(f"Warning: '{target_word}' not found in tokenized sentence.")
            return None
    
    # 转换为模型输入
    indexed_tokens = tokenizer.convert_tokens_to_ids(tokens)
    tokens_tensor = torch.tensor([indexed_tokens])
    
    # 获取BERT输出
    with torch.no_grad():
        outputs = bert_model(tokens_tensor)
        hidden_states = outputs.last_hidden_state
    
    # 提取目标词的嵌入
    # BERT使用WordPiece分词,可能会将单词分成多个子词
    # 这里我们简单地取第一个子词的嵌入
    token_embedding = hidden_states[0, target_idx].numpy()
    
    return token_embedding

# 比较Word2Vec和BERT对"bank"一词的表示
target_word = "bank"

# Word2Vec的表示(静态)
if target_word in word2vec_model.wv:
    word2vec_embedding = word2vec_model.wv[target_word]
    print(f"Word2Vec embedding of '{target_word}': Shape = {word2vec_embedding.shape}")
else:
    print(f"'{target_word}' not in vocabulary")

# BERT的表示(上下文相关)
bert_embeddings = []
contexts = []

for sentence in sentences:
    bert_embedding = get_bert_embedding(sentence, target_word)
    if bert_embedding is not None:
        bert_embeddings.append(bert_embedding)
        contexts.append(sentence)

if bert_embeddings:
    print(f"BERT embeddings of '{target_word}' in different contexts: Shape = {bert_embeddings[0].shape}")

# 可视化不同上下文中的词表示
def visualize_embeddings():
    # 使用PCA将高维向量降至2维
    pca = PCA(n_components=2)
    
    # 对BERT嵌入进行降维
    if bert_embeddings:
        bert_2d = pca.fit_transform(bert_embeddings)
        
        plt.figure(figsize=(10, 6))
        for i, (context, embed_2d) in enumerate(zip(contexts, bert_2d)):
            plt.scatter(embed_2d[0], embed_2d[1], marker='o', s=100)
            plt.annotate(f"Context {i+1}", (embed_2d[0], embed_2d[1]), fontsize=12)
        
        # 如果有Word2Vec嵌入,也进行可视化
        if 'word2vec_embedding' in locals():
            word2vec_2d = pca.transform([word2vec_embedding])
            plt.scatter(word2vec_2d[0, 0], word2vec_2d[0, 1], marker='x', s=100, color='red')
            plt.annotate("Word2Vec (static)", (word2vec_2d[0, 0], word2vec_2d[0, 1]), fontsize=12)
        
        plt.title(f"Embeddings of '{target_word}' in Different Contexts")
        plt.grid(True)
        
        # 添加上下文说明
        plt.figtext(0.5, 0.01, "\n".join([f"Context {i+1}: {ctx}" for i, ctx in enumerate(contexts)]), 
                   ha="center", fontsize=10, bbox={"facecolor":"orange", "alpha":0.2, "pad":5})
        
        plt.tight_layout()
        plt.show()

# 执行可视化
visualize_embeddings()

# 计算BERT嵌入之间的余弦相似度
def cosine_similarity(v1, v2):
    return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))

# 打印不同上下文中BERT嵌入的相似度
if len(bert_embeddings) > 1:
    print("\nBERT embedding similarities between different contexts:")
    for i in range(len(bert_embeddings)):
        for j in range(i+1, len(bert_embeddings)):
            sim = cosine_similarity(bert_embeddings[i], bert_embeddings[j])
            print(f"Similarity between Context {i+1} and Context {j+1}: {sim:.4f}")

上述代码对比了Word2Vec和BERT在处理一词多义问题上的差异。使用"bank"这个词作为例子,可以看到:

  1. Word2Vec为"bank"仅提供一个静态向量表示,无论它出现在河岸、金融机构还是其他上下文中
  2. BERT为"bank"在不同上下文中生成不同的向量表示,能够捕捉到词的多义性
  3. 通过余弦相似度可以看到,BERT表示出的不同语义上下文中的"bank"向量之间的相似度较低

BERT的优势:

  • 能够处理一词多义问题
  • 捕捉丰富的上下文语义信息
  • 支持微调适应下游任务
  • 性能在多种NLP任务上表现优异

Word2Vec的优势:

  • 计算效率高,训练和推理速度快
  • 资源需求小,即使在普通硬件上也能运行
  • 模型简单,易于理解和实现
  • 对于某些简单任务效果足够好

3. RNN及其变体的序列建模能力

循环神经网络(RNN)是处理序列数据的经典模型,它通过维护一个隐藏状态来捕捉序列中的依赖关系。

RNN及其变体的对比
模型类型结构特点优势劣势适用场景
标准RNN简单的循环结构结构简单,参数少难以捕捉长距离依赖,梯度问题严重简短序列,实时性要求高的场景
LSTM包含遗忘门、输入门和输出门能捕捉长距离依赖,缓解梯度问题参数较多,计算复杂长序列建模,如机器翻译、语音识别
GRU包含更新门和重置门性能接近LSTM,但参数更少在某些任务上不如LSTM计算资源受限时的长序列建模
双向RNN同时考虑过去和未来信息对上下文建模更全面不适用于实时场景,需要完整序列文本分类、命名实体识别等

让我们使用PyTorch实现一个简单的RNN语言模型,并分析其中的梯度问题:

import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
import time

# 设置随机种子,确保结果可复现
torch.manual_seed(42)

# 定义一个简单的字符级RNN语言模型
class CharRNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size, model_type='rnn'):
        super(CharRNN, self).__init__()
        self.hidden_size = hidden_size
        self.model_type = model_type.lower()
        
        # 字符嵌入层
        self.embedding = nn.Embedding(input_size, hidden_size)
        
        # 根据模型类型选择RNN层
        if self.model_type == 'rnn':
            self.rnn = nn.RNN(hidden_size, hidden_size, batch_first=True)
        elif self.model_type == 'lstm':
            self.rnn = nn.LSTM(hidden_size, hidden_size, batch_first=True)
        elif self.model_type == 'gru':
            self.rnn = nn.GRU(hidden_size, hidden_size, batch_first=True)
        else:
            raise ValueError("Unsupported model type. Use 'rnn', 'lstm', or 'gru'.")
        
        # 输出层
        self.fc = nn.Linear(hidden_size, output_size)
    
    def forward(self, x, hidden=None):
        # 初始化隐藏状态(如果没有提供)
        if hidden is None:
            if self.model_type == 'lstm':
                h0 = torch.zeros(1, x.size(0), self.hidden_size)
                c0 = torch.zeros(1, x.size(0), self.hidden_size)
                hidden = (h0, c0)
            else:
                hidden = torch.zeros(1, x.size(0), self.hidden_size)
        
        # 前向传播
        embedded = self.embedding(x)
        
        if self.model_type == 'lstm':
            output, (hidden, cell) = self.rnn(embedded, hidden)
            hidden_for_grad = hidden
        else:
            output, hidden = self.rnn(embedded, hidden)
            hidden_for_grad = hidden
        
        output = self.fc(output)
        
        # 保存隐藏状态的梯度范数(用于分析梯度问题)
        self.hidden_grad_norm = None
        hidden_for_grad.register_hook(lambda grad: self._save_grad_norm(grad))
        
        return output, hidden
    
    def _save_grad_norm(self, grad):
        self.hidden_grad_norm = torch.norm(grad).item()

# 创建字符级语言建模的数据集
def create_dataset(text, seq_length=25):
    chars = sorted(list(set(text)))
    char_to_idx = {ch: i for i, ch in enumerate(chars)}
    idx_to_char = {i: ch for i, ch in enumerate(chars)}
    
    # 将文本转换为索引
    text_encoded = [char_to_idx[ch] for ch in text]
    
    # 创建输入-输出对:每个输入是一个序列,输出是序列之后的字符
    X, y = [], []
    for i in range(0, len(text_encoded) - seq_length):
        X.append(text_encoded[i:i+seq_length])
        y.append(text_encoded[i+seq_length])
    
    # 转换为PyTorch张量
    X = torch.tensor(X, dtype=torch.long)
    y = torch.tensor(y, dtype=torch.long)
    
    return X, y, chars, char_to_idx, idx_to_char

# 训练模型并分析梯度
def train_and_analyze(model_type='rnn', seq_length=25, num_epochs=5):
    # 使用一段文本数据
    text = """It was a bright cold day in April, and the clocks were striking thirteen. 
    Winston Smith, his chin nuzzled into his breast in an effort to escape the vile wind, 
    slipped quickly through the glass doors of Victory Mansions, though not quickly enough to 
    prevent a swirl of gritty dust from entering along with him."""
    
    # 创建数据集
    X, y, chars, char_to_idx, idx_to_char = create_dataset(text, seq_length)
    
    # 模型参数
    input_size = len(chars)  # 词汇表大小
    hidden_size = 128
    output_size = len(chars)  # 词汇表大小
    
    # 创建模型
    model = CharRNN(input_size, hidden_size, output_size, model_type)
    
    # 损失函数和优化器
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=0.001)
    
    # 用于记录梯度范数
    grad_norms = []
    losses = []
    
    # 训练模型
    for epoch in range(num_epochs):
        # 记录每个位置的梯度范数
        epoch_grad_norms = []
        epoch_losses = []
        
        # 初始化隐藏状态
        if model_type == 'lstm':
            hidden = (torch.zeros(1, 1, hidden_size), torch.zeros(1, 1, hidden_size))
        else:
            hidden = torch.zeros(1, 1, hidden_size)
        
        # 每个样本独立训练(为了分析梯度流动)
        for i in range(len(X)):
            # 获取样本
            input_seq = X[i].unsqueeze(0)
            target = y[i]
            
            # 前向传播
            optimizer.zero_grad()
            output, hidden = model(input_seq, hidden)
            
            # 计算损失
            loss = criterion(output.squeeze(0)[-1], target)
            
            # 反向传播
            loss.backward(retain_graph=True)
            
            # 记录每个位置的梯度范数
            if model.hidden_grad_norm is not None:
                epoch_grad_norms.append(model.hidden_grad_norm)
            
            # 记录损失
            epoch_losses.append(loss.item())
            
            # 更新权重
            optimizer.step()
            
            # 分离隐藏状态(防止梯度在序列之间流动)
            if model_type == 'lstm':
                hidden = (hidden[0].detach(), hidden[1].detach())
            else:
                hidden = hidden.detach()
        
        # 保存本轮的梯度范数
        grad_norms.append(epoch_grad_norms)
        losses.append(np.mean(epoch_losses))
        
        print(f"Epoch {epoch+1}/{num_epochs}, Loss: {losses[-1]:.4f}")
    
    return model, grad_norms, losses

# 分析不同RNN变体的梯度问题
def analyze_gradients():
    # 训练不同类型的RNN并收集梯度信息
    models = ['rnn', 'lstm', 'gru']
    all_grad_norms = {}
    all_losses = {}
    
    for model_type in models:
        print(f"\nTraining {model_type.upper()} model...")
        _, grad_norms, losses = train_and_analyze(model_type=model_type, num_epochs=3)
        all_grad_norms[model_type] = grad_norms
        all_losses[model_type] = losses
    
    # 可视化梯度范数
    plt.figure(figsize=(12, 10))
    
    # 第一个子图:每个模型最后一个epoch的梯度分布
    plt.subplot(2, 1, 1)
    for model_type in models:
        plt.plot(all_grad_norms[model_type][-1], label=f"{model_type.upper()}")
    plt.title("Gradient Norms in Last Epoch")
    plt.xlabel("Sequence Position")
    plt.ylabel("Gradient Norm")
    plt.legend()
    plt.grid(True)
    
    # 第二个子图:每个模型的损失曲线
    plt.subplot(2, 1, 2)
    for model_type in models:
        plt.plot(all_losses[model_type], label=f"{model_type.upper()}")
    plt.title("Training Loss")
    plt.xlabel("Epoch")
    plt.ylabel("Loss")
    plt.legend()
    plt.grid(True)
    
    plt.tight_layout()
    plt.show()

# 执行分析
analyze_gradients()

# 梯度裁剪演示
def demonstrate_gradient_clipping():
    # 使用标准RNN训练,分别展示有无梯度裁剪的区别
    text = """To be, or not to be, that is the question:
    Whether 'tis nobler in the mind to suffer
    The slings and arrows of outrageous fortune,
    Or to take arms against a sea of troubles
    And by opposing end them."""
    
    # 创建数据集
    X, y, chars, char_to_idx, idx_to_char = create_dataset(text, seq_length=50)
    
    # 模型参数
    input_size = len(chars)
    hidden_size = 128
    output_size = len(chars)
    
    # 训练函数
    def train_with_clipping(use_clipping, max_norm=1.0):
        model = CharRNN(input_size, hidden_size, output_size, 'rnn')
        criterion = nn.CrossEntropyLoss()
        optimizer = optim.Adam(model.parameters(), lr=0.01)
        
        losses = []
        grad_norms = []
        
        for epoch in range(10):
            epoch_loss = 0
            epoch_grad_norm = 0
            hidden = torch.zeros(1, 1, hidden_size)
            
            for i in range(len(X)):
                optimizer.zero_grad()
                input_seq = X[i].unsqueeze(0)
                target = y[i]
                
                output, hidden = model(input_seq, hidden)
                loss = criterion(output.squeeze(0)[-1], target)
                
                loss.backward()
                
                # 计算梯度范数
                total_norm = 0
                for p in model.parameters():
                    if p.grad is not None:
                        param_norm = p.grad.data.norm(2)
                        total_norm += param_norm.item() ** 2
                total_norm = total_norm ** 0.5
                grad_norms.append(total_norm)
                
                # 应用梯度裁剪(如果启用)
                if use_clipping:
                    torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm)
                
                optimizer.step()
                hidden = hidden.detach()
                
                epoch_loss += loss.item()
            
            losses.append(epoch_loss / len(X))
            print(f"Epoch {epoch+1}/10, Loss: {losses[-1]:.4f}")
        
        return losses, grad_norms
    
    # 训练带梯度裁剪和不带梯度裁剪的模型
    print("\nTraining RNN without gradient clipping...")
    no_clip_losses, no_clip_grads = train_with_clipping(use_clipping=False)
    
    print("\nTraining RNN with gradient clipping...")
    clip_losses, clip_grads = train_with_clipping(use_clipping=True, max_norm=1.0)
    
    # 可视化结果
    plt.figure(figsize=(12, 10))
    
    # 梯度范数
    plt.subplot(2, 1, 1)
    plt.plot(no_clip_grads, label='Without Clipping')
    plt.plot(clip_grads, label='With Clipping')
    plt.yscale('log')  # 使用对数尺度更好地显示范围差异
    plt.title('Gradient Norms During Training')
    plt.xlabel('Training Step')
    plt.ylabel('Gradient Norm (log scale)')
    plt.legend()
    plt.grid(True)
    
    # 损失
    plt.subplot(2, 1, 2)
    plt.plot(no_clip_losses, label='Without Clipping')
    plt.plot(clip_losses, label='With Clipping')
    plt.title('Training Loss')
    plt.xlabel('Epoch')
    plt.ylabel('Loss')
    plt.legend()
    plt.grid(True)
    
    plt.tight_layout()
    plt.show()

# 执行梯度裁剪演示
demonstrate_gradient_clipping()

4. RNN的梯度问题分析与解决方案

RNN在处理长序列时面临的主要挑战是梯度消失和梯度爆炸问题,这严重影响了模型捕捉长距离依赖的能力。

梯度消失问题

当误差信号向前传播时,由于重复的矩阵乘法和激活函数的导数(如sigmoid函数导数最大值为0.25),梯度会指数级减小,导致长距离依赖的信息几乎无法影响模型参数更新。

例如,对于标准RNN,如果隐藏层转换矩阵的最大特征值小于1,那么梯度会随着时间步的增加呈指数级衰减:

∂L/∂h_t = ∏(i=t+1 to T) (W^T * diag(f'(h_i)))

当这个连乘积中的每个项小于1时,乘积会随着序列长度增加而迅速趋向于0。

梯度爆炸问题

相反,如果隐藏层转换矩阵的最大特征值大于1,梯度会随着时间步的增加呈指数级增长,导致参数更新过大,模型无法收敛。

解决方案
  1. 梯度裁剪: 当梯度范数超过某个阈值时,按比例缩小梯度,防止梯度爆炸
  2. 使用ReLU激活函数: 避免sigmoid和tanh在饱和区域的梯度消失问题
  3. 使用LSTM/GRU: 这些变体通过门控机制和额外的记忆单元缓解梯度问题
  4. 残差连接: 类似ResNet的跳跃连接可以帮助梯度直接流动
  5. Transformer架构: 完全抛弃循环结构,使用自注意力机制建模序列关系
LSTM如何缓解梯度问题

LSTM通过以下机制缓解梯度问题:

  1. 记忆单元(Cell State): 提供了一条信息高速公路,允许梯度无阻碍地流动
  2. 遗忘门: 控制丢弃哪些信息,减少不相关信息的干扰
  3. 输入门: 控制新信息的添加,使模型能够选择性地更新状态
  4. 输出门: 控制哪些信息会输出,进一步优化信息流

5. 动手实践:构建基础文本分类器

现在,让我们将所学知识应用到实际问题中,构建一个简单的文本分类器:

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt

# 定义一个简单的数据集
# 使用一些示例句子和情感标签(0=负面,1=正面)
sentences = [
    "I love this movie so much!",
    "This film is amazing and wonderful",
    "The acting was great and the plot was engaging",
    "I enjoyed watching this show",
    "This is my favorite movie of all time",
    "The story was captivating from start to finish",
    "I hate this movie, it was terrible",
    "This film is boring and predictable",
    "The acting was poor and the plot made no sense",
    "I regret watching this show",
    "This is the worst movie I've ever seen",
    "The story was confusing and uninteresting"
]

labels = [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0]

# 数据预处理
def preprocess_data(sentences, labels):
    # 创建词汇表
    vocab = set()
    for sentence in sentences:
        for word in sentence.lower().split():
            vocab.add(word)
    
    word_to_idx = {word: i+1 for i, word in enumerate(sorted(vocab))}  # 0保留为padding
    word_to_idx['<PAD>'] = 0
    
    # 将句子转换为索引序列
    indexed_sentences = []
    for sentence in sentences:
        indexed_sentence = [word_to_idx[word] for word in sentence.lower().split()]
        indexed_sentences.append(indexed_sentence)
    
    # 填充序列到相同长度
    max_length = max(len(s) for s in indexed_sentences)
    padded_sentences = []
    for sentence in indexed_sentences:
        padded = sentence + [0] * (max_length - len(sentence))
        padded_sentences.append(padded)
    
    # 转换为PyTorch张量
    X = torch.tensor(padded_sentences, dtype=torch.long)
    y = torch.tensor(labels, dtype=torch.float32)
    
    return X, y, word_to_idx, len(vocab) + 1

# 准备数据
X, y, word_to_idx, vocab_size = preprocess_data(sentences, labels)

# 定义模型
class TextClassifier(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim, model_type='lstm'):
        super(TextClassifier, self).__init__()
        
        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        self.model_type = model_type.lower()
        
        if model_type == 'lstm':
            self.rnn = nn.LSTM(embedding_dim, hidden_dim, batch_first=True)
        elif model_type == 'gru':
            self.rnn = nn.GRU(embedding_dim, hidden_dim, batch_first=True)
        else:
            self.rnn = nn.RNN(embedding_dim, hidden_dim, batch_first=True)
        
        self.fc = nn.Linear(hidden_dim, output_dim)
        
    def forward(self, text):
        # text shape: [batch_size, sequence_length]
        
        embedded = self.embedding(text)
        # embedded shape: [batch_size, sequence_length, embedding_dim]
        
        if self.model_type == 'lstm':
            output, (hidden, cell) = self.rnn(embedded)
        else:
            output, hidden = self.rnn(embedded)
        
        # 使用最后一个时间步的隐藏状态
        if isinstance(hidden, tuple):
            hidden = hidden[0]
        
        hidden = hidden.squeeze(0)
        # hidden shape: [batch_size, hidden_dim]
        
        return self.fc(hidden)

# 初始化模型和训练参数
EMBEDDING_DIM = 100
HIDDEN_DIM = 128
OUTPUT_DIM = 1  # 二分类,输出维度为1

model = TextClassifier(vocab_size, EMBEDDING_DIM, HIDDEN_DIM, OUTPUT_DIM, model_type='lstm')
optimizer = optim.Adam(model.parameters(), lr=0.01)
criterion = nn.BCEWithLogitsLoss()

# 训练模型
def train(model, X, y, epochs=100):
    losses = []
    accuracies = []
    
    for epoch in range(epochs):
        # 前向传播
        optimizer.zero_grad()
        predictions = model(X).squeeze(1)
        loss = criterion(predictions, y)
        
        # 反向传播
        loss.backward()
        optimizer.step()
        
        # 计算准确率
        predictions = torch.round(torch.sigmoid(predictions))
        correct = (predictions == y).float().sum()
        accuracy = correct / len(y)
        
        losses.append(loss.item())
        accuracies.append(accuracy.item())
        
        if (epoch+1) % 10 == 0:
            print(f"Epoch {epoch+1}/{epochs}, Loss: {loss.item():.4f}, Accuracy: {accuracy.item():.4f}")
    
    return losses, accuracies

# 训练模型
losses, accuracies = train(model, X, y, epochs=100)

# 可视化训练过程
plt.figure(figsize=(12, 5))

plt.subplot(1, 2, 1)
plt.plot(losses)
plt.title('Training Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.grid(True)

plt.subplot(1, 2, 2)
plt.plot(accuracies)
plt.title('Training Accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.grid(True)

plt.tight_layout()
plt.show()

# 测试模型
def predict_sentiment(model, sentence, word_to_idx):
    model.eval()
    
    # 预处理句子
    tokens = sentence.lower().split()
    indexed = [word_to_idx.get(token, 0) for token in tokens]  # 使用0处理未知词
    
    # 转换为张量
    tensor = torch.LongTensor(indexed).unsqueeze(0)
    
    # 预测
    prediction = torch.sigmoid(model(tensor).squeeze())
    
    return prediction.item(), "Positive" if prediction.item() > 0.5 else "Negative"

# 测试一些新句子
test_sentences = [
    "I really enjoyed this film",
    "This movie is terrible and boring",
    "The acting was quite good",
    "I fell asleep during the movie"
]

print("\nTesting the model on new sentences:")
for sentence in test_sentences:
    score, sentiment = predict_sentiment(model, sentence, word_to_idx)
    print(f'"{sentence}" - Sentiment: {sentiment}, Score: {score:.4f}')

# 比较不同RNN变体的性能
def compare_rnn_variants():
    models = {
        'rnn': TextClassifier(vocab_size, EMBEDDING_DIM, HIDDEN_DIM, OUTPUT_DIM, model_type='rnn'),
        'lstm': TextClassifier(vocab_size, EMBEDDING_DIM, HIDDEN_DIM, OUTPUT_DIM, model_type='lstm'),
        'gru': TextClassifier(vocab_size, EMBEDDING_DIM, HIDDEN_DIM, OUTPUT_DIM, model_type='gru')
    }
    
    results = {}
    
    for name, model in models.items():
        print(f"\nTraining {name.upper()} model...")
        optimizer = optim.Adam(model.parameters(), lr=0.01)
        losses, accuracies = train(model, X, y, epochs=100)
        results[name] = (losses, accuracies)
    
    # 可视化比较
    plt.figure(figsize=(12, 10))
    
    plt.subplot(2, 1, 1)
    for name, (losses, _) in results.items():
        plt.plot(losses, label=name.upper())
    plt.title('Training Loss Comparison')
    plt.xlabel('Epoch')
    plt.ylabel('Loss')
    plt.legend()
    plt.grid(True)
    
    plt.subplot(2, 1, 2)
    for name, (_, accuracies) in results.items():
        plt.plot(accuracies, label=name.upper())
    plt.title('Training Accuracy Comparison')
    plt.xlabel('Epoch')
    plt.ylabel('Accuracy')
    plt.legend()
    plt.grid(True)
    
    plt.tight_layout()
    plt.show()

# 比较不同RNN变体
compare_rnn_variants()

总结与关键点回顾

今天我们深入学习了自然语言处理的基础概念,特别是词嵌入技术、序列建模以及RNN的梯度问题。以下是关键知识点的总结:

词嵌入技术

  1. 词嵌入定义:将单词映射到低维稠密向量空间的技术
  2. 主要优势:解决了独热编码的维度灾难和语义鸿沟问题
  3. 实现方式:通过神经网络训练上下文预测任务来学习表示

Word2Vec与BERT对比

特性Word2VecBERT
词表示类型静态词嵌入动态上下文相关表示
一词多义不支持支持
模型复杂度浅层模型深层双向Transformer
训练方法上下文词预测掩码语言模型
资源需求

BERT能够根据上下文生成不同的词向量表示,而Word2Vec为每个词只提供一个固定的向量,无法处理一词多义的情况。

RNN及其变体

  1. 标准RNN:简单的循环结构,但存在严重的梯度问题
  2. LSTM:通过门控机制和记忆单元解决长距离依赖问题
  3. GRU:LSTM的简化版本,性能相近但参数更少
  4. 双向RNN:考虑序列的过去和未来信息,提供更全面的上下文

RNN梯度问题

  1. 梯度消失:梯度随着时间步呈指数级衰减,导致长距离依赖无法学习
  2. 梯度爆炸:梯度随着时间步呈指数级增长,导致训练不稳定
  3. 解决方案
    • 使用LSTM/GRU架构
    • 梯度裁剪
    • 残差连接
    • 使用ReLU激活函数
    • 采用Transformer架构

清华大学全三版的《DeepSeek教程》完整的文档需要的朋友,关注我私信:deepseek 即可获得。

怎么样今天的内容还满意吗?再次感谢朋友们的观看,关注GZH:凡人的AI工具箱,回复666,送您价值199的AI大礼包。最后,祝您早日实现财务自由,还请给个赞,谢谢!


http://www.kler.cn/a/582947.html

相关文章:

  • 尚硅谷爬虫note16
  • 计算机操作系统(二) 操作系统的发展过程
  • 从学习ts的三斜线指令到项目中声明类型的最佳实践
  • winform中chart控件解决显示大量曲线数据卡顿方法——删旧添新法
  • linux下的离线升级替换脚本参考
  • ThinkPHP6用户登录系统的全过程
  • WPS二次开发系列:Android 第三方应用如何获取WPS端内文档
  • 计算机网络——DHCP
  • 蓝桥杯软件比赛_蓝桥杯软件比赛:软考前的实战演练场
  • 编写Dockerfile制作Redis镜像,生成镜像名为redis:v1.1,并推送到私有仓库。
  • 面试之《vue keep-alive原理》
  • Redis存数据就像存钱:RDB定期存款 vs AOF实时记账
  • 《HTML视觉大框架:构建现代网页设计的基石》
  • JVM内存结构笔记04-字符串常量池
  • CentOS 7系统初始化及虚拟化环境搭建手册
  • 基于django+vue的购物商城系统
  • 05.基于 TCP 的远程计算器:从协议设计到高并发实现
  • 图解AUTOSAR_CP_TcpIp
  • 1.数据清洗与预处理——Python数据挖掘(数据抽样、数据分割、异常值处理、缺失值处理)
  • 每天一道算法题【蓝桥杯】【下降路径最小和】