【机器学习实战】-Logistic回归

【机器学习实战】-Logistic回归


假设现在有一些数据点,我们用一条直线对这些点进行拟合(该线称为最佳拟合直线),这个拟合过程就称作回归。Logistic回归进行分类的主要思想是:根据现有数据对分类边界线建立回归公式,以此进行分类。这里的“回归”一词源于最佳拟合,表示要找到最佳拟合参数集,训练分类器时的做法就是寻找最佳拟合参数,使用的是最优化算法。

                        Logistic回归的一般过程 
(1) 收集数据:采用任意方法收集数据。 
(2) 准备数据:由于需要进行距离计算,因此要求数据类型为数值型。另外,结构化数据
格式则最佳。 
(3) 分析数据:采用任意方法对数据进行分析。 
(4) 训练算法:大部分时间将用于训练,训练的目的是为了找到最佳的分类回归系数。 
(5) 测试算法:一旦训练步骤完成,分类将会很快。 
(6) 使用算法:首先,我们需要输入一些数据,并将其转换成对应的结构化数值;
	接着,基于训练好的回归系数就可以对这些数值进行简单的回归计算,判定它们属于哪个类别;
	在这之后,我们就可以在输出的类别上做一些其他分析工作

1.基于Logistic回归和Sigmoid函数的分类

					         Logistic回归 
					**优点:**计算代价不高,易于理解和实现。
					**缺点:**容易欠拟合,分类精度可能不高。
					**适用数据类型:**数值型和标称型数据。

想要的函数是接收所有输入然后预测类别,例如:在两个类的情况下输出0或1。海威塞德阶跃函数(单位阶跃函数)在跳跃点上从0瞬间跳跃到1,瞬间跳跃过程有时很难处理。Sigmoid函数也有类似的性质输出为0或1。Sigmoid函数具体计算公式如下:
σ ( z ) = 1 1 + e − z \sigma(z)=\frac{1}{1+e^{-z}} σ(z)=1+ez1
当x为0时,Sigmoid函数值为0.5。
随着x的增大,对应的Sigmoid值将逼近于1;而随着x的减小,Sigmoid值将逼近于0。如果横坐标
刻度足够大,Sigmoid函数看起来很像一个阶跃函数。
在这里插入图片描述
Sigmoid函数是一种阶跃函数(step function)。在数学中,如果实数域上的某个函数可以用半开区间上的指示函数的有限次线性组合来表示,那么这个函数就是阶跃函数。而数学中指示函数(indicator function)是定义在某集合X上的函数,表示其中有哪些元素属于某一子集A。
在每个特征上都乘以一个回归系数,然后把所有的结果值相加,将这个总和代入Sigmoid函数中,进而得到一个范围在0~1之间的数值。任何大于0.5的数据被分入1类,小于0.5即被归入0类。

2.基于最优化方法的最佳回归系数确定

Sigmoid函数的输入记为z,由下面公式得出:
z = w 0 x 0 + w 1 x 1 + w 2 x 2 + ⋯ + w n x n z=w_0x_0+w_1x_1+w_2x_2+\dots+w_nx_n z=w0x0+w1x1+w2x2++wnxn
上述公式可以写成 z = w T x z=w^Tx z=wTx其中向量x是分类器的输入数据,向量w是我们要找的最佳参数(系数),使分类器尽可能准确。为了寻找最佳参数,需要最优化理论的知识。

2.1梯度上升法

基本思想:要找到某函数的最大值,最好的方法使沿着函数的梯度方向探寻。如果梯度记为 ▽ \bigtriangledown ,则函数f(x,y)的梯度由下式表示:
▽ f ( x , y ) = ⟮ ∂ f ( x , y ) ∂ x ∂ f ( x , y ) ∂ y ⟯ \bigtriangledown f(x,y)=\lgroup \begin{array}{c} \frac{\partial f(x,y)}{\partial x} \\ \frac{\partial f(x,y)}{\partial y} \\ \end{array} \rgroup f(x,y)=xf(x,y)yf(x,y)
这个梯度意味着沿x的方向移动 ∂ f ( x , y ) ∂ x \frac{\partial f(x,y)}{\partial x} xf(x,y),沿y的方向移动 ∂ f ( x , y ) ∂ y \frac{\partial f(x,y)}{\partial y} yf(x,y)。其中函数 f ( x , y ) f(x,y) f(x,y)必须要在待计算的点上有定义并且可微。

梯度上升算法到达每个点后都会重新估计移动的方向。从P0开始,计算完该点的梯度,函数就根据梯度移动到下一点P1。在P1点,梯度再次被重新计算,并沿新的梯度方向移动到P2。如此循环迭代,直到满足停止条件。迭代的过程中,梯度算子总是保证我们能选取到最佳的移动方向。

梯度说的是移动方向,未提到移动量的大小,该值称为步长,记为 α \alpha α,用向量表示,梯度上升算法的迭代公式如下:
w : = w + α ∇ w f ( w ) w:=w+\alpha\nabla_w f(w) w:=w+αwf(w),该公式一直被迭代执行,直至达到某个停止条件为止,比如迭代次数达到某个指定值或算法达到某个可以允许的误差范围内。

梯度下降算法: 你最经常听到的应该是梯度下降算法,它与这里的梯度上升算法是一样的,只是公式中的加法需要变成减法。因此,对应的公式可以写成
w : = w + α ∇ w f ( w ) w:=w+\alpha\nabla_w f(w) w:=w+αwf(w)
梯度上升算法用来求函数的最大值,而梯度下降算法用来求函数的最小值。
!需要注意的点: 梯度是指函数变化最快的方向,对于逻辑回归问题,误差函数的梯度是对每一个权重值求偏导得到的向量。这个向量的方向指示了权重值应该如何更新才能让误差最小化。在梯度上升算法中,我们需要通过不断更新权重来使误差函数最小化,要更新权重就需要计算梯度。

在书中代码上,对于逻辑回归问题而言,误差函数其实是一个对数似然函数。但是因为这个函数并没有一个简单的解析式,无法直接求导,所以在实现过程中,我们通过真实值与预测值之间的差异来逼近梯度。也就是说,我们通过最小化误差函数来找到权重的最优解,而误差函数的梯度是通过预测值与真实值的差异来计算的。

这种方法是梯度下降算法以及其变种的常见实现方式,也常用于其他机器学习和深度学习模型的训练中。因为这种方法可以避免对复杂的误差函数进行求导,从而使算法的实现变得更加简单和灵活。

2.2训练算法:使用梯度上升找到最佳参数

有100个样本点,每个点包含两个数值型特征:X1和X2。在此数据集上,我们将通过使用梯度上升法找到最佳回归系数,也就是拟合出Logistic回归模型的最佳参数。
梯度上升法的伪代码如下:

每个回归系数初始化为1 
重复R次:
 计算整个数据集的梯度
 使用alpha × gradient更新回归系数的向量
 返回回归系数

代码实现:

# 加载数据
def loadDataSet():
    dataMat = []; labelMat = []
    fr = open('testSet.txt')
    for line in fr.readlines():
        lineArr = line.strip().split()
        # 特征数据记录,dataMat[0]设置成1是偏置参数
        dataMat.append([1.0, float(lineArr[0]), float(lineArr[1])])
        # 标签值记录
        labelMat.append(int(lineArr[2]))
    return dataMat, labelMat

# sigmoid函数
def sigmoid(inX):
    return 1.0/(1 + np.exp(-inX))

def gradAscent(dataMatIn, classLabels):
    # 特征值
    dataMatrix = np.mat(dataMatIn)
    # 标签值转换为行向量
    labelMat = np.mat(classLabels).transpose()
    # m,特征向量行数,n,特征向量列数
    m, n = np.shape(dataMatrix)
    # 学习率,步长
    alpha = 0.001
    # 迭代次数
    maxCrycles = 500
    # 权重初始化为n行1列的数
    weights = np.ones((n, 1))
    # 梯度上升进行权值迭代
    for k in range(maxCrycles):
        # 预测数据
        h = sigmoid(dataMatrix*weights)
        # 真实数据与预测数据之间的差异
        error = (labelMat - h)
        # 更新权值
        weights = weights + alpha * dataMatrix.transpose() * error
    return weights

loadDataSet()函数是加载testSet.txt数据的函数,把dataMat[1]和dataMat[2]记录的是数据的特征,dataMat[3]则是数据的标签,把dataMat[0]设置成1是模拟偏置参数。
sigmoid()函数是按照前面的sigmoid原理设置的,输出预测值。
gradAscent()函数实现梯度上升,更新权值参数weights,这里面梯度是通过真实值与预测值之间的差异来逼近梯度,而不是求导得到梯度的。变量alpha是向目标移动的步长,maxCycles是迭代次数。在for循环迭代完成后,将返回训练好的回归系数,每一次迭代都是全体数据进行迭代。

2.3 分析数据:画出决策边界

代码及解释:

# 画出数据集和Logistic回归最佳拟合直线的函数
def plotBestFit(weights):
    # 读取数据
    dataMat, labelMat = loadDataSet()
    # 获得数据行数
    dataArr = np.array(dataMat)
    n = np.shape(dataArr)[0]
    # 记录标签为1的数据
    xcord1 = []; ycord1 = []
    # 记录标签为2的数据
    xcord2 = []; ycord2 = []
    # 画出属于两类的数据
    for i in range(n):
        if int(labelMat[i]) == 1:
            xcord1.append(dataArr[i, 1]); ycord1.append(dataArr[i, 2])
        else:
            xcord2.append(dataArr[i, 1]); ycord2.append(dataArr[i, 2])
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.scatter(xcord1, ycord1, s=30, c='red', marker='s')
    ax.scatter(xcord2, ycord2, s=30, c='green')
    # 计算分类边界
    x = np.arange(-3.0, 3.0, 0.1)
    # 公式推导得到边界的分类就是当sigmoid函数输入为0时
    y = (-weights[0]-weights[1]*x)/weights[2]
    ax.plot(x, y)
    plt.xlabel('X1'); plt.ylabel('X2');
    plt.show()

唯一要指出的是,设置了sigmoid函数为0。0是两个分类(类别1和类别0)的分界处。因此,我们设定 0 = w 0 x 0 + w 1 x 1 + w 2 x 2 0 = w_0x_0 + w_1x_1 + w_2x_2 0=w0x0+w1x1+w2x2,然后解出 X 2 X_2 X2 X 1 X_1 X1的关系式(即分隔线的方程,注意X0=1)。 公式推导过程:
y = ( − w e i g h t s [ 0 ] − w e i g h t s [ 1 ] ∗ x ) / w e i g h t s [ 2 ] y=(-weights[0]-weights[1]*x)/weights[2] y=(weights[0]weights[1]x)/weights[2]
上面公式是基于权重(weights)计算的。权重向量包含截距项(weights[0])和特征的系数(weights[1]和weights[2])。对于一条决策边界,公式 ( w e i g h t s [ 0 ] + w e i g h t s [ 1 ] ∗ x + w e i g h t s [ 2 ] ∗ y = 0 ) (weights[0]+ weights[1]*x+ weights[2]*y=0) (weights[0]+weights[1]x+weights[2]y=0)表示了边界上的点,但我们希望得到y关于x的函数形式。为了解决这个问题,可以将这个公式变形,分离出y,即可得到:
y = ( − w e i g h t s [ 0 ] − w e i g h t s [ 1 ] ∗ x ) / w e i g h t s [ 2 ] . y=(-weights[0]-weights[1]*x)/weights[2]. y=(weights[0]weights[1]x)/weights[2].
决策边界的斜率由权重向量中的weights[1]和weights[2]决定,而截距由weights[0]决定。决策边界的位置和形状取决于这些权重的值。
调用代码:

dataArr, labelMat = loadDataSet()
print(gradAscent(dataArr, labelMat))
# 画图验证
weights = gradAscent(dataArr, labelMat)
plotBestFit(weights.getA())

在这里插入图片描述
分类结果相当不错,从图上看只错分了两到四个点。但是,尽管例子简单且数据集很小,这个方法却需要大量的计算(300次乘法)。

2.4 训练算法:随机梯度

梯度上升算法每次计算回归系数时都需要遍历整个数据集,如果样本数和特征数上升,计算的复杂度就太高了。一种
改进方法是一次仅用一个样本点来更新回归系数,该方法称为随机梯度上升算法。由于可以在新样本到来时对分类器进行增量式更新,因而随机梯度上升算法是一个在线学习算法。与“在线学习”相对应,一次处理所有数据被称作是“批处理”。
随机梯度上升算法可以写成如下的伪代码:

所有回归系数初始化为1 
对数据集中每个样本 
 计算该样本的梯度 
 使用alpha × gradient更新回归系数值 
返回回归系数值 

随机梯度上升算法代码:

# 随机梯度上升算法
def stocGradAscent0(dataMatrix, classLabels):
    m, n = np.shape(dataMatrix)
    alpha = 0.01
    weights = np.ones(n)
    # 单个点进行梯度上升算法
    for i in range(m):
        h = sigmoid(sum(dataMatrix[i]*weights))
        error = classLabels[i] - h
        weights = weights + alpha * error * np.array(dataMatrix[i])
    return weights

随机梯度上升算法与梯度上升算法在代码上很相似,但也有一些区别:第一,后者的变量h和误差error都是向量,而前者则全是数值;第二,前者没有矩阵的转换过程,所有变量的数据类型都是NumPy数组。
调用代码:

weights1 = stocGradAscent0(dataArr, labelMat)
print(weights1)
plotBestFit(weights1)

效果图如下:
在这里插入图片描述
分类器错分了三分之一的样本,但是直接比较梯度上升代码与随机梯度上升代码时不公平的,因为前者的结果是在整个数据集上迭代了500次才得到的。一个判断优化算法优劣的可靠方法是看它是否收敛,也就是说参数是否达到了稳定值,是否还会不断地变化。
书中随机梯度上升算法,在迭代次数为200的过程中回归系数的变化情况(书中的图)。
在这里插入图片描述
从图中可以看出回归系数经过大量迭代才能达到稳定值,并且仍然有局部的波动现象。另外值得注意的是,在大的波动停止后,还有一些小的周期性波动。不难理解,产生这种现象的原因是存在一些不能正确分类的样本点(数据集并非线性可分),在每次迭代时会引发系数的剧烈改变。我们期望算法能避免来回波动,从而收敛到某个值。另外,收敛速度也需要加快。
线性可分:线性可分是指两个不同的类别在某种表示下可以用一条直线或超平面清晰地分开,即两个类别的数据点可以在这条直线或超平面的两侧。这条直线或超平面被称为决策边界。

针对上述问题,提出了改进的随机梯度上升算法:
代码如下:

# 改进的随机梯度上升算法
def stocGradAscent1(dataMatrix, classLables, numIter=150):
    m, n = np.shape(dataMatrix)
    weights = np.ones(n)
    for j in range(numIter):
        dataIndex = list(range(m))
        for i in range(m):
            alpha = 4/(1.0 + j + i) + 0.01
            randIndex = int(np.random.uniform(0, len(dataIndex)))
            h = sigmoid(sum(dataMatrix[randIndex]*weights))
            error = classLables[randIndex] - h
            weights = weights + alpha * error * np.array(dataMatrix[randIndex])
            del(dataIndex[randIndex])
    return weights

改进的地方: 一方面,alpha在每次迭代的时候都会调整,这会缓解图上的数据波动或者高频波动。另外,虽然alpha会随着迭代次数不断减小,但永远不会减小到0,这是因为alpah变换中还存在一个常数项。必须这样做的原因是为了保证在多次迭代之后新数据仍然具有一定的影响。如果要处理的问题是动态变化的,那么可以适当加大上述常数项,来确保新的值获得更大的回归系数。另一点值得注意的是,在降低alpha的函数中,alpha每次减少1/(j+i) ,其中j是迭代次数,i是样本点的下标。这样当j<<max(i)时,alpha就不是严格下降的。避免参数的严格下降也常见于模拟退火算法等其他优化算法中。

下面是对上面内容的解释,仅供参考,如有不对欢迎指正:在每次迭代中,stocGradAscent1算法之所以不会严格下降,是因为它使用了随机梯度下降算法(SGD),每次只使用一个样本点来更新参数。这样会引入一定的噪声,导致参数在迭代过程中波动,不能严格下降。而对于局部最小值的问题,虽然该算法采用了降低学习速率的方法,但由于SGD算法的随机性,可能会出现某些样本点的梯度方向与整体方向不一致,使得参数陷入局部最小值。因此,该算法的降低学习速率的方法并不能完全避免陷入局部最小值的问题,但可以使算法避免过早陷入局部最小值,提高算法的性能并加速收敛速度。

当j<<max(i)时,alpha的值变化很小,因为j和i相加比max(i)小得多,所以alpha的值接近一个常数。这个常数(0.01)用来确保在alpha非常小的情况下,参数仍然会有一些小的变化。但这也意味着alpha的变化很小,甚至接近于0,不能保证严格的下降,即参数每次更新不会呈线性下降的趋势。

这个常数项的引入,主要为了保持alpha的变化,维持参数更新的动量,以防止在某些情况下算法停滞不前。但由于它是一个常数项,不能基于数据自动调整;当训练任务本身具有高度非线性、多态、不寻常数据等复杂特征时,此项可能加速训练但无法有效规避局部最小值的陷入。因此,其在实际应用中的效果需要根据具体问题的复杂程度、数据的特征及算法的性质等综合考虑后决定是否使用常数项。

严格下降是指对于一个连续可导的函数,若其梯度(导数)值在某个区间内的取值恒小于0,那么该函数在这个区间内严格递减。换言之,在严格下降的情况下,函数的值每次更新都会呈现一个下降的趋势,不夹带上升的波动。

在机器学习优化中,严格下降是希望得到的一个性质。因为在梯度下降优化过程中,我们希望通过不断调整参数的值,使得损失函数在训练数据集上取得最小值。如果优化过程中的损失值出现了上升的情况,说明参数的更新方向与此时的梯度方向相反,可能是步长过大、学习率设置不当或进入了局部最优等导致的,需要对算法进行优化或重新调整参数的学习率等超参数,以便达到更优的结果。而保证函数呈严格下降趋势,可以在避免代码出错的同时,将优化效果最大化。

严格下降是一种朝着梯度方向每次都能使损失函数值下降的性质,它并不一定意味着一定会避免局部最小值。实际上,在许多非凸优化问题中,如神经网络、深度学习中,参数空间通常是高维度的,包含了许多局部最优解,从而可能出现梯度一直下降但最终收敛于局部最优解的情况。事实上,严格下降往往可能使模型陷入局部最小值或鞍点等不太优秀的解。

因此,为避免陷入局部最小值,一般需要采用一些优化技巧,例如改变学习率的策略、随机初始化参数或使用正则化等方法,来实现全局最优或次优解的搜索。此外,还有一些高级优化算法,如牛顿法、共轭梯度法、拟牛顿法等,可以使用二阶信息(Hessian矩阵)或历史梯度信息来快速寻找全局最优解,从而更好地避免陷入局部最小值的困扰。

另一方面通过随机选取样本来更新回归系数。这种方法将减少周期性的波动(如图5-6中的波动)。具体实现方法与第3章类似,这种方法每次随机从列表中选出一个值,然后从列表中删掉该值(再进行下一次迭代)。 此外,改进算法还增加了一个迭代次数作为第3个参数。如果该参数没有给定的话,算法将默认迭代150次。如果给定,那么算法将按照新的参数值进行迭代。
效果图如下:
在这里插入图片描述
从图中可以看出方法比固定alpha的方法收敛速度更快,系数没有像之前那样出现周期性的波动,这归功于stocGradAscent1()里的样本随机选择机制;stocGradAscent1()可以收敛得更快,因为不再是遍历整个数据集了,而是随机选取。
调用代码:

weights2 = stocGradAscent1(dataArr, labelMat)
print(weights2)
plotBestFit(weights2)

显示效果:
在这里插入图片描述

3.项目完整代码

代码及注释如下:

'''逻辑回归使用梯度上升算法'''
import numpy as np
import matplotlib.pyplot as plt

# 加载数据
def loadDataSet():
    dataMat = []; labelMat = []
    fr = open('testSet.txt')
    for line in fr.readlines():
        lineArr = line.strip().split()
        # 特征数据记录,dataMat[0]设置成1是偏置参数
        dataMat.append([1.0, float(lineArr[0]), float(lineArr[1])])
        # 标签值记录
        labelMat.append(int(lineArr[2]))
    return dataMat, labelMat

# sigmoid函数
def sigmoid(inX):
    return 1.0/(1 + np.exp(-inX))

def gradAscent(dataMatIn, classLabels):
    # 特征值
    dataMatrix = np.mat(dataMatIn)
    # 标签值转换为行向量
    labelMat = np.mat(classLabels).transpose()
    # m,特征向量行数,n,特征向量列数
    m, n = np.shape(dataMatrix)
    # 学习率,步长
    alpha = 0.001
    # 迭代次数
    maxCrycles = 500
    # 权重初始化为n行1列的数
    weights = np.ones((n, 1))
    # 梯度上升进行权值迭代
    for k in range(maxCrycles):
        # 预测数据
        h = sigmoid(dataMatrix*weights)
        # 真实数据与预测数据之间的差异
        error = (labelMat - h)
        # 更新权值
        weights = weights + alpha * dataMatrix.transpose() * error
    return weights



# 画出数据集和Logistic回归最佳拟合直线的函数
def plotBestFit(weights):
    # 读取数据
    dataMat, labelMat = loadDataSet()
    # 获得数据行数
    dataArr = np.array(dataMat)
    n = np.shape(dataArr)[0]
    # 记录标签为1的数据
    xcord1 = []; ycord1 = []
    # 记录标签为2的数据
    xcord2 = []; ycord2 = []
    # 画出属于两类的数据
    for i in range(n):
        if int(labelMat[i]) == 1:
            xcord1.append(dataArr[i, 1]); ycord1.append(dataArr[i, 2])
        else:
            xcord2.append(dataArr[i, 1]); ycord2.append(dataArr[i, 2])
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.scatter(xcord1, ycord1, s=30, c='red', marker='s')
    ax.scatter(xcord2, ycord2, s=30, c='green')
    # 计算分类边界
    x = np.arange(-3.0, 3.0, 0.1)
    # 公式推导得到边界的分类就是当sigmoid函数输入为0时
    y = (-weights[0]-weights[1]*x)/weights[2]
    ax.plot(x, y)
    plt.xlabel('X1'); plt.ylabel('X2');
    plt.show()

dataArr, labelMat = loadDataSet()
print(gradAscent(dataArr, labelMat))
# 画图验证
weights = gradAscent(dataArr, labelMat)
plotBestFit(weights.getA())

# 随机梯度上升算法
def stocGradAscent0(dataMatrix, classLabels):
    m, n = np.shape(dataMatrix)
    alpha = 0.01
    weights = np.ones(n)
    # 单个点进行梯度上升算法
    for i in range(m):
        h = sigmoid(sum(dataMatrix[i]*weights))
        error = classLabels[i] - h
        weights = weights + alpha * error * np.array(dataMatrix[i])
    return weights


weights1 = stocGradAscent0(dataArr, labelMat)
print(weights1)
plotBestFit(weights1)

# 改进的随机梯度上升算法
def stocGradAscent1(dataMatrix, classLables, numIter=150):
    m, n = np.shape(dataMatrix)
    weights = np.ones(n)
    for j in range(numIter):
        dataIndex = list(range(m))
        for i in range(m):
            alpha = 4/(1.0 + j + i) + 0.01
            randIndex = int(np.random.uniform(0, len(dataIndex)))
            h = sigmoid(sum(dataMatrix[randIndex]*weights))
            error = classLables[randIndex] - h
            weights = weights + alpha * error * np.array(dataMatrix[randIndex])
            del(dataIndex[randIndex])
    return weights
weights2 = stocGradAscent1(dataArr, labelMat)
print(weights2)
plotBestFit(weights2)

4.总结

逻辑回归的思想还是求权重系数,也有很多学习到的新概念,严格下降(动态调小alpha)、如何减少周期性的波动(随机选取梯度)啊等知识,梯度回归就是找梯度上升快的方向,对多个特征求导才能得到,但是也可以用真实值与预测值之间的误差来近似导数,学会了梯度上升算法,同理也学会了梯度下降算法。
如有错误或不当之处欢迎大家批评指正,路还很长,加油。

  • 13
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值