第1.2讲、从 RNN 到 LSTM 再到 Self-Attention:深度学习中序列建模的演进之路

处理序列数据(如文本、语音、时间序列)一直是深度学习的重要课题。在这个领域中,我们从 RNN(Recurrent Neural Network)出发,经历了 LSTM(Long Short-Term Memory)的改进,最终发展到了当今大放异彩的 Self-Attention(自注意力机制)。本文将带你理解它们的概念、工作原理、优缺点,并分析这一演进路径的必然性。


一、RNN:循环神经网络的起点

✅ 概念

RNN 是一种擅长处理序列数据的神经网络结构,其设计核心是:当前的输出不仅取决于当前的输入,还依赖于前一时刻的隐藏状态(记忆)。这让模型具备了“记忆能力”。




✅ 优点

  • 简单,结构直观,易于实现。
  • 能够处理变长输入的序列任务。

❌ 缺点

  • 梯度消失/爆炸:长序列训练时,梯度可能变得非常小或非常大,导致学习失败。
  • 长期依赖困难:模型很难保留远距离的信息,比如一个句首的名词对句尾动词的影响。
  • 序列必须逐步处理,无法并行,导致训练速度慢。

✅ 应用场景

  • 文本分类、语言模型、机器翻译、语音识别、对话系统、推荐系统等。
    ####案例代码:文本生成
import streamlit as st
import graphviz
import torch
import torch.nn as nn
import torch.optim as optim
from collections import Counter
import matplotlib.pyplot as plt
import re

# ========== 文本清洗 ==========
# 清洗文本,只保留中文、标点和空格
# 输入:原始文本字符串
# 输出:清洗后的文本字符串
def clean_text(text):
    text = re.sub(r'[^\u4e00-\u9fa5。,!?\s]', '', text)  # 保留中文、标点和空格
    text = re.sub(r'\s+', ' ', text)
    return text.strip()

# ========== Tokenizer 和词表 ==========
# 分词函数,将文本按空格分割为词列表
def tokenize(text):
    return text.split()

# 构建词表,统计词频,限制最大词表大小
def build_vocab(tokens, max_vocab_size=5000):
    counter = Counter(tokens)
    most_common = counter.most_common(max_vocab_size - 2)
    idx2word = ['<PAD>', '<UNK>'] + [word for word, _ in most_common]
    word2idx = {word: idx for idx, word in enumerate(idx2word)}
    return word2idx, idx2word

# ========== 模型定义 ==========
# 定义词级RNN模型
class WordRNN(nn.Module):
    def __init__(self, vocab_size, embed_size=128, hidden_size=256):
        super().__init__()
        self.embed = nn.Embedding(vocab_size, embed_size)  # 词嵌入层
        self.rnn = nn.RNN(embed_size, hidden_size, batch_first=True)  # RNN层
        self.fc = nn.Linear(hidden_size, vocab_size)  # 输出层

    def forward(self, x, h=None):
        x = self.embed(x)
        out, h = self.rnn(x, h)
        out = self.fc(out)
        return out, h

# ========== 文本生成 ==========
# 根据起始词生成指定长度的文本
def generate_text(model, word2idx, idx2word, start_word, length=30):
    model.eval()  # 设置为评估模式
    idx = torch.tensor([[word2idx.get(start_word, word2idx['<UNK>'])]])
    result = [start_word]
    hidden = None
    for _ in range(length):
        out, hidden = model(idx, hidden)
        prob = torch.softmax(out[0, -1], dim=0)  # 取最后一个时间步的输出并softmax
        idx = torch.multinomial(prob, 1).unsqueeze(0)  # 按概率采样下一个词
        word = idx2word[idx.item()]
        result.append(word)
    return ''.join(result)

# ========== Streamlit UI ==========
# 设置Streamlit页面配置
st.set_page_config(page_title="词级中文文本生成器", layout="centered")
st.title("🧠 中文词级文本生成器(鲁迅风格)")
# ========== 数据流向过程图 ==========
st.subheader("📊 数据流向过程图")
flow_chart = graphviz.Digraph(format="png")
flow_chart.attr(rankdir="LR")  # 从左到右

flow_chart.node("A", "上传鲁迅小说文本")
flow_chart.node("B", "文本清洗")
flow_chart.node("C", "分词")
flow_chart.node("D", "构建词表")
flow_chart.node("E", "构建训练数据")
flow_chart.node("F", "训练RNN模型")
flow_chart.node("G", "生成文本")

flow_chart.edges([
    ("A", "B"),
    ("B", "C"),
    ("C", "D"),
    ("D", "E"),
    ("E", "F"),
    ("F", "G"),
])
st.graphviz_chart(flow_chart)
st.markdown("📂 上传鲁迅小说 `.txt` 文件,自动训练词级 RNN 模型,并生成相似风格的句子。")

# 文件上传控件
uploaded_file = st.file_uploader("📁 上传文本文件", type="txt")
# 起始词输入框
start_word = st.text_input("🌱 起始词", value="我")
# 生成长度滑块
length = st.slider("📏 生成长度(词数)", 10, 100, 30)

if uploaded_file:
    # 读取并清洗文本
    raw_text = uploaded_file.read().decode("utf-8")
    cleaned_text = clean_text(raw_text)
    tokens = tokenize(cleaned_text)

    # 限制最大词数,加速预览
    max_tokens = 1000
    tokens = tokens[:max_tokens]

    # 构建词表
    word2idx, idx2word = build_vocab(tokens)
    vocab_size = len(word2idx)

    # 构建训练数据(输入序列和目标序列)
    seq_input = [word2idx.get(w, word2idx['<UNK>']) for w in tokens[:-1]]
    seq_target = [word2idx.get(w, word2idx['<UNK>']) for w in tokens[1:]]
    input_tensor = torch.tensor(seq_input).unsqueeze(0)
    target_tensor = torch.tensor(seq_target).unsqueeze(0)

    # 初始化模型、损失函数和优化器
    model = WordRNN(vocab_size)
    loss_fn = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=0.01)

    # 展示训练过程
    st.subheader("📈 模型训练过程")
    loss_list = []
    progress_bar = st.progress(0)
    loss_chart = st.empty()

    num_epochs = 50  # 训练轮数

    for epoch in range(num_epochs):
        model.train()
        optimizer.zero_grad()
        output, _ = model(input_tensor)
        loss = loss_fn(output.view(-1, vocab_size), target_tensor.view(-1))
        loss.backward()
        optimizer.step()

        loss_list.append(loss.item())
        progress_bar.progress((epoch + 1) / num_epochs)

        # 每轮画一次loss曲线
        if epoch % 1 == 0:
            fig, ax = plt.subplots()
            ax.plot(loss_list, label='Loss')
            ax.set_xlabel("Epoch")
            ax.set_ylabel("Loss")
            ax.set_title("Training loss curve")
            ax.legend()
            loss_chart.pyplot(fig)

    st.success("✅ 模型训练完成!")

    # 生成文本按钮
    if st.button("🚀 开始生成"):
        result = generate_text(model, word2idx, idx2word, start_word, length)
        st.subheader("📝 生成结果:")
        st.write(result)



二、LSTM:为记忆加上“门”的改良版 RNN

✅ 概念

LSTM 是为了解决 RNN 长期依赖问题而提出的。它通过门控机制(门=控制信息流)来保留有用信息,遗忘无用信息,从而实现长期记忆的存储与更新。



✅ 优点

  • 解决了 RNN 的长期依赖问题。
  • 更容易捕捉复杂的序列结构。

❌ 缺点

  • 结构复杂,计算量大,训练较慢。
  • 同样存在串行依赖问题:必须一个时刻一个时刻地处理,无法并行。

✅ 应用场景

  • 文本分类、语言模型、机器翻译、语音识别、对话系统、推荐系统等。
    ####案例代码:文本生成
import streamlit as st
import graphviz
import torch
import torch.nn as nn
import torch.optim as optim
from collections import Counter
import matplotlib.pyplot as plt
import re
import os
import tempfile

# ========== 文本清洗 ==========
# 清洗文本:保留中文、标点、空格
# 该函数用于清洗输入文本,只保留中文字符、常用标点和空格

def clean_text(text):
    text = re.sub(r'[^00-\u9fa5。,!?\s]', '', text)
    text = re.sub(r'\s+', ' ', text)
    return text.strip()

# ========== Tokenizer 和词表 ==========
# 分词
# 该函数将文本按空格分割为词

def tokenize(text):
    return text.split()

# 构建词表
# 统计词频,生成词到索引和索引到词的映射

def build_vocab(tokens, max_vocab_size=5000):
    counter = Counter(tokens)
    most_common = counter.most_common(max_vocab_size - 2)
    idx2word = ['<PAD>', '<UNK>'] + [word for word, _ in most_common]
    word2idx = {word: idx for idx, word in enumerate(idx2word)}
    return word2idx, idx2word

# ========== 模型定义 ==========
# 构建词级模型,支持 RNN / LSTM / GRU
# WordRNN 类用于定义词级循环神经网络模型
class WordRNN(nn.Module):
    def __init__(self, vocab_size, embed_size=128, hidden_size=256, rnn_type='RNN'):
        super().__init__()
        self.embed = nn.Embedding(vocab_size, embed_size)  # 词嵌入层
        if rnn_type == 'LSTM':
            self.rnn = nn.LSTM(embed_size, hidden_size, batch_first=True)  # LSTM
        elif rnn_type == 'GRU':
            self.rnn = nn.GRU(embed_size, hidden_size, batch_first=True)   # GRU
        else:
            self.rnn = nn.RNN(embed_size, hidden_size, batch_first=True)   # RNN
        self.rnn_type = rnn_type
        self.fc = nn.Linear(hidden_size, vocab_size)  # 输出层

    def forward(self, x, h=None):
        x = self.embed(x)
        out, h = self.rnn(x, h)
        out = self.fc(out)
        return out, h

# ========== 文本生成 ==========
# 用训练好的模型生成文本
# 该函数根据起始词和长度,利用模型生成文本

def generate_text(model, word2idx, idx2word, start_word, length=30, temperature=1.0):
    model.eval()
    idx = torch.tensor([[word2idx.get(start_word, word2idx['<UNK>'])]])
    result = [start_word]
    hidden = None
    for _ in range(length):
        out, hidden = model(idx, hidden)
        logits = out[0, -1] / temperature
        prob = torch.softmax(logits, dim=0)
        idx = torch.multinomial(prob, 1).unsqueeze(0)
        word = idx2word[idx.item()]
        result.append(word)
    return ''.join(result)

# ========== Streamlit UI ==========
# Streamlit 页面设置和流程图展示
st.set_page_config(page_title="词级中文文本生成器", layout="centered")
st.title("\U0001F9E0 中文词级文本生成器(鲁迅风格)")

st.subheader("\U0001F4CA 数据流向过程图")
flow_chart = graphviz.Digraph(format="png")
flow_chart.attr(rankdir="LR")
flow_chart.node("A", "上传鲁迅小说文本")
flow_chart.node("B", "文本清洗")
flow_chart.node("C", "分词")
flow_chart.node("D", "构建词表")
flow_chart.node("E", "构建训练数据")
flow_chart.node("F", "训练***模型")
flow_chart.node("G", "生成文本")
flow_chart.edges([("A", "B"), ("B", "C"), ("C", "D"), ("D", "E"), ("E", "F"), ("F", "G")])
st.graphviz_chart(flow_chart)
st.markdown("\U0001F4C2 上传鲁迅小说 `.txt` 文件,自动训练词级 *** 模型,并生成相似风格的句子。")

# 多文件上传
# 支持上传多个文本文件
uploaded_files = st.file_uploader("\U0001F4C1 上传文本文件(可多选)", type="txt", accept_multiple_files=True)

# 生成文本的起始词和长度
start_word = st.text_input("\U0001F331 起始词", value="我")
length = st.slider("\U0001F4CF 生成长度(词数)", 10, 100, 30)

# ========== 超参数设置 ==========
# 侧边栏设置模型超参数
st.sidebar.header("\U0001F527 模型设置")
rnn_type = st.sidebar.selectbox("选择模型类型", ["RNN", "LSTM", "GRU"], index=0)
embed_size = st.sidebar.slider("词嵌入维度", 64, 512, 128, step=32)
hidden_size = st.sidebar.slider("隐藏层大小", 64, 512, 256, step=32)
num_epochs = st.sidebar.slider("训练轮数", 10, 200, 50, step=10)
learning_rate = st.sidebar.select_slider("学习率", options=[0.001, 0.005, 0.01, 0.02, 0.05], value=0.01)

# 保存路径根据模型类型区分
model_save_path = f"luxun_{rnn_type.lower()}_model.pt"

if uploaded_files:
    # 拼接多个文本文件
    raw_text = ''.join([file.read().decode("utf-8") for file in uploaded_files])
    cleaned_text = clean_text(raw_text)  # 文本清洗
    tokens = tokenize(cleaned_text)      # 分词
    tokens = tokens[:1000]  # 限制长度,防止内存溢出

    word2idx, idx2word = build_vocab(tokens)  # 构建词表
    vocab_size = len(word2idx)

    # 构建训练输入输出序列
    seq_input = [word2idx.get(w, word2idx['<UNK>']) for w in tokens[:-1]]
    seq_target = [word2idx.get(w, word2idx['<UNK>']) for w in tokens[1:]]
    input_tensor = torch.tensor(seq_input).unsqueeze(0)
    target_tensor = torch.tensor(seq_target).unsqueeze(0)

    # 初始化模型
    model = WordRNN(vocab_size, embed_size, hidden_size, rnn_type)
    optimizer = optim.Adam(model.parameters(), lr=learning_rate)
    loss_fn = nn.CrossEntropyLoss()

    # 加载已有模型(断点训练),需判断结构是否匹配
    if os.path.exists(model_save_path):
        try:
            model.load_state_dict(torch.load(model_save_path))
            st.info("已加载已保存的模型参数,继续训练...")
        except RuntimeError as e:
            st.warning(f"⚠️ 模型结构变更,未加载已有模型参数。\n{str(e).splitlines()[0]}")

    st.subheader("\U0001F4C8 模型训练过程")
    loss_list = []
    progress_bar = st.progress(0)
    loss_chart = st.empty()

    # 训练主循环
    for epoch in range(num_epochs):
        model.train()
        optimizer.zero_grad()
        output, _ = model(input_tensor)
        loss = loss_fn(output.view(-1, vocab_size), target_tensor.view(-1))
        loss.backward()
        optimizer.step()

        loss_list.append(loss.item())
        progress_bar.progress((epoch + 1) / num_epochs)

        # 实时绘制损失曲线
        fig, ax = plt.subplots()
        ax.plot(loss_list, label='Loss')
        ax.set_xlabel("Epoch")
        ax.set_ylabel("Loss")
        ax.set_title("Training loss curve")
        ax.legend()
        loss_chart.pyplot(fig)

    # 保存模型参数
    torch.save(model.state_dict(), model_save_path)
    st.success("✅ 模型训练完成,已保存到本地!")

    # 生成文本并提供下载
    if st.button("\U0001F680 开始生成"):
        result = generate_text(model, word2idx, idx2word, start_word, length)
        st.subheader("\U0001F4DD 生成结果:")
        st.write(result)

        # 保存生成结果为临时文件供下载
        with tempfile.NamedTemporaryFile(delete=False, suffix=".txt", mode="w", encoding="utf-8") as tmpfile:
            tmpfile.write(result)
            tmpfile_path = tmpfile.name

        with open(tmpfile_path, "rb") as f:
            st.download_button("⬇️ 下载生成文本", f, file_name="generated_text.txt", mime="text/plain")




三、Self-Attention:打破顺序限制的序列建模方式

✅ 概念

在一个序列中,每个词作为 Query 同时也是 Key 和 Value。
用于捕捉句子内部不同位置之间的关系。
举个例子:对于句子 “The cat sat on the mat”,每个词都可以看到整个句子,包括它自己。

Self-Attention 是一种机制,它允许模型在处理每个词或时间步时关注序列中的其他所有位置。它完全摒弃了“隐藏状态”的串行传递方式,而是通过注意力权重计算全局信息。

它是 Transformer 模型的核心模块,也是当前 NLP(如 GPT、BERT)主流架构的基础。

✅ 优点

  • 全局感知能力强,可以建模任意位置间的依赖关系。
  • 支持并行计算,极大提升训练效率。
  • 在大型语料预训练(如 GPT/BERT)中表现出色。

❌ 缺点

  • 对长序列的计算复杂度高( O ( n 2 ) O(n^2) O(n2)),会占用大量内存。
  • 缺乏天然的位置信息(需手动加 positional encoding)。

####案例代码

import streamlit as st
import torch
import torch.nn as nn
import torch.nn.functional as F
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
from transformers import BertTokenizer, BertModel

# ----------------------------
# Self-Attention Layer
# ----------------------------
class SelfAttention(nn.Module):
    def __init__(self, embed_dim):
        super(SelfAttention, self).__init__()
        self.query = nn.Linear(embed_dim, embed_dim)
        self.key   = nn.Linear(embed_dim, embed_dim)
        self.value = nn.Linear(embed_dim, embed_dim)
        self.scale = torch.sqrt(torch.tensor(embed_dim, dtype=torch.float32))

    def forward(self, x):
        Q = self.query(x)
        K = self.key(x)
        V = self.value(x)

        attn_scores = torch.bmm(Q, K.transpose(1, 2)) / self.scale
        attn_weights = F.softmax(attn_scores, dim=-1)
        out = torch.bmm(attn_weights, V)
        return out, attn_weights, Q, K, V

# ----------------------------
# Streamlit UI
# ----------------------------
st.set_page_config(page_title="Self-Attention 可视化")
st.title("🧠 Self-Attention 中文可视化 Demo")

# 用户输入句子
sentence = st.text_input("✏️ 输入一个句子(支持中英文混合):", "A cat relishes fish.")

# 加载中文/英文 BERT 模型
#Google 发布的中文版本的 BERT(Bidirectional Encoder Representations from Transformers)模型,
#它是一个预训练的通用语言理解模型
#BERT模型作用:
#  1. 生成上下文相关的中文词向量(Embedding:与传统词向量(如 Word2Vec)不同,BERT 根据上下文动态生成向量
 # 2、作为下游任务的通用编码器:你可以把 bert-base-chinese 当成一个“理解中文句子”的模块;
  #3、适配中文文本结构 与英文不同,中文没有空格分词。bert-base-chinese:使用字级别(Character-level)分词(WordPiece)

# 专门为中文来设计的
# model_code='bert-base-chinese'
# 混合语言来设计
# model_code='bert-base-multilingual-cased'
# 专门为英文设计
#model_code = 'bert-base-uncased'
@st.cache_resource
def load_model():
    model_code = 'bert-base-uncased'
    tokenizer = BertTokenizer.from_pretrained(model_code)
    model = BertModel.from_pretrained(model_code)
    return tokenizer, model

tokenizer, bert = load_model()

# 编码输入
inputs = tokenizer(sentence, return_tensors="pt")
with torch.no_grad():
    outputs = bert(**inputs)
    embeddings = outputs.last_hidden_state  # (1, seq_len, hidden_dim)

# 去掉 batch 维度
x = embeddings

# 实例化 Self-Attention
embed_dim = x.shape[-1]
attn_layer = SelfAttention(embed_dim)
output, weights, Q, K, V = attn_layer(x)

# 可视化
tokens = tokenizer.convert_ids_to_tokens(inputs["input_ids"][0])
weights_matrix = weights[0].detach().numpy()

st.subheader("🎯 自注意力权重矩阵")
fig, ax = plt.subplots(figsize=(10, 6))
sns.heatmap(weights_matrix, xticklabels=tokens, yticklabels=tokens, cmap="YlOrBr", annot=True, fmt=".2f", ax=ax)
ax.set_xlabel("Key(followed)")
ax.set_ylabel("Query(viewed)")
st.pyplot(fig)

st.subheader("🎯 自注意力权重矩阵表格(部分示意)")
# 取前 N 个 token 可读性更好(可调)
N = min(10, len(tokens))
partial_weights = weights_matrix[:N, :N]
partial_tokens = tokens[:N]

# 构建 DataFrame
df_weights = pd.DataFrame(partial_weights, index=partial_tokens, columns=partial_tokens)
df_weights.index.name = "Query\\Key"

st.dataframe(df_weights.style.format(precision=2))

#####在 BERT 模型中,[CLS][SEP] 是两个特殊的标记(token),用于帮助模型理解输入结构和任务目标:


🟩 [CLS]分类标记(Classification)
  • 含义:Classification 的缩写。

  • 位置:始终出现在输入的最开头。

  • 作用

    • 它的输出向量(embedding)被用作整个句子的“全局语义表示”。
    • 主要用于句子分类任务,如情感分析、句子对关系判断(如是否是问答对)等。
    • 在 BERT 输出的张量中,可以通过 outputs.last_hidden_state[:, 0, :] 取出 [CLS] 的向量。

🟨 [SEP]分隔标记(Separator)
  • 含义:Separator 的缩写。

  • 作用

    • 在句子之间起“分隔”作用。
    • 用于区分 两个句子的边界,如句子对输入(句子1 + [SEP] + 句子2)。
  • 位置

    • 如果是单句:[CLS] + 句子 + [SEP]
    • 如果是句子对:[CLS] + 句子A + [SEP] + 句子B + [SEP]


🧠 总结对比

Token全称用于在 input_ids 中的位置
[CLS]Classification句子整体的语义表示最前面
[SEP]Separator句子/段落分隔单句结尾 / 句子对之间与结尾


四、演进趋势的背后动因

阶段代表模型优点遇到的瓶颈
RNNElman RNN序列建模记忆能力弱,难学长期依赖
LSTMLSTM / GRU引入门机制,缓解梯度问题仍然串行,训练效率低
Self-AttentionTransformer并行计算,全局感知,预训练强对长文本成本高

演进趋势源于两个根本问题:

  1. 模型对长期依赖的建模能力不足 → LSTM 出现。
  2. 模型训练效率受串行计算限制 → Self-Attention 出现。

随着算力和数据的增长,Self-Attention 在规模化预训练中大显身手,最终逐步取代 RNN/LSTM 成为主流。


五、总结:从线性记忆到全局关注的进化

模型记忆方式并行能力长距离建模能力主流应用
RNN隐藏状态简单序列建模
LSTM门控记忆较强机器翻译、语音识别
Self-Attention全局注意力GPT/BERT 等大型语言模型

从 RNN 到 LSTM,再到 Self-Attention,是深度学习对信息依赖建模能力不断提升、计算效率不断优化的必然路径。掌握这一演进过程,不仅有助于理解 NLP 模型的本质,也为未来模型设计提供了重要启发。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值