C++神经网络预测模型

参考链接:AI基础教程 / 神经网络原理简明教程(来自github)

一、前言

C++课程设计中有用到神经网络预测模型,网上参考代码很多,但是大部分无法运行或与我的目标不一致。所以我重新写了一个能用的,下面提供完整的C++工程。
可以进入顶部参考链接了解详细原理和公式推导过程。参考链接使用Python语言,由于课程设计要求,我按照源码思路用C++重写了一遍,包括一个前馈神经网络和一个带时间步长的神经网络(简单的RNN),如下图。
实验发现,普通的前馈网络预测效果较差,RNN更适合用于与时间相关的数据。
普通前馈:
在这里插入图片描述
简单的RNN:
在这里插入图片描述

二、目标

已知25天的销售量数据,预测未来几天的销售量。
数据如下:

X特征(第n天):
double x[25] = {1, 2, 3, …, 24, 25}

Y标签(第n天的销售量):
double y[25] = {300, 320, 340, 360, 380, 415, 423, 430, 394, 350, 340, 320,320, 340, 380, 360, 380, 410, 420, 430, 394, 350, 340, 300, 320};

三、源码

请按照规定创建文件和类,在vs2019中可以直接运行。
提示:训练过程中出现“特别大的loss值”是偶然现象,请多次训练以获取最优值。
提供两种模型,请仔细查看注释
首先得保证你的ide能运行C++工程,然后按标题名创建3个文件并复制代码,编译运行即可。

3.1 main.cpp
#include <iostream>
#include <time.h>
#include "Network.h"

using namespace std;
int main()
{
        /**
         * 可以直接运行程序
         * 训练举例,偶尔会训练失败出现nan等错误,再次训练即可
         * 两种网络使用方式请根据[1] [2]提示进行选择
         * 默认为RNN训练
         */
        double y[25] = {300, 320, 340, 360, 380, 415, 423, 430, 394, 350, 340, 320, 320, 340, 380,
        360, 380, 410, 420, 430, 394, 350, 340, 300, 320};
        double x[25];
        for (int i = 0; i < 25; i++)
        {
                x[i] = double(i) + 1.0;
        }
 
        Network n(5, 25);     /*< 神经元数量=5, 训练数据量=25*/
        n.readTrainData(x, y); /*< 读取x, y*/

        //[1]
        /*用RNN训练*/
        n.RNNTrain(10000);    /*< 用RNN训练10000次*/
        n.RNNShowResult();   /*< 输出训练结果,将原数据和训练结果复制到excel中绘图进行对比*/
        /*end 用RNN训练*/


        //[2]
        /*用普通网络训练,请注释上面的RNN训练部分*/

 //       n.train(5000);  /*< 自动输出结果*/

        /*end 用普通网络训练*/
}
}
3.2 Network.h
#pragma once

class Network
{
private:
	double* w1;	/*< 权重w1*/
	double* w2;	/*< 权重w2*/
	double* b1;	/*< 偏移量b1*/
	double b2;	/*< 偏移量b2*/
	double* gw1;	/*< 权重w1梯度*/
	double* gw2;	/*< 权重w2梯度*/
	double* gb1;	/*< 偏移量b1梯度*/
	double gb2;	/*< 偏移量b2梯度*/
	double* trainX;	/*< 待训练的数据*/
	double* trainY;
	double output;	/*< 输出值*/
	
	double* z1;
	double* a1;
	double eta;   /*< 更新效率*/
	int lenData;  /*< 数据长度*/
	int lenWeight; /*< 权重数量*/
	double* saveZ;
	double* SAOutput;

	double* normalParamX; /*< 归一化参数,用于恢复数据*/
	double* normalParamY;

	/*RNN-Param*/
	double* W;
	double* U;
	double* V;
	double* bH;
	double bZ;


	double* h1;
	double* s1;
	double x1;
	double gz1;
	double* gh1;
	double* gbh1;
	double* gU1;

	double* h2;
	double* s2;
	double x2;
	double z2;
	double gz2;
	double gbZ;
	double* gh2;
	double* gbh2;
	double* gV2;
	double* gU2;
	double* gW;


public:
	Network(int numOfWeights, int numOfData);
	~Network();
	void forward(double x);		/*< 向前计算*/
	void backward(double x, double y);	/*< 向后计算*/
	void readTrainData(double* dataX, double* dataY);	/*< 读取数据,并做归一化处理*/
	void normalize(double* normalParam, double* data);
	void train(int iterTime);
	void update();
	void randomParam();
	void shuffle(double* a, double* b);
	void showResult();

	/*-----------------RNN-----------------*/
	void forward1(double x1, double* U, double* V, double* W, double* bh );
	void forward2(double x2, double* U, double* V, double* W, double* bh, double bz, double* s1);
	void backward2(double y, double* s1);
	void backward1(double y, double* gh2);

	void RNNTrain(int maxEpoch);
	void RNNUpdate();
	void RNNInitParam();
	void RNNShowResult();
	
	bool RNNCheckLoss(double x1, double x2, double y, int totalIterTime);
	/*--------------end RNN-----------------*/


	double test(double x);
	double loss(double* z, double* y); /*< 损失函数*/
	double singleLoss(double z, double y);
	double sigmoid(double x);
	double dSigmoid(double x);
	double resData(double normalData, double differMaxMin, double avgRes); /*< 复原数据*/

	double dTanh(double y); /*< tanh求导*/

	bool checkErrorAndLoss(double x, double y, int totalIterTime);
};


3.3 Network.cpp
#include "Network.h"
#include <algorithm>
#include <iostream>
#include <time.h> 
#include <math.h>


/**
* @brief 初始化神经网络
* @param 随机产生权重的数量
* @detail 随机产生初始权重
*/
using namespace std;
Network::Network( int numOfWeights, int numOfData)
{
	//b1 = b2 = 0.0;
	eta = 0.01;
	lenData = numOfData;
	lenWeight = numOfWeights; /*< 权重数量等于神经元数量*/
	w1 = new double[lenWeight];
	w2 = new double[lenWeight];
	b1 = new double[lenWeight];
	output = 0.0;
	trainX = new double[lenData];
	trainY = new double[lenData];
	z1 = new double[lenWeight];
	a1 = new double[lenWeight];
	gw1 = new double[lenWeight];
	gw2 = new double[lenWeight];
	gb1 = new double[lenWeight];
	gb2 = 0.0;
	
	SAOutput = new double[lenWeight];
	saveZ = new double[lenData];
	srand((unsigned)time(NULL));
	randomParam();

	normalParamX = new double[2];
	normalParamY = new double[2];


	/*----------------RNN Init---------------*/
	W = new double[lenWeight];
	U = new double[lenWeight];
	V = new double[lenWeight];
	bH = new double[lenWeight];
	bZ = 0.0;

	h1 = new double[lenWeight];
	s1 = new double[lenWeight];
	x1 = 0.0;
	gz1 = 0.0;
	gh1 = new double[lenWeight];
	gbh1 = new double[lenWeight];
	gU1 = new double[lenWeight];

	h2 = new double[lenWeight];
	s2 = new double[lenWeight];
	x2 = 0.0;
	z2 = 0.0;
	gz2 = 0.0;
	gbZ = 0.0;
	gh2 = new double[lenWeight];
	gbh2 = new double[lenWeight];
	gV2 = new double[lenWeight];
	gU2 = new double[lenWeight];
	gW = new double[lenWeight];

	RNNInitParam();
	/*-------------end RNN Init-------------*/

	cout << "初始化完成;" << endl;
}


Network::~Network()
{
	delete[]w1;
	delete[]w2;
	delete[]b1;
	delete[]trainX;
	delete[]trainY;
	delete[]z1;
	delete[]a1;
	delete[]gw1;
	delete[]gw2;
	delete[]gb1;
	delete[]SAOutput;
	delete[]saveZ;

	delete normalParamX;
	delete normalParamY;


	/*------RNN Delete---------*/
	delete[]W;
	delete[]U;
	delete[]V;
	delete[]bH;
	delete[]h1;
	delete[]s1;
	delete[]gh1;
	delete[]gbh1;
	delete[]gU1;
	delete[]h2;
	delete[]s2;
	delete[]gh2;
	delete[]gbh2;
	delete[]gV2;
	delete[]gU2;
	delete[]gW;
	/*------ end RNN Delete----*/

	cout << "end" << endl;
}



/**
* 向前计算
*/
void Network::forward(double x)
{
	//const int m = lenWeight;
	
	/*向前计算*/
	//std::cout << "开始向前计算。。。output:" << std::endl;
	double z2;
	z2 = 0.0;
	for (int i = 0; i < lenWeight; i++)
	{
		z1[i] = x * w1[i] + b1[i];
		a1[i] = sigmoid(z1[i]);
		z2 += a1[i] * w2[i];
	}
	z2 += b2;
	output = z2;
	//std::cout << output << std::endl;
	//cout << "向前计算完毕;" << endl;
}


/**
* 向后计算
*/
void Network::backward(double x, double y)
{
	//cout << "开始向后计算。。。gb1:" << endl;
	
	double* gz1 = new double[lenWeight];
	double* ga1 = new double[lenWeight];
	double gz2 =  output - y; /*< 第二层梯度输入*/
	//cout << y << endl;
	for (int i = 0; i < lenWeight; i++)
	{
		gw2[i] = gz2 * a1[i];					/*< w2梯度*/
		ga1[i] = gz2 * w2[i];					/*< a1偏导*/
		gz1[i] = ga1[i] * dSigmoid(a1[i]);	        /*< 第一层梯度输入*/
		gw1[i] = x * gz1[i];					/*< w1梯度*/
		gb1[i] = gz1[i];						/*< gb1梯度*/
	}
	gb2 = gz2;						/*< b2梯度*/

	delete[]gz1;
	delete[]ga1;

	//cout << "向后计算完毕;" << endl;
}


/**
* 读取数据,并做归一化处理
*/
void Network::readTrainData(double* dataX, double* dataY)
{
	/*归一化数据*/
	normalize(normalParamX, dataX);
	normalize(normalParamY, dataY);

	for (int i = 0; i < lenData; i++)
	{
		trainX[i] = dataX[i];
		trainY[i] = dataY[i];
		//cout << trainX[i] << endl;
		//cout << trainY[i] << endl;
	}
}

void Network::normalize(double* normalParam, double* data)
{
	double maximum;
	double minimum;
	double avg;
	maximum = *std::max_element(data, data + lenData);
	minimum = *std::min_element(data, data + lenData);
	avg = data[0];
	for (int i = 1; i < lenData; i++)
	{
		avg += data[i];
	}
	avg = avg / lenData;
	
	for (int i = 0; i < lenData; i++)
	{
		data[i] = (data[i] - avg) / (maximum - minimum);
	}

	normalParam[0] = avg;
	normalParam[1] = (maximum - minimum);
}


/**
* 开始训练
*/
void Network::train(int iterTime)
{	
	double x;
	double y;

	int totalIterTime;

	bool needStop;

	for (int i = 0; i < iterTime; i++)
	{
		shuffle(trainX, trainY);
		for (int m = 0; m < lenData; m++)
		{
			x = trainX[m];
			y = trainY[m];
			forward(x);
			backward(x, y);
			update(); /*< 更新权重和偏移值*/
			totalIterTime = i * lenData + m;
			if (totalIterTime % 50 == 0)
			{
				needStop = checkErrorAndLoss(x, y, totalIterTime);
				if (needStop)
				{
					break;
				} /*< end if*/
			} /*< end if*/
		} /*< end for*/

		if (needStop)
		{
			break;
		} /*< end if*/

	} /*< end for*/
	for (int i = 0; i < lenData; i++)
	{
		forward(trainX[i]);
		cout << trainX[i] << "===" << trainY[i] << "=====" << output << endl;
	}
	cout << "w1--" << w1[0] << endl;
	cout << "w2--" << w2[0] << endl;
	cout << "b1--" << b1[0] << endl;
	cout << "b2--" << b2 << endl;
	checkErrorAndLoss(0.45, 0.45, totalIterTime);
}

void Network::update()
{
	for (int i = 0; i < lenWeight; i++)
	{
		w1[i] = w1[i] - gw1[i] * eta;
		w2[i] = w2[i] - gw2[i] * eta;
		b1[i] = b1[i] - gb1[i] * eta;
	}
	b2 = b2 - gb2 * eta;
}


/*产生随机参数*/
void Network::randomParam()
{
	for (int i = 0; i < lenWeight; i++)
	{
		w1[i] = double((rand() % 100)) / 100;
		w2[i] = double((rand() % 100)) / 100;
		b1[i] = double((rand() % 100)) / 100;
	}
	b2 = double((rand() % 100)) / 100;
}



void Network::shuffle(double* a, double* b)
{
	int len = lenData; // 全集元素数量
	srand(unsigned(time(NULL))); // 摇号机准备
	for (int i = len; i > 1; i--) // 从全集开始摇号,直至只剩一个,已排在最后
	{
		int cur = len - i + (rand() % i); // 在剩余集合中摇号
		double tmpa = a[len - i]; // 当前序号位置挪空
		double tmpb = b[len - i];
		a[len - i] = a[cur]; // 摇出的彩球放入当前序号
		b[len - i] = b[cur];
		a[cur] = tmpa; // 放好剩余的彩球
		b[cur] = tmpb;
	}
}

void Network::showResult()
{
	//cout << "gb2==" << gb2 << endl;
	//cout << "output==" << output << endl;
	/*cout << "w1==" << w1[0] << endl;
	cout << "w2==" << w2[0] << endl;
	cout << "b1==" << b1[0] << endl;
	cout << "b2==" << b2 << endl;*/
	//cout << "gw2==" << gw2[0] << endl;

}

void Network::forward1(double x1, double* u, double* v, double* w, double* bh)
{
	for (int i = 0; i < lenWeight; i++)
	{
		this->W[i] = w[i];
		this->h1[i] = x1 * u[i] + bh[i];
		this->s1[i] = tanh(this->h1[i]);
	}
	this->x1 = x1;
	
}

void Network::forward2(double x2, double* u, double* v, double* w, double* bh, double bz, double* s1)
{
	this->x2 = x2;
	this->z2 = 0.0;
	for (int i = 0; i < lenWeight; i++)
	{
		this->V[i] = v[i];
		this->h2[i] = x2 * u[i] + s1[i] * w[i] + bh[i];
		this->s2[i] = tanh(this->h2[i]);
		this->z2+= this->s2[i] * this->V[i];
	}
	this->z2 += bz;
}

void Network::backward2(double y, double* s1)
{
	this->gz2 = this->z2 - y;
	this->gbZ = this->gz2;
	for (int i = 0; i < lenWeight; i++)
	{
		this->gh2[i] = (this->gz2) * (this->V[i]) * dTanh(this->s2[i]);
		this->gbh2[i] = this->gh2[i];
		this->gV2[i] = this->s2[i] * this->gz2;
		this->gU2[i] = this->x2 * this->gh2[i];
		this->gW[i] = s1[i] * this->gh2[i];
	}
}

void Network::backward1(double y, double* gh2)
{
	for (int i = 0; i < lenWeight; i++)
	{
		this->gh1[i] = gh2[i] * this->W[i] * dTanh(this->s1[i]);
		this->gbh1[i] = this->gh1[i];
		this->gU1[i] = this->x1 * this->gh1[i];
	}
}

void Network::RNNTrain(int maxEpoch)
{
	double x1;
	double y1;
	double x2;
	double y2;

	int totalIterTime;
	bool needStop = false;

	for (int i = 0; i < maxEpoch; i++)
	{
		for (int j = 0; j < lenData; j++)
		{
			x1 = trainX[j];
			y1 = 0.0;
			x2 = trainX[j + 1];
			y2 = trainY[j + 1];
			forward1(x1, U, V, W, bH);
			forward2(x2, U, V, W, bH, bZ, s1);
			backward2(y2, s1);
			backward1(y1, gh2);
			RNNUpdate();
			//cout << gbh1[0] << endl;

			totalIterTime = i * lenData + j;
			if (totalIterTime % 500 == 0)
			{
				needStop = RNNCheckLoss(x1, x2, trainY[j], totalIterTime);
				if (needStop)
				{
					break;
				} /*< end if*/
			} /*< end if*/
		}
	}
}

void Network::RNNUpdate()
{
	for (int i = 0; i < lenWeight; i++)
	{
		U[i] = U[i] - (gU1[i] + gU2[i]) * eta;
		V[i] = V[i] - (gV2[i]) * eta;
		W[i] = W[i] - gW[i] * eta;
		bH[i] = bH[i] - (gbh1[i] + gbh2[i]) * eta;
	}
	bZ = bZ - gbZ * eta;
	
}

void Network::RNNInitParam()
{
	for (int i = 0; i < lenWeight; i++)
	{
		U[i] = double((rand() % 100)) / 100;
		V[i] = double((rand() % 100)) / 100;
		W[i] = double((rand() % 100)) / 100;
		bH[i] = double((rand() % 100)) / 100;
	}
	bZ = double((rand() % 100)) / 100;
}

void Network::RNNShowResult()
{
	cout << "---------- RNN - Param -----------" << endl;
	for (int i = 0; i < lenWeight; i++)
	{
		cout << "U" << i << ":    " << U[i] << endl;
		cout << "V" << i << ":    " << V[i] << endl;
		cout << "W" << i << ":    " << W[i] << endl;
		cout << "bh" << i << ":    " << bH[i] << endl;
	}
	cout << "bz" << ":    " << bZ << endl;
	cout << "-------- END RNN - Param --------" << endl;
	cout << endl;

	cout << endl;
	cout << "---------- RNN - Test -----------" << endl;
	double* vldOutput = new double[lenData];
	/*验证*/
	vldOutput[0] = trainY[0];
	for (int i = 0; i < lenData - 1; i++)
	{
		forward1(trainX[i], U, V, W, bH);
		forward2(trainX[i + 1], U, V, W, bH, bZ, s1);
		vldOutput[i + 1] = z2;
		//cout << "testX: " << trainX[i + 1] << "testY: " << trainY[i + 1] << "--------outputY: " << vldOutput[i + 1] << endl;
		
		cout << resData(vldOutput[i + 1], normalParamY[1], normalParamY[0]) << endl;
	}
	cout << "-------- END RNN - Test --------" << endl;
}

bool Network::RNNCheckLoss(double X1, double X2, double y, int totalIterTime)
{
	double lossTrain;
	double lossVld;
	bool     needStop;
	double* vldOutput = new double[lenData];
	cout << "iterTime= " << totalIterTime << endl;
	forward1(X1, U, V, W, bH);
	forward2(X2, U, V, W, bH, bZ, s1);
	lossTrain = singleLoss(z2, y);
	cout << "lossTrain= " << lossTrain << endl;

	/*验证*/
	vldOutput[0] = trainY[0];
	for (int i = 0; i < lenData-1; i++)
	{
		forward1(trainX[i], U, V, W, bH);
		forward2(trainX[i+1], U, V, W, bH, bZ, s1);
		vldOutput[i+1] = z2;
	}
	lossVld = loss(vldOutput, trainY);
	cout << "lossValid= " << lossVld << endl;

	if (lossVld <= 0.000001)
	{
		needStop = true;
	}
	else
	{
		needStop = false;
	}
	return needStop;
	return false;
}

double Network::test(double x)
{
	double z = 0.0;
	double data;
	double avgX = normalParamX[0];
	double differMaxMinX = normalParamX[1];
	double avgY = normalParamY[0];
	double differMaxMinY = normalParamY[1];

	data  = (x - avgX) / differMaxMinX;
	forward(data);
	z = resData(output, differMaxMinY, avgY);

	return z;
}



double Network::loss(double* z, double* y)
{
	double ls;
	int n;

	ls = 0;
	n = lenData;
	
	for (int i = 0; i < n; i++)
	{
		ls += (z[i] - y[i]) * (z[i] - y[i]);
	}
	ls = (0.5 / n) * ls;
	return ls;
}

double Network::singleLoss(double z, double y)
{
	double ls;
	ls = (z - y) * (z - y) * 0.5;
	return ls;
}


double Network::sigmoid(double x)
{
	double s;
	s = 1.0/(exp(-x)+1.0);
	return s;
}


/*sigmoid导数*/
double Network::dSigmoid(double x)
{
	double s;
	s = x * (1.0 - x);
	return s;
}


double Network::resData(double normalData, double differMaxMin, double avgRes)
{
	double data;
	data = normalData * differMaxMin + avgRes;
	return data;
}

double Network::dTanh(double y)
{
	return (1 - y * y);
}

bool Network::checkErrorAndLoss(double x, double y, int totalIterTime)
{
	double lossTrain;
	double lossVld;
	bool     needStop;
	double* vldOutput = new double[lenData];
	cout << "iterTime= " << totalIterTime << endl;
	forward(x);
	lossTrain = singleLoss(output, y);
	cout << "lossTrain= " << lossTrain << endl;

	/*验证*/
	for (int i = 0; i < lenData; i++)
	{
		forward(trainX[i]);
		vldOutput[i] = output;
	}
	lossVld = loss(vldOutput, trainY);
	cout << "lossValid= " << lossVld << endl;
	
	if (lossVld <= 0.000001)
	{
		needStop = true;
	}
	else
	{
		needStop = false;
	}
	return needStop;
}


VS部分,完。








四、结尾备注:

1 在VS2019 社区版中测试正常运行,并输出结果。
2 关于(模型)参数保存:

建议保存为json格式,原项目是用Qt做的,所以下面提供有关参数保存和读取的Qt源码,理解思路就好

#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>

/**
 * @brief 从已有的json文件中读取训练完成的参数
 */
void RNN::readParam()
{
     //打开文件
     QFile file("rnn_param.json");
     if(!file.open(QIODevice::ReadOnly)) {
         qDebug() << "File open failed!";
     } else {
         qDebug() <<"File open successfully!";
     }
     QJsonParseError *error=new QJsonParseError;
     QJsonDocument jdc=QJsonDocument::fromJson(file.readAll(),error);

     //判断文件是否完整
     if(error->error!=QJsonParseError::NoError)
     {
       qDebug()<<"parseJson:"<<error->errorString();
     }


     QJsonObject obj = jdc.object();        //获取对象
//     qDebug() <<"object size:"<<obj.size();

     lenData = obj["lenData"].toInt();
     lenWeight = obj["lenWeight"].toInt();
     trainY[0] = obj["trainY0"].toDouble();

     QJsonArray arr =  obj["WeightsBias"].toArray();
     for (int i=0; i<lenWeight; i++)
     {
         this->U[i] = arr[i].toObject()["U"].toDouble();
         this->V[i] = arr[i].toObject()["V"].toDouble();
         this->W[i] = arr[i].toObject()["W"].toDouble();
         this->bH[i] = arr[i].toObject()["bH"].toDouble();
     }
     this->bZ = arr[0].toObject()["bZ"].toDouble();
//     qDebug() << bZ;
     normalParamX[0] = obj["AverageX"].toDouble();
     normalParamX[1] = obj["differMaxMinX"].toDouble();
     normalParamY[0] = obj["AverageY"].toDouble();
     normalParamY[1] = obj["differMaxMinY"].toDouble();

     file.close();
}


/**
 * @brief 将训练完成的参数保存为json格式的文件
 */
void RNN::saveParam()
{
    //打开文件
    QFile file("rnn_param.json");
    if(!file.open(QIODevice::WriteOnly)) {
        qDebug() << "File open failed!";
    } else {
        qDebug() <<"File open successfully!";
    }
    file.resize(0);

    QJsonDocument jdoc;
    QJsonObject obj;
    QJsonArray WeightsBias;

    for(int i=0;i<lenWeight;i++)
    {
        QJsonObject trainParam;     //定义数组成员
        trainParam["U"] = U[i];
        trainParam["V"] = V[i];
        trainParam["W"] = W[i];
        trainParam["bH"] = bH[i];
        trainParam["bZ"] = bZ;
        WeightsBias.append(trainParam);
    }

    obj["WeightsBias"] = WeightsBias;
    obj["AverageX"] = normalParamX[0];
    obj["differMaxMinX"] = normalParamX[1];
    obj["AverageY"] = normalParamY[0];
    obj["differMaxMinY"] = normalParamY[1];
    obj["lenData"] = lenData;
    obj["lenWeight"] = lenWeight;
    obj["trainY0"] = trainY[0];


    jdoc.setObject(obj);
    file.write(jdoc.toJson(QJsonDocument::Indented)); //Indented:表示自动添加/n回车符
    file.close();
}
3 Qt测试结果

保存的参数文件:
在这里插入图片描述

训练预测可视化界面
在这里插入图片描述

提示:完整项目已经开源:
C++ QT Creater 车票信息管理及预测系统

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值