首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

純 C 語言實現多層感知機(MLP):矩陣運算、Sigmoid、SGD 訓練與嵌入式移植指南

     編輯:關於C語言

純 C 語言實現多層感知機(MLP):矩陣運算、Sigmoid、SGD 訓練與嵌入式移植指南

無依賴、無動態分配、固定最大維度的純 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

二、關鍵算法實現

1. 初始化:Xavier/Glorot 初始化

// dnn.c
#include "dnn.h"
#include 

void 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;
    }
}

2. 前向傳播

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;
}

3. 反向傳播(輸出層 MSE 損失)

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);
        }
    }
}

4. 參數更新(SGD)

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;
        }
    }
}

5. 訓練/預測封裝

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"
#include 

int 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)

五、局限與擴展方向

  • 僅支持全連接、Sigmoid、MSE、SGD,現代 CNN/RNN/Transformer 需專用結構。
  • 無批次歸一化、Dropout、Adam 等正則化/優化器。
  • 擴展:添加 ReLU/LeakyReLU、Softmax、交叉熵、動量法、學習率衰減、量化感知訓練。

總結

純 C MLP 以 固定內存佈局、矩陣級前向反向、SGD 參數更新 實現了神經網絡核心邏輯。代碼無第三方依賴、可直接編入裸機項目,是嵌入式 AI 落地的起點。理解其數據流(輸入→隱層→輸出→誤差→梯度→更新)與內存模型,為後續移植 TensorFlow Lite Micro、CMSIS-NN、TVM MicroTVM 打下堅實基礎。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved