纯C语言实现CNN-46页超详细解析——(二)(反向传播以前解析)

接上文,初始化操作与文件操作结束后,下一步:

交互内容

一些结构体

struct parameter //网络参数结构体
{
    double kernel1[3][3];
    double kernel11[3][3];
    double kernel2[3][3];
    double kernel22[3][3];
    double kernel3[3][3];
    double kernel33[3][3];
    double firsthiddenlayer[1152][180];
    double secondhiddenlayer[180][45];
    double outhiddenlayer[45][10];
};
struct result //保存网络每一步输出结果的结构体,为反向传播计算梯度提供数据
{
    double picturedata[30][30];
    double firstcon[28][28];
    double firstcon1[28][28];
    double secondcon[26][26];
    double secondcon1[26][26];
    double thirdcon[24][24];
    double thirdcon1[24][24];
    double beforepool[1][1152];
    double firstmlp[1][180];
    double firstrelu[1][180];
    double secondmlp[1][45];
    double secondrelu[1][45];
    double outmlp[1][10]; //全连接输出
    double result[10]; //softmax输出
};

选择是否使用已训练的网络参数

tip:是否使用

iiii:
        printf("请问您是否希望从已训练的网络参数文件中读取网络参数?是请按y,否请按n。\n");
    setbuf(stdin, NULL); //清空键盘缓冲区

这段代码是用户交互的一部分,提供了一个简单的文本菜单,让用户选择是否从文件中读取已训练的网络参数。下面是代码的逐行解释:

  1. iiii:
    这一行是一个标签,用于与 goto 语句配合使用,以便在用户输入不是预期的 ‘y’ 或 ‘n’ 时,可以跳回这个位置重新接收输入。

  2. printf("请问您是否希望从已训练的网络参数文件中读取网络参数?是请按y,否请按n。\\n");
    这行代码使用 printf 函数打印出提示信息,询问用户是否希望从文件中读取已训练的网络参数。如果用户选择读取,他们应该按 ‘y’;如果不想读取,应该按 ‘n’。

  3. setbuf(stdin, NULL);
    这行代码调用 setbuf 函数,将标准输入流 stdin 的缓冲区设置为 NULL。在 C 语言中,输入通常被缓冲,这意味着在输入被读取之前,它会先存储在内存中的某个地方。调用 setbuf(stdin, NULL) 的目的是禁用标准输入的缓冲,这样输入就可以立即被读取,而不会被存储在缓冲区中。

    使用 setbuf(stdin, NULL) 是一种清空键盘输入缓冲区的方法,确保在下一次读取输入时,立即获取用户的输入,而不是从缓冲区读取旧的输入。

然而,需要注意的是,setbuf 函数在某些情况下可能不会按预期工作,特别是在 Windows 系统上,或者当程序已经从缓冲读取过输入后。在这种情况下,使用 fflush 函数清空输入缓冲区可能是一个更好的选择:

fflush(stdin);

此外,goto 语句的使用通常不是最佳实践,因为它可能导致代码难以理解和维护。在实际编程中,考虑使用循环(如 whiledo-while)来处理用户输入,直到获得有效的响应。例如:

char choice;
do {
    printf("请问您是否希望从已训练的网络参数文件中读取网络参数?是请按y,否请按n。\\n");
    scanf(" %c", &choice); // 注意前面的空格,用于消耗前面的换行符
} while (choice != 'y' && choice != 'n');

这样的循环结构可以更清晰地处理用户输入,直到用户输入了有效的选项。

具体操作

 char g;
    g = getch();
    if (g == 'y') {
        int h = read_file(storage);
        if (h == 1) {
            printf("参数包不存在!开始自动随机初始化网络参数\n");
            initialization(storage);
            printf_file(storage);
            printf("网络参数初始化完毕!\n");
        }
        if (h == 0) printf("参数读取成功!\n");
    } else if (g == 'n') {
        initialization(storage);
        printf_file(storage);
        printf("网络参数初始化完毕!\n");
    } else goto iiii;

这段代码是程序中的一个分支结构,用于根据用户输入决定是否从文件中读取已训练的网络参数,或者直接初始化网络参数。代码使用了 getch() 函数来获取用户的单个字符输入,而不会等待用户按下回车键。

  • 打开已训练网络参数

    int read_file(struct parameter * parameter4) //用于训练前读取网络参数的函数
    {
        FILE * fp;
        fp = fopen("Training_set//Network_parameter.bin", "rb");
        if (fp == NULL) {
            printf("文件打开失败,请检查网络参数文件是否在训练集文件夹内!\n");
            return 1;
        }
        struct parameter * parameter1;
        parameter1 = (struct parameter * ) malloc(sizeof(struct parameter));
        fread(parameter1, sizeof(struct parameter), 1, fp);
        ( * parameter4) = ( * parameter1);
        fclose(fp);
        free(parameter1);
        parameter1 = NULL;
        return 0;
    }
    

    这段代码定义了一个名为 read_file 的函数,其目的是从文件中读取神经网络的参数,并将其存储在结构体 parameter 中。以下是对函数的详细解释:

    1. 函数声明:

      int read_file(struct parameter * parameter4)
      
      
      • int 表示这个函数将返回一个整数值。
      • read_file 是函数的名称。
      • struct parameter * parameter4 是函数的参数,它是一个指向 parameter 结构体的指针。
    2. 打开文件:

      fp = fopen("Training_set//Network_parameter.bin", "rb");
      
      
      • 使用 fopen 函数以二进制读取模式 ("rb") 尝试打开文件 "Training_set//Network_parameter.bin"
      • 如果文件无法打开,fp 将为 NULL
    3. 错误检查:

      if (fp == NULL) {
          printf("文件打开失败,请检查网络参数文件是否在训练集文件夹内!\\n");
          return 1;
      }
      
      
      • 如果 fpNULL,则打印错误信息,并返回 1 表示打开文件失败。
    4. 动态内存分配:

      parameter1 = (struct parameter * ) malloc(sizeof(struct parameter));
      
      
      • parameter 结构体分配内存,并将其地址赋给指针 parameter1
    5. 读取文件内容到结构体:

      fread(parameter1, sizeof(struct parameter), 1, fp);
      
      
      • 使用 fread 函数从文件 fp 中读取一个 parameter 结构体大小的数据到 parameter1 指向的内存中。
    6. 复制参数:

      ( * parameter4) = ( * parameter1);
      
      
      • parameter1 指向的结构体内容复制到 parameter4 指向的结构体中。
    7. 关闭文件和释放内存:

      fclose(fp);
      free(parameter1);
      parameter1 = NULL;
      
      
      • 使用 fclose 关闭文件 fp
      • 使用 free 释放之前分配给 parameter1 的内存。
      • parameter1 设置为 NULL,以避免悬空指针问题。
    8. 正常返回:

      return 0;
      
      
      • 如果函数成功执行,返回 0 表示成功。

    这个函数通过从文件中读取数据,允许程序加载之前训练好的神经网络参数,而不是从头开始训练。这是一种常见的做法,特别是在需要微调模型或在不同实验中重用训练成果时。函数中的动态内存分配和错误处理是良好的编程实践,有助于避免内存泄漏和处理潜在的I/O错误。

  • 下面是逐行解释:

    1. char g;
      声明一个 char 类型的变量 g,用于存储用户输入的单个字符。
    2. g = getch();
      调用 getch() 函数读取用户输入的单个字符,并将其存储在变量 g 中。getch() 函数通常来自 <conio.h> 头文件,它是一些编译器(特别是某些Windows平台上的编译器)特有的,用于获取控制台输入。
    3. if (g == 'y') { ... }
      如果用户输入字符 y,则进入此条件分支:
      • 调用 read_file(storage) 函数尝试从文件中读取网络参数。
      • 如果 read_file 返回 1,表示文件不存在或读取失败,则打印错误消息,并执行 initialization(storage) 函数来随机初始化网络参数,然后调用 printf_file(storage) 函数保存初始化后的参数,并打印初始化完成的消息。
      • 如果 read_file 返回 0,表示读取成功,则打印读取成功的消息。
    4. else if (g == 'n') { ... }
      如果用户输入字符 n,则进入此条件分支:
      • 执行 initialization(storage) 函数来初始化网络参数。
      • 调用 printf_file(storage) 函数保存初始化后的参数,并打印初始化完成的消息。
    5. else goto iiii;
      如果用户输入既不是 y 也不是 n,则使用 goto 语句跳转到代码开头的标签 iiii。这个标签通常位于循环的开始,允许用户重新输入。

    使用 getch() 函数可以实时获取用户的输入,而不需要按回车键,这在某些交互式应用程序中很有用。然而,<conio.h> 头文件和 getch() 函数不是C语言的标准库的一部分,它们主要在MS-DOS、Windows和一些旧的Unix系统中可用。在编写跨平台代码时,应避免使用这些非标准的函数,而使用如 getchar() 这样的标准库函数。

    此外,goto 语句的使用通常不推荐,因为它可能导致代码的逻辑流程变得难以追踪和维护。更现代的编程实践推荐使用循环结构(如 whiledo-while)来处理用户输入,直到获得有效的响应。

选择训练以及保存操作

oooo:
        printf("请输入您想训练的次数:\n");
    int d;
    scanf("%d", & d);
    printf("开始训练\n");
    learn(d, data, storage); //开始训练
    test_network(data, storage); //测试网络
    printf_file(storage);
    kkkk:
        printf("继续训练请按1,退出请按2\n");
    setbuf(stdin, NULL);
    char v;
    v = getch();
    if (v == '1') goto oooo;
    else if (v == '2') {
        printf_file(storage);
        return 0;
    } //退出则在退出之前保存网络参数
    else goto kkkk;
    return 0;
  • 这段代码是一个交互式训练和测试神经网络的程序片段。它使用 goto 语句来控制程序流程,但这种做法并不是最佳实践,因为它会使代码难以阅读和维护。下面我将解释代码的逻辑:

    1. oooo:
      这是一个标签,goto 语句将使用这个标签来跳转回这个位置,从而允许用户重新输入训练次数。
    2. printf("请输入您想训练的次数:\\n");
      打印提示信息,要求用户输入训练迭代的次数。
    3. int d;
      声明一个整型变量 d,用于存储用户输入的训练次数。
    4. scanf("%d", &d);
      使用 scanf 函数读取用户输入的整数并存储在变量 d 中。
    5. printf("开始训练\\n");
      打印开始训练的信息。
    6. learn(d, data, storage);
      调用 learn 函数,传入训练次数 d 和两个指向结构体的指针 datastorage,以执行训练过程。
    7. test_network(data, storage);
      调用 test_network 函数,传入 datastorage 指针,以测试训练后的网络性能。
    8. printf_file(storage);
      调用 printf_file 函数,可能用于打印或保存当前的网络参数。
    9. kkkk:
      这是另一个标签,用于在用户选择继续训练或退出时控制程序流程。
    10. printf("继续训练请按1,退出请按2\\n");
      打印提示信息,告诉用户如何继续训练或退出程序。
    11. setbuf(stdin, NULL);
      调用 setbuf 函数,将标准输入流 stdin 的缓冲区设置为 NULL,这可能是为了立即获取用户输入,而不是从缓冲区读取。
    12. char v;
      声明一个字符变量 v,用于存储用户输入的字符。
    13. v = getch();
      使用 getch() 函数读取用户的单个字符输入,并存储在变量 v 中。
    14. if (v == '1') goto oooo;
      如果用户输入 ‘1’,则使用 goto 跳转回 oooo 标签,允许用户重新输入训练次数并继续训练。
    15. else if (v == '2') { ... }
      如果用户输入 ‘2’,则打印保存网络参数的信息,然后返回 0 退出程序。
    16. else goto kkkk;
      如果用户输入既不是 ‘1’ 也不是 ‘2’,则跳转回 kkkk 标签,让用户重新输入。
    17. return 0;
      程序正常退出,返回 0

    尽管使用 goto 可以实现这种交互式循环,但更好的做法是使用循环结构,如 whiledo-while,因为它们提供了更清晰和结构化的代码。此外,setbuf(stdin, NULL) 的使用也值得注意,因为它可能不是在所有环境中都有效,且其行为可能依赖于特定的实现和平台。通常,使用标准输入函数如 getchar() 就足够了,因为它会在每次调用后自动刷新输入流。

  • 保存训练后的网络参数

    void printf_file(struct parameter * parameter4) //用于训练结束后保存网络参数的函数
    {
        FILE * fp;
        fp = fopen("Training_set//Network_parameter.bin", "wb"); //采用二进制格式保存参数,便于读取
        struct parameter * parameter1;
        parameter1 = (struct parameter * ) malloc(sizeof(struct parameter));
        ( * parameter1) = ( * parameter4);
        fwrite(parameter1, sizeof(struct parameter), 1, fp); //打印网络结构体
        fclose(fp);
        free(parameter1);
        parameter1 = NULL;
        return;
    }
    

    这段代码定义了一个名为 printf_file 的函数,其目的是在训练结束后将神经网络的参数保存到文件中。以下是对函数的详细解释:

    1. 函数声明:

      void printf_file(struct parameter * parameter4)
      
      
      • void 表示这个函数不返回任何值。
      • printf_file 是函数的名称。
      • struct parameter * parameter4 是函数的参数,它是一个指向 parameter 结构体的指针,该结构体包含网络参数。
    2. 打开文件:

      fp = fopen("Training_set//Network_parameter.bin", "wb");
      
      
      • 使用 fopen 函数以二进制写入模式 ("wb") 尝试打开或创建文件 "Training_set//Network_parameter.bin"
      • 如果文件无法打开,fp 将为 NULL
    3. 动态内存分配:

      parameter1 = (struct parameter * ) malloc(sizeof(struct parameter));
      
      
      • parameter 结构体分配内存,并将其地址赋给指针 parameter1
    4. 复制参数:

      ( * parameter1) = ( * parameter4);
      
      
      • parameter4 指向的结构体内容复制到 parameter1 指向的新分配的内存中。
    5. 写入文件内容:

      fwrite(parameter1, sizeof(struct parameter), 1, fp);
      
      
      • 使用 fwrite 函数将 parameter1 指向的结构体(即网络参数)写入到文件 fp 中。
    6. 关闭文件和释放内存:

      fclose(fp);
      free(parameter1);
      parameter1 = NULL;
      
      
      • 使用 fclose 关闭文件 fp
      • 使用 free 释放之前分配给 parameter1 的内存。
      • parameter1 设置为 NULL,以避免悬空指针问题。
    7. 函数返回:

      return;
      
      
      • 函数执行完毕,返回。

    这个函数通过将网络参数写入到文件中,允许程序保存训练后的神经网络状态,这样参数可以被重新加载,用于后续的推理或进一步的训练。函数中的动态内存分配、文件操作和错误处理是必要的,以确保参数正确保存且避免内存泄漏。

    需要注意的是,这个函数假设 parameter4 指向的结构体包含了有效的网络参数。此外,函数没有包含错误检查,例如检查 fopenfwrite 是否成功执行。在实际应用中,应该添加适当的错误处理来提高程序的健壮性。


最后一个板块:

训练

训练总代码

 learn(d, data, storage); //开始训练
    test_network(data, storage); //测试网络

data是训练一轮后的结果参数

learn

  • 训练后输出结果以及过程中保存最优参数

     learningrate = pow((max2 / 10.0), 1.7);               
            if (learningrate >= 0.01) learningrate = 0.01;
            if ((o + 1) % 10 == 0) //每十次训练(若有300个样本则是训练了3000次)刷新一次训练进度及学习率等数据
            {
                if (o != 9) printf("\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b");
                if (100 * ((double)(o + 1) / (double) m) < 10) printf("训练进度: %lf", 100 * ((double)(o + 1) / (double) m));
                else printf("训练进度:%lf", 100 * ((double)(o + 1) / (double) m));
                if (max2 < 10) printf("%%  交叉熵损失: %lf  学习率:%.10lf", max2, learningrate);
                else if (max2 >= 10) printf("%%  交叉熵损失:%lf  学习率:%.10lf", max2, learningrate);
                if (learningrate < 0.0000000001) printf_file2(parameter1); //如果找到局部最优则打印网络参数
            }
    

    这段代码是训练神经网络时用于显示训练进度和调整学习率的一部分。以下是详细的中文解释:

    1. if ((o + 1) % 10 == 0): 这个条件判断当前的训练次数(o + 1)除以10的余数是否为0。如果是,这意味着每训练10次(对于300个样本,就是训练了3000次),执行一次下面的代码块。
    2. if (o != 9) printf("\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b");: 如果当前的训练次数不是9(即不是第一次检查时),则使用 \\b(退格符)来覆盖控制台上之前打印的进度信息。
    3. if (100 * ((double)(o + 1) / (double) m) < 10) printf("训练进度: %lf", 100 * ((double)(o + 1) / (double) m));: 如果当前训练次数的百分比小于10%,则打印训练进度。这里将当前训练次数加1后转换为double类型,与总次数m相除,再乘以100得到百分比,并打印出来。
    4. else printf("训练进度:%lf", 100 * ((double)(o + 1) / (double) m));: 如果当前训练次数的百分比大于或等于10%,则直接打印训练进度。
    5. if (max2 < 10) printf("%% 交叉熵损失: %lf 学习率:%.10lf", max2, learningrate);: 如果变量max2(可能表示当前的最大损失或误差)小于10,打印当前的交叉熵损失和学习率,学习率保留10位小数。
    6. else if (max2 >= 10) printf("%% 交叉熵损失:%lf 学习率:%.10lf", max2, learningrate);: 如果max2大于或等于10,也打印当前的交叉熵损失和学习率,学习率同样保留10位小数。
    7. if (learningrate < 0.0000000001) printf_file2(parameter1);: 如果当前的学习率小于一个非常小的数(0.0000000001),则调用printf_file2函数,可能用于在找到局部最优解时保存或打印网络参数。

    这段代码的目的是提供一个简单的训练进度和状态的实时更新,帮助用户了解训练过程的当前状态,包括训练进度、损失和学习率等关键信息。通过控制台输出这些信息,用户可以监控训练过程是否顺利进行。此外,通过检查学习率,可以在达到一定的收敛条件时采取相应的措施,例如保存网络参数。

  • 洗牌,先随机生成随机种子,然后随机两两打乱

    int a, b;
            srand(time(NULL)); //洗牌算法,用于打乱样本
            for (int q = 0; q < 300; q++) {
                a = (int)((rand() / (RAND_MAX + 1.0)) * 300); //确定本轮随机交换的变量下标
                b = (int)((rand() / (RAND_MAX + 1.0)) * 300);
                if (a >= 0 && a < 300 && (a != b) && b >= 0 && b < 300) {
                    struct sample * sample5;
                    sample5 = (struct sample * ) malloc(sizeof(struct sample));
                    ( * sample5) = Sample[a];
                    Sample[a] = Sample[b];
                    Sample[b] = ( * sample5);
                    free(sample5);
                    sample5 = NULL;
                } else continue;
            }
    

    这段代码的目的是实现一个洗牌算法,用于随机打乱一个包含样本数据的数组。以下是代码的详细解释:

    1. int a, b;
      声明两个整型变量 ab,它们将用于存储随机生成的索引。
    2. srand(time(NULL));
      使用当前时间作为种子初始化随机数生成器,确保每次程序运行时生成的随机数序列都是不同的。
    3. for (int q = 0; q < 300; q++) { ... }
      开始一个循环,这个循环将执行300次,用于进行300次随机交换尝试。
    4. a = (int)((rand() / (RAND_MAX + 1.0)) * 300);
      生成一个0到299之间的随机整数 a。这里使用 rand() 函数生成一个随机数,然后通过除以 RAND_MAX + 1(确保结果在0到1之间)并乘以300来缩放到所需的范围。
    5. b = (int)((rand() / (RAND_MAX + 1.0)) * 300);
      类似地,生成另一个0到299之间的随机整数 b
    6. if (a >= 0 && a < 300 && (a != b) && b >= 0 && b < 300) { ... }
      检查生成的索引 ab 是否都在0到299的范围内,并且 ab 不相同。这是必要的,因为数组 Sample 的索引范围是从0到299。
    7. struct sample * sample5;
      声明一个指向 sample 结构体的指针 sample5
    8. sample5 = (struct sample * ) malloc(sizeof(struct sample));
      sample5 分配内存,大小为一个 sample 结构体的大小。
    9. ( * sample5) = Sample[a];
      Sample 数组中索引为 a 的元素复制到 sample5 指向的新分配的内存中。
    10. Sample[a] = Sample[b];
      Sample 数组中索引为 b 的元素复制到索引为 a 的位置。
    11. Sample[b] = ( * sample5);
      sample5 指向的元素(即原来的 Sample[a])复制到索引为 b 的位置。
    12. free(sample5);
      释放之前为 sample5 分配的内存。
    13. sample5 = NULL;
      sample5 设置为 NULL,这是一个好习惯,可以避免悬空指针问题。
    14. else continue;
      如果索引 ab 不满足条件(例如,它们相同或超出范围),则跳过本次循环的剩余部分,继续下一次迭代。

    代码中存在一些可以改进的地方:

    • if 条件中,ab 已经通过相同的随机数生成逻辑得到了限制在0到299之间,因此不需要再次检查 a >= 0 && a < 300 && b >= 0 && b < 300
    • else continue; 是多余的,因为如果 if 条件不满足,循环会自动继续到下一次迭代。
    • 每次交换样本后,应该检查 ab 是否指向不同的样本,如果是相同的,交换是多余的。

    此外,代码中的注释 “洗牌算法,用于打乱样本” 说明了这段代码的用途,即使样本数据随机化,类似于洗牌,以确保数据的随机性。这对于训练机器学习模型时避免过拟合和提高模型的泛化能力是非常重要的。

  • 训练过程

    for (int i = 0; i < SAMPLE_NUM * 10; i++) //训练已经打乱的所有样本
            {
                max2 = 0;
                struct sample * sample3;
                sample3 = (struct sample * ) malloc(sizeof(struct sample));
                ( * sample3) = Sample[i];
                int y = sample3 -> number;
                forward_propagating(data, & sample3 -> a[0][0], parameter1); //正向传播
                backPropagation(y, data, parameter1); //反向传播
                free(sample3);
                sample3 = NULL;
                double g = Cross_entropy( & data -> result[0], y); //计算本轮最大交叉熵损失,用于指导调整学习率
                if (g > max2) max2 = g;
            }
        }
    

    这段代码是神经网络训练过程中的一个环节,目的是遍历所有已打乱的样本数据,进行正向传播和反向传播,同时计算并更新本轮训练的最大交叉熵损失。以下是代码的详细解释:

    1. for (int i = 0; i < SAMPLE_NUM * 10; i++) { ... }
      这是一个循环,遍历所有的样本。SAMPLE_NUM * 10 应该是样本总数,其中 SAMPLE_NUM 是每个数字类别的样本数量,10 表示数字 0 到 9 的类别总数。
    2. max2 = 0;
      将变量 max2 初始化为 0。这个变量用于记录本轮训练中遇到的最大交叉熵损失。
    3. struct sample * sample3;
      声明一个指向 sample 结构体的指针 sample3
    4. sample3 = (struct sample * ) malloc(sizeof(struct sample));
      sample3 分配内存,大小为一个 sample 结构体的大小。
    5. ( * sample3) = Sample[i];
      通过赋值运算符将 Sample 数组中索引为 i 的样本复制到 sample3 指向的内存中。
    6. int y = sample3 -> number;
      sample3 结构体中取出样本的标签 number,并将其存储在变量 y 中。
    7. forward_propagating(data, & sample3 -> a[0][0], parameter1);
      调用 forward_propagating 函数进行正向传播。传入当前样本的像素数据 sample3 -> a,以及网络参数 parameter1
    8. backPropagation(y, data, parameter1);
      调用 backPropagation 函数进行反向传播。传入当前样本的标签 y,以及网络的中间数据 data 和参数 parameter1
    9. free(sample3);
      释放之前为 sample3 分配的内存。
    10. sample3 = NULL;
      sample3 设置为 NULL,避免悬空指针问题。
    11. double g = Cross_entropy( & data -> result[0], y);
      调用 Cross_entropy 函数计算当前样本的交叉熵损失,并将结果存储在变量 g 中。
    12. if (g > max2) max2 = g;
      如果新计算的交叉熵损失 g 大于当前记录的最大值 max2,则更新 max2

    这个循环在每次迭代中处理一个样本,通过正向传播和反向传播更新网络权重,并记录下本轮训练中的最大交叉熵损失。这种方法有助于监控训练过程中的损失变化,并对学习率等超参数进行调整。然而,代码中存在一些潜在问题:

    • 每次迭代都为 sample3 分配和释放内存,这可能会导致不必要的性能开销。通常,更好的做法是预先分配一个数组来存储所有样本指针,然后在循环结束后一次性释放。
    • forward_propagatingbackPropagation 函数调用中,确保 data 结构体和 parameter1 指针正确地指向了当前网络的状态和参数。
    • 交叉熵损失的计算和更新逻辑可能需要根据实际的损失函数和训练策略进行调整。
  • 前向传播

    void forward_propagating(struct result * data, double * a, struct parameter * c) //正向传播的函数
    {
        for (int z = 0; z < 30; z++)
            for (int g = 0; g < 30; g++) data -> picturedata[z][g] = a[z * 30 + g]; //这个循环没用,只是为了调试网络时为了使输入图片可视化的操作,保存下图片数据
        Convolution(30, 30, 3, a, & c -> kernel1[0][0], & data -> firstcon[0][0]); //第一通道第一层卷积
        Convolution(30, 30, 3, a, & c -> kernel11[0][0], & data -> firstcon1[0][0]); //第二通道第一层卷积
        Convolution(28, 28, 3, & data -> firstcon[0][0], & c -> kernel2[0][0], & data -> secondcon[0][0]); //第一通道第二层卷积
        Convolution(28, 28, 3, & data -> firstcon1[0][0], & c -> kernel22[0][0], & data -> secondcon1[0][0]); //第二通道第二层卷积
        Convolution(26, 26, 3, & data -> secondcon[0][0], & c -> kernel3[0][0], & data -> thirdcon[0][0]); //第一通道第三层卷积
        Convolution(26, 26, 3, & data -> secondcon1[0][0], & c -> kernel33[0][0], & data -> thirdcon1[0][0]); //第二通道第三层卷积
        Matrix_expansion(24, 24, & data -> thirdcon[0][0], & data -> thirdcon1[0][0], & data -> beforepool[0][0]); //把卷积输出扩展成全连接输入
        Matrix_multiplication(1, 1152, 180, & data -> beforepool[0][0], & c -> firsthiddenlayer[0][0], & data -> firstmlp[0][0]); //第一层全连接
        Leakyrelu(1, 180, & data -> firstmlp[0][0], & data -> firstrelu[0][0]); //激活函数
        Matrix_multiplication(1, 180, 45, & data -> firstrelu[0][0], & c -> secondhiddenlayer[0][0], & data -> secondmlp[0][0]); //第二层全连接
        Leakyrelu(1, 45, & data -> secondmlp[0][0], & data -> secondrelu[0][0]); //激活函数
        Matrix_multiplication(1, 45, 10, & data -> secondrelu[0][0], & c -> outhiddenlayer[0][0], & data -> outmlp[0][0]); //第三层全连接
        double p = 0;
        for (int i = 0; i < 10; i++) //softmax分类器
        {
            p += (exp(data -> outmlp[0][i]));
        };
        for (int i = 0; i < 10; i++) {
            data -> result[i] = exp(data -> outmlp[0][i]) / p;
            result[i] = data -> result[i]; //softmax输出
        };
    
        return;
    }
    

    传入参数中的data 是上一次前向传播后的结果参数,parameter 是网络的参数

    • Convolution

      void Convolution(int m, int n, int p, double * a, double * b, double * c) //做卷积运算的函数
      {
          for (int i = 0; i < (m - p + 1); i++) {
              for (int j = 0; j < (n - p + 1); j++) {
                  int u = 0;
                  c[i * (n - p + 1) + j] = 0;
                  for (int k = i; k < i + 3; k++) {
                      int d = 0;
                      for (int l = j; l < j + 3; l++) {
                          c[i * (n - p + 1) + j] += a[k * n + l] * b[u * p + d];
                          d++;
                      }
                      u++;
                  }
              }
          }
      }
      

      这段代码定义了一个名为 Convolution 的函数,用于执行卷积运算,这是卷积神经网络(CNN)中的一个基本操作。卷积运算涉及将一个小的卷积核(或滤波器)滑动遍历输入图像(或特征图)的每个位置,并计算卷积核与图像的局部区域的点积,从而生成输出特征图。以下是对函数的详细解释:

      1. 函数参数:
        • int m: 输入图像的高度。
        • int n: 输入图像的宽度。
        • int p: 卷积核的高度和宽度,假设卷积核是正方形的。
        • double *a: 指向输入图像数据的指针。
        • double *b: 指向卷积核数据的指针。
        • double *c: 指向输出特征图数据的指针。
      2. 卷积计算:
        • 外层循环 for (int i = 0; i < (m - p + 1); i++) 遍历输出特征图的每一行。由于卷积核有 p 行,因此在高度方向上,输出的高度将是 m - p + 1
        • 内层循环 for (int j = 0; j < (n - p + 1); j++) 遍历输出特征图的每一列。同理,输出的宽度将是 n - p + 1
      3. 初始化输出:
        • 在计算每个输出元素之前,c[i * (n - p + 1) + j] = 0; 将该位置初始化为0,为累加做准备。
      4. 卷积核滑动:
        • 嵌套循环 for (int k = i; k < i + 3; k++)for (int l = j; l < j + 3; l++) 负责滑动卷积核遍历输入图像的局部区域。这里假设卷积核大小为3x3。
      5. 点积计算:
        • c[i * (n - p + 1) + j] += a[k * n + l] * b[u * p + d]; 这行代码计算卷积核和输入图像对应元素的乘积之和,即点积。变量 ud 分别用于卷积核的行和列索引。
      6. 索引更新:
        • d++u++ 用于更新卷积核的索引,以便正确地访问卷积核和输入图像的数据。
      7. 输出特征图:
        • 经过上述循环,c 数组中相应的位置将累积了整个卷积操作的结果,形成输出特征图的一个元素。

      这个 Convolution 函数实现了基本的卷积操作,没有考虑任何填充(padding)、步长(stride)或偏置项(bias),这些在实际的卷积神经网络中是常见的概念。此外,为了提高性能,通常会使用优化的库(如cuDNN、MKL等)来执行卷积操作,而不是手动实现。在实际应用中,还需要考虑内存管理、错误检查和并行计算等问题。

    • Matrix_expansion

      void Matrix_expansion(int m, int n, double * a, double * c, double * b) //把两个通道的卷积输出矩阵(24*24)展开并合并成一个1152长度的数组(向量)方便输入到全连接层
      {
          int k = 0;
          for (int i = 0; i < m; i++)
              for (int j = 0; j < n; j++) {
                  b[k] = a[i * n + j];
                  k++;
              }
          k = m * n;
          for (int i = 0; i < m; i++)
              for (int j = 0; j < n; j++) {
                  b[k] = c[i * n + j];
                  k++;
              }
      }
      

      这段代码定义了一个名为 Matrix_expansion 的函数,其作用是将两个相同尺寸的二维矩阵(通道)展平并合并成一个一维数组。这通常用于准备数据输入到全连接层(也称为密集层),在卷积神经网络(CNN)中这是一个常见的步骤。以下是对函数的详细解释:

      1. 函数参数:
        • int m: 输入矩阵的高度。
        • int n: 输入矩阵的宽度。
        • double *a: 指向第一个通道的二维矩阵的指针。
        • double *c: 指向第二个通道的二维矩阵的指针。
        • double *b: 指向输出一维数组的指针,这个数组足够大,可以容纳两个通道的所有数据。
      2. 第一次展平:
        • 外层循环 for (int i = 0; i < m; i++) 遍历第一个通道矩阵的每一行。
        • 内层循环 for (int j = 0; j < n; j++) 遍历每一行中的每个元素。
        • b[k] = a[i * n + j];a 矩阵中的当前元素复制到输出数组 b 中。
        • k++ 更新索引 k,以便在下一次迭代中将数据复制到 b 的下一个位置。
      3. 更新索引:
        • k = m * n; 在复制完第一个通道的所有元素后,将索引 k 设置为第一个通道展平后的长度,为复制第二个通道的数据做准备。
      4. 第二次展平:
        • 接下来的外层循环和内层循环与第一次展平相同,但现在它们用于复制第二个通道 c 的数据到 b 数组中。
      5. 合并:
        • 通过两次循环,b 数组现在包含了两个通道的展平数据,顺序是第一个通道的所有数据,紧接着是第二个通道的所有数据。
      6. 返回值:
        • 函数没有返回值。它通过直接修改 b 数组来合并两个通道的数据。
      7. 内存管理:
        • 假设 b 数组已经被分配了足够的内存来存储两个通道的所有数据(2 * m * n)。

      这个函数是将卷积层的输出转换为全连接层输入的典型方法。在卷积神经网络中,这种展平操作是必要的,因为全连接层期望输入为一维数组,而卷积层的输出是多维的。通过这种方式,可以确保网络的下一层(全连接层)能够接收并处理来自前一层(卷积层)的信息。

    • Matrix_multiplication

      void Matrix_multiplication(int m, int n, int p, double * a, double * b, double * c) //正向传播做矩阵乘法的函数
      {
          for (int i = 0; i < m; i++)
              for (int j = 0; j < p; j++) {
                  c[i * n + j] = 0;
                  for (int k = 0; k < n; k++) c[i * n + j] += a[i * n + k] * b[k * p + j];
              }
      }
      

      这段代码定义了一个名为 Matrix_multiplication 的函数,用于执行两个矩阵的乘法操作。这是线性代数中的基本操作,也是神经网络中正向传播过程中常用的计算步骤。以下是对函数的详细解释:

      1. 函数参数:
        • int m: 第一个矩阵 a 的行数。
        • int n: 第一个矩阵 a 的列数,同时也是结果矩阵 c 的列数。
        • int p: 第二个矩阵 b 的列数,也是结果矩阵 c 的行数。
        • double *a: 指向第一个矩阵数据的指针。
        • double *b: 指向第二个矩阵数据的指针。
        • double *c: 指向结果矩阵数据的指针。
      2. 矩阵乘法计算:
        • 外层循环 for (int i = 0; i < m; i++) 遍历结果矩阵 c 的每一行。
        • 次外层循环 for (int j = 0; j < p; j++) 遍历结果矩阵 c 的每一列。
        • 在计算每个元素 c[i * n + j] 之前,先将其初始化为0:c[i * n + j] = 0;
      3. 累加计算:
        • 内层循环 for (int k = 0; k < n; k++) 用于计算矩阵 a 的第 i 行和矩阵 b 的第 j 列之间的点积。
        • 表达式 a[i * n + k] * b[k * p + j] 计算了矩阵 a 中第 i 行第 k 列元素与矩阵 b 中第 k 行第 j 列元素的乘积。
        • 结果 c[i * n + j] 是通过累加所有这些乘积得到的。
      4. 结果矩阵:
        • 经过上述循环,c 数组中相应的位置将存储了两个矩阵相乘的结果。
      5. 内存管理:
        • 假设 c 数组已经被分配了足够的内存来存储结果矩阵 m x p 的所有数据。

      这个函数实现了矩阵乘法的基本算法,没有使用任何特定的优化技术。在实际应用中,矩阵乘法的性能至关重要,尤其是在大规模数据处理和深度学习中。为了提高性能,通常会使用特殊的库(如BLAS或cuBLAS等)来执行矩阵运算,这些库提供了高度优化的算法和利用硬件加速的能力。

      此外,这段代码中的矩阵乘法假设了矩阵 ab 的尺寸是兼容的,即 a 的列数 n 必须与 b 的行数 p 相等。如果这些条件不满足,矩阵乘法将无法进行。在实际编程中,还需要考虑错误检查和异常处理,以确保程序的健壮性。

    • Leakyrelu

      void Leakyrelu(int m, int n, double * a, double * b) //激活函数
      {
          for (int i = 0; i < m; i++)
              for (int j = 0; j < n; j++) {
                  (b[i * n + j]) = max(a[i * n + j], a[i * n + j] * 0.05);
              }
      }
      

      这段代码定义了一个名为 Leakyrelu 的函数,它实现了一种称为 LeakyReLU 的激活函数。激活函数是神经网络中非线性变换的一部分,允许网络学习复杂的模式。LeakyReLU 是 ReLU(Rectified Linear Unit)函数的一个变体,它允许负输入的小梯度,从而有助于解决 ReLU 函数中可能出现的“死亡ReLU”问题(即神经元在训练过程中可能永远不会激活)。以下是对函数的详细解释:

      1. 函数参数:
        • int m: 输入矩阵 a 的行数。
        • int n: 输入矩阵 a 的列数。
        • double *a: 指向输入数据的指针,这是需要应用激活函数的原始数据。
        • double *b: 指向输出数据的指针,将存储激活函数的结果。
      2. 激活函数计算:
        • 外层循环 for (int i = 0; i < m; i++) 遍历输入矩阵 a 的每一行。
        • 内层循环 for (int j = 0; j < n; j++) 遍历每一行中的每个元素。
      3. LeakyReLU 公式:
        • (b[i * n + j]) = max(a[i * n + j], a[i * n + j] * 0.05); 这行代码实现了 LeakyReLU 激活函数的公式。对于每个元素 a[i * n + j]
          • 如果该元素大于0,则 b[i * n + j] 直接等于 a[i * n + j],这是标准的 ReLU 行为。
          • 如果该元素小于0,则 b[i * n + j] 等于该元素乘以0.05(或其他小的正斜率),这允许负值有一个小的梯度,而不是完全抑制。
      4. 内存管理:
        • 函数不直接分配或释放内存,它仅通过指针 b 修改已存在的内存区域。
      5. 注意事项:
        • 函数假设 b 指针指向的内存足够大,可以存储 m x n 个元素。
        • 函数没有显式的错误检查,例如检查 ab 指针是否为 NULL
      6. 性能优化:
        • 函数实现是逐元素的,这意味着它可以很容易地在并行计算环境中优化,例如使用 SIMD 指令集或 GPU 加速。
      7. 函数返回:
        • 函数没有返回值,它通过直接修改 b 指针指向的内存来存储结果。

      LeakyReLU 激活函数因其在训练深层网络时的性能优势而被广泛使用。它避免了 ReLU 函数的一些缺点,同时保持了网络的非线性特性。在实际应用中,LeakyReLU 的斜率参数(这里是0.05)可以根据具体问题进行调整。

    • softmax

          double p = 0;
          for (int i = 0; i < 10; i++) //softmax分类器
          {
              p += (exp(data -> outmlp[0][i]));
          };
          for (int i = 0; i < 10; i++) {
              data -> result[i] = exp(data -> outmlp[0][i]) / p;
              result[i] = data -> result[i]; //softmax输出
          };
      

      这段代码实现了softmax函数,它是机器学习和深度学习中常用的激活函数,特别是在多分类问题中。softmax函数将一个向量或矩阵的每个元素转换为0到1之间的值,这些值的总和为1,从而可以被解释为概率分布。以下是对代码的详细解释:

      1. 初始化累加变量:
        • double p = 0;
          这行代码初始化了一个名为 p 的变量,它将用于累加指数函数的结果。
      2. 计算指数和:
        • 第一个循环 for (int i = 0; i < 10; i++) 遍历数组 data->outmlp[0] 中的10个元素。
        • 在循环内部,exp(data->outmlp[0][i]) 计算了 data->outmlp[0][i] 的指数(自然指数e的幂)。
        • p += (exp(data->outmlp[0][i])); 将每个指数值累加到变量 p 中。
      3. 计算softmax:
        • 第二个循环再次遍历数组 data->result
        • exp(data->outmlp[0][i]) / p; 计算了softmax函数的结果,即每个元素的指数值除以所有指数值的总和。
        • data->result[i] = ...; 将计算得到的softmax值赋给 data->result[i]
      4. 存储softmax输出:
        • result[i] = data->result[i];
          这行代码将softmax的结果复制到另一个数组 result 中。这个数组可能是全局变量或函数的其他部分需要使用的数组。
      5. 注意事项:
        • 代码中的分号 ; 在C语言中是语句结束的标志,但在循环和条件语句之后不需要使用分号。
        • 在实际应用中,计算指数和时可能会发生数值溢出或下溢的问题。在实现时,通常需要对输入进行规范化(减去最大值)以减少这种风险。
        • 由于 exp 函数可能返回一个非常大的数值,因此在计算 p 时可能会超出 double 类型的范围。在实际应用中,可能需要使用更稳健的实现,如expm1函数来避免溢出。
      6. 性能优化:
        • 由于softmax计算在深度学习中非常常见,许多深度学习框架和数学库都提供了优化的实现。
      7. 函数返回:
        • 这段代码没有返回值,它通过直接修改 data->resultresult 数组来存储softmax的结果。

      总结来说,这段代码通过计算输入数组的每个元素的指数,然后除以这些指数的总和,实现了softmax函数,从而得到了一个概率分布。这在多分类神经网络的输出层非常常见,用于预测每个类别的概率。

  • 14
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值