PyTorch 学习

张量基础操作

张量创建

基本张量创建

  • torch.tensor(data)创建指定内容的张量
  • torch.Tensor(size)创建指定形状的张量
  • 创建指定类型的张量
    • 可通过 torch.IntTensor()、torch.FloatTensor()等创建
    • 或在 torch.tensor()中通过 dtype 参数指定类型

指定区间的张量创建

  • torch.arange(start, end, step)在区间内按步长创建张量
  • torch.linspace(start, end, steps)在区间内按元素数量创建张量
  • torch.logspace(start, end, steps, base)在指数区间内按指定底数创建张量

按数值填充张量

  • torch.zeros(size)创建指定形状的全 0 张量
  • torch.ones(size)创建指定形状的全 1 张量
  • torch.full(size,value)创建指定形状的按指定值填充的张量
  • torch.empty(size)创建指定形状的未初始化的张量
  • torch.zeros_like(input)创建与给定张量形状相同的全 0 张量
  • torch.ones_like(input)创建与给定张量形状相同的全 1 张量
  • torch.full_like(input,value)创建与给定张量形状相同的按指定值填充的张量
  • torch.empty_like(input)创建与给定张量形状相同的未初始化的张量
  • torch.eye(n, [m])创建单位矩阵

随机张量创建

  • torch.rand(size)创建在[0,1)上均匀分布的,指定形状的张量
  • torch.randint(low, high, size)创建在[low,high)上均匀分布的,指定形状的 张量
  • torch.randn(size)创建标准正态分布的,指定形状的张量
  • torch.normal(mean,std,size)创建自定义正态分布的,指定形状的张量
  • torch.rand_like(input)创建在[0,1)上均匀分布的,与给定张量形状相同的张量
  • torch.randint_like(input, low, high)创建在[low,high)上均匀分布的,与给 定张量形状相同的张量
  • torch.randn_like(input)创建标准正态分布的,与给定张量形状相同的张量
  • torch.randperm(n)生成从 0 到 n-1 的随机排列,类似洗牌
  • torch.random.initial_seed()查看随机数种子
  • torch.manual_seed(seed)设置随机数种子

张量转换

张量元素类型转换

  • Tensor.type(dtype)修改张量的类型
  • Tensor.double()等修改张量的类型

Tensor 与 ndarray 转换

  • Tensor.numpy()将 Tensor 转换为 ndarray,共享内存。使用 copy()避免共享内存
  • Tensor.from_numpy(ndarray)将 ndarray 转换为 Tensor,共享内存。使用 copy()避免 共享内存
  • torch.tensor(ndarray)将 ndarray 转换为 Tensor,不共享内存

Tensor 与标量转换

若张量中只有 1 个元素,Tensor.item()可提取张量中元素为标量。

张量数值计算

基本运算

  • 四则运算

    • +、-、*、/加减乘除
    • add()、sub()、mul()、div()加减乘除,不改变原数据
    • add_()、sub_()、mul_()、div_()加减乘除、修改原数据
  • -、neg()、neg_()取负

  • **、pow()、pow_()求幂

  • sqrt()、sqrt_()求平方根

  • exp()、exp_()以 e 为底数求幂

  • log()、log_()以 e 为底求对数

哈达玛积(元素级乘法)

  • 两个矩阵对应位置元素相乘称为哈达玛积(Hadamard product)。 使用*、mul()实现两个形状相同的张量之间对位相乘。

矩阵乘法运算

  • mm()严格用于二维矩阵相乘
  • @、matmul()支持多维张量,按最后两个维度做矩阵乘法,其他维度广播

节省内存

运行一些操作时可能导致为新的结果分配内存,例如 X=X@Y,发现 id(X)会指向另一个 位置,这是因为 Python 首先计算 X@Y,为结果分配新的内存,再令 X 指向内存中的新位 置。

  • 如果后续 X 不再重复使用,可以使用 X[:] = X @ Y 来减少内存开销

张量运算函数

  • sum()求和
  • mean()求均值
  • max()/min()求最大/最小值及其索引
  • argmax()/argmin()求最大值/最小值的索引
  • std()求标准差
  • unique()去重
  • sort()排序

张量索引操作

简单索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import torch 
tensor1 = torch.randint(1, 9, (3, 5, 4))
print(tensor1)
print()

# 取 第 0 维第 0
print(tensor1[0])
print()

# 取 第 0 维所有,第 1 维第 1
print(tensor1[:, 1])
print()

# 取 第 0 维所有,第 1 维第 1,第 2 维第 3
print(tensor1[2, 1, 3])

列表索引

1
2
3
4
5
6
7
8
9
10
11
12
import torch 

tensor1 = torch.randint(1, 9, (3, 5, 4))
print(tensor1)
print()

# 取 第 0 维第 0,第 1 维第 1 和 第 0 维第 1,第 1 维第 2
print(tensor1[[0, 1], [1, 2]])
print()

# 取 第 0 维第 0,第 1 维第 1、2 和 第 0 维第 1,第 1 维第 1、2
print(tensor1[[[0], [1]], [1, 2]])

范围索引

1
2
3
4
5
6
7
8
9
10
11
12
13
import torch 

tensor1 = torch.randint(1, 9, (3, 5, 4))
print(tensor1)
print()

# 取 第 0 维第 1 到最后
print(tensor1[1:])
print()

# 取 第 0 维最后,第 1 维 1 到 3(包含 3),第 2 维 0 到 2(包含 2)
print(tensor1[-1:, 1:4, 0:3])
print()

布尔索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import torch 

tensor1 = torch.randint(1, 9, (3, 5, 4))
print(tensor1)
print()

# 取 第 2 维第 0 大于 5 的,返回(dim0,dim1)形状的索引
print(tensor1[:, :, 0] > 5)
print(tensor1[tensor1[:, :, 0] > 5])
print()

# 取 第 1 维第 1 大于 5 的,返回(dim0,dim2)形状的索引
mask = tensor1[:, 1, :] > 5
print(mask)
tensor2 = tensor1.permute(0, 2, 1) # 转换维度为(dim0,dim2,dim1)
print(tensor2[mask])
tensor2 = tensor2[mask].permute(1, 0) # 转换维度为(dim1,?)
print(tensor2)
print()

# 取 第 1 维第 1,第 2 维第 2 大于 5 的,返回(dim0)形状的索引
print(tensor1[:, 1, 2] > 5)
print(tensor1[tensor1[:, 1, 2] > 5])

张量形状操作

交换维度

  • transpose()交换两个维度
  • permute()重新排列多个维度

调整形状

  • reshape()调整张量的形状
  • view()调整张量的形状,需要内存连续。共享内存
    • is_contiguous()判断是否内存连续
    • contiguous()转换为内存连续

增加或删除维度

  • unsqueeze()在指定维度上增加 1 个维度
  • squeeze()删除大小为 1 的维度

张量拼接操作

  • torch.cat()张量拼接,按已有维度拼接。除拼接维度外,其他维度大小须相同
  • torch.stack()张量堆叠,按新维度堆叠。所有张量形状必须一致

自动微分模块

PyTorch 具有一个内置的微分引擎 torch.autograd 以支持计算图的梯度自动计算

该计算图中 x、w、b 为叶子节点,即最基础的节点。叶子节点的数据并非由计算生成, 因此是整个计算图的基石,叶子节点张量不可以执行 in-place 操作。而最终的 loss 为根节点

可通过 is_leaf 属性查看张量是否为叶子节点

自动微分的关键就是记录节点的数据与运算。数据记录在张量的 data 属性中,计算记 录在张量的 grad_fn 属性中

计算图根据搭建方式可分为静态图和动态图,PyTorch 是动态图机制,在计算的过程中 逐步搭建计算图,同时对每个 Tensor 都存储 grad_fn 供自动微分使用。当计算到根节点后, 在根节点调用 backward()方法即可反向传播计算计算图中所有节点的梯度。

非叶子节点的梯度在反向传播之后会被释放掉(除非设置参数 retain_grad=True)。而 叶子节点的梯度在反向传播之后会保留(累积)。通常需要使用 optimizer.zero_grad()清零参数的梯度。

若设置张量参数 requires_grad=True,则 PyTorch 会追踪所有基于该张量的操作,并在反向传播时计算其梯度。依赖于叶子节点的节点,requires_grad 默认为 True。

有时我们希望将某些计算移动到计算图之外,可以使用 Tensor.detach()返回一个新的变量,该变量与原变量具有相同的值,但丢失计算图中如何计算原变量的信息。换句话说,梯度不会在该变量处继续向下传播。

线性回归案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import torch
import os
import matplotlib.pyplot as plt
from torch import nn, optim # 模型、损失函数和优化器
from torch.utils.data import DataLoader, TensorDataset # 数据集和数据加载器

os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE"

# 设置随机种子(可选,为了结果可复现)
torch.manual_seed(42)

# 生成 150 个样本,每个样本 1 个特征
X = torch.randn(150, 1) # 输入特征,形状 [150, 1]

# 真实的权重和偏置
w = torch.tensor([2.5]) # 真实权重
b = torch.tensor([5.2]) # 真实偏置

# 理想的线性关系:y = w * X + b
# 再加上一些噪声,模拟真实情况
noise = torch.randn(150, 1) * 0.3 # 噪声
y = w * X + b + noise # 目标值(带噪声的标签)

# 打包成数据集
dataset = TensorDataset(X, y)

# 创建数据加载器:每个 batch 10 个样本,训练时打乱顺序
dataloader = DataLoader(dataset, batch_size=10, shuffle=True)

# 定义模型:输入特征 1 个,输出 1 个
model = nn.Linear(in_features=1, out_features=1)

# 损失函数:均方误差(MSE)
loss_fn = nn.MSELoss()

# 优化器:随机梯度下降(SGD),学习率 0.01
optimizer = optim.SGD(model.parameters(), lr=0.01)

# 用于记录每个 epoch 的平均损失
loss_list = []

for epoch in range(10):
total_loss = 0.0 # 累计损失
for x_batch, y_batch in dataloader:
# 前向传播:模型预测
y_pred = model(x_batch)

# 计算损失
loss = loss_fn(y_pred, y_batch)

# 累加损失
total_loss += loss.item() # 使用 .item() 获取标量值

# 反向传播
loss.backward()

# 更新参数
optimizer.step()

# 清空梯度(非常重要!)
optimizer.zero_grad()

# 计算该 epoch 的平均损失,并记录
avg_loss = total_loss / len(dataloader)
loss_list.append(avg_loss)

# 可选:打印每个 epoch 的 loss(调试用)
plt.plot(loss_list)
plt.show()

参数初始化和正则化

常数初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
linear = nn.Linear(5, 2)

# 全部参数初始化为 0
nn.init.zeros_(linear.weight)
print(linear.weight)

# 全部参数初始化为 1
nn.init.ones_(linear.weight)
print(linear.weight)

# 全部参数初始化为一个常数
nn.init.constant_(linear.weight, 10)
print(linear.weight)

注意:将权重初始值设为 0 将无法正确进行学习。严格地说,不能将权重初始值设成一 样的值。因为这意味着反向传播时权重全部都会进行相同的更新,被更新为相同的值(对称 的值)。这使得神经网络拥有许多不同的权重的意义丧失了。为了防止“权重均一化”(瓦 解权重的对称结构),必须随机生成初始值。

秩初始化

权重参数初始化为单位矩阵。

1
2
3
# 参数初始化为单位矩阵 
nn.init.eye_(linear.weight)
print(linear.weight)

正态分布初始化

权重参数按指定均值与标准差正态分布初始化。

1
2
3
# 参数初始化为按指定均值与标准差正态分布 
nn.init.normal_(linear.weight, mean=0.0, std=1.0)
print(linear.weight)

均匀分布初始化

权重参数在指定区间内均匀分布初始化。

1
2
3
# 参数初始化为在区间内均匀分布 
nn.init.uniform_(linear.weight, a=0, b=10)
print(linear.weight)

Xavier 初始化(也叫 Glorot 初始化)

1
2
3
4
5
6
7
# Xavier 正态分布初始化 
nn.init.xavier_normal_(linear.weight)
print(linear.weight)

# Xavier 均匀分布初始化
nn.init.xavier_uniform_(linear.weight)
print(linear.weight)

He 初始化(也叫 Kaiming 初始化)

1
2
3
4
5
6
7
# Kaiming 正态分布初始化 
nn.init.kaiming_normal_(linear.weight)
print(linear.weight)

# Kaiming 均匀分布初始化
nn.init.kaiming_uniform_(linear.weight)
print(linear.weight)
特性 Xavier 初始化 He 初始化
提出者 Xavier Glorot Kaiming He(何凯明)
适用激活函数 Sigmoid、Tanh(对称、有界) ReLU、LeakyReLU(非对称、有稀疏性)
核心目标 保持输入输出方差一致(前向 + 反向传播稳定) 针对 ReLU 的“一半神经元关闭”特性,调整方差
方差计算依据 fan_in + fan_out fan_in(通常)
分布形式 均匀分布 或 正态分布 均匀分布 或 正态分布
典型使用场景 全连接网络 + Tanh/Sigmoid CNN / 深层网络 + ReLU

根本原因就是:

神经网络的训练依赖于梯度的稳定传播,而权重的初始值会直接影响梯度的大小与方向!如果初始值不合适,就会出现梯度消失或爆炸,导致模型根本学不动。

Xavier 初始化(Glorot)和 He 初始化(何凯明)是两种常用的神经网络权重初始化方法,它们的核心区别在于:Xavier 适用于 Sigmoid/Tanh 等对称激活函数,通过保持输入输出方差一致来避免梯度问题;而 He 初始化针对 ReLU 及其变种,调整了方差计算方式以适应其稀疏激活特性。它们的出现都是为了解决深度神经网络中因权重初始化不当导致的梯度消失或爆炸问题,是训练稳定、高效神经网络的关键技术。

Dropout 随机失活

Dropout(随机失活,暂退法)是一种在学习的过程中随机关闭神经元的方法。

可以通过 torch.nn.Dropout(p)来使用 Dropout,并通过参数 p 来设置失活概率。

1
2
3
4
dropout = torch.nn.Dropout(p=0.5) 
x = torch.randint(1, 10, (10,), dtype=torch.float32)
print("Dropout 前:", x)
print("Dropout 后:", dropout(x))

应用案例:房价预测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# 导入所需的模块
import torch
import pandas as pd
import torch.nn as nn
import matplotlib.pyplot as plt

from sklearn.model_selection import train_test_split # 划分数据集
from sklearn.pipeline import Pipeline # 管道操作
from sklearn.impute import SimpleImputer # 处理缺失值
from sklearn.compose import ColumnTransformer # 列转换器
from sklearn.preprocessing import StandardScaler, OneHotEncoder # 针对数值型和类别型特征的处理操作
from torch.utils.data import TensorDataset, DataLoader # 数据集和数据加载器


# 特征工程
def create_dataset():
# 1. 从文件读取数据
data = pd.read_csv('../data/house_prices.csv')

# 2. 去除无关特征(特征选择)
data.drop(['Id'], axis=1, inplace=True)

# 3. 划分特征和目标值
X = data.drop(['SalePrice'], axis=1)
y = data['SalePrice']

# 4. 划分训练集和测试集
x_train, x_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# print(x_test.shape,y_test.shape,x_train.shape, y_train.shape)

# 5. 特征预处理(特征转换)
# 5.1 筛选两类特征:数值型 和 类别型
numerical_features = X.select_dtypes(exclude=['object']).columns
categorical_features = X.select_dtypes(include=['object']).columns

# 5.2 对两种类型的特征,分别定义不同的转换操作
numerical_transform = Pipeline(steps=[
('fillNA', SimpleImputer(strategy='mean')),
('scaler', StandardScaler())
])
categorical_transform = Pipeline(steps=[
('fillNA', SimpleImputer(strategy='constant', fill_value='NaN')),
('oneHotEncoder', OneHotEncoder(handle_unknown='ignore'))
])

# 5.3 构建列转换器
preprocessor = ColumnTransformer(
transformers=[
('numerical', numerical_transform, numerical_features),
('categorical', categorical_transform, categorical_features)
]
)
# 5.4 做列转换,并生成新的DataFrame
x_train = pd.DataFrame(preprocessor.fit_transform(x_train).toarray(), columns=preprocessor.get_feature_names_out())
x_test = pd.DataFrame(preprocessor.transform(x_test).toarray(), columns=preprocessor.get_feature_names_out())

# 5.5 构建Tensor数据集
train_dataset = TensorDataset(torch.tensor(x_train.values).float(), torch.tensor(y_train.values).float())
test_dataset = TensorDataset(torch.tensor(x_test.values).float(), torch.tensor(y_test.values).float())

return train_dataset, test_dataset, x_train.shape[1]


# 主流程
# 1. 得到数据集
train_dataset, test_dataset, n_features = create_dataset()
print(test_dataset)
print(n_features)

# 2. 搭建神经网络模型
model = nn.Sequential(
nn.Linear(in_features=n_features, out_features=128),
nn.BatchNorm1d(128),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(in_features=128, out_features=1),
)


# 3. 定义损失函数
def log_rmse(y_pred, y_true):
y_pred = y_pred.squeeze()
y_pred = torch.clamp(y_pred, 1, float('inf'))
mse = nn.MSELoss()
return torch.sqrt(mse(torch.log(y_pred), torch.log(y_true)))


# 4. 模型训练和测试
def train_test(model, train_dataset, test_dataset, lr, n_epochs, batch_size, device):
# 单独定义权重初始化的函数
def init_weights(m):
if isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight)

# 1. 初始化
model.apply(init_weights)
model.to(device)

# 2. 定义优化器
optimizer = torch.optim.Adam(model.parameters(), lr=lr)

train_losses = [] # 记录每个epoch训练误差
test_losses = [] # 记录每个epoch测试误差

# 3. 按照epoch进行训练和测试
for epoch in range(n_epochs):
# 3.1 训练
model.train()
train_loss_total = 0
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
# 通过 dataloader 遍历每个mini-batch数据
for idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
pred = model(data)
loss_value = log_rmse(pred, target) # 计算损失
train_loss_total += loss_value.item() # 累加损失
loss_value.backward() # 反向传播,计算梯度
optimizer.step() # 更新参数
optimizer.zero_grad() # 更新参数

# 打印进度条
print(f"\repoch:{epoch:0>3}[{'=' * (int((idx + 1) / len(train_loader) * 50)):<50}]", end="")

# 每轮 epoch 后,计算平均训练误差
train_losses.append(train_loss_total / len(train_loader))

# 3.2 测试
model.eval()
test_loss_total = 0
test_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
with torch.no_grad():
for data, target in train_loader:
data, target = data.to(device), target.to(device)
pred = model(data)
loss_value = log_rmse(pred, target) # 计算损失
test_loss_total += loss_value.item() # 累加损失

# 每轮 epoch 后,计算平均测试误差
test_losses.append(test_loss_total / len(test_loader))

# 打印输出
print(f"train loss: {train_losses[-1]}, test loss: {test_losses[-1]}")

# 所有epoch训练结束,返回误差列表
return test_losses, test_losses


# 判断是否支持cude,如果支持则使用GPU,否则使用CPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
train_loss_list, test_loss_list = train_test(model, train_dataset, test_dataset, lr=0.1, n_epochs=100, batch_size=64,
device=device)