神经网络自我构建

1、数据集

从数据集的来源不同,我们对数据集的导入分为两种:

1.1、从第三方库中导入

使用PyTorch内置的数据集类(如torchvision.datasets.ImageFolder、torchvision.datasets.CIFAR10等),这些类提供了访问常见数据集的简单方法。
下面是一个加载CIFAR10数据集的示例代码

 1 import torchvision.datasets as datasets
 2 import torchvision.transforms as transforms
 3 
 4 # 定义数据预处理
 5 transform = transforms.Compose(
 6     [transforms.ToTensor(),
 7      transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
 8 

#函数介绍
transforms.Compose 函数,用于将多个变换组合成一个可调用的变换对象。
预处理常用的变换包括对图像进行缩放、裁剪、旋转、归一化等操作,以便更好地适应训练模型
#参数列表:
transforms.ToTensor()表示将图片转换成向量
transforms.Normalize() 则是将每个通道的像素值减去均值(在这里是 0.5)并除以标准差
(在这里也是 0.5),以使得每个通道的像素值都在 -1 到 1 之间。

 9 # 加载CIFAR10数据集
10 trainset = datasets.CIFAR10(root='./data', train=True,
11                                         download=True, transform=transform)
12 testset = datasets.CIFAR10(root='./data', train=False,
13                                        download=True, transform=transform)

#以下是参数列表
root:数据集下载位置
train:表示是否为训练集,true表示是训练集,false的话一般是测试集
transform:参数指定了如何对数据进行预处理(或变换),此处参数指定了在上方所写的预处理方式
#至此,我们得到名为trainset的变量,它是一个包含 CIFAR-10 训练集的 torchvision.datasets.CIFAR10 对象,其中包含了处理后的图像数据和对应的标签。

一张彩色的图片有三个颜色通道-------由三个颜色(red,green,blue)矩阵表示,那很明显一个像素矩阵是表示不了的,所以得用三个矩阵来表示。
然而,当你把图像用三个矩阵表示出来并输入计算机中,计算机怎么知道这是一个彩色的图片而不是三个黑白图呢?

这里们需要引入一个新的数据形式,张量(tensor)

什么是张量?

PyTorch 中的所有内容都基于 Tensor(张量) 操作的。张量可以具有不同的尺寸,它可以是 1 维(标量),2 维(矢量),甚至 3 维(矩阵)或更高。

特点:

  • 比 NumPy 更灵活,可以使用 GPU 的强大计算能力。
  • 开源高效的深度学习研究平台。

扩充知识:

张量的创建

torch.empty( ):创建一个未初始化的tensor

1 import torch
2 torch.empty(2,3)
3 # 或者 torch.empty(2,3,out = result);print(result) # torch创建张量的每一个函数都有参数out:提供输出 Tensor 作为参数
4 '''
5 tensor([[1.1692e-19, 1.5637e-01, 5.0783e+31],
6         [4.2964e+24, 2.6908e+20, 2.7490e+20]])
7 '''

torch.rand(size):随机初始化值在 0-1 之间的tensor(服从均匀分布)

1 # torch.rand(size)
2 torch.rand(5, 3)  # 初始化 5*3 大小的 0-1 之间的张量

torch.zeros(size) /torch.ones(size):初始化全为 0/全为 1的tensor

1 x = torch.zeros(5, 3)
2 y = torch.ones(5, 3)
3 print(x)
4 print(y)

torch.tensor(list):创建指定值的tensor

创建 Tensor 并使用现有数据初始化,list 可以为 NumPy 中的一个列表。

1 #创建的张量中的值为 [5.5,3]
2 x = torch.tensor([5.5, 3])
3 print(x)
4 print(x.size())

1.2、导入本地的数据集

自定义数据集类,这个方法适用于加载自己的数据集。下面是一个示例代码,后面再做详细解释:

 1 import torchvision
 2 LOAD_CIFAR = True
 3 DOWNLOAD_CIFAR = True
 4 
 5 train_data = torchvision.datasets.CIFAR10(
 6     root='./data/',
 7     train=True,
 8     transform=torchvision.transforms.ToTensor(),
 9     download=DOWNLOAD_CIFAR,
10 )

(1)怎么将多个图片(几千,甚至几万张)导入代码中,并存储起来?
我们并不是将图片都读入代码中,我们只是将图片对应的文件地址以及标签对应的名字分别写入图片列表列表和标签列表中。

(2)图片数据集往往是有标注的,但是图片不能写文字,所以怎么将标注和图片一一对应?
将图片和标注一一对应可以通过两种方式实现:

(3)怎么区分数据集和训练集合?

CustomDataset是一个自定义的数据集类,它的构造函数需要两个参数:
txt_path和transform。其中,txt_path是包含数据集信息的文本文件的路径,
transform是对数据集进行预处理的函数或变换。
train_dataset = CustomDataset(txt_path='./train.txt', transform=transform)
test_dataset = CustomDataset(txt_path='./test.txt', transform=transform)

(4)怎么使用这些数据集?因为我们不可能一张一张的调用吧,最好想上方代码中一样,交给一个对象管理

开发者自然想到这个问题,为此提供了一个专门的数据类数据加载器(DataLoader),这个类很重要,神经网络训练都是使用这个类,其主要用于将数据集分批加载到内存中,以便在训练过程中更高效地处理数据。

1 dataloader = DataLoader(dataset, batch_size=3, shuffle=True)
2 #其参数含义如下
3             dataset是一个PyTorch的数据集对象,它包含了整个数据集。
4                batch_size=3:每个批次中包含3个数据样本。
5             shuffle=True:每次获取批次数据时是否对数据集进行随机打乱操作,这样可以增加模型的鲁棒性,
6                    防止模型对数据的顺序产生过拟合。
 DataLoader是PyTorch中一个用于加载数据的工具类,它能够将数据集中的数据转换成批次的数据,方便机器学习模型的训练。

2、构建模型

 1 import torch
 2 from torch import nn
 3 from torch.nn import functional as F
 4 class MLP(nn.Module):
 5     # 用模型参数声明层。这里,我们声明两个全连接的层
 6     def __init__(self):
 7         super().__init__() # 调用MLP的父类Module的构造函数来执行必要的初始化。
 8         self.hidden = nn.Linear(20, 256)  # 隐藏层,参数(20256)代表了权重矩阵
 9         self.out = nn.Linear(256, 10)  # 输出层,参数(256, 10)代表了权重矩阵
10             #以上代码我们便构建了:
11             #第一层(输入层)有20个参数,
12             #第二层(隐藏层)有256个参数,
13             #第三层(输出层)有10个参数    
14             # 的神经网络,当然如果你想要更深或更大的网络可以自行调整,这就是所谓的超参数。
15 这就解决了我们上面提出的第4个问题
16 
17 
18 
19     # 定义模型的前向传播,即如何根据输入X返回所需的模型输出
20     def forward(self, X):
21                 # 注意,这里我们使用ReLU的函数版本,其在nn.functional模块中定义。
22         return self.out(F.relu(self.hidden(X)))
23                 #这里采用了逐层调用的方式实现传播,即前一层的输出作为下一层的输入,
24                 #采用 .调用 的形式会被认为全连接调用。
25                     这里解决了上述的第2和第5个问题
26 net = MLP()#实例化这个类
27 X=torch.rand(2,20)#随机产生一个一组张量
28 net(X)#将这个这两输入到模型中,得到最终结果。

我们无论实现一个两层、三层、还是几十层的网络,都是这几个步骤,
唯一的区别在于,每个模型的:网络层数、参数数目,激活函数类别不同

1 import torch
2 from torch import nn
3 from torch.nn import functional as F
4 
5 net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
6 
7 X = torch.rand(2, 20)
8 net(X)

这段代码含义

  1. 这段代码定义了一个神经网络模型 net,该模型包含两个全连接层和一个 ReLU 激活函数
  2. 具体来说,nn.Sequential 表示将一个序列的层按照顺序串联起来,每层的输出作为下一层的输入。
  3. 接着,nn.Linear(20, 256) 表示一个从输入大小为 20,输出大小为 256 的全连接层,即将输入的 20维数据通过矩阵乘法映射到一个 256 维的空间中。
  4. nn.ReLU() 表示激活函数为 ReLU,可以通过非线性映射提取输入的特征,从而增强模型的表达能力。
  5. 最后一个 nn.Linear(256, 10) 表示一个从输入大小为 256,输出大小为 10 的全连接层,即将 256 维的特征向量映射为一个 10 维的向量,每个维度表示相应类别的得分。

nn.ReLU()层的位置不可以随便放。nn.ReLU()是一个激活函数层,一般情况下需要紧接着一个全连接层使用,用于提取非线性特征。如果将nn.ReLU()层放在全连接层的前面,就无法提取到输入的非线性特征了,这会严重影响模型的性能。

但是在某些特殊情况下,例如深度残差网络 (ResNet) 中,激活函数层与卷积层之间还有一个残差块,因此其位置就可以放置在更靠后的地方。但这个需要具体问题具体分析。

3、训练

人工智能得学习,反映在代码程序中,就是所谓的参数。每个神经元连接都有着一个参数

参数越多越好吗?

答案:不一定。如果你想实现更复杂的功能,则必不可少的得使用更多的参数,但是参数太多的话会导致过拟合的现象,但是太少的化会导致欠拟合

如果我把所有的参数都集中在一层,可不可以?

这是一个非常值得品味的问题,既然足够多的参数就能实现相应的功能,我们为什么不把这些参数放在同一层呢?到时候也不叫什么深度学习,改名叫宽度学习就行了。
这个问题,在早些之前确实存在这种说法,足够宽的神经元也能实现相应的功能,但是专家们经过实验发现,同等表示能力下,深度神经网络比“宽度”神经网路网络有更少的参数,所以只需要更少的数据就可以学校到,也就是说,深度神经网络有更低的样本复杂度。这也是深度神经网路更加实用的原因。

每层的参数怎么定

刚开始时候,我们人为肯定没办法定一个完美的参数–————不用训练就实现了那种。所以,大部分情况下参数都是人为随机指定的,常用的方法就是:

1 w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
2 #参数w按照均值0,方差1的正态分布初始化
3 #requires_grad=True表示进行梯度计算。
4 b = torch.zeros(1, requires_grad=True)
5 #参数b,置为0

机器怎么“学习”?或者说参数怎么更新。

我们把输入的数据经过第一层运算后,传递给第二层,然后第二层传递给第三层…直到输出层。这种计算成为前向传播
经过一次前向传播后,我们在输出层得到一个结果,我们将得到结果与正确的结果最对比,我们利用损失函数计算误差(也叫损失),得到损失值;损失函数有着多种,针对不同领域有着不同的损失函数,如果你觉得这些损失函数不够完美,读者也可以自定义一种损失函数。
得到损失值后,我们就要开始更新参数了:
更新参数采用的是梯度下降算法:

新的参数=原来的参数----学习率 * 损失值对该参数的偏导

损失值对该参数的偏导又称为该参数的梯度;值得注意的是,经过人工智能的发展,梯度下降算法也被人们不断优化,人们接连提出了随机梯度下降(SGD),已经现在常用的Adm优化算法
经过反向传播完成一次参数更新后,即完成了一次训练。

以下我们展示下,如何定义损失函数、优化算法以及如何进行一次参数更新:
定义损失函数

def squared_loss(y_hat, y):  #@save
    """均方损失"""
    return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2

定义优化算法

def sgd(params, lr, batch_size):  #@save
    """小批量随机梯度下降"""
    with torch.no_grad():
        for param in params:
            param -= lr * param.grad / batch_size
            param.grad.zero_()

采用的**随机梯度下降算法(SGD)**进行参数更新lr是学习率,param.grad是参数的梯度, batch_size是批量大小。

定义训练函数

主要步骤就是:计算损失值------由损失值计算梯度------由梯度进行反向传播优化
 1 lr = 0.03
 2 num_epochs = 3
 3 net = linreg
 4 loss = squared_loss
 5 
 6 for epoch in range(num_epochs):
 7     for X, y in data_iter(batch_size, features, labels):
 8         l = loss(net(X, w, b), y)  # X和y的小批量损失
 9         # 因为l形状是(batch_size,1),而不是一个标量。l中的所有元素被加到一起,
10         # 并以此计算关于[w,b]的梯度
11         l.sum().backward()
12         sgd([w, b], lr, batch_size)  # 使用参数的梯度更新参数
13     with torch.no_grad():
14         train_l = loss(net(features, w, b), labels)
15         print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')

当然像这种函数早已被前驱给封装好,我们可通过第三方库进行直接调用,而你需要做的就是指定参数。
以下是对上述代码的简洁实现:

1 loss = nn.MSELoss() 
#使用MSELoss损失函数,即均方损失函数,与上述代码损失函数想同
1 optimizer= torch.optim.SGD(net.parameters(), lr=0.03)
2 #定义优化算法,这里采用的是SGD(随机梯度下降)优化算法,学习率为0.03
3  和上述的优化器代码相同
1 num_epochs = 10
2 d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, optimizer)

这里采用的都d2l库中的函数,net是我们所定义的网咯模型, train_iter, test_iter是我们之前提到的很重要的数据迭代器loss是定义的损失函数, num_epochs是指定的训练次数; optimizer是定义的优化器

 

posted @ 2024-02-01 00:02  taohuaxiaochunfeng  阅读(18)  评论(0编辑  收藏  举报