互联网大厂经典面试题:手撕Transformer

互联网大厂面试中,往往涉及手撕代码环节。Transformer作为现在大模型的基本架构,在学术界以及工业界都有很广泛的应用,因此成为了一个重要考点,本文着重介绍如何快速理解transformer以及通过python“手撕”实现(以演示为主,不能直接运行)。
1. 自注意力机制(Self-Attention)

自注意力机制的核心思想是:每个位置的输出由所有位置的信息加权得到。自注意力机制使得模型能够关注输入序列中不同位置之间的关系。

计算步骤

  1. 查询(Query)、键(Key)和值(Value):每个输入元素通过 线性变换 生成查询、键和值。

  2. 计算注意力分数:通过查询和键的点积来计算注意力分数,然后通过 Softmax 转化为权重。

  3. 加权求和:使用注意力权重加权值(Value),得到每个位置的输出。

公式:

\text{Attention}(Q, K, V) = \text{Softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right) V

以下是完整的代码实现,面试中通常只会涉及其中的一部分:

import torch
import torch.nn as nn
import torch.nn.functional as F

# 位置编码模块,用于为输入的词嵌入加上位置信息
class PositionalEncoding(nn.Module):
    def __init__(self, d_model, max_len=5000):
        super(PositionalEncoding, self).__init__()

        # 创建一个位置编码矩阵,形状为 (max_len, d_model)
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len).unsqueeze(1).float()  # 位置从 0 到 max_len-1
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * -(torch.log(torch.tensor(10000.0)) / d_model))

        # 偶数维度使用正弦函数,奇数维度使用余弦函数
        pe[:, 0::2] = torch.sin(position * div_term)  # 位置编码的偶数维度
        pe[:, 1::2] = torch.cos(position * div_term)  # 位置编码的奇数维度
        self.register_buffer('pe', pe)  # 将位置编码保存在 buffer 中,这样它不会被训练

    def forward(self, x):
        # 将位置编码加到输入 x 上,x 是输入的词嵌入
        return x + self.pe[:x.size(1)].detach()  # 不计算梯度(detach)

# 自注意力机制(Self-Attention)模块
class SelfAttention(nn.Module):
    def __init__(self, d_model, n_heads):
        super(SelfAttention, self).__init__()
        self.d_model = d_model
        self.n_heads = n_heads
        self.depth = d_model // n_heads  # 每个头的深度

        # 定义查询、键、值的线性变换层
        self.query = nn.Linear(d_model, d_model)
        self.key = nn.Linear(d_model, d_model)
        self.value = nn.Linear(d_model, d_model)

        # 输出的线性变换层
        self.out = nn.Linear(d_model, d_model)

    def split_heads(self, x):
        # 将输入张量 x 按照头数(n_heads)进行拆分,方便多头注意力计算
        batch_size = x.size(0)  # 获取批次大小
        x = x.view(batch_size, -1, self.n_heads, self.depth)  # (batch_size, seq_len, n_heads, depth)
        return x.permute(0, 2, 1, 3)  # 变换维度,使其变成 (batch_size, n_heads, seq_len, depth)

    def forward(self, x):
        # 将输入 x(形状为 batch_size x seq_len x d_model)分为多个头
        query = self.split_heads(self.query(x))  # (batch_size, n_heads, seq_len, depth)
        key = self.split_heads(self.key(x))      # (batch_size, n_heads, seq_len, depth)
        value = self.split_heads(self.value(x))  # (batch_size, n_heads, seq_len, depth)

        # 计算缩放点积注意力
        score = torch.matmul(query, key.transpose(-2, -1)) / self.depth**0.5  # (batch_size, n_heads, seq_len, seq_len)
        attention = F.softmax(score, dim=-1)  # 对 score 进行 softmax 归一化

        # 根据注意力权重加权值
        output = torch.matmul(attention, value)  # (batch_size, n_heads, seq_len, depth)
        output = output.permute(0, 2, 1, 3).contiguous().view(x.size(0), -1, self.d_model)  # 将多个头的输出拼接
        return self.out(output)  # 通过线性层得到最终输出

# 前馈神经网络模块,通常包含两个全连接层和 ReLU 激活函数
class FeedForward(nn.Module):
    def __init__(self, d_model):
        super(FeedForward, self).__init__()
        self.fc1 = nn.Linear(d_model, d_model * 4)  # 第一层,通常会扩大维度
        self.fc2 = nn.Linear(d_model * 4, d_model)  # 第二层,恢复到 d_model

    def forward(self, x):
        x = F.relu(self.fc1(x))  # 使用 ReLU 激活函数
        return self.fc2(x)  # 输出

# Transformer 编码器层,由自注意力机制和前馈神经网络组成
class EncoderLayer(nn.Module):
    def __init__(self, d_model, n_heads):
        super(EncoderLayer, self).__init__()
        self.self_attention = SelfAttention(d_model, n_heads)  # 自注意力机制
        self.ffn = FeedForward(d_model)  # 前馈神经网络
        self.norm1 = nn.LayerNorm(d_model)  # 层归一化
        self.norm2 = nn.LayerNorm(d_model)  # 层归一化

    def forward(self, x):
        # 自注意力计算
        attn_out = self.self_attention(x)
        x = self.norm1(x + attn_out)  # 残差连接 + 层归一化

        # 前馈神经网络计算
        ffn_out = self.ffn(x)
        return self.norm2(x + ffn_out)  # 残差连接 + 层归一化

# Transformer 编码器,由多个编码器层堆叠组成
class Encoder(nn.Module):
    def __init__(self, d_model, n_heads, num_layers, max_len=5000):
        super(Encoder, self).__init__()
        self.d_model = d_model
        self.positional_encoding = PositionalEncoding(d_model, max_len)  # 位置编码
        self.layers = nn.ModuleList([EncoderLayer(d_model, n_heads) for _ in range(num_layers)])  # 多个编码器层

    def forward(self, x):
        x = self.positional_encoding(x)  # 添加位置编码
        for layer in self.layers:  # 遍历每一层编码器
            x = layer(x)
        return x

# 整个 Transformer 模型(仅包含编码器部分)
class Transformer(nn.Module):
    def __init__(self, d_model, n_heads, num_layers, vocab_size, max_len=5000):
        super(Transformer, self).__init__()
        self.embedding = nn.Embedding(vocab_size, d_model)  # 嵌入层
        self.positional_encoding = PositionalEncoding(d_model, max_len)  # 位置编码
        self.encoder = Encoder(d_model, n_heads, num_layers, max_len)  # 编码器
        self.fc_out = nn.Linear(d_model, vocab_size)  # 输出层

    def forward(self, x):
        x = self.embedding(x)  # 获取词嵌入
        x = self.positional_encoding(x)  # 添加位置编码
        x = self.encoder(x)  # 通过编码器
        return self.fc_out(x)  # 最后输出

工程上的实现方式

在工程中使用 Transformer 模型,肯定不会真的有人去从底层对其进行“手撕”,通常情况下我们会通过现有的深度学习框架(如 PyTorchTensorFlow)来调用已实现的 Transformer 模型。现代的深度学习框架提供了很多预训练的 Transformer 模型,并且通常支持多种预训练模型的加载和微调,可以直接利用这些现成的工具。

Hugging Face 提供了非常方便的接口来加载、微调和使用 Transformer 模型。这个库提供了许多预训练的 Transformer 模型(如 BERTGPT 等),并且可以很方便地进行模型加载和推理。

安装 Hugging Face Transformers 库

首先,安装 Hugging Face 的 Transformerstorch 库:

pip install transformers torch

加载预训练的 Transformer 模型(例如 BERT)

假设你需要加载预训练的 BERT 模型,并进行文本分类任务。下面是如何在工程中调用 Transformer 的例子。

from transformers import BertTokenizer, BertForSequenceClassification
import torch

# 1. 加载预训练模型和分词器
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')  # 选择预训练模型
model = BertForSequenceClassification.from_pretrained('bert-base-uncased', num_labels=2)  # 用于二分类任务

# 2. 编写输入文本并进行编码
text = "This is a sample text for classification"
inputs = tokenizer(text, return_tensors='pt', padding=True, truncation=True, max_length=512)

# 3. 推理:将编码后的输入数据传入模型进行预测
with torch.no_grad():  # 推理时不计算梯度
    outputs = model(**inputs)

# 4. 输出预测结果
logits = outputs.logits
prediction = torch.argmax(logits, dim=-1)  # 获取最大概率的类别
print(f"Prediction: {prediction.item()}")

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值