实现线性回归——【torch学习笔记】

实现线性回归

引用翻译:《动手学深度学习》

深度学习的热潮激发了各种成熟的软件框架的发展,这些框架将实现深度学习模型的大部分重复性工作自动化。

可以仅依靠NDarray进行数据存储和线性代数,以及autograd软件包中的自动分化功能。

在实践中,由于许多更抽象的操作,如数据迭代器、损失函数、模型架构和优化器,都非常普遍,深度学习库也会给我们提供这些库函数。

在本节中,我们将学习如何用DataLoader更简洁地实现的线性回归模型。

一、生成数据集

首先,我们将生成与上一节中使用的相同的数据集。

import torch
import numpy as np
def synthetic_data(w, b, num_examples):
    """
    根据公式:
    generate y = X w + b + noise
    随机生成数据,便于进行参数的比较
    """
    X = np.random.normal(scale=1, size=(num_examples, len(w)))
    y = np.dot(X, w) + b
    y += np.random.normal(scale=0.01, size=y.shape)
    X=torch.from_numpy(X).float()
    y=torch.from_numpy( y).float().reshape(-1,1)
    return X,y
true_w = torch.Tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)

数据结构案例:可发现是一个二维回归的数据集

print('features:',features[1:5])
print('labels:',labels[1:5])
features: tensor([[-0.5174,  0.7613],
        [ 0.3949, -0.6982],
        [-1.0928,  0.2136],
        [-1.2361,  1.0599]])
labels: tensor([[ 0.5729],
        [ 7.3657],
        [ 1.2911],
        [-1.8883]])

二、读取数据

我们可以调用DataLoader模块来读取数据,而不是滚动我们自己的迭代器。第一步将是实例化一个ArrayDataset,它需要一个或多个NDArrays作为参数。在这里,我们将特征和标签作为参数传入。接下来,我们将使用ArrayDataset来实例化一个DataLoader,这也需要我们指定一个batch_size,并指定一个布尔值shuffle,表示我们是否希望DataLoader在每个周期(通过数据集)来洗数据。

TensorDataset 可以用来对 tensor 进行打包,就好像 python 中的 zip 功能。该类通过每一个 tensor 的第一个维度进行索引。因此,该类中的 tensor 第一维度必须相等。输出是 一般通过切片,如dataset[1:5]等形式,或者使用:

for x_train, y_label in dataset: 的形式进行分离。

from torch.utils.data import TensorDataset,DataLoader
def load_array(data_arrays, batch_size, is_train=True):
    """
    data_arrays参数可以是一个或多个NDArrays作为参数。
    
    如 data_arrays传入:(features, labels)
    """
    dataset=TensorDataset(*(features,labels))
    dataloader = DataLoader(dataset=dataset,batch_size=batch_size,shuffle=True)
    return dataloader

batch_size =10
data_iter = load_array((features, labels), batch_size)
(tensor([[-0.5174,  0.7613],
        [ 0.3949, -0.6982],
        [-1.0928,  0.2136],
        [-1.2361,  1.0599]]), tensor([[ 0.5729],
        [ 7.3657],
        [ 1.2911],
        [-1.8883]]))

现在我们可以用与上一节中调用data_iter函数一样的方式来使用data_iter。
节中调用的一样。为了验证它的工作,我们可以读取并打印第一批小的实例。

for X, y in data_iter:
    print(X)
    print(y)
    break
tensor([[ 1.2312, -1.5933],
        [ 0.0038, -0.6301],
        [-0.8138, -0.0938],
        [-0.8518,  1.0997],
        [-2.1277, -0.2154],
        [ 0.1531, -1.4706],
        [-0.4099,  0.1905],
        [ 0.2857,  0.1269],
        [-1.1213, -0.5866],
        [ 0.6277, -0.3096]])
tensor([[12.0653],
        [ 6.3439],
        [ 2.9058],
        [-1.2391],
        [ 0.6782],
        [ 9.5031],
        [ 2.7404],
        [ 4.3432],
        [ 3.9594],
        [ 6.5159]])

三、定义模型

当我们在上一节中从头开始实现线性回归时,我们必须定义模型参数,并明确写出计算结果,用基本的线性代数操作产生输出。你应该知道如何做到这一点。但是一旦你的模型变得更加复杂,即使是对模型进行定性的简单改变也可能导致许多低级别的改变。

我们将 torch.nn 导入为 nn .对于标准操作,我们可以使用 nn 的预定义层,这使得我们可以特别关注用于构建模型的层,而不必关注实现。为了定义一个线性模型,我们首先导入nn模块,它定义了大量的神经网络层(注意,"nn "是神经网络的缩写)。我们将首先定义一个模型变量net,它是一个Sequential实例。在nn中,Sequential实例可以被视为一个容器,它将各个层依次串联起来。当输入数据被给定时,容器中的每一层都会被依次计算,一层的输出将是下一层的输入。在这个例子中,由于我们的模型只包括一个层,所以我们并不真正需要Sequential。但由于我们未来几乎所有的模型都会涉及到多层,所以让我们尽早养成这个习惯。回顾一下单层网络的结构。该层是全连接的,因为它通过矩阵-向量乘法的方式连接所有的输入和所有的输出。在nn中,全连接层被定义在线性类中。由于我们只想生成一个单一的标量输出,所以我们把这个数字设为1。

from IPython.display import SVG
SVG(filename="../img/singleneuron.svg")


class LinearRegressionModel(torch.nn.Module): 
    def __init__(self): 
        super(LinearRegressionModel, self).__init__() 
        self.layer1 = torch.nn.Linear(2, 1,bias=True)
    def forward(self, x): 
        y_pred = self.layer1(x)
        return y_pred 
    
net = LinearRegressionModel()  # 此时的net即相当于平时的model

四、初始化模型参数

在使用net之前,我们需要初始化模型参数,比如线性回归模型中的权重和偏置。我们指定每个权重参数应该从均值为0、标准差为0.01的正态分布中随机抽样。默认情况下,偏置参数将被初始化为零。权重和偏置都将被附加上梯度。

net.layer1.weight.data=torch.Tensor(np.random.normal(size=(1,2),scale=0.01,loc=0))
net.layer1.bias.data=torch.Tensor([0])
# 其中参数值如下:
print('w:',np.random.normal(size=(1,2),scale=0.01,loc=0))  # 权重
print('b:',torch.Tensor([0]))  # 偏置
w: [[-0.00667654 -0.00194487]]
b: tensor([0.])

上面的代码看起来很简单,但实际上这里发生了很奇怪的事情。我们正在为一个网络初始化参数,尽管我们还没有告诉 nn 输入有多少维。在我们的例子中,它可能是2,也可能是2000,所以我们不能只是预先分配足够的空间来使它工作。要注意的是,由于参数还没有被初始化,我们还不能以任何方式操纵它们。

五、定义损失函数

在nn中,有许多损失模块,定义了各种损失函数,我们将直接使用其实现的平方损失(MSELoss)。

loss = torch.nn.MSELoss(reduction = "sum") 

六、定义优化算法

不足为奇的是,我们并不是第一个实现小批量随机梯度下降的人,因此Torch通过其Trainer类支持SGD以及这种算法的一些变体。当我们实例化Trainer时,我们将指定要优化的参数

(可通过net.parameters()从我们的网络中获得),我们希望使用的优化算法(SGD),以及我们优化算法所需的超参数字典。SGD只要求我们设置学习率的值,(这里我们把它设置为0.03)。

trainer = torch.optim.SGD(net.parameters(), lr = 0.03) 

七、训练

你可能已经注意到,通过Torch表达我们的模型需要相对较少的代码行。我们不必单独分配参数,定义损失函数,或实现随机梯度下降。一旦我们开始处理更复杂的模型,依靠火炬抽象的好处将大大增加。但是,一旦我们有了所有的基本部件,训练循环本身就与我们从头开始实现一切时的情况惊人地相似。为了唤起你的记忆:在一定数量的epochs中,我们将对数据集(train_data)进行一次完整的传递,每次抓取一个小型批次的输入和相应的地面真实标签。

对于每个批次,我们将经历以下程序。

  • 通过调用net(X)生成预测结果,并计算损失l(正向传递)。

  • 通过调用l.backward()来计算梯度(后向通道)。

  • 通过调用SGD优化器来更新模型参数(注意,训练器已经知道要优化哪些参数,所以我们只需要传入批次大小。

为了慎重起见,我们在每个历时后计算损失,并打印出来以监测进展。

num_epochs = 8
for epoch in range(num_epochs): 
    for X,y in data_iter:
        l=loss(net(X) ,y)
        trainer.zero_grad() 
        l.backward() 
        trainer.step() 
    l_epoch = loss(net(features), labels) 
    print('epoch {}, loss {}'.format(epoch+1, l_epoch)) 
epoch 1, loss 0.10564027726650238
epoch 2, loss 0.11902596801519394
epoch 3, loss 0.10855665057897568
epoch 4, loss 0.1051761731505394
epoch 5, loss 0.09899577498435974
epoch 6, loss 0.10524554550647736
epoch 7, loss 0.1103854775428772
epoch 8, loss 0.11089073866605759
print(net(X))
print(y)
tensor([[ 6.7532],
        [ 1.1355],
        [ 5.8866],
        [ 2.1347],
        [ 5.6749],
        [-1.3126],
        [ 1.0842],
        [ 0.8536],
        [ 6.2930],
        [ 7.7130]], grad_fn=)
tensor([[ 6.7607],
        [ 1.1386],
        [ 5.8751],
        [ 2.1213],
        [ 5.6716],
        [-1.3175],
        [ 1.0893],
        [ 0.8706],
        [ 6.2715],
        [ 7.7113]])

我们学到的模型参数和实际的模型参数进行比较,如下图。我们从网中得到我们需要的层,并访问其权重(weight)和偏置(bias)。我们学到的参数和实际参数非常接近。

w = list(net.parameters())[0][0]
print('估计w的误差', true_w.reshape(w.shape) - w)
b = list(net.parameters())[1][0]
print('估计b的误差', true_b - b)
估计w的误差 tensor([-0.0008,  0.0004], grad_fn=)
估计b的误差 tensor(-0.0004, grad_fn=)

八、预测

预测时,需要保障输入与训练集的输入一致,若做了数据变化的也要进行同样的处理,避免数据口径不一致导致预测异常。

转化为相同规格的张量后,载入网络进行预测

# 对新样本点进行预测
print(X) # 先观察训练集格式
tensor([[ 1.2156, -0.0380],
        [ 0.7361,  1.3330],
        [ 1.7320,  0.5208],
        [ 0.2738,  0.7670],
        [ 0.1346, -0.3566],
        [ 0.0375,  1.6423],
        [-0.5170,  0.6109],
        [-0.8291,  0.4951],
        [-0.8588, -1.1228],
        [ 0.8781, -0.5189]])

转换数据为对应的张量:

# 按照样本点格式,转化为torch
new_data = torch.tensor([[ 1.26156, -0.0380],
        [ 0.7961,  1.3330]])   # 新样本点

调用模型进行预测:

# 调用模型进行预测
predict = net(new_data)
print(predict)  # 输出预测值
tensor([[6.8452],
        [1.2552]], grad_fn=)

九、练习

  1. 如果我们用l = loss(output, y).mean()代替l = loss(output, y),我们需要将trainer.step(batch_size)相应改为trainer.step(1)。为什么?

  2. 查看pytorch文档,看看提供了哪些损失函数和初始化方法在nn.loss和init模块中,用Huber的损失来代替损失。

  3. 如何访问Linear.weight的梯度?

你可能感兴趣的:(深度学习——torch学习笔记,线性回归,深度学习,torch,神经网络)