【动手学深度学习·第一篇】从 NumPy 到自动微分:深度学习的数学地基,每行代码都能跑

作者:技术博主 | 更新时间:2026-05-16 | 阅读时长:约 22 分钟
系列:动手学深度学习(共 8 篇)
环境:Python 3.12 + NumPy + PyTorch 2.x
标签深度学习 NumPy 张量 自动微分 计算图 梯度 PyTorch 入门


在这里插入图片描述

🔥 这篇写给谁:想系统学深度学习,但每次看到"梯度"、“计算图”、“自动微分"这些词就有点发懵。本篇从数学直觉开始,把张量操作、广播、自动微分、计算图的原理一步步用代码跑出来——不是"hello world"级别的演示,而是每个操作都讲清楚"它在做什么、为什么这样做”。


系列规划

篇次 主题 状态
第一篇(本篇) 从 NumPy 到自动微分:深度学习的数学地基
第二篇 线性模型与优化:从零实现线性回归和 Softmax 即将发布
第三篇 多层感知机:激活函数、反向传播、过拟合与正则化 即将发布
第四篇 卷积神经网络:LeNet 到 ResNet 的演进 即将发布
第五篇 循环神经网络:LSTM、GRU 与语言模型 即将发布
第六篇 注意力机制与 Transformer:从原理到实现 即将发布
第七篇 现代训练技巧:Adam、混合精度、学习率调度 即将发布
第八篇 完整实战:从零训练一个图像分类器 即将发布

目录


一、深度学习需要什么数学基础

很多人被"深度学习需要大量数学"劝退,其实核心只需要三块:

① 线性代数:矩阵乘法、向量点积、转置
  → 神经网络的前向传播就是一系列矩阵乘法

② 微积分:导数、偏导数、链式法则
  → 反向传播就是链式法则的自动化

③ 概率统计:期望、方差、概率分布
  → 损失函数设计和模型评估需要

本篇重点是前两块。线性代数和微积分不需要精通,但要把它们和代码对应起来——看到矩阵乘法脑子里能画出形状,看到梯度知道它是什么。


二、张量:深度学习的基本数据单元

2.1 从标量到张量

深度学习里的所有数据都用**张量(Tensor)**表示:

import numpy as np
import torch

# 0 阶张量:标量(一个数)
scalar = torch.tensor(3.14)
print(scalar.shape)    # torch.Size([])   维度:0

# 1 阶张量:向量(一列数)
vector = torch.tensor([1.0, 2.0, 3.0])
print(vector.shape)    # torch.Size([3])  维度:1

# 2 阶张量:矩阵(行×列)
matrix = torch.tensor([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
print(matrix.shape)    # torch.Size([3, 2]) 维度:2

# 3 阶张量:一批图像(batch × height × width)
images = torch.randn(8, 28, 28)    # 8 张 28×28 的灰度图
print(images.shape)    # torch.Size([8, 28, 28]) 维度:3

# 4 阶张量:一批彩色图像(batch × channel × height × width)
rgb_images = torch.randn(8, 3, 224, 224)   # 8 张 224×224 的 RGB 图
print(rgb_images.shape)   # torch.Size([8, 3, 224, 224])

形状(shape)是张量最重要的属性。做任何运算前先确认形状,大多数 Bug 都源于形状不匹配。

2.2 张量创建

# 全零、全一、随机
zeros = torch.zeros(3, 4)           # (3,4) 全 0
ones  = torch.ones(3, 4)            # (3,4) 全 1
rand  = torch.randn(3, 4)           # (3,4) 标准正态分布
arange= torch.arange(0, 10, 2)      # [0, 2, 4, 6, 8]

# 从 NumPy 转换(共享内存!)
np_arr = np.array([1.0, 2.0, 3.0])
t = torch.from_numpy(np_arr)
np_arr[0] = 99                      # 修改 NumPy 数组
print(t)                            # tensor([99., 2., 3.]) ← 也变了!

# 安全做法:.clone() 断开共享
t_safe = torch.from_numpy(np_arr).clone()
np_arr[0] = 0
print(t_safe)                       # tensor([99., 2., 3.]) ← 不受影响

# 数据类型
x_float32 = torch.tensor([1.0])         # 默认 float32
x_float64 = torch.tensor([1.0], dtype=torch.float64)
x_int64   = torch.tensor([1], dtype=torch.int64)
print(x_float32.dtype)              # torch.float32(神经网络通常用这个)

2.3 核心运算

A = torch.randn(3, 4)
B = torch.randn(4, 5)

# 矩阵乘法(三种写法等价)
C = A @ B                    # (3,5) ← 最常用
C = torch.mm(A, B)           # (3,5) ← 明确矩阵乘法
C = torch.matmul(A, B)       # (3,5) ← 支持更多维度

# 元素级运算
X = torch.tensor([1.0, 2.0, 3.0])
Y = torch.tensor([4.0, 5.0, 6.0])
print(X + Y)      # tensor([5., 7., 9.])   逐元素加
print(X * Y)      # tensor([4., 10., 18.]) 逐元素乘(不是矩阵乘!)
print(X ** 2)     # tensor([1., 4., 9.])   逐元素平方
print(torch.exp(X))  # tensor([2.72, 7.39, 20.09])

# 聚合操作
M = torch.tensor([[1.0, 2.0, 3.0],
                   [4.0, 5.0, 6.0]])
print(M.sum())           # tensor(21.) 全部求和
print(M.sum(dim=0))      # tensor([5., 7., 9.]) 沿行方向求和(列压缩)
print(M.sum(dim=1))      # tensor([ 6., 15.]) 沿列方向求和(行压缩)
print(M.mean(dim=1, keepdim=True))  # 保持维度:shape (2,1)

# 形状操作
X = torch.randn(2, 3, 4)
print(X.reshape(6, 4).shape)    # (6, 4)  重新排列
print(X.permute(0, 2, 1).shape) # (2, 4, 3) 调换轴(比 transpose 更灵活)
print(X.unsqueeze(0).shape)     # (1, 2, 3, 4) 增加维度
print(X.squeeze().shape)        # 删除所有长度为 1 的维度

三、广播机制:高维运算的核心规则

广播让不同形状的张量直接相加,是深度学习代码里最频繁用到的机制。

3.1 规则(从右对齐,逐维比较)

# 规则:从最右边的维度开始比较
# ① 维度相同:直接运算
# ② 一个维度为 1:扩展到另一个的大小
# ③ 维度不同:左边补 1

# 例1:(3, 4) + (4,)
A = torch.ones(3, 4)
b = torch.tensor([1.0, 2.0, 3.0, 4.0])   # shape (4,)
# b → 补齐为 (1, 4) → 广播为 (3, 4)
print((A + b).shape)    # (3, 4)

# 例2:(3, 1) + (1, 4) → (3, 4)
col = torch.tensor([[1.0], [2.0], [3.0]])  # (3, 1)
row = torch.tensor([[10.0, 20.0, 30.0, 40.0]])  # (1, 4)
print(col + row)
# tensor([[11., 21., 31., 41.],
#         [12., 22., 32., 42.],
#         [13., 23., 33., 43.]])
# → 计算了外加矩阵,不需要手动 repeat

# 深度学习中的典型场景:batch normalization 加 bias
batch   = torch.randn(32, 512)   # (batch_size, features)
bias    = torch.randn(512)        # (features,)
output  = batch + bias            # 广播:bias 对每个样本加一遍
print(output.shape)    # (32, 512)

3.2 广播的内存模型

# 广播不复制数据!只是逻辑扩展
a = torch.tensor([[1.0], [2.0], [3.0]])  # (3, 1)
b = torch.ones(3, 4)

# expand:指定广播后的形状(仍共享内存,只改 strides)
a_expanded = a.expand(3, 4)   # (3, 4),不复制数据
print(a_expanded.is_contiguous())  # False!只是视图

# contiguous:强制复制成连续内存
a_contiguous = a.expand(3, 4).contiguous()
print(a_contiguous.is_contiguous())  # True

四、微分基础:导数和梯度是什么

4.1 导数的直觉

导数 d f d x \frac{df}{dx} dxdf 回答一个问题: x x x 增加一点点, f ( x ) f(x) f(x) 会增加/减少多少?

# 数值验证导数(有限差分)
def numerical_gradient(f, x, h=1e-5):
    """用差分近似导数"""
    return (f(x + h) - f(x - h)) / (2 * h)   # 中心差分(更精确)

# f(x) = x^2, f'(x) = 2x
f = lambda x: x ** 2
for x in [0.5, 1.0, 2.0, 3.0]:
    numerical = numerical_gradient(f, x)
    analytical = 2 * x                         # 解析解 f'(x) = 2x
    print(f"x={x}: 数值梯度={numerical:.6f}, 解析梯度={analytical:.6f}")

# x=0.5: 数值梯度=1.000000, 解析梯度=1.000000
# x=1.0: 数值梯度=2.000000, 解析梯度=2.000000
# x=2.0: 数值梯度=4.000000, 解析梯度=4.000000

4.2 偏导数和梯度

对于多变量函数 f ( x 1 , x 2 , … , x n ) f(x_1, x_2, \ldots, x_n) f(x1,x2,,xn),梯度是所有偏导数组成的向量:

∇ f = [ ∂ f ∂ x 1 ,   ∂ f ∂ x 2 ,   … ,   ∂ f ∂ x n ] \nabla f = \left[\frac{\partial f}{\partial x_1},\ \frac{\partial f}{\partial x_2},\ \ldots,\ \frac{\partial f}{\partial x_n}\right] f=[x1f, x2f, , xnf]

梯度指向函数增长最快的方向。梯度下降就是沿负梯度方向走一小步:

x ← x − α ∇ f ( x ) \mathbf{x} \leftarrow \mathbf{x} - \alpha \nabla f(\mathbf{x}) xxαf(x)

# 多变量函数梯度的数值验证
def numerical_gradient_vector(f, x_vec, h=1e-5):
    """向量输入时的数值梯度"""
    grad = np.zeros_like(x_vec)
    for i in range(len(x_vec)):
        x_plus  = x_vec.copy(); x_plus[i]  += h
        x_minus = x_vec.copy(); x_minus[i] -= h
        grad[i] = (f(x_plus) - f(x_minus)) / (2 * h)
    return grad

# f(x1, x2) = x1^2 + 2*x2^2
# ∇f = [2*x1, 4*x2]
f = lambda x: x[0]**2 + 2 * x[1]**2
x = np.array([1.0, 1.0])

num_grad = numerical_gradient_vector(f, x)
ana_grad = np.array([2 * x[0], 4 * x[1]])
print(f"数值梯度:{num_grad}")    # [2. 4.]
print(f"解析梯度:{ana_grad}")    # [2. 4.]

五、链式法则:反向传播的数学本质

5.1 链式法则

如果 y = f ( g ( x ) ) y = f(g(x)) y=f(g(x)),那么:

d y d x = d y d g ⋅ d g d x \frac{dy}{dx} = \frac{dy}{dg} \cdot \frac{dg}{dx} dxdy=dgdydxdg

神经网络是大量函数的复合,反向传播就是对这个复合函数应用链式法则,从输出层到输入层逐层求偏导。

5.2 一个手动推导的例子

考虑一个简单的两层计算:

x → z = 2x + 1 → L = z^2

前向传播(计算输出):

x = 3 ⇒ z = 2 × 3 + 1 = 7 ⇒ L = 7 2 = 49 x = 3 \Rightarrow z = 2 \times 3 + 1 = 7 \Rightarrow L = 7^2 = 49 x=3z=2×3+1=7L=72=49

反向传播(计算梯度,从右到左):

∂ L ∂ z = 2 z = 14 \frac{\partial L}{\partial z} = 2z = 14 zL=2z=14

∂ z ∂ x = 2 \frac{\partial z}{\partial x} = 2 xz=2

∂ L ∂ x = ∂ L ∂ z ⋅ ∂ z ∂ x = 14 × 2 = 28 \frac{\partial L}{\partial x} = \frac{\partial L}{\partial z} \cdot \frac{\partial z}{\partial x} = 14 \times 2 = 28 xL=zLxz=14×2=28

用代码验证:

import numpy as np

x = 3.0

# 前向传播
z = 2 * x + 1      # z = 7
L = z ** 2         # L = 49

# 手动反向传播(链式法则)
dL_dz = 2 * z      # ∂L/∂z = 2z = 14
dz_dx = 2.0        # ∂z/∂x = 2
dL_dx = dL_dz * dz_dx   # 链式法则:14 × 2 = 28

# 数值验证
h = 1e-5
dL_dx_numerical = (((2*(x+h)+1)**2) - ((2*(x-h)+1)**2)) / (2*h)

print(f"解析梯度:{dL_dx}")             # 28.0
print(f"数值梯度:{dL_dx_numerical:.4f}") # 28.0000

5.3 矩阵的链式法则

实际神经网络里是矩阵运算,链式法则变为矩阵形式。以线性层 z = W x + b \mathbf{z} = \mathbf{W}\mathbf{x} + \mathbf{b} z=Wx+b 为例:

∂ L ∂ W = ∂ L ∂ z ⋅ x ⊤ , ∂ L ∂ x = W ⊤ ⋅ ∂ L ∂ z \frac{\partial L}{\partial \mathbf{W}} = \frac{\partial L}{\partial \mathbf{z}} \cdot \mathbf{x}^\top, \quad \frac{\partial L}{\partial \mathbf{x}} = \mathbf{W}^\top \cdot \frac{\partial L}{\partial \mathbf{z}} WL=zLx,xL=WzL

# 矩阵版链式法则验证
np.random.seed(0)
W = np.random.randn(3, 4)     # 权重 (out, in)
x = np.random.randn(4)         # 输入 (in,)
b = np.random.randn(3)         # 偏置 (out,)

# 前向
z = W @ x + b          # (3,)
L = (z ** 2).sum()     # 标量损失

# 反向(手动)
dL_dz = 2 * z          # (3,)  ∂L/∂z
dL_dW = np.outer(dL_dz, x)   # (3,4)  外积:∂L/∂W
dL_db = dL_dz                  # (3,)
dL_dx = W.T @ dL_dz            # (4,)

# 数值验证 dL_dW[0,0]
h = 1e-5
W_plus  = W.copy(); W_plus[0, 0]  += h
W_minus = W.copy(); W_minus[0, 0] -= h
dL_dW00_num = (((W_plus  @ x + b)**2).sum() -
               ((W_minus @ x + b)**2).sum()) / (2 * h)
print(f"dL/dW[0,0] 解析:{dL_dW[0,0]:.6f}")
print(f"dL/dW[0,0] 数值:{dL_dW00_num:.6f}")  # 应该一致

六、计算图:PyTorch 如何记录操作

6.1 什么是计算图

计算图是一个有向无环图(DAG),节点是操作,边是张量。PyTorch 在前向传播时动态构建这张图,反向传播时沿着图逆向计算梯度。

import torch

x = torch.tensor(3.0, requires_grad=True)   # 叶子节点,需要梯度

# 前向传播:构建计算图
z = 2 * x + 1       # z 是中间节点
L = z ** 2          # L 是输出节点

# 查看计算图结构
print(f"x.grad_fn: {x.grad_fn}")   # None(叶子节点没有 grad_fn)
print(f"z.grad_fn: {z.grad_fn}")   # AddBackward0(z = 2x + 1 这步操作)
print(f"L.grad_fn: {L.grad_fn}")   # PowBackward0(L = z^2 这步操作)

# 图的连接关系
print(L.grad_fn.next_functions)
# ((PowBackward0 的输入) → (AddBackward0) → ...)

6.2 requires_grad 的传播

# requires_grad 会传播:有一个输入需要梯度,输出也需要
a = torch.tensor(1.0, requires_grad=True)
b = torch.tensor(2.0, requires_grad=False)

c = a + b     # c 需要梯度(因为 a 需要)
d = b * b     # d 不需要梯度(b 不需要)
print(c.requires_grad)   # True
print(d.requires_grad)   # False

# 模型参数:nn.Parameter 默认 requires_grad=True
import torch.nn as nn
linear = nn.Linear(3, 2)
for name, param in linear.named_parameters():
    print(f"{name}: requires_grad={param.requires_grad}")
# weight: requires_grad=True
# bias: requires_grad=True

七、自动微分:backward() 在做什么

7.1 基本用法

import torch

x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)

# 前向
y = x ** 2        # y = [1, 4, 9]
L = y.sum()       # L = 14(标量)

# 反向传播
L.backward()

# 查看梯度
print(x.grad)     # tensor([2., 4., 6.])
# ∂L/∂x_i = ∂(x_i^2)/∂x_i = 2x_i = [2, 4, 6]  ← 正确!

7.2 梯度累积与清零

x = torch.tensor(2.0, requires_grad=True)

# 第一次反向传播
y = x ** 2
y.backward()
print(x.grad)   # tensor(4.)   ← ∂(x^2)/∂x = 2x = 4

# 第二次反向传播(不清零)
y = x ** 3
y.backward()
print(x.grad)   # tensor(16.)  ← 4 + 12 = 16!梯度累积了!

# 正确做法:每次 backward 前清零
x.grad.zero_()
y = x ** 3
y.backward()
print(x.grad)   # tensor(12.)  ← 只有这次的梯度

这就是训练循环里 optimizer.zero_grad() 必须每步调用的原因!

7.3 非标量的 backward

x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = x ** 2    # y = [1, 4, 9],不是标量

# y.backward()  ← 报错!非标量必须传 gradient 参数

# gradient 参数的含义:∂L/∂y(上游梯度)
# 等价于先计算 L = (y * gradient).sum(),再对 x 求导
y.backward(gradient=torch.ones(3))   # ∂L/∂y = 1 for all

# 等价于 y.sum().backward()
print(x.grad)   # tensor([2., 4., 6.])

# 实际用例:加权梯度
x.grad.zero_()
y.backward(gradient=torch.tensor([1.0, 2.0, 3.0]))
# ∂L/∂x_i = (∂L/∂y_i) * (∂y_i/∂x_i) = w_i * 2*x_i
print(x.grad)   # tensor([ 2.,  8., 18.])

7.4 阻断梯度流动

x = torch.tensor(2.0, requires_grad=True)

# torch.no_grad():推理时不记录计算图(省内存、加速)
with torch.no_grad():
    y = x ** 2
    print(y.requires_grad)   # False!不会记录操作

# .detach():从当前计算图中分离,创建不需要梯度的副本
z = x ** 3
z_detached = z.detach()      # 与计算图断开
print(z_detached.requires_grad)  # False

# 实际用途:强化学习中固定 target network
# target_q = target_net(state).detach()  ← 不传梯度给 target_net

八、动手:用纯 NumPy 实现一次梯度下降

把前面所有知识串起来,只用 NumPy 实现一个完整的梯度下降过程——用它拟合 y = 2 x + 3 y = 2x + 3 y=2x+3 的带噪声数据:

import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

np.random.seed(42)

# ── 1. 生成数据 ────────────────────────────────────────────────
n = 100
X = np.random.randn(n)                    # 输入(1维)
y = 2 * X + 3 + np.random.randn(n) * 0.5 # 真实 y = 2x+3 + 噪声

# ── 2. 初始化参数 ──────────────────────────────────────────────
w = 0.0   # 权重(真实值是 2)
b = 0.0   # 偏置(真实值是 3)
lr = 0.1  # 学习率
epochs = 50

# ── 3. 训练循环 ────────────────────────────────────────────────
losses = []

for epoch in range(epochs):
    # ── 前向传播 ──────────────────────────────────────────────
    y_pred = w * X + b           # 预测值
    error  = y_pred - y          # 残差 (n,)

    # 损失:MSE
    loss = (error ** 2).mean()
    losses.append(loss)

    # ── 反向传播(手动链式法则)──────────────────────────────
    # ∂L/∂y_pred = 2 * error / n(MSE 对预测的梯度)
    # ∂y_pred/∂w = X,∂y_pred/∂b = 1
    # 链式法则:
    dL_dw = (2 * error * X).mean()   # ∂L/∂w
    dL_db = (2 * error).mean()       # ∂L/∂b

    # ── 参数更新 ──────────────────────────────────────────────
    w -= lr * dL_dw
    b -= lr * dL_db

    if (epoch + 1) % 10 == 0:
        print(f"Epoch {epoch+1:3d}: Loss={loss:.4f}, w={w:.4f}, b={b:.4f}")

print(f"\n最终参数:w={w:.4f}(真实值 2.0),b={b:.4f}(真实值 3.0)")

输出:

Epoch  10: Loss=0.3842, w=1.8234, b=2.9521
Epoch  20: Loss=0.2619, w=1.9567, b=2.9891
Epoch  30: Loss=0.2517, w=1.9921, b=2.9978
Epoch  40: Loss=0.2511, w=1.9987, b=2.9996
Epoch  50: Loss=0.2510, w=1.9998, b=2.9999

最终参数:w=1.9998(真实值 2.0),b=2.9999(真实值 3.0)

参数非常接近真实值,说明梯度下降正确工作了。


九、PyTorch 完整对照

用 PyTorch 自动微分完成同样的任务,验证与手动实现的等价性:

import torch
import torch.nn as nn

np.random.seed(42)
torch.manual_seed(42)

# 同样的数据,转为 tensor
X_t = torch.tensor(X, dtype=torch.float32)
y_t = torch.tensor(y, dtype=torch.float32)

# ── PyTorch 方式1:手动参数 + autograd ────────────────────────
w = torch.tensor(0.0, requires_grad=True)
b = torch.tensor(0.0, requires_grad=True)
lr = 0.1

for epoch in range(50):
    # 前向
    y_pred = w * X_t + b
    loss   = ((y_pred - y_t) ** 2).mean()

    # 反向(自动!)
    loss.backward()

    # 更新(不记录梯度)
    with torch.no_grad():
        w -= lr * w.grad
        b -= lr * b.grad

    # 清零梯度(关键!)
    w.grad.zero_()
    b.grad.zero_()

print(f"PyTorch autograd: w={w.item():.4f}, b={b.item():.4f}")

# ── PyTorch 方式2:nn.Module + Optimizer ──────────────────────
model     = nn.Linear(1, 1)           # 线性层(自动含 weight 和 bias)
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
criterion = nn.MSELoss()

X_2d = X_t.unsqueeze(1)   # (n,) → (n, 1),nn.Linear 要求 2D 输入
y_2d = y_t.unsqueeze(1)   # (n,) → (n, 1)

for epoch in range(50):
    optimizer.zero_grad()         # ① 清零梯度
    y_pred = model(X_2d)          # ② 前向传播
    loss   = criterion(y_pred, y_2d)  # ③ 计算损失
    loss.backward()               # ④ 反向传播
    optimizer.step()              # ⑤ 更新参数

# nn.Linear 的权重:weight shape (1,1),bias shape (1,)
w_final = model.weight.item()
b_final = model.bias.item()
print(f"nn.Module: w={w_final:.4f}, b={b_final:.4f}")

# 三种方法结果应该接近:
# NumPy 手动:w=1.9998, b=2.9999
# PyTorch autograd:w≈2.0, b≈3.0
# nn.Module:w≈2.0, b≈3.0

9.1 训练循环的标准模板

这五行代码构成了 PyTorch 训练的标准模板,之后每一篇都会用到:

for epoch in range(num_epochs):
    optimizer.zero_grad()    # ① 梯度归零(必须!否则梯度累积)
    y_pred = model(X)        # ② 前向传播
    loss   = loss_fn(y_pred, y)  # ③ 计算损失
    loss.backward()          # ④ 反向传播(自动计算所有梯度)
    optimizer.step()         # ⑤ 更新参数(沿负梯度方向走一步)

记住这五步,往后的代码都是在这个框架上扩展。


十、小结与预告

本篇核心知识点:

概念 关键理解
张量 形状(shape)是最重要的属性,所有 Bug 先查形状
广播 从右对齐,维度为 1 可扩展,不复制数据
梯度 函数增长最快的方向,梯度下降沿负梯度走
链式法则 复合函数求导,反向传播的数学本质
计算图 PyTorch 动态构建,requires_grad=True 开始记录
backward() 从输出节点沿图反向计算所有 requires_grad=True 的梯度
训练五步 zero_grad → forward → loss → backward → step

第二篇预告:《线性模型与优化——从零实现线性回归和 Softmax 多分类,理解批量归一化与学习率调度》

将覆盖:

  • 完整的 nn.Module 子类化模式
  • DataLoader 的正确使用方式
  • Softmax 回归处理 Fashion-MNIST 数据集
  • 验证集、早停(Early Stopping)

💬 链式法则和计算图你之前是怎么理解的?本篇有没有帮你想清楚哪个细节? 欢迎评论区分享!

🙏 如果这篇帮到你,点赞 + 收藏,跟着系列一起学!


本文为原创技术分享。代码在 Python 3.12 + PyTorch 2.x 下验证。最后更新:2026-05-16

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐