神经网络
基础的模型和算法
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}
代码说明
激活函数:
activation_function(int sum)根据加权和是否超过阈值,决定输出是1(激活)还是0(未激活)。- 这里的逻辑是
sum >= 2,类似于 AND 门的逻辑。
输入和权重:
- 用户输入两个值(
0或1)。 - 权重设置为
[1, 1],表示每个输入的权重相同。
- 用户输入两个值(
加权和:
- 计算公式为
sum += inputs[i] * weights[i]。
- 计算公式为
输出:
- 使用激活函数决定最终输出。
示例运行
输入两个值 1 和 1:
请输入两个输入(0或1):
输入1: 1
输入2: 1
神经元输出: 1
输入两个值 1 和 0:
请输入两个输入(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}
代码说明
泛化设计:
- 支持任意数量的输入,通过动态分配数组 (
malloc) 来处理inputs和weights。 - 用户可以指定输入数量、每个输入值、每个权重值,以及阈值。
- 支持任意数量的输入,通过动态分配数组 (
灵活性:
- 可以轻松调整权重和阈值以实现不同的逻辑门。例如,设置
weights = {1, 1}和threshold = 2是 AND 门,threshold = 1是 OR 门。
- 可以轻松调整权重和阈值以实现不同的逻辑门。例如,设置
m_p_model 函数:
- 独立封装了 m-p 模型的逻辑,输入为输入数组、权重数组、输入数量和阈值,输出为激活结果。
激活函数:
- 灵活的阈值比较,可根据具体需求调整。
示例运行
示例 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}
代码说明
激活函数:
- 使用阶跃函数作为激活函数,若加权和大于等于 0 则输出
1,否则输出0。
- 使用阶跃函数作为激活函数,若加权和大于等于 0 则输出
训练数据:
- 使用逻辑与 (
AND) 的数据集,输入为[0, 0]、[0, 1]、[1, 0]、[1, 1],标签为[0, 0, 0, 1]。
- 使用逻辑与 (
训练过程:
- 逐个样本计算加权和。
- 通过误差 (
error = label - prediction) 更新权重和偏置:- 权重更新公式:
weights[j] += learning_rate * error * inputs[i][j] - 偏置更新公式:
bias += learning_rate * error
- 权重更新公式:
测试过程:
- 使用训练后的权重和偏置,预测每个样本的输出,并与标签对比。
示例运行结果
输出会随着每轮训练更新权重和偏置:
训练过程:
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#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}
改进点说明
多分类支持:
- 每个类别都有一组权重和一个偏置。
- 使用所有类别的加权和,预测时选择加权和最大的类别。
浮点特征支持:
- 输入特征使用
float类型,支持更精确的数据。
- 输入特征使用
动态数组:
- 样本、权重和偏置使用动态分配内存,使得代码能够处理任意数量的样本和特征。
测试运行结果
示例输入:
训练数据为:
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}
特性
- 两层网络结构:
- 输入层 → 隐藏层(ReLU 激活) → 输出层(Sigmoid 激活)。
- 动态内存分配:
- 样本、权重、偏置全部使用动态数组,支持任意大小的输入。
- 灵活性:
- 可通过参数调整网络结构(隐藏层大小、学习率、训练轮数等)。
编译与运行
保存代码为 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}$:损失函数
算法详细步骤
- 初始化权重和偏置:随机初始化网络的权重和偏置。
- 输入数据:将输入样本通过网络计算输出。
- 计算损失:使用损失函数(如均方误差或交叉熵)计算网络输出与目标输出之间的误差。
- 反向传播误差:
- 计算输出层的梯度。
- 按照链式法则将误差传播到隐藏层,计算每层的梯度。
- 更新参数:用梯度下降法更新网络的权重和偏置。
- 重复步骤 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 $$
优势
- 高效:通过链式法则,误差反向传播可以快速计算梯度。
- 通用性:适用于各种神经网络架构(全连接网络、卷积网络、循环网络等)。
- 可扩展性:可以结合多种优化算法(如动量、Adam)提高训练效率。
局限性
- 梯度消失问题:当使用 Sigmoid 等激活函数时,梯度可能会因多次链式求导而衰减,导致深层网络训练困难。
- 局部最优:梯度下降可能会陷入局部最优解。
- 计算开销:对于深层网络,计算梯度可能非常耗时。 误差反向传播算法是深度学习的基石,许多现代优化方法(如 Adam、RMSProp)都是基于它的改进。
误差函数和激活函数
激活函数和误差函数是神经网络中的两个核心概念,分别在前向传播和反向传播中扮演重要角色。以下是它们的定义和作用:
1. 激活函数(Activation Function)
作用:
- 激活函数主要用于引入非线性,从而使神经网络能够拟合复杂的非线性关系。如果没有激活函数,网络的每一层只会执行线性变换,最终等价于单层线性模型,无法解决复杂问题。
- 激活函数还可以对输出值进行缩放或映射到某个范围(如 [0, 1] 或 [-1, 1]),这有助于训练过程的稳定性。
常见激活函数:
- ReLU(Rectified Linear Unit):
- 表达式:$f(x) = \max(0, x)$
- 特点:计算简单,适合深度网络,能缓解梯度消失问题。
- 缺点:可能导致“神经元死亡”(输出始终为0)。
- Sigmoid:
- 表达式:$f(x) = \frac{1}{1 + e^{-x}}$
- 特点:输出范围为 [0, 1],适合概率相关任务。
- 缺点:容易导致梯度消失,计算开销较大。
- Tanh(双曲正切):
- 表达式:$f(x) = \tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}$
- 特点:输出范围为 [-1, 1],相比 Sigmoid,梯度消失问题较小。
- 缺点:仍可能出现梯度消失。
- Leaky ReLU:
- 表达式:$f(x) = x$(当 $x > 0$),$f(x) = \alpha x$(当 $x \leq 0$)
- 特点:在 ReLU 的基础上,引入一个小的斜率 $\alpha$,解决神经元死亡问题。
- Softmax:
- 用于多分类问题,将输出转换为概率分布。
- 表达式:$f_i(x) = \frac{e^{x_i}}{\sum_{j} e^{x_j}}$
激活函数的选择:
- ReLU:深度网络的默认选择。
- Sigmoid/Tanh:浅层网络或特定任务中用(如二分类)。
- Softmax:多分类任务的输出层。
- Leaky ReLU:当遇到“神经元死亡”问题时使用。
2. 误差函数(Loss Function / Error Function)
作用:
- 误差函数用于衡量模型预测值与真实值之间的差异(误差)。
- 它是反向传播的起点,定义了网络的优化目标,帮助模型调整参数(通过梯度下降或其他优化算法)。
常见误差函数:
- 均方误差(Mean Squared Error, MSE):
- 表达式:$L = \frac{1}{n} \sum_{i=1}^n (y_i - \hat{y}_i)^2$
- 用途:回归问题。
- 特点:对异常值敏感。
- 交叉熵损失(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})$
- 用途:分类问题。
- 特点:对概率分布预测很有效。
- 绝对误差(Mean Absolute Error, MAE):
- 表达式:$L = \frac{1}{n} \sum_{i=1}^n |y_i - \hat{y}_i|$
- 用途:回归问题。
- 特点:对异常值不敏感,但优化时不平滑。
- 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}
编译与运行
- 将代码保存为
activation_loss.c。 - 使用
gcc编译:gcc activation_loss.c -o activation_loss -lm(需要链接数学库-lm)。 - 运行程序:
./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. 与概率的区别
似然函数和概率函数表面上很相似,但角色完全不同:
- 概率:
- 已知参数 $\theta$,计算数据 $X$ 出现的概率。
- 问题:$P(X|\theta)$,固定参数,数据变化。
- 例子:如果投硬币的概率 $\theta=0.7$,连续投两次正面向上的概率是多少?
- 似然:
- 已知数据 $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. 随机梯度下降的优缺点
优点
- 效率高:相比传统梯度下降法(Batch Gradient Descent),每次更新只需计算一个或少量样本的梯度,适合大数据集。
- 可以跳出局部最优:由于随机性,SGD 在非凸函数中更容易跳出局部最优解,找到更好的解。
- 在线学习能力:SGD 可以边训练边处理新数据,不需要重新读取所有数据。
缺点
- 不稳定性:由于随机采样,梯度的波动较大,优化路径不稳定,可能导致收敛缓慢或震荡。
- 收敛精度低:相比全局梯度,随机梯度是近似值,可能无法找到最优解。
- 学习率敏感:学习率需要仔细调整,过大可能跳过最优点,过小会导致收敛太慢。
3. 改进方法
为了克服 SGD 的缺点,研究者提出了多种改进算法:
- Mini-batch SGD:每次用一个小批量的数据(如 32 或 64 个样本)来计算梯度,兼顾计算效率和梯度稳定性。
- 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$ 的梯度 学习率决定了参数在梯度方向上移动的距离:
- 学习率过大:可能导致跳过最优点,甚至无法收敛(发散)。
- 学习率过小:虽然可以接近最优点,但收敛速度会非常慢,耗费大量时间。
2. 学习率的选择
学习率过大
- 参数变化剧烈,目标函数可能在最优点附近震荡甚至发散。
- 训练损失无法稳定下降,可能出现“跳跃”现象。
学习率过小
- 参数更新步长很小,虽然模型最终可能收敛,但训练时间会显著增加。
- 可能陷入局部最优或停留在平坦区域中,效率低下。
合适的学习率
- 训练损失逐步下降,并在一定时间内收敛到理想的目标函数值。
- 更新步长既不太大也不太小,能够有效探索目标函数空间。
3. 动态学习率调整
在实际训练中,固定学习率往往难以适应整个训练过程,因此常采用动态调整策略:
常见方法
- 学习率衰减(Learning Rate Decay)
- 随着训练的进行逐步减小学习率,使模型在接近最优解时步伐更小。
- 示例公式:
$$ \eta_t = \frac{\eta_0}{1 + kt} $$
其中 $\eta_0$ 是初始学习率,$t$ 是训练的步数,$k$ 是衰减因子。 2. 阶梯式衰减(Step Decay)
- 在训练过程中,每隔固定的时间步长将学习率减少一定比例。
- 例如:每 10 个 epoch 将学习率乘以 0.1。
- 指数衰减(Exponential Decay)
- 学习率按指数形式衰减:
$$ \eta_t = \eta_0 \cdot e^{-\lambda t} $$ 4. 余弦退火(Cosine Annealing)
- 通过余弦函数周期性调整学习率,避免过早收敛到局部最优。
- 学习率调度器(Learning Rate Schedulers)
- 在深度学习框架中(如 TensorFlow 和 PyTorch),可通过调度器动态调整学习率。
自适应学习率
一些优化算法自动调整学习率,无需手动设计:
- Adagrad:对稀疏参数应用较大的学习率,对频繁更新的参数应用较小的学习率。
- RMSProp:通过指数加权移动平均,平滑梯度平方的变化。
- 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