图片分类 猫狗大战 pytorch VGG

一.简介

    猫狗大战其实是Kaggle公司(在墨尔本创立的) 于2013年举办的比赛,判断一张输入图像是“猫”还是“狗”,并分别用0,1标识出来。
  AI研习社猫狗大战赛题的要求:https://god.yanxishe.com/41 (目前比赛已经结束,但仍可做为练习赛每天提交测试结果)
  在这个比赛中,有25000张标记好的猫和狗的图片用做训练,有12500张图片用做测试。这个竞赛是2013年开展的,如果你能够达到80%的准确率,在当年是一个 state-of-the-art的成绩😎。
  现在,参考和借鉴大师经验后,我们使用VGG模型实现猫狗大战😆。

二.理论

  VGGNet是牛津大学计算机视觉组和Google DeepMind公司的研究员仪器研发的深度卷积神经网络,主要探究卷积神经网络的深度和其性能之间的关系,通过反复堆叠33的小卷积核和22的最大池化层,VGGNet成功的搭建了16-19层的深度卷积神经网络。与之前的state-of-the-art的网络结构相比,错误率大幅度下降;同时,VGG的泛化能力非常好,在不同的图片数据集上都有良好的表现。到目前为止,VGG依然经常被用来提取特征图像。
  VGGNet使用的全部都是3x3的小卷积核和2x2的池化核,通过不断加深网络来提升性能。各个级别VGG的模型结构如下表所示,其下方为不同模型的参数数量。可以看到,虽然从A到E每一级网络逐渐变深,但是网络的参数量并没有增长很多,这是因为参数量主要都消耗在最后3个全连接层了。前面的卷积层数量虽多,但是参数量其实都不大,不过训练时候耗时的依然是卷积层,因为这部分计算量比较大。其中D,E分别为VGG16和VGG19。
不同VGG模型的网络结构
  VGG拥有5个卷积段,每一个卷积段有2-3个卷积层,同时每段的结尾都会连接一个最大池化层,来缩小图片尺寸。每段内的卷积核数量都一样,越靠后的段的卷积核数量越多:64-128-256-512-512。其中经常出现多个完全一样的3x3卷积层堆叠在一起的情况,这是个非常有用的设计。如下图所示,两层3x3的串联卷积结果相当于一个5x5的卷积,即最后一个像素会与周围5x5个像素产生关联,可以说感受野大小为5x5。而3层3x3的卷积核的串联结果则相当于1个7x7的卷积层。除此之外,3个串联的3x3卷积层的参数数量要比一个7x7卷积层的参数数量小得多,即333C2/77C2 = 55%,__更少的参数意味着减少过拟合,而且更重要的是3个3x3卷积层拥有比1个7x7的卷积层更少的非线性变换(前者拥有3次而后者只有一次),使得CNN对特征的学习能力更强。

3×3卷积核

参考链接:https://www.jianshu.com/p/15e413985f25

三.实现

  首先说明的是,直到此刻我还是没有成功地在测试集上得到一个很好地预测结果,验证集上准确率正常,但测试集上准确率只有一半左右,搜索很多网站去找原因,还没找到具体是哪有问题,鉴于我需要提交任务,所以先写博客,回头再去debug,😭。

测试集上:
测试集上的正确率

  实现这个项目,是打算使用VGG神经网络,环境使用的是Google的colab。
colab文档连接:https://colab.research.google.com/drive/1kVua8TIQSa2w-vYQ7Y3DqFFKfy-1jlXq?usp=sharing

1️⃣.实现准备

(1)测试是否使用GPU上

import torch
import torch.nn as nn
# 判断是否存在GPU设备
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print('Using gpu: %s ' % torch.cuda.is_available())

(2)上传及解压数据集

数据集文件夹格式

数据集格式:

训练集,作用显而易见

验证集,供我们的模型验证测试结果

测试集,不可见的数据集,检验模型

我们先通过train,valid来训练自己的模型,并测试准确率

参考链接:https://www.uud.me/qiwenzalun/colab-unzip-unrar.html
#测试集在内的所有数据可以从https://static.leiphone.com/cat_dog.rar下载😴 💤,也可以去研习社。

#下载并解压数据,从大佬网站上下载训练数据集
! wget http://fenggao-image.stor.sinaapp.com/dogscats.zip

! unzip '/content/dogscats.zip'

#! unzip '/content/Test.zip'

#下载并解压数据 
#! wget https://static.leiphone.com/cat_dog.rar
#########在colab上解压使用,注意文件名
#! unrar x '/content/drive/MyDrive/AI/cat_dog.rar'```

(3)由文件创建数据集

#导入头文件 定义为不同名
import numpy as np
import matplotlib.pyplot as plt
import os
###torch的tensor和numpy的array之间是内存共享的,
###这意味着二者的转化几乎不消耗什么资源,并且修改其中一个会同时改变另一个的值。
import torch
import torch.nn as nn
import torchvision
from torchvision import models,transforms,datasets
import time
import json


#transform.Normalize()则把0-1变换到(-1,1) 
#mean:各通道的均值 std:各通道的标准差 output = (input - mean) / std
normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])

##Compose里面的参数实际上就是个列表,而这个列表里面的元素就是你想要执行的transform操作。
##生成一个CenterCrop类的对象,用来将图片从中心裁剪成224*224
##eg:out = transforms.ToTensor()(img)把一个取值范围是[0,255]的PIL.Image转换成Tensor张量
vgg_format = transforms.Compose([
                transforms.Resize(224),
                transforms.CenterCrop(224), 
                transforms.ToTensor(),
                normalize,
            ])

########2333#########至此,我们已经生成了一个自定义的vgg格式😈#########2333###############

#文件路径,右击左侧文件名点击复制路径(相对路径一直出错,修改工作路径都不行-_-||)
#data_dir = '/content/drive/MyDrive/AI/cat_dog/data'
data_dir = '/content/dogscats'

#数据集格式:训练集,作用显而易见 验证集,供我们的模型验证测试结果  测试集,不可见的数据集,检验模型
#交叉验证参考:https://blog.csdn.net/robitmind/article/details/105384341


dsets = {x: datasets.ImageFolder(os.path.join(data_dir, x), vgg_format)
         for x in ['train', 'valid']}

##ImageFolder()格式:
    ###root:图片存储的根目录,即各类别文件夹所在目录的上一级目录
    ###transform:对图片进行预处理的操作(函数),原始图片作为输入,返回一个转换后的图片。
    ###target_transform:对图片类别进行预处理的操作,输入为 target,输出对其的转换。如果不传该参数,即对 target 不做任何转换,返回的顺序索引 0,1, 2…
    ###loader:表示数据集加载方式,通常默认加载方式即可。
    ###is_valid_file:获取图像文件的路径并检查该文件是否为有效文件的函数(用于检查损坏文件)
###对数据的放置有要求,必须在data_dir目录下放置train和val两个文件夹,然后每个文件夹下,每一类图片单独放在一个文件夹里。


#遍历数组获取文件数目
dset_sizes = {x: len(dsets[x]) for x in ['train', 'valid']}
#我猜啊,可能是数据集的种类🙄
dset_classes = dsets['train'].classes

(4)显示图片的函数

#直接使用预训练好的VGG模型。下载ImageNet1000个类的JSON 文件
#对输入的5个图片利用VGG模型进行预测,同时,使用softmax或Logsoftmax对结果进行处理,
!wget https://s3.amazonaws.com/deep-learning-models/image-models/imagenet_class_index.json

##显示图片的小程序😄
def imshow(inp, title=None):
#   Imshow for Tensor.
    inp = inp.numpy().transpose((1, 2, 0))
    mean = np.array([0.485, 0.456, 0.406])
    std = np.array([0.229, 0.224, 0.225])
    inp = np.clip(std * inp + mean, 0,1)
    plt.imshow(inp)
    if title is not None:
        plt.title(title)
    plt.pause(0.001)  # pause a bit so that plots are updated

(5)装载数据集

import torch
import torch.nn as nn
##torch.utils.data.DataLoader()格式:
  ###1、dataset:(数据类型 dataset) 2、batch_size:(数据类型 int)3、shuffle:(数据类型 bool)
  ###7.num_workers:(数据类型 Int)工作者数量,默认是0。使用多少个子进程来导入数据。设置为0,
#就是使用主进程来导入数据。注意:这个数字必须是大于等于0的,负数估计会出错。
loader_train = torch.utils.data.DataLoader(dsets['train'], batch_size= 37, 
                            shuffle=True, num_workers=6)
loader_valid = torch.utils.data.DataLoader(dsets['valid'], batch_size = 7, 
                            shuffle=False, num_workers=6)


##valid 把第一个 batch 保存到 inputs_try, labels_try,分别查看
count = 1
for data in loader_valid:
    #print(count, end='\n')
    if count == 1:
        inputs_try,labels_try = data
    count +=1
print(labels_try)
print(inputs_try.shape)
## 显示 labels_try 的图片,即valid里第一个batch的图片
out = torchvision.utils.make_grid(inputs_try)

#注意,先运行imshow,它是自定义函数
#imshow(out, title=[dset_classes[x] for x in labels_try])

2️⃣.创建VGG16模型

(1)载入固定的vgg16

##pretrained=False 不导入网络结构,默认为false
model_vgg = models.vgg16(pretrained=True)

with open('./imagenet_class_index.json') as f: class_dict = json.load(f)
dic_imagenet = [class_dict[str(i)][1] for i in range(len(class_dict))]



#to(device) 就是保存到device上一份
inputs_try , labels_try = inputs_try.to(device), labels_try.to(device)

#释放无关内存
if hasattr(torch.cuda, 'empty_cache'):
	torch.cuda.empty_cache()
model_vgg = model_vgg.to(device)

outputs_try = model_vgg(inputs_try)

"""
print(outputs_try)
print(outputs_try.shape)
"""

'''
可以看到结果为5行,1000列的数据,每一列代表对每一种目标识别的结果。
但是我也可以观察到,结果非常奇葩,有负数,有正数,
为了将VGG网络输出的结果转化为对每一类的预测概率,我们把结果输入到 Softmax 函数
'''
m_softm = nn.Softmax(dim=1)
probs = m_softm(outputs_try)
vals_try,pred_try = torch.max(probs,dim=1)

print( 'prob sum: ', torch.sum(probs,1))
print( 'vals_try: ', vals_try)
print( 'pred_try: ', pred_try)

print([dic_imagenet[i] for i in pred_try.data])
#imshow(torchvision.utils.make_grid(inputs_try.data.cpu()), 
       #title=[dset_classes[x] for x in labels_try.data.cpu()])

(2)展示网络结构

print(model_vgg)

#在model_vgg基础上创建一个新的vgg模型
model_vgg_new = model_vgg;

for param in model_vgg_new.parameters():
    param.requires_grad = False
#nn.Linear 设置为全连接层 需要注意的是全连接层的输入与输出都是二维张量,
#一般形状为[batch_size, size],不同于卷积层要求输入输出是四维张量。


model_vgg_new.classifier._modules['6'] = nn.Linear(4096, 2)
model_vgg_new.classifier._modules['7'] = torch.nn.LogSoftmax(dim = 1)


""" #或者是增加一些层次
model_vgg_new.classifier._modules['6'] = nn.Linear(4096, 4096)
model_vgg_new.classifier._modules['7'] = torch.nn.Dropout()
model_vgg_new.classifier._modules['8'] = torch.nn.ReLU() 
model_vgg_new.classifier._modules['9'] = nn.Linear(4096, 4096)
model_vgg_new.classifier._modules['10'] = torch.nn.ReLU() 
model_vgg_new.classifier._modules['11'] = nn.Linear(4096, 4096)
model_vgg_new.classifier._modules['12'] = torch.nn.Dropout()
model_vgg_new.classifier._modules['13'] = torch.nn.ReLU() 
model_vgg_new.classifier._modules['14'] = nn.Linear(4096, 2)
model_vgg_new.classifier._modules['15'] = torch.nn.LogSoftmax(dim = 1)
"""

model_vgg_new = model_vgg_new.to(device)
#print(model_vgg_new.classifier)

3️⃣.训练模型

(1)训练模型的函数

import torch.optim
from torch.optim import lr_scheduler

from PIL import Image, ImageFile
ImageFile.LOAD_TRUNCATED_IMAGES = True

'''
第一步:创建损失函数和优化器
损失函数 NLLLoss() 的 输入 是一个对数概率向量和一个目标标签. 
它不会为我们计算对数概率,适合最后一层是log_softmax()的网络. 
'''
criterion = nn.NLLLoss()

# 设置学习率
lr = 0.0007

##修改学习率的值,一般是将其改小,如果准确率有所提升,但是升的很慢,则说明是训练速度太慢了。这时候需要
##适当增加BATCH_SIZE的数量,但是BATCH_SIZE会影响最终的准确率,BATCH_SIZE越大,则最终网络的准确率越低。


# 随机梯度下降
optimizer_vgg = torch.optim.SGD(model_vgg_new.classifier[6].parameters(),lr = lr)
##torch.optim.SGD:
  ###params (iterable) – 待优化参数的iterable或者是定义了参数组的dict
  ###lr (float) – 学习率
  ###momentum (float, 可选) – 动量因子(默认:0)
  ###weight_decay (float, 可选) – 权重衰减(L2惩罚)(默认:0)
  ###dampening (float, 可选) – 动量的抑制因子(默认:0)
  ###nesterov (bool, 可选) – 使用Nesterov动量(默认:False)


####暂时用不到exp_lr_scheduler = lr_scheduler.StepLR(optimizer_vgg, step_size=7, gamma=0.1)


def train_model(model,dataloader,size,epochs=1,optimizer=None):
    model.train()
    
    for epoch in range(epochs):
        running_loss = 0.0
        running_corrects = 0
        count = 0
        for inputs,classes in dataloader:
            inputs = inputs.to(device)
            classes = classes.to(device)
            outputs = model(inputs)
            loss = criterion(outputs,classes)  
            
            optimizer.zero_grad()
            loss.backward()    #梯度是否回传
            optimizer.step()

            _,preds = torch.max(outputs.data,1)
            # statistics
            running_loss += loss.data.item()
            running_corrects += torch.sum(preds == classes.data)
            count += len(inputs)
            #print('Training: No. ', count, ' process ... total: ', size)
        epoch_loss = running_loss / size
        epoch_acc = running_corrects.data.item() / size
        print('Loss: {:.4f} Acc: {:.4f}'.format(
                     epoch_loss, epoch_acc))
        

(2)训练模型

# 模型训练
train_model(model_vgg_new,loader_train,size=dset_sizes['train'], epochs=17, 
            optimizer=optimizer_vgg)

4️⃣.在验证集上测试训练的模型

(1)模型测试函数

#pandas 大熊猫们??😱
#pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。
#Pandas 纳入了大量库和一些标准的数据模型,提供了高效地操作大型数据集所需的工具。
import pandas as pd


#在某个数据集上检测正确率,测试模型
def test_model(model, dataloader, size):
    #model.eval(). 否则的话,有输入数据,即使不训练,它也会改变权值。
    #eval()就是保证BN和dropout不发生变化,框架会自动把BN和DropOut固定住,不会取平均,
    #而是用训练好的值,不然的话,一旦test的batch_size过小,很容易就会被BN层影响结果!!!
    model.eval()
    #.zeros 初始化为0
    predictions = np.zeros(size)
    all_classes = np.zeros(size)
    all_proba = np.zeros((size,2))
    i = 0
    running_loss = 0.0
    running_corrects = 0
    #对于dataloader 这个加载器中的每一个变量
    for inputs,classes in dataloader:
    #将所有最开始读取数据时的tensor变量copy一份到device所指定的GPU上去,之后的运算都在GPU上进行。
        inputs = inputs.to(device)
        classes = classes.to(device)
        outputs = model(inputs)
        loss = criterion(outputs,classes)  
        #torch.max()简单来说是返回一个tensor中的最大值。        
        _,preds = torch.max(outputs.data,1)
        # statistics
        running_loss += loss.data.item()
        running_corrects += torch.sum(preds == classes.data)
        #t.numpy()将Tensor变量转换为ndarray变量,其中t是一个Tensor变量,
        #可以是标量,也可以是向量,转换后dtype与Tensor的dtype一致。
        predictions[i:i+len(classes)] = preds.to('cpu').numpy()
        all_classes[i:i+len(classes)] = classes.to('cpu').numpy()
        all_proba[i:i+len(classes),:] = outputs.data.to('cpu').numpy()
        i += len(classes)
        #print('Testing: No. ', i, ' process ... total: ', size)        
    epoch_loss = running_loss / size
    epoch_acc = running_corrects.data.item() / size
    print('Loss: {:.4f} Acc: {:.4f}'.format(
                     epoch_loss, epoch_acc))
    return predictions, all_proba, all_classes


(2)使用模型验证


predictions, all_proba, all_classes = test_model(model_vgg_new, loader_valid, size=dset_sizes['valid'])

(3)输出验证结果

# 单次可视化显示的图片个数
n_view = 7
correct = np.where(predictions==all_classes)[0]
from numpy.random import random, permutation
idx = permutation(correct)[:n_view]
print('random correct idx: ', idx)
loader_correct = torch.utils.data.DataLoader([dsets['valid'][x] for x in idx],
                  batch_size = n_view,shuffle=True)
for data in loader_correct:
    inputs_cor,labels_cor = data
# Make a grid from batch
out = torchvision.utils.make_grid(inputs_cor)
imshow(out, title=[l.item() for l in labels_cor])

# 类似的思路,可以显示错误分类的图片,这里不再重复代码

(4)存储加载训练模型
参考:https://blog.csdn.net/qq_37555071/article/details/109864166

#定义初始化model的函数
def set_parameter_requires_grad(model, feature_extracting):
    if feature_extracting:
        for param in model.parameters():
            param.requires_grad = False
def initialize_model(num_classes, feature_extract, use_pretrained=True):
    model_vgg = None
    model_vgg = models.vgg16(pretrained=use_pretrained)
    # 更改输出层
    set_parameter_requires_grad(model_vgg, feature_extract)
    model_vgg.classifier[6] = nn.Linear(4096, num_classes)
    model_vgg.classifier.add_module('7',torch.nn.LogSoftmax(dim = 1))
    return model_vgg

##存储训练模型😳(天秀的操作)
#不同的存储模式可能需要保存不同量级的参数,有的只保存网络结构而有的是保存全网络

model_path = '/content/drive/MyDrive/AI/models/model_vgg_1.pt'
torch.save(model_vgg_new.state_dict(), model_path)

#载入model
#注意保存的文件名😬
model_path = '/content/drive/MyDrive/AI/models/model_vgg_1.pt'
model_vgg_new = initialize_model(num_classes=2,feature_extract = True,use_pretrained=True)
model_vgg_new.to(device)
model_vgg_new.load_state_dict(torch.load(model_path))

5️⃣.在测试集上运行

import torch
from torch.utils.data import Dataset,DataLoader

#载入测试集上一级路径,这次是真刀真枪了😱
test_dir = '/content/Test'

normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
vgg_format = transforms.Compose([
                transforms.CenterCrop(224),
                transforms.ToTensor(),
                normalize,
            ])

test_set = datasets.ImageFolder(test_dir, vgg_format)
test_loader = DataLoader(test_set, batch_size = 1, drop_last = False, shuffle = False)

test_predictions, test_all_proba, test_all_classes = test_model(model_vgg_new, test_loader, size=len(test_loader.dataset))


with open("cat_dog_result.csv", 'w') as out:
    for i in range(len(test_loader.dataset)):
        out.write("{},{}\n".format(i, test_predictions[i]))

很多笔记我直接记在代码文件中了,看起来有些乱,表情也成斜体了。此外,代码中一定还有许多问题,像如代码的结构,随着学习需要不断修改。

四.总结

(1)
  首先说一下存在的问题,看起来简单的实现,其实需要注意很多✍️。我的程序在验证集上正常预测,测试集正确率低,可能(概率很小)是由于过拟合(overfit),体现在数据量小,网络复杂,learning rate 比较高,又没有设置任何防止过拟合的机制。

解决方法主要包括:

1.简化模型,利用现有深度学习手段增加数据(翻转,平移,随机裁剪)
2.利用 dropout层
3.利用正则化

当然,也可能是没有把数据规格化normalize、没有在分验证集之前打乱数据shuffle、数据和标签没有对上、最后一层没有使用正确的激活函数等等,暂时我还没找到具体原因。
另外,设置batch normalization需要batch size至少16张,理论上,在数据集充足的条件下,batch size越大越好。越大其确定的下降方向越准,引起的训练震荡越小。

(2)在算力充足的条件下,可以尝试大的数据集,如果数据集不够,使用交叉验证(参考:https://blog.csdn.net/weixin_40475450/article/details/80578943)的方法。
(3)尤其需要注意数据集的格式✍️,一般分为train、valid、test三部分,我们通过train和valid训练数据集,使用test测试我们的model。
(4)在调试程序的过程中,要注意归一化是否训练集和测试集相统一,测试model的函数中要记得梯度归零,反向传播的操作。
(5)model.eval以及model.train参考:https://www.zhihu.com/question/363144860/answer/952524603
(6)模型优化的两种方法:牛顿法,梯度下降法。
参考链接:https://blog.csdn.net/genghaihua/article/details/91468909
最后,说一下自己的感受,自己勉强算是Python和pytorch刚刚入门了,前期不努力,后期要恶补😭。自己从头到尾大概把每一个语句及相关的内容搜索了一下,心里感觉挺充实的,不知道在以后的应用中能不能回想起来。

参考链接:https://www.yuque.com/gaopursuit/drqyg6/fvopym


https://blog.csdn.net/qq_37555071/article/details/109864166


https://www.jianshu.com/p/15e413985f25


表情来源:https://blog.csdn.net/shiliang97/article/details/103316542

五.我又回来了

  上回说到我碰到了一个问题,验证集上正确率高,测试集正确率低(一半左右),今天修行了一上午一晚上时间,这里修一修,那里改一改,具体改的哪自己都忘了,诶嘿~😁😁😁还是没弄清楚到底哪有问题(我估计是文件输出部分不对),不过,虽然本地代码显示准确率仍然不高,提交上去还可以。
测试集正确率仍然低

研习社提交结果
现在的心情,有点😂😂😂😂(容我乐一会儿)~~

说一下,我能想起来改的地方:
(1)我是recruit,对于Python,pytorch理解不透彻,所以整篇代码都是仿照大佬代码修改的。因为验证集和测试集有差异,我怀疑是自己的set或者是loader与之前不一致,或者数据集的处理方式与之前不统一,所以,我修改了目录结构(Test下还有一个test,当时为了使用ImageFolder函数)。
新的目录结构
可以再参考:https://blog.csdn.net/qq_42860166/article/details/94833421
(2)第二处修改是:我把ImageFile.LOAD_TRUNCATED_IMAGES = True注释掉了,当时图片格式报错,加了句LOAD_TRUNCATED_IMAGES。
参考:https://blog.csdn.net/scool_winter/article/details/89426509
(3)第三处修改:我加了一段代码,用来改文件名称,我也不知道为什么,我搜的CSDN上的博客,怀疑可能是自己的问题,然后就加了。(当时自己的心理有点像被迫害妄想症,老是感觉这代码和我对着干😡 )

#####(2)修改文件名
import os
path = '/content/dogscats/Test/test'
file_list = os.listdir(path)
 
for file in file_list:
    # 补0 10表示补0后名字共10位
    filename = file.zfill(10)
    # print(filename)
    new_name = ''.join(filename)
    os.rename(path + '/' + file, path + '/' + new_name)

参考:https://blog.csdn.net/qq_32545287/article/details/107584554
(4)当我发现自己的测试集正确率怎么也不高时,为了手动debug加了一句打印语句,当时是想和其他人正确的结果比较一下,看看相差多少,然后才发现基本一致(可以用Excel选中两列 ctrl+‘\’ )…😤

for pre in test_predictions:
  print(pre, end = '\t');

修改后的colab文档:https://colab.research.google.com/drive/121HKilp-kDQ531OMHyS4ywVstLzLrMNq?usp=sharing
(因为代码结构不好,所以代码中存在的错误仍然有)
  肝完,收工😸。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值