1 简介
下图是深度学习框架从2000年左右萌芽阶段开始的发展脉络;
- 萌芽阶段像torch等框架存在API复杂、无GPU支持等不足;
- 到成长阶段,出现了Caffe、Chainer等,开始支持多GPU和复杂网络;
- 稳定阶段有PyTorch、TensorFlow等,具备自动微分等特性;
- 深化阶段则注重编译层优化等多方面;
- PyTorch在这个过程中不断发展,在稳定阶段及之后都有重要地位,未来还将面向大模型等趋势发展;
PyTorch是一个Python深度学习框架,核心是用张量(Tensor)来处理数据,张量是同类型数据的多维矩阵,而且在PyTorch里张量是以类的形式存在,运算方法也封装在类里,方便用户操作;
2 张量的创建
先查看自己的 CUDA 版本:
nvcc -V
再在官网查看合适的 torch 版本:Previous PyTorch Versions;
导包:
import torch import numpy as np
2.1 基本创建方式
torch.tensor()
:根据指定数据创建张量# 创建一个包含单个元素10的标量张量 data = torch.tensor(10) print(data) # 使用numpy生成一个2行3列的随机数数组(符合标准正态分布) data = np.random.randn(2, 3) # 将numpy数组转换为PyTorch张量 data = torch.tensor(data) print(data) # 定义一个Python列表(包含两个子列表,每个子列表有3个浮点数) data = [[10., 20., 30.], [40., 50., 60.]] # 将Python列表转换为PyTorch张量 data = torch.tensor(data) print(data)
torch.Tensor()
:根据指定形状创建张量,也可以用来创建指定数据的张量# 创建一个2行3列的未初始化张量(默认数据类型为float32) # 注意:未初始化的张量会包含内存中的随机值 data = torch.Tensor(2, 3) print(data) # 从包含一个元素的列表创建张量(默认数据类型为float32) data = torch.Tensor([10]) print(data) # 从包含两个元素的列表创建张量(默认数据类型为float32) data = torch.Tensor([10, 20]) print(data)
torch.IntTensor()
、torch.FloatTensor()
、torch.DoubleTensor()
创建指定类型的张量# 创建一个2行3列的未初始化整数张量(数据类型为int32) data = torch.IntTensor(2, 3) print(data) # 从包含浮点数的列表创建整数张量(会自动截断小数部分) data = torch.IntTensor([2.5, 3.3]) print(data) # 创建不同数据类型的空张量 # data = torch.ShortTensor() # 16位整数张量 # data = torch.LongTensor() # 64位整数张量 # data = torch.FloatTensor()# 32位浮点数张量(PyTorch默认浮点数类型) # data = torch.DoubleTensor()# 64位浮点数张量
2.2 创建线性和随机张量
torch.arange()
和torch.linspace()
创建线性张量# 使用torch.arange创建从0开始,到10结束(不包含10),步长为2的一维张量 # 相当于生成序列:0, 2, 4, 6, 8 data = torch.arange(0, 10, 2) print(data) # 使用torch.linspace创建从0到9(包含9)的10个等间隔的数值组成的张量 # 相当于将0到9的区间平均分成9段,取10个端点值 data = torch.linspace(0, 9, 10) print(data)
torch.random.init_seed()
和torch.random.manual_seed()
用于设置随机种子torch.randn()
创建随机张量# 使用torch.randn创建一个2行3列的张量,元素值服从标准正态分布(均值为0,标准差为1) data = torch.randn(2, 3) # 打印这个随机生成的2x3张量(每次运行结果可能不同) print(data) # 打印当前的随机数种子,随机数种子用于控制随机数生成,相同种子会产生相同的随机序列 print('随机数种子:', torch.random.initial_seed()) # 手动设置随机数种子为100,确保后续随机操作可复现 torch.random.manual_seed(100) # 使用相同种子生成2x3的标准正态分布张量,每次设置种子100后运行,结果都相同 data = torch.randn(2, 3) # 打印设置种子后生成的张量 print(data) # 打印当前的随机数种子,确认已设置为100 print('随机数种子:', torch.random.initial_seed())
2.3 创建0-1张量
torch.ones()
和torch.ones_like()
创建全1张量# 创建一个2行3列的张量,所有元素都为0(默认数据类型为float32) data = torch.zeros(2, 3) print(data) # 创建一个与data形状相同的全0张量(形状为2x3,数据类型与原张量一致) data = torch.zeros_like(data) print(data)
torch.zeros()
和torch.zeros_like()
创建全0张量# 创建一个2行3列的张量,所有元素都为1(默认数据类型为float32) data = torch.ones(2, 3) print(data) # 创建一个与data形状相同的全1张量(形状为2x3,数据类型与原张量一致) data = torch.ones_like(data) print(data)
torch.full()
和torch.full_like()
创建全为指定值张量# 创建一个2行3列的张量,所有元素都为指定值10(默认数据类型为int64) data = torch.full([2, 3], 10) print(data) # 创建一个与data形状相同的张量,所有元素都为指定值20(数据类型与原张量一致) data = torch.full_like(data, 20) print(data)
2.4 张量的数据类型转换
data.type(torch.DoubleTensor)
# 创建一个2行3列、元素值都为10的张量 # 当填充整数时,torch.full默认数据类型为torch.int64(长整型) data = torch.full([2, 3], 10) # 打印张量的数据类型,输出: torch.int64 print(data.dtype) # 使用type()方法将张量数据类型转换为torch.DoubleTensor(即float64,双精度浮点型) data = data.type(torch.DoubleTensor) # 打印转换后的数据类型,输出: torch.float64 print(data.dtype) # 以下是其他常用数据类型转换的示例 # data_short = data.type(torch.ShortTensor) # 转换为int16(短整型) # data_int = data.type(torch.IntTensor) # 转换为int32(整型) # data_long = data.type(torch.LongTensor) # 转换为int64(长整型) # data_float = data.type(torch.FloatTensor) # 转换为float32(单精度浮点型)
data.double()
# 重新创建一个2行3列、元素值都为10的张量(默认int64类型) data = torch.full([2, 3], 10) # 打印当前数据类型,输出: torch.int64 print(data.dtype) # 使用double()方法将张量转换为float64类型(与torch.DoubleTensor等价) # 这是一种更简洁的类型转换方式 data = data.double() # 打印转换后的数据类型,输出: torch.float64 print(data.dtype) # 以下是其他常用数据类型转换的简洁方法(注释状态) # data = data.short() # 转换为int16 # data = data.int() # 转换为int32 # data = data.long() # 转换为int64 # data = data.float() # 转换为float32
3 张量的类型转换
3.1 张量转换为NumPy数组
使用
Tensor.numpy()
函数可以将张量转换为ndarray
数组,但是共享内存,可以使用copy()
函数避免共享;# 创建一个包含[2, 3, 4]的PyTorch张量 data_tensor = torch.tensor([2, 3, 4]) # 使用.numpy()方法将PyTorch张量转换为NumPy数组 # 注意:此时的NumPy数组与原张量共享内存 data_numpy = data_tensor.numpy() # 打印原对象类型:<class 'torch.Tensor'> print(type(data_tensor)) # 打印转换后对象类型:<class 'numpy.ndarray'> print(type(data_numpy)) # 修改NumPy数组的第一个元素为100 # 由于共享内存,原PyTorch张量也会受到影响 data_numpy[0] = 100 # 打印原张量,结果变为:tensor([100, 3, 4]) print(data_tensor) # 打印修改后的NumPy数组,结果为:[100 3 4] print(data_numpy)
# 重新创建一个包含[2, 3, 4]的PyTorch张量 data_tensor = torch.tensor([2, 3, 4]) # 使用.numpy().copy()将张量转换为NumPy数组并创建副本 # 此时的NumPy数组与原张量不共享内存,是独立的副本 data_numpy = data_tensor.numpy().copy() # 打印原对象类型:<class 'torch.Tensor'> print(type(data_tensor)) # 打印转换后对象类型:<class 'numpy.ndarray'> print(type(data_numpy)) # 修改副本NumPy数组的第一个元素为100 # 由于是独立副本,原PyTorch张量不受影响 data_numpy[0] = 100 # 打印原张量,结果保持不变:tensor([2, 3, 4]) print(data_tensor) # 打印修改后的NumPy数组,结果为:[100 3 4] print(data_numpy)
3.2 NumPy数组转换为张量
使用
from_numpy()
可以将ndarray
数组转换为Tensor
,默认共享内存,使用copy()
函数避免共享;data_numpy = np.array([2, 3, 4]) data_tensor = torch.from_numpy(data_numpy) print(type(data_numpy)) print(type(data_tensor)) data_tensor[0] = 100 print(data_tensor) print(data_numpy)
使用
torch.tensor()
可以将ndarray
数组转换为Tensor
,默认不共享内存;data_numpy = np.array([2, 3, 4]) data_tensor = torch.tensor(data_numpy) print(type(data_numpy)) print(type(data_tensor)) data_tensor[0] = 100 print(data_tensor) print(data_numpy)
3.3 标量张量和数字转换
对于只有一个元素的张量,可以使用
item()
函数将该值从张量中提取出来;data = torch.tensor([30,]) print(data.item()) data = torch.tensor(30) print(data.item())
4 张量数值计算
4.1 张量基本运算
加、减、乘、除、取负号:
add()
、sub()
、mul()
、div()
、neg()
;带下划线的版本会修改原数据:
add_()
、sub_()
、mul_()
、div_()
、neg_()
;# 创建一个2行3列的张量,元素是从0到10(不包含10)之间的随机整数 data = torch.randint(0, 10, [2, 3]) print(data) # 不修改原数据 new_data = data.add(10) # 等价于:new_data = data + 10 print(new_data) # 直接修改原数据 data.add_(10) # 等价于: data += 10 print(data) print(data.sub(100)) print(data.mul(100)) print(data.div(100)) print(data.neg())
4.2 张量点乘运算
点乘(Hadamard)指的是两个同维矩阵对应位置的元素相乘;
使用
mul()
或运算符*
实现;data1 = torch.tensor([[1, 2], [3, 4]]) data2 = torch.tensor([[5, 6], [7, 8]]) data = torch.mul(data1, data2) print(data) data = data1 * data2 print(data)
4.3 张量矩阵乘法运算
在标准的矩阵乘法运算中,第一个矩阵的形状为
(n, m)
(即有n
行m
列),第二个矩阵的形状需要为(m, p)
(即有m
行p
列)。这样两个矩阵相乘后得到的结果矩阵形状为(n, p)
(有n
行p
列)。这是基于矩阵乘法的定义,第一个矩阵的列数必须和第二个矩阵的行数相等,这样才能进行乘法运算;在 PyTorch 中,运算符
@
被用于进行两个矩阵的乘积运算。例如,如果有两个张量a
和b
,它们满足矩阵乘法的形状要求,那么a @ b
就会得到它们的乘积张量;torch.matmul
函数用于进行矩阵乘法运算。它和运算符@
有相似之处,但也有不同;- 对于普通的二维矩阵乘法,
torch.matmul
和@
的作用是一样的; - 但
torch.matmul
更加灵活,它对进行乘积运算的两矩阵形状没有像标准矩阵乘法那样严格的限定;- 当输入的是形状不同的张量时,只要对应的最后几个维度符合矩阵运算规则就可以进行运算;
- 例如,对于三维及以上的张量,
torch.matmul
会在最后的两个维度上进行矩阵乘法运算,而前面的维度则会被视为批量维度;
- 对于普通的二维矩阵乘法,
例:
- 假设有两个二维张量
a
和b
,a
的形状是(2, 3)
,b
的形状是(3, 4)
,那么a @ b
或者torch.matmul(a, b)
得到的结果形状是(2, 4)
,这符合标准的矩阵乘法规则; - 如果有两个三维张量
a
形状为(5, 2, 3)
,b
形状为(5, 3, 4)
,那么torch.matmul(a, b)
会得到一个形状为(5, 2, 4)
的张量,这里的5
就是批量维度,在每一个批量上进行(2, 3)
和(3, 4)
的矩阵乘法运算;
data1 = torch.tensor([[2, 3], [3, 4]]) data2 = torch.tensor([[5, 2, 3], [5, 3, 4]]) # 方式一: data3 = data1 @ data2 print(data3) print("data3形状:", data3.shape) # 方式二: data4 = torch.matmul(data1, data2) print(data4) print("data4形状:", data4.shape)
- 假设有两个二维张量
5 张量运算函数
PyTorch 为每个张量封装很多实用的计算函数:均值、平方根、求和、指数计算、对数计算等;
data = torch.randint(0, 10, [2, 3], dtype=torch.float64) print(data) # 1. 计算均值 # 注意: tensor 必须为 Float 或者 Double 类型 print(data.mean()) print(data.mean(dim=0)) # 按列计算均值 print(data.mean(dim=1)) # 按行计算均值 # 2. 计算总和 print(data.sum()) print(data.sum(dim=0)) print(data.sum(dim=1)) # 3. 计算平方 print(torch.pow(data, 2)) # 4. 计算平方根 print(data.sqrt()) # 5. 指数计算, e^n 次方 print(data.exp()) # 6. 对数计算 print(data.log()) # 以 e 为底 print(data.log2()) print(data.log10())
6 张量索引操作
准备数据:
data = torch.randint(0, 10, [4, 5]) print(data)
6.1 简单行列索引
print(data[0]) # 第1行
print(data[:, 0]) # 第1列
6.2 列表索引
# 返回 (0, 1)、(1, 2) 两个位置的元素
print(data[[0, 1], [1, 2]])
# 返回 0、1 行的 1、2 列共4个元素
print(data[[[0], [1]], [1, 2]])
6.3 范围索引
# 前3行的前2列数据
print(data[:3, :2])
# 第2行到最后一行的前2列数据
print(data[2:, :2])
6.4 布尔索引
# 第三列中大于5的行数据
print(data[data[:, 2] > 5])
# 第二行中大于5的列数据
print(data[:, data[1] > 5])
6.5 多维索引
# [3, 4, 5] 表示这个张量的形状,即有 3 个块,每个块包含 4 行 5 列的数据
data = torch.randint(0, 10, [3, 4, 5])
print(data)
# 获取0轴上的第一个数据
# 0 表示在第 0 个轴(可以理解为块的维度)上选择索引为 0 的块
# : 表示在对应的轴上选择所有的元素。所以这行代码的作用是获取第 0 个块的所有行和所有列的数据
print(data[0, :, :])
# 获取1轴上的第一个数据
# : 表示在第 0 个轴上选择所有的块
# 0 表示在第 1 个轴(行的维度)上选择索引为 0 的行
# : 表示在第 2 个轴(列的维度)上选择所有的列。所以这行代码的作用是获取每个块中第 0 行的所有列的数据
print(data[:, 0, :])
# 获取2轴上的第一个数据
# : 表示在第 0 个轴上选择所有的块
# : 表示在第 1 个轴上选择所有的行
# 0 表示在第 2 个轴上选择索引为 0 的列。所以这行代码的作用是获取每个块中所有行的第 0 列的数据
print(data[:, :, 0])
7 张量形状操作
7.1 reshape()
reshape()
函数可以在保证张量数据不变的前提下改变数据的维度,将其转换成指定的形状;data = torch.tensor([[10, 20, 30], [40, 50, 60]]) # 1. 使用 shape 属性或者 size 方法都可以获得张量的形状 # # 使用.shape属性获取张量形状,返回torch.Size([2, 3]) # 同时访问shape的第0维和第1维,分别是2和3 print(data.shape, data.shape[0], data.shape[1]) print(data.size(), data.size(0), data.size(1)) # 2. 使用 reshape() 函数修改张量形状 # 将原张量reshape为1行6列的新张量 # 原张量元素总数为2*3=6,新形状1*6=6,元素总数保持不变 new_data = data.reshape(1, 6) print(new_data.shape)
7.2 squeeze()
和unsqueeze()
squeeze()
函数删除形状为 1 的维度(降维),unsqueeze
函数添加形状为1的维度(升维);mydata1 = torch.tensor([1, 2, 3, 4, 5]) print('mydata1--->', mydata1.shape, mydata1) # 一个普通的数组,1维数据 # 在第0维(最外层)添加一个维度,将形状从(5,)变为(1, 5) # 相当于在原张量外面套了一层方括号,变成一个包含原张量的二维张量 mydata2 = mydata1.unsqueeze(dim=0) print('在0维度上 拓展维度:', mydata2, mydata2.shape) # 形状变为1行5列 # 在第1维(元素维度)添加一个维度,将形状从(5,)变为(5, 1) # 相当于给每个元素都套上了一层方括号,变成每个元素都是长度为1的数组 mydata3 = mydata1.unsqueeze(dim=1) print('在1维度上 拓展维度:\n', mydata3, mydata3.shape) # 形状变为5行1列 # 在最后一个维度(即第1维,因为原张量是1维的)添加一个维度 # 效果与dim=1相同,形状从(5,)变为(5, 1) mydata4 = mydata1.unsqueeze(dim=-1) print('在-1维度上 拓展维度:\n', mydata4, mydata4.shape) # 形状变为5行1列 # 压缩mydata4中长度为1的维度(这里是第0维) # 将形状从(5, 1)变回(5,) mydata5 = mydata4.squeeze() print('压缩维度:', mydata5, mydata5.shape) # 恢复为原始的一维张量
7.3 transpose()
和permute()
transpose()
函数可以实现交换张量形状的指定维度;例如: 一个张量的形状为
(2, 3, 4)
可以通过transpose()
函数把 3 和 4 进行交换, 将张量的形状变为(2, 4, 3)
;permute()
函数可以一次交换更多的维度;data = torch.tensor(np.random.randint(0, 10, [3, 4, 5])) print('data shape:', data.size()) # 1.交换1和2维度 mydata2 = torch.transpose(data, 1, 2) print('mydata2.shape--->', mydata2.shape) # 2.将data的形状修改为 (4, 5, 3), 需要变换多次 mydata3 = torch.transpose(data, 0, 1) mydata4 = torch.transpose(mydata3, 1, 2) print('mydata4.shape--->', mydata4.shape) # 3.使用 permute() 函数将形状修改为 (4, 5, 3) # 3.1.方法1 mydata5 = torch.permute(data, [1, 2, 0]) print('mydata5.shape--->', mydata5.shape) # 3.2.方法2 mydata6 = data.permute([1, 2, 0]) print('mydata6.shape--->', mydata6.shape)
7.4 view()
和contiguous()
view()
可以修改张量形状,但 前提是张量数据存储在 “整块连续内存” 里。比如一个形状为(2, 3)
(2 行 3 列)的张量,数据在内存里是连续排列的,就可以用view(3, 2)
改成 3 行 2 列;PyTorch 里有些张量,不是 “一整块连续内存” 存的。典型场景是:张量经过
transpose()
(转置)、permute()
(维度重排)后,数据在内存的存储顺序被打乱,变成 “分散块” 组成。这种情况下,view()
无法直接变形,因为它依赖连续内存的逻辑顺序来计算新形状,内存乱了就 “不知道咋变”;例:
- 假设原张量是
(4, 3)
的二维数组,内存里是按行连续存的:[1,2,3, 4,5,6, 7,8,9, 10,11,12]
; - 用
transpose(0, 1)
转置后,逻辑上变成(3, 4)
,但内存里还是原来的顺序(没真正 “物理转置”,只是改了访问逻辑)。这时候直接用view(6, 2)
会报错,因为内存布局和逻辑形状不匹配,view()
不知道怎么从分散的逻辑映射里算出新形状;
- 假设原张量是
如果张量内存不连续(比如转置或
permute()
后),想继续用view()
,可以先调用contiguous()
。它会强制把张量数据复制 / 整理到一块连续内存 ,代价是额外内存和时间,但整理后就能正常用view()
变形了;data = torch.tensor( [[10, 20, 30],[40, 50, 60]]) print('data--->\n', data, data.shape) # 1.判断是否使用整块内存 print(data.is_contiguous()) # True # 2.view() mydata2 = data.view(3, 2) print('mydata2--->\n', mydata2, mydata2.shape) # 3.判断是否使用整块 print(mydata2.is_contiguous()) # 4.使用transpose()函数修改形状 mydata3 = torch.transpose(data, 0, 1) print('mydata3--->\n', mydata3, mydata3.shape) print(mydata3.is_contiguous()) # 5.需要先使用 contiguous() 函数转换为整块内存的张量,再使用view()函数 mydata4 = mydata3.contiguous().view(2, 3) print('mydata4--->', mydata4.shape, mydata4)
8 张量拼接操作
torch.cat()
函数可以将两个张量根据指定的维度拼接起来,不改变维度数;data1 = torch.randint(0, 10, [1, 2, 3]) # 1个块、2行、3列 data2 = torch.randint(0, 10, [1, 2, 3]) print(data1) print(data2) # 1.按0维度拼接 new_data = torch.cat([data1, data2], dim=0) print(new_data) print(new_data.shape) # 2.按1维度拼接 new_data = torch.cat([data1, data2], dim=1) print(new_data) print(new_data.shape) # 3.按2维度拼接 new_data = torch.cat([data1, data2], dim=2) print(new_data) print(new_data.shape)
按
dim=0
拼接:在 “块维度” 合并,原(1,2,3)
+(1,2,3)
→ 拼接后(2,2,3)
(块数从 1 变 2,行、列不变);按
dim=1
拼接:在 “行维度” 合并,原(1,2,3)
+(1,2,3)
→ 拼接后(1,4,3)
(行数从 2 变 4,块数、列数不变);按
dim=2
拼接:在 “列维度” 合并,原(1,2,3)
+(1,2,3)
→ 拼接后(1,2,6)
(列数从 3 变 6,块数、行数不变)。
9 自动微分模块
9.1 概述
训练神经网络时,最常用的算法就是反向传播。在该算法中,参数(模型权重)会根据损失函数关于对应参数的梯度进行调整”;
反向传播(Backpropagation):神经网络训练的核心算法。简单说,先正向计算预测值,再反向从损失出发,算每个参数对损失的影响(梯度),最后用梯度更新参数,让模型预测更准;
梯度(Gradient):损失函数相对于模型参数的导数,代表“参数变一点,损失会怎么变”,是参数更新的依据;
为了计算这些梯度,PyTorch 内置了名为
torch.autograd
的微分引擎。它支持任意计算图的自动梯度计算;torch.autograd
:PyTorch 实现自动微分的核心模块,能自动构建计算图,反向传播时自动算梯度,不用手动推导复杂求导公式;计算图(Computational Graph):把运算过程拆成节点(如乘法、加法、激活函数等)和边(数据流向),方便自动找梯度传播路径;
例:下图里的流程对应一个简单线性模型的正向计算:
符号含义:
x
:输入数据(比如训练样本的特征)w
、b
:模型参数(权重、偏置,虚线框标了Parameters
,说明是要学习、更新的)*
:乘法运算(x
和w
相乘,即x*w
)+
:加法运算(把乘法结果和b
相加,即x*w + b
,得到z
)z
:模型的原始输出(还没和真实标签比)y
:真实标签(训练数据里的标准答案)MSE
:损失函数(Mean Squared Error,均方误差,用来算z
和y
的差距,即loss
)loss
:最终的损失值(衡量模型预测多“差”,训练就是要让它越小越好)
正向与反向:
- 正向:数据从
x
进,经w
加权、b
偏移,得到z
,再和y
算loss
,是“预测”过程; - 反向:从
loss
出发,沿计算图反向走,算loss
对w
、b
的梯度(即∂loss/∂w
、∂loss/∂b
),用于更新参数,是“学习”过程;
- 正向:数据从
实操工具:
backward
和grad
backward()
方法:触发反向传播的“开关”。在 PyTorch 里,对loss
张量调.backward()
,会自动从loss
开始,沿计算图反向算所有参数的梯度;grad
属性:存梯度的地方。比如对w
、b
这些参数,算完梯度后,可通过w.grad
、b.grad
取到,用于后续参数更新(如用优化器optimizer.step()
)。
9.2 例1:标量级别的简单计算
# 1.当X为标量时梯度的计算
def test01():
# 创建标量张量x,值为5,这里x可看作输入标量(比如简单线性模型的单个输入值)
x = torch.tensor(5)
# 创建标量张量y,值为0.,作为目标值(比如真实标签),dtype默认为float32(因用了小数点写法)
y = torch.tensor(0.)
# 创建需要更新的权重张量w,初始值为1.,设置requires_grad=True表示需要计算它的梯度
# dtype指定为torch.float32,确保数据类型,方便后续自动微分计算
w = torch.tensor(1., requires_grad=True, dtype=torch.float32)
# 创建需要更新的偏置张量b,初始值为3.,同样设置requires_grad=True来跟踪梯度
b = torch.tensor(3., requires_grad=True, dtype=torch.float32)
# 定义简单线性变换,计算网络的输出值z,公式为 z = x * w + b ,这里是标量之间的运算
z = x * w + b
# 实例化均方误差(MSELoss)损失函数,用于计算预测值z和目标值y之间的损失
loss = torch.nn.MSELoss()
# 计算损失,将预测值z和目标值y传入损失函数,得到当前的损失值loss
loss = loss(z, y)
# 执行自动微分的反向传播,会根据计算图从loss开始反向计算需要求梯度的张量(w和b)的梯度
loss.backward()
# 打印w变量的梯度,backward后梯度会存储在w.grad属性中
print("W的梯度:", w.grad)
# 打印b变量的梯度,同理,梯度存储在b.grad属性中
print("b的梯度:", b.grad)
test01()
- 逻辑梳理:
- 自动微分基础配置:对
w
和b
设置requires_grad=True
,这是让 PyTorch 跟踪它们梯度的关键开关,后续反向传播时会自动计算并把结果存在.grad
属性里; - 正向计算流程:先通过
z = x * w + b
模拟简单线性模型的预测(标量版),再用MSELoss
算预测z
和真实y
的损失,构建出**“输入→计算→损失”的正向计算图**; - 反向传播与梯度计算:调用
loss.backward()
后,PyTorch 会沿着计算图反向走,用链式法则自动算w
和b
对损失的梯度,结果存在w.grad
、b.grad
,后续就能用这些梯度更新参数(比如梯度下降);
- 自动微分基础配置:对
- 注意:
MSELoss
要求输入z
和y
形状匹配、数据类型一致(这里都是标量float32
,没问题)。
9.3 例2:张量级别的神经网络层模拟
# 神经网络简单正向计算与反向求梯度过程(模拟线性层 + MSE 损失场景)
def test02():
# 输入张量 x,形状是 2 行 5 列,元素值全为 1。这里可理解为模拟 2 个样本,每个样本 5 个特征
x = torch.ones(2, 5)
# 目标值张量 y,形状是 2 行 3 列,元素值全为 0。这里可理解为 2 个样本对应的 3 维真实标签
y = torch.zeros(2, 3)
# 设置要更新的权重张量 w,形状是 5 行 3 列
# requires_grad=True 开启自动微分跟踪,让 PyTorch 记录该张量参与的运算,后续可计算其梯度
w = torch.randn(5, 3, requires_grad=True)
# 设置要更新的偏置张量 b,形状是 3 维(可理解为 1 行 3 列,广播机制会适配运算)
b = torch.randn(3, requires_grad=True)
# 设置网络的输出值 z,先通过 torch.matmul 做矩阵乘法,再加上偏置 b。即x(2,5)和 w(5,3)相乘,得到(2,3)形状的结果,再与 b(3)做广播加法
# 这一步模拟了神经网络中线性层(如全连接层)的计算:z = x@w + b
z = torch.matmul(x, w) + b # 矩阵乘法
# 实例化均方误差(MSELoss)损失函数,用于计算预测值 z 和目标值 y 之间的均方误差
# MSELoss 会计算两个张量对应元素差的平方的均值,常作为回归任务的损失
loss = torch.nn.MSELoss()
# 计算实际损失:将预测输出 z 和目标值 y 传入损失函数,得到当前的损失值
# 这里会构建从输入到损失的计算图,为后续反向传播做准备
loss = loss(z, y)
# 执行自动微分的反向传播过程
# backward 函数会从损失 tensor 开始,沿着计算图反向遍历,
# 计算并存储 requires_grad=True 的张量(w、b)的梯度到它们的 .grad 属性中
loss.backward()
# 打印权重 w 的梯度,backward 计算得到的梯度会存在 w.grad 中
# 梯度反映了损失对 w 中每个元素的影响程度,用于后续参数更新(如梯度下降)
print("W的梯度:", w.grad)
# 打印偏置 b 的梯度,同理,梯度存储在 b.grad 中
print("b的梯度:", b.grad)
test02()
- 关键知识点:
- 张量与形状设计:
- 输入
x
形状(2, 5)
对应 “2 个样本、每个样本 5 维特征”;目标y
形状(2, 3)
对应 “2 个样本、每个样本 3 维标签”; - 权重
w
形状(5, 3)
是为了和x
做矩阵乘法:x(2,5) @ w(5,3) = z(2,3)
,匹配y
的形状,符合线性层维度变换逻辑(输入特征数 5 → 输出特征数 3);
- 输入
- 自动微分核心:
requires_grad=True
是 “开关”,让 PyTorch 跟踪张量参与的运算,构建计算图;loss.backward()
触发反向传播,基于链式法则自动计算梯度,无需手动推导求导公式,大大简化实现;
- 损失函数作用:
MSELoss
量化预测z
和真实y
的差异,给反向传播提供 “优化方向”——梯度是 “损失对参数的变化率”,参数更新会朝着减小损失的方向进行; - 梯度的意义:
w.grad
和b.grad
存储的梯度,是后续优化器(如torch.optim.SGD
)更新参数的依据,比如按w = w - lr * w.grad
(lr 是学习率)的方式调整,让模型预测更接近目标;
- 张量与形状设计:
9.4 test01
VS test02
test01
和test02
虽然核心区别在于处理的数据维度和模拟的场景不同,前者是标量级别的简单计算,后者是张量级别的神经网络层模拟,具体差异如下;数据维度不同
test01
:处理的是标量(0维张量),所有变量(x
、w
、b
、z
、y
)都是单个数值,没有形状概念;- 例如:
x = torch.tensor(5)
(标量)、w = torch.tensor(1.)
(标量);
- 例如:
test02
:处理的是多维张量,所有变量都有明确的形状,模拟真实神经网络的输入输出结构;- 例如:
x = torch.ones(2, 5)
(2行5列的二维张量,代表2个样本、每个样本5个特征)、w = torch.randn(5, 3)
(5行3列的权重矩阵);
- 例如:
运算逻辑不同
test01
:执行标量运算;- 计算过程是简单的数值计算:
z = x * w + b
(单个数值的乘法和加法);
- 计算过程是简单的数值计算:
test02
:执行张量运算,核心是矩阵乘法(模拟神经网络的全连接层);z = torch.matmul(x, w) + b
,其中x(2,5) @ w(5,3)
得到(2,3)
的输出,再通过广播机制加偏置b(3)
,完全对应神经网络中“输入→线性变换→输出”的流程;
模拟场景不同
test01
:模拟最简单的单变量线性模型(如y = wx + b
拟合单个数据点),仅用于演示“自动微分的基本原理”:如何从损失反向计算标量参数的梯度;test02
:模拟真实神经网络中的线性层(全连接层),输入是批量样本(2个样本),输出是对应维度的预测值(3维),完全贴合实际训练场景(批量处理样本、矩阵运算实现层间连接);
梯度形状不同
test01
:梯度是标量(与参数形状一致),例如w.grad
和b.grad
都是单个数值;
test02
:梯度是张量(与参数形状一致),例如w.grad
是(5,3)
的张量(与权重w
形状相同),b.grad
是(3,)
的张量(与偏置b
形状相同),这与真实神经网络中参数梯度的形状完全匹配。
10 线性回归案例
接下来使用 PyTorch 的各个组件来构建线性回归的实现。在 PyTorch 中进行模型构建的整个流程一般分为四个步骤:
- 准备训练集数据
- 构建要使用的模型
- 设置损失函数和优化器
- 模型训练
导包:
# 导包: import torch from torch.utils.data import TensorDataset # 构造数据集对象 from torch.utils.data import DataLoader # 数据加载器 from torch import nn # nn模块中有平方损失函数和假设函数 from torch import optim # optim模块中有优化器函数 from sklearn.datasets import make_regression # 创建线性回归模型数据集 import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签 plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
数据集构建函数:
# 数据集构建函数: def create_dataset(): # 使用make_regression创建线性回归数据集 x, y, coef = make_regression(n_samples=100, # 是样本数量 n_features=1, # 是特征数量 noise=10, # 噪声 coef=True, # 设置为会返回系数 bias=1.5, # 偏置 random_state=0# 固定随机种子保证结果可复现 ) # 将构建的numpy数组形式的数据转换为张量类型,方便PyTorch进行处理 x = torch.tensor(x) y = torch.tensor(y) return x, y, coef
构建数据集:
# 构建数据集: if __name__ == "__main__": # 调用create_dataset函数生成数据,得到特征x、标签y、真实系数coef x,y,coef=create_dataset() # 绘制数据的真实的线性回归结果,先绘制样本点的散点图 plt.scatter(x, y) # 生成用于绘制真实拟合直线的x值,在原始数据x的最小值到最大值之间取1000个点 x_plot = torch.linspace(x.min(), x.max(), 1000) # 根据真实的系数coef和偏置1.5计算对应的y值,构建真实拟合直线 y1 = torch.tensor([v * coef + 1.5 for v in x_plot]) # 绘制真实拟合直线,设置标签为'real' plt.plot(x_plot, y1, label='real') plt.grid() # 显示网格线 plt.legend() # 显示图例 plt.show() # 展示绘制的图形
使用DataLoader构建数据加载器并进行模型构建:
# 使用DataLoader构建数据加载器并进行模型构建 # 先再次调用create_dataset函数获取数据(这里可以优化,前面已经生成过,可直接复用,此处为了流程清晰所以再次调用) x, y, coef = create_dataset() # 构造数据集对象,将特征x和标签y封装成TensorDataset,方便后续用DataLoader加载 dataset = TensorDataset(x, y) # 构造数据加载器,指定数据集为dataset,批量大小batch_size为16,是否打乱数据shuffle为True dataloader = DataLoader(dataset=dataset, batch_size=16, shuffle=True) # 构造模型,使用nn.Linear定义一个线性层,输入特征维度in_features为1,输出特征维度out_features为1,模拟y = w*x + b的线性关系 model = nn.Linear(in_features=1, out_features=1)
设置损失函数和优化器:
# 设置损失函数和优化器 # 构造平方损失函数,使用均方误差损失MSELoss,用于计算预测值和真实值的误差 criterion = nn.MSELoss() # 构造优化函数,使用随机梯度下降SGD优化器,传入模型的参数model.parameters(),学习率lr设置为1e-2 optimizer = optim.SGD(params=model.parameters(), lr=1e-2)
模型训练:
# 模型训练 epochs = 100 # 设置训练的轮数为100轮 loss_epoch = [] # 用于存储每一轮或每个批次的损失变化,这里后续实际存储的是每个batch的平均损失(代码里有可优化点,实际是每个batch计算后追加,最后长度是batch数量总和) total_loss=0.0 # 用于累加损失值 train_sample=0.0 # 用于累加训练样本数量 for _ in range(epochs): # 外层循环,遍历训练轮数 for train_x, train_y in dataloader: # 内层循环,遍历数据加载器,按批次获取训练数据 # 将一个batch的训练数据送入模型,注意转换数据类型为torch.float32,因为模型默认处理这种类型 y_pred = model(train_x.type(torch.float32)) # 计算损失值,先将真实标签train_yreshape成与预测值y_pred相同的形状(-1, 1),并转换数据类型,然后计算均方误差 loss = criterion(y_pred, train_y.reshape(-1, 1).type(torch.float32)) total_loss += loss.item() # 将损失值(张量形式)转换为Python数值并累加 train_sample += len(train_y) # 累加当前batch的样本数量 optimizer.zero_grad() # 梯度清零,避免梯度累积影响下一次计算 loss.backward() # 自动微分(反向传播),计算模型参数的梯度 optimizer.step() # 更新参数,根据计算的梯度和优化器的策略来调整模型参数 loss_epoch.append(total_loss/train_sample) # 计算到当前的平均损失并添加到loss_epoch列表中,这里其实是每个batch后都添加,会导致列表长度是batch数量,而非epochs数量,后续绘制时需要注意
可视化
# 可视化 plt.plot(range(len(loss_epoch)), loss_epoch) plt.title('损失变化曲线') # 设置图表标题 plt.grid() # 显示网格线 plt.show() # 展示损失变化曲线 # 绘制拟合直线 plt.scatter(x, y) # 绘制样本点的散点图 x_plot = torch.linspace(x.min(), x.max(), 1000) # 生成用于绘制拟合直线的x值 # 根据训练后模型的权重model.weight和偏置model.bias计算预测的y值,构建训练后的拟合直线。注意model.weight和model.bias是张量,需要取出数值计算,这里代码有问题,应该是v * model.weight.item() + model.bias.item() y1 = torch.tensor([v * model.weight + model.bias for v in x_plot]) # 根据真实的系数和偏置构建真实拟合直线 y2 = torch.tensor([v * coef + 1.5 for v in x_plot]) plt.plot(x_plot, y1, label='训练') # 绘制训练后的拟合直线 plt.plot(x_plot, y2, label='真实') # 绘制真实拟合直线 plt.grid() # 显示网格线 plt.legend() # 显示图例 plt.show() # 展示拟合直线对比图