AUGUSTUS的深度学习笔记二
写于2025年1月18日,说来惭愧,被期末考试折磨完后,可算可以重拾深度学习,与此同时我还要自学CUDA 编程,刷python算法题,做PPT,可谓忙的不亦乐乎,寒假自1月12日开始,至今尚未休息。我上次写深度学习笔记已经是20224年12月的事情了,和当时一样,本次还是基于《动手学深度学习》这本书来弄,期间我先是花费了足足一天来给电脑配上CUDA和PYtorch的开发环境,再又未经世事的想在win+vscode+cl编译器+cuda基础上配置cuda 的编程环境,最终大败而归,几乎成为笑柄。如今再次开始学,唏嘘不已,转行绝非易事,以我微电子这个神奇专业的背景来看更是如此,我上大学至今,如果不是打机器人比赛很可能三年不写代码。现在自D2L第五章深度学习计算开始弄。
备注:环境配置参考我博客中前面的文章,需要d2l包+conda-cuda-pytorch环境。本文中代码来源于动手学深度学习的ipynb格式的电子书中。
第五章-深度学习计算
本章对应在jupyter格式电子书中是chapter_deep learning computation.本文中主要介绍层和块的概念以及如何构建和计算。
层与块
层在神经网络中完成以下的工作:1.接收一些输入2.生成一些标量化的输出3.根据设定的参数完成目标哈四年的要求。而块是由python中的class定义的,包含多个层或者一个层。示例代码如下:
import torch
import torch.nn.functional as F#从torch的netural network里面的functional调用出来
from torch import nn
class CenteredLayer(nn.Module):
def __init__(self):#类中定义函数时候一般都要有self
super().__init__()
def forward(self, X):
return X - X.mean()
layer = CenteredLayer()#初始声明
layer(torch.FloatTensor([1, 2, 3, 4, 5]))
net = nn.Sequential(nn.Linear(8, 128), CenteredLayer())#调用nn.sequential函数来初始化一个顺序模型,其包括一个线性层和一个自己定义的层
Y = net(torch.rand(4, 8))#采用4*8的随即张量作为输入
Y.mean()
class MyLinear(nn.Module):
def __init__(self, in_units, units):#初始化函数
super().__init__()
self.weight = nn.Parameter(torch.randn(in_units, units))
self.bias = nn.Parameter(torch.randn(units,))
def forward(self, X):#定义前向传播函数
linear = torch.matmul(X, self.weight.data) + self.bias.data
return F.relu(linear)
linear = MyLinear(5, 3)
print(linear.weight)
linear(torch.rand(2, 5))
net = nn.Sequential(MyLinear(64, 8), MyLinear(8, 1))
net(torch.rand(2, 64))#使用随即生成的2*64的张良作为输入
注意:class定义的就是层,而调用nn.sequential是在层的基础上构建一个顺序模型。块的详细的定义说明如下所示:
class MLP(nn.Module):
# 用模型参数声明层。这里,我们声明两个全连接的层
def __init__(self):
# 调用MLP的父类Module的构造函数来执行必要的初始化。
# 这样,在类实例化时也可以指定其他函数参数,例如模型参数params(稍后将介绍)
super().__init__()
self.hidden = nn.Linear(20, 256) # 隐藏层
self.out = nn.Linear(256, 10) # 输出层
# 定义模型的前向传播,即如何根据输入X返回所需的模型输出
def forward(self, X):
# 注意,这里我们使用ReLU的函数版本,其在nn.functional模块中定义。
return self.out(F.relu(self.hidden(X)))
其中所出现的RELU函数表达式为,是整流函数,全名为Rectified Linear Unit,线性整流单元:
管理参数
在实际训练模型的时候可能涉及成千上万乃至上亿个参数,如何管理这些参数就成了关键的事情,pytorch中就给了参数管理的办法,代码示例如下,主要是查询神经网络参数。
import torch#导入Pytorch包
from torch import nn#从torch中导入netural network模组
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))#创建一个顺序模型,包含两个线性层和一个Relu激活函数
X = torch.rand(size=(2, 4))
net(X)#将数据输入神经网络前向传播
print(net[2].state_dict())#直接获取参数,获取的有权重和偏置
print(type(net[2].bias))#打印偏置的类型
print(net[2].bias)#打印偏置
print(net[2].bias.data)#打印偏置的数据
net[2].weight.grad == None#检查权重是否有梯度
print(*[(name, param.shape) for name, param in net[0].named_parameters()])#打印网络中所有层的参数名和形状。
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])
net.state_dict()['2.bias'].data#直接访问指定的参数
def block1():
return nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
nn.Linear(8, 4), nn.ReLU())
def block2():
net = nn.Sequential()
for i in range(4):
# 在这里嵌套
net.add_module(f'block {i}', block1())#block2嵌套block1
return net
rgnet = nn.Sequential(block2(), nn.Linear(4, 1))#创建顺序模型
rgnet(X)
print(rgnet)
rgnet[0][1][0].bias.data
def init_normal(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, mean=0, std=0.01)
nn.init.zeros_(m.bias)
net.apply(init_normal)
net[0].weight.data[0], net[0].bias.data[0]
def init_constant(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 1)
nn.init.zeros_(m.bias)
net.apply(init_constant)
net[0].weight.data[0], net[0].bias.data[0]
def init_xavier(m):
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight)
def init_42(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 42)
net[0].apply(init_xavier)
net[2].apply(init_42)
print(net[0].weight.data[0])
print(net[2].weight.data)
def my_init(m):
if type(m) == nn.Linear:
print("Init", *[(name, param.shape)
for name, param in m.named_parameters()][0])
nn.init.uniform_(m.weight, -10, 10)
m.weight.data *= m.weight.data.abs() >= 5
net.apply(my_init)
net[0].weight[:2]
net[0].weight.data[:] += 1
net[0].weight.data[0, 0] = 42
net[0].weight.data[0]
# 我们需要给共享层一个名称,以便可以引用它的参数
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
shared, nn.ReLU(),
shared, nn.ReLU(),
nn.Linear(8, 1))#创建共享层模型,多个线性层,三个激活函数
net(X)
# 检查参数是否相同
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100
# 确保它们实际上是同一个对象,而不只是有相同的值
print(net[2].weight.data[0] == net[4].weight.data[0])
'''激活函数在神经网络中扮演着至关重要的角色,它们的主要作用如下:
引入非线性:激活函数为神经网络引入了非线性特性,这使得神经网络能够学习和模拟复杂的函数映射。如果没有激活函数,神经网络就相当于一个线性回归模型,只能处理线性可分的问题,这大大限制了其能力。
决策功能:在神经网络的每个神经元中,激活函数决定了是否“激活”这个神经元,即是否将信号传递到下一层。例如,在ReLU激活函数中,只有当输入超过某个阈值(如0)时,神经元才会激活并传递信号。
梯度传播:在反向传播过程中,激活函数的导数(梯度)用于计算损失函数关于网络参数的梯度。这些梯度随后用于更新网络的权重,以优化网络的性能。
压缩和归一化:某些激活函数(如Sigmoid和Tanh)能够将输出压缩到特定的范围内,这有助于防止网络中的梯度消失或爆炸问题,并有助于数据的归一化。'''
读写内存
在实际训练模型中,有一些模型经常要训练好几天,为了防止丢失内容,就需要去保存运算后的张量,当然也就需要读取。示例代码如下:
import torch
from torch import nn
from torch.nn import functional as F
x = torch.arange(4)#创建一个包含四个元素的张量
torch.save(x, 'x-file')#保存
x2 = torch.load('x-file')
x2
y = torch.zeros(4)#创建四个零的张量
torch.save([x, y],'x-files')
x2, y2 = torch.load('x-files')
(x2, y2)
mydict = {'x': x, 'y': y}#创建字典类型
torch.save(mydict, 'mydict')
mydict2 = torch.load('mydict')
mydict2
class MLP(nn.Module):
def __init__(self):
super().__init__()
self.hidden = nn.Linear(20, 256)
self.output = nn.Linear(256, 10)
def forward(self, x):
return self.output(F.relu(self.hidden(x)))
net = MLP()
X = torch.randn(size=(2, 20))
Y = net(X)
torch.save(net.state_dict(), 'mlp.params')#保存训练参数
clone = MLP()
clone.load_state_dict(torch.load('mlp.params'))
clone.eval()
Y_clone = clone(X)
Y_clone == Y
调用GPU来计算
这段时间经常和cuda和英伟达的GPU打交道,实际上可知,python中调用CUDA是辅助训练,而研究并行计算需要CUDA C编程。第五章这里涉及的较少,主要是讲解如何把张量或者训练的模型存储在GPU上面。
示例代码如下:
import torch
from torch import nn
torch.device('cpu'), torch.device('cuda'), torch.device('cuda:1')#查询设备可以调用的GPU数目
torch.cuda.device_count()
def try_gpu(i=0): #@save
"""如果存在,则返回gpu(i),否则返回cpu()"""
if torch.cuda.device_count() >= i + 1:
return torch.device(f'cuda:{i}')
return torch.device('cpu')
def try_all_gpus(): #@save
"""返回所有可用的GPU,如果没有GPU,则返回[cpu(),]"""
devices = [torch.device(f'cuda:{i}')
for i in range(torch.cuda.device_count())]
return devices if devices else [torch.device('cpu')]
try_gpu(), try_gpu(10), try_all_gpus()
x = torch.tensor([1, 2, 3])
x.device#查询张量储存的设备
X = torch.ones(2, 3, device=try_gpu())
X
Y = torch.rand(2, 3, device=try_gpu(1))
Y
net = nn.Sequential(nn.Linear(3, 1))
net = net.to(device=try_gpu())
net(X)
print(net[0].weight.data.device)
第六章卷积神经网络
卷积神经网络是一类强大的,为处理图像而生的,为处理图像数据而生的神经网络。
卷积层
严格来说,卷积层的说法是错误的,卷积层的运算实际上是互相关运算。卷积层对输入和卷积核权重进行互相关运算,并在添加标量偏置之后产生输出。所以,卷积层中的两个被训练的参数是卷积核权重和标量偏置。就像我们之前随机初始化全连接层一样,在训练基于卷积层的模型时,我们也随机初始化卷积核权重。卷积核的定义如下:
卷积核(Convolution Kernel),在深度学习和图像处理中,是一个小的矩阵,用于在卷积操作中提取图像的局部特征。卷积核在图像处理中起到了滤波器的作用,可以用来检测图像中的边缘、角落、纹理等特征。
示例代码如下:构建了卷积层并不断迭代更新卷积核。
import torch
from torch import nn
from d2l import torch as d2l
def corr2d(X, K): #@save,接收两个参数,输入的矩阵和卷积核
"""计算二维互相关运算"""
h, w = K.shape
Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
Y[i, j] = (X[i:i + h, j:j + w] * K).sum()
return Y
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])#建立张量X
K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])#建立卷积核K
corr2d(X, K)#调用使得两者互相关运算
class Conv2D(nn.Module):#nn.model是定义神经网络类的基础参数,定义了一个二维卷积层
def __init__(self, kernel_size):
super().__init__()
self.weight = nn.Parameter(torch.rand(kernel_size))
self.bias = nn.Parameter(torch.zeros(1))
def forward(self, x):
return corr2d(x, self.weight) + self.bias
X = torch.ones((6, 8))
X[:, 2:6] = 0
X
K = torch.tensor([[1.0, -1.0]])
Y = corr2d(X, K)
Y
corr2d(X.t(), K)
# 构造一个二维卷积层,它具有1个输出通道和形状为(1,2)的卷积核
conv2d = nn.Conv2d(1,1, kernel_size=(1, 2), bias=False)
# 这个二维卷积层使用四维输入和输出格式(批量大小、通道、高度、宽度),
# 其中批量大小和通道数都为1
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
lr = 3e-2 # 学习率
for i in range(10):
Y_hat = conv2d(X)
l = (Y_hat - Y) ** 2
conv2d.zero_grad()
l.sum().backward()
# 迭代卷积核
conv2d.weight.data[:] -= lr * conv2d.weight.grad
if (i + 1) % 2 == 0:
print(f'epoch {i+1}, loss {l.sum():.3f}')
conv2d.weight.data.reshape((1, 2))
填充
如上所述,在应用多层卷积时,我们常常丢失边缘像素。由于我们通常使用小卷积核,因此对于任何单个卷积,我们可能只会丢失几个像素。但随着我们应用许多连续卷积层,累积丢失的像素数就多了。解决这个问题的简单方法即为填充(padding):在输入图像的边界填充元素(通常填充元素是0)。实际上在应用openmv和K210等基础摄像开发模块的时候就会遇到类似的问题。在计算互相关时,卷积窗口从输入张量的左上角开始,向下、向右滑动。在前面的例子中,我们默认每次滑动一个元素。但是,有时候为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素。我们将每次滑动元素的数量称为步幅(stride)。
综上所述:采用填充和步幅可以有效的管理数据的维度。
import torch
from torch import nn
# 为了方便起见,我们定义了一个计算卷积层的函数。
# 此函数初始化卷积层权重,并对输入和输出提高和缩减相应的维数
def comp_conv2d(conv2d, X):
# 这里的(1,1)表示批量大小和通道数都是1
X = X.reshape((1, 1) + X.shape)
Y = conv2d(X)
# 省略前两个维度:批量大小和通道
return Y.reshape(Y.shape[2:])
# 请注意,这里每边都填充了1行或1列,因此总共添加了2行或2列
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
X = torch.rand(size=(8, 8))
comp_conv2d(conv2d, X).shape
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
comp_conv2d(conv2d, X).shape
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
comp_conv2d(conv2d, X).shape
多输入与多输出
实际上可以将卷积层看作多维的,这是和物理世界中的RGB图像配合的,以前讨论的大多是灰度图像(单维度的),当添加通道的时候,可以将每个通道进行互相关操作,最后相加。为了实现多输出,可以为每个输出通道建立相应的卷积核张量,再通过输入的多通道分别对其进行互相关计算,即可得到各个输出通道的结果。
示例代码如下:
import torch
import d2l
def corr2d_multi_in(X, K):
# 先遍历“X”和“K”的第0个维度(通道维度),再把它们加在一起
return sum(d2l.corr2d(x, k) for x, k in zip(X, K))
X = torch.tensor([[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]],
[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]])
K = torch.tensor([[[0.0, 1.0], [2.0, 3.0]], [[1.0, 2.0], [3.0, 4.0]]])
corr2d_multi_in(X, K)
def corr2d_multi_in_out(X, K):
# 迭代“K”的第0个维度,每次都对输入“X”执行互相关运算。
# 最后将所有结果都叠加在一起
return torch.stack([corr2d_multi_in(X, k) for k in K], 0)
K = torch.stack((K, K + 1, K + 2), 0)
K.shape
corr2d_multi_in_out(X, K)
def corr2d_multi_in_out_1x1(X, K):
c_i, h, w = X.shape
c_o = K.shape[0]
X = X.reshape((c_i, h * w))
K = K.reshape((c_o, c_i))
# 全连接层中的矩阵乘法
Y = torch.matmul(K, X)
return Y.reshape((c_o, h, w))
X = torch.normal(0, 1, (3, 3, 3))#normal是正态分布
K = torch.normal(0, 1, (2, 3, 1, 1))
Y1 = corr2d_multi_in_out_1x1(X, K)
Y2 = corr2d_multi_in_out(X, K)
assert float(torch.abs(Y1 - Y2).sum()) < 1e-6
汇聚层
汇聚层具有双重目的:降低卷积层对位置的敏感度,同时降低对空间降采样表示的敏感性。对于给定的特殊元素,最大汇聚层会输出该窗口内的最大值,而平均汇聚层会输出该窗口内的平均值。
示例代码如下:
import torch
from torch import nn
import d2l
def pool2d(X, pool_size, mode='max'):#默认模式是max
p_h, p_w = pool_size
Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
if mode == 'max':
Y[i, j] = X[i: i + p_h, j: j + p_w].max()
elif mode == 'avg':
Y[i, j] = X[i: i + p_h, j: j + p_w].mean()
return Y
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
pool2d(X, (2, 2))
pool2d(X, (2, 2), 'avg')
X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))#产生输入数据
X
pool2d = nn.MaxPool2d(3)#Initializes internal Module state
pool2d(X)
pool2d = nn.MaxPool2d(3, padding=1, stride=2)#定义填充和步幅,stride在openmv是同名
pool2d(X)
pool2d = nn.MaxPool2d((2, 3), stride=(2, 3), padding=(0, 1))
pool2d(X)
X = torch.cat((X, X + 1), 1)
X
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
卷积神经网络Lenet实战
在前面若干节的基础上,将开始构建卷积神经网络。Lenet由两个部分构成,分别是卷积编码器(由两个卷积层构成),全连接层密块(由三个全连接层构成)。本次还采用lenet来做图像物体识别,具体代码如下:
首先是构建卷积神经网络,其次训练并评估该网络,由于卷积网络的特性采用GPU来训练可以加快训练速度。
import torch
from torch import nn
import d2l
net = nn.Sequential(#构建卷积神经网络
nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Flatten(),
nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),
nn.Linear(120, 84), nn.Sigmoid(),
nn.Linear(84, 10))
X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32)#构建输入张量
for layer in net:
X = layer(X)
print(layer.__class__.__name__,'output shape: \t',X.shape)
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
def evaluate_accuracy_gpu(net, data_iter, device=None): #@save
"""使用GPU计算模型在数据集上的精度"""
if isinstance(net, nn.Module):
net.eval() # 设置为评估模式
if not device:
device = next(iter(net.parameters())).device
# 正确预测的数量,总预测的数量
metric = d2l.Accumulator(2)
with torch.no_grad():
for X, y in data_iter:
if isinstance(X, list):
# BERT微调所需的(之后将介绍)
X = [x.to(device) for x in X]
else:
X = X.to(device)
y = y.to(device)
metric.add(d2l.accuracy(net(X), y), y.numel())
return metric[0] / metric[1]
#@save
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
"""用GPU训练模型(在第六章定义)"""
def init_weights(m):
if type(m) == nn.Linear or type(m) == nn.Conv2d:
nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)
print('training on', device)
net.to(device)
optimizer = torch.optim.SGD(net.parameters(), lr=lr)
loss = nn.CrossEntropyLoss()#定义损失
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=['train loss', 'train acc', 'test acc'])
timer, num_batches = d2l.Timer(), len(train_iter)
for epoch in range(num_epochs):
# 训练损失之和,训练准确率之和,样本数
metric = d2l.Accumulator(3)
net.train()#调用训练
for i, (X, y) in enumerate(train_iter):
timer.start()#开启计时
optimizer.zero_grad()
X, y = X.to(device), y.to(device)
y_hat = net(X)
l = loss(y_hat, y)
l.backward()
optimizer.step()
with torch.no_grad():
metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])
timer.stop()#停止计时
train_l = metric[0] / metric[2]
train_acc = metric[1] / metric[2]
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(train_l, train_acc, None))
test_acc = evaluate_accuracy_gpu(net, test_iter)
animator.add(epoch + 1, (None, None, test_acc))
print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, '
f'test acc {test_acc:.3f}')#打印输出训练损失和精准度
print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec '
f'on {str(device)}')
lr, num_epochs = 0.9, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())#调用GPU上的train函数
运行结果如下所示,可知训练的精度不断增加。

更多推荐

所有评论(0)