首页 / 资讯中心 / 文章详情

Python深度学习入门:从张量到神经网络实战

Python深度学习入门:从张量到神经网络实战 ★ FEATURED ARTICLE
1. 为什么用 Python 切入深度学习是最务实的路径1.1 从“调包”到“懂原理”的认知转折很多人第一次接触深度学习都是从一行model.fit()开始的。跑通了准确率也还行但心里是虚的——不知道数据在内部到底经历了什么变换也不知道为什么换个激活函数结果就崩了。我刚开始也是这样觉得能跑出结果就行直到有一次遇到一个诡异的问题训练集准确率一路飙升到 99%验证集却死活卡在 60% 不动。当时完全不知道从哪下手只能瞎调参数浪费了整整两天。后来我才明白问题的根源在于我对“深度学习到底在算什么”这件事没有建立起直观的认知。而 Python 恰好是打破这层窗户纸最顺手的工具——它的语法足够简洁不会让你在语言本身耗费太多精力生态足够丰富从数据处理到模型搭建到可视化一条龙全包更重要的是它的交互式特性让你可以随时停下来打印中间结果画个图看看这种“边做边看”的方式对理解深度学习的内部机制帮助极大。这个速成课程的第一篇我想做的事情很明确不堆公式不搞数学恐吓用 Python 代码和图解的方式把深度学习最核心的几个概念——张量、计算图、梯度下降、反向传播——一个一个拆开揉碎讲清楚。适合谁看如果你已经会写基本的 Python对 NumPy 有一点了解但深度学习的门还没真正迈进去那这篇内容就是为你准备的。如果你已经能熟练调包但总觉得心里不踏实想回头把基础打牢那也合适。1.2 为什么不是 MATLAB 或 C这个问题我被问过很多次。选 Python 做深度学习的入门语言不是因为别的语言不行而是因为 Python 在“学习友好度”这个维度上几乎是无敌的。MATLAB 在数值计算方面确实强工具箱也很完善但它的深度学习生态相对封闭很多最新的模型和技巧都是 Python 社区先跑出来的。而且 MATLAB 是商业软件对个人学习者来说成本不低。C 的性能毋庸置疑工业部署阶段确实大量使用但用 C 来学深度学习就像用手术刀削苹果——不是不行是你得先把刀磨好而磨刀的过程可能就把你的学习热情消耗殆尽了。Python 的优势在于你写import numpy as np就能做数值计算写import matplotlib.pyplot as plt就能画图写import torch或者import tensorflow就能搭神经网络。所有的工具都在一个语言体系里不需要在多个环境之间来回切换。这种一致性对初学者来说太重要了——你的注意力应该花在理解概念上而不是花在“这个函数在哪个库里”“那个库的安装包怎么又冲突了”这些事情上。实操心得如果你刚开始学不要一上来就装 CUDA、配 GPU 环境。先用 CPU 版本把核心概念跑通等确实需要加速了再折腾 GPU。我见过太多人卡在环境配置这一步就放弃了非常可惜。1.3 图解的价值让抽象概念“看得见”深度学习里有很多概念光看文字描述很难建立直觉。比如“张量的维度变换”你读十遍定义不如画一张图。再比如“梯度下降”公式写出来就那几行但如果你能看到损失函数曲面上的点一步步往下滚的动画理解会深刻得多。这个系列我会大量使用图解的方式。不是那种花哨的信息图而是用 Python 的绘图库直接生成的示意图——因为这样你可以自己改代码、改参数看看图会变成什么样。这种“可交互的图解”比静态的图片有价值得多。举个例子后面讲到卷积操作的时候我会用代码生成一个小的输入矩阵和一个卷积核然后一步步展示卷积核在输入上滑动的过程每一步的输出都实时画出来。你看到那个图就立刻明白卷积到底在干什么了。这比看公式推导快得多。2. 核心概念拆解张量、计算图与自动求导2.1 张量深度学习的数据容器张量这个词听起来很唬人但其实它就是一个多维数组。标量是 0 维张量向量是 1 维张量矩阵是 2 维张量再往上就是 3 维、4 维甚至更高维的张量。你可以把它想象成一个可以任意嵌套的盒子最外层是一个大盒子打开里面是一排小盒子再打开每个小盒子里面又是一排更小的盒子以此类推。在 Python 里NumPy 的ndarray就是张量的一种实现。但深度学习框架里的张量比如 PyTorch 的Tensor比 NumPy 数组多了两个关键能力一是可以在 GPU 上运行二是可以自动追踪计算历史支持自动求导。这两个能力是深度学习训练的基础。import numpy as np # 0 维张量标量 scalar np.array(5) print(scalar.ndim) # 0 # 1 维张量向量 vector np.array([1, 2, 3]) print(vector.ndim) # 1 # 2 维张量矩阵 matrix np.array([[1, 2], [3, 4]]) print(matrix.ndim) # 2 # 3 维张量 tensor_3d np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]) print(tensor_3d.ndim) # 3理解张量的关键在于理解它的“形状”shape。形状描述了每个维度上有多少个元素。比如一个形状为(3, 4)的张量就是一个 3 行 4 列的矩阵。形状为(2, 3, 4)的张量就是 2 个 3 行 4 列的矩阵叠在一起。为什么形状这么重要因为深度学习里的几乎所有操作都对形状有要求。两个矩阵相乘前一个的列数必须等于后一个的行数。卷积操作的输入通道数必须等于卷积核的输入通道数。如果你不理解形状遇到报错就只能瞎猜。注意事项初学者最常见的错误之一就是搞混“维度”和“形状”。维度是张量的轴数形状是每个轴上的元素个数。一个形状为(3,)的张量维度是 1不是 3。这个区别在调试的时候非常关键。2.2 计算图把运算过程画成一张网计算图是理解深度学习框架内部工作机制的关键。简单来说计算图就是把所有的计算步骤表示成一张有向图节点是张量或者运算操作边表示数据流动的方向。举个例子假设我们要计算z (a b) * c。用计算图表示就是先有一个加法节点输入是a和b输出是a b然后有一个乘法节点输入是a b的结果和c输出是z。整个计算过程就是数据从输入节点流向输出节点的过程。为什么要有计算图因为有了它框架就可以自动计算梯度。反向传播的本质就是在计算图上从输出往输入方向走一遍利用链式法则逐层计算梯度。没有计算图你就得手动推导每个参数的梯度公式那工作量是不可想象的。import torch # 创建需要计算梯度的张量 a torch.tensor(2.0, requires_gradTrue) b torch.tensor(3.0, requires_gradTrue) c torch.tensor(4.0, requires_gradTrue) # 前向计算 z (a b) * c # 反向传播 z.backward() print(a.grad) # dz/da c 4 print(b.grad) # dz/db c 4 print(c.grad) # dz/dc a b 5上面这段代码展示了 PyTorch 的自动求导机制。当你调用z.backward()的时候PyTorch 会沿着计算图反向走一遍自动算出每个叶子节点的梯度。你不需要手动推导任何公式。这个机制背后的原理就是链式法则。z对a的梯度等于z对(ab)的梯度乘以(ab)对a的梯度。第一项是c第二项是1所以结果是c。框架帮你做了这些计算你只需要关注前向逻辑。2.3 自动求导深度学习的引擎自动求导是深度学习框架最核心的能力之一。没有它训练神经网络就是一件几乎不可能完成的任务——一个稍微大一点的网络可能有几百万甚至几十亿个参数手动推导每个参数的梯度公式根本不现实。自动求导有两种模式前向模式和反向模式。深度学习用的是反向模式也就是我们常说的反向传播。为什么用反向模式因为神经网络的输出通常是一个标量损失值而输入是大量的参数。反向模式在这种情况下计算效率最高。理解自动求导的关键是理解“计算图”和“链式法则”的结合。框架在前向传播的时候会记录所有的操作构建出一张计算图。反向传播的时候从损失值出发沿着计算图反向走每遇到一个操作就应用链式法则把梯度传递下去。# 一个更复杂的例子 x torch.tensor([1.0, 2.0, 3.0], requires_gradTrue) y x ** 2 2 * x 1 z y.sum() z.backward() print(x.grad) # dz/dx 2x 2 [4, 6, 8]这个例子中y x^2 2x 1所以dy/dx 2x 2。当x [1, 2, 3]时梯度就是[4, 6, 8]。框架自动算出了这个结果。实操心得调试自动求导问题的时候一个非常有用的技巧是用torch.autograd.gradcheck来验证你的梯度计算是否正确。它会用数值方法近似计算梯度然后和你通过反向传播得到的梯度对比。如果两者差距很大说明你的前向计算或者反向传播有问题。3. 从零搭建一个微型神经网络3.1 环境准备与工具选型动手写代码之前先把环境搭好。我推荐用 Anaconda 来管理 Python 环境因为它可以很方便地创建隔离的环境避免不同项目之间的依赖冲突。# 创建一个新的环境 conda create -n dl-tutorial python3.10 conda activate dl-tutorial # 安装核心库 pip install numpy matplotlib torch为什么选 PyTorch 而不是 TensorFlow对于学习来说PyTorch 的动态计算图更直观调试更方便。你可以像写普通 Python 代码一样写模型随时打印中间结果。TensorFlow 的静态图模式在部署阶段有优势但学习曲线更陡。当然这只是个人偏好两者都是优秀的框架。NumPy 用于数据处理Matplotlib 用于可视化。这两个库是 Python 科学计算的基础设施必须熟练掌握。注意事项安装 PyTorch 的时候官网会给你一个命令生成器根据你的操作系统、包管理器、Python 版本、CUDA 版本生成对应的安装命令。如果你没有 GPU选择 CPU 版本即可。不要随便复制网上的安装命令版本不匹配会导致各种奇怪的错误。3.2 用 NumPy 手动实现一个线性回归在调用框架之前先用 NumPy 手动实现一个最简单的线性回归。这样做的好处是你能清楚地看到每一步在算什么没有黑盒。import numpy as np import matplotlib.pyplot as plt # 生成模拟数据 np.random.seed(42) X np.random.rand(100, 1) y 3 * X 2 np.random.randn(100, 1) * 0.1 # 初始化参数 w np.random.randn(1, 1) b np.zeros(1) # 超参数 lr 0.1 epochs 100 losses [] for epoch in range(epochs): # 前向传播 y_pred X w b # 计算损失均方误差 loss np.mean((y_pred - y) ** 2) losses.append(loss) # 计算梯度 grad_w 2 * X.T (y_pred - y) / len(X) grad_b 2 * np.mean(y_pred - y) # 更新参数 w - lr * grad_w b - lr * grad_b print(f学习到的 w: {w.flatten()}, b: {b.flatten()}) print(f真实值 w: 3, b: 2)这段代码虽然简单但包含了深度学习训练的所有核心要素前向传播、损失计算、梯度计算、参数更新。你把这个循环跑一百遍就完成了一次完整的训练过程。梯度是怎么推导出来的损失函数是L (1/n) * sum((y_pred - y)^2)。对w求偏导根据链式法则结果是(2/n) * X^T (y_pred - y)。对b求偏导结果是(2/n) * sum(y_pred - y)。这些推导过程看起来简单但它们是理解更复杂网络的基础。3.3 用 PyTorch 重写感受框架的便利同样的任务用 PyTorch 来写会简洁很多因为梯度计算被自动处理了。import torch import torch.nn as nn # 准备数据 X torch.rand(100, 1) y 3 * X 2 torch.randn(100, 1) * 0.1 # 定义模型 model nn.Linear(1, 1) # 定义损失函数和优化器 criterion nn.MSELoss() optimizer torch.optim.SGD(model.parameters(), lr0.1) # 训练 for epoch in range(100): # 前向传播 y_pred model(X) loss criterion(y_pred, y) # 反向传播 optimizer.zero_grad() loss.backward() optimizer.step() print(f学习到的 w: {model.weight.item()}, b: {model.bias.item()})对比两段代码你会发现 PyTorch 版本少了手动计算梯度的部分。loss.backward()自动完成了所有梯度计算optimizer.step()自动更新了参数。这就是框架的价值——让你专注于模型结构的设计而不是底层的数学运算。但要注意optimizer.zero_grad()这一步不能省。PyTorch 默认会累积梯度如果你不清零下一次反向传播的梯度会加到上一次的梯度上导致结果错误。这个坑我踩过当时训练 loss 一直不下降排查了半天才发现是忘了清零。3.4 训练过程可视化让 loss 曲线告诉你发生了什么训练过程中loss 曲线是最重要的诊断工具。它能告诉你模型是在学习、在震荡、还是已经过拟合了。import matplotlib.pyplot as plt # 记录每个 epoch 的 loss losses [] for epoch in range(200): y_pred model(X) loss criterion(y_pred, y) losses.append(loss.item()) optimizer.zero_grad() loss.backward() optimizer.step() # 绘制 loss 曲线 plt.figure(figsize(10, 5)) plt.plot(losses) plt.xlabel(Epoch) plt.ylabel(Loss) plt.title(Training Loss Curve) plt.grid(True) plt.show()一条健康的 loss 曲线应该是先快速下降然后逐渐趋于平缓。如果 loss 一直震荡不下降可能是学习率太大了。如果 loss 下降得很慢可能是学习率太小了。如果 loss 下降到一定程度后开始上升那可能是过拟合了。实操心得我习惯在训练的时候每隔一定 epoch 打印一次 loss同时把 loss 值存到一个列表里训练结束后画出来。这样既能实时监控训练状态又能在训练结束后做完整的分析。不要只盯着最终结果过程同样重要。4. 常见问题与排查技巧实录4.1 梯度消失与梯度爆炸训练深层网络的时候梯度消失和梯度爆炸是两个绕不开的问题。梯度消失是指反向传播过程中梯度越来越小导致浅层参数几乎不更新。梯度爆炸则相反梯度越来越大参数更新幅度过大loss 直接飞掉。怎么判断如果你发现 loss 完全不下降而且浅层参数的梯度接近零那大概率是梯度消失。如果 loss 突然变成 NaN 或者一个巨大的数那可能是梯度爆炸。解决方法有几种。对于梯度爆炸最直接的是梯度裁剪设置一个阈值超过就截断。torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)对于梯度消失可以考虑用 ReLU 激活函数代替 Sigmoid因为 ReLU 在正区间的梯度恒为 1不会衰减。另外Batch Normalization 也能有效缓解这个问题它通过规范化每层的输入分布让梯度更稳定。4.2 过拟合模型记住了训练数据但没学会规律过拟合的表现是训练集 loss 持续下降但验证集 loss 先下降后上升。模型把训练数据的噪声也学进去了导致泛化能力差。判断过拟合最简单的方法就是看训练集和验证集的 loss 曲线。如果两者差距越来越大那就是过拟合了。解决过拟合的手段比较多。增加数据量是最根本的方法但很多时候数据就那么多没法增加。这时候可以用数据增强比如对图片做旋转、翻转、裁剪人为制造更多样本。另外Dropout 也是常用的正则化手段训练时随机丢弃一部分神经元强迫网络不依赖某些特定的路径。model nn.Sequential( nn.Linear(784, 256), nn.ReLU(), nn.Dropout(0.5), nn.Linear(256, 10) )L2 正则化权重衰减也有效它在损失函数里加了一项参数的平方和惩罚过大的权重。optimizer torch.optim.SGD(model.parameters(), lr0.01, weight_decay1e-4)4.3 学习率设置最重要的超参数如果只能调一个超参数那一定是学习率。学习率太大loss 震荡甚至发散学习率太小收敛太慢可能卡在局部最小值出不来。我一般会先用一个较大的学习率比如 0.1跑几个 epoch看看 loss 的变化。如果 loss 震荡得很厉害就减小到 0.01 再试。如果 loss 下降得太慢就适当增大。这个过程可能需要反复几次但值得花时间。更高级的做法是使用学习率调度器在训练过程中动态调整学习率。比如每过 30 个 epoch 把学习率乘以 0.1。scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size30, gamma0.1)还有一种很实用的技巧是 warmup在训练初期用很小的学习率然后逐渐增大到设定的值。这对 Transformer 这类模型特别有效可以避免训练初期的不稳定。4.4 常见报错速查表报错信息可能原因解决方法RuntimeError: mat1 and mat2 shapes cannot be multiplied矩阵维度不匹配检查输入和权重的形状确保前者的列数等于后者的行数RuntimeError: Expected all tensors to be on the same device张量在不同设备上用.to(device)把所有张量移到同一个设备RuntimeError: element 0 of tensors does not require grad计算图被断开检查是否对不需要梯度的张量调用了backward()CUDA out of memory显存不足减小 batch size或者用torch.cuda.empty_cache()清理缓存ValueError: Expected input batch_size to match target batch_size输入和标签的 batch 大小不一致检查 DataLoader 的输出确保输入和标签一一对应注意事项遇到报错不要慌先看错误信息的最后一行那里通常包含了最关键的信息。然后从报错的位置往上找看看是哪个变量的形状或者设备不对。大部分错误都是形状不匹配或者设备不一致导致的。4.5 调试技巧打印中间结果深度学习代码的调试比普通程序麻烦因为很多计算是批量进行的你没法单步跟踪每一个样本。我的习惯是在关键位置打印张量的形状和统计信息。def debug_tensor(name, tensor): print(f{name}: shape{tensor.shape}, fmin{tensor.min().item():.4f}, fmax{tensor.max().item():.4f}, fmean{tensor.mean().item():.4f}, frequires_grad{tensor.requires_grad})这个函数可以在前向传播的每个关键步骤调用帮你快速定位问题。比如如果某一层的输出全是零那可能是激活函数的问题。如果某一层的输出数值特别大那可能是初始化或者学习率的问题。另外PyTorch 的 hook 机制也很有用可以在不修改模型代码的情况下获取中间层的输出或者梯度。def hook_fn(module, input, output): print(f{module.__class__.__name__}: output shape{output.shape}) for layer in model.children(): layer.register_forward_hook(hook_fn)这个技巧在调试复杂模型的时候特别有用你可以清楚地看到数据在网络中是怎么流动的。5. 从微型网络到真实场景的过渡5.1 用 MNIST 手写数字识别验证所学前面用线性回归做了演示但那个问题太简单了不足以检验你是否真的理解了深度学习的流程。手写数字识别是一个更好的试金石——它有真实的图像输入有多分类输出有足够的复杂度让你遇到各种实际问题。from torchvision import datasets, transforms from torch.utils.data import DataLoader # 数据预处理 transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) # 加载数据 train_dataset datasets.MNIST(./data, trainTrue, downloadTrue, transformtransform) test_dataset datasets.MNIST(./data, trainFalse, transformtransform) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) test_loader DataLoader(test_dataset, batch_size1000, shuffleFalse)这里的Normalize用了 MNIST 数据集的全局均值和标准差。为什么要做归一化因为如果输入数据的分布差异很大梯度下降会走得很曲折。归一化之后所有特征的尺度一致优化过程更稳定。模型结构用一个简单的多层感知机就够了。model nn.Sequential( nn.Flatten(), nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 128), nn.ReLU(), nn.Linear(128, 10) )训练循环和之前一样但这次要加上验证步骤每个 epoch 结束后在测试集上评估准确率。def evaluate(model, test_loader): model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in test_loader: outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() return correct / totalmodel.eval()和torch.no_grad()是评估时的标准操作。前者把 Dropout 和 BatchNorm 切换到推理模式后者关闭梯度计算以节省内存和计算资源。5.2 训练技巧batch size 与学习率的配合batch size 和学习率是一对需要配合调整的超参数。一般来说batch size 越大梯度估计越准确但更新次数越少。batch size 越小梯度噪声越大但更新越频繁。经验法则是如果 batch size 翻倍学习率也可以适当增大但不要线性增大。比如 batch size 从 64 增加到 256学习率可以从 0.01 增加到 0.02 左右而不是 0.04。为什么因为更大的 batch size 意味着更准确的梯度方向你可以更放心地迈大步。但步子太大容易越过最优点所以不能增加太多。我一般会从 batch size 64 或 128 开始学习率 0.01 或 0.001。如果显存够用可以试试更大的 batch size配合适当增大的学习率训练速度会快很多。5.3 模型保存与加载别让训练成果白费训练一个模型可能要几个小时甚至几天如果不保存下次就得重来。PyTorch 提供了两种保存方式保存整个模型和只保存参数。# 保存整个模型 torch.save(model, model.pth) # 只保存参数推荐 torch.save(model.state_dict(), model_params.pth) # 加载参数 model nn.Sequential(...) # 先定义相同的结构 model.load_state_dict(torch.load(model_params.pth))推荐只保存参数因为保存整个模型会把类的定义也序列化进去如果代码结构变了加载可能会失败。只保存参数的话你只需要保证模型结构一致就行。保存的时候还可以把优化器的状态也存下来这样可以从断点继续训练。checkpoint { epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), loss: loss, } torch.save(checkpoint, checkpoint.pth)这个技巧在训练大模型的时候特别有用万一训练中断了可以从最近的 checkpoint 恢复不用从头开始。5.4 下一步该学什么如果你跟着这篇内容把线性回归和 MNIST 都跑通了那深度学习的入门阶段就算完成了。接下来可以往几个方向深入。想深入计算机视觉可以学卷积神经网络理解卷积、池化、感受野这些概念。想深入自然语言处理可以学循环神经网络和 Transformer理解序列建模和注意力机制。想深入生成模型可以学自编码器和生成对抗网络。但不管往哪个方向走核心的训练流程都是一样的定义模型、定义损失、定义优化器、循环训练、评估验证。你把这一套流程练熟了换任何模型都只是换几个模块的事情。实操心得学深度学习最忌讳只看不练。你看十篇教程不如自己从头写一个完整的训练脚本。遇到报错就查文档、搜解决方案这个过程才是真正长本事的时候。我当初学的时候一个简单的 MNIST 分类写了三遍才跑通但跑通之后后面学更复杂的模型就快多了。最后分享一个小技巧养成写实验记录的习惯。每次训练用了什么超参数、结果怎么样、遇到了什么问题、怎么解决的都记下来。过一段时间回头看你会发现很多当时觉得莫名其妙的问题其实都有规律可循。这个习惯我坚持了好几年受益良多。
阅读完成 · 觉得有帮助?
咨询建站