写于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,线性整流单元:

                                                          f(x)=max(0,x)

管理参数

在实际训练模型的时候可能涉及成千上万乃至上亿个参数,如何管理这些参数就成了关键的事情,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函数

运行结果如下所示,可知训练的精度不断增加。

Logo

脑启社区是一个专注类脑智能领域的开发者社区。欢迎加入社区,共建类脑智能生态。社区为开发者提供了丰富的开源类脑工具软件、类脑算法模型及数据集、类脑知识库、类脑技术培训课程以及类脑应用案例等资源。

更多推荐