digit_eye开发记录(3): C语言读取MNIST数据集

在前两篇,我们解读了 MNIST 数据集的 IDX 文件格式,并分别用 C++ 和 Python 做了 读取 MNIST 数据集的实现。 基于 C++ 的代码稍长,基于 Python 的代码则明显更短,然而它们的共同特点是:依赖了外部库:

  • 基于 C++ 的实现: 依赖了 OpenCV
  • 基于 Python 的实现: 依赖了 Numpy

基于 C++ 的实现,有哪些问题

为了配置 OpenCV,无论是手动下载 OpenCV 预编译包 + 自行写 CMake 配置; 还是安装 vcpkg 后,从 vcpkg 安装 OpenCV + 自行写 CMake 配置,都略微麻烦:

  • vcpkg install opencv 会在本地源码编译 opencv,耗时几十分钟

即便配置完毕,还会看到关于 cmake minimum version 的提示:
在这里插入图片描述
读取 MNIST 数据集这个任务的规模很小,不用 vcpkg、不用 OpenCV,完全可以做到的。更进一步,还可以拿掉 C++ 的 std::vectorstd::stringstd::fstream. 那么为啥不用 C 语言实现?完全可以。

基于 Python 的实现,有哪些问题

Pure Python 的性能堪忧,调用 Numpy 库性能确实不错,但 Numpy 是 C/C++ 实现,这性能其实和 Python 本身无关。

如果为了让代码短小,那么基于 numpy 的实现也仍显啰嗦:tensorflow/pytorch/keras/sklearn 等开源库,早就提供了 mnist 的读取的实现,安静的做一个调用者,也挺快乐的,不是吗?

基于 C 语言的实现 - 可视化怎么做?

1. 基于 ImageWatch 的自定义图像格式可视化

基于 C++ 的实现, 用了 OpenCV 是为了图像可视化,是为了验证图像和标签是否配对。抛开 OpenCV,在 Windows 下可以使用 Visual Studio 中的 ImageWatch 插件,自行扩展一下,可以得到可视化。

先看一下效果:左侧是meta信息,表明是 DE_GrayImage 类型的数据结构,大小是28x28,元素是 UINT8 类型,通道是1个;右图则是 ImageWatch 可视化的结果
在这里插入图片描述

ImageWatch 还提供了常见图像操作,如阈值化,@thread(image, 128) 后可视化为:
在这里插入图片描述
又或者,旋转90度:@rot90(image):
在这里插入图片描述
其他更多操作,可以在 ImageWatch文档 找到:
在这里插入图片描述
我们回到如何显示上述的 DE_GrayImage 类型的问题上:首先在C代码中定义:

typedef struct DE_GrayImage
{
    unsigned int width;
    unsigned int height;
    unsigned char* data;
} DE_GrayImage;

然后创建文件 C:\Users\zz\Documents\Visual Studio 2022\Visualizers\DE_GrayImage.natvis, 内容如下:

<?xml version="1.0" encoding="utf-8"?> 
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010"> 
  <UIVisualizer ServiceId="{A452AFEA-3DF6-46BB-9177-C0B08F318025}" Id="1"  
                MenuName="Add to Image Watch"/> 
  <Type Name="DE_GrayImage"> 
    <UIVisualizer ServiceId="{A452AFEA-3DF6-46BB-9177-C0B08F318025}" Id="1" /> 
  </Type> 
  <Type Name="DE_GrayImage"> 
    <Expand> 
      <Synthetic Name="[type]"> 
        <DisplayString>UINT8</DisplayString> 
      </Synthetic>
      <Item Name="[channels]">1</Item> 
      <Item Name="[width]">width</Item> 
      <Item Name="[height]">height</Item> 
      <Item Name="[data]">data</Item> 
      <Item Name="[stride]">width</Item> 
    </Expand> 
  </Type>   
</AutoVisualizer>

简单解释下:

  • [type], [channels], [width], [height], [data], [stride] 是 ImageWatch 插件规定我们在编写 .natvis 文件来可视化图像时,需要填写的字段
  • <Item Name="[channels]">1</Item> 是为 channels 硬编码一个数值
  • <Synthetic Name="[type]" 则是指定数据类型

保存 .natvis 文件后,重新执行 Visual Studio 里的调试会话,就可以查看 DE_GrayImage 类型的图像的可视化了。嗯, ImageWatch 挺强大的。

不过, ImageWatch 也有不足

第一个不足:当 ImageWatch 查看的表达式本身非法时,并没有什么提示。

例如 dataset->images[0], 在 print_sample 函数内,ImageWatch 能正常显示图像内容,因为此时 dataset->images[0] 是合法的表达式
在这里插入图片描述
而当调用堆栈回到 main 函数, dataset->images[0] 不再是合法表达式, ImageWatch 直接显示为 invalid:
在这里插入图片描述
而仔细检查了代码后,发现此时 dataset 类型是 DataSet 而非 DataSet* 后,改为使用 dataset. Images[0] ,就能正常显示:
在这里插入图片描述

第二个不足: @mem(address, type, channels, width, height, stride) 并不能把一块内存当作图像显示

在这里插入图片描述

2. 化繁为简,在控制台显示图像

void print_sample(const DataSet* dataset, int index)
{
    DE_GrayImage* image = &dataset->images[index];

    printf("label: %d\n", (int)dataset->labels[index]);
    for (int i=0; i<28; i++)
    {
        for (int j=0; j<28; j++)
        {
            for (int k=0; k<3;k++)
                printf("%c", image->data[i * 28 + j] > 128 ? '#' : ' ');
        }
        printf("\n");
    }
}

在这里插入图片描述
在这里插入图片描述

完整代码

对于 MNIST 数据的读取,由于我们已经很熟悉它的格式,这里直接给出 C 风格的文件读取写法.

#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>

long get_filesize(FILE* fp)
{
    fseek(fp, 0, SEEK_END);
    long filesize = ftell(fp);
    fseek(fp, 0, SEEK_SET);
    return filesize;
}

typedef enum Endian {
    ENDIAN_LSB = 0,
    ENDIAN_MSB = 1
} Endian;

int read_int_from_4_bytes(unsigned char* buf, Endian endian)
{
    int x = 0;
    int c[2][4] = {
        { (1 << 0),  (1 << 8), (1 << 16), (1 << 24) },
        { (1 << 24), (1 << 16), (1 << 8), (1 << 0) }
    };
    for (int i=0; i<4; i++)
        x += buf[i] * c[endian][i];
    return x;
}

typedef struct DE_GrayImage
{
    unsigned int width;
    unsigned int height;
    unsigned char* data;
} DE_GrayImage;

typedef struct DataSet
{
    DE_GrayImage* images;
    uint8_t* labels;
    uint8_t* image_buf;
    uint8_t* label_buf;
    int num_images;
    int num_labels;
} DataSet;

void destroy_dataset(DataSet* dataset)
{
    if (dataset)
    {
        free(dataset->image_buf);
        dataset->image_buf = NULL;

        free(dataset->label_buf);
        dataset->labels = NULL;
        
        free(dataset->images);
        dataset->images = NULL;
    }
}

void load_labels(DataSet* dataset, const char* filename)
{
    FILE* fin = fopen(filename, "rb");
    long filesize = get_filesize(fin);
    unsigned char* buf = (unsigned char*)malloc(filesize + 1);
    if (buf == NULL)
        exit(1);
    buf[filesize] = '\0';
    dataset->label_buf = buf;
    fread((void*)buf, filesize, 1, fin);
    fclose(fin);
    dataset->num_labels = read_int_from_4_bytes(buf + 4, ENDIAN_MSB);
    dataset->labels = buf + 8;
}

void load_images(DataSet* dataset, const char* filename)
{
    FILE* fin = fopen(filename, "rb");
    long filesize = get_filesize(fin);

    unsigned char* buf = (unsigned char*)malloc(filesize + 1);
    if (buf == NULL)
        exit(1);
    dataset->image_buf = buf;
    buf[filesize] = '\0';
    fread((void*)buf, filesize, 1, fin);
    fclose(fin);

    uint8_t magic[4] = { buf[0], buf[1], buf[2], buf[3] };

    int num_images = read_int_from_4_bytes(buf + 4, ENDIAN_MSB);
    int rows = read_int_from_4_bytes(buf + 8, ENDIAN_MSB);
    int cols = read_int_from_4_bytes(buf + 12, ENDIAN_MSB);
   
    DE_GrayImage* images = (DE_GrayImage*)malloc(sizeof(DE_GrayImage) * num_images);
    if (images == NULL) 
        exit(1);
    dataset->images = images;
    for (int i=0; i<num_images; i++)
    {
        images[i].height = rows;
        images[i].width = cols;
        images[i].data = buf + 16 + i * rows * cols;
    }
}

void print_sample(const DataSet* dataset, int index)
{
    DE_GrayImage* image = &dataset->images[index];

    printf("label: %d\n", (int)dataset->labels[index]);
    for (int i=0; i<28; i++)
    {
        for (int j=0; j<28; j++)
        {
            for (int k=0; k<3;k++)
                printf("%c", image->data[i * 28 + j] > 128 ? '#' : ' ');
        }
        printf("\n");
    }
}

int main()
{
    DataSet dataset;
    load_images(&dataset, "C:/work/digit_eye/data/train-images.idx3-ubyte");
    load_labels(&dataset, "C:/work/digit_eye/data/train-labels.idx1-ubyte");
    
    print_sample(&dataset, 0);
    print_sample(&dataset, 233);
    print_sample(&dataset, 666);

    printf("wait\n");
    destroy_dataset(&dataset);

    return 0;
}

总结

这一篇尝试了以最少依赖的方式,实现 MNIST 数据集的读取,假定了读者已经熟悉 MNIST 数据集格式。 使用 C 语言而非 C++,在图像可视化方面去掉了对于 OpenCV 的依赖,探索了使用 ImageWatch 插件、 在控制台输出这两种方式;在文件读取方面使用 C标准库的 fopen, fread, ftell 等 API 替代了 C++ 的 std::fstream

References

  • https://learn.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2015/debugger/image-watch/image-watch-reference?view=vs-2015#pixel-formats
### 使用C语言实现MNIST手写数字识别 尽管大多数现代机器学习框架支持Python等高级语言,但在特定硬件资源受限的情况下,如嵌入式系统中,使用C语言来实现实时的手写数字识别具有重要意义。考虑到这一点,下面提供一种简化的方式,即利用神经网络库或自行构建简单的前馈神经网络来进行MNIST数据集上的训练与预测。 #### 方法一:借助现有轻量化库 对于希望快速搭建原型而不深入底层细节的开发者而言,可以选择一些专门为嵌入式环境优化过的深度学习推理引擎,比如TinyDNN、uTensor或是提到的TinyMaix[^2]。这些工具允许在低功耗微控制器上部署经过预先训练好的模型,而无需重新编写整个算法逻辑。 - 安装并配置所选库; - 导出已训练完成的权重参数至目标平台; - 编译运行测试案例以验证功能正确性; 这种方法的优势在于能够迅速获得可用的结果,并且减少了对复杂理论背景的需求。 #### 方法二:纯手工打造简易感知器 如果倾向于完全自主掌控每一个环节,则可以从零开始创建一个多层感知器(Multilayer Perceptron),这是一种经典的浅度神经网络架构。以下是简化的流程: 1. 数据准备阶段 - 下载MNIST数据库并通过适当手段解析成适合处理的形式。 ```c #include <stdio.h> // 假设已经定义好了读取图像文件的功能... void load_mnist_images(const char *filename, float ***images); ``` 2. 构建网络结构 - 初始化各层节点数量及其连接权值矩阵。 ```c #define INPUT_SIZE 784 // MNIST图片大小为28*28=784像素点 #define HIDDEN_LAYER_NODES 30 #define OUTPUT_CLASSES 10 // 数字范围是从0到9 typedef struct { double weights[HIDDEN_LAYER_NODES][INPUT_SIZE]; double biases[HIDDEN_LAYER_NODES]; } HiddenLayer; HiddenLayer hidden_layer; double output_weights[OUTPUT_CLASSES][HIDDEN_LAYER_NODES]; double output_biases[OUTPUT_CLASSES]; ``` 3. 实现激活函数以及反向传播机制 - 对于每一层应用sigmoid或其他类型的非线性变换操作。 - 计算误差梯度用于更新参数。 ```c double sigmoid(double z){ return 1 / (1 + exp(-z)); } void backpropagation(/* 参数列表 */){ /* 更新规则 */ } ``` 4. 测试评估过程 - 将新样本送入上述建立起来的体系当中执行正向传递直至得出最终类别标签。 ```c int predict(float *input_image){ int i,j,k; for(i=0;i<HIDDEN_LAYER_NODES;++i){ hidden_layer.outputs[i]=0.; for(j=0;j<INPUT_SIZE;++j) hidden_layer.outputs[i]+=hidden_layer.weights[i][j]*input_image[j]; hidden_layer.outputs[i]=sigmoid(hidden_layer.outputs[i]+hidden_layer.biases[i]); } double max_prob=-INFINITY; int predicted_class=-1; for(k=0;k<OUTPUT_CLASSES;++k){ double sum=0.; for(i=0;i<HIDDEN_LAYER_NODES;++i) sum+=output_weights[k][i]*hidden_layer.outputs[i]; if((sum=output_biases[k])>max_prob){ max_prob=sum; predicted_class=k; } } return predicted_class; } ``` 需要注意的是,以上代码片段仅为示意性质,实际项目可能还需要考虑更多因素,例如数值稳定性改进措施、批标准化技巧的应用等。此外,由于缺乏图形界面的支持,在展示效果方面可能会有所欠缺。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值