1. Transformer架构运行机制

Transformer架构是一种强大的神经网络结构,主要用于自然语言处理任务。它摒弃了传统的循环神经网络(RNN)和卷积神经网络(CNN),完全基于注意力机制构建。根据图示,我们可以看到Transformer由以下主要部分组成:
#####简单结构拆分图

#####完整的详细结构

编码器部分(左侧)

编码器由N个相同层堆叠而成,每个层包含:

  1. Multi-Head Attention:处理输入序列中各位置之间的关系
  2. Add & Norm:残差连接和层归一化
  3. Feed Forward:对每个位置独立应用的前馈神经网络
  4. Add & Norm:另一个残差连接和层归一化

解码器部分(右侧)

解码器同样由N个相同层堆叠,每个层包含:

  1. Masked Multi-Head Attention:防止当前位置看到未来位置的信息
  2. Add & Norm:残差连接和层归一化
  3. Multi-Head Attention:关注编码器输出
  4. Add & Norm:残差连接和层归一化
  5. Feed Forward:前馈神经网络
  6. Add & Norm:最后的残差连接和层归一化

数据流程

  1. 输入序列首先通过Input Embedding转换为向量表示
  2. 加入Positional Encoding以保留序列中的位置信息
  3. 向量流经编码器的N个层
  4. 解码器接收移位的输出序列的Output EmbeddingPositional Encoding
  5. 解码器处理编码器的输出并生成预测
  6. 最后通过Linear层和Softmax转换为概率分布

2. 关键术语中英文对照

英文术语 中文翻译 解释
Input Embedding 输入嵌入 将输入词或标记转换为固定维度的向量表示
Output Embedding 输出嵌入 将目标序列中的词或标记转换为向量表示
Positional Encoding 位置编码 为每个位置添加信息,因为Transformer本身没有位置概念
Multi-Head Attention 多头注意力 允许模型同时关注不同位置的信息,从不同的表示子空间获取信息
Masked Multi-Head Attention 掩码多头注意力 防止解码时看到未来的信息,只关注已经生成的输出
Add & Norm 添加与归一化 残差连接+层归一化,帮助训练深层网络
Feed Forward 前馈网络 对每个位置独立应用的全连接前馈网络
Linear 线性层 线性变换,调整维度以便输出预测
Softmax 软最大化函数 将输出转换为概率分布
Nx N倍重复 表示编码器和解码器中的层数重复N次

3. Transformer的工作流程

  1. 输入处理

    • 将文本序列转换为词嵌入向量
    • 添加位置编码以保留顺序信息
  2. 编码器处理

    • 通过自注意力机制捕获输入序列中的依赖关系
    • 每个词都能"关注"序列中的所有其他词
    • 残差连接和层归一化帮助信息流动和训练稳定性
    • 前馈网络处理每个位置的表示
  3. 解码器处理

    • 接收已经生成的输出序列(右移一位)
    • 掩码自注意力确保预测只依赖已生成的输出
    • 编码器-解码器注意力使解码器关注输入序列的相关部分
    • 通过前馈网络进一步处理信息
  4. 输出生成

    • 线性变换调整维度
    • 通过Softmax函数生成下一个词的概率分布

4. 实际应用案例

案例1:机器翻译

假设我们要将英文翻译成中文:

输入

"The cat sits on the mat."

处理流程

  1. 输入嵌入:将每个英文单词转换为向量
  2. 添加位置编码:标记每个词的位置
  3. 编码器处理:捕获句子中的上下文关系(比如"sits"与"cat"的关系)
  4. 解码器处理:
    • 开始标记:“”
    • 生成第一个中文词:“猫”,基于编码器输出和当前生成的序列
    • 生成第二个中文词:“坐”,依此类推
  5. 最终输出:“猫坐在垫子上。”

案例2:文本摘要

输入

"研究人员发现,每日锻炼30分钟可以显著降低心脏病风险。这项研究跟踪了5000名参与者长达10年,结果表明定期运动不仅有益心脑血管健康,还能改善整体生活质量。"

处理流程

  1. 输入嵌入和位置编码
  2. 编码器处理完整文章,识别关键信息和主题
  3. 解码器生成摘要:
    • 关注输入文章中的重要部分
    • 逐词生成摘要
  4. 输出摘要:“研究表明每日30分钟锻炼可降低心脏病风险并改善生活质量。”

案例代码

transformer_translation_example.py

import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
from torch.utils.data import Dataset, DataLoader

# 简化版Transformer用于机器翻译示例
class SimpleTransformer(nn.Module):
    def __init__(self, src_vocab_size, tgt_vocab_size, d_model=256, nhead=8, num_encoder_layers=3, num_decoder_layers=3):
        super(SimpleTransformer, self).__init__()
        
        # 嵌入层
        self.src_embedding = nn.Embedding(src_vocab_size, d_model)
        self.tgt_embedding = nn.Embedding(tgt_vocab_size, d_model)
        self.positional_encoding = nn.Parameter(self.create_positional_encoding(100, d_model), requires_grad=False)
        
        # Transformer层
        self.transformer = nn.Transformer(
            d_model=d_model,
            nhead=nhead,
            num_encoder_layers=num_encoder_layers,
            num_decoder_layers=num_decoder_layers
        )
        
        # 输出层
        self.output_layer = nn.Linear(d_model, tgt_vocab_size)
        
    def create_positional_encoding(self, max_seq_len, d_model):
        pos_enc = torch.zeros(max_seq_len, d_model)
        positions = torch.arange(0, max_seq_len, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-np.log(10000.0) / d_model))
        pos_enc[:, 0::2] = torch.sin(positions * div_term)
        pos_enc[:, 1::2] = torch.cos(positions * div_term)
        pos_enc = pos_enc.unsqueeze(0)
        return pos_enc
        
    def create_padding_mask(self, seq):
        return (seq == 0).transpose(0, 1)
    
    def forward(self, src, tgt):
        # 源序列和目标序列
        src = src.transpose(0, 1)  # [seq_len, batch_size]
        tgt = tgt.transpose(0, 1)  # [seq_len, batch_size]
        
        # 创建掩码
        src_padding_mask = self.create_padding_mask(src)
        tgt_padding_mask = self.create_padding_mask(tgt)
        
        # PyTorch transformer要求tgt_mask以防止看到未来信息
        tgt_seq_len = tgt.size(0)
        tgt_mask = torch.triu(torch.ones(tgt_seq_len, tgt_seq_len), diagonal=1).bool()
        
        # 嵌入
        src_embedded = self.src_embedding(src) * np.sqrt(self.src_embedding.embedding_dim)
        tgt_embedded = self.tgt_embedding(tgt) * np.sqrt(self.tgt_embedding.embedding_dim)
        
        # 添加位置编码
        src_embedded = src_embedded + self.positional_encoding[:, :src_embedded.size(1)]
        tgt_embedded = tgt_embedded + self.positional_encoding[:, :tgt_embedded.size(1)]
        
        # Transformer前向传播
        transformer_output = self.transformer(
            src_embedded, tgt_embedded,
            tgt_mask=tgt_mask,
            src_key_padding_mask=src_padding_mask,
            tgt_key_padding_mask=tgt_padding_mask
        )
        
        # 输出层
        output = self.output_layer(transformer_output)
        return output.transpose(0, 1)  # [batch_size, seq_len, vocab_size]

# 简单的翻译数据集
class TranslationDataset(Dataset):
    def __init__(self, src_sentences, tgt_sentences, src_vocab, tgt_vocab, max_len=50):
        self.src_sentences = src_sentences
        self.tgt_sentences = tgt_sentences
        self.src_vocab = src_vocab
        self.tgt_vocab = tgt_vocab
        self.max_len = max_len
        
    def __len__(self):
        return len(self.src_sentences)
    
    def __getitem__(self, idx):
        src_tokens = self.tokenize(self.src_sentences[idx], self.src_vocab)
        tgt_tokens = self.tokenize(self.tgt_sentences[idx], self.tgt_vocab)
        
        # 添加起始和结束标记到目标序列
        tgt_input = [self.tgt_vocab['<START>']] + tgt_tokens
        tgt_output = tgt_tokens + [self.tgt_vocab['<END>']]
        
        # 填充到相同长度
        src_padded = self.pad_sequence(src_tokens, self.max_len)
        tgt_input_padded = self.pad_sequence(tgt_input, self.max_len)
        tgt_output_padded = self.pad_sequence(tgt_output, self.max_len)
        
        return {
            'src': torch.tensor(src_padded),
            'tgt_input': torch.tensor(tgt_input_padded),
            'tgt_output': torch.tensor(tgt_output_padded)
        }
    
    def tokenize(self, sentence, vocab):
        # 简单的按空格分词,实际应用中会使用更复杂的分词器
        return [vocab.get(word, vocab['<UNK>']) for word in sentence.split()]
    
    def pad_sequence(self, seq, max_len):
        # 填充序列到最大长度
        return seq + [0] * (max_len - len(seq)) if len(seq) < max_len else seq[:max_len]

# 简单的训练函数
def train_model(model, dataloader, epochs=10):
    criterion = nn.CrossEntropyLoss(ignore_index=0)
    optimizer = optim.Adam(model.parameters(), lr=0.0001)
    
    model.train()
    for epoch in range(epochs):
        total_loss = 0
        for batch in dataloader:
            src = batch['src']
            tgt_input = batch['tgt_input']
            tgt_output = batch['tgt_output']
            
            optimizer.zero_grad()
            
            # 前向传播
            output = model(src, tgt_input)
            
            # 重塑输出和目标以适应损失函数
            output = output.reshape(-1, output.size(-1))
            tgt_output = tgt_output.reshape(-1)
            
            # 计算损失
            loss = criterion(output, tgt_output)
            total_loss += loss.item()
            
            # 反向传播
            loss.backward()
            optimizer.step()
            
        print(f"Epoch {epoch+1}, Loss: {total_loss/len(dataloader)}")

# 翻译函数
def translate(model, sentence, src_vocab, tgt_vocab, max_len=50):
    # 反向词汇表映射
    idx_to_tgt = {v: k for k, v in tgt_vocab.items()}
    
    # 预处理输入句子
    tokens = [src_vocab.get(word, src_vocab['<UNK>']) for word in sentence.split()]
    tokens = tokens + [0] * (max_len - len(tokens)) if len(tokens) < max_len else tokens[:max_len]
    src_tensor = torch.tensor([tokens])
    
    # 开始解码
    model.eval()
    with torch.no_grad():
        # 初始化目标序列
        tgt_input = torch.tensor([[tgt_vocab['<START>']]])
        
        # 逐个生成目标词
        for _ in range(max_len):
            output = model(src_tensor, tgt_input)
            next_word_idx = output[0, -1].argmax().item()
            
            # 如果生成了结束标记,停止生成
            if next_word_idx == tgt_vocab['<END>']:
                break
                
            # 添加预测的词到输入序列
            next_word_tensor = torch.tensor([[next_word_idx]])
            tgt_input = torch.cat([tgt_input, next_word_tensor], dim=1)
        
        # 将索引转换回单词
        generated_words = []
        for idx in tgt_input[0][1:]:  # 忽略<START>标记
            generated_words.append(idx_to_tgt.get(idx.item(), '<UNK>'))
    
    return ' '.join(generated_words)

# 示例使用
def translation_example():
    # 简单的英中翻译示例
    # 创建词汇表
    english_vocab = {
        '<PAD>': 0, '<UNK>': 1, '<START>': 2, '<END>': 3,
        'the': 4, 'cat': 5, 'sits': 6, 'on': 7, 'mat': 8, 'dog': 9, 'runs': 10,
        'in': 11, 'park': 12, 'i': 13, 'love': 14, 'programming': 15
    }
    
    chinese_vocab = {
        '<PAD>': 0, '<UNK>': 1, '<START>': 2, '<END>': 3,
        '猫': 4, '坐': 5, '在': 6, '垫子': 7, '上': 8, '狗': 9, '跑': 10,
        '公园': 11, '里': 12, '我': 13, '喜欢': 14, '编程': 15
    }
    
    # 示例训练数据
    english_sentences = [
        'the cat sits on the mat',
        'the dog runs in the park',
        'i love programming'
    ]
    
    chinese_sentences = [
        '猫 坐 在 垫子 上',
        '狗 在 公园 里 跑',
        '我 喜欢 编程'
    ]
    
    # 创建数据集和数据加载器
    dataset = TranslationDataset(english_sentences, chinese_sentences, english_vocab, chinese_vocab)
    dataloader = DataLoader(dataset, batch_size=1, shuffle=True)
    
    # 创建模型
    model = SimpleTransformer(
        src_vocab_size=len(english_vocab),
        tgt_vocab_size=len(chinese_vocab)
    )
    
    # 训练模型
    train_model(model, dataloader, epochs=100)
    
    # 测试翻译
    test_sentence = 'the cat sits on the mat'
    translation = translate(model, test_sentence, english_vocab, chinese_vocab)
    print(f"英文: '{test_sentence}'")
    print(f"翻译: '{translation}'")

if __name__ == "__main__":
    translation_example()

streamlit_transformer_demo.py

import streamlit as st
import torch
import numpy as np
import time
import matplotlib.pyplot as plt
import seaborn as sns
from transformer_translation_example import SimpleTransformer

# 设置页面配置
st.set_page_config(
    page_title="Transformer可视化演示",
    layout="wide",
    initial_sidebar_state="expanded"
)

# 标题和介绍
st.title("Transformer模型可视化演示")
st.markdown("""
这个应用演示了Transformer模型的工作原理,特别是在机器翻译任务上的应用。
您可以输入英文句子,查看模型如何逐步将其翻译成中文,同时可视化注意力机制。
""")

# 创建词汇表
@st.cache_resource
def get_vocabs():
    english_vocab = {
        '<PAD>': 0, '<UNK>': 1, '<START>': 2, '<END>': 3,
        'the': 4, 'cat': 5, 'sits': 6, 'on': 7, 'mat': 8, 'dog': 9, 'runs': 10,
        'in': 11, 'park': 12, 'i': 13, 'love': 14, 'programming': 15, 'a': 16,
        'is': 17, 'beautiful': 18, 'day': 19, 'today': 20, 'computer': 21, 'science': 22
    }
    
    chinese_vocab = {
        '<PAD>': 0, '<UNK>': 1, '<START>': 2, '<END>': 3,
        '猫': 4, '坐': 5, '在': 6, '垫子': 7, '上': 8, '狗': 9, '跑': 10,
        '公园': 11, '里': 12, '我': 13, '喜欢': 14, '编程': 15, '一个': 16,
        '是': 17, '美丽的': 18, '天': 19, '今天': 20, '计算机': 21, '科学': 22
    }
    return english_vocab, chinese_vocab

english_vocab, chinese_vocab = get_vocabs()

# 创建反向词汇表映射
idx_to_en = {v: k for k, v in english_vocab.items()}
idx_to_cn = {v: k for k, v in chinese_vocab.items()}

# 创建和加载模型
@st.cache_resource
def load_model():
    model = SimpleTransformer(
        src_vocab_size=len(english_vocab),
        tgt_vocab_size=len(chinese_vocab),
        d_model=256,
        nhead=8,
        num_encoder_layers=3,
        num_decoder_layers=3
    )
    # 在实际应用中,你可以加载预训练的模型权重
    # model.load_state_dict(torch.load('model_weights.pth'))
    return model

model = load_model()

# 修改SimpleTransformer以返回注意力权重
class VisualizableTransformer(SimpleTransformer):
    def forward(self, src, tgt, return_attention=False):
        src = src.transpose(0, 1)
        tgt = tgt.transpose(0, 1)
        
        src_padding_mask = self.create_padding_mask(src)
        tgt_padding_mask = self.create_padding_mask(tgt)
        
        tgt_seq_len = tgt.size(0)
        tgt_mask = torch.triu(torch.ones(tgt_seq_len, tgt_seq_len), diagonal=1).bool()
        
        src_embedded = self.src_embedding(src) * np.sqrt(self.src_embedding.embedding_dim)
        tgt_embedded = self.tgt_embedding(tgt) * np.sqrt(self.tgt_embedding.embedding_dim)
        
        src_embedded = src_embedded + self.positional_encoding[:, :src_embedded.size(1)]
        tgt_embedded = tgt_embedded + self.positional_encoding[:, :tgt_embedded.size(1)]
        
        # 在实际项目中,这里需要修改torch.nn.Transformer来返回注意力权重
        # 为了演示,我们将生成随机的注意力矩阵
        transformer_output = self.transformer(
            src_embedded, tgt_embedded,
            tgt_mask=tgt_mask,
            src_key_padding_mask=src_padding_mask,
            tgt_key_padding_mask=tgt_padding_mask
        )
        
        output = self.output_layer(transformer_output)
        
        if return_attention:
            # 为了演示,生成一个假的注意力矩阵
            # 在实际应用中,这应该从Transformer内部获取
            batch_size = src.size(1)
            src_len = src.size(0)
            tgt_len = tgt.size(0)
            attention = torch.zeros(batch_size, tgt_len, src_len)
            
            # 创建一个更合理的注意力矩阵,对角线上的值更高
            for i in range(tgt_len):
                attention_row = torch.zeros(src_len)
                # 让当前token更关注相应位置的输入token及其周围
                center = min(i, src_len-1)
                for j in range(src_len):
                    attention_row[j] = max(0, 1 - 0.2 * abs(j - center))
                attention[0, i] = attention_row / attention_row.sum()
            
            return output.transpose(0, 1), attention
        
        return output.transpose(0, 1)

# 创建可视化模型
@st.cache_resource
def load_viz_model():
    return VisualizableTransformer(
        src_vocab_size=len(english_vocab),
        tgt_vocab_size=len(chinese_vocab),
        d_model=256,
        nhead=8,
        num_encoder_layers=3,
        num_decoder_layers=3
    )

viz_model = load_viz_model()

# 预处理输入句子
def preprocess_sentence(sentence, vocab, max_len=50):
    tokens = [vocab.get(word.lower(), vocab['<UNK>']) for word in sentence.split()]
    tokens = tokens + [0] * (max_len - len(tokens)) if len(tokens) < max_len else tokens[:max_len]
    return torch.tensor([tokens])

# 执行翻译并生成注意力可视化
def translate_and_visualize(input_sentence, max_len=15):
    src_tokens = preprocess_sentence(input_sentence, english_vocab, max_len)
    
    # 初始化目标序列
    tgt_input = torch.tensor([[chinese_vocab['<START>']]])
    
    generated_tokens = []
    attention_matrices = []
    
    # 逐步生成翻译
    with torch.no_grad():
        for i in range(max_len):
            # 预测下一个词和注意力
            output, attention = viz_model(src_tokens, tgt_input, return_attention=True)
            next_word_idx = output[0, -1].argmax().item()
            
            # 保存结果
            generated_tokens.append(next_word_idx)
            attention_matrices.append(attention[0, -1].numpy())
            
            # 如果生成了结束标记,停止生成
            if next_word_idx == chinese_vocab['<END>']:
                break
                
            # 添加预测的词到输入序列
            next_word_tensor = torch.tensor([[next_word_idx]])
            tgt_input = torch.cat([tgt_input, next_word_tensor], dim=1)
    
    # 将索引转换回单词
    src_words = [idx_to_en.get(idx.item(), '<UNK>') for idx in src_tokens[0] if idx.item() != 0]
    tgt_words = [idx_to_cn.get(idx, '<UNK>') for idx in generated_tokens if idx != chinese_vocab['<END>']]
    
    return src_words, tgt_words, attention_matrices

# 创建注意力热图
def plot_attention(attention, src_words, tgt_words):
    fig, ax = plt.subplots(figsize=(10, 8))
    sns.heatmap(attention[:len(tgt_words), :len(src_words)], 
                xticklabels=src_words, 
                yticklabels=tgt_words,
                cmap='viridis', ax=ax)
    plt.xlabel('Source Tokens')
    plt.ylabel('Generated Tokens')
    plt.title('Attention Weights')
    plt.tight_layout()
    return fig

# 主页面
st.header("Transformer机器翻译演示")

# 输入区域
input_text = st.text_input("请输入英文句子:", 
                           value="the cat sits on the mat")

# 默认示例下拉菜单
examples = {
    "示例1": "the cat sits on the mat",
    "示例2": "the dog runs in the park",
    "示例3": "i love programming",
    "示例4": "today is a beautiful day"
}
selected_example = st.selectbox("或选择一个预设示例:", list(examples.keys()))

if selected_example:
    input_text = examples[selected_example]

# 翻译按钮
if st.button("翻译") or input_text:
    with st.spinner("正在翻译..."):
        # 为了演示效果,添加一个短暂的延迟
        time.sleep(0.5)
        
        # 执行翻译并获取注意力矩阵
        src_words, tgt_words, attention_matrices = translate_and_visualize(input_text)
        
        # 显示翻译结果
        col1, col2 = st.columns(2)
        with col1:
            st.subheader("输入 (英文):")
            st.write(" ".join(src_words))
        with col2:
            st.subheader("输出 (中文):")
            st.write(" ".join(tgt_words))
        
        # 显示注意力可视化
        st.subheader("注意力机制可视化")
        
        # 为每个生成的词创建一个注意力热图
        for i, attention in enumerate(attention_matrices):
            if i < len(tgt_words):
                st.write(f"生成词 '{tgt_words[i]}' 的注意力分布:")
                fig = plot_attention(attention.reshape(1, -1), src_words, [tgt_words[i]])
                st.pyplot(fig)
        
        # 完整注意力矩阵
        st.subheader("完整注意力热图")
        # 合并所有注意力矩阵
        complete_attention = np.vstack([att.reshape(1, -1) for att in attention_matrices[:len(tgt_words)]])
        fig = plot_attention(complete_attention, src_words, tgt_words)
        st.pyplot(fig)

# 添加解释性内容
with st.expander("什么是Transformer和注意力机制?"):
    st.markdown("""
    ## Transformer架构
    
    Transformer是一种基于自注意力机制的神经网络架构,由Google在2017年提出。它摒弃了传统的循环神经网络(RNN)结构,
    完全依赖于注意力机制来模拟序列中的依赖关系。
    
    ### 主要组成部分:
    
    1. **编码器-解码器结构**: 编码器处理输入序列,解码器生成输出序列
    2. **多头注意力机制**: 允许模型同时关注输入序列的不同部分
    3. **位置编码**: 提供序列中单词位置的信息
    4. **前馈神经网络**: 对每个位置进行独立处理
    5. **残差连接和层归一化**: 帮助训练更深的网络
    
    ### 注意力机制
    
    注意力机制是Transformer的核心,它允许模型计算输入序列中每个位置对其他位置的关注度。
    上面的热图展示了模型生成每个中文词时对输入英文句子各部分的关注程度。
    
    颜色越深表示关注度越高,这让我们能直观地看到模型如何"理解"输入句子并做出翻译决策。
    """)

with st.expander("如何理解可视化结果?"):
    st.markdown("""
    ## 解读注意力热图
    
    注意力热图展示了模型在生成每个中文词时,对输入英文句子中各部分的关注程度。
    
    ### 如何阅读热图:
    
    - **纵轴**: 代表生成的中文词
    - **横轴**: 代表输入的英文词
    - **颜色深浅**: 颜色越深,表示关注度越高
    
    ### 典型模式:
    
    1. **对角线模式**: 如果热点沿着对角线分布,说明模型主要关注输入序列中的对应位置
    2. **垂直条带**: 如果某个输入词有一条垂直的高注意力区域,说明这个词对多个输出词都很重要
    3. **水平条带**: 如果某个输出词有一条水平的高注意力区域,说明它依赖于多个输入词
    
    通过观察这些模式,我们可以理解模型如何"思考"以及它学习到的语言对应关系。
    """)

# 添加页脚
st.markdown("---")
st.markdown("Transformer架构可视化演示 | 基于PyTorch和Streamlit")

5. Transformer的优势

  1. 并行计算:不同于RNN的顺序处理,Transformer可以并行处理整个序列,大幅提高训练效率
  2. 长距离依赖:注意力机制可以直接连接任意两个位置,更好地捕获长距离依赖
  3. 丰富的表示:多头注意力从不同角度学习文本表示
  4. 灵活性:可应用于各种NLP任务,如翻译、摘要、问答等

6. 结论

Transformer架构以其强大的表现能力和计算效率,已成为现代NLP的基石。它是GPT、BERT、T5等先进模型的基础架构,推动了自然语言处理领域的飞速发展。理解Transformer的工作原理对于掌握现代NLP技术至关重要。

通过注意力机制替代传统的序列处理方法,Transformer实现了更好的并行性和对长距离依赖的建模,从而在各种语言任务上取得了突破性的进展。随着技术的不断发展,基于Transformer的模型将继续引领NLP领域的创新。

Logo

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

更多推荐