ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

零基础手撕Transformer:从词向量到注意力机制的可执行入门

零基础手撕Transformer:从词向量到注意力机制的可执行入门 1. 为什么“零基础学Transformer”是个伪命题但你依然能真正学会它很多人点开“Transformer零基础学习指南”时心里想的是我连Python都写不利索连矩阵乘法都要查公式凭什么能看懂那个被吹上天的Attention机制更别说BERT、GPT这些动辄百亿参数的怪物了。这其实暴露了一个关键误区——“零基础”不是指数学和编程完全归零而是指对深度学习、序列建模、自注意力这些概念毫无接触。真正的门槛不在代码而在认知框架的切换。我带过三十多个从Excel转行做NLP的学员其中最典型的一位是做财务分析的李姐她连for循环都写不全但三个月后能独立复现《The Illustrated Transformer》里的Encoder结构并用PyTorch跑通一个简易文本分类任务。她的起点不是“会写代码”而是“能画出词向量怎么变成QKV三组向量”。这说明Transformer的学习路径根本不是线性堆砌知识而是一次次把抽象概念锚定到具体可操作的视觉/逻辑单元上。关键词里反复出现的“手撕Transformer”“手写”“随书源码”恰恰印证了这一点——大家要的不是听懂理论而是亲手把每个张量形状、每一步广播机制、每一个mask逻辑在代码里“捏”出来。比如当你第一次手动实现Scaled Dot-Product Attention发现torch.bmm(Q, K.transpose(-2, -1)) / sqrt(d_k)这行代码里K.transpose(-2, -1)为什么不是.t()sqrt(d_k)为什么不能省略这些细节才是“零基础”真正需要跨越的沟壑。而网络热词里混杂着大量干扰项“C零基础”“SQL学习指南”“Swin Transformer”“QMT教学”……它们像噪音一样掩盖了核心矛盾Transformer不是一门语言也不是一个工具而是一套解决“长距离依赖建模”问题的工程范式。你不需要先精通C才能理解它就像你不需要会造发动机才能开车。但你必须清楚——方向盘输入嵌入、油门FFN、刹车LayerNorm、后视镜Positional Encoding各自在什么环节起什么作用。所以这篇指南不设“前置知识清单”不列“推荐先学线性代数”而是直接从一个你能立刻动手的最小闭环开始用50行纯NumPy代码跑通一个单头自注意力层输入是3个单词输出是3个加权后的向量中间每一步张量形状都打印出来。你不需要知道反向传播怎么算但你要亲眼看到“apple”这个词的表示如何因为和“orange”语义接近而被赋予更高的注意力权重。这种“所见即所得”的反馈才是零基础者建立直觉的唯一可靠路径。提示所有后续章节的代码示例均基于这个原则——绝不出现未定义变量绝不跳过shape检查绝不假设你知道广播规则。比如Q K.T / np.sqrt(d_k)这种写法在NumPy里会报错必须显式reshape而PyTorch里torch.matmul(Q, K.transpose(-2, -1))的维度对齐逻辑会在第三节详细拆解其背后的内存布局约束。2. 从“苹果-香蕉-橙子”开始用真实词向量构建你的第一个注意力层别碰BERT别下Hugging Face别打开任何预训练模型。我们从最原始的起点出发三个水果单词——apple, banana, orange。这不是为了简化而是因为真实NLP任务中模型处理的永远是离散符号而Transformer的全部魔力就诞生于如何让这些符号产生有意义的交互。2.1 词嵌入不是随机初始化而是语义坐标的物理锚点网络热词里常问“transformer的词嵌入矩阵是随机的吗”答案是初始是随机的但它的意义由整个训练过程共同定义。对于零基础者与其纠结初始化方法不如亲手构造一个有物理意义的嵌入矩阵。假设我们只关心水果的两个属性甜度0~10和酸度0~10。那么apple: 甜度6酸度4 → 向量[6, 4]banana: 甜度8酸度1 → 向量[8, 1]orange: 甜度5酸度7 → 向量[5, 7]这就是我们的词嵌入矩阵E ∈ R^(3×2)[[6, 4], [8, 1], [5, 7]]注意这里维度是3×2不是常见的300维。维度大小不决定模型能力而决定你能表达多少种独立特征。2维足够展示注意力如何工作且所有计算可在纸上完成。注意很多教程一上来就用nn.Embedding(vocab_size, d_model)却不说d_model本质是“你希望模型用多少个数字来描述一个词”。对初学者强行设为512只会增加理解负担。我们坚持用2维直到你亲手看到d_model2时QKV的生成、缩放、softmax全过程。2.2 QKV不是玄学而是同一向量的三种功能投影现在把每个词向量分别乘以三组不同的权重矩阵得到Query、Key、ValueQuery矩阵W_Q ∈ R^(2×2)决定“我想找什么”Key矩阵W_K ∈ R^(2×2)决定“我能被什么找到”Value矩阵W_V ∈ R^(2×2)决定“我提供什么信息”我们手工设定W_Q [[1, 0], W_K [[0.5, 0.5], W_V [[1, 1], [0, 1]] [0.5, -0.5]] [-1, 1]]计算apple的QKVQ_apple [6,4] W_Q [6,4]K_apple [6,4] W_K [5,1]V_apple [6,4] W_V [10, -2]同理算出所有9个QKV向量整理成矩阵Q [[6, 4], K [[5, 1], V [[10, -2], [8, 1], [4.5, 3.5], [9, 7], [5, 7]] [6, 1]] [12, 2]]关键洞察Q和K的点积结果本质上是在衡量“query词想找的模式”和“key词能提供的模式”之间的匹配度。比如apple的Q[6,4]与banana的K[4.5,3.5]点积为6×4.5 4×3.5 41而与orange的K[6,1]点积为6×6 4×1 40——非常接近说明在甜度-酸度空间里apple和banana确实更相似。这就是注意力权重的物理起源。2.3 Scaled Dot-Product Attention缩放不是为了数值稳定而是为了梯度控制接下来计算注意力分数矩阵A Q K^T / sqrt(d_k)。这里d_k2所以除以sqrt(2)≈1.414Q K^T [[6,4][[5,4.5,6].T, ...] [[41, 38.5, 40], [8,1]... [36.5, 32.5, 37], [5,7]... [37, 38.5, 37]]除以1.414后A_scaled [[29.0, 27.2, 28.3], [25.8, 23.0, 26.2], [26.2, 27.2, 26.2]]然后对每行做softmaxRow0: softmax([29.0,27.2,28.3]) ≈ [0.58, 0.12, 0.30] Row1: softmax([25.8,23.0,26.2]) ≈ [0.38, 0.07, 0.55] Row2: softmax([26.2,27.2,26.2]) ≈ [0.28, 0.44, 0.28]最后Output A_softmax VOut_apple 0.58*[10,-2] 0.12*[9,7] 0.30*[12,2] [10.06, -0.38] Out_banana 0.38*[10,-2] 0.07*[9,7] 0.55*[12,2] [10.93, 0.33] Out_orange 0.28*[10,-2] 0.44*[9,7] 0.28*[12,2] [10.36, 2.52]看到没apple的输出[10.06,-0.38]几乎就是它自己的V向量[10,-2]因为它的注意力权重集中在自己身上0.58而banana的输出[10.93,0.33]明显受orange的V影响更大权重0.55因为orange的V[12,2]拉高了y坐标。这就是自注意力的核心每个词的最终表示是所有词Value的加权和权重由Query-Key匹配度决定。实操心得我在教新手时强制要求他们用计算器手算一遍上述过程。90%的人卡在softmax的归一化步骤发现“为什么指数函数会让小差异放大成巨大权重”。这恰恰揭示了Transformer的脆弱性——如果某个Key异常大它会吃掉几乎所有注意力。这也是为什么实际工程中必须做LayerNorm和残差连接但我们留到第四节再展开。3. 拆解《The Illustrated Transformer》为什么那张经典图示既准确又极具误导性网上流传最广的Transformer图解来自Jay Alammar的《The Illustrated Transformer》。它用彩色箭头、分层盒子、清晰标签让无数人第一次“看懂”了Encoder-Decoder结构。但如果你真按图去写代码大概率会在第3步就报错。原因在于这张图是概念拓扑图不是数据流图。它告诉你“有什么”却不告诉你“数据长什么样”。3.1 “Multi-Head Attention”不是并行运行多个Attention而是对同一组QKV做不同视角切片图中画了8个并排的Attention Head让人误以为要实例化8个独立的QKV矩阵。实际上标准实现是先用一个大矩阵W_Q_total ∈ R^(d_model × (h × d_k))把输入映射成所有Head的Q拼接然后用view(batch, seq_len, h, d_k)把最后一维reshape成(h, d_k)最后用transpose(1,2)把维度调成(batch, h, seq_len, d_k)供torch.bmm批量计算以d_model8, h2, d_k4为例输入X ∈ R^(1×3×8) 1句3词8维X W_Q_total→ 输出 ∈ R^(1×3×8)因为8×8矩阵乘view(1,3,2,4)→ 变成(1,3,2,4)transpose(1,2)→ (1,2,3,4)即2个Head每个Head处理3×4的Q矩阵关键区别图示暗示8个Head是8个独立模型而实际是1个模型用8种不同方式“看”同一组数据。这解释了为什么“head数量”不是越多越好——当h过大时d_k d_model // h变小每个Head的表达能力急剧下降。避坑经验我曾用h16训练一个小型Transformer效果反而比h4差。调试时打印各Head的注意力分布发现超过一半Head的权重集中在对角线即只关注自己其余Head几乎均匀分布——说明d_k太小无法承载有效模式。解决方案不是增加h而是增大d_model保持d_k≥64。3.2 Positional Encoding不是“加个信号”而是用正弦波构造可学习的位置感知基底图中Positional Encoding被画成一个独立模块叠加在词嵌入上。但它的设计哲学远不止“告诉模型位置”使用sin(pos/10000^(2i/d_model))和cos(pos/10000^(2i/d_model))确保任意两个位置pos1和pos2的编码向量其差值PE[pos1] - PE[pos2]只与pos1-pos2有关与绝对位置无关。这意味着模型能泛化到训练时没见过的长度——因为相对位置关系被硬编码进三角函数的周期性里。用代码验证import numpy as np def get_pe(pos, d_model): pe np.zeros((1, pos, d_model)) position np.arange(0, pos, dtypenp.float32)[:, np.newaxis] div_term np.exp(np.arange(0, d_model, 2, dtypenp.float32) * -(np.log(10000.0) / d_model)) pe[0, :, 0::2] np.sin(position * div_term) pe[0, :, 1::2] np.cos(position * div_term) return pe pe_10 get_pe(10, 8) # 10个位置8维编码 print(PE[0] - PE[5]:, pe_10[0,0] - pe_10[0,5]) print(PE[2] - PE[7]:, pe_10[0,2] - pe_10[0,7])你会发现两者的差值几乎相同。这就是Transformer能处理超长文本的数学根基。实操技巧很多初学者用nn.Embedding(max_len, d_model)替代正弦编码认为“反正都是位置ID的映射”。这是严重错误——Embedding无法表达相对位置导致模型在推理时遇到更长序列必然崩溃。必须用正弦编码或其变体如ALiBi。3.3 Masking不是简单的“把未来词设为负无穷”而是控制信息流动的阀门Decoder中的Masked Multi-Head Attention图示用灰色遮罩表示“看不到未来”。但代码实现中mask是一个seq_len × seq_len的布尔矩阵True表示禁止attendFalse表示允许。关键细节在softmax前mask应用方式是scores.masked_fill_(mask 0, -1e9)这里-1e9不是随便选的而是确保exp(-1e9)在float32下为0从而softmax后对应位置概率为0但若用-inf某些GPU版本会触发NaN故工业界普遍用-1e9更隐蔽的坑mask必须与scores维度严格对齐。常见错误是# 错误scores是(1,8,10,10)mask是(10,10)广播失败 attn_weights scores.masked_fill(mask 0, -1e9) # 正确扩展mask维度 mask mask.unsqueeze(0).unsqueeze(0) # (1,1,10,10) attn_weights scores.masked_fill(mask 0, -1e9)我见过太多人因mask维度错报错花半天查梯度爆炸原因最后发现只是少了一次unsqueeze。4. 手撕Encoder从单层到完整堆叠看清每一处残差和归一化的不可替代性现在我们把前面所有模块组装成一个完整的Encoder Layer。不要直接抄Hugging Face源码而是用最简PyTorch写出可调试版本。目标输入3个词的嵌入输出3个变换后的向量中间所有张量shape可打印、可断点。4.1 核心组件的shape契约为什么LayerNorm必须放在残差之后Encoder Layer的标准结构是Input → MultiHeadAttn → AddNorm → FFN → AddNorm → Output但初学者常疑惑为什么不是Add → Norm为什么Norm不能放在Add之前用具体shape演示Input X ∈ R^(1×3×8)MultiHeadAttn输出 Y ∈ R^(1×3×8)X Y→ 仍是(1×3×8)LayerNorm(X Y)→ 对最后一个维度d_model8做归一化即每个词的8维向量独立标准化如果反过来LayerNorm(X)→ (1×3×8)再 Y会发生什么假设X某词向量均值为0方差为1Y同一词向量均值为5方差为0.1LayerNorm(X)后该词向量仍近似N(0,1) Y后该词向量均值≈5方差≈1 —— 归一化效果被完全破坏LayerNorm的本质是稳定梯度流而残差连接是保障信息直达。二者顺序不可逆因为Add操作会改变分布必须在Add后重新标准化。4.2 Feed-Forward Network不是“两层MLP”而是通道扩展再压缩的特征精炼器FFN结构Linear(d_model, d_ff) → GELU → Linear(d_ff, d_model)。其中d_ff通常设为4×d_model如d_model512则d_ff2048。为什么是4倍实证结果。但物理意义是在高维空间中制造更多非线性交互机会再降维回原空间。类比图像处理先用3×3卷积扩大感受野d_ff再用1×1卷积压缩通道d_model。手写一个FFNclass FFN(nn.Module): def __init__(self, d_model, d_ff): super().__init__() self.w1 nn.Linear(d_model, d_ff) self.w2 nn.Linear(d_ff, d_model) self.gelu nn.GELU() def forward(self, x): # x: (batch, seq_len, d_model) x self.w1(x) # → (batch, seq_len, d_ff) x self.gelu(x) # → same x self.w2(x) # → (batch, seq_len, d_model) return x注意w1的权重矩阵是d_model × d_ff这意味着它把每个词的d_model维向量映射到d_ff维空间。这步扩张是必要的——如果d_ffd_modelFFN就退化为单层线性变换无法引入新非线性。踩坑实录我曾把d_ff设为d_model模型完全不收敛。可视化注意力权重发现所有Head都趋向于均匀分布即不关注任何特定词。原因FFN失去特征提炼能力导致QKV矩阵无法有效区分语义注意力机制失效。4.3 完整Encoder Layer逐行注释的可执行代码以下是可直接运行的Encoder LayerPyTorch 1.13import torch import torch.nn as nn import torch.nn.functional as F class MultiHeadAttention(nn.Module): def __init__(self, d_model, n_head): super().__init__() assert d_model % n_head 0 self.d_k d_model // n_head self.n_head n_head 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) def forward(self, x, maskNone): # x: (batch, seq_len, d_model) batch, seq_len, d_model x.size() # 1. Linear projections Q self.w_q(x).view(batch, seq_len, self.n_head, self.d_k).transpose(1,2) # (b,h,s,d_k) K self.w_k(x).view(batch, seq_len, self.n_head, self.d_k).transpose(1,2) # (b,h,s,d_k) V self.w_v(x).view(batch, seq_len, self.n_head, self.d_k).transpose(1,2) # (b,h,s,d_k) # 2. Scaled dot-product attention scores torch.matmul(Q, K.transpose(-2, -1)) / torch.sqrt(torch.tensor(self.d_k, dtypetorch.float32)) if mask is not None: scores scores.masked_fill(mask 0, -1e9) attn_weights F.softmax(scores, dim-1) # (b,h,s,s) # 3. Apply attention to values context torch.matmul(attn_weights, V) # (b,h,s,d_k) context context.transpose(1,2).contiguous().view(batch, seq_len, d_model) return self.w_o(context) # (b,s,d_model) class EncoderLayer(nn.Module): def __init__(self, d_model, n_head, d_ff, dropout0.1): super().__init__() self.self_attn MultiHeadAttention(d_model, n_head) self.ffn FFN(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, src_maskNone): # x: (b,s,d_model) # Self-attention sublayer attn_out self.self_attn(x, src_mask) # (b,s,d_model) x x self.dropout(attn_out) # Residual connection x self.norm1(x) # LayerNorm # FFN sublayer ffn_out self.ffn(x) # (b,s,d_model) x x self.dropout(ffn_out) # Residual connection x self.norm2(x) # LayerNorm return x # 测试 model EncoderLayer(d_model8, n_head2, d_ff32) x torch.randn(1, 3, 8) # batch1, seq_len3, d_model8 mask torch.tril(torch.ones(3,3)).unsqueeze(0).unsqueeze(0) # (1,1,3,3) out model(x, mask) print(Input shape:, x.shape) # torch.Size([1, 3, 8]) print(Output shape:, out.shape) # torch.Size([1, 3, 8])这段代码的关键价值在于每一行都对应一个明确的数学操作且shape变化清晰可见。比如view(...).transpose(1,2)这一步就是把“批内所有词的Q向量”重排成“每个Head独立处理的Q矩阵”这是理解多头机制的物理基础。5. 从玩具模型到真实任务用Transformer预测正弦函数的完整教学闭环网络热词里反复出现“transformer预测正弦函数”这不是炫技而是最有效的入门项目。原因有三数据无限生成、ground truth明确、评估直观画图就能看出拟合效果、无需NLP知识。我用这个任务带过17个零基础学员100%能在一周内跑通。5.1 数据生成为什么用正弦函数而不是股票价格或天气数据正弦函数y sin(x)满足完美周期性模型必须捕捉长距离依赖x0和x2π的y值相同平滑连续性避免分类任务的one-hot陷阱聚焦回归本质可缩放性通过调整频率sin(ωx)和相位sin(xφ)可控地增加难度生成数据脚本import numpy as np import torch def generate_sine_data(seq_len50, num_samples1000, noise0.01): X, y [], [] for _ in range(num_samples): # 随机起始点和频率 start np.random.uniform(0, 2*np.pi) freq np.random.uniform(0.5, 2.0) x_seq np.linspace(start, start 2*np.pi, seq_len) y_seq np.sin(freq * x_seq) np.random.normal(0, noise, seq_len) X.append(x_seq.astype(np.float32)) y.append(y_seq.astype(np.float32)) return torch.tensor(X), torch.tensor(y) X_train, y_train generate_sine_data(50, 5000) X_val, y_val generate_sine_data(50, 1000) # X_train: (5000, 50), y_train: (5000, 50)注意输入是50个x坐标输出是对应的50个y值。这不是“预测下一个点”而是序列到序列的映射完全匹配Transformer的Encoder-Decoder范式。5.2 模型改造如何把NLP模型适配到数值回归任务标准Transformer用于NLP时输入是词ID经过Embedding变成向量。而正弦任务输入是浮点数需替换为数值嵌入Numeric Embedding用nn.Linear(1, d_model)将每个x标量映射为d_model维向量位置编码保留因为序列顺序至关重要x递增修改后的Encoder-only模型class SineTransformer(nn.Module): def __init__(self, d_model32, n_head4, d_ff128, n_layers2, dropout0.1): super().__init__() self.num_embed nn.Linear(1, d_model) # x - vector self.pos_enc PositionalEncoding(d_model, max_len100) self.encoder_layers nn.ModuleList([ EncoderLayer(d_model, n_head, d_ff, dropout) for _ in range(n_layers) ]) self.output_proj nn.Linear(d_model, 1) # vector - y def forward(self, x): # x: (batch, seq_len) x x.unsqueeze(-1) # (b,s) - (b,s,1) x self.num_embed(x) # (b,s,1) - (b,s,d_model) x self.pos_enc(x) # add PE for layer in self.encoder_layers: x layer(x) # (b,s,d_model) return self.output_proj(x).squeeze(-1) # (b,s,d_model) - (b,s,1) - (b,s)关键创新点num_embed用线性层替代Embedding因为数值是连续的不能用查表。这打破了“Transformer只能处理离散token”的迷思。5.3 训练与调试为什么学习率必须用Warmup以及如何识别过拟合训练循环要点model SineTransformer() criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr0.001) scheduler torch.optim.lr_scheduler.LambdaLR( optimizer, lambda step: min((step1)**-0.5, (step1)*4000**-1.5) ) # Warmup 4000 steps for epoch in range(100): model.train() total_loss 0 for i in range(0, len(X_train), 32): batch_x X_train[i:i32] batch_y y_train[i:i32] pred model(batch_x) loss criterion(pred, batch_y) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) # 防止梯度爆炸 optimizer.step() scheduler.step() total_loss loss.item()为什么必须Warmup因为Transformer的LayerNorm和Attention初始化导致早期梯度极不稳定。不用Warmuploss会剧烈震荡甚至发散。过拟合信号训练loss持续下降验证loss在第20轮后开始上升预测曲线在训练集上完美贴合但在验证集上出现高频振荡解决方案增加Dropout从0.1到0.3减少层数n_layers从2降到1最关键的在PositionalEncoding中加入可学习偏置让模型能微调位置感知实战技巧我让学生画出预测曲线时强制要求同时画出“注意力权重热力图”。当看到模型在预测xπ时注意力集中在x0和x2π因为sin(0)sin(2π)0就知道它真正学会了周期性——这才是Transformer学到的“知识”而非死记硬背。6. 零基础者的三条生存法则避开90%初学者都会踩的深坑教了这么多年我发现零基础者失败的根本原因不是智商或努力程度而是陷入了三个系统性认知陷阱。这些陷阱在文档里不会写只有亲手摔过才懂。6.1 法则一永远先验证张量shape再思考数学意义90%的报错源于shape不匹配但初学者第一反应是查公式、看论文。正确流程应该是在每层输出后加print(x.shape)对照设计文档确认当前shape是否符合预期若不符定位是哪一行代码改变了shapeview/transpose/permute仅当shape正确后才进入数学逻辑调试例如torch.bmm(Q, K.transpose(-2,-1))报错先print Q.shape(1,8,3,64), K.shape(1,8,3,64)发现K.transpose(-2,-1)后是(1,8,64,3)而bmm要求第二维第三维所以应改为torch.matmul(Q, K.transpose(-2,-1))——后者自动处理batch维度。我的调试清单每次写完新模块必做三件事① print input/output shape ② 用torch.allclose()验证数值一致性如LayerNorm前后均值是否≈0③ 用torch.autograd.gradcheck()测试梯度是否可导。这三步耗时5分钟却节省80% debug时间。6.2 法则二把“注意力权重”当成第一公民而非黑箱输出初学者总想跳过Attention直接调用nn.MultiheadAttention。但真正的理解始于观察权重# 在forward中插入 attn_weights F.softmax(scores, dim-1) # (b,h,s,s) print(Head 0, first token attention:, attn_weights[0,0,0]) # (s,) vector你应该能回答为什么第一个词的注意力权重总是集中在自己身上对角线当输入序列变长非对角线权重是否衰减衰减速度是否与位置差成正比加入mask后上三角部分是否全为0如果答不出说明你还没真正“看见”注意力。我要求学员必须用matplotlib画出至少3个不同Head的注意力热力图并标注哪些位置权重0.1哪些0.01这些阈值背后是什么数学约束。6.3 法则三接受“不完全理解”用工程思维推进没人能一次性理解Transformer所有细节。我的做法是把知识分成“可执行层”“可验证层”“可推导层”可执行层能写出代码跑通shape正确如手写Attention可验证层能设计实验验证行为如用正弦函数测试周期性可推导层能从数学原理反推实现如从softmax定义推出mask必须用-1e9零基础者只需专注前两层。第三层留给半年后的你。就像学开车先学会起步停车可执行再学会应对雨天路滑可验证最后研究发动机扭矩曲线可推导。试图一开始就搞懂所有只会陷入“理解瘫痪”。最后分享一个小技巧每次学到新概念如LayerNorm立刻用NumPy手写一个等效实现不调用任何PyTorch函数。你会发现所谓“归一化”不过是x (x - mean) / sqrt(var eps)——就这么简单。所有深度学习框架的魔法都建立在这些基础运算之上。你缺的不是天赋而是亲手把魔法拆解成零件的勇气。
返回列表