找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1186

积分

0

好友

210

主题
发表于 3 天前 | 查看: 8| 回复: 0

本文将在第一个cuDNN程序(LeNet识别MNIST)的基础上进行升级,使用C++和cuDNN库改造网络,实现对CIFAR-10数据集的训练与测试,并成功将准确率提升至60%以上。

环境与目标

本次实验在以下环境中进行:

  • 操作系统: Windows 10 专业版
  • 开发工具: Visual Studio 2015 (C++)
  • 硬件: NVIDIA GeForce GTX 1060 (6GB)
  • 计算平台: CUDA 9.0, cuDNN 7.1.4

核心目标有两个:

  1. 加载CIFAR-10的RGB三通道图像数据。
  2. 修改LeNet网络架构,使其输入通道数从1(MNIST灰度图)变为3(CIFAR-10彩色图)。

一、CIFAR-10 数据加载类

我们需要重写数据加载部分,以读取CIFAR-10的二进制格式文件。数据集包含50000张训练图和10000张测试图,每张图是32x32像素的RGB图像。

首先定义数据样本和数据集类的基本结构:

struct cifar10Samlpe
{
    cifar10Samlpe(const std::vector<float>& image_, const unsigned char &label_) :image(image_), label(label_) {}
    std::vector<float> image; // 32*32*3
    unsigned char label;
};

class cifar10Dataset {
public:
    cifar10Dataset(const std::string &image_file, const std::string &label_file) {
        load_images(image_file);
        if (images.size() != labels.size()) {
            throw std::runtime_error("Number of images and labels mismatch");
        }
        indices.resize(images.size());
        for (size_t i = 0; i<indices.size(); i++)
            indices[i] = i;
    }
    // ... 其他成员函数(next_batch, size等)
private:
    std::vector<std::vector<float>> images;
    std::vector<unsigned char> labels;
    std::vector<size_t> indices;
    size_t current_idx = 0;
    void load_images(const std::string &path) {
        // 数据加载实现
    }
};

关键的 load_images 函数需要按顺序读取每个二进制文件(data_batch_1.bindata_batch_5.bin)。每个样本的存储格式为:1字节的标签,接着是3072字节的图像数据(顺序为红色通道、绿色通道、蓝色通道)。

以下是加载第一个训练批次的代码示例:

void load_images(const std::string &path) {
    FILE* mnist_file = NULL;
    int num = 50000; int rows = 32; int cols = 32;
    images.resize(num, std::vector<float>(rows * cols * 3)); // 注意:3通道
    labels.resize(num);
    unsigned char label;
    unsigned char image_buffer[32 * 32];

    // 加载 data_batch_1.bin (样本0-9999)
    int err = fopen_s(&mnist_file, "c:\\data_batch_1.bin", "rb");
    if (mnist_file != NULL) {
        for (int i = 0; i < 10000; i++) {
            fread((char*)&label, sizeof(label), 1, mnist_file);
            labels[i] = label;

            // 读取蓝色通道(B)
            fread(image_buffer, 1, 32*32, mnist_file);
            for (int j = 0; j <32; j++) {
                for (int k = 0; k < 32; k++) {
                    int shuffle_index = j * 32 + k;
                    images[i][shuffle_index] = image_buffer[shuffle_index] / 255.0f;
                }
            }
            // 读取绿色通道(G)
            fread(image_buffer, 1, 32 * 32, mnist_file);
            for (int j = 0; j <32; j++) {
                for (int k = 0; k < 32; k++) {
                    int shuffle_index = j * 32 + k;
                    images[i][shuffle_index + 32*32] = image_buffer[shuffle_index] / 255.0f;
                }
            }
            // 读取红色通道(R)
            fread(image_buffer, 1, 32 * 32, mnist_file);
            for (int j = 0; j <32; j++) {
                for (int k = 0; k < 32; k++) {
                    int shuffle_index = j * 32 + k;
                    images[i][shuffle_index + 32 * 32 * 2] = image_buffer[shuffle_index] / 255.0f;
                }
            }
        }
        fclose(mnist_file);
    }
    // ... 类似地加载 data_batch_2.bin 到 data_batch_5.bin (样本10000-49999)
}

需要注意的是,对于人工智能和图像处理任务,Python生态中的TensorFlow或PyTorch通常有更便捷的数据加载工具,但在C++底层实现中,我们需要手动处理这些二进制格式。

我们还需要一个类似的 cifar10DatasetTEST 类来加载测试集 test_batch.bin

二、网络架构(LeNet)的关键修改

网络类 LeNet 的构造函数需要修改,将第一层卷积的输入通道数从1改为3,这是本次改造最核心的一步。

class LeNet :public Layer {
public:
    LeNet(cublasHandle_t &cublas_, cudnnHandle_t &cudnn_, int batch_) :cublas(cublas_), cudnn(cudnn_), batch(batch_) {
        // 关键修改:第一层Conv2D输入通道数从1改为3
        layers.emplace_back(std::make_shared<Conv2D>(cudnn, batch, 3, 12, 32, 32, 3));
        layers.emplace_back(std::make_shared<ReLU>(cudnn, batch, 12, 30, 30));
        layers.emplace_back(std::make_shared<Conv2D>(cudnn, batch, 12, 12, 30, 30, 3));
        layers.emplace_back(std::make_shared<ReLU>(cudnn, batch, 12, 28, 28));
        layers.emplace_back(std::make_shared<MaxPool2D>(cudnn, batch, 12, 28, 28, 2, 2, 0, 2));
        // ... 后续层定义保持不变
        layers.emplace_back(std::make_shared<Linear>(cublas, batch, 24 * 5 * 5, 120));
        layers.emplace_back(std::make_shared<ReLU>(cudnn, batch, 120, 1, 1));
        layers.emplace_back(std::make_shared<Linear>(cublas, batch, 120, 84));
        layers.emplace_back(std::make_shared<ReLU>(cudnn, batch, 84, 1, 1));
        layers.emplace_back(std::make_shared<Linear>(cublas, batch, 84, 10));

        // 为输入和梯度分配GPU内存时,尺寸需匹配 3*32*32
        cudaMalloc(&output, batch * 10 * sizeof(float));
        cudaMalloc(&grad_input, batch * 3 * 32 * 32 * sizeof(float));
    }
    // ... forward, backward 等其他函数
private:
    // ... 成员变量
};

此外,在 forwardbackward 函数中,涉及数据拷贝的地方也要确保尺寸计算正确,与 3 * 32 * 32 相匹配。

三、训练与测试函数调整

训练和测试函数中,设备内存分配和数据拷贝的维度都需要从 1*28*28 (MNIST) 调整为 3*32*32 (CIFAR-10)。

训练函数 train 的关键修改点:

// 分配输入内存:batch_size * 3通道 * 32高 * 32宽
cudaMalloc(&d_inputs, batch_size * 32*32 *3 * sizeof(float));
...
// 拷贝数据时,偏移量计算要包含3个通道
cudaMemcpy(d_inputs + batch_idx * 32*32 * 3,
           batch[batch_idx].image.data(),
           32 * 32 * 3 * sizeof(float),
           cudaMemcpyHostToDevice);

测试函数 test 也需进行类似的维度调整。

四、主函数与训练策略

在主函数中,我们初始化数据集和网络,并进行多轮训练。为了提高准确率,我们采用了一个简单的学习率衰减策略:如果当前轮次的测试分数低于历史最佳分数,则略微降低学习率。

int main() {
    cifar10Dataset Datasets = cifar10Dataset{ "c:\\data_batch_1.bin", "c:\\data_batch_1.bin" };
    cifar10DatasetTEST DatasetsTest = cifar10DatasetTEST{ "c:\\test_batch.bin", "c:\\test_batch.bin" };

    // 初始化CUDA、cuBLAS、cuDNN上下文及流
    cudaSetDevice(0);
    cudaStream_t stream;
    cudaStreamCreate(&stream);
    cublasHandle_t cublas;
    cudnnHandle_t cudnn;
    cublasCreate(&cublas);
    cudnnCreate(&cudnn);

    int batch_size = 10;
    float lr = 0.01f;
    float best_score = 0.0f;

    LeNet LeNet_net(cublas, cudnn, batch_size);

    for (int epoch = 0; epoch < 40; epoch++) {
        train(5000, batch_size, lr, LeNet_net, 0, stream, Datasets);
        float current_score = test(LeNet_net, stream, DatasetsTest);

        if (current_score > best_score) {
            best_score = current_score;
        } else {
            // 测试分数未提升,降低学习率
            lr = lr - 0.01f / 25;
        }
        std::cout << "learn rate " << lr << std::endl;
    }

    // 释放资源
    cublasDestroy(cublas);
    cudnnDestroy(cudnn);
    cudaStreamDestroy(stream);
    return 0;
}

这种在后端服务或高性能计算任务中常见的“训练-评估-调整”循环,体现了通过程序逻辑自动优化模型参数的思路。

五、运行结果

经过40轮训练,并配合动态调整学习率,程序成功运行,CIFAR-10测试集的准确率突破了60%。下图展示了训练过程中部分轮次的测试分数(test score)和对应的学习率(learn rate)变化。

C++/cuDNN实战:改造LeNet网络实现CIFAR10图像分类,准确率达60% - 图片 - 1

总结

本次实践成功地将基于cuDNN的LeNet网络从MNIST迁移到了CIFAR-10数据集。主要工作量集中在数据加载和网络输入层的适配。虽然目前60%的准确率相较于CPU版本的71%还有差距,且远低于现代深度学习模型在此数据集上的表现,但这个过程巩固了使用cuDNN进行C++ 深度学习编程的基础流程。通过调整网络结构、超参数以及引入更先进的正则化方法,仍有进一步的提升空间。使用GPU(cuDNN)进行训练相比CPU确实带来了显著的加速,后续通过优化代码和熟悉更高级的cuDNN API,可以进一步提升开发效率和模型性能。




上一篇:基于BS架构与SpringBoot+Vue的冷链物流监控系统设计与实现
下一篇:对称二叉树LeetCode题解:递归算法判断二叉树对称性详解
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 18:06 , Processed in 0.121677 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表