本系列第二篇,聚焦 CNN 整體網路架構與前向傳播實作。參考 LeNet-5 並簡化調整:移除輸入填邊、C3 採全連接卷積、省略 C5/F6 層,直接由 S4 接全連接輸出層。適用於 MNIST 手寫數字辨識任務。
| 層名 | 類型 | 輸入 | 核心參數 | 輸出 |
|---|---|---|---|---|
| C1 | 卷積 | 1×28×28 | 6×5×5 核, Sigmoid | 6×24×24 |
| S2 | 平均池化 | 6×24×24 | 2×2 窗口, stride=2 | 6×12×12 |
| C3 | 全連接卷積 | 6×12×12 | 6×12 個 5×5 核, Sigmoid | 12×8×8 |
| S4 | 平均池化 | 12×8×8 | 2×2 窗口, stride=2 | 12×4×4 |
| O5 | 全連接 | 192 (12×4×4) | 192×10 權重, Sigmoid | 10 (類別機率) |
// 卷積層
typedef struct {
int inW, inH, kSize, inCh, outCh;
bool fullConnect;
bool *connectMap; // 非全連接時的連接模式
float ****kernels; // [inCh][outCh][kSize][kSize]
float ****dkernels; // 梯度
float *bias; // [outCh]
float ***v, ***y, ***d; // 前向/反向中間值
} ConvLayer;
// 池化層
typedef struct {
int inW, inH, kSize, inCh, outCh, poolType; // 0=Avg, 1=Max
float *bias;
float ***y, ***d;
} PoolLayer;
// 全連接輸出層
typedef struct {
int inDim, outDim;
float **weights; // [inDim][outDim]
float *bias; // [outDim]
float *v, *y, *d;
bool fullConnect;
} OutputLayer;
// 整體網路
typedef struct {
ConvLayer *C1, *C3;
PoolLayer *S2, *S4;
OutputLayer *O5;
float *error, *loss;
} CNN;
// 輸入: float ***input (inCh x inH x inW)
for (int oc = 0; oc < C1->outCh; ++oc) {
for (int ic = 0; ic < C1->inCh; ++ic) {
float **conv_out = conv2d(C1->kernels[ic][oc], C1->kSize,
input[ic], C1->inH, C1->inW, VALID);
add_matrix(C1->v[oc], C1->v[oc], conv_out, outH, outW);
free_matrix(conv_out);
}
// 加 bias 並通過 Sigmoid
for (int r = 0; r < outH; ++r)
for (int c = 0; c < outW; ++c)
C1->y[oc][r][c] = sigmoid(C1->v[oc][r][c] + C1->bias[oc]);
}
for (int ch = 0; ch < S2->outCh; ++ch)
avg_pooling(S2->y[ch], outH, outW, C1->y[ch], inH, inW, S2->kSize);
for (int oc = 0; oc < C3->outCh; ++oc) {
for (int ic = 0; ic < C3->inCh; ++ic) {
float **conv_out = conv2d(C3->kernels[ic][oc], C3->kSize,
S2->y[ic], inH, inW, VALID);
add_matrix(C3->v[oc], C3->v[oc], conv_out, outH, outW);
free_matrix(conv_out);
}
for (int r = 0; r < outH; ++r)
for (int c = 0; c < outW; ++c)
C3->y[oc][r][c] = sigmoid(C3->v[oc][r][c] + C3->bias[oc]);
}
for (int ch = 0; ch < S4->outCh; ++ch)
avg_pooling(S4->y[ch], outH, outW, C3->y[ch], inH, inW, S4->kSize);
// 展平為 1D 向量 (192)
float *flat = malloc(O5->inDim * sizeof(float));
int idx = 0;
for (int ch = 0; ch < S4->outCh; ++ch)
for (int r = 0; r < outH; ++r)
for (int c = 0; c < outW; ++c)
flat[idx++] = S4->y[ch][r][c];
// 矩陣向量乘法: v = flat * W + bias
mat_vec_mul(O5->v, flat, O5->weights, O5->bias, O5->inDim, O5->outDim);
for (int i = 0; i < O5->outDim; ++i)
O5->y[i] = sigmoid(O5->v[i]);
free(flat);
float sigmoid(float x) { return 1.0f / (1.0f + expf(-x)); }
// 2D 卷積 (VALID padding)
float **conv2d(float **kernel, int k, float **input, int H, int W, int mode) {
int outH = H - k + 1, outW = W - k + 1;
float **out = alloc_matrix(outH, outW);
for (int r = 0; r < outH; ++r)
for (int c = 0; c < outW; ++c) {
float sum = 0;
for (int kr = 0; kr < k; ++kr)
for (int kc = 0; kc < k; ++kc)
sum += kernel[kr][kc] * input[r+kr][c+kc];
out[r][c] = sum;
}
return out;
}
// 平均池化
void avg_pooling(float **out, int oH, int oW, float **in, int iH, int iW, int k) {
for (int r = 0; r < oH; ++r)
for (int c = 0; c < oW; ++c) {
float sum = 0;
for (int kr = 0; kr < k; ++kr)
for (int kc = 0; kc < k; ++kc)
sum += in[r*k+kr][c*k+kc];
out[r][c] = sum / (k*k);
}
}
typedef struct { int epochs; float alpha; } TrainOpts;
void cnn_init(CNN *net) {
// 依結構體逐層 malloc,權重用 Xavier/He 初始化
// bias 歸零
}
TrainOpts opts = { .epochs = 30, .alpha = 0.01f };
本文完整定義了 CNN 的層級結構、資料結構與前向傳播流程。下一篇將實作反向傳播 (BP) 算法:計算輸出層誤差、逐層反傳梯度、更新權重與 bias,並結合 MNIST 資料集完成端到端訓練與測試。