scratch lenet(6): feature_map可视化的C语言实现
文章目录
- scratch lenet(6): feature_map可视化的C语言实现
 - 1. 目的
 - 2. FeatureMap 的归一化
 - 2.1 公式
 - 2.2 代码实现
 - 2.3 代码调用
 
- 3. 可视化结果
 - 4. References
 
1. 目的
将卷积层(Convolution)、下采样层(SubSampling,也就是池化)层前向计算结果,归一化后转为图像,保存为文件, 用于可视化感受结果,也用于快速调试排查错误。
卷积、池化的输出结果是 double 类型, 数据范围超过了 [0, 255]. 通过统计最大最小值, 可以执行归一化并转为图像。
实际上还可以用于反向传播更新后的 feature map 的可视化, 不过目前还没实现反向传播, 暂时只有 conv, pool 的前向计算结果 feature map 的可视化。
2. FeatureMap 的归一化
2.1 公式
normalized ( v ) = v − min max − min \text{normalized}(v) = \frac{v - \text{min}}{\text{max} - \text{min}} normalized(v)=max−minv−min
2.2 代码实现
代码实现不依赖于 C 标准库中的 float.h, 因此自行定义 double 类型的最大最小值 M_DBL_MAX, M_DBL_MIN.
为了增加代码复用性、减小复杂度, 每次处理一个通道的 feature map, 输出结果是一张灰度图(传入者负责申请释放内存):
#define M_DBL_MAX ((double)1.79769313486231570814527423731704357e+308L)
#define M_DBL_MIN ((double)2.22507385850720138309023271733240406e-308L)
void get_normalized_gray_image_from_channel(double* channel, int height, int width, uchar* out_image)
{
    double max_value = -M_DBL_MAX;
    double min_value = M_DBL_MAX;
    for (int i = 0; i < height; i++)
    {
        for (int j = 0; j < width; j++)
        {
            int idx = i * width + j;
            if (channel[idx] > max_value)
            {
                max_value = channel[idx];
            }
            if (channel[idx] < min_value)
            {
                min_value = channel[idx];
            }
        }
    }
    for (int i = 0; i < height; i++)
    {
        for (int j = 0; j < width; j++)
        {
            int idx = i * width + j;
            out_image[idx] = 255 * (channel[idx] - min_value) / (max_value - min_value);
        }
    }
}
 
2.3 代码调用
前一小节是实现代码, 还需要知道怎样调用。在卷积、池化层里,对计算结果应用上述函数即可。
保存结果时, 使用了 .pgm 图像格式。 .pgm 图像的读写操作实现代码,见 scratch lenet(1): 读写 pgm 图像文件
void forward_C1()
{ 
    double** kernel = C1_kernel;
    //int in_channel = C1.in_c;
    int kh = C1.kh;
    int kw = C1.kw;
    int out_h = C1.out_h;
    int out_w = C1.out_w;
    int input_h = C1.in_h;
    int input_w = C1.in_w;
    double* input = g_input;
    int number_of_kernel = C1.number_of_kernel;
    double* bias = C1_bias;
    double** output = C1_output;
    for (int k = 0; k < number_of_kernel; k++)
    {
        simple_conv(input, input_h, input_w, kernel[k], kh, kw, output[k], out_h, out_w, bias[k]);
    }
    // 如下是执行 feature map 的归一化、并保存为 .pgm 图像的过程
    const char* save_prefix = "C1_output";
    for (int k = 0; k < number_of_kernel; k++)
    {
        double* channel = output[k];
        
        // get normalized (gray) image from one feature map channel
        uchar* output_image = (uchar*)malloc(sizeof(uchar) * out_h * out_w);
        get_normalized_gray_image_from_channel(channel, out_h, out_w, output_image);
        char savepath[200] = { 0 };
        sprintf(savepath, "%s%d.pgm", save_prefix, k);
        write_pgm_image(output_image, out_w, out_h, savepath);
        free(output_image);
    }
}
void forward_S2()
{
    double** input = C1_output;
    int number_of_kernel = C1.number_of_kernel;
    int kh = S2.kh;
    int kw = S2.kw;
    double** output = S2_output;
    int input_w = S2.in_w;
    int out_h = S2.out_h;
    int out_w = S2.out_w;
    for (int k = 0; k < number_of_kernel; k++)
    {
        double* input_channel = input[k];
        double* output_channel = output[k];
        for (int i = 0; i < out_h; i++)
        {
            for (int j = 0; j < out_w; j++)
            {
                double sum = 0;
                for (int ki = 0; ki < kh; ki++)
                {
                    for (int kj = 0; kj < kw; kj++)
                    {
                        int si = i * 1 + ki;
                        int sj = j * 1 + kj;
                        sum += input_channel[si * input_w + sj];
                    }
                }
                output_channel[i * out_w + j] = sum;
            }
        }
        // 如下是执行 feature map 的归一化、并保存为 .pgm 图像的过程
        const char* save_prefix = "S2_output";
        for (int k = 0; k < number_of_kernel; k++)
        {
            double* channel = output[k];
            
            // get normalized (gray) image from one feature map channel
            uchar* output_image = (uchar*)malloc(sizeof(uchar) * out_h * out_w);
            get_normalized_gray_image_from_channel(channel, out_h, out_w, output_image);
            char savepath[200] = { 0 };
            sprintf(savepath, "%s%d.pgm", save_prefix, k);
            write_pgm_image(output_image, out_w, out_h, savepath);
            free(output_image);
        }
    }
}
 
3. 可视化结果
第一张图: 左侧为 C1 结果, 正确; 右侧为 S2 结果, 错误, 看起来只对 C1 输出结果的左上 1/4 执行了 conv:(通过可视化,可以为 Debug 快速提供思路):
 
第二张图: 修复了 SubSampling 前向计算结果后,得到的 C1 和 S2 的输出结果图:
 
4. References
- https://en.cppreference.com/w/c/types/limits
 - scratch lenet(1): 读写 pgm 图像文件
 








![[架构之路-214]- UML-类图图解、详解、结构化、本质化讲解](https://img-blog.csdnimg.cn/20201231140635472.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzM2MTcyMg==,size_16,color_FFFFFF,t_70)










