無依賴、無動態分配、固定最大維度的純 C 多層感知機(MLP)實現,適合嵌入式裸機、單片機、DSP 等資源受限環境。採用全連接矩陣形式、Sigmoid 激活、隨機梯度下降 (SGD),支持任意隱層數(受編譯期常量限制)。
// dnn.h #ifndef DNN_H #define DNN_H #include#include // 配置常量(編譯期確定,無動態分配) #define DNN_MAX_IN 16 // 最大輸入維度 #define DNN_MAX_OUT 4 // 最大輸出維度 #define DNN_MAX_HID 32 // 最大隱層神經元數(單層) #define DNN_MAX_LAYERS 4 // 最大隱層數 // 神經元結構:權重、偏置、輸入緩存、輸出、誤差、梯度 typedef struct { float w[DNN_MAX_IN]; // 權重向量(輸入維度動態使用) float b; // 偏置 float in[DNN_MAX_IN]; // 前向傳播輸入緩存(用於反向傳播) float out; // 輸出值 float err; // 誤差項 (delta) } DNN_Neuron; // 層結構 typedef struct { DNN_Neuron neurons[DNN_MAX_HID]; int n_in; // 輸入維度 int n_out; // 神經元數(輸出維度) } DNN_Layer; // 網絡結構 typedef struct { DNN_Layer layers[DNN_MAX_LAYERS + 1]; // +1 輸出層 int n_layers; // 隱層數 int n_in; // 網絡輸入維度 int n_out; // 網絡輸出維度 float lr; // 學習率 } DNN_Network; // 激活函數 static inline float sigmoid(float x) { return 1.0f / (1.0f + expf(-x)); } static inline float sigmoid_deriv(float y) { return y * (1.0f - y); } // y = sigmoid(x) // 接口 void dnn_init(DNN_Network *net, int n_in, int n_out, int n_layers, const int *hid_sizes, float lr); void dnn_forward(DNN_Network *net, const float *input, float *output); void dnn_backward(DNN_Network *net, const float *target); void dnn_update(DNN_Network *net); void dnn_train(DNN_Network *net, const float *input, const float *target); float dnn_predict(DNN_Network *net, const float *input, float *output); #endif
// dnn.c #include "dnn.h" #includevoid dnn_init(DNN_Network *net, int n_in, int n_out, int n_layers, const int *hid_sizes, float lr) { net->n_in = n_in; net->n_out = n_out; net->n_layers = n_layers; net->lr = lr; srand((unsigned)time(NULL)); // 簡單種子,嵌入式可用 ADC 噪聲/時間戳 int prev_size = n_in; for (int l = 0; l < n_layers; ++l) { int cur_size = hid_sizes[l]; DNN_Layer *layer = &net->layers[l]; layer->n_in = prev_size; layer->n_out = cur_size; float scale = sqrtf(6.0f / (prev_size + cur_size)); // Xavier uniform for (int i = 0; i < cur_size; ++i) { for (int j = 0; j < prev_size; ++j) { layer->neurons[i].w[j] = ((float)rand() / RAND_MAX * 2 - 1) * scale; } layer->neurons[i].b = 0.0f; } prev_size = cur_size; } // 輸出層 DNN_Layer *out_layer = &net->layers[n_layers]; out_layer->n_in = prev_size; out_layer->n_out = n_out; float scale = sqrtf(6.0f / (prev_size + n_out)); for (int i = 0; i < n_out; ++i) { for (int j = 0; j < prev_size; ++j) { out_layer->neurons[i].w[j] = ((float)rand() / RAND_MAX * 2 - 1) * scale; } out_layer->neurons[i].b = 0.0f; } }
void dnn_forward(DNN_Network *net, const float *input, float *output) {
float *prev_out = (float *)input; // 第一層輸入直接用外部數組
for (int l = 0; l <= net->n_layers; ++l) {
DNN_Layer *layer = &net->layers[l];
for (int i = 0; i < layer->n_out; ++i) {
DNN_Neuron *n = &layer->neurons[i];
float sum = n->b;
for (int j = 0; j < layer->n_in; ++j) {
n->in[j] = prev_out[j]; // 緩存輸入用於反向
sum += n->w[j] * prev_out[j];
}
n->out = sigmoid(sum);
}
// 準備下一層輸入:當前層所有神經元輸出
// 為避免動態分配,使用靜態緩存區(最大隱層尺寸)
static float layer_out[DNN_MAX_HID];
for (int i = 0; i < layer->n_out; ++i) layer_out[i] = layer->neurons[i].out;
prev_out = layer_out;
}
// 複製最終輸出
for (int i = 0; i < net->n_out; ++i) output[i] = net->layers[net->n_layers].neurons[i].out;
}
void dnn_backward(DNN_Network *net, const float *target) {
// 輸出層誤差:delta = (y - t) * sigmoid'(y)
DNN_Layer *out_layer = &net->layers[net->n_layers];
for (int i = 0; i < out_layer->n_out; ++i) {
DNN_Neuron *n = &out_layer->neurons[i];
float diff = n->out - target[i];
n->err = diff * sigmoid_deriv(n->out);
}
// 隱層反向傳播:delta = sum(next_w * next_delta) * sigmoid'(y)
for (int l = net->n_layers - 1; l >= 0; --l) {
DNN_Layer *layer = &net->layers[l];
DNN_Layer *next_layer = &net->layers[l + 1];
for (int i = 0; i < layer->n_out; ++i) {
DNN_Neuron *n = &layer->neurons[i];
float sum_err = 0.0f;
for (int k = 0; k < next_layer->n_out; ++k) {
sum_err += next_layer->neurons[k].w[i] * next_layer->neurons[k].err;
}
n->err = sum_err * sigmoid_deriv(n->out);
}
}
}
void dnn_update(DNN_Network *net) {
for (int l = 0; l <= net->n_layers; ++l) {
DNN_Layer *layer = &net->layers[l];
for (int i = 0; i < layer->n_out; ++i) {
DNN_Neuron *n = &layer->neurons[i];
for (int j = 0; j < layer->n_in; ++j) {
n->w[j] -= net->lr * n->err * n->in[j];
}
n->b -= net->lr * n->err;
}
}
}
void dnn_train(DNN_Network *net, const float *input, const float *target) {
dnn_forward(net, input, NULL);
dnn_backward(net, target);
dnn_update(net);
}
float dnn_predict(DNN_Network *net, const float *input, float *output) {
dnn_forward(net, input, output);
return output[0]; // 單輸出場景返回首值
}
// main.c #include "dnn.h" #includeint main() { // 4-8-1 網絡:4輸入、1隱層8神經元、1輸出 int hid[] = {8}; DNN_Network net; dnn_init(&net, 4, 1, 1, hid, 0.05f); // 訓練數據:簡邏輯或線性可分問題 float X[8][4] = { {0,0,0,0}, {0,0,0,1}, {0,0,1,0}, {0,0,1,1}, {1,1,1,1}, {1,1,1,0}, {1,1,0,1}, {1,1,0,0} }; float Y[8][1] = {{0},{0},{0},{0},{1},{1},{1},{1}}; // 訓練 50000 輪 for (int epoch = 0; epoch < 50000; ++epoch) { int idx = rand() % 8; dnn_train(&net, X[idx], Y[idx]); } // 測試 printf("測試結果:\n"); for (int i = 0; i < 8; ++i) { float out; dnn_predict(&net, X[i], &out); printf("輸入: %.0f %.0f %.0f %.0f -> 輸出: %.4f (目標: %.0f)\n", X[i][0], X[i][1], X[i][2], X[i][3], out, Y[i][0]); } return 0; }
| 約束 | 應對策略 |
|---|---|
| 無堆/無 malloc | 所有數組靜態分配(編譯期常量上限),網絡結構體作為全局/棧變量 |
| 無標準庫 | 自行實現 expf 查表/多項式近似,sqrtf 牛頓法,rand LFSR |
| 無 FPU | 改用定點數 Q15/Q31,或軟浮點庫(-msoft-float) |
| RAM 極小 | 減小 DNN_MAX_*,複用緩存區,權重存 Flash 運行時讀取 |
| 實時性 | 固定推理週期,無動態分支,可計算最坏執行時間 (WCET) |
純 C MLP 以 固定內存佈局、矩陣級前向反向、SGD 參數更新 實現了神經網絡核心邏輯。代碼無第三方依賴、可直接編入裸機項目,是嵌入式 AI 落地的起點。理解其數據流(輸入→隱層→輸出→誤差→梯度→更新)與內存模型,為後續移植 TensorFlow Lite Micro、CMSIS-NN、TVM MicroTVM 打下堅實基礎。