最近在学习神经网络相关的知识,为了巩固自己对相关知识的理解,尝试使用C语言来编写一个简单的神经网络.
前馈神经网络是一个最简单的神经网络模型,每层神经元采用全连接的方式与下一层神经元相连接,信号的传递方向是单向的.如图所示:
神经网络依赖于矩阵运算,首先实现前馈神经网络需要使用到的矩阵运算.
新建 mymatrix.h 文件:
定义数据类型和错误类型:
typedef double m_float;
// 定义错误类型
#define M_ERRTYPE_NULL (0)
#define M_ERRTYPE_MEMERR (1)
#define M_ERRTYPE_PARERR (2)
#define M_ERRTYPE_CALCERR (3)
声明如下函数:
其中参数m=要计算的矩阵,row=矩阵的行数,column=矩阵的列数
// 初始化矩阵为0,成功返回0
int m_init_matrix(m_float*m, int row, int column);
// 初始化为单位矩阵
int m_init_identity(m_float*m, int len);
// 初始化矩阵为随机数范围是min到max,成功返回0
int m_init_matrix_random(m_float*m, int row, int column);
// 复制矩阵
int m_matrix_copy(m_float*out, m_float*m,int row,int column);
// 打印矩阵值
int m_printf(m_float*m, int row, int column);
// 两个矩阵相加,out可以和a或b是同一个变量
int m_matrix_add(m_float* out, m_float* a, m_float* b, int row, int column);
// 两个矩阵相减,out可以和a或b是同一个变量
int m_matrix_sub(m_float* out, m_float* a, m_float* b, int row, int column);
// 两个矩阵相乘,column_a==row_b
int m_matrix_multiply(m_float* out, m_float* a, m_float* b, int row_a, int column_a, int column_b);
// a的转置与b相乘,row_a==row_b
int m_matrix_multiply_tr_a(m_float* out, m_float* a, m_float* b, int row_a, int column_a, int column_b);
// a与b的转置相乘,column_a==column_b
int m_matrix_multiply_tr_b(m_float* out, m_float* a, m_float* b, int row_a, int column_a, int row_b);
// 求矩阵的hadamard积,out可以和a或b是同一个变量
int m_matrix_hadamard(m_float* out, m_float* a, m_float* b, int row, int column);
// 求矩阵的转置矩阵
int m_matrix_transpose(m_float* out, m_float* a, int row, int column);
// 取得矩阵第i行,第j列的数据
#define m_macro_matrix_value(m,column,i,j) (m)[(i)*(column)+(j)]
// 初始化矩阵为0,成功返回0
int m_init_matrix(m_float* m, int row, int column)
{
for (int i = 0; i < row * column; i++)
{
m[i] = 0;
}
return M_ERRTYPE_NULL;
}
// 初始化为单位矩阵
int m_init_identity(m_float* m, int len)
{
for (int i = 0; i < len; i++)
{
for (int j = 0; j < len; j++)
{
if (i == j)
m[i * len + j] = 1;
else
m[i * len + j] = 0;
}
}
return M_ERRTYPE_NULL;
}
// 初始化矩阵为随机数
int m_init_matrix_random(m_float* m, int row, int column)
{
static int seed = 0;
if (seed == 0) seed = (int)time(NULL);
for (int i = 0; i < row * column; i++)
{
srand(seed + i);
seed = rand();
m[i] = seed % 10000 / 10000.0;
}
return M_ERRTYPE_NULL;
}
// 复制矩阵
int m_matrix_copy(m_float* out, m_float* m, int row, int column)
{
for (int i = 0; i < row * column; i++)
{
out[i] = m[i];
}
return M_ERRTYPE_NULL;
}
// 打印矩阵值
int m_printf(m_float* m, int row, int column)
{
printf("matrix[%d*%d]:\r\n",row,column);
for (int i = 0; i < row; i++)
{
printf("[");
for (int j = 0; j < column; j++)
printf("%f,", m[i * column + j]);
printf("]\r\n");
}
return M_ERRTYPE_NULL;
}
// 两个矩阵相加,out可以和a或b是同一个变量
int m_matrix_add(m_float* out, m_float* a, m_float* b, int row, int column)
{
for (int i = 0; i < row * column; i++)
{
out[i] = a[i] + b[i];
}
return M_ERRTYPE_NULL;
}
// 两个矩阵相减,out可以和a或b是同一个变量
int m_matrix_sub(m_float* out, m_float* a, m_float* b, int row, int column)
{
for (int i = 0; i < row * column; i++)
{
out[i] = a[i] - b[i];
}
return M_ERRTYPE_NULL;
}
// 两个矩阵相乘
int m_matrix_multiply(m_float* out, m_float* a, m_float* b, int row_a, int column_a, int column_b)
{
m_float value;
for (int i = 0; i < row_a; i++)
{
for (int j = 0; j < column_b; j++)
{
value = 0;
for (int k = 0; k < column_a; k++)
{
value += m_macro_matrix_value(a, column_a, i, k) *
m_macro_matrix_value(b, column_b, k, j);
}
m_macro_matrix_value(out, column_b, i, j) = value;
}
}
return M_ERRTYPE_NULL;
}
// a的转置与b相乘
int m_matrix_multiply_tr_a(m_float* out, m_float* a, m_float* b, int row_a, int column_a, int column_b)
{
m_float value;
for (int i = 0; i < column_a; i++)
{
for (int j = 0; j < column_b; j++)
{
value = 0;
for (int k = 0; k < row_a; k++)
{
value += m_macro_matrix_value(a, column_a, k, i) *
m_macro_matrix_value(b, column_b, k, j);
}
m_macro_matrix_value(out, column_b, i, j) = value;
}
}
return M_ERRTYPE_NULL;
}
// a与b的转置相乘
int m_matrix_multiply_tr_b(m_float* out, m_float* a, m_float* b, int row_a, int column_a, int row_b)
{
m_float value;
for (int i = 0; i < row_a; i++)
{
for (int j = 0; j < row_b; j++)
{
value = 0;
for (int k = 0; k < column_a; k++)
{
value += m_macro_matrix_value(a, column_a, i, k) *
m_macro_matrix_value(b, column_a, j, k);
}
m_macro_matrix_value(out, row_b, i, j) = value;
}
}
return M_ERRTYPE_NULL;
}
// 求矩阵的hadamard积,out可以和a或b是同一个变量
int m_matrix_hadamard(m_float* out, m_float* a, m_float* b, int row, int column)
{
for (int i = 0; i < row * column; i++)
{
out[i] = a[i] * b[i];
}
return M_ERRTYPE_NULL;
}
根据本人的理解,输入层只有自变量,所以定义神经网络类的时候没有把输入层计算在内,方便程序编写.
每一层神经元包含的变量有输入x;偏置b;参数w;输出a,每一层的输出a同时也是下一层的输入x.如果神经网络不具备学习功能,仅用这些参数就够了.
每层神经元用于学习的变量有净输出值z,这个值用于计算激活函数的梯度;损失值loss,这个变量用于反向传播时把误差向前传播.
变量loss是网络的整体误差,此误差越小越好.
// 定义神经网络最大层数(不包括输入层)
#define NERVE_MAX_LAYER_NUM (10)
// 定义一个神经网络
typedef struct {
// 网络层数,不包括输入层
int layer_num;
// 数据源个数
// 第0层神经元接收的是外部输入数据,
// 第i(i>0)层神经元接收的是前一层神经元的输出数据
int src_num[NERVE_MAX_LAYER_NUM+1];
// 外部输入数据,初始化时分配内存
m_float* src_data;
// 矫正值,用于计算误差,初始化时分配内存
m_float* y;
// 各层神经元数目
int layer_cell_num[NERVE_MAX_LAYER_NUM];
// 神经元输入数据,此变量不申请内存,
// 第0层指向src_table,第i(i>0)层指向a[i-1]
m_float* src_table[NERVE_MAX_LAYER_NUM+1];
// 各层神经元在激活函数之前的输出值,在初始化时分配内存
m_float* z_table[NERVE_MAX_LAYER_NUM];
// 各层神经元的输出数据,在初始化时分配内存
m_float* a_table[NERVE_MAX_LAYER_NUM];
// 各层神经元的偏置,在初始化时分配内存
m_float* b_table[NERVE_MAX_LAYER_NUM];
// 各层神经元的权重,在初始化时分配内存
// 对于第i层来说w是一个[layer_cell_num[i]*src_num[i]]的二维数组
// 其中第j行存储的是第j个神经元对数据源的权重
m_float* w_table[NERVE_MAX_LAYER_NUM];
// 各层神经元的误差,在初始化时分配内存
m_float* loss_table[NERVE_MAX_LAYER_NUM];
m_float loss;// 误差
}nerve_obj;
ReLU(x)=max(0,x),此函数在x小于0时恒等于0,在x>0时为x本身.
经我测试,激活函数ReLU比sigmoid学习效果要好,但斜率不能太大,猜测可能是太大的斜率产生了过大的数据,使之后的误差评价函数产生了指数爆炸.
所以在此给了0.1的斜率,实测没有产生指数爆炸.
// 激活函数
static int nerve_relu(m_float *a,m_float *z,int len)
{
for (int i = 0; i < len; i++)
{
a[i] = z[i] > 0.0 ? z[i]*0.1 : 0;
}
return 0;
}
// 激活函数的导函数乘以一个数l,z是自变量
static int nerve_deriv_relu(m_float* out, m_float* l,m_float* z, int len)
{
for (int i = 0; i < len; i++)
{
out[i] = z[i] > 0 ? l[i]*0.1 : 0;
}
return 0;
}
Softmax函数用于评价输出y=c的概率.
Softmax(y=c|x[1…n])=exp(x[c])/(exp(x[1])+…+exp(x[n])).
在编码中使用了对数公式,在一定程度上避免了指数爆炸问题.
// softmax 函数,也可以看作是最后一层的激活函数
static m_float nerve_softmax(m_float *out,m_float *a,int len)
{
m_float sum = 0,max;
max = a[0];
for (int i = 0; i < len; i++)
{
if (max < a[i])
max = a[i];
}
for (int i = 0; i < len; i++)
{
sum += exp(a[i]-max);
}
for (int i = 0; i < len; i++)
{
out[i] = exp(a[i]-max) / sum;
}
return sum;
}
手写数字识别是分类问题,使用交叉熵损失函数来评价整体误差.
其中变量y为校正值,a为输出值,
对于手写数字识别任务来说,校正值只可能有一个为1,其余都为0,假设这个为1的项是第c项,则恒有误差loss=-log(a[c]).
// 误差计算函数
static m_float nerve_loss(m_float *y,m_float *a,int len)
{
m_float t = 0;
for (int i = 0; i < len; i++)
{
if(y[i]>0)
t+= y[i] * log(a[i]);
}
return -t;
}
实现神经网络初始化,输入输出接口等操作.其中nerve_get_max_index输出的是输出矩阵中最大值的序号,对于手写数字识别任务来说,此序号就是识别的数字.
// 初始化神经网络
int nerve_init(nerve_obj* n, int layer, int src_num, int* cell_nums)
{
memset(n, 0, sizeof(nerve_obj));
n->layer_num = layer;
n->src_num[0] = src_num;
n->src_data = calloc(n->src_num[0], sizeof(m_float));
n->y = calloc(cell_nums[n->layer_num - 1], sizeof(m_float));
n->src_table[0] = n->src_data;
for (int i = 0; i < n->layer_num; i++)
{
n->layer_cell_num[i] = cell_nums[i];
n->src_num[i+1]= cell_nums[i];
n->z_table[i] = calloc(n->layer_cell_num[i], sizeof(m_float));
n->a_table[i] = calloc(n->layer_cell_num[i], sizeof(m_float));
n->b_table[i] = calloc(n->layer_cell_num[i], sizeof(m_float));
n->loss_table[i] = calloc(n->layer_cell_num[i], sizeof(m_float));
n->w_table[i] = calloc(n->layer_cell_num[i] * n->src_num[i], sizeof(m_float));
m_init_matrix_random(n->w_table[i], n->layer_cell_num[i], n->src_num[i]);
m_init_matrix_random(n->b_table[i], 1, n->layer_cell_num[i]);
n->src_table[i + 1] = n->a_table[i];
}
return 0;
}
// 获取输入缓冲区
m_float* nerve_get_input_ptr(nerve_obj* n)
{
return n->src_data;
}
// 获取输出缓冲区
m_float* nerve_get_output_ptr(nerve_obj* n)
{
return n->a_table[n->layer_num-1];
}
// 获取校正值缓冲区
m_float* nerve_get_y_ptr(nerve_obj* n)
{
return n->y;
}
// 获取最大输出值的序号
int nerve_get_max_index(nerve_obj* n)
{
int index = 0;
m_float* a = nerve_get_output_ptr(n);
m_float max=a[0];
for (int i = 0; i < n->layer_cell_num[n->layer_num - 1]; i++)
{
if (max < a[i])
{
max = a[i];
index = i;
}
}
return index;
}
正向传播就是从第0层开始,依次计算z=w*x+b;a=f(z).其中f(.)为激活函数.
由于最后一层的激活函数为softmax,所以分开计算.
// 计算神经网络
int nerve_calc(nerve_obj* n)
{
for (int i = 0; i < n->layer_num; i++)
{
m_matrix_multiply(n->z_table[i], n->w_table[i], n->src_table[i],
n->layer_cell_num[i], n->src_num[i], 1);
m_matrix_add(n->z_table[i], n->z_table[i], n->b_table[i], n->layer_cell_num[i], 1);
if(i<n->layer_num-1)
nerve_relu(n->a_table[i], n->z_table[i], n->layer_cell_num[i]);
else
nerve_softmax(n->a_table[i], n->z_table[i], n->layer_cell_num[i]);
}
}
对于交叉熵评价函数,其误差loss=(a-y),若是其他评价函数,需要对此函数进行修改.
// 计算最后一层的误差2L(z)/2(z)
static int nerve_last_loss(m_float *out,m_float *a,m_float *y,int len)
{
m_matrix_sub(out, a, y, 1, len);
return 0;
}
在本程序中学习率a只能取负值.
最后一层的误差使用公式loss=(a-y)计算得出;
其余层的误差使用公式loss[n]=f’(z)(tr_wloss[n+1])得出,其中tr_w为w的转置矩阵,f’(.)为激活函数的导函数.
更新权重使用公式w=w+alosstr_x;b=b+a*loss.其中tr_x为x的转置矩阵.
在编码中可以使用函数m_matrix_multiply_tr_b来计算新权重,这里使用的多重循环.
// 反向传播学习,a学习率
int nerve_calc_bp(nerve_obj* n,m_float a)
{
m_float* w;
// 计算预测误差
n->loss=nerve_loss(n->y, nerve_get_output_ptr(n),n->layer_cell_num[n->layer_num-1]);
// 计算最后一层的误差
nerve_last_loss(n->loss_table[n->layer_num - 1], nerve_get_output_ptr(n),n->y, n->layer_cell_num[n->layer_num - 1]);
// 计算其余层的误差
for (int i = n->layer_num-1; i > 0; i--)
{
// 第i层的误差权重和
m_matrix_multiply_tr_a(n->loss_table[i - 1], n->w_table[i], n->loss_table[i],
n->layer_cell_num[i], n->src_num[i], 1);
// 使用激活梯度函数可能是因为梯度消失的原因不能收敛
nerve_deriv_relu(n->loss_table[i - 1], n->loss_table[i - 1], n->z_table[i - 1], n->layer_cell_num[i - 1]);
}
// 更新权重
for (int i = 0; i < n->layer_num; i++)
{
w = n->w_table[i];
for (int j = 0; j < n->layer_cell_num[i]; j++)
{
for (int k = 0; k < n->src_num[i]; k++)
{
m_macro_matrix_value(w, n->src_num[i],j,k)+=
a * n->src_table[i][k] * n->loss_table[i][j];
}
n->b_table[i][j] += a * n->loss_table[i][j];
}
}
}
// 统计最近n次的误差
#define TRAIN_LOSS_NUM (200)
// 计算最近TRAIN_LOSS_NUM次的误差
m_float calc_loss(m_float loss)
{
static int num = 0, ptr=0;
static m_float loss_table[TRAIN_LOSS_NUM];
m_float loss_ave, loss_sum;
if (num < TRAIN_LOSS_NUM)
{
loss_table[num] = loss;
num++;
}
else
{
loss_table[ptr] = loss;
ptr++;
if (ptr >= num) ptr = 0;
}
loss_sum = 0;
for (int i = 0; i < num; i++)
loss_sum += loss_table[i];
loss_ave = loss_sum / num;
return loss_ave;
}
m_float drain_dst_table[10][10] = {
{ 1,0,0,0,0,0,0,0,0,0 },
{ 0,1,0,0,0,0,0,0,0,0 },
{ 0,0,1,0,0,0,0,0,0,0 },
{ 0,0,0,1,0,0,0,0,0,0 },
{ 0,0,0,0,1,0,0,0,0,0 },
{ 0,0,0,0,0,1,0,0,0,0 },
{ 0,0,0,0,0,0,1,0,0,0 },
{ 0,0,0,0,0,0,0,1,0,0 },
{ 0,0,0,0,0,0,0,0,1,0 },
{ 0,0,0,0,0,0,0,0,0,1 },
};
int main()
{
// 神经网络类
nerve_obj nerve = { 0 };
// 输入,输出,校正值
m_float* bmp_x = 0, * out = 0, * z_true;
// 训练集
const char* train_data, * train_p;
// 测试集
const char* test_data, * test_p;
// 两层神经网络,第一层88个神经元,第二层10个神经元
int cells[] = { 2,88,10 };
// 序号1,序号2,测试成功的次数
int index, index2, test_true;
// 加载数据集
train_data = data_read_file(".\\hand\\mnist_train.csv");
test_data = data_read_file(".\\hand\\mnist_test.csv");
// 初始化神经网络
nerve_init(&nerve, cells[0], 28 * 28, &cells[1]);
bmp_x = nerve_get_input_ptr(&nerve);
out = nerve_get_output_ptr(&nerve);
z_true = nerve_get_y_ptr(&nerve);
printf("初始化完成\r\n");
for (int m = 0; m < 1; m++)
{
train_p = train_data;
for (int i = 0; i < 59999; i++)
{
// 读取一行数据,也就是一个训练图片
index = data_tr_buff(bmp_x, train_p);
train_p = data_get_next_line(train_p);
memcpy(z_true, drain_dst_table[index], sizeof(m_float) * 10);
// 计算神经网络
nerve_calc(&nerve);
// 使用校正值来学习
nerve_calc_bp(&nerve, -0.05);
// 显示训练次数和误差,正常情况下误差loss会越来越小
printf("time=%d,loss=%f \r", i, calc_loss(nerve.loss));
}
printf("\n训练完成\r\n");
test_p = test_data;
test_true = 0;
for (int i = 0; i < 9999; i++)
{
// 读取一行数据
index = data_tr_buff(bmp_x, test_p);
test_p = data_get_next_line(test_p);
// 计算神经网络
nerve_calc(&nerve);
// 获取识别的数字
index2 = nerve_get_max_index(&nerve);
// 如果识别的数字与标签数字相等,则识别成功
//if ((index == index2)&&(out[index2]>0.85))
if (index == index2)
test_true++;
printf("time=%d \r", i);
}
printf("测试识别正确 %d 次\r\n", test_true);
}
return 0;
}
对于60000个数据的训练集,1000个数据的测试集mnist来说,训练之后的测试成功次数基本都在9000次以上,也就是识别成功率在90%以上.
因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- ryyc.cn 版权所有 湘ICP备2023022495号-3
违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务