数据准备

import torch

# 固定随机种子,使得运行结果可以稳定复现
torch.manual_seed(1024)
# 产生训练用的数据
x_origin = torch.linspace(100, 300, 200)
# 将变量X归一化,否则梯度下降法很容易不稳定
x = (x_origin - torch.mean(x_origin)) / torch.std(x_origin)
epsilon = torch.randn(x.shape)
y = 10 * x + 5 + epsilon

torch.linspace

使用 torch.linspace 函数时,需要指定三个主要参数:起始值(start)、结束值(end)和步数(steps)。步数决定了张量中将包含多少个元素。例如,创建一个从3到10的张量,包含5个元素,可以使用以下代码:

import torch

# 创建一个从3到10,包含5个元素的张量
tensor = torch.linspace(3, 10, steps=5)
print(tensor)
输出将是:
tensor([ 3.0000, 4.7500, 6.5000, 8.2500, 10.0000])

参数解释
start:张量的起始值。
end:张量的结束值。
steps:张量中元素的数量。
dtype:可选参数,用于指定返回张量的数据类型。
device:可选参数,用于指定张量所在的设备(如CPU或GPU)。
requires_grad:可选参数,指定是否需要计算梯度。

torch.std

在PyTorch中,标准差是通过 torch.std 函数计算的,它可以计算张量(tensor)的标准差。标准差是衡量数据集中数值分散程度的统计量,它是方差的平方根。在PyTorch中,标准差的计算考虑了贝塞尔校正(Bessel's correction),这意味着在计算样本标准差时,默认会使用 n-1 而不是 n 作为分母。

import torch

# 创建一个张量
a = torch.tensor([[0.2035, 1.2959, 1.8101, -0.4644],
[1.5027, -0.3270, 0.5905, 0.6538],
[-1.5745, 1.3330, -0.5596, -0.6548],
[0.1264, -0.5080, 1.6420, 0.1992]])
# 计算每行的标准差,并保持维度
std_a = torch.std(a, dim=1, keepdim=True)
print(std_a)

输出将是每行元素标准差的张量。
参数解释
input:输入张量。
dim:要减少的维度,可以是单个维度、维度列表或 None。
correction:样本大小与样本自由度之间的差异,默认为贝塞尔校正,即 correction=1。
keepdim:输出张量是否保留 dim 维度。
out:输出张量。

模型定义

# 为了使用PyTorch的高层封装函数,我们通过继承Module类来定义函数
class Linear(torch.nn.Module):
    
    def __init__(self):
        """
        定义线性回归模型的参数:a, b
        """
        super().__init__()
        self.a = torch.nn.Parameter(torch.zeros(()))
        self.b = torch.nn.Parameter(torch.zeros(()))

    def forward(self, x):
        """
        根据当前的参数估计值,得到模型的预测结果
        参数
        ----
        x :torch.tensor,变量x
        返回
        ----
        y_pred :torch.tensor,模型预测值
        """
        return self.a * x + self.b

    def string(self):
        """
        输出当前模型的结果
        """
        return f'y = {self.a.item():.2f} * x + {self.b.item():.2f}'

forward

我们在定义自已的网络的时候,需要继承nn.Module类,并重新实现构造函数__init__和forward这两个方法。但有一些注意技巧:

(1)一般把网络中具有可学习参数的层(如全连接层、卷积层等)放在构造函数__init__()中,当然我也可以吧不具有参数的层也放在里面;

(2)一般把不具有可学习参数的层(如ReLU、dropout、BatchNormanation层)可放在构造函数中,也可不放在构造函数中,如果不放在构造函数__init__里面,则在forward方法里面可以使用nn.functional来代替
(3)forward方法是必须要重写的,它是实现模型的功能,实现各个层之间的连接关系的核心。

super().__init__()

super().__init__() 是 Python 中用于调用父类初始化方法的语法,常用于继承场景,确保子类能够正确初始化父类的属性和方法。

class Parent:
def __init__(self):
print("Parent initialized")

class Child(Parent):
def __init__(self):
super().__init__() # 调用父类的 __init__ 方法
print("Child initialized")

child = Child()

输出:
Parent initialized
Child initialized

继承父类初始化逻辑:通过 super().__init__(),子类可以调用父类的 __init__ 方法,避免重复代码。
多继承支持:在多继承中,super() 遵循方法解析顺序(MRO),确保所有父类的初始化方法都被正确调用。

Python中的f字符串

f字符串(formatted string literals)是Python 3.6引入的一种字符串格式化方法,它使得字符串格式化更加简便和高效。
基本用法
在f字符串中,可以在大括号 {} 内放入任何有效的Python表达式,这些表达式会在运行时被求值并替换为其结果。

name = "Alice"
print(f"Hello, {name}!") # 输出:Hello, Alice!

表达式求值与函数调用
f字符串的大括号内可以填入表达式或调用函数,Python会求出其结果并填入返回的字符串内。

import math
print(f"The value of pi is approximately {math.pi:.2f}.") # 输出:The value of pi is approximately 3.14

多行f字符串
f字符串还可以用于多行字符串。

name = "Eric"
age = 27
msg = (
f"Hello!\n"
f"I'm {name}.\n"
f"I'm {age}."
)
print(msg)
# 输出:
# Hello!
# I'm Eric
# I'm 27

自定义格式
可以使用格式说明符对数字进行格式化,例如对齐、宽度、符号、补零、精度等。

number = 1234.5678
print(f"{number:,.2f}") # 输出:1,234.57

创建实例、参数设置与使用第三方库训练

# 定义模型
model = Linear()
# 确定最优化算法
learning_rate = 0.1
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)

for t in range(20):
    # 根据当前的参数估计值,得到模型的预测结果
    # 也就是调用forward函数
    y_pred = model(x)
    # 计算损失函数
    loss = (y - y_pred).pow(2).mean()
    # 将上一次的梯度清零
    optimizer.zero_grad()
    # 计算损失函数的梯度
    loss.backward()
    # 迭代更新模型参数的估计值
    optimizer.step()
    print(f'Step {t + 1}, Loss: {loss: .2f}; Result: {model.string()}')

SGD

torch.optim.SGD 是PyTorch中用于实现随机梯度下降(Stochastic Gradient Descent,SGD)的类,它是深度学习中常用的优化算法之一。SGD通过迭代更新模型参数来最小化目标函数,通常用于训练神经网络。
在PyTorch中,要使用SGD优化器,首先需要创建一个SGD对象,并将模型的参数传递给它。然后,在训练循环中,执行前向传播、计算损失、执行反向传播并调用优化器的step()方法来更新参数。例如:

import torch
import torch.optim as optim

# 定义模型和损失函数
model = torch.nn.Linear(10, 1)
criterion = torch.nn.MSELoss()
# 定义优化器
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=0.0001)
# 在训练循环中使用优化器
for epoch in range(epochs):
# 前向传播
output = model(input_data)
loss = criterion(output, target)
# 反向传播和优化
optimizer.zero_grad()
loss.backward()
optimizer.step()

SGD的参数解释
params(必须参数):需要优化的参数(张量)的迭代器,例如模型的参数model.parameters()。
lr(必须参数):学习率,控制每次参数更新的步长。
momentum(默认值为0):动量,用于加速SGD收敛,引入了上一步梯度的指数加权平均。
dampening(默认值为0):阻尼项,用于减缓动量的速度。
weight_decay(默认值为0):权重衰减,也称为L2正则化项,用于防止过拟合。
nesterov(默认值为False):Nesterov动量,采用Nesterov动量更新规则。

自定义训练

# 利用代码实现PyTorch封装的梯度下降法
model = Linear()
for t in range(20):
    # 根据当前的参数估计值,得到模型的预测结果
    # 也就是调用forward函数
    y_pred = model(x)
    # 计算损失函数
    loss = (y - y_pred).pow(2).mean()
    # 计算损失函数的梯度
    loss.backward()
    with torch.no_grad():
        for param in model.parameters():
            # 迭代更新模型参数的估计值,等同于optimizer.step()
            param -= learning_rate * param.grad
            # 将梯度清零,等同于optimizer.zero_grad()
            param.grad = None
    print(f'Step {t + 1}, Loss: {loss: .2f}; Result: {model.string()}')

loss是需要减小的,所以是减去梯度。

torch.no_grad()

在 PyTorch 中,with torch.no_grad 是一个上下文管理器,用于在特定代码块中禁用自动求导功能。这在模型评估或推断时非常有用,因为在这些情况下不需要计算梯度,从而可以提高代码执行效率。以下是一个简单的示例,展示了如何使用 with torch.no_grad:

import torch

# 创建一些张量并设置 requires_grad=True
x = torch.randn(10, 5, requires_grad=True)
y = torch.randn(10, 5, requires_grad=True)
z = torch.randn(10, 5, requires_grad=True)

# 在 with torch.no_grad 语句块中进行计算
with torch.no_grad():
w = x + y + z
print(w.requires_grad) # 输出: False
print(w.grad_fn) # 输出: None

print(w.requires_grad) # 输出: False

在上述代码中,即使 x、y 和 z 的 requires_grad 属性为 True,在 with torch.no_grad 语句块中计算得到的新张量 w 的 requires_grad 属性也会被自动设置为 False,且 grad_fn 为 None。

改进:随机梯度下降

每次根据批大小(batch_size)抽取一部分数据,以部分数据的损失值近似整体损失,以减小运算量。

# 定义损失函数
mse = lambda y, y_pred: (y - y_pred).pow(2).mean()
# 在最优化算法的运行过程中,记录模型在批量数据和整体数据上的损失
# 定义每批次用到的数据量
batch_size = 20
# 定义模型
model = Linear()
# 确定最优化算法
learning_rate = 0.1
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
stats = {'batch_loss': [], 'total_loss': []}

for t in range(20):
    # 选取当前批次的数据,用于训练模型
    ix = (t * batch_size) % len(x)
    xx = x[ix: ix + batch_size]
    yy = y[ix: ix + batch_size]
    # 计算当前批次数据的损失
    loss = mse(yy, model(xx))
    # 将上一次的梯度清零
    optimizer.zero_grad()
    # 计算损失函数的梯度
    loss.backward()
    # 迭代更新模型参数的估计值
    optimizer.step()
    # 预估模型在整个数据集上面的损失
    stats['batch_loss'].append(loss.item())
    stats['total_loss'].append(mse(y, model(x)).item())

Python 的 Lambda 表达式

Lambda 表达式是 Python 中的匿名函数,用于简化代码,通常用于编写简单的、一次性的函数。Lambda 表达式的基本语法如下:

lambda 参数: 表达式
 
# 定义损失函数
mse = lambda y, y_pred: (y - y_pred).pow(2).mean()
Lambda 表达式的特点

匿名性:Lambda 函数没有名称,适合临时使用。

简洁性:通常用于简单的操作,避免定义完整的函数。

单行限制:Lambda 函数只能包含一个表达式,不能包含复杂的逻辑或多行代码。

切片

xx = x[ix: ix + batch_size] 是一个典型的Python切片操作,用于从可迭代对象(如列表、数组等)中提取子集。

参数说明

x:原始可迭代对象(如列表、NumPy数组等)。

ix:切片起始索引(包含)。

ix + batch_size:切片结束索引(不包含)。

batch_size:单次提取的元素数量。

注意事项

当剩余元素不足batch_size时,自动返回剩余全部元素。

若x是NumPy数组,切片返回的是视图而非副本,修改切片会影响原数组。

代码跟踪与尝试

复现:

import torch
import matplotlib.pyplot as plt
torch.manual_seed(1024)
x_ori=torch.linspace(100,300,200)

x=(x_ori-torch.mean(x_ori))/torch.std(x_ori)
e=torch.randn(x.shape)
y=10*x+5+e

class Linear(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.a=torch.nn.Parameter(torch.zeros(()))
        self.b=torch.nn.Parameter(torch.zeros(()))
    def forward(self,x):
        return self.a*x+self.b
    def string(self):
        return f'y={self.a.item():.2f}*x+{self.b.item():.2f}'

model=Linear()
lr=0.1
optimizer=torch.optim.SGD(model.parameters(),lr=lr)

for i in range(20):
    y_p=model(x)
    loss=(y-y_p).pow(2).mean()
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    print(f'Step {i + 1}, Loss: {loss: .2f}; Result: {model.string()}')

model=Linear()
for i in range(20):
    y_p=model(x)
    loss=(y-y_p).pow(2).mean()
    loss.backward()
    with torch.no_grad():
        for param in model.parameters():
            param-=lr*param.grad
            param.grad=None
    print(f'Step {i + 1}, Loss: {loss: .2f}; Result: {model.string()}')

mse = lambda y, y_pred: (y - y_pred).pow(2).mean()
model=Linear()
batch_size=20
optimizer=torch.optim.SGD(model.parameters(),lr=lr)
stats = {'batch_loss': [], 'total_loss': []}

for i in range(20):
    ix = (i * batch_size) % len(x)
    xx = x[ix: ix + batch_size]
    yy = y[ix: ix + batch_size]
    y_p=model(x)
    loss=(y-y_p).pow(2).mean()
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    stats['batch_loss'].append(loss.item())
    stats['total_loss'].append(mse(y, model(x)).item())
# 创建一个图形框
fig = plt.figure(figsize=(6, 6), dpi=100)
# 在图形框里只画一幅图
ax = fig.add_subplot(111)
# 解决中文显示问题
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams.update({'font.size': 13})
ax.set_xlabel('训练步数', fontsize=18)
ax.set_ylabel('模型损失', fontsize=18)
ax.plot(stats['batch_loss'], 'k', label='批次损失')
ax.plot(stats['total_loss'], 'r-.', label='整体损失')
legend = plt.legend(shadow=True)
plt.savefig("sgd.png", dpi=200)
plt.show()

结果:

Step 1, Loss:  125.25; Result: y=1.98*x+1.02
Step 2, Loss:  80.72; Result: y=3.56*x+1.83
Step 3, Loss:  52.16; Result: y=4.83*x+2.49
Step 4, Loss:  33.84; Result: y=5.85*x+3.01
Step 5, Loss:  22.10; Result: y=6.66*x+3.42
Step 6, Loss:  14.57; Result: y=7.31*x+3.76
Step 7, Loss:  9.74; Result: y=7.83*x+4.03
Step 8, Loss:  6.64; Result: y=8.25*x+4.24
Step 9, Loss:  4.66; Result: y=8.59*x+4.41
Step 10, Loss:  3.38; Result: y=8.85*x+4.55
Step 11, Loss:  2.56; Result: y=9.07*x+4.66
Step 12, Loss:  2.04; Result: y=9.24*x+4.74
Step 13, Loss:  1.71; Result: y=9.38*x+4.81
Step 14, Loss:  1.49; Result: y=9.49*x+4.87
Step 15, Loss:  1.35; Result: y=9.58*x+4.91
Step 16, Loss:  1.26; Result: y=9.65*x+4.95
Step 17, Loss:  1.21; Result: y=9.71*x+4.98
Step 18, Loss:  1.17; Result: y=9.75*x+5.00
Step 19, Loss:  1.15; Result: y=9.79*x+5.02
Step 20, Loss:  1.13; Result: y=9.82*x+5.03
Step 1, Loss:  125.25; Result: y=1.98*x+1.02
Step 2, Loss:  80.72; Result: y=3.56*x+1.83
Step 3, Loss:  52.16; Result: y=4.83*x+2.49
Step 4, Loss:  33.84; Result: y=5.85*x+3.01
Step 5, Loss:  22.10; Result: y=6.66*x+3.42
Step 6, Loss:  14.57; Result: y=7.31*x+3.76
Step 7, Loss:  9.74; Result: y=7.83*x+4.03
Step 8, Loss:  6.64; Result: y=8.25*x+4.24
Step 9, Loss:  4.66; Result: y=8.59*x+4.41
Step 10, Loss:  3.38; Result: y=8.85*x+4.55
Step 11, Loss:  2.56; Result: y=9.07*x+4.66
Step 12, Loss:  2.04; Result: y=9.24*x+4.74
Step 13, Loss:  1.71; Result: y=9.38*x+4.81
Step 14, Loss:  1.49; Result: y=9.49*x+4.87
Step 15, Loss:  1.35; Result: y=9.58*x+4.91
Step 16, Loss:  1.26; Result: y=9.65*x+4.95
Step 17, Loss:  1.21; Result: y=9.71*x+4.98
Step 18, Loss:  1.17; Result: y=9.75*x+5.00
Step 19, Loss:  1.15; Result: y=9.79*x+5.02
Step 20, Loss:  1.13; Result: y=9.82*x+5.03

拓展:广播机制

广播机制(Broadcasting)是指在进行运算时,自动扩展较小的张量以匹配较大的张量的尺寸,而不需要复制数据。这种机制可以提高运算效率,节省内存。
广播机制允许两个形状不同的张量进行运算。其基本规则如下:
每个张量至少有一个维度。
从末尾维度开始遍历,两个张量的每个对应维度需要满足以下条件之一: 维度相等。 其中一个维度为1。 其中一个维度不存在^3^。
如果两个张量是“可广播的”,则计算过程遵循以下规则:
在较小张量前面增加维度,使其维度与较大张量相等。
计算结果的维度值取两个张量中较大的那个值。
扩展维度的过程是将数值进行复制。


以下是一些使用广播机制的示例代码:
import torch

# 示例1:两个张量的维度相同
a = torch.rand(4, 32, 14, 14)
b = torch.rand(4, 32, 14, 14)
print((a + b).shape) # 输出:torch.Size([4, 32, 14, 14])

# 示例2:一个张量的某些维度为1
a = torch.rand(4, 32, 14, 14)
b = torch.rand(1, 32, 1, 1)
print((a + b).shape) # 输出:torch.Size([4, 32, 14, 14])

# 示例3:一个张量的维度较小
a = torch.rand(4, 32, 14, 14)
c = torch.rand(32, 1, 1)
print((a + c).shape) # 输出:torch.Size([4, 32, 14, 14])

注意事项
在使用广播机制时,需要注意以下几点:
确保张量的维度符合广播规则,否则会引发错误。
避免在原地操作中使用广播,因为原地操作不允许改变张量的形状。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐