0802-編程實戰_貓和狗二分類_深度學習項目架構


0802-編程實戰_貓和狗二分類_深度學習項目架構

pytorch完整教程目錄:https://www.cnblogs.com/nickchen121/p/14662511.html

一、比賽介紹

接下來我們將通過 pytorch 完成 Kaggle 上的經典比賽:Dogs vs. Cats

Dogs vs. Cats 是一個傳統的二分類問題,它的訓練集包含 25000 張圖片,這些圖片都放在同一個文件夾中,命名格式為 <category>.<num>.jpg,例如 cat.10000.jpgdog.100.jpg,測試集包含 12500 張圖片,命名為 <num>.jpg,例如 1000.jpg

參賽者需要根據訓練集的圖片訓練模型,並在測試集上進行預測,輸出它是狗的概率。最后提交的 csv 文件如下,第一列是圖片的 <num>,第二列是圖片為狗的概率。

id label
10001 0.889
10002 0.01
... ...

二、數據加載

數據的相關處理主要保存在 data/dataset.py 中。

關於數據加載,之前提過,基本原理就是先使用 Dataset 封裝數據集,再使用 Dataloader 實現數據並行加載。

Kaggle 提供的數據包括訓練集和測試集,但是在我們使用的時候,還需要從訓練集中抽取一部分作為驗證集。

對於上述所說的三個數據集,雖然它們的相應操作不太一樣,但是如果專門寫出三個 Dataset,則會顯得復雜並冗余,因此在這里通過添加一些判斷來區分三者。比如我們希望對訓練集做一些數據增強處理,如隨機裁剪、隨機翻轉、加噪聲等,但是對於驗證集和測試集則不需要。

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Coding by https://www.cnblogs.com/nickchen121/
# Datatime:2021/5/3 10:15
# Filename:dataset.py
# Toolby: PyCharm

import os
from PIL import Image
from torch.utils import data
import numpy as np
from torchvision import transforms as T


class DogCat(data.Dataset):
    def __init__(self, root, transforms=None, train=True, test=False):
        """
        目標:獲取所有圖片地址,並根據訓練、驗證、測試划分數據
        """
        self.test = test  # 獲取測試集
        imgs = [os.path.join(root, img)
                for img in os.listdir(root)]  # 拼接所有圖片路徑,路徑地址如下所示
        """
        test1: data/test1/8973.jpg
        train: data/train/cat.10004.jpg
        """

        # 區分數據集是否為測試集,並對數據集的圖片進行排序
        if self.test:
            imgs = sorted(
                imgs,
                key=lambda x: int(x.split('.')[-2].split('/')[-1]))  # 切割出 8973
        else:
            imgs = sorted(imgs,
                          key=lambda x: int(x.split('.')[-2]))  # 切割出 10004

        # 划分訓練、驗證集,驗證:訓練 = 3:7
        imgs_num = len(imgs)
        if self.test:
            self.imgs = imgs
        elif train:
            self.imgs = imgs[:int(0.7 * imgs_num)]  # 訓練集來自數據集的前 70%
        else:
            self.imgs = imgs[int(0.7 * imgs_num):]

        # 數據轉換操作,測試驗證和訓練的數據轉換有所區別
        if transforms is None:

            # Normalize給定均值:(R,G,B) 方差:(R,G,B),將會把Tensor正則化
            normalize = T.Normalize(mean=[0.485, 0.456, 0.406],
                                    std=[0.229, 0.224, 0.225])

            # 測試集和驗證集
            if self.test or not train:
                self.transforms = T.Compose([
                    T.Scale(224),  # 讓圖片統一大小為:224*224
                    T.CenterCrop(224),  # 中心切割
                    T.ToTensor(),
                    normalize
                ])
            # 訓練集
            else:
                self.transforms = T.Compose([
                    T.Scale(256),  # 讓圖片統一大小為:256*256
                    T.RandomSizedCrop(224),  # 隨機切割圖片后,resize成給定的大小 224*224
                    T.RandomHorizontalFlip(),  # 一半的概率翻轉,一半的概率不翻轉
                    T.ToTensor(),
                    normalize
                ])

    def __getitem__(self, index):
        """
        返回一張圖片的數據
        如果是測試集,沒有圖片 id,如 8973.jpg 返回 8973

        test1: data/test1/8973.jpg
        train: data/train/cat.10004.jpg
        """
        img_path = self.imgs[index]
        if self.test:
            label = self.imgs[index].split('.')[-2]  # type:str # 切割出 8973.jpg
            label = int(label.split('/')[-1])  # 切割出 8973

        else:
            label = 1 if 'dog' in img_path.split(
                '/')[-1] else 0  # 切割出 cat.10004.jpg,通過判斷對圖片增加標簽

        data = Image.open(img_path)
        data = self.transforms(data)  # 對圖片進行處理

        return data, label

    def __len__(self):
        """
        返回數據集中所有圖片的個數
        """
        return len(self.imgs)

# train_dataset = DogCat(opt.train_data_root, train=True)  # opt 是未來會講到的配置對象
# trainloader = DataLoader(train_dataset,
#                          batch_size=opt.batch_size,
#                          shuffle=True,
#                          num_workers=opt.num_workers)
# 
# for ii, (data, label) in enumerate(trainloader):
#     train()

上述代碼中我們需要注意三個點:

  • 把文件讀取等費時操作放在 __getitem__ 函數中,利用多進程加速
  • 一次性把所有圖片讀進內存,不僅費時也會占用較大內存,而且不方便進行數據增強操作
  • 訓練集中的 30% 作為驗證集,可以用來檢查模型的訓練效果,避免過擬合

三、模型定義

模型的定義主要保存在 models 目錄下,其中 BasicModule 是對 nn.Module 的簡易封裝,提供快速加載和保存模型的接口。

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Coding by https://www.cnblogs.com/nickchen121/
# Datatime:2021/5/3 10:22
# Filename:BasicModule.py
# Toolby: PyCharm
import time
import torch as t


class BasicModule(t.nn.Module):
    """
    封裝了 nn.Module,主要提供 save 和 load 兩個方法
    """

    def __init__(self):
        super(BasicModule, self).__init__()
        self.model_name = str(type(self))  # 模型的默認名字

    def load(self, path):
        """
        可加載指定路徑的模型
        :param path:
        :return:
        """
        self.load_state_dict(t.load(path))

    def save(self, name=None):
        """
        保存模型,默認使用“模型名字+時間”作為文件名,
        如 AlexNet_0710_23:57:29.pth
        :param name:
        :return:
        """
        if name is None:
            prefix = 'checkpoints/' + self.model_name + '.'
            name = time.strftime(prefix + '%m%d_%H:%M:%S.pth')
        t.save(self.state_dict(), name)
        return name

在實際使用中,直接調用 model.save() 以及 model.load(opt.load_path) 即可。

其他自定義模型一般繼承 BasicModule,然后實現自己的模型。由於實現了 AlexNet 和 ResNet34,在 models/__init__.py 中,可以寫下下述代碼:

from .AlexNet import AlexNet
from .ResNet34 import ResNet34

這樣主函數中就可以寫:

from models import AlexNet
# 或
import models
model = models.AlexNet()
# 或
import models
model = getattr('models', 'AlexNet')()

上述在主函數中的代碼中,其中最后寫法最關鍵,這樣意味着我們可以通過字符串直接指定使用的模型,而不需要使用判斷語句,同時也不需要在每次新增加模型后都修改代碼。

但是最好的方法,就是在新增模型后需要在 models.__init__.py 中加上 from .new_module import new_module,避免使用第一種方法時報錯,或者避免使用 model = getattr('models', 'AlexNet')() 時找不到該對象。

最后,在模型定義的時候,需要注意以下三點:

  • 盡量使用 nn.Sequenetial
  • 將經常使用的結構封裝為子 module
  • 將重復且有規律性的結構用函數生成

四、工具函數

在項目中,我們可能需要用到一些經常使用的方法,這些方法可以統一放入到 utils 文件夾中,需要時再導入。

在這個項目中,主要封裝了可視化工具 visdom 的一些操作。

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Coding by https://www.cnblogs.com/nickchen121/
# Datatime:2021/5/3 10:23
# Filename:visualize.py
# Toolby: PyCharm
import visdom
import time
import numpy as np


class Visualizer(object):
    """
    封裝了 visdom 的基本操作,但仍然可以通過 `self.vis.function`
    或者 `self.function` 調用原生的 visdom 接口
    例如:
    self.text('hello visdom')
    self.histogram(t.randn(1000))
    self.line(t.arange(0, 10), t.arange(1, 11))
    """

    def __init__(self, env='default', **kwargs):
        self.vis = visdom.Visdom(env=env, **kwargs)

        # 保存('loss', 23) 即 loss 的第 23 個點
        self.index = {}
        self.log_text = ''

    def reinit(self, env='default', **kwargs):
        """
        修改 visdom 的配置
        :param env:
        :param kwargs:
        :return:
        """
        self.vis = visdom.Visdom(env=env, **kwargs)
        return self

    def plot_many(self, d: dict):
        """
        一次 plot 多個
        :param d: dict(name, value) i.e. ('loss', 0.11)
        :return:
        """
        for k, v in d.items():
            self.plot(k, v)

    def img_many(self, d: dict):
        """
        處理多張圖片
        :param d:
        :return:
        """
        for k, v in d.items():
            self.img(k, v)

    def plot(self, name, y, **kwargs):
        """
        self.plot('loss', 1.00)
        :param name: 
        :param y: 
        :param kwargs: 
        :return: 
        """
        x = self.index.get(name, 0)
        self.vis.line(Y=np.array([y]),
                      X=np.array([x]),
                      win=name,
                      opts=dict(title=name),
                      update=None if x == 0 else 'append',
                      **kwargs)
        self.index[name] = x + 1

    def img(self, name, img_, **kwargs):
        """
        self.img('input_img', t.Tensor(64, 64))
        self.img('input_imgs', t.Tensor(3, 64, 64))
        self.img('input_img', t.Tensor(100, 1, 64, 64))
        self.img('input_imgs', t.Tensor(100, 3, 64, 64), nrows=10)
        :param name:
        :param img_:
        :param kwargs:
        :return:
        """
        self.vis.images(img_.cpu().numpy,
                        win=name,
                        opts=dict(title=name),
                        **kwargs)

    def log(self, info, win='log_text'):
        """
        self.log({'loss':1, 'lr':0.0001}
        :param info:
        :param win:
        :return:
        """
        self.log_text += ('[{time}] {info} <br>'.format(
            time=time.strftime('%m%d_%H%M%S'),
            info=info
        ))
        self.vis.text(self.log_text, win)

    def __getattr__(self, name):
        """
        自定義的 plot,image,log,plot_many 等除外
        self.function 等價於 self.vis.function
        :param name:
        :return:
        """
        return getattr(self.vis, name)

五、配置文件

在模型定義、數據處理和訓練過程中會產生許多變量,這些變量應該提供默認值,並且統一放在配置文件中。如此做的話,在后期調試、修改代碼的時候會方便很多,在這里,我們把所有課配置項都放在 config.py 中。

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Coding by https://www.cnblogs.com/nickchen121/
# Datatime:2021/5/3 10:20
# Filename:config.py
# Toolby: PyCharm
class DefaultConfig(object):
    env = 'default'
    model = 'AlexNet'  # 使用的模型,名字必須與 models/__init__.py 中的名字一致

    train_data_root = './data/train/'  # 訓練集存放路徑
    test_data_root = './data/test1'  # 測試集存放路徑
    load_model_path = 'checkpoints/model.pth'  # 加載預訓練模型的路徑,為 None 代表不加載

    batch_size = 128  # batch_size
    use_gpu = False  # use GPU or not
    num_workers = 4  # num of workers for loading data
    print_freq = 20  # print info every N batch

    debug_file = '/tmp/debug'  # if os.path.exists(debug_file): enter ipdb
    result_file = 'result.csv'

    max_epoch = 10
    lr = 0.1  # initial learning rate
    lr_decay = 0.95  # when val_loss increase, lr = lr*lr_decay
    weight_decay = 1e-4  # 損失函數

從上述代碼中可以看出可配置的參數主要包括以下三類:

  • 數據集參數(文件路徑、batch_size 等)
  • 訓練參數(學習率、訓練 epoch 等)
  • 模型參數

定義好了上述配置參數后,可以在程序中這樣使用配置參數:

import models
from config import DefaultConfig

opt = DefaultConfig()
lr = opt.lr
model = getattr(models, opt.model)
dataset = DogCat(opt.traini_data_error)

上述所說的都是默認參數,在默認配置類中,我們還可以提供一個更新函數,根據字典更新配置參數。

    def parse(self, kwargs: dict):
        """
        根據字典 kwargs 更新 config 參數
        :param kwargs:
        :return:
        """
        # 更新配置參數
        for k, v in kwargs.items():
            if not hasattr(self, k):
                warnings.warn(f"Warning: opt has not attribut {k}")
            setattr(self, k, v)

        # 打印配置信息
        print('user config: ')
        for k, v in self.__class__.__dict__.items():  # type:str
            if not k.startswith('__'):
                print(k, getattr(self, k))

當然,在實際使用時沒必要每次修改 config.py,只需要通過命令行傳入所需要的參數,覆蓋默認配置就行,例如

opt = DefaultConfig()
new_config = {'lr': 0.1, 'use_gpu': False}
opt.parse(new_config)
opt.lr == 0.1

六、main.py

6.1 命令行工具 fire

在講解 main 文件前,我們先熟悉一個我們可能可以用到的一個命令行工具 fire,可以通過 pip install fire 安裝,下面介紹下 fire 的基礎用法,假設 example.py 文件代碼如下:

# example.py
import file


def add(x, y):
    return x + y


def mul(**kwargs):
    a = kwargs['a']
    b = kwargs['b']
    return a * b


if __name__ == '__main__':
    fire.Fire()

那我們可以在命令行中通過以下語句調用 example 文件中定義的函數:

python example.py add 1 2  # 執行 add(1, 2)
python example.py mul --a=1 --b=2  # 執行 mul(a=1, b=2), kwargs={'a':1, 'b':2}
python example.py add --x=1 --y=2  # 執行 add(x=1, y=2)

從上述代碼可以看出,只要在程序中運行了 fire.Fire(),就可以通過命令行參數 `python file [args,] {--kwargs,}。當然,fire 還支持更多的高級功能,具體可以參考 官方指南

6.2 main.py的代碼組織結構

在我們這個項目的 main.py 中主要包括以下四個函數,其中三個需要命令行執行,main.py 的代碼組織結構如下所示:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Coding by https://www.cnblogs.com/nickchen121/
# Datatime:2021/5/3 10:20
# Filename:main.py
# Toolby: PyCharm
import os
import csv
import ipdb
import fire
import torch as t
from torchnet import meter
from inspect import getsource
from torch.nn import functional
from torch.autograd import Variable
from torch.utils.data import DataLoader

import models
from config import opt
from data.dataset import DogCat
from utils.visualize import Visualizer


def train(**kwargs):
    """
    訓練
    :param kwargs:
    :return:
    """
    pass


def val(model, dataloader):
    """
    計算模型在驗證集上的准確率等信息,用來輔助訓練
    :param model:
    :param dataloader:
    :return:
    """
    pass


def test(**kwargs):
    """
    測試(inference)
    :param kwargs:
    :return:
    """
    pass


def dc_help():
    """
    打印幫助的信息
    :return:
    """
    print('help')


if __name__ == '__main__':
    fire.Fire()

main.py 搭建好這樣的組織結構后,可以通過 python main.py <function> --args==xx 的方式執行訓練或測試。

6.3 訓練

訓練的主要步驟如下:

  • 定義網絡
  • 定義數據
  • 定義損失函數和優化器
  • 計算重要指標
  • 開始訓練
    • 訓練網絡
    • 可視化各種指標
    • 計算在驗證集上的指標

其中訓練函數的代碼如下:

def train(**kwargs):
    """
    訓練
    :param kwargs:
    :return:
    """

    # 根據命令行參數更新配置
    opt.parse(kwargs)
    vis = Visualizer(opt.env)

    # step1:模型
    model = getattr(models, opt.model)()
    if opt.load_model_path:
        model.load(opt.load_model_path)
    if opt.use_gpu: model.cuda()

    # step2:數據
    train_data = DogCat(opt.train_data_root, train=True)
    val_data = DogCat(opt.train_data_root, train=False)
    train_dataloader = DataLoader(train_data,
                                  opt.batch_size,
                                  shuffle=True,
                                  num_workers=opt.num_workers)
    val_dataloader = DataLoader(val_data,
                                opt.batch_size,
                                shuffle=False,
                                num_workers=opt.num_workers)

    # step3:目標函數和優化器
    criterion = t.nn.CrossEntropyLoss()
    lr = opt.lr
    optimizer = t.optim.Adam(model.parameters(),
                             lr=lr,
                             weight_decay=opt.weight_decay)

    # step4:統計指標:平滑處理之后的損失,還有混淆矩陣
    loss_meter = meter.AverageValueMeter()  # 平均損失
    confusion_matrix = meter.ConfusionMeter(2)  # 混淆矩陣
    previous_loss = 1e100

    # 訓練
    for epoch in range(opt.max_epoch):

        loss_meter.reset()
        confusion_matrix.reset()

        for ii, (data, label) in enumerate(train_dataloader):

            # 訓練模型參數
            inp = Variable(data)
            target = Variable(label)
            if opt.use_gpu:
                inp = inp.cuda()
                target = target.cuda()
            optimizer.zero_grad()
            score = model(inp)
            loss = criterion(score, target)
            loss.backward()
            optimizer.step()

            # 更新統計指標及可視化
            loss_meter.add(loss.data[0])
            confusion_matrix.add(score.data, target.data)

            if ii % opt.print_freq == opt.print_freq - 1:
                vis.plot('loss', loss_meter.value()[0])

                # 如果需要的話,進入 debug 模式
                if os.path.exists(opt.debug_file):
                    ipdb.set_trace()

        model.save()

        # 計算驗證集上的指標及可視化
        val_cm, val_accuracy = val(model, val_dataloader)
        vis.plot('val_accuracy', val_accuracy)
        vis.log('epoch:{epoch},lr:{lr},loss:{loss},train_cm:{train_cm},val_cm{val_cm}'
                .format(epoch=epoch,
                        loss=loss_meter.value()[0],
                        val_cm=str(val_cm.value()),
                        train_cm=str(confusion_matrix.value()),
                        lr=lr))

        # 如果損失不再下降,則降低學習率
        if loss_meter.value()[0] > previous_loss:
            lr = lr * opt.lr_decay
            for param_group in optimizer.param_groups:
                param_group['lr'] = lr

        previous_loss = loss_meter.value()[0]

6.3.1 torchnet 中的 meter

在訓練的代碼中,這里用到了 PyTorchNet 里的一個工具:meter。由於 PyTorchNet 是從 TorchNet 中遷移來的,提供了很多有用的工具,但目前的開發和文檔都不是特別完善,這里不多做贅述,只講上述用到的幾個方法。

mter 提供了一些輕量級工具,可以幫助用戶快速的統計訓練過程中的一些指標。
* AverageValueMeter 能夠計算所有數的平均值和標准差,可以用來統計一個 epoch 中損失的平均值
* confusionmeter 用來統計分類問題中的分類情況,是一個比准確率更詳細的統計指標,給出的是一個混淆矩陣

混淆矩陣舉例:

樣本 判為狗 判為貓
實際是貓 35 15
實際是狗 9 91

注:想詳細了解混淆矩陣的在第七小節

6.4 驗證

驗證相比較訓練來說簡單很多,但是需要注意把模型置於驗證模式(model.eval()),驗證完成后還需要把它設置回訓練模式(model.train()),這兩句代碼會影響 BatchNorm 和 Dropout 等層的運行模式。驗證模型准確率的代碼如下:

def val(model, dataloader):
    """
    計算模型在驗證集上的准確率等信息,用來輔助訓練
    :param model:
    :param dataloader:
    :return:
    """
    # 把模型設置為驗證模式
    model.eval()

    confusion_matrix = meter.ConfusionMeter(2)
    for ii, data in enumerate(dataloader):
        inp, label = data
        val_inp = Variable(inp, volatile=True)
        val_label = Variable(label.long(), volatile=True)
        if opt.use_gpu:
            val_inp = val_inp.cuda()
            val_label = val_label.cuda()
        score = model(val_inp)
        confusion_matrix.add(score.data.squeeze(), label.long())

    # 把模型恢復為訓練模式
    model.train()

    cm_value = confusion_matrix.value()
    accuracy = 100. * (cm_value[0][0] + cm_value[1][1]) / (cm_value.sum())

    return confusion_matrix, accuracy

6.5 測試

測試的時候,需要計算每個樣本屬於狗的概率,並把結果保存為 csv 文件,測試的代碼和驗證比較相似,但需要自己加載模型和數據。

def write_csv(results, file_name):
    with open(file_name, 'w') as f:
        writer = csv.writer(f)
        writer.writerow(['id', 'label'])
        writer.writerows(results)


def test(**kwargs):
    """
    測試(inference)
    :param kwargs:
    :return:
    """
    opt.parse(kwargs)

    # 模型
    model = getattr(models, opt.model)().eval()
    if opt.load_model_path:
        model.load(opt.load_model_path)
    if opt.use_gpu: model.cuda()

    # 數據
    train_data = DogCat(opt.test_data_root, test=True)
    test_dataloader = DataLoader(train_data,
                                 batch_sampler=opt.batch_size,
                                 shuffle=False,
                                 num_workers=opt.num_workers)

    results = []
    for ii, (data, path) in enumerate(test_dataloader):
        inp = Variable(data, volatile=True)
        if opt.use_gpu: inp = inp.cuda()
        score = model(inp)

        probability = probability = functional.softmax(score, dim=1)[:, 0].detach().tolist()
        batch_results = [(path_, probability_) for path_, probability_ in zip(path, probability)]
        results += batch_results

    write_csv(results, opt.result_file)
    return results

6.6 幫助函數

為了讓他人方便使用,程序中應該還需要提供一個幫助函數,用於說明函數是如何使用的。

程序的命令行接口有很多參數,如果手動用字符串表示不僅復雜,而且后期修改 config 文件時還需要修改對應的幫助信息。為此,這里使用 Python 標准庫中的 inspect 方法,可以自動獲取 config 的源代碼。

dg_help 的代碼如下:

def dc_help():
    """
    打印幫助的信息
    :return:
    """
    print('''
    usage:python{0} <function> [--args=value,]
    <function> := train | test | help
    example:
        python {0} train --env='env0701' --lr=0.01
        python {0} test --dataset='path/to/dataset/root/'
        python {0} help
    avaiable args:
    '''.format(__file__))

    source = (getsource(opt.__class__))  # 獲取配置信息
    print(source)

七、使用

如 dc_help 函數打印的信息描述的一樣,可以通過命令行參數指定變量名。下面是三個使用例子,fire 會把包含 “-” 命令行參數自動轉成下划線 “_”,也會把非數字的數值轉成字符串,所以 --train--data-root=data/train--train_data_root = 'data/train' 是等價的。

感興趣的可以把數據集下載下來進行測試:貓狗分類數據集

由於本章只是講解項目架構,我就不做測試,但是代碼應該沒什么大問題,修修補補就行了。

想要具體代碼的可以加我微信:chenyoudea,但是沒必要找我要,我也沒有嘗試去跑通這個代碼,並且我也沒有下載數據集,因為這一章沒必要。

# 訓練模型
python main.py train
    --train-data-root=data/train/
    --load-model-path=None
    --lr=0.005
    --batch-size=32
    --model='ResNet34'
    --max-epoch=20
    
python main.py train --train-data-root=data/train/ --load-model-path=None --lr=0.005 --batch-size=32 --model='ResNet34' --max-epoch=20

    
# 測試模型
python main.py test
    --test-data-root=data/test1
    --load-model-path=None
    --batch-szie=128
    --model='ResNet34'
    --num-workers=12
    
# 打印幫助信息
python main.py dc_help

八、爭議

這里還是多說一嘴,因為這個風格更多的是書籍作者陳雲老師的風格,並不是說以后你寫的代碼都要以這個為標准,這個項目架構更多的是作為一個題意或一種參考。

也就是說,不要把本篇文章的觀點作為一個必須遵守的規范,但是前期的學習可以按照這個架構來,這樣不容易犯錯。但是,對於未來你遇到的很多項目,尤其對於每個公司的項目,項目架構相信都是不一樣的,不唯經驗主義,不唯教條主義,這才是一個碼農想進階的必經之路。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM