基于C++复现BP神经网络手写字识别

基于C++复现BP神经网络手写字识别



前言

BP(Back-propagation,反向传播)神经网络是最传统的神经网络。也就是使用了Back-propagation算法的神经网络。BP神经网络是一种按误差反向传播(简称误差反传)训练的多层前馈网络,其算法称为BP算法,它的基本思想是梯度下降法,利用梯度搜索技术,以期使网络的实际输出值和期望输出值的误差均方差为最小。基本BP算法包括信号的前向传播和误差的反向传播两个过程。即计算误差输出时按从输入到输出的方向进行,而调整权值和阈值则从输出到输入的方向进行。正向传播时,输入信号通过隐含层作用于输出节点,经过非线性变换,产生输出信号,若实际输出与期望输出不相符,则转入误差的反向传播过程。误差反传是将输出误差通过隐含层向输入层逐层反传,并将误差分摊给各层所有单元,以从各层获得的误差信号作为调整各单元权值的依据。通过调整输入节点与隐层节点的联接强度和隐层节点与输出节点的联接强度以及阈值,使误差沿梯度方向下降,经过反复学习训练,确定与最小误差相对应的网络参数(权值和阈值),训练即告停止。此时经过训练的神经网络即能对类似样本的输入信息,自行处理输出误差最小的经过非线形转换的信息。

组成

BP神经网络(Backpropagation Neural Network)由多个神经元(Neurons)和多层组成,通常包括以下几个主要组成部分:
1.输入层(Input Layer): 输入层接受外部输入数据,每个输入特征对应一个输入神经元。例如,对于图像分类任务,每个像素可以被看作一个输入特征,输入层的神经元数量等于输入特征的数量。
2.隐藏层(Hidden Layers): 隐藏层位于输入层和输出层之间,可以包含一个或多个层次。这些层中的每一层都包括多个神经元。隐藏层的主要作用是学习从输入到输出之间的复杂映射关系,以便更好地拟合数据。每个隐藏层都有其独特的权重和激活函数。
3.输出层(Output Layer): 输出层生成网络的最终输出,其神经元数量通常取决于问题类型。例如,对于二分类问题,可以有一个输出神经元表示两个类别的概率,对于多分类问题,可以有多个输出神经元,每个神经元对应一个类别的概率。
4.权重(Weights): 每个连接输入、隐藏和输出层的神经元都有一个权重,权重决定了信号传递的强度和方向。在训练中,这些权重会不断调整以最小化损失函数。
5.偏差(Biases): 每个神经元都有一个偏差项,它可以调整神经元对输入信号的敏感程度。偏差项允许神经元更灵活地学习。
6.激活函数(Activation Function): 每个神经元都通过激活函数对输入进行非线性变换。常见的激活函数包括Sigmoid、ReLU、Tanh等,它们引入非线性性质,使神经网络能够适应更复杂的数据分布。
7.前向传播(Forward Propagation): 在前向传播过程中,输入数据从输入层传递到输出层。每个神经元接收来自前一层神经元的输入,将这些输入与其权重相乘,并将结果传递给激活函数。前向传播计算网络的输出。
8.反向传播(Backpropagation): 反向传播是训练BP神经网络的关键步骤。它通过计算损失函数关于权重和偏差的梯度,然后使用梯度下降或其他优化算法来更新权重和偏差,以减小损失函数的值。
9.损失函数(Loss Function): 损失函数用于衡量网络的输出与实际目标的差异。在训练中,目标是最小化损失函数,以提高网络的性能。
在这里插入图片描述

应用

1.图像识别和分类: BP神经网络可以用于图像识别和分类任务,如手写数字识别、物体识别、人脸识别等。
2.自然语言处理(NLP): 在自然语言处理领域,BP神经网络可以用于文本分类、情感分析、机器翻译等任务。
3.声音识别: 用于语音识别和语音合成,如语音助手和语音命令识别。
BP神经网络是一种多功能的机器学习模型,适用于各种领域的模式识别和预测任务。

一、BP神经网络手工推导

在这里插入图片描述

上图为神经网络的一部分,x1,x2,x3为外部输入,w1,w2,w3为这些输入的权重(表示这些输入的重要程度)。后面的大⚪相当与一个神经元,其中∑ = x1w1 + x2w2 + x3w2 - β ( β为偏置) ,y = f(∑) ,f为激活函数,最常见激活函数是Sigmoid函数,其图像及表达式如下图所示(本文后面所有激活函数都为Sigmoid函数)

在这里插入图片描述
在这里插入图片描述
下面开始代码实战

二、固定学习率

1.下载MNIST数据集


包含70000张图片,每张大小28*28=784

解压后如下所示在这里插入图片描述

2.复现

提示:把以下程序和数据集放同一目录下
BP.h

#ifndef BP_H_INCLUDED
#define BP_H_INCLUDED
const int INPUT_LAYER = 784; //输入层维度
const int HIDDEN_LAYER = 40; //隐含层维度
const int OUTPUT_LAYER = 10; //输出层维度
const double LEARN_RATE = 0.3; //学习率
const int TRAIN_TIMES = 10; //迭代训练次数
class BP
{
private:
    int input_array[INPUT_LAYER]; //输入向量
    int aim_array[OUTPUT_LAYER]; //目标结果
    double weight1_array[INPUT_LAYER][HIDDEN_LAYER]; //输入层与隐含层之间的权重
    double weight2_array[HIDDEN_LAYER][OUTPUT_LAYER]; //隐含层与输出层之间的权重
    double output1_array[HIDDEN_LAYER]; //隐含层输出
    double output2_array[OUTPUT_LAYER]; //输出层输出
    double deviation1_array[HIDDEN_LAYER]; //隐含层误差
    double deviation2_array[OUTPUT_LAYER]; //输出层误差
    double threshold1_array[HIDDEN_LAYER]; //隐含层阈值
    double threshold2_array[OUTPUT_LAYER]; //输出层阈值
public:
    void Init(); //初始化各参数
    double Sigmoid(double x); //sigmoid激活函数
    void GetOutput1(); //得到隐含层输出
    void GetOutput2(); //得到输出层输出
    void GetDeviation1(); //得到隐含层误差
    void GetDeviation2(); //得到输出层误差
    void Feedback1(); //反馈输入层与隐含层之间的权重
    void Feedback2(); //反馈隐含层与输出层之间的权重
    void Train(); //训练
    void Test(); //测试
};
#endif // BP_H_INCLUDED

BP.cpp

#include <bits/stdc++.h>
#include "BP.h"
using namespace std;
//初始化各参数
void BP::Init()
{
    srand(time(NULL));
    for (int i = 0; i < INPUT_LAYER; i++)
        for (int j = 0; j < HIDDEN_LAYER; j++)
            weight1_array[i][j] = rand()/(double)(RAND_MAX) * 2 - 1;
    for (int i = 0; i < HIDDEN_LAYER; i++)
        for (int j = 0; j < OUTPUT_LAYER; j++)
            weight2_array[i][j] = rand()/(double)(RAND_MAX) * 2 - 1;
    for (int i = 0; i < HIDDEN_LAYER; i++)
        threshold1_array[i] = rand()/(double)(RAND_MAX) * 2 - 1;
    for (int i = 0; i < OUTPUT_LAYER; i++)
        threshold2_array[i] = rand()/(double)(RAND_MAX) * 2 - 1;
}
//sigmoid激活函数
double BP::Sigmoid(double x)
{
    return 1.0 / (1.0 + exp(-x));
}
//得到隐含层输出
void BP::GetOutput1()
{
    for (int j = 0; j < HIDDEN_LAYER; j++)
    {
        double total = threshold1_array[j];
        for (int i = 0; i < INPUT_LAYER; i++)
            total += input_array[i] * weight1_array[i][j];
        output1_array[j] = Sigmoid(total);
    }
}
//得到输出层输出
void BP::GetOutput2()
{
    for (int j = 0; j < OUTPUT_LAYER; j++)
    {
        double total = threshold2_array[j];
        for (int i = 0; i < HIDDEN_LAYER; i++)
            total += output1_array[i] * weight2_array[i][j];
        output2_array[j] = Sigmoid(total);
    }
}
//得到隐含层误差
void BP::GetDeviation1()
{
    for (int i = 0; i < HIDDEN_LAYER; i++)
    {
        double total = 0;
        for (int j = 0; j < OUTPUT_LAYER; j++)
            total += weight2_array[i][j] * deviation2_array[j];
        deviation1_array[i] = (output1_array[i]) * (1.0 - output1_array[i]) * total;
    }
}
//得到输出层误差
void BP::GetDeviation2()
{
    for (int i = 0; i < OUTPUT_LAYER; i++)
        deviation2_array[i] = (output2_array[i]) * (1.0 - output2_array[i]) * (output2_array[i] - aim_array[i]);
}
//反馈输入层与隐含层之间的权重
void BP::Feedback1()
{
    for (int j = 0; j < HIDDEN_LAYER; j++)
    {
        threshold1_array[j] -= LEARN_RATE * deviation1_array[j];
        for (int i = 0; i < INPUT_LAYER; i++)
            weight1_array[i][j] = weight1_array[i][j] - LEARN_RATE * input_array[i] * deviation1_array[j];
    }
}
//反馈隐含层与输出层之间的权重
void BP::Feedback2()
{
    for (int j = 0; j < OUTPUT_LAYER; j++)
    {
        threshold2_array[j] = threshold2_array[j] - LEARN_RATE * deviation2_array[j];
        for (int i = 0; i < HIDDEN_LAYER; i++)
            weight2_array[i][j] = weight2_array[i][j] - LEARN_RATE * output1_array[i] * deviation2_array[j];
    }
}
//训练
void BP::Train()
{
    FILE *train_images;
    FILE *train_labels;
    train_images = fopen("train-images.idx3-ubyte", "rb");
    train_labels = fopen("train-labels.idx1-ubyte", "rb");
    unsigned char image[INPUT_LAYER];
    unsigned char label[OUTPUT_LAYER];
    unsigned char temp[100];
    //读取文件开头
    fread(temp, 1, 16, train_images);
    fread(temp, 1, 8, train_labels);
    int times = 0; //当前训练了几次
    cout << "开始训练..." << endl << endl;
    while (!feof(train_images) && !feof(train_labels))
    {
        fread(image, 1, INPUT_LAYER, train_images);
        fread(label, 1, 1, train_labels);
        //设置输入向量
        for (int i = 0; i < INPUT_LAYER; i++)
        {
            if((unsigned int)image[i] < 64)
                input_array[i] = 0;
            else
                input_array[i] = 1;
        }
        //设置目标值
        int index = (unsigned int)label[0];
        memset(aim_array, 0, sizeof(aim_array));
        aim_array[index] = 1;
        GetOutput1(); //得到隐含层输出
        GetOutput2(); //得到输出层输出
        GetDeviation2(); //得到输出层误差
        GetDeviation1(); //得到隐含层误差
        Feedback1(); //反馈输入层与隐含层之间的权重
        Feedback2(); //反馈隐含层与输出层之间的权重
        ++times;
        if(times % 2000 == 0)
            cout << "已训练 " << times << "组" << endl;
        if(times % 10000 == 0) //每10000组就测试一下
            Test();
    }
}
//测试
void BP::Test()
{
    FILE *test_images;
    FILE *test_labels;
    test_images = fopen("t10k-images.idx3-ubyte", "rb");
    test_labels = fopen("t10k-labels.idx1-ubyte", "rb");
    unsigned char image[784];
    unsigned char label[10];
    unsigned char temp[100];
    //读取文件开头
    fread(temp, 1, 16, test_images);
    fread(temp, 1, 8, test_labels);
    int total_times = 0; //当前测试了几次
    int success_times = 0; //当前正确了几次
    cout << "开始测试..." << endl;
    while (!feof(test_images) && !feof(test_labels))
    {
        fread(image, 1, INPUT_LAYER, test_images);
        fread(label, 1, 1, test_labels);
        //设置输入向量
        for (int i = 0; i < INPUT_LAYER; i++)
        {
            if ((unsigned int)image[i] < 64)
                input_array[i] = 0;
            else
                input_array[i] = 1;
        }
        //设置目标值
        memset(aim_array, 0, sizeof(aim_array));
        int index = (unsigned int)label[0];
        aim_array[index] = 1;
        GetOutput1(); //得到隐含层输出
        GetOutput2(); //得到输出层输出
        //以输出结果中最大的那个值所对应的数字作为预测的数字
        double maxn = -99999999;
        int max_index = 0;
        for (int i = 0; i < OUTPUT_LAYER; i++)
        {
            if (output2_array[i] > maxn)
            {
                maxn = output2_array[i];
                max_index = i;
            }
        }
        //如果预测正确
        if (aim_array[max_index] == 1)
            ++success_times;
        ++total_times;
        if(total_times % 2000 == 0)
            cout << "已测试:" << total_times << "组" << endl;
    }
    cout << "正确率: " << 100.0 * success_times / total_times << "%" << endl << endl;
    cout << "*************************" << endl << endl;
}
int main(int argc, char * argv[])
{
    BP bp;
    bp.Init();
    //训练数据反复利用TRAIN_TIMES次
    for(int i = 0; i < TRAIN_TIMES; i++)
    {
        cout << "开始第" << i + 1 << "轮迭代" << endl << endl;
        bp.Train();
    }
    return 0;
}

3.实验结果

不同的学习率得到准确率结果对比如下表(其中每一轮记录较好的准确率)

轮数\学习率0.30.20.1
第一轮92.4292.6690.54
第三轮93.4693.5393.19
第五轮94.0193.5394.11
第七轮94.6094.5794.20
第九轮94.8495.1394.49
第十轮95.0695.1694.59
总时间281s237.2s222.3s

三、动态学习率

1.调整程序

BP.h代码如下(示例):

#ifndef BP_H_INCLUDED
#define BP_H_INCLUDED
const int INPUT_LAYER = 784; //输入层维度
const int HIDDEN_LAYER = 40; //隐含层维度
const int OUTPUT_LAYER = 10; //输出层维度
const double LEARN_RATE = 0.3; // 初始学习率
const double LEARNING_RATE_DECAY = 0.95; // 学习率衰减率
const int TRAIN_TIMES = 10; //迭代训练次数
class BP
{
private:
    int input_array[INPUT_LAYER]; //输入向量
    int aim_array[OUTPUT_LAYER]; //目标结果
    double weight1_array[INPUT_LAYER][HIDDEN_LAYER]; //输入层与隐含层之间的权重
    double weight2_array[HIDDEN_LAYER][OUTPUT_LAYER]; //隐含层与输出层之间的权重
    double output1_array[HIDDEN_LAYER]; //隐含层输出
    double output2_array[OUTPUT_LAYER]; //输出层输出
    double deviation1_array[HIDDEN_LAYER]; //隐含层误差
    double deviation2_array[OUTPUT_LAYER]; //输出层误差
    double threshold1_array[HIDDEN_LAYER]; //隐含层阈值
    double threshold2_array[OUTPUT_LAYER]; //输出层阈值
public:
    void Init(); //初始化各参数
    double Sigmoid(double x); //sigmoid激活函数
    void GetOutput1(); //得到隐含层输出
    void GetOutput2(); //得到输出层输出
    void GetDeviation1(); //得到隐含层误差
    void GetDeviation2(); //得到输出层误差
    void Feedback1(); //反馈输入层与隐含层之间的权重
    void Feedback2(); //反馈隐含层与输出层之间的权重
    void Train(); //训练
    void Test(); //测试
};
#endif // BP_H_INCLUDED

BP.cpp代码如下(示例):

#include <bits/stdc++.h>
#include "BP.h"
using namespace std;

//初始化各参数
void BP::Init()
{
    srand(time(NULL));
    for (int i = 0; i < INPUT_LAYER; i++)
        for (int j = 0; j < HIDDEN_LAYER; j++)
            weight1_array[i][j] = rand()/(double)(RAND_MAX) * 2 - 1;
    for (int i = 0; i < HIDDEN_LAYER; i++)
        for (int j = 0; j < OUTPUT_LAYER; j++)
            weight2_array[i][j] = rand()/(double)(RAND_MAX) * 2 - 1;
    for (int i = 0; i < HIDDEN_LAYER; i++)
        threshold1_array[i] = rand()/(double)(RAND_MAX) * 2 - 1;
    for (int i = 0; i < OUTPUT_LAYER; i++)
        threshold2_array[i] = rand()/(double)(RAND_MAX) * 2 - 1;
}
//sigmoid激活函数
double BP::Sigmoid(double x)
{
    return 1.0 / (1.0 + exp(-x));
}
//得到隐含层输出
void BP::GetOutput1()
{
    for (int j = 0; j < HIDDEN_LAYER; j++)
    {
        double total = threshold1_array[j];
        for (int i = 0; i < INPUT_LAYER; i++)
            total += input_array[i] * weight1_array[i][j];
        output1_array[j] = Sigmoid(total);
    }
}
//得到输出层输出
void BP::GetOutput2()
{
    for (int j = 0; j < OUTPUT_LAYER; j++)
    {
        double total = threshold2_array[j];
        for (int i = 0; i < HIDDEN_LAYER; i++)
            total += output1_array[i] * weight2_array[i][j];
        output2_array[j] = Sigmoid(total);
    }
}
//得到隐含层误差
void BP::GetDeviation1()
{
    for (int i = 0; i < HIDDEN_LAYER; i++)
    {
        double total = 0;
        for (int j = 0; j < OUTPUT_LAYER; j++)
            total += weight2_array[i][j] * deviation2_array[j];
        deviation1_array[i] = (output1_array[i]) * (1.0 - output1_array[i]) * total;
    }
}
//得到输出层误差
void BP::GetDeviation2()
{
    for (int i = 0; i < OUTPUT_LAYER; i++)
        deviation2_array[i] = (output2_array[i]) * (1.0 - output2_array[i]) * (output2_array[i] - aim_array[i]);
}
//反馈输入层与隐含层之间的权重
void BP::Feedback1()
{
    for (int j = 0; j < HIDDEN_LAYER; j++)
    {
        threshold1_array[j] -= LEARN_RATE * deviation1_array[j];
        for (int i = 0; i < INPUT_LAYER; i++)
            weight1_array[i][j] = weight1_array[i][j] - LEARN_RATE * input_array[i] * deviation1_array[j];
    }
}
//反馈隐含层与输出层之间的权重
void BP::Feedback2()
{
    for (int j = 0; j < OUTPUT_LAYER; j++)
    {
        threshold2_array[j] = threshold2_array[j] - LEARN_RATE * deviation2_array[j];
        for (int i = 0; i < HIDDEN_LAYER; i++)
            weight2_array[i][j] = weight2_array[i][j] - LEARN_RATE * output1_array[i] * deviation2_array[j];
    }
}
//训练
void BP::Train()
{
	double current_learn_rate = LEARN_RATE; // 当前学习率
    FILE *train_images;
    FILE *train_labels;
    train_images = fopen("train-images.idx3-ubyte", "rb");
    train_labels = fopen("train-labels.idx1-ubyte", "rb");
    unsigned char image[INPUT_LAYER];
    unsigned char label[OUTPUT_LAYER];
    unsigned char temp[100];
    //读取文件开头
    fread(temp, 1, 16, train_images);
    fread(temp, 1, 8, train_labels);
    int times = 0; //当前训练了几次
    cout << "开始训练..." << endl << endl;
    while (!feof(train_images) && !feof(train_labels))
    {
        fread(image, 1, INPUT_LAYER, train_images);
        fread(label, 1, 1, train_labels);
        //设置输入向量
        for (int i = 0; i < INPUT_LAYER; i++)
        {
            if((unsigned int)image[i] < 64)
                input_array[i] = 0;
            else
                input_array[i] = 1;
        }
        //设置目标值
        int index = (unsigned int)label[0];
        memset(aim_array, 0, sizeof(aim_array));
        aim_array[index] = 1;
        GetOutput1(); //得到隐含层输出
        GetOutput2(); //得到输出层输出
        GetDeviation2(); //得到输出层误差
        GetDeviation1(); //得到隐含层误差
        Feedback1(); //反馈输入层与隐含层之间的权重
        Feedback2(); //反馈隐含层与输出层之间的权重
        ++times;
        if(times % 2000 == 0)
            cout << "已训练 " << times << "组" << endl;
        if(times % 10000 == 0) //每10000组就测试一下
            Test();
        current_learn_rate *= LEARNING_RATE_DECAY;// 在每个训练步骤后逐渐减小学习率
    }
}
//测试
void BP::Test()
{
    FILE *test_images;
    FILE *test_labels;
    test_images = fopen("t10k-images.idx3-ubyte", "rb");
    test_labels = fopen("t10k-labels.idx1-ubyte", "rb");
    unsigned char image[784];
    unsigned char label[10];
    unsigned char temp[100];
    //读取文件开头
    fread(temp, 1, 16, test_images);
    fread(temp, 1, 8, test_labels);
    int total_times = 0; //当前测试了几次
    int success_times = 0; //当前正确了几次
    cout << "开始测试..." << endl;
    while (!feof(test_images) && !feof(test_labels))
    {
        fread(image, 1, INPUT_LAYER, test_images);
        fread(label, 1, 1, test_labels);
        //设置输入向量
        for (int i = 0; i < INPUT_LAYER; i++)
        {
            if ((unsigned int)image[i] < 64)
                input_array[i] = 0;
            else
                input_array[i] = 1;
        }
        //设置目标值
        memset(aim_array, 0, sizeof(aim_array));
        int index = (unsigned int)label[0];
        aim_array[index] = 1;
        GetOutput1(); //得到隐含层输出
        GetOutput2(); //得到输出层输出
        //以输出结果中最大的那个值所对应的数字作为预测的数字
        double maxn = -99999999;
        int max_index = 0;
        for (int i = 0; i < OUTPUT_LAYER; i++)
        {
            if (output2_array[i] > maxn)
            {
                maxn = output2_array[i];
                max_index = i;
            }
        }
        //如果预测正确
        if (aim_array[max_index] == 1)
            ++success_times;
        ++total_times;
        if(total_times % 2000 == 0)
            cout << "已测试:" << total_times << "组" << endl;
    }
    cout << "正确率: " << 100.0 * success_times / total_times << "%" << endl << endl;
    cout << "*************************" << endl << endl;
}
int main(int argc, char * argv[])
{
    BP bp;
    bp.Init();
    //训练数据反复利用TRAIN_TIMES次
    for(int i = 0; i < TRAIN_TIMES; i++)
    {
        cout << "开始第" << i + 1 << "轮迭代" << endl << endl;
        bp.Train();
    }
    return 0;
}

2.实验结果

以下表中是每一轮较好的准确率,总时间247s

轮数准确率
第一轮91.91
第三轮93.97
第五轮94.53
第七轮95.10
第九轮95.18
第十轮95.17

附录

本文参考:手动搭建BP神经网络

  • 3
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
C语言作为一种高级编程语言,可用于编写BP神经网络的实现。BP神经网络是一种常见的人工神经网络模型,用于解决分类和回归问题。 在C语言中,我们可以定义和实现神经网络的各个组件,包括神经元、层级结构、权重和偏差等。首先,我们需要定义神经元结构,其中包括输入、输出、权重和偏差等参数。然后,在每个层级中,我们定义并连接神经元,形成神经网络。在网络的前向传播过程中,我们通过计算神经元的输出来传递信号。接下来,我们使用反向传播算法来调整权重和偏差,以逐步优化网络的性能。 对于C语言编写BP神经网络,我们可以使用数组和结构体等数据结构进行参数的定义和存储。我们可以利用循环和条件语句等控制结构实现网络的结构和算法。此外,C语言提供了丰富的数学函数库,用于实现神经网络的激活函数、损失函数以及训练和测试过程中的数学运算。 然而,需要注意的是,C语言的处理速度相对较快,但编写BP神经网络的代码可能相对复杂,需要对神经网络的原理和算法有一定的理解。此外,尽管C语言具有高效性,但它相对于其他更高级的编程语言来说,可能需要更多手动的内存管理和错误处理。 总而言之,利用C语言编写BP神经网络需要对神经网络的原理和C语言的编程知识有一定的了解。合理的数据结构和算法设计,以及对数学函数库的使用,可以帮助我们实现高效且可靠的BP神经网络

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值