news 2026/10/8 3:01:10

PyTorch张量与矩阵运算:从线性代数到神经网络的实操指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PyTorch张量与矩阵运算:从线性代数到神经网络的实操指南

读《动手学深度学习》之前,我对线性代数的印象停留在大学期末试卷上——行列式、逆矩阵、特征值,考完就忘。直到在 PyTorch 里写第一个线性层,看着数据的形状从[2, 4]变成[2, 1],我才真正理解书里反复强调的一句话:深度学习模型的每一次前向计算,本质上都是一连串的矩阵运算在搬运数据。

这篇内容是我把书中"线性代数"一章重刷了三遍之后的实操笔记,重点解决三个问题:PyTorch 环境怎么一次性配好不再折腾、张量和矩阵运算在代码里该怎么写、以及最关键的——这些数学概念跟神经网络到底是什么关系。如果你刚装好 PyTorch,准备认真啃这本书,这篇应该能帮你少走不少弯路。

1. 先跑通 PyTorch 环境:conda、CUDA 版本和第一个 import

1.1 用 conda 创建独立环境,别再往基础环境里堆包

我见过太多人装 PyTorch 踩坑,根源只有一个:直接把包装进了 Anaconda 的 base 环境,装到一半发现跟某个旧包冲突,又不敢删。正确做法是给《动手学深度学习》单独开一个环境,互不干扰。

conda create -n d2l python=3.10 conda activate d2l

Python 版本我推荐 3.10,不是越新越好。3.11、3.12 虽然出来了,但一些第三方库的预编译 wheel 可能还没跟上,3.10 是目前兼容性最稳的区间。

激活环境之后,用conda install还是pip install?我的习惯是 PyTorch 相关一律用 pip,因为它能直接到 PyTorch 官方源拉取对应 CUDA 版本的包,conda 源有时候会滞后。

pip install torch torchvision

这里多说一句:如果你用的是 NVIDIA 显卡,纯 CPU 版和 GPU 版的差别非常大。一个 epoch 的训练,GPU 可能几秒钟就跑完,CPU 得上分钟。去 PyTorch 官网的 Get Started 页面,选择你的操作系统、包管理方式和 CUDA 版本,页面会直接生成安装命令。

1.2 验证安装正确性,别急着写模型代码

装完第一件事不是写神经网络,而是先验证三件事:版本号、CUDA 是否可用、基础矩阵运算是否正常。

import torch print(torch.__version__) # 2.x.x print(torch.cuda.is_available())# True 才有意义 print(torch.zeros(2, 3)) # 矩阵创建是否正常

torch.cuda.is_available()返回True才代表 GPU 能被 PyTorch 调用。如果返回False,最常见的原因是 CUDA 驱动和 PyTorch 编译时的 CUDA 版本不匹配。新版本 PyTorch 对 CUDA 12.8 的支持已经不错了,装之前看一眼自己显卡驱动支持的 CUDA 版本,再选对应的安装命令。

还有一个高频报错值得提:在 Windows 的 PowerShell 或 cmd 里输入conda提示"无法将 conda 项识别"。这不是你装错了,是 conda 的可执行文件路径没有加到系统 PATH 里。两个解法:一是永远从 Anaconda Prompt 进入终端,这个入口自带环境变量;二是手动把C:\ProgramData\Anaconda3\Scripts加进 PATH。

提示:环境配置阶段遇到任何报错,先看最后一行红色文字,大多数情况下错误原因就藏在最后一句里,不用急着搜全文。

2. 从标量到高维张量:shape 是深度学习的头等大事

2.1 张量创建:每种写法都有对应场景

线性代数里你学的是标量、向量、矩阵,PyTorch 里统一叫张量(Tensor)。表面是换了个名字,实际是维度变了:标量是 0 维张量,向量是 1 维,矩阵是 2 维,再往上就是深度学习里真正的主角——高维张量。

# 标量:一个数,0 维 scalar = torch.tensor(3.0) # 向量:一维,比如一个样本的 4 个特征 vector = torch.arange(4) # tensor([0, 1, 2, 3]) # 矩阵:二维,比如 3 个样本,每个样本 4 个特征 matrix = torch.randn(3, 4) # shape [3, 4] # 高维张量:批量图片就是典型 images = torch.randn(2, 3, 32, 32) # 2 张图,3 通道(RGB),32x32 像素

最后那个[2, 3, 32, 32]是深度学习最常见的格式:批量大小、通道数、高、宽。初学最容易在这里懵——为什么一张图要用四维表示?因为一张彩色图片有三个颜色通道,每个通道是一个二维像素矩阵,再加上一次处理多张图片,维度自然就叠上去了。

2.2 形状操作是基本功:reshape、view、squeeze、unsqueeze

书里反复强调 shape 的重要性,真不是小题大做。深度学习中 80% 的报错都是 shape 不匹配,剩下的 20% 是不匹配但没报错。

x = torch.arange(12) print(x.shape) # torch.Size([12]) print(x.reshape(3, 4)) # 变成 3 行 4 列 # 增加一个维度:常用于给单样本加 batch 维 y = torch.tensor([1.0, 2.0, 3.0]) print(y.unsqueeze(0).shape) # torch.Size([1, 3]) print(y.unsqueeze(1).shape) # torch.Size([3, 1]) # 去掉长度为 1 的维度 z = torch.randn(1, 3, 1, 4) print(z.squeeze().shape) # torch.Size([3, 4])

reshape和view看着像,底层逻辑完全不同。view要求张量在内存中是连续存储的,不连续就报错;reshape更宽容,返回的可能是一个拷贝。我的建议是:不清楚是否连续时用reshape,安全第一。

2.3 广播机制:不同 shape 的张量也能做运算

广播可能是新手遇到的第一个"反直觉但合理"的机制。按线性代数的规则,两个矩阵相加要求维度完全一致,但 PyTorch 允许不同 shape 的张量做逐元素运算:

a = torch.ones(3, 1) # shape [3, 1] b = torch.ones(1, 4) # shape [1, 4] print(a + b) # 结果是 [3, 4]

这个过程分两步:先把[3, 1]横向扩展成[3, 4],再把[1, 4]纵向扩展成[3, 4],然后逐元素相加。你不需要真的把数据复制一遍,PyTorch 在底层做了优化。

这个机制在归一化操作里特别常见。比如你有[batch, features]的特征矩阵,要按样本维度减均值,均值向量的 shape 是[batch],两者直接相减就会触发广播。

注意:广播不是万能的。[3, 4]和[2, 4]相加会直接报错,因为维度 3 和 2 不是"其中一个为 1"的关系。看到 shape 不匹配的报错时,先检查两个张量从右往左的每个维度是否满足"相等或其中一个为 1"。

3. 矩阵运算的直觉:点积是相似度,矩阵乘法是数据变换

3.1 三个容易搞混的函数:dot、mv、mm

PyTorch 里的矩阵乘法函数有好几个,初学最容易混淆。我踩过的坑是这样的:

  • torch.dot(x, y):两个一维向量的点积,结果是一个标量。要求长度一样。
  • torch.mv(A, x):矩阵和向量相乘,A是二维、x是一维,结果是向量。
  • torch.mm(A, B):两个矩阵相乘,标准的线性代数矩阵乘法。
x = torch.tensor([1.0, 2.0]) y = torch.tensor([3.0, 4.0]) A = torch.randn(2, 3) B = torch.randn(3, 2) print(torch.dot(x, y)) # tensor(11.) print(torch.mv(A, x)) # A 是 [2, 3],x 是 [3],结果是 [2] print(torch.mm(A, B)) # A 是 [2, 3],B 是 [3, 2],结果是 [2, 2]

3.2 矩阵乘法的形状变化,是最实用的技能

矩阵乘法形状匹配规则只有一条:A[m, k]乘B[k, n],结果C[m, n],即左边的列数必须等于右边的行数。

深度学习里怎么理解这件事?把一个矩阵看成"一组样本",另一个矩阵看成"线性变换",矩阵乘法就是把这组样本整体映射到新的空间。以X = [batch_size, features]为例,乘一个W = [features, hidden]的权重矩阵,结果[batch_size, hidden]表示每个样本经过变换后获得了新的 hidden 维特征。

我用一个具体例子帮你建立直觉。假设 4 个学生考了 3 门试,成绩矩阵X是[4, 3],现在想按"平时分占 30%、期中占 30%、期末占 40%"算出总评,权重向量w是[3],那么X @ w就是[4],每个元素对应一个学生的加权总评成绩。这个例子几乎就是神经网络线性层的本质——给特征加权求和。

3.3 点积的深层含义:向量相似度

点积不只是算术运算,它还衡量两个向量的相似程度。两个向量方向越接近,点积越大;垂直时点积为 0;方向相反时点积为负。

这个性质在深度学习里直接喂出了注意力机制。Transformer 里计算 Query 和 Key 的匹配分数,用的就是Q @ K.T:每个 Query 和所有 Key 做点积,得到一个相似度矩阵,再经过 softmax 变成权重。理解了点积的几何意义,再回头看注意力机制就会豁然开朗。

query = torch.randn(2, 4) # 2 个查询 key = torch.randn(5, 4) # 5 个键 scores = query @ key.T # [2, 5],每个查询与所有键的相似度

3.4 Hadamard 积和范数:看似简单却在模型里无处不在

逐元素乘法(Hadamard 积)用*操作符,要求两个张量形状完全一致,对应元素相乘。它在深度学习中用来做掩码操作:生成一个 0/1 掩码矩阵并与目标矩阵逐元素相乘,就能实现"只保留某些位置的值",Dropout 就是这么干的——随机生成 0/1 掩码,把一部分神经元"关掉"。

范数衡量的是向量长度。L2 范数是最常用的,公式是平方和的平方根。PyTorch 里用torch.norm(x)就能直接算。

为什么 L2 范数和深度学习关系紧密?因为这正是 L2 正则化的底层逻辑。模型过拟合时,权重往往会长得很大,正则化就是在损失函数里加一项权重的 L2 范数平方,强迫模型保持权重较小:

# 手动实现 L2 正则化项 reg_loss = torch.norm(model.weight) ** 2 # 用 PyTorch 内置方法:在优化器里直接设置 weight_decay optimizer = torch.optim.SGD(model.parameters(), lr=0.01, weight_decay=0.001)

weight_decay参数的意义就是:每次更新权重时,额外往零方向拉一点。热词里很多人搜"深度学习 L2 正则化 pytorch 代码",其实就是这一行配置的事。

4. 从 y = XW + b 到神经网络:线性变换为什么撑起整个深度学习

4.1 线性层做的事情,就是线性代数书里的线性变换

我当初学线性代数完全没概念:线性变换到底有什么用?直到把它对应到神经网络的一层才反应过来。一个线性层做的事就是y = XW + b:

  • X是输入,shape 是[batch, in_features]
  • W是权重,shape 是[in_features, out_features]
  • b是偏置,shape 是[out_features]
  • y是输出,shape 是[batch, out_features]

如果从几何视角看,W这个矩阵做的事就是:对输入空间进行旋转、缩放、翻转。偏置b则是平移——把整个空间平移一段距离。没有偏置,变换就永远通过原点,模型表达能力打折。

import torch.nn as nn layer = nn.Linear(in_features=4, out_features=3) x = torch.randn(2, 4) # 2 个样本,每个 4 维 y = layer(x) # shape [2, 3] print(layer.weight.shape) # [3, 4],注意是 [out, in] print(layer.bias.shape) # [3]

注意nn.Linear的权重 shape 是[out_features, in_features],跟数学公式里习惯写的[in, out]相反。这就是为什么有时候你手动实现线性层时会发现维度对不上——转置一下就好。

4.2 多个线性层叠加,为什么还需要激活函数

线性代数的基本结论之一:有限个线性变换复合,结果仍然是线性变换。这意味着如果你把几个线性层直接串起来,无论叠多少层,整体上还是等价于一个线性层,那"深度"就没有意义了。

这就是激活函数出现的原因:在每个线性层之间插入非线性变换(ReLU、sigmoid、tanh),打破线性叠加的限制。现代深度学习中,一个多层感知机块长这样:

def mlp_block(in_dim, out_dim): return nn.Sequential( nn.Linear(in_dim, out_dim), nn.ReLU() )

ReLU做的事情极简:负数变 0,正数保留。但正是这个简单的非线性操作,让神经网络从"只能拟合直线"变成"能拟合任何函数"。

4.3 从线性代数的角度看 softmax 回归

《动手学深度学习》第一个完整模型就是 softmax 回归,它本质上是:一个线性层 + softmax 操作。线性层把输入映射到类别得分,softmax 把得分转换成概率分布。

假设 10 个类别,通过线性层得到 10 个原始分数,softmax 做两件事:先对分数取指数(保证非负),再归一化(保证加起来等于 1)。矩阵运算来看就是:

logits = x @ W + b # [batch, num_classes] probs = torch.softmax(logits, dim=1) # [batch, num_classes]

dim=1表示在第 1 维(类别维)上做归一化。这也是新手容易踩的一个点:softmax 的维度选错,结果就会变成"每行不是 1"或者对称位置的数字串了。

4.4 手写一个最简单的训练循环,观察线性层的学习过程

理论说得再多,不如跑一个最小例子。我建议你在学完线性代数这一章后,立刻做这个实验:

import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset # 生成一份线性关系的合成数据:y = 2*x1 - 1.5*x2 + 0.5 torch.manual_seed(42) X = torch.randn(1000, 2) y = 2 * X[:, 0] - 1.5 * X[:, 1] + 0.5 + torch.randn(1000) * 0.1 model = nn.Linear(2, 1) # 单个线性层 loss_fn = nn.MSELoss() optimizer = torch.optim.SGD(model.parameters(), lr=0.01) dataset = TensorDataset(X, y) loader = DataLoader(dataset, batch_size=64) for epoch in range(10): for xb, yb in loader: optimizer.zero_grad() pred = model(xb).squeeze() loss = loss_fn(pred, yb) loss.backward() optimizer.step() print(f"epoch {epoch + 1}, loss = {loss.item():.4f}") print("训练后的权重:", model.weight.detach().numpy()) print("训练后的偏置:", model.bias.detach().numpy())

训练结束后,你会发现学到的权重接近[2.0, -1.5]、偏置接近[0.5]。这个实验最有价值的地方在于让你看到:所谓"机器学习",就是用一个线性层去拟合数据的线性规律,权重通过梯度下降不断调整,最终逼近真实参数。整个过程全部建立在线性代数之上。

5. 向量化思维与自动求导:体会 PyTorch 的 backward 机制

5.1 为什么 PyTorch 不需要你手动写梯度

学过微积分的人都知道,求解多层复合函数的梯度是一件极其繁琐的事情。PyTorch 的自动求导(autograd)机制把这个过程自动化了:它维护一张计算图,记录每个张量是怎么算出来的。当你调用loss.backward()时,梯度沿着计算图反向传播,链式法则被逐节点执行。

听起来像魔法,但底层的数学仍然是线性代数。考虑一个线性层y = XW + b,损失函数如 MSE,反向传播时你需要计算d_loss / d_W,即损失对权重矩阵的偏导。这个过程如果用数学语言推导,涉及雅可比矩阵的连乘。但 PyTorch 做了一个聪明的折中:它不显式计算雅可比矩阵,而是为每个参数直接更新梯度张量,形状和参数完全一致。

model = nn.Linear(2, 1) loss_fn = nn.MSELoss() x = torch.randn(1, 2) target = torch.randn(1) loss = loss_fn(model(x), target) loss.backward() # 查看梯度 print(model.weight.grad) # shape [1, 2] print(model.bias.grad) # shape [1]

这个机制的核心要求是:backward()只能从标量开始反向传播。为什么必须是标量?因为损失函数定义的是"一个数值",衡量预测与真实值的差距。如果你对一个向量调用backward(),必须传入grad_tensors参数来指定每个分量的权重,否则会直接报错。这也是每次训练循环里loss都是标量的原因。

5.2 梯度清零、梯度裁剪、梯度爆炸:三个绕不开的实操细节

第一个细节:梯度累加。调用backward()后,梯度不是被覆盖,而是累加到.grad上。所以每个训练循环开始前都要执行optimizer.zero_grad(),清空上一步的梯度。如果不做这一步,梯度会在多轮之间累加,训练结果完全错乱。

第二个细节:梯度裁剪。深层网络或 RNN 训练时,梯度可能变得非常大,导致参数更新幅度过大,损失值跳到 NaN。解决办法是设置梯度上限:

torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

这句代码把所有参数的梯度范数裁剪到不超过 1.0。很多人在训练 Transformer 或 LSTM 时遇到过 NaN 损失,第一反应是调学习率,但更可能是梯度爆炸,试试裁剪往往立竿见影。

第三个细节:no_grad上下文。验证集或测试集前向推理时,不需要计算梯度。用with torch.no_grad():包起来,可以省内存、提速。这个操作不影响结果,因为推理本来就不需要反向传播。

# 验证阶段 model.eval() with torch.no_grad(): val_pred = model(val_X)

5.3 向量化是 GPU 发挥威力的前提

学线性代数这一章时,你可能会觉得torch.mm(A, B)和 for 循环逐行算没啥区别。区别太大了。

import time A = torch.randn(1000, 1000) B = torch.randn(1000, 1000) # 循环方式 start = time.time() result_loop = torch.empty(1000, 1000) for i in range(1000): result_loop[i] = torch.mv(A, B[i]) print("循环耗时:", time.time() - start) # 向量化方式 start = time.time() result_matrix = torch.mm(A, B) print("矩阵乘法耗时:", time.time() - start)

在 CPU 上这个差距通常是几十倍,GPU 上更夸张。原因很好理解:矩阵乘法是高度规则化的运算,底层可以调用高度优化的 BLAS 库,并且天然适合 GPU 的并行架构。GPU 有几千个核心同时计算,而 for 循环只能一个接一个跑。

这也是《动手学深度学习》整本书反复强调向量化思维的原因。写代码时看到 for 循环,先想想能不能换成矩阵运算——不要只是处理二维矩阵,也要习惯三维、四维张量整体的运算。一开始觉得抽象,但一旦从"逐元素"上升到"逐张量",你才能真正发挥 PyTorch 的效率。

5.4 一个组合实验:在线性拟合中加入 L2 正则化

把正文的线性拟合实验升级一下:给模型手动加上 L2 正则化项,观察权重的变化,这是检验你对线性代数和自动求导理解是否到位的好方式。

model = nn.Linear(2, 1) lambda_reg = 0.1 for epoch in range(20): optimizer.zero_grad() pred = model(X).squeeze() mse_loss = loss_fn(pred, y) l2_penalty = torch.norm(model.weight) ** 2 # 权重矩阵的 L2 范数平方 total_loss = mse_loss + lambda_reg * l2_penalty total_loss.backward() optimizer.step() print(f"epoch {epoch + 1}, loss = {total_loss.item():.4f}")

你会发现,加入正则化后,权重不再精确等于[2.0, -1.5],而是略微偏小,偏置也受到轻微压制。这就是正则化的效果:用一点点拟合精度,换取权重的稳定性和泛化能力。这一坨代码用到了本篇文章讲过的所有核心内容:张量形状、矩阵乘法、范数、自动求导、优化器更新。跑通之后,你对"线性代数在深度学习里到底有什么用"这个问题,应该已经有了自己的答案。

我在实际学习中有个体会:这章内容与其说在教数学,不如说在教你一种思维方式——把数据看作张量,把层看作变换,把训练看作优化张量参数。每个抽象的数学概念都能在代码里找到一个对应的函数,遇到不理解的公式,先写两行 PyTorch 跑一遍,把输入输出 shape 打出来,往往比死磕推导过程更有效。

最后分享一个小技巧:遇到维度对不上的报错时,别急着搜博客,随手拿张纸把两个张量的 shape 写出来,从右往左逐个维度比对,哪一对既不等值其一也不是 1,问题就出在哪。五分钟内基本能定位,比翻十篇教程都快。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 2:59:50

可编程的编程语言:Racket宏与#lang实战

“可编程的编程语言”这六个字第一次砸到我时,我是有点想抬杠的。什么编程语言不可编程?Java里我也能写个解释器,Python里也能做元编程,连C都有函数指针。直到我在Racket里真正做完一轮语言扩展,才理解这个说法的分量&…

作者头像 李华
网站建设 2026/10/8 2:59:16

TensorFlow 2.0/Keras实战入门:从环境搭建到训练第一个神经网络模型

写这篇教程的念头,其实是被身边好几个朋友问出来的。他们想学Python深度学习,一上来就被“TensorFlow还是PyTorch”的选择题卡住,接着又卡在环境安装上,最后连门都没摸到就放弃了。这篇文章不折腾框架之争,直接聚焦一条…

作者头像 李华
网站建设 2026/10/8 2:59:13

归并排序原理与Java实现:从分治思想到JDK排序优化

很多 Java 开发者对排序的印象停留在“调 Arrays.sort() 就完事”,但一旦面试官问起“归并排序的原理是什么”或者让你“手写一个归并排序”,不少人会卡在 merge 那一步。这不是基础不牢,而是平时只看结论不拆过程。归并排序恰恰是所有主流排…

作者头像 李华
网站建设 2026/10/8 2:55:39

Android+Java毕业设计实战指南:一套骨架搞定4S店与公交查询系统

每年到了毕业设计选题的时候,总有学弟学妹拿着类似的题目来问我:汽车4S店管理系统、公交实时通、车来了动态速查……乍一看是三个完全不相干的题目,但把需求拆开就会发现,它们的内核高度一致:Android端做交互界面&…

作者头像 李华
网站建设 2026/10/8 2:55:18

风光互补制氢合成氨容量-调度双层优化与Cplex求解

最近在复现一篇关于风光互补制氢合成氨系统的容量-调度优化论文,用的求解器是Cplex,代码环境是Matlab。断断续续啃了两周,踩了好些坑,也把整个系统的建模逻辑捋清楚了。这篇文章就把这次复现的完整思路、模型构建、Cplex接入方式和…

作者头像 李华
网站建设 2026/10/8 2:55:14

MySQL索引下推原理详解:从回表代价到联合索引优化实践

MySQL索引下推这个优化很多人只是听过名字,知道是MySQL 5.6引入的新特性,但真要问它到底怎么工作、什么时候能帮你省时间、什么情况下它根本帮不上忙,能讲清楚的人就不多了。我最早接触ICP的时候也是糊里糊涂,光知道执行计划里出现…

作者头像 李华