在上一篇博客中介绍了tensor的基本数据变换:Pytorch入门篇2 玩转tensor(查看、提取、变换)。通过掌握数据变换,我们能够熟练的在pytorch中使用各种类型的数据进行科学计算,为使用pytorch搭建神经网络提供了数据层面的支持。本篇博客将更近一步讲解如何使用pytorch进行梯度传播,并通过梯度传播实现一个线性回归的实战案例。
众所周知,所谓的神经网络就是一个拥有众多参数的复杂函数y=f(x),深度学习就是要从数据x和标签y中确定出神经网络中的各个参数。这个过程本质上是一个最优化问题。既然涉及到寻找最优参数,就避免不了进行参数更新,目前的神经网络基本都是根据梯度传播机制实现参数更新。
注:本系列博客着重讲解如何使用Pytorch实现神经网络,对神经网络的一些基本概念和数学层面的实现逻辑不做过多介绍,如果多这些底层思想不了解的同学,建议先去学习一些神经网路网络的基本知识,例如观看吴恩达老师的深度学习课程。
当创建一个张量时,可以指定它是否需要梯度requires_grad=True
。这一操作会使得PyTorch能够跟踪该张量的计算历史并自动计算梯度。
下面给出一个简单的示例,演示如何使用PyTorch自动计算梯度。在示例中,我们定义了一个函数 y = 3 x 2 + 2 x + 1 y=3x^2+2x+1 y=3x2+2x+1并计算了在 x = 2 x=2 x=2处的梯度。
import torch
# 创建一个张量,设置 requires_grad=True 以开启梯度计算
x = torch.tensor(2., requires_grad=True)
# 定义一个函数
y = 3*x**2 + 2*x + 1
# 反向传播并计算梯度
y.backward()
# 查看 x 的梯度值
print(x.grad)
运行上述代码,,通过x.grad
查看x的梯度值。可以得到输出结果14.0,即 y = 3 x 2 + 2 x + 1 y=3x^2+2x+1 y=3x2+2x+1在 x = 2 x=2 x=2处的梯度为 14 14 14。(可以手工计算验证,求y在x=2处的导数)
在此次线性回归实验中,我们提前准备了一组x和y的数据,并将其存储在.csv文件中。这组数据是我们通过程序生成的,它大概符合 y = 4 x + 3 y=4x+3 y=4x+3的线性数据分布,我们在其中加入了一定程度的噪声。数据生成程序为:
import numpy as np
import pandas as pd
# 设置随机种子,以便每次运行代码时生成的随机数相同
np.random.seed(42)
# 生成随机的x值
x = np.random.rand(100)
# 计算每个x值对应的y值,并添加一些随机噪声
y = 3 * x + 4 + 0.2 * np.random.randn(100)
# 将x和y组成一个二维数组,并将其转化为DataFrame
data = pd.DataFrame(np.column_stack([x,y]), columns=['x', 'y'])
# 将DataFrame保存为csv文件
data.to_csv('./data1.csv', index=False)
大家可以根据程序自行生成数据,在之后的线性回归程序中对数据data1进行调用。
对数据进行可视化:
import matplotlib.pyplot as plt
import numpy as np
# 创建数据
points = np.genfromtxt("./data1.csv", delimiter=",")
# 画散点图
plt.scatter(points[:,0],points[:,1])
# 设置坐标轴标签
plt.xlabel('x label')
plt.ylabel('y label')
# 显示图形
plt.show()
为了让大家更好的理解线性回归实现的底层逻辑,下面仅依靠Numpy手动实现线性规划。
在此次线性回归实验中,我们定义回归函数为 y = w x + b y=wx+b y=wx+b。一开始, w w w和 b b b随机初始化,然后通过梯度传播机制不断更新w和b,最后获取最优的回归结果。其中我们将衡量回归结果的Loss定义为: L o s s = ( w x + b − y ) 2 Loss=(wx+b-y)^2 Loss=(wx+b−y)2。
Loss计算程序为:
def compute_error_for_line_given_points(b, w, points):
totalError = 0
for i in range(0, len(points)):
x = points[i, 0]
y = points[i, 1]
totalError += (y - (w * x + b)) ** 2
return totalError / float(len(points))
遵循梯度传播机制,回归函数中两个参数w和b的更新公式为:
w = w − α ∂ L ∂ w w = w - \alpha\frac{\partial L}{\partial w} w=w−α∂w∂L
b = b − α ∂ L ∂ b b = b - \alpha\frac{\partial L}{\partial b} b=b−α∂b∂L
其中L为loss函数的值, α \alpha α为学习率。
def step_gradient(b_current, w_current, points, learningRate):
b_gradient = 0
w_gradient = 0
N = float(len(points))
for i in range(0, len(points)):
x = points[i, 0]
y = points[i, 1]
b_gradient += -(2/N) * (y - ((w_current * x) + b_current))
w_gradient += -(2/N) * x * (y - ((w_current * x) + b_current))
new_b = b_current - (learningRate * b_gradient)
new_m = w_current - (learningRate * w_gradient)
return [new_b, new_m]
通过循环,不断的计算Loss并更新 w w w和 b b b,最后确定出最优的 w w w和 b b b作为线性回归的最终结果。
def gradient_descent_runner(points, starting_b, starting_m, learning_rate, num_iterations):
b = starting_b
m = starting_m
for i in range(num_iterations):
b, m = step_gradient(b, m, np.array(points), learning_rate)
return [b, m]
基于numpy实现的线性回归完整程序如下:
import numpy as np
# y = wx + b
def compute_error_for_line_given_points(b, w, points):
totalError = 0
for i in range(0, len(points)):
x = points[i, 0]
y = points[i, 1]
totalError += (y - (w * x + b)) ** 2
return totalError / float(len(points))
def step_gradient(b_current, w_current, points, learningRate):
b_gradient = 0
w_gradient = 0
N = float(len(points))
for i in range(0, len(points)):
x = points[i, 0]
y = points[i, 1]
b_gradient += -(2/N) * (y - ((w_current * x) + b_current))
w_gradient += -(2/N) * x * (y - ((w_current * x) + b_current))
new_b = b_current - (learningRate * b_gradient)
new_m = w_current - (learningRate * w_gradient)
return [new_b, new_m]
def gradient_descent_runner(points, starting_b, starting_m, learning_rate, num_iterations):
b = starting_b
m = starting_m
for i in range(num_iterations):
b, m = step_gradient(b, m, np.array(points), learning_rate)
return [b, m]
def run():
points = np.genfromtxt("./data1.csv", delimiter=",")
learning_rate = 0.1
initial_b = 0 # initial y-intercept guess
initial_m = 0 # initial slope guess
num_iterations = 1000
print("Running...")
[b, m] = gradient_descent_runner(points, initial_b, initial_m, learning_rate, num_iterations)
print("After {0} iterations b = {1}, m = {2}, error = {3}".
format(num_iterations, b, m,
compute_error_for_line_given_points(b, m, points))
)
if __name__ == '__main__':
run()
从第四部分可以看出,一个简单的线性回归需要编写很多的代码,这种策略对于搭建更加复杂的神经网络来说显然是不可取的。pytorch就是为了解决这一问题而生的。他通过内置神经网络种的各类功能型函数方便用户快速调用,节省了大量的基础编程时间,使得用户可以将主要精力投身到更高阶的算法设计中。
import torch
#设置随机种子
torch.manual_seed(42)
# 定义模型
class LinearRegressionModel(torch.nn.Module):
def __init__(self):
super(LinearRegressionModel, self).__init__()
self.linear = torch.nn.Linear(1, 1) # 使用一个线性层 transformation: y = wx+b
def forward(self, x):
y_pred = self.linear(x)
return y_pred
def run():
#1 导入数据集
points = np.genfromtxt("./data1.csv", delimiter=",")
x = points[:, 0]
y = points[:, 1]
# 将NumPy数组转换为张量
x_data = torch.from_numpy(x.reshape(100,1)).float()
y_data = torch.from_numpy(y.reshape(100,1)).float()
#2 定义线性回归模型
model = LinearRegressionModel()
#3 定义损失函数和优化器
criterion = torch.nn.MSELoss(reduction='mean')
optimizer = torch.optim.Adam(model.parameters(), lr=0.1)
#4 训练模型
num_iterations = 1000
print("Running...")
for epoch in range(num_iterations):
# 前向传播
y_pred = model(x_data)
# 计算loss
loss = criterion(y_pred, y_data)
# 梯度清零
optimizer.zero_grad()
# 反向传播
loss.backward()
# 更新参数
optimizer.step()
#5 打印训练结果
#将torch中的tensor矩阵参数转为numpy类型
m_martix = model.linear.weight.detach().numpy()
b_martix = model.linear.bias.detach().numpy()
m = m_martix[0][0]
b = b_martix[0]
print("After {0} iterations b = {1}, m = {2}, error = {3}".
format(num_iterations, b, m, loss.item())
)
if __name__ == '__main__':
run()
class torch.nn.Linear(in_features: int, out_features: int, bias: bool = True)
:这个模块应该被用于构建一个简单的线性神经网络。为了具有可学习的参数(权值和偏置项),在构造函数中指定输入和输出的大小。
参数
torch.nn.Linear(1, 1)
来构造线性回归函数。相较于与之前我们手动实现的 L o s s = ( w x + b − y ) 2 Loss=(wx+b-y)^2 Loss=(wx+b−y)2损失计算,Pytorch提供了各类已经集成好的损失函数,例如上面程序中调用的torch.nn.MSELoss(reduction='mean')
就是一种基于均方误差实现的损失函数;
Numpy实现的线性回归程序在处理参数更新部分时,学习率的步长时锁定不动的,但是Pytorch中提供了一些更好的处理策略来实时的调整学习率使得网络能够更快的收敛进而达到全局最优,在本程序中我们使用了Adam
方法来加速网络收敛。
虽然上面的程序仅仅实现了一个线性回归功能,但是它已经具备了Pytorch搭建神经网络的大部分组件。下面我们就以上面的程序为例,总结梳理使用Pytorch搭建神经网络的通用程序范式,通过掌握此范式,可以完成各类复杂神经网络的书写。想要快速学习pytorch神经网络程序的小伙伴可以重点关注!
__init__()
函数里定义模型的各个层(如卷积、池化、全连接等)并设置好输入输出数据尺寸;在forward()
函数里对各个层进行拼接,完成网络的搭建在上述两种方法实现的线性回归中,我们将迭代次数都设置为1000,进行拟合结果对比。
基于numpy的运行结果为:
Running…
After 1000 iterations b = 4.043019455022063, m = 2.9080449128447525, error = 0.032263382558699455
基于Pytorch的运行结果为:
Running…
After 1000 iterations b = 4.04301643371582, m = 2.908050060272217, error = 0.0322633795440197
从数据集生成部分我们知道正确的 w w w和 b b b应该是接近4和3的(因为数据中有噪音,所以不可能直接等于这个数)。从结果来看,两种方法实现的线性回归程序都接近这个结果,说明程序本身具备了我们所希望看到的拟合能力。
梯度传播是神经玩网络实现参数更新的通用方法,正是通过梯度传播网络才拥有了学习能力。Pytorch中提供了完善的梯度传播调用方法,虽然在实际编程中我们很少单独使用到这个梯度传播的功能,但了解它的底层逻辑和一些必要的调用技巧会加深我们对网络的理解,提高我们的编程能力。最后,博客中实现的基于Pytorch的线性回归程序很有代表性,它几乎涵盖了从零搭建一个神经网络所必要的全部步骤,值得学习。