scratch lenet(6): feature_map可视化的C语言实现

scratch lenet(6): feature_map可视化的C语言实现

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)=maxminvmin

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

  1. https://en.cppreference.com/w/c/types/limits
  2. scratch lenet(1): 读写 pgm 图像文件
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值