深度学习笔记(一)
感知机模型及相关基础概念

神经网络

基础的模型和算法

M-P模型

以下是一个简单的 m-P 神经网络模型(McCulloch-Pitts 模型)的 C 语言示例代码。这个代码实现了一个二输入单输出的神经元逻辑门(如 AND 门):

 1#include <stdio.h>
 2
 3// 定义激活函数
 4int activation_function(int sum) {
 5    return sum >= 2 ? 1 : 0;  // 阈值为2,类似AND门
 6}
 7
 8int main() {
 9    // 定义输入和权重
10    int inputs[2];     // 两个输入
11    int weights[2] = {1, 1}; // 权重
12    int threshold = 2; // 阈值
13
14    // 输入提示
15    printf("请输入两个输入(0或1):\n");
16    for (int i = 0; i < 2; i++) {
17        printf("输入%d: ", i + 1);
18        scanf("%d", &inputs[i]);
19        if (inputs[i] != 0 && inputs[i] != 1) {
20            printf("无效输入,请输入0或1。\n");
21            return 1;
22        }
23    }
24
25    // 计算加权和
26    int sum = 0;
27    for (int i = 0; i < 2; i++) {
28        sum += inputs[i] * weights[i];
29    }
30
31    // 使用激活函数计算输出
32    int output = activation_function(sum);
33
34    // 输出结果
35    printf("神经元输出: %d\n", output);
36
37    return 0;
38}

代码说明

  1. 激活函数

    • activation_function(int sum) 根据加权和是否超过阈值,决定输出是 1(激活)还是 0(未激活)。
    • 这里的逻辑是 sum >= 2,类似于 AND 门的逻辑。
  2. 输入和权重

    • 用户输入两个值(01)。
    • 权重设置为 [1, 1],表示每个输入的权重相同。
  3. 加权和

    • 计算公式为 sum += inputs[i] * weights[i]
  4. 输出

    • 使用激活函数决定最终输出。

示例运行

输入两个值 11

请输入两个输入(0或1):
输入1: 1
输入2: 1
神经元输出: 1

输入两个值 10

请输入两个输入(0或1):
输入1: 1
输入2: 0
神经元输出: 0

此代码可扩展为更多输入、复杂逻辑的神经网络。如果需要更复杂的 m-P 模型(例如多层结构),可以进一步增加神经元和连接逻辑。


改进后的M-P模型

下面是一个改进后的 McCulloch-Pitts (m-P) 模型的 C 语言实现,可以支持任意数量的输入、权重和阈值,使其适用于不同的逻辑门或神经元的实现:

 1#include <stdio.h>
 2#include <stdlib.h>
 3
 4// 定义激活函数
 5int activation_function(int sum, int threshold) {
 6    return sum >= threshold ? 1 : 0;  // 阈值判定
 7}
 8
 9// m-P 模型实现
10int m_p_model(int *inputs, int *weights, int num_inputs, int threshold) {
11    int sum = 0;
12    for (int i = 0; i < num_inputs; i++) {
13        sum += inputs[i] * weights[i]; // 计算加权和
14    }
15    return activation_function(sum, threshold); // 返回激活函数输出
16}
17
18int main() {
19    int num_inputs;
20    
21    // 动态输入个数
22    printf("请输入输入的数量: ");
23    scanf("%d", &num_inputs);
24    if (num_inputs <= 0) {
25        printf("输入数量必须大于0。\n");
26        return 1;
27    }
28
29    // 分配动态数组
30    int *inputs = (int *)malloc(num_inputs * sizeof(int));
31    int *weights = (int *)malloc(num_inputs * sizeof(int));
32    if (inputs == NULL || weights == NULL) {
33        printf("内存分配失败。\n");
34        return 1;
35    }
36
37    // 获取输入值
38    printf("请输入 %d 个输入值(0或1):\n", num_inputs);
39    for (int i = 0; i < num_inputs; i++) {
40        printf("输入%d: ", i + 1);
41        scanf("%d", &inputs[i]);
42        if (inputs[i] != 0 && inputs[i] != 1) {
43            printf("无效输入,请输入0或1。\n");
44            free(inputs);
45            free(weights);
46            return 1;
47        }
48    }
49
50    // 获取权重值
51    printf("请输入 %d 个权重值:\n", num_inputs);
52    for (int i = 0; i < num_inputs; i++) {
53        printf("权重%d: ", i + 1);
54        scanf("%d", &weights[i]);
55    }
56
57    // 获取阈值
58    int threshold;
59    printf("请输入阈值: ");
60    scanf("%d", &threshold);
61
62    // 调用 m-p 模型
63    int output = m_p_model(inputs, weights, num_inputs, threshold);
64
65    // 输出结果
66    printf("神经元输出: %d\n", output);
67
68    // 释放内存
69    free(inputs);
70    free(weights);
71
72    return 0;
73}

代码说明

  1. 泛化设计

    • 支持任意数量的输入,通过动态分配数组 (malloc) 来处理 inputsweights
    • 用户可以指定输入数量、每个输入值、每个权重值,以及阈值。
  2. 灵活性

    • 可以轻松调整权重和阈值以实现不同的逻辑门。例如,设置 weights = {1, 1}threshold = 2 是 AND 门,threshold = 1 是 OR 门。
  3. m_p_model 函数

    • 独立封装了 m-p 模型的逻辑,输入为输入数组、权重数组、输入数量和阈值,输出为激活结果。
  4. 激活函数

    • 灵活的阈值比较,可根据具体需求调整。

示例运行

示例 1:实现 AND 门

输入:

请输入输入的数量: 2
请输入 2 个输入值(0或1):
输入1: 1
输入2: 1
请输入 2 个权重值:
权重1: 1
权重2: 1
请输入阈值: 2

输出:

神经元输出: 1
示例 2:实现 OR 门

输入:

请输入输入的数量: 2
请输入 2 个输入值(0或1):
输入1: 1
输入2: 0
请输入 2 个权重值:
权重1: 1
权重2: 1
请输入阈值: 1

输出:

神经元输出: 1
示例 3:自定义

输入:

请输入输入的数量: 3
请输入 3 个输入值(0或1):
输入1: 1
输入2: 1
输入3: 0
请输入 3 个权重值:
权重1: 2
权重2: 1
权重3: 1
请输入阈值: 3

输出:

神经元输出: 1

感知机

以下是一个简单的感知机 (Perceptron) 的 C 语言实现。感知机是一种二分类模型,它使用输入的加权和与偏置来计算输出,并通过权重的更新进行学习。

示例代码

 1#include <stdio.h>
 2#include <stdlib.h>
 3
 4// 激活函数(阶跃函数)
 5int activation_function(float weighted_sum) {
 6    return weighted_sum >= 0 ? 1 : 0;  // 阈值为0
 7}
 8
 9// 感知机模型
10void perceptron_train(float **inputs, int *labels, float *weights, float *bias, int num_samples, int num_features, float learning_rate, int epochs) {
11    for (int epoch = 0; epoch < epochs; epoch++) {
12        printf("Epoch %d:\n", epoch + 1);
13        for (int i = 0; i < num_samples; i++) {
14            // 计算加权和
15            float weighted_sum = *bias;
16            for (int j = 0; j < num_features; j++) {
17                weighted_sum += inputs[i][j] * weights[j];
18            }
19
20            // 预测值
21            int prediction = activation_function(weighted_sum);
22
23            // 计算误差
24            int error = labels[i] - prediction;
25
26            // 更新权重和偏置
27            for (int j = 0; j < num_features; j++) {
28                weights[j] += learning_rate * error * inputs[i][j];
29            }
30            *bias += learning_rate * error;
31
32            // 输出权重和偏置
33            printf("  样本 %d: 更新后权重 = [", i + 1);
34            for (int j = 0; j < num_features; j++) {
35                printf("%.2f%s", weights[j], j == num_features - 1 ? "" : ", ");
36            }
37            printf("], 偏置 = %.2f\n", *bias);
38        }
39        printf("\n");
40    }
41}
42
43int main() {
44    int num_samples = 4;      // 样本数量
45    int num_features = 2;     // 每个样本的特征数量
46    float learning_rate = 0.1; // 学习率
47    int epochs = 10;          // 训练轮数
48
49    // 训练数据 (逻辑与AND操作)
50    float training_inputs[4][2] = {
51        {0, 0},
52        {0, 1},
53        {1, 0},
54        {1, 1}
55    };
56    int training_labels[4] = {0, 0, 0, 1}; // AND操作的标签
57
58    // 转换为指针形式
59    float *inputs[4];
60    for (int i = 0; i < 4; i++) {
61        inputs[i] = training_inputs[i];
62    }
63
64    // 初始化权重和偏置
65    float weights[2] = {0.0, 0.0}; // 初始权重为0
66    float bias = 0.0;              // 初始偏置为0
67
68    // 训练感知机
69    perceptron_train(inputs, training_labels, weights, &bias, num_samples, num_features, learning_rate, epochs);
70
71    // 测试感知机
72    printf("测试感知机:\n");
73    for (int i = 0; i < num_samples; i++) {
74        float weighted_sum = bias;
75        for (int j = 0; j < num_features; j++) {
76            weighted_sum += training_inputs[i][j] * weights[j];
77        }
78        int prediction = activation_function(weighted_sum);
79        printf("  输入: [%.1f, %.1f], 预测: %d, 标签: %d\n",
80               training_inputs[i][0], training_inputs[i][1], prediction, training_labels[i]);
81    }
82
83    return 0;
84}

代码说明

  1. 激活函数:

    • 使用阶跃函数作为激活函数,若加权和大于等于 0 则输出 1,否则输出 0
  2. 训练数据:

    • 使用逻辑与 (AND) 的数据集,输入为 [0, 0][0, 1][1, 0][1, 1],标签为 [0, 0, 0, 1]
  3. 训练过程:

    • 逐个样本计算加权和。
    • 通过误差 (error = label - prediction) 更新权重和偏置:
      • 权重更新公式: weights[j] += learning_rate * error * inputs[i][j]
      • 偏置更新公式: bias += learning_rate * error
  4. 测试过程:

    • 使用训练后的权重和偏置,预测每个样本的输出,并与标签对比。

示例运行结果

输出会随着每轮训练更新权重和偏置:

训练过程:
Epoch 1:
  样本 1: 更新后权重 = [0.00, 0.00], 偏置 = 0.00
  样本 2: 更新后权重 = [0.00, 0.00], 偏置 = 0.00
  样本 3: 更新后权重 = [0.10, 0.00], 偏置 = 0.00
  样本 4: 更新后权重 = [0.10, 0.10], 偏置 = 0.10

...

Epoch 10:
  样本 1: 更新后权重 = [0.10, 0.10], 偏置 = -0.10
  样本 2: 更新后权重 = [0.10, 0.10], 偏置 = -0.10
  样本 3: 更新后权重 = [0.10, 0.10], 偏置 = -0.10
  样本 4: 更新后权重 = [0.10, 0.10], 偏置 = -0.10
测试过程:
测试感知机:
  输入: [0.0, 0.0], 预测: 0, 标签: 0
  输入: [0.0, 1.0], 预测: 0, 标签: 0
  输入: [1.0, 0.0], 预测: 0, 标签: 0
  输入: [1.0, 1.0], 预测: 1, 标签: 1

优化方向

  • 将代码扩展为多分类任务。
  • 增加支持浮点特征的输入。
  • 使用动态数组进一步泛化输入数据的规模。

优化后的感知机实现

以下是经过优化后的感知机代码,实现了以下功能:

  1. 支持多分类任务。
  2. 支持浮点特征输入。
  3. 使用动态数组泛化输入数据的规模。

优化后的代码更通用,适用于任意特征维度和类别数量的场景。


优化后的代码

  1#include <stdio.h>
  2#include <stdlib.h>
  3#include <math.h>
  4
  5// 激活函数:返回预测类别(多分类通过比较加权和最大值实现)
  6int activation_function(float *weighted_sum, int num_classes) {
  7    int max_index = 0;
  8    float max_value = weighted_sum[0];
  9    for (int i = 1; i < num_classes; i++) {
 10        if (weighted_sum[i] > max_value) {
 11            max_value = weighted_sum[i];
 12            max_index = i;
 13        }
 14    }
 15    return max_index;
 16}
 17
 18// 感知机训练函数
 19void perceptron_train(float **inputs, int *labels, float **weights, float *biases,
 20                      int num_samples, int num_features, int num_classes, float learning_rate, int epochs) {
 21    for (int epoch = 0; epoch < epochs; epoch++) {
 22        printf("Epoch %d:\n", epoch + 1);
 23        for (int i = 0; i < num_samples; i++) {
 24            // 计算每个类别的加权和
 25            float *weighted_sum = (float *)calloc(num_classes, sizeof(float));
 26            for (int c = 0; c < num_classes; c++) {
 27                weighted_sum[c] = biases[c];
 28                for (int j = 0; j < num_features; j++) {
 29                    weighted_sum[c] += inputs[i][j] * weights[c][j];
 30                }
 31            }
 32
 33            // 预测类别
 34            int prediction = activation_function(weighted_sum, num_classes);
 35
 36            // 计算误差并更新权重和偏置
 37            if (labels[i] != prediction) {
 38                for (int j = 0; j < num_features; j++) {
 39                    // 正确类别权重更新
 40                    weights[labels[i]][j] += learning_rate * inputs[i][j];
 41                    // 错误类别权重更新
 42                    weights[prediction][j] -= learning_rate * inputs[i][j];
 43                }
 44                // 更新偏置
 45                biases[labels[i]] += learning_rate;
 46                biases[prediction] -= learning_rate;
 47            }
 48
 49            // 输出权重和偏置
 50            printf("  样本 %d: 权重更新后:\n", i + 1);
 51            for (int c = 0; c < num_classes; c++) {
 52                printf("    类别 %d: 权重 = [", c);
 53                for (int j = 0; j < num_features; j++) {
 54                    printf("%.2f%s", weights[c][j], j == num_features - 1 ? "" : ", ");
 55                }
 56                printf("], 偏置 = %.2f\n", biases[c]);
 57            }
 58            free(weighted_sum);
 59        }
 60        printf("\n");
 61    }
 62}
 63
 64// 主函数
 65int main() {
 66    int num_samples = 6;       // 样本数量
 67    int num_features = 2;      // 每个样本的特征数量
 68    int num_classes = 3;       // 分类数量
 69    float learning_rate = 0.1; // 学习率
 70    int epochs = 10;           // 训练轮数
 71
 72    // 训练数据(3类数据:0, 1, 2)
 73    float training_inputs[6][2] = {
 74        {1.0, 1.0}, {1.5, 2.0}, {2.0, 1.5},  // 类别 0
 75        {-1.0, -1.0}, {-1.5, -2.0}, {-2.0, -1.5} // 类别 1 和 2
 76    };
 77    int training_labels[6] = {0, 0, 0, 1, 2, 1};
 78
 79    // 转换为指针形式
 80    float **inputs = (float **)malloc(num_samples * sizeof(float *));
 81    for (int i = 0; i < num_samples; i++) {
 82        inputs[i] = training_inputs[i];
 83    }
 84
 85    // 动态分配权重和偏置
 86    float **weights = (float **)malloc(num_classes * sizeof(float *));
 87    for (int i = 0; i < num_classes; i++) {
 88        weights[i] = (float *)calloc(num_features, sizeof(float)); // 初始化为0
 89    }
 90    float *biases = (float *)calloc(num_classes, sizeof(float)); // 偏置初始化为0
 91
 92    // 训练感知机
 93    perceptron_train(inputs, training_labels, weights, biases, num_samples, num_features, num_classes, learning_rate, epochs);
 94
 95    // 测试感知机
 96    printf("测试感知机:\n");
 97    for (int i = 0; i < num_samples; i++) {
 98        float *weighted_sum = (float *)calloc(num_classes, sizeof(float));
 99        for (int c = 0; c < num_classes; c++) {
100            weighted_sum[c] = biases[c];
101            for (int j = 0; j < num_features; j++) {
102                weighted_sum[c] += inputs[i][j] * weights[c][j];
103            }
104        }
105        int prediction = activation_function(weighted_sum, num_classes);
106        printf("  输入: [%.2f, %.2f], 预测类别: %d, 实际类别: %d\n",
107               inputs[i][0], inputs[i][1], prediction, training_labels[i]);
108        free(weighted_sum);
109    }
110
111    // 释放动态内存
112    for (int i = 0; i < num_classes; i++) {
113        free(weights[i]);
114    }
115    free(weights);
116    free(biases);
117    free(inputs);
118
119    return 0;
120}

改进点说明

  1. 多分类支持:

    • 每个类别都有一组权重和一个偏置。
    • 使用所有类别的加权和,预测时选择加权和最大的类别。
  2. 浮点特征支持:

    • 输入特征使用 float 类型,支持更精确的数据。
  3. 动态数组:

    • 样本、权重和偏置使用动态分配内存,使得代码能够处理任意数量的样本和特征。

测试运行结果

示例输入:

训练数据为:

1类别 0: [1.0, 1.0], [1.5, 2.0], [2.0, 1.5]
2类别 1: [-1.0, -1.0], [-1.5, -2.0]
3类别 2: [-2.0, -1.5]
示例输出:
 1Epoch 1:
 2  样本 1: 权重更新后:
 3    类别 0: 权重 = [0.10, 0.10], 偏置 = 0.10
 4    类别 1: 权重 = [0.00, 0.00], 偏置 = 0.00
 5    类别 2: 权重 = [0.00, 0.00], 偏置 = 0.00
 6
 7...
 8测试感知机:
 9  输入: [1.00, 1.00], 预测类别: 0, 实际类别: 0
10  输入: [-1.50, -2.00], 预测类别: 1, 实际类别: 1

优化后优势

  • 可扩展性: 支持任意类别数和特征数,适应多分类任务。
  • 动态内存: 更灵活处理大规模数据。
  • 高可读性: 分步骤组织清晰易懂,便于理解。

多层感知机

以下是一个简单的多层感知机(MLP)实现范例,使用 C 语言构建两层神经网络。此代码实现了前向传播、误差计算(均方误差),以及权重更新(基于梯度下降)的功能。


多层感知机范例代码

  1#include <stdio.h>
  2#include <stdlib.h>
  3#include <math.h>
  4
  5// 激活函数(ReLU 和 Sigmoid)
  6float relu(float x) {
  7    return x > 0 ? x : 0;
  8}
  9
 10float relu_derivative(float x) {
 11    return x > 0 ? 1 : 0;
 12}
 13
 14float sigmoid(float x) {
 15    return 1 / (1 + exp(-x));
 16}
 17
 18float sigmoid_derivative(float x) {
 19    return sigmoid(x) * (1 - sigmoid(x));
 20}
 21
 22// 随机初始化权重
 23void initialize_weights(float **weights, int rows, int cols) {
 24    for (int i = 0; i < rows; i++) {
 25        for (int j = 0; j < cols; j++) {
 26            weights[i][j] = ((float)rand() / RAND_MAX) - 0.5; // 随机值在 [-0.5, 0.5]
 27        }
 28    }
 29}
 30
 31// 前向传播
 32void forward_propagation(float *inputs, float **weights_hidden, float *bias_hidden,
 33                         float **weights_output, float *bias_output,
 34                         float *hidden_layer, float *output_layer,
 35                         int input_size, int hidden_size, int output_size) {
 36    // 计算隐藏层输出
 37    for (int i = 0; i < hidden_size; i++) {
 38        hidden_layer[i] = bias_hidden[i];
 39        for (int j = 0; j < input_size; j++) {
 40            hidden_layer[i] += inputs[j] * weights_hidden[i][j];
 41        }
 42        hidden_layer[i] = relu(hidden_layer[i]);
 43    }
 44
 45    // 计算输出层
 46    for (int i = 0; i < output_size; i++) {
 47        output_layer[i] = bias_output[i];
 48        for (int j = 0; j < hidden_size; j++) {
 49            output_layer[i] += hidden_layer[j] * weights_output[i][j];
 50        }
 51        output_layer[i] = sigmoid(output_layer[i]);
 52    }
 53}
 54
 55// 训练模型
 56void train(float **inputs, float **expected_outputs, float **weights_hidden, float *bias_hidden,
 57           float **weights_output, float *bias_output,
 58           int num_samples, int input_size, int hidden_size, int output_size,
 59           float learning_rate, int epochs) {
 60    // 动态分配中间层数组
 61    float *hidden_layer = (float *)malloc(hidden_size * sizeof(float));
 62    float *output_layer = (float *)malloc(output_size * sizeof(float));
 63    float *hidden_deltas = (float *)malloc(hidden_size * sizeof(float));
 64    float *output_deltas = (float *)malloc(output_size * sizeof(float));
 65
 66    for (int epoch = 0; epoch < epochs; epoch++) {
 67        float total_error = 0;
 68
 69        for (int sample = 0; sample < num_samples; sample++) {
 70            // 前向传播
 71            forward_propagation(inputs[sample], weights_hidden, bias_hidden,
 72                                weights_output, bias_output, hidden_layer, output_layer,
 73                                input_size, hidden_size, output_size);
 74
 75            // 计算均方误差
 76            for (int i = 0; i < output_size; i++) {
 77                float error = expected_outputs[sample][i] - output_layer[i];
 78                total_error += error * error;
 79
 80                // 输出层误差
 81                output_deltas[i] = error * sigmoid_derivative(output_layer[i]);
 82            }
 83
 84            // 计算隐藏层误差
 85            for (int i = 0; i < hidden_size; i++) {
 86                hidden_deltas[i] = 0;
 87                for (int j = 0; j < output_size; j++) {
 88                    hidden_deltas[i] += output_deltas[j] * weights_output[j][i];
 89                }
 90                hidden_deltas[i] *= relu_derivative(hidden_layer[i]);
 91            }
 92
 93            // 更新权重和偏置
 94            for (int i = 0; i < output_size; i++) {
 95                for (int j = 0; j < hidden_size; j++) {
 96                    weights_output[i][j] += learning_rate * output_deltas[i] * hidden_layer[j];
 97                }
 98                bias_output[i] += learning_rate * output_deltas[i];
 99            }
100
101            for (int i = 0; i < hidden_size; i++) {
102                for (int j = 0; j < input_size; j++) {
103                    weights_hidden[i][j] += learning_rate * hidden_deltas[i] * inputs[sample][j];
104                }
105                bias_hidden[i] += learning_rate * hidden_deltas[i];
106            }
107        }
108
109        if ((epoch + 1) % 1000 == 0) {
110            printf("Epoch %d: Total Error = %.6f\n", epoch + 1, total_error);
111        }
112    }
113
114    free(hidden_layer);
115    free(output_layer);
116    free(hidden_deltas);
117    free(output_deltas);
118}
119
120// 测试模型
121void test(float **inputs, float **weights_hidden, float *bias_hidden,
122          float **weights_output, float *bias_output,
123          int num_samples, int input_size, int hidden_size, int output_size) {
124    float *hidden_layer = (float *)malloc(hidden_size * sizeof(float));
125    float *output_layer = (float *)malloc(output_size * sizeof(float));
126
127    printf("\nTesting the model:\n");
128    for (int sample = 0; sample < num_samples; sample++) {
129        forward_propagation(inputs[sample], weights_hidden, bias_hidden,
130                            weights_output, bias_output, hidden_layer, output_layer,
131                            input_size, hidden_size, output_size);
132
133        printf("Input: ");
134        for (int i = 0; i < input_size; i++) {
135            printf("%.1f ", inputs[sample][i]);
136        }
137        printf("Output: ");
138        for (int i = 0; i < output_size; i++) {
139            printf("%.6f ", output_layer[i]);
140        }
141        printf("\n");
142    }
143
144    free(hidden_layer);
145    free(output_layer);
146}
147
148int main() {
149    // 超参数
150    int input_size = 2;   // 输入特征数量
151    int hidden_size = 3;  // 隐藏层神经元数量
152    int output_size = 1;  // 输出层神经元数量(回归任务)
153    int num_samples = 4;  // 样本数量
154    float learning_rate = 0.1;
155    int epochs = 10000;
156
157    // 样本输入(XOR 问题)
158    float input_data[4][2] = {
159        {0, 0},
160        {0, 1},
161        {1, 0},
162        {1, 1}
163    };
164    float output_data[4][1] = {
165        {0},
166        {1},
167        {1},
168        {0}
169    };
170
171    // 转换为动态数组
172    float **inputs = (float **)malloc(num_samples * sizeof(float *));
173    float **expected_outputs = (float **)malloc(num_samples * sizeof(float *));
174    for (int i = 0; i < num_samples; i++) {
175        inputs[i] = input_data[i];
176        expected_outputs[i] = output_data[i];
177    }
178
179    // 动态分配权重和偏置
180    float **weights_hidden = (float **)malloc(hidden_size * sizeof(float *));
181    float **weights_output = (float **)malloc(output_size * sizeof(float *));
182    for (int i = 0; i < hidden_size; i++) {
183        weights_hidden[i] = (float *)malloc(input_size * sizeof(float));
184    }
185    for (int i = 0; i < output_size; i++) {
186        weights_output[i] = (float *)malloc(hidden_size * sizeof(float));
187    }
188    float *bias_hidden = (float *)malloc(hidden_size * sizeof(float));
189    float *bias_output = (float *)malloc(output_size * sizeof(float));
190
191    // 初始化权重和偏置
192    initialize_weights(weights_hidden, hidden_size, input_size);
193    initialize_weights(weights_output, output_size, hidden_size);
194    for (int i = 0; i < hidden_size; i++) bias_hidden[i] = 0;
195    for (int i = 0; i < output_size; i++) bias_output[i] = 0;
196
197    // 训练
198    train(inputs, expected_outputs, weights_hidden, bias_hidden,
199          weights_output, bias_output, num_samples, input_size,
200          hidden_size, output_size, learning_rate, epochs);
201
202    // 测试
203    test(inputs, weights_hidden, bias_hidden, weights_output, bias_output, num_samples, input_size, hidden_size, output_size);
204
205    // 释放内存
206    for (int i = 0; i < hidden_size; i++) free(weights_hidden[i]);
207    for (int i = 0; i < output_size; i++) free(weights_output[i]);
208    free(weights_hidden);
209    free(weights_output);
210    free(bias_hidden);
211    free(bias_output);
212    free(inputs);
213    free(expected_outputs);
214
215    return 0;
216}

特性

  1. 两层网络结构:
    • 输入层 → 隐藏层(ReLU 激活) → 输出层(Sigmoid 激活)。
  2. 动态内存分配:
    • 样本、权重、偏置全部使用动态数组,支持任意大小的输入。
  3. 灵活性:
    • 可通过参数调整网络结构(隐藏层大小、学习率、训练轮数等)。

编译与运行

保存代码为 mlp.c,然后运行以下命令:

1gcc -o mlp mlp.c -lm
2./mlp

-lm:链接数学库 math.h


输出示例

1Epoch 10000: Total Error = 0.000028
2
3Testing the model:
4Input: 0.0 0.0 Output: 0.001234 
5Input: 0.0 1.0 Output: 0.998765 
6Input: 1.0 0.0 Output: 0.998765 
7Input: 1.0 1.0 Output: 0.001234

最终训练完成后,模型可以正确拟合 XOR 问题!

误差反向传播算法

误差反向传播算法(Backpropagation)是神经网络训练中的一种核心算法,用于优化网络的权重和偏置,从而使网络能够更好地拟合训练数据。以下是它的基本概念和工作原理:

核心思想

误差反向传播算法的主要思想是通过梯度下降法最小化损失函数。它利用链式法则将输出误差逐层地向后传播,计算每个神经元的梯度,以此更新网络的参数(权重和偏置)。

工作流程

误差反向传播分为两个主要步骤:

1. 前向传播(Forward Propagation)
  • 输入数据从输入层经过各层神经元的计算,逐层传递到输出层。
  • 输出层的结果通过损失函数与真实值计算误差。
2. 反向传播(Backward Propagation)

误差通过网络从输出层向输入层逐层传播,用于计算每个参数对误差的贡献(梯度)。

  • 输出层误差计算:从损失函数计算输出层的误差。
  • 梯度计算:根据链式法则,计算损失函数对每个参数的偏导数。
  • 权重更新:使用梯度下降法调整参数,更新公式为:

$$ w_{ij}^{(t+1)} = w_{ij}^{(t)} - \eta \cdot \frac{\partial \mathcal{L}}{\partial w_{ij}} $$

其中:$w_{ij}$:权重$\eta$:学习率$\mathcal{L}$:损失函数

算法详细步骤

  1. 初始化权重和偏置:随机初始化网络的权重和偏置。
  2. 输入数据:将输入样本通过网络计算输出。
  3. 计算损失:使用损失函数(如均方误差或交叉熵)计算网络输出与目标输出之间的误差。
  4. 反向传播误差
  • 计算输出层的梯度。
  • 按照链式法则将误差传播到隐藏层,计算每层的梯度。
  1. 更新参数:用梯度下降法更新网络的权重和偏置。
  2. 重复步骤 2-5,直到损失收敛或达到最大迭代次数。

数学细节

以一个两层神经网络为例,假设:

  • 激活函数为 $\sigma(x)$(如 Sigmoid 或 ReLU)。
  • 损失函数为 $\mathcal{L}$。
输出层梯度:

$$ \delta^{(L)} = \frac{\partial \mathcal{L}}{\partial z^{(L)}} = \frac{\partial \mathcal{L}}{\partial a^{(L)}} \cdot \sigma’(z^{(L)}) $$

隐藏层梯度:

$$ \delta^{(l)} = \left( W^{(l+1)} \delta^{(l+1)} \right) \cdot \sigma’(z^{(l)}) $$

权重更新公式:

$$ W^{(l)} = W^{(l)} - \eta \cdot \delta^{(l)} \cdot (a^{(l-1)})^T $$

优势

  1. 高效:通过链式法则,误差反向传播可以快速计算梯度。
  2. 通用性:适用于各种神经网络架构(全连接网络、卷积网络、循环网络等)。
  3. 可扩展性:可以结合多种优化算法(如动量、Adam)提高训练效率。

局限性

  1. 梯度消失问题:当使用 Sigmoid 等激活函数时,梯度可能会因多次链式求导而衰减,导致深层网络训练困难。
  2. 局部最优:梯度下降可能会陷入局部最优解。
  3. 计算开销:对于深层网络,计算梯度可能非常耗时。 误差反向传播算法是深度学习的基石,许多现代优化方法(如 Adam、RMSProp)都是基于它的改进。

误差函数和激活函数

激活函数和误差函数是神经网络中的两个核心概念,分别在前向传播和反向传播中扮演重要角色。以下是它们的定义和作用:

1. 激活函数(Activation Function)

作用:
  • 激活函数主要用于引入非线性,从而使神经网络能够拟合复杂的非线性关系。如果没有激活函数,网络的每一层只会执行线性变换,最终等价于单层线性模型,无法解决复杂问题。
  • 激活函数还可以对输出值进行缩放或映射到某个范围(如 [0, 1] 或 [-1, 1]),这有助于训练过程的稳定性。
常见激活函数:
  1. ReLU(Rectified Linear Unit):
  • 表达式:$f(x) = \max(0, x)$
  • 特点:计算简单,适合深度网络,能缓解梯度消失问题。
  • 缺点:可能导致“神经元死亡”(输出始终为0)。
  1. Sigmoid:
  • 表达式:$f(x) = \frac{1}{1 + e^{-x}}$
  • 特点:输出范围为 [0, 1],适合概率相关任务。
  • 缺点:容易导致梯度消失,计算开销较大。
  1. Tanh(双曲正切):
  • 表达式:$f(x) = \tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}$
  • 特点:输出范围为 [-1, 1],相比 Sigmoid,梯度消失问题较小。
  • 缺点:仍可能出现梯度消失。
  1. Leaky ReLU:
  • 表达式:$f(x) = x$(当 $x > 0$),$f(x) = \alpha x$(当 $x \leq 0$)
  • 特点:在 ReLU 的基础上,引入一个小的斜率 $\alpha$,解决神经元死亡问题。
  1. Softmax:
  • 用于多分类问题,将输出转换为概率分布。
  • 表达式:$f_i(x) = \frac{e^{x_i}}{\sum_{j} e^{x_j}}$
激活函数的选择:
  • ReLU:深度网络的默认选择。
  • Sigmoid/Tanh:浅层网络或特定任务中用(如二分类)。
  • Softmax:多分类任务的输出层。
  • Leaky ReLU:当遇到“神经元死亡”问题时使用。

2. 误差函数(Loss Function / Error Function)

作用:
  • 误差函数用于衡量模型预测值与真实值之间的差异(误差)。
  • 它是反向传播的起点,定义了网络的优化目标,帮助模型调整参数(通过梯度下降或其他优化算法)。
常见误差函数:
  1. 均方误差(Mean Squared Error, MSE):
  • 表达式:$L = \frac{1}{n} \sum_{i=1}^n (y_i - \hat{y}_i)^2$
  • 用途:回归问题。
  • 特点:对异常值敏感。
  1. 交叉熵损失(Cross-Entropy Loss):
  • 二分类:$L = -\frac{1}{n} \sum_{i=1}^n [y_i \log(\hat{y}_i) + (1 - y_i) \log(1 - \hat{y}_i)]$
  • 多分类:$L = -\frac{1}{n} \sum_{i=1}^n \sum_{k=1}^K y_{i,k} \log(\hat{y}_{i,k})$
  • 用途:分类问题。
  • 特点:对概率分布预测很有效。
  1. 绝对误差(Mean Absolute Error, MAE):
  • 表达式:$L = \frac{1}{n} \sum_{i=1}^n |y_i - \hat{y}_i|$
  • 用途:回归问题。
  • 特点:对异常值不敏感,但优化时不平滑。
  1. Huber 损失:
  • 平衡 MSE 和 MAE,在异常值存在时表现更好。
  • 表达式:

$$ L = \begin{cases} \frac{1}{2}(y_i - \hat{y}_i)^2 & \text{if } |y_i - \hat{y}_i| \leq \delta \ \delta |y_i - \hat{y}_i| - \frac{1}{2} \delta^2 & \text{otherwise} \end{cases} $$ 5. Hinge 损失:

  • 用于支持向量机(SVM)。
  • 表达式:$L = \sum_{i} \max(0, 1 - y_i \hat{y}_i)$
误差函数的选择:
  • 回归任务:MSE 或 MAE。
  • 二分类任务:交叉熵损失。
  • 多分类任务:交叉熵损失(Softmax 输出)。
  • 异常值存在时:Huber 损失。

3. 两者的关系:

  • 激活函数:决定了神经元的输出形式及特性,直接影响网络的表达能力。

  • 误差函数:用来衡量预测结果的好坏,引导网络的参数调整。 两者结合形成了一个闭环:

  • 激活函数处理输入和隐藏层的数据流,使模型具备强大的拟合能力。

  • 误差函数通过反向传播,将损失传播到激活函数及模型参数,从而优化整个网络。

4. C语言实现

以下是常见激活函数和误差函数的 C 语言实现,每个函数都假设接收单个输入或数组,具体可以根据实际需求调整。


1. 激活函数的 C 实现
 1#include <math.h>
 2#include <stdio.h>
 3
 4// ReLU 激活函数
 5double relu(double x) {
 6    return x > 0 ? x : 0;
 7}
 8
 9// Leaky ReLU 激活函数
10double leaky_relu(double x, double alpha) {
11    return x > 0 ? x : alpha * x;
12}
13
14// Sigmoid 激活函数
15double sigmoid(double x) {
16    return 1.0 / (1.0 + exp(-x));
17}
18
19// Tanh 激活函数
20double tanh_activation(double x) {
21    return tanh(x); // C 标准库自带 tanh 函数
22}
23
24// Softmax 激活函数(作用于数组)
25void softmax(double* input, double* output, int size) {
26    double sum_exp = 0.0;
27    for (int i = 0; i < size; i++) {
28        output[i] = exp(input[i]);
29        sum_exp += output[i];
30    }
31    for (int i = 0; i < size; i++) {
32        output[i] /= sum_exp;
33    }
34}

2. 误差函数的 C 实现

均方误差 (MSE)

1double mean_squared_error(double* y_true, double* y_pred, int size) {
2    double error = 0.0;
3    for (int i = 0; i < size; i++) {
4        double diff = y_true[i] - y_pred[i];
5        error += diff * diff;
6    }
7    return error / size;
8}

交叉熵损失 (Cross-Entropy Loss)

1double cross_entropy_loss(double* y_true, double* y_pred, int size) {
2    double loss = 0.0;
3    for (int i = 0; i < size; i++) {
4        loss -= y_true[i] * log(y_pred[i] + 1e-15); // 加上 1e-15 避免 log(0)
5    }
6    return loss / size;
7}

绝对误差 (MAE)

1double mean_absolute_error(double* y_true, double* y_pred, int size) {
2    double error = 0.0;
3    for (int i = 0; i < size; i++) {
4        error += fabs(y_true[i] - y_pred[i]); // fabs 是 C 标准库的绝对值函数
5    }
6    return error / size;
7}

Huber 损失

 1double huber_loss(double* y_true, double* y_pred, int size, double delta) {
 2    double loss = 0.0;
 3    for (int i = 0; i < size; i++) {
 4        double diff = y_true[i] - y_pred[i];
 5        if (fabs(diff) <= delta) {
 6            loss += 0.5 * diff * diff;
 7        } else {
 8            loss += delta * (fabs(diff) - 0.5 * delta);
 9        }
10    }
11    return loss / size;
12}

Hinge 损失

1double hinge_loss(double* y_true, double* y_pred, int size) {
2    double loss = 0.0;
3    for (int i = 0; i < size; i++) {
4        double margin = 1 - y_true[i] * y_pred[i];
5        loss += margin > 0 ? margin : 0;
6    }
7    return loss / size;
8}

3. 示例代码:激活函数和误差函数结合

一个简单示例,结合激活函数和 MSE 计算:

 1int main() {
 2    double input[] = {0.5, -1.2, 3.0}; // 输入数据
 3    double y_true[] = {1.0, 0.0, 1.0}; // 真实值
 4    double y_pred[3]; // 预测值
 5    int size = 3;
 6
 7    // 使用 Sigmoid 激活函数
 8    for (int i = 0; i < size; i++) {
 9        y_pred[i] = sigmoid(input[i]);
10    }
11
12    // 计算均方误差
13    double loss = mean_squared_error(y_true, y_pred, size);
14    printf("Mean Squared Error: %f\n", loss);
15
16    // 计算 Softmax(多分类任务)
17    double softmax_output[3];
18    softmax(input, softmax_output, size);
19    printf("Softmax Output: ");
20    for (int i = 0; i < size; i++) {
21        printf("%f ", softmax_output[i]);
22    }
23    printf("\n");
24
25    return 0;
26}

编译与运行

  1. 将代码保存为 activation_loss.c
  2. 使用 gcc 编译:gcc activation_loss.c -o activation_loss -lm(需要链接数学库 -lm)。
  3. 运行程序:./activation_loss

似然函数

似然函数 (Likelihood Function) 是概率统计中的一个核心概念,特别是在参数估计和机器学习中常见。理解它可以从以下几个方面来剖析:

1. 似然函数的定义

似然函数衡量的是在某些已知观测数据(样本)下,某个参数值导致这些数据出现的可能性。它是原本的概率密度函数或概率质量函数的一种“反向使用”。

用数学公式表示:

  • 假设有一个数据集 $X = {x_1, x_2, \dots, x_n}$,每个 $x_i$ 是一个观测值。
  • 数据的分布由参数 $\theta$ 描述,其概率分布 $P(X|\theta)$ 表示在参数为 $\theta$ 时,数据 $X$ 出现的概率。 似然函数定义为:

$$ L(\theta | X) = P(X | \theta) $$

  • $\theta$ 是函数的自变量(模型参数)。
  • $X$ 是已知的观测数据,固定不变。
  • $L(\theta | X)$ 表示 $\theta$ 给定时,生成数据 $X$ 的可能性。

2. 与概率的区别

似然函数和概率函数表面上很相似,但角色完全不同

  1. 概率
  • 已知参数 $\theta$,计算数据 $X$ 出现的概率。
  • 问题:$P(X|\theta)$,固定参数,数据变化
  • 例子:如果投硬币的概率 $\theta=0.7$,连续投两次正面向上的概率是多少?
  1. 似然
  • 已知数据 $X$,反过来判断哪种参数 $\theta$ 更可能导致这些数据的出现。
  • 问题:$L(\theta | X)$,固定数据,参数变化
  • 例子:如果观察到两次投硬币结果都是正面,硬币正面概率 $\theta$ 更可能是 0.6 还是 0.8?

3. 似然函数的作用

似然函数的核心用途是用来估计未知参数 $\theta$,即最大似然估计 (MLE)

  • 目标:找到一个参数 $\theta$,使得似然函数 $L(\theta|X)$ 达到最大值。
  • 直观理解:通过调整参数 $\theta$,让观察到的数据 $X$ 在该参数下的出现可能性最大。 用数学表达:

$$ \hat{\theta}{MLE} = \arg\max{\theta} L(\theta | X) $$

4. 似然函数的实际例子

(1) 伯努利分布

假设我们投掷硬币 $n=10$ 次,结果观测到正面 $x=7$ 次,反面 $n-x=3$ 次。假设正面出现的概率是 $\theta$,那么观测到这些结果的概率是:

$$ P(X | \theta) = \binom{n}{x} \theta^x (1 - \theta)^{n - x} $$

这里,似然函数就是:

$$ L(\theta | X) = \binom{n}{x} \theta^x (1 - \theta)^{n - x} $$

  • 数据 $X$ 是固定的 ($n=10, x=7$)。
  • 参数 $\theta$ 是未知的,我们想找到使 $L(\theta | X)$ 最大的值。
(2) 高斯分布

假设我们有 $n$ 个观测数据 $X = {x_1, x_2, \dots, x_n}$,这些数据服从高斯分布:

$$ P(x_i|\mu, \sigma^2) = \frac{1}{\sqrt{2\pi\sigma^2}} \exp\left(-\frac{(x_i - \mu)^2}{2\sigma^2}\right) $$

那么,似然函数为:

$$ L(\mu, \sigma^2 | X) = \prod_{i=1}^n \frac{1}{\sqrt{2\pi\sigma^2}} \exp\left(-\frac{(x_i - \mu)^2}{2\sigma^2}\right) $$

通过最大化 $L(\mu, \sigma^2 | X)$,可以估计高斯分布的参数 $\mu$ 和 $\sigma^2$。

5. 对数似然的使用

为了方便处理,通常会对似然函数取对数(称为对数似然函数):

$$ \ell(\theta | X) = \log L(\theta | X) $$

  • 对数能将乘积变为加法,简化了数学操作。
  • 最大化对数似然和最大化似然函数是等价的。

6. 总结

  • 似然函数的本质:在已知观测数据的情况下,衡量模型参数的合理性。
  • 直观意义:似然函数告诉我们,哪些参数更可能导致观察到的数据。
  • 关键作用:在统计学习和机器学习中,最大化似然是估计模型参数的常用方法。

softmax是似然函数吗

严格来说,Softmax 本身不是似然函数,但它与似然函数密切相关,尤其在多分类问题中常用于计算分类任务的对数似然 (Log-Likelihood)。下面具体分析它们的关系:

1. Softmax 是什么?

Softmax 是一种激活函数,将模型的原始输出(logits)转换成一个概率分布:

$$ P(y_i) = \frac{\exp(z_i)}{\sum_{j} \exp(z_j)} $$

其中:

  • $z_i$ 是模型对类别 $i$ 的得分(logit)。
  • $P(y_i)$ 是类别 $i$ 的预测概率。 Softmax 将任意实数的输出转换为 0 到 1 的概率值,且所有类别的概率和为 1,满足概率分布的要求。

2. 似然函数是什么?

似然函数是参数化模型的目标函数,表示在特定参数下,模型生成观测数据的可能性。假设:

  • $X = {x_1, x_2, \dots, x_n}$ 是观测数据。
  • $\theta$ 是模型的参数(如权重和偏置)。
  • $P(X | \theta)$ 表示数据 $X$ 在参数 $\theta$ 下的似然。 对于分类任务,假设数据集有 $N$ 个样本,每个样本属于 $C$ 个类别之一,目标是最大化每个样本正确分类的概率。似然函数为:

$$ L(\theta) = \prod_{i=1}^N P(y_i | x_i, \theta) $$

其中,$P(y_i | x_i, \theta)$ 是模型对样本 $x_i$ 属于真实类别 $y_i$ 的预测概率。

3. Softmax 和似然函数的关系

在多分类任务中,Softmax 是用来计算 $P(y_i | x_i, \theta)$ 的工具,特别是用于计算似然函数的概率值。

连接点:交叉熵损失和对数似然

多分类任务通常使用交叉熵损失,它直接基于对数似然函数构建:

$$ \ell(\theta) = -\sum_{i=1}^N \log P(y_i | x_i, \theta) $$

在这里:

  • $P(y_i | x_i, \theta)$ 是 Softmax 输出的概率值。 因此,Softmax 不是似然函数本身,而是似然函数中的一部分,用来计算类别的概率。

4. 为什么 Softmax 常与似然函数结合?

  • 归一化输出:Softmax 将模型的输出(logits)转化为概率,确保输出符合概率分布的要求。
  • 最大似然估计:在分类任务中,Softmax 是参数化模型(如神经网络)的核心组件,用来构造似然函数,之后通过最大化似然估计参数。
  • 优化目标:交叉熵损失是对数似然的负数,通过最小化交叉熵等价于最大化似然。

5. 例子

假设有 3 个类别,输入模型的 logits 是 $z = [2.0, 1.0, 0.1]$。用 Softmax 计算每个类别的概率:

$$ P(y_i) = \frac{\exp(z_i)}{\sum_{j} \exp(z_j)} $$

计算结果:

$$ P(y_1) = 0.659, \quad P(y_2) = 0.242, \quad P(y_3) = 0.099 $$

真实类别为 $y_{\text{true}} = 1$,那么对数似然为:

$$ \log P(y_{\text{true}}) = \log(0.659) \approx -0.417 $$

这是交叉熵损失的一个样本值。

6. 总结

  • Softmax 是一个激活函数,用来将模型输出转化为概率分布。
  • 似然函数用于衡量参数下数据出现的可能性。
  • 在分类任务中,Softmax 是计算似然函数的一部分,特别是在对数似然或交叉熵损失中被广泛应用。
  • Softmax 本身不是似然函数,但它为似然函数提供了类别概率的计算方法。

随机梯度下降法

随机梯度下降法(Stochastic Gradient Descent, SGD)是一种优化算法,主要用于训练机器学习和深度学习模型,特别是在神经网络中被广泛应用。它是一种通过迭代优化目标函数的方法,以下是它的基本原理和特点:

1. 随机梯度下降的基本原理

目标

优化问题通常以最小化(或最大化)某个目标函数为目标。例如,最小化模型的损失函数 $L(\theta)$,其中 $\theta$ 是需要优化的参数。

梯度下降法

梯度下降法通过沿目标函数梯度下降的方向更新参数,以逐步找到目标函数的局部或全局最小值。更新公式如下:

$$ \theta = \theta - \eta \nabla L(\theta) $$

  • $\eta$: 学习率(控制步长大小)
  • $\nabla L(\theta)$: 目标函数对参数 $\theta$ 的梯度
随机梯度下降法

传统的梯度下降需要计算整个数据集上的梯度,计算量较大。随机梯度下降(SGD)则随机从数据集中选择一个样本 $x_i$ 或一个小批量(mini-batch),用它们的梯度来近似整体梯度:

$$ \theta = \theta - \eta \nabla L_i(\theta) $$

  • $L_i(\theta)$: 单个样本的损失
  • 随机选择样本的方式大幅降低了计算复杂度,适合处理大规模数据集。

2. 随机梯度下降的优缺点

优点
  1. 效率高:相比传统梯度下降法(Batch Gradient Descent),每次更新只需计算一个或少量样本的梯度,适合大数据集。
  2. 可以跳出局部最优:由于随机性,SGD 在非凸函数中更容易跳出局部最优解,找到更好的解。
  3. 在线学习能力:SGD 可以边训练边处理新数据,不需要重新读取所有数据。
缺点
  1. 不稳定性:由于随机采样,梯度的波动较大,优化路径不稳定,可能导致收敛缓慢或震荡。
  2. 收敛精度低:相比全局梯度,随机梯度是近似值,可能无法找到最优解。
  3. 学习率敏感:学习率需要仔细调整,过大可能跳过最优点,过小会导致收敛太慢。

3. 改进方法

为了克服 SGD 的缺点,研究者提出了多种改进算法:

  1. Mini-batch SGD:每次用一个小批量的数据(如 32 或 64 个样本)来计算梯度,兼顾计算效率和梯度稳定性。
  2. Momentum(动量):在更新方向上加入历史梯度的动量项,减少震荡,加快收敛。

$$ v_t = \beta v_{t-1} + \nabla L_i(\theta) $$

$$ \theta = \theta - \eta v_t $$ 3. AdaGrad、RMSProp、Adam:动态调整学习率,提高收敛效率。

  • Adam 是最常用的优化方法,结合了动量和自适应学习率的优点。

4. 代码示例

以下是一个简单的 Python 实现,用 SGD 优化二次函数 $f(x) = x^2$:

 1import numpy as np
 2
 3# 超参数
 4learning_rate = 0.1
 5num_epochs = 100
 6
 7# 初始化参数
 8theta = np.random.randn()  # 初始点
 9print(f"初始参数: {theta:.4f}")
10
11# SGD 迭代
12for epoch in range(num_epochs):
13    gradient = 2 * theta  # f(x) = x^2 的梯度为 2x
14    theta -= learning_rate * gradient  # 参数更新
15    print(f"Epoch {epoch+1}: 参数值: {theta:.4f}, 梯度: {gradient:.4f}")

运行后,你会看到参数逐步向 0(最小值)靠拢。

5. 应用场景

  • 神经网络训练:SGD 是深度学习训练的核心方法,尤其在使用大规模数据集(如 ImageNet)时。
  • 推荐系统:通过 SGD 优化矩阵分解的目标函数。
  • 强化学习:用 SGD 优化策略或价值函数。 SGD 是一个简单而强大的工具,它在现代机器学习中的地位不可替代!

学习率

学习率是优化算法中的一个超参数,控制每次参数更新的步长大小。它在梯度下降法(包括随机梯度下降和各种变体)中至关重要,直接影响模型的训练效率和收敛结果。

1. 学习率的作用

学习率在参数更新中的公式如下:

$$ \theta = \theta - \eta \nabla L(\theta) $$

  • $\eta$: 学习率(Learning Rate)
  • $\nabla L(\theta)$: 当前参数 $\theta$ 的梯度 学习率决定了参数在梯度方向上移动的距离:
  1. 学习率过大:可能导致跳过最优点,甚至无法收敛(发散)。
  2. 学习率过小:虽然可以接近最优点,但收敛速度会非常慢,耗费大量时间。

2. 学习率的选择

学习率过大
  • 参数变化剧烈,目标函数可能在最优点附近震荡甚至发散。
  • 训练损失无法稳定下降,可能出现“跳跃”现象。
学习率过小
  • 参数更新步长很小,虽然模型最终可能收敛,但训练时间会显著增加。
  • 可能陷入局部最优或停留在平坦区域中,效率低下。
合适的学习率
  • 训练损失逐步下降,并在一定时间内收敛到理想的目标函数值。
  • 更新步长既不太大也不太小,能够有效探索目标函数空间。

3. 动态学习率调整

在实际训练中,固定学习率往往难以适应整个训练过程,因此常采用动态调整策略:

常见方法
  1. 学习率衰减(Learning Rate Decay)
  • 随着训练的进行逐步减小学习率,使模型在接近最优解时步伐更小。
  • 示例公式:

$$ \eta_t = \frac{\eta_0}{1 + kt} $$

其中 $\eta_0$ 是初始学习率,$t$ 是训练的步数,$k$ 是衰减因子。 2. 阶梯式衰减(Step Decay)

  • 在训练过程中,每隔固定的时间步长将学习率减少一定比例。
  • 例如:每 10 个 epoch 将学习率乘以 0.1。
  1. 指数衰减(Exponential Decay)
  • 学习率按指数形式衰减:

$$ \eta_t = \eta_0 \cdot e^{-\lambda t} $$ 4. 余弦退火(Cosine Annealing)

  • 通过余弦函数周期性调整学习率,避免过早收敛到局部最优。
  1. 学习率调度器(Learning Rate Schedulers)
  • 在深度学习框架中(如 TensorFlow 和 PyTorch),可通过调度器动态调整学习率。
自适应学习率

一些优化算法自动调整学习率,无需手动设计:

  1. Adagrad:对稀疏参数应用较大的学习率,对频繁更新的参数应用较小的学习率。
  2. RMSProp:通过指数加权移动平均,平滑梯度平方的变化。
  3. Adam:结合动量和自适应学习率,广泛应用于深度学习任务。

4. 学习率的调试技巧

网格搜索

尝试不同的学习率值(如 $10^{-4}$, $10^{-3}$, $10^{-2}$),找到损失函数下降最快且模型性能最优的学习率。

学习率热身(Warm-up Learning Rate)

从较小的学习率开始,逐步增大到目标学习率,以避免初始阶段参数更新过大。

观察损失曲线
  • 如果损失震荡或发散:减小学习率。
  • 如果损失下降非常慢:增大学习率。

5. 示例代码

以下是 PyTorch 中动态调整学习率的示例:

 1import torch
 2import torch.nn as nn
 3import torch.optim as optim
 4from torch.optim.lr_scheduler import StepLR
 5
 6# 模型、损失函数和优化器
 7model = nn.Linear(10, 1)
 8criterion = nn.MSELoss()
 9optimizer = optim.SGD(model.parameters(), lr=0.1)
10
11# 学习率调度器(每 10 个 epoch 学习率减半)
12scheduler = StepLR(optimizer, step_size=10, gamma=0.5)
13
14# 模拟训练
15for epoch in range(30):
16    optimizer.zero_grad()
17    # 假设输入 x 和目标 y
18    x = torch.randn(5, 10)
19    y = torch.randn(5, 1)
20    
21    # 前向传播和反向传播
22    outputs = model(x)
23    loss = criterion(outputs, y)
24    loss.backward()
25    optimizer.step()
26    
27    # 调整学习率
28    scheduler.step()
29    
30    print(f"Epoch {epoch+1}: Loss={loss.item():.4f}, LR={scheduler.get_last_lr()[0]:.4f}")

6. 总结

  • 学习率是优化算法的关键超参数,直接影响模型的训练效率和性能。
  • 合适的学习率是一个权衡:过大可能导致发散,过小会降低收敛速度。
  • 动态调整学习率和使用自适应学习率算法可以有效提高模型性能。

最后修改于 2025-01-26 14:54