将卷积层(Convolution)、下采样层(SubSampling,也就是池化)层前向计算结果,归一化后转为图像,保存为文件, 用于可视化感受结果,也用于快速调试排查错误。
卷积、池化的输出结果是 double 类型, 数据范围超过了 [0, 255]. 通过统计最大最小值, 可以执行归一化并转为图像。
实际上还可以用于反向传播更新后的 feature map 的可视化, 不过目前还没实现反向传播, 暂时只有 conv, pool 的前向计算结果 feature map 的可视化。
normalized ( v ) = v − min max − min \text{normalized}(v) = \frac{v - \text{min}}{\text{max} - \text{min}} normalized(v)=max−minv−min
代码实现不依赖于 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);
}
}
}
前一小节是实现代码, 还需要知道怎样调用。在卷积、池化层里,对计算结果应用上述函数即可。
保存结果时, 使用了 .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);
}
}
}
第一张图: 左侧为 C1 结果, 正确; 右侧为 S2 结果, 错误, 看起来只对 C1 输出结果的左上 1/4 执行了 conv:(通过可视化,可以为 Debug 快速提供思路):
第二张图: 修复了 SubSampling 前向计算结果后,得到的 C1 和 S2 的输出结果图: