计算机视觉读书系列(2)——卷积神经网络

对应《Deep Learning for Vision System》书中的"Convolutional neural networks"一章

大纲脉络与书中一致,后附上个人编写torch框架下代码。


1、MLP用于图像分类

当我们预计使用MLP模型来对图像数据进行分析处理时,如分类任务,由于模型结构的原因,我们需要将结构化的图像数据拉伸(flatten)为一维数据,例如将一张28x28的数据(矩阵)拉成784维度的一维数据(向量),随后再将其喂入神经网络,最后得到预测结果。

此类做法有两大致命缺陷:

**++(1)参数量过大++。**由于MLP的结构为全连接,因此其参数量会非常大。

**++(2)损坏结构化数据++。**图像数据中形状、结构类特征对图片理解至关重要,拉扯会损失结构化信息。

相应地,卷积神经网络对应的提出权值共享感受野来解决对应问题。

2、卷积神经网络(CNN)结构

2.1 总览

回顾机器学习(包括深度学习)的整个架构,其包括:输入数据------预处理------特征提取------解释模型。++显然,MLP在图像任务中的特征提取表现较差(事实上MLP作为最后的分类解释模型表现很好)。因此现在需要替换的是流程中的特征提取模块,也即使用CNN来进行特征提取,使用MLP来进行最终的分类。++

特征提取的最终的结果是特征向量,该特征向量可以作为MLP模型的输入,用于最后的模型分类

特征提取过程中每经过一层则维度较小,但特征图的数量上升,也即维度减小,深度增加。(把每一个特征图理解为一个特征或一个属性,特征越多表示描述的角度和方面就越多,每一个特征图是聚焦某一个特征,因此其数据维度自然会降低)

特征图:特征图(feature map)是卷积核对图像进行卷积所得到的结果。

2.2 特征提取直观认识

如上图所示,一张一维的图片通过(四次)卷积操作获得了四个特征图片,也就是特征图(feature map), 所以原始数据从 28x28x1 变成了 20x20x4 (举例)。这20x20x4就是四个特征图(feature map)

2.3 图像分类问题直观认识

以人脸识别为例,卷积神经网络通过识别局部特征来最终实现人脸识别。

3、卷积网络基本成分------卷积、池化、全连接

3.1 卷积层

卷积层通常被用于进行特征学习或特征提取。全连接层通常被用于进行分类。

卷积:卷积层作为一个特征发现窗口、滑过整个图像的像素值,并提取有图像对象种有意义的特征。

卷积核(convolution filter):卷积核也即CNN网络中的权重, 用来操作图像的算子。其长度通常为方形,随机初始化,并随网络训练而调整(学习)。卷积方式是对应位置相乘相加,最后加上偏差。如下:

在数字图像处理中,卷积核被称为算子,不同类型的算子能对图像进行不同类型的操作,如:边缘锐化算子、边缘提取算子、图像增强算子等。 如下图:

因此,可以理解为,每一个卷积核(算子)能得到一个图像的特征,现在网络需要自主通过学习了解如何提取有用的特征,也即通过学习获得卷积核中的参数,不同的卷积核(算子)所提取的特征不同,这也是为什么将提取的结果称为特征图,因为每一个图代表谋一个特征的提取结果。

感受野:卷积核所操作的区域被称为感受野(receptive field)。

特征图(feature map):卷积核操作过后的图像被称为特征图。

卷积中四个超参数卷积核个数 (通道数,也即每一层的卷积核的个数)、卷积核尺寸、步长、填充

(1)卷积核个数。如果在这一层使用N个卷积核,那个下一层的feature map的深度或者通道数也即为N。

(2)卷积核尺寸。通常由2x2、3x3、5x5。小的卷积核用于提取图像中更加精细的特征,较大的卷积核会忽略细节而提取更大维度的特征。(经验主义而言:卷积核的大小最小2x2最大5x5).

(3)步长。滑动的时候跳跃的像素个数。

(4)填充。对卷积后的图像的填充。

步长和填充的作用:能保证卷积后的图像大小和原始图像的大小一致,方便后续的池化操作计算图像输出大小。

3.2 池化层或降采样

如上所述,每一个卷积核都会生成一幅feature map,那么在卷积模型中数据量就会出现较快的增长,因此需要对数据进行降采样,以及对图像数据进行压缩,以期降低数据量。

池化层主要包括两类,最大池化和平均池化。最大池化操作与卷积操作一致,有一个一定大小的"卷积核"对输入图像进行滑动,同时也伴有步长,只不过卷积核中没有参数,**且输出为卷积核所对应区域中元素最大的值,这也即最大池化。**易知,若池化的卷积核尺寸为2x2,那么最终的数据量将缩减为原来的1/4。

(全局)平均池化是一个更加极端的池化方式,其计算整个通道(channel)或说整张特征图数字的平均值。如下:

++池化层主要用于减少数据的维度降低计算量等++。

(注:现有研究也表明,省去池化层,仅仅使用卷积层一直对图像数据进行数据提取,也能得到较好的效果。另一个方面,随着硬件存储等的发展,降低数据量的急迫性没有这么大,因此现在由很多研究舍弃池化层)

卷积被用于在原始数据中提取图像特征,因此在卷积神经网络中,卷积层结构通常是多个卷积模块(包括卷积和池化)的堆叠,用于连续不断的提取图像中的特征,直到提取到足够小的大小,将其拉伸为一个向量,用作最优预测模型(多为MLP-based)的输出。如下:

3.3 全连接层

全连接层也即多层神经网络,卷积模块所获得的结果传入神经网络中,进行图像分类。

如上图所示,将5x5x40压缩成1000维度的向量,再将这个向量传入MLP中进行分类。

4、使用CNN进行图像分类

4.1 组件一个卷积神经网络

本文给出了一个keras框架下的基于CNN网络的图像分类。

将模型结输出在屏幕如下:

卷积: 模型的输入为灰度图像,其维度为(28,28,1)。第一个卷积模块 conv2d_1,其拥有32个卷积核,其输出维度为(28,28,32)。(下同)

池化:然后经过一个池化层,该池化为尺寸为2的最大池化,无参数,其输出的数据维度(14,14,32),通道数没变,但数据维度标为之前的1/2,总数据变为之前的1/4。(下同)

全连接:首先用flatten将提取好的特征图拉伸,将(7,7,64)拉伸为向量(3136)

4.2 参数量分析

关于卷积中参数量的计算:conv2d_1 层共有32个卷积核,size为3,其处理的图像的通道为1,每个卷积核有一个偏执参数。因此每个卷积核的参数量为:(3x3x1+1)x32=320。

又如,conv2d_2的参数应为:(3x3x32+1)x64=18496。卷积核的通道数与处理对象特征图的通道数一致,如处理(14,14,32)的图像(特征图),则单个卷积核的形状为(3,3,32)。

可训练参数与不可训练参数:在后续的计算机视觉中,或者后续的视觉大模型中,经常会使用预训练大模型。预训练大模型是指在大数据集上训练好的模块,如在ImageNet数据集上训练的特征提取模型,我们可以将其视作图像特征提取的工具,这个工具不会参与我们的模型训练,因为他是训练好的,因此在实际使用时将其视为一个特征提取工具冻结其参数。

5、增加dropout以防止过拟合

5.1 什么是过拟合

欠拟合:模型没能拟合训练数据,通常是模型过于简单。

过拟合:对于训练数据拟合太过,记住了训练数据但没有真的学习到特征,通常是建立了一个比较复杂的网络模型(或者数据量太少)。

5.2 什么是dropout层

dropout层能平衡我们的神经网络,让我们每个神经元节点能被平等的对待,使得预测结果不会过度的依赖于某个特征或某个神经元节点。其工作方式如下:

在实际过程中,每个神经元都有一定的概率(dropout的超参数)被失活(被out),如上图所示,而每个神经元表示一个特征,以达到训练结果不会过度依赖某一个特征的目的。

++dropout减少了神经元之间的相互依赖,在这种情况下,模型一定程度上可能看作多个若分类模型的组合模型。++

5.3 为什么需要dropout层

防止过拟合!!!

6、使用卷积神经网络对彩色图像进行分类

彩色图像为三通道图像,因此在数据输入部分需要采用三通道的卷积核。

使用卷积神经网络对彩色图像进行分类的步骤如下

(1)加载数据集

(2)图像预处理:

rescale:将图像/255转化为0-1之间数值

准备标签:one-hot编码

数据集分割

定义框架

组建模型:将数据集、模型、优化器等合并

训练模型

加载模型(从最优的记录中)

评估模型

附:个人编写基于torch实现书中示例

所有代码均上传至(数据太大,自行网站下载):

https://github.com/xurongtang/CV_practicehttps://github.com/xurongtang/CV_practice

附1、手写数字识别项目

脚本1(dataset_load.py) :加载MINIST数据集,参考博客:Fashion MNIST数据集的处理------"...-idx3-ubyte"文件解析_t10k-images-idx3-ubyte-CSDN博客文章浏览阅读8.4k次,点赞6次,收藏39次。MNIST数据集可能是计算机视觉所接触的第一个图片数据集。而 Fashion MNIST 是在遵循 MNIST 的格式和大小的基础上,提升了一定的难度,在比较算法的性能时可以有更好的区分度。Fashion MNIST 数据集包含 60000 张图片的训练集和 10000 张图片的测试集。图片的大小为 28×28,共784个像素。像素的灰度值介于0~255之间的整数。_t10k-images-idx3-ubytehttps://blog.csdn.net/weixin_43276033/article/details/124163762如下:

python 复制代码
import torch
import cv2,os
import numpy as np
import struct
import matplotlib.pyplot as plt

def __decode_idx3_ubyte(file):
    """
    解析数据文件
    """
    # 读取二进制数据
    with open(file, 'rb') as fp:
        bin_data = fp.read()
    
    # 解析文件中的头信息
    # 从文件头部依次读取四个32位,分别为:
    # magic,numImgs, numRows, numCols
    # 偏置
    offset = 0
    # 读取格式: 大端
    fmt_header = '>iiii'
    magic, numImgs, numRows, numCols = struct.unpack_from(fmt_header, bin_data, offset)
    # print(magic,numImgs,numRows,numCols)
    
    # 解析图片数据
    # 偏置掉头文件信息
    offset = struct.calcsize(fmt_header)
    # 读取格式
    fmt_image = '>'+str(numImgs*numRows*numCols)+'B'
    data = torch.tensor(struct.unpack_from(fmt_image, bin_data, offset)).reshape(numImgs, numRows, numCols)
    return data

def __decode_idx1_ubyte(file):
    """
    解析标签文件
    """
    # 读取二进制数据
    with open(file, 'rb') as fp:
        bin_data = fp.read()
    
    # 解析文件中的头信息
    # 从文件头部依次读取两个个32位,分别为:
    # magic,numImgs
    # 偏置
    offset = 0
    # 读取格式: 大端
    fmt_header = '>ii'
    magic, numImgs = struct.unpack_from(fmt_header, bin_data, offset)
    # print(magic,numImgs)
    # 解析图片数据
    # 偏置掉头文件信息
    offset = struct.calcsize(fmt_header)
    # 读取格式
    fmt_image = '>'+str(numImgs)+'B'
    data = torch.tensor(struct.unpack_from(fmt_image, bin_data, offset))
    return data

def get_MINIST():
    # 文件路径
    data_path = 'E:/ComputerVision_Proj/dataset/MINIST/DATA/'
    file_names = ['t10k-images-idx3-ubyte',
                    't10k-labels-idx1-ubyte',
                    'train-images-idx3-ubyte',
                    'train-labels-idx1-ubyte']
    test_set = (__decode_idx3_ubyte(os.path.join(data_path, file_names[0])),
                __decode_idx1_ubyte(os.path.join(data_path, file_names[1])))
    train_set = (__decode_idx3_ubyte(os.path.join(data_path, file_names[2])),
                __decode_idx1_ubyte(os.path.join(data_path, file_names[3])))
    return train_set, test_set

if __name__ == '__main__':
    train_set, test_set = get_MINIST()
    print(train_set[0].shape,train_set[1].shape)
    print(test_set[0].shape,test_set[1].shape)
    test = np.array(train_set[0][0])
    plt.imshow(test)
    plt.show()

脚本2(cnn_digital_recognize):完全复现下列结构。

代码如下:

python 复制代码
## Author: 撞南墙者 ##
## Date: 2024-12-7 ##
## CopyRight: 随意使用,欢迎交流 ## 
import torch,random
import copy
import numpy as np
import torch.nn as nn
import matplotlib.pyplot as plt
from dataset_load import get_MINIST
from torch.utils.data import DataLoader
from tqdm import tqdm

random.seed(2021)
np.random.seed(2021)
torch.manual_seed(2021)

class Custom_dataset(torch.utils.data.Dataset):
    def __init__(self,dataset):
        self.input_dataset,self.label_dataset = dataset
        self.one_hot_code_flag = True
        self.num_classes = 10

    def __getitem__(self,index):
        input_img = self.input_dataset[index]
        if self.one_hot_code_flag:
            label_img = self.one_hot_code(self.label_dataset[index],self.num_classes)
        else:
            label_img = self.label_dataset[index]
        return input_img,label_img
    
    def __len__(self):
        length = self.input_dataset.shape[0]
        assert length == self.label_dataset.shape[0]
        return length

    @staticmethod
    def one_hot_code(input_data: torch.Tensor,num_classes) -> torch.Tensor:
        one_hot = np.zeros((num_classes),dtype=np.int8)
        for i in range(num_classes):
            if input_data == i:
                one_hot[i] = 1
                break
        one_hot = torch.Tensor(one_hot)
        return one_hot

# 定义模型
class CNN_to_digital_number_recognize(nn.Module):
    
    def __init__(self,input_dim):
        super().__init__()
        self.input_dim = input_dim
        ksize1 = 5
        ksize2 = 5
        padding1 = 0    # ksize1//2
        padding2 = 0    # ksize2//2
        final_channels = 20
        pooling_size = 2
        # 如果想保持图像大小,padding = kernel_size//2
        self.conv1 = nn.Conv2d(in_channels=1,out_channels=10,kernel_size=ksize1,padding=padding1)
        self.pool1 = nn.MaxPool2d(kernel_size=pooling_size)
        self.conv2 = nn.Conv2d(in_channels=10,out_channels=final_channels,kernel_size=ksize2,padding=padding2)
        self.pool2 = nn.MaxPool2d(kernel_size=pooling_size)
        # 计算最后的元素总数
        final_elements = final_channels * ((((input_dim[1]-(ksize1//2)*2) // pooling_size) - (ksize2//2)*2) // pooling_size)**2
        self.flatten = nn.Flatten()
        self.linear1 = nn.Linear(final_elements,512)
        self.linear2 = nn.Linear(512,10)
        # 构建
        self.ConvBlock1 = nn.Sequential(
            self.conv1,
            nn.ReLU(),
            self.pool1)
        self.ConvBlock2 = nn.Sequential(
            self.conv2,
            nn.ReLU(),
            self.pool2)
        self.LinearBlock = nn.Sequential(
            self.flatten,
            self.linear1,
            nn.ReLU(),
            self.linear2,
            nn.Softmax(dim=1))

    def forward(self,x):
        # 卷积输入的数据维度为 (Batch,channel,height,weight)
        x = x.view(-1,self.input_dim[0],self.input_dim[1],self.input_dim[2]).to(torch.float)
        x = self.ConvBlock1(x)
        x = self.ConvBlock2(x)
        x = self.LinearBlock(x)
        # print(x.shape)
        return x

def training_process(train_set,model,params_dict: dict):
    ## 参数设置 ##
    BATCHSIZE = params_dict['batchsize']
    LEARNING_RATE = params_dict['learning_rate']
    EPOCHS = params_dict['epochs']
    device = torch.device(params_dict['device'])
    val_rate = 0.15
    ## 模型准备 ## 
    model.to(device)
    optimizer = torch.optim.SGD(model.parameters(),lr=LEARNING_RATE)
    loss_func = nn.CrossEntropyLoss()  # 对于分类问题
    ## 数据准备 ##
    length = train_set[0].shape[0]
    train_dataset = Custom_dataset((train_set[0][:int(length*(1-val_rate))],train_set[1][:int(length*(1-val_rate))]))
    train_loader = DataLoader(train_dataset,batch_size=BATCHSIZE,shuffle=True)
    val_dataset = Custom_dataset((train_set[0][:int(length*val_rate)],train_set[1][:int(length*val_rate)]))
    val_loader = DataLoader(val_dataset,batch_size=BATCHSIZE,shuffle=True)
    ## 定义模型验证/测试函数 ##
    def val(model,val_loader):
        val_loss = []
        model.eval()
        for _,(batch_x,batch_y) in enumerate(val_loader):
            batch_x = batch_x.float().to(device)
            batch_y = batch_y.float().to(device)
            batch_out = model(batch_x)
            # 计算误差
            loss = loss_func(batch_out,batch_y)
            val_loss.append(loss.item())
        model.train()
        return np.mean(val_loss)
    ## 记录训练过程 ##
    train_loss_ls = []
    val_loss_ls = []
    early_stopping_count = 0
    ## 开始训练 ## 
    print("Start training...")
    for epo in tqdm(range(EPOCHS),colour='yellow'):
        batch_loss = []
        for _,(batch_x,batch_y) in enumerate(train_loader):
            optimizer.zero_grad()
            batch_x = batch_x.float().to(device)
            batch_y = batch_y.float().to(device)
            batch_out = model(batch_x)
            # 计算误差
            loss = loss_func(batch_out,batch_y)
            batch_loss.append(loss.item())
            loss.backward()
            optimizer.step()
            if params_dict['device'] == 'cuda':
                torch.cuda.empty_cache()
        train_loss_ls.append(np.mean(batch_loss))
        val_loss_ls.append(val(model,val_loader))
        if epo == 0:
            best_val_loss = val_loss_ls[-1]
        # print(f"Iters {epo+1:>3}/{EPOCHS} is end, Train_loss:{train_loss_ls[-1]:>8.4f}, Valid_loss:{val_loss_ls[-1]:>8.4f}.")
        if val_loss_ls[-1] <= best_val_loss:
            best_val_loss = val_loss_ls[-1]
            best_model = copy.deepcopy(model)
            early_stopping_count = 0
            # print("Best model has been saved.")
        else:
            early_stopping_count += 1
        if early_stopping_count >= 5:
            print("Early stopping by 5 epochs...")
            break
    print("Training finished.")
    return best_model.eval()

def main():
    train_set,test_set = get_MINIST()
    print("train dataset size: ",train_set[0].shape)
    print("test dataset size: ",test_set[0].shape)
    # print(np.unique(np.array(train_set[1]),return_counts=True))
    Model = CNN_to_digital_number_recognize(input_dim=(1,28,28))
    trained_model = training_process(train_set,
                                     Model,
                                     params_dict={
                                            'batchsize':8,
                                            'learning_rate':0.001,
                                            'epochs':100,
                                            'device':'cuda'
                                            })
    # 简单测试,计算预测准确率
    count = 0
    trained_model.cpu()
    test_input_data,test_label_data = test_set
    print("Testing...")
    for i,input_test in enumerate(tqdm(test_input_data,colour='yellow')):
        res = trained_model(input_test.unsqueeze(0))
        pred_label = torch.argmax(res).item()
        if pred_label == test_label_data[i]:
            count += 1
    print(f"Accuracy:{(count/len(test_input_data))*100:>6.2f}%")
    
if __name__ == '__main__':
    main()

代码运行结果:测试集精度约为98.95%

附2、十类别彩色图像分类(CIFAR-10 and CIFAR-100 datasets

首先数据读取

在附1的数据处理代码中加入以下函数即可

python 复制代码
## Author: 撞南墙者 ##
## Date: 2024-12-7 ##
## CopyRight: 随意使用,欢迎交流 ## 

def get_CIFAR10():
    root_path = 'E:/ComputerVision_Proj/dataset/cifar-10-batches-py/'
    train_path = ['data_batch_1', 'data_batch_2', 'data_batch_3', 'data_batch_4', 'data_batch_5']
    import pickle
    res_data = []
    res_labels = []
    for i in range(5):
        with open(os.path.join(root_path, root_path+train_path[i]), 'rb') as fp:
            res = pickle.load(fp, encoding='bytes')
        labels = res[b'labels']
        data = res[b'data']
        assert len(labels) == len(data)
        dataset_input = np.zeros((len(labels), 3, 32, 32))
        dataset_labels = np.array(labels,dtype=np.uint8)
        for j in range(len(labels)):
            dataset_input[j] = data[j].reshape(3, 32, 32)
        res_data.append(dataset_input)
        res_labels.append(dataset_labels)
    all_train_dataset_input = np.concatenate(res_data, axis=0)
    all_train_dataset_labels = np.concatenate(res_labels, axis=0)
    # test dataset
    with open(os.path.join(root_path, 'test_batch'), 'rb') as fp:
        res = pickle.load(fp, encoding='bytes')
    labels = res[b'labels']
    data = res[b'data']
    assert len(labels) == len(data)
    test_input = np.zeros((len(labels), 3, 32, 32))
    test_labels = np.array(labels,dtype=np.uint8)
    for j in range(len(labels)):
        test_input[j] = data[j].reshape(3, 32, 32)
    # 转化
    train_input = torch.tensor(all_train_dataset_input)
    train_labels = torch.tensor(all_train_dataset_labels)
    test_input = torch.tensor(test_input)
    test_labels = torch.tensor(test_labels)
    return (train_input,train_labels),(test_input,test_labels)

其结果为:

只需在原来代码中改动部分参数即可。

python 复制代码
## Author: 撞南墙者 ##
## Date: 2024-12-7 ##
## CopyRight: 随意使用,欢迎交流 ## 
import torch,random
import copy
import numpy as np
import torch.nn as nn
import matplotlib.pyplot as plt
from dataset_load import get_CIFAR10
from torch.utils.data import DataLoader
from tqdm import tqdm

random.seed(2021)
np.random.seed(2021)
torch.manual_seed(2021)

class Custom_dataset(torch.utils.data.Dataset):
    def __init__(self,dataset):
        self.input_dataset,self.label_dataset = dataset
        self.one_hot_code_flag = True
        self.num_classes = 10

    def __getitem__(self,index):
        input_img = self.input_dataset[index]
        if self.one_hot_code_flag:
            label_img = self.one_hot_code(self.label_dataset[index],self.num_classes)
        else:
            label_img = self.label_dataset[index]
        return input_img,label_img
    
    def __len__(self):
        length = self.input_dataset.shape[0]
        assert length == self.label_dataset.shape[0]
        return length

    @staticmethod
    def one_hot_code(input_data: torch.Tensor,num_classes) -> torch.Tensor:
        one_hot = np.zeros((num_classes),dtype=np.int8)
        for i in range(num_classes):
            if input_data == i:
                one_hot[i] = 1
                break
        one_hot = torch.Tensor(one_hot)
        return one_hot

# 定义模型
class CNN_to_digital_number_recognize(nn.Module):
    
    def __init__(self,input_dim):
        super().__init__()
        self.input_dim = input_dim
        ksize1 = 5
        ksize2 = 5
        padding1 = 0    # ksize1//2
        padding2 = 0    # ksize2//2
        final_channels = 20
        pooling_size = 2
        # 如果想保持图像大小,padding = kernel_size//2
        self.conv1 = nn.Conv2d(in_channels=3,out_channels=10,kernel_size=ksize1,padding=padding1)
        self.pool1 = nn.MaxPool2d(kernel_size=pooling_size)
        self.conv2 = nn.Conv2d(in_channels=10,out_channels=final_channels,kernel_size=ksize2,padding=padding2)
        self.pool2 = nn.MaxPool2d(kernel_size=pooling_size)
        # 计算最后的元素总数
        final_elements = final_channels * ((((input_dim[1]-(ksize1//2)*2) // pooling_size) - (ksize2//2)*2) // pooling_size)**2
        self.flatten = nn.Flatten()
        self.linear1 = nn.Linear(final_elements,512)
        self.linear2 = nn.Linear(512,10)
        # 构建
        self.ConvBlock1 = nn.Sequential(
            self.conv1,
            nn.ReLU(),
            nn.Dropout(0.5),
            self.pool1)
        self.ConvBlock2 = nn.Sequential(
            self.conv2,
            nn.ReLU(),
            nn.Dropout(0.5),
            self.pool2)
        self.LinearBlock = nn.Sequential(
            self.flatten,
            self.linear1,
            nn.ReLU(),
            self.linear2,
            nn.Softmax(dim=1))

    def forward(self,x):
        # 卷积输入的数据维度为 (Batch,channel,height,weight)
        x = x.view(-1,self.input_dim[0],self.input_dim[1],self.input_dim[2]).to(torch.float)
        x = self.ConvBlock1(x)
        x = self.ConvBlock2(x)
        x = self.LinearBlock(x)
        # print(x.shape)
        return x

def training_process(train_set,model,params_dict: dict):
    ## 参数设置 ##
    BATCHSIZE = params_dict['batchsize']
    LEARNING_RATE = params_dict['learning_rate']
    EPOCHS = params_dict['epochs']
    device = torch.device(params_dict['device'])
    val_rate = 0.15
    ## 模型准备 ## 
    model.to(device)
    optimizer = torch.optim.SGD(model.parameters(),lr=LEARNING_RATE)
    loss_func = nn.CrossEntropyLoss()  # 对于分类问题
    ## 数据准备 ##
    length = train_set[0].shape[0]
    train_dataset = Custom_dataset((train_set[0][:int(length*(1-val_rate))],train_set[1][:int(length*(1-val_rate))]))
    train_loader = DataLoader(train_dataset,batch_size=BATCHSIZE,shuffle=True)
    val_dataset = Custom_dataset((train_set[0][:int(length*val_rate)],train_set[1][:int(length*val_rate)]))
    val_loader = DataLoader(val_dataset,batch_size=BATCHSIZE,shuffle=True)
    ## 定义模型验证/测试函数 ##
    def val(model,val_loader):
        val_loss = []
        model.eval()
        for _,(batch_x,batch_y) in enumerate(val_loader):
            batch_x = batch_x.float().to(device)
            batch_y = batch_y.float().to(device)
            batch_out = model(batch_x)
            # 计算误差
            loss = loss_func(batch_out,batch_y)
            val_loss.append(loss.item())
        model.train()
        return np.mean(val_loss)
    ## 记录训练过程 ##
    train_loss_ls = []
    val_loss_ls = []
    early_stopping_count = 0
    ## 开始训练 ## 
    print("Start training...")
    for epo in tqdm(range(EPOCHS),colour='yellow'):
        batch_loss = []
        for _,(batch_x,batch_y) in enumerate(train_loader):
            optimizer.zero_grad()
            batch_x = batch_x.float().to(device)
            batch_y = batch_y.float().to(device)
            batch_out = model(batch_x)
            # 计算误差
            loss = loss_func(batch_out,batch_y)
            batch_loss.append(loss.item())
            loss.backward()
            optimizer.step()
            if params_dict['device'] == 'cuda':
                torch.cuda.empty_cache()
        train_loss_ls.append(np.mean(batch_loss))
        val_loss_ls.append(val(model,val_loader))
        if epo == 0:
            best_val_loss = val_loss_ls[-1]
        # print(f"Iters {epo+1:>3}/{EPOCHS} is end, Train_loss:{train_loss_ls[-1]:>8.4f}, Valid_loss:{val_loss_ls[-1]:>8.4f}.")
        if val_loss_ls[-1] <= best_val_loss:
            best_val_loss = val_loss_ls[-1]
            best_model = copy.deepcopy(model)
            early_stopping_count = 0
            # print("Best model has been saved.")
        else:
            early_stopping_count += 1
        if early_stopping_count >= 5:
            print("Early stopping by 5 epochs...")
            break
    print("Training finished.")
    return best_model.eval()

def main():
    train_set,test_set = get_CIFAR10()
    print("train dataset size: ",train_set[0].shape)
    print("test dataset size: ",test_set[0].shape)
    # print(np.unique(np.array(train_set[1]),return_counts=True))
    Model = CNN_to_digital_number_recognize(input_dim=(3,32,32))
    trained_model = training_process(train_set,
                                     Model,
                                     params_dict={
                                            'batchsize':32,
                                            'learning_rate':0.001,
                                            'epochs':100,
                                            'device':'cuda'
                                            })
    # 简单测试,计算预测准确率
    count = 0
    trained_model.cpu()
    test_input_data,test_label_data = test_set
    print("Testing...")
    for i,input_test in enumerate(tqdm(test_input_data,colour='yellow')):
        res = trained_model(input_test.unsqueeze(0))
        pred_label = torch.argmax(res).item()
        if pred_label == test_label_data[i]:
            count += 1
    print(f"Accuracy:{(count/len(test_input_data))*100:>6.2f}%")
    
if __name__ == '__main__':
    main()

结果为,精度在46.10%,可以选择调整参数或者网络架构等提升其精度:


欢迎批评指导,共勉!

相关推荐
陈广亮11 分钟前
构建具有长期记忆的 AI Agent:从设计模式到生产实践
人工智能
会写代码的柯基犬20 分钟前
DeepSeek vs Kimi vs Qwen —— AI 生成俄罗斯方块代码效果横评
人工智能·llm
Mintopia1 小时前
OpenClaw 是什么?为什么节后热度如此之高?
人工智能
爱可生开源社区1 小时前
DBA 的未来?八位行业先锋的年度圆桌讨论
人工智能·dba
叁两4 小时前
用opencode打造全自动公众号写作流水线,AI 代笔太香了!
前端·人工智能·agent
前端付豪4 小时前
LangChain记忆:通过Memory记住上次的对话细节
人工智能·python·langchain
strayCat232554 小时前
Clawdbot 源码解读 7: 扩展机制
人工智能·开源
王鑫星4 小时前
SWE-bench 首次突破 80%:Claude Opus 4.5 发布,Anthropic 的野心不止于写代码
人工智能
lnix4 小时前
当“大龙虾”养在本地:我们离“反SaaS”的AI未来还有多远?
人工智能·aigc