飞桨AI Studio - 人工智能学习与实训社区     

飞桨PaddlePaddle-源于产业实践的开源深度学习平台

        先抛结论,对于想要快速了解某一领域有哪些比较适合落地的算法的从业人员来说,是一个很好的参考系统。从中可以知道从哪些模型里选型、如何轻量化、如何加速、一些非常细节的FAQ。但是,这个框架维护上还是存在欠缺,比如很多人反馈的教程调不通,盘子铺得较大但维护没跟上;遇到一些报错的时候,相比pytorch这种大量使用的框架,能查到的解决方案较少。                 我遇到的三个坑是:

1、P100开发环境效果验证OK——UWSGI+NGINX搭好项目后——多进程报错,勉强单进程跑跑,网上的说法是paddle有多进程的问题,需要把import放到多进程里,但是试了一番没成功。

2、同一个镜像,相同的代码,换个服务器上测试环境V100直接卡死,没有报错提示,就是卡住了,后来倒腾了几天试版本,又是降低paddle版本,又是换显卡类型,终于搞定。这一点网上很多人也提到了,报错信息不完善。所以,如果是新手,并需要在短期内上线服务的场景,慎重。

3、cpu推理正常,即加参数use_gpu=False,但gpu推理乱码,这里需要基于box输出判断是识别乱码还是整个流程乱码了,如果box数量正常和cpu一致,那就是识别乱码,大概率是词典的问题。如果确定是gpu乱码,会比较麻烦,和显卡型号、驱动版本、cuda、paddle-paddle-gpu版本有关,可以搜索关键词(paddle gpu 乱码),遇到该问题的人也挺多,解决方法一般是各种升级降级,只能试出来,个人认为这属于paddle框架的兼容性问题。

        上面吐槽了一番,但值得肯定的是,paddle在轻量化及速度上还是不错的。

        如果后面的读者成功解决了UWSGI+NGINX多进程部署的问题,请分享一下你的经验。报错信息如下:

[Hint: 'cudaErrorInitializationError'. The API call failed because the CUDA driver and runtime could not be initialized. ] (at /paddle/paddle/phi/backends/gpu/cuda/cuda_info.cc:172)

一、paddleocr

1.1 简介

GitHub - PaddlePaddle/PaddleOCR: Awesome multilingual OCR toolkits based on PaddlePaddle (practical ultra lightweight OCR system, support 80+ languages recognition, provide data annotation and synthesis tools, support training and deployment among server, mobile, embedded and IoT devices)在README_ch.md里可以看到ppocr最新加入的一些算法 

FAQ(doc/doc_ch/FAQ.md)里可以看到一些常见的问题

1.2 ppocr支持的算法

查看tools/program.py。

注:pp会不断更新,最新最全的还是要看源码,这里只是示例说明

ssert alg in [
        'EAST', 'DB', 'SAST', 'Rosetta', 'CRNN', 'STARNet', 'RARE', 'SRN',
        'CLS', 'PGNet', 'Distillation', 'NRTR', 'TableAttn', 'SAR', 'PSE',
        'SEED', 'SDMGR', 'LayoutXLM', 'LayoutLM', 'LayoutLMv2', 'PREN', 'FCE',
        'SVTR', 'ViTSTR', 'ABINet', 'DB++', 'TableMaster', 'SPIN', 'VisionLAN',
        'Gestalt', 'SLANet', 'RobustScanner', 'CT', 'RFL', 'DRRG', 'CAN',
        'Telescope'
    ]

检测:sast、psenet、db

识别:百度自研结合语义基于Transformer的srn、基于ctc的CRNN、StarNet和Rosetta,基于attention的RARE。80种语言

端到端识别:PGnet

Kie:sdmgr

sdmgr利用双模态信息(文本及坐标+视觉)进行图节点分类。图像特征通过UNet提取特征图,并通过ROI Pooling获取切片的图像特征。对每个字符进行one-hot编码,投影到低维空间,并输入Bi-LSTM获取文本特征。

特征融合尝试了三种方法(CONCAT、线性求和、克罗内克积),克罗内克积最优

sdmgr属于mmocr工作

GitHub - open-mmlab/mmocr at 4882c8a317cc0f59c96624ce14c8c10d05fa6dbc

原理:

带你读AI论文:SDMG-R结构化提取—无限版式小票场景应用 - 华为云开发者联盟 - 博客园

sdmgr_cc_moe的博客-CSDN博客

DocVQALayoutLM、LayoutLMv2,LayoutXLM, ser和re由LayoutXLM实现

语义实体识别SER

关系抽取RE:

Style-Text

    基于论文《Editing Text in the Wild》中的SRNet

PaddleOCR/data_synthesis.md at release/2.4 · PaddlePaddle/PaddleOCR · GitHub

其他数据合成工具

SynthText_Chinese_versionTextRecognitionDataGeneratortext_renderer

SynthText3D、UnrealText、StyleText

PP-Structure之版面分析: 文字、标题、图片、列表和表格5类,基于yolov2

PP-Structure之表格解析:基于RARE算法,后面又出了个SLANet

轻量化移动端部署:EasyEdge、Paddle-Lite

1.3一张图的OCR

    过程分为:文本检测——切子图,并根据长宽比旋转90度,主要是为了处理竖排文字(0、90度)——文本方向分类(0、180度)——文本识别。

检测:DBNet 

分类:每个切片的方向分类,在pp中将切片方向分类放在检测后面,而不是方向分类——检测——识别。这样做的好处有:1)可以处理竖排文字 2)支持对同一张图中多个不同文档图像的处理。缺陷1)实测经常会发生在第一步旋转90度就转错了 2)耗时比对整张图像进行方向分类慢一点。

        在paddleocr的方向分类模块中,不仅仅依赖模型的分类输出,还会参考softmax的值,当方向分类为180且置信度大于0.9时才会将图片进行旋转180度操作。如果定位时有上下行都切出来的现象,就容易造成置信度0.5左右的分类错误。猜想是因为模型不太确定这个几个字是上下结构或左右结构。

        而且我不理解,为什么模型判断方向这一步只做二分类,不做四分类。很多单字的定位最后因为方向搞错了,无法识别正确。

识别:CRNN

        pp做了很多速度和显存上的优化,其中动态宽度对速度的提升非常有效果,实测自己训练的大训练模型(基于Restnet,200M左右)修改动态宽度和其他一些点后和pp的速度也很接近了,考虑到字数和时间比,甚至更快一点。关于pp的轻量化,知乎有一篇很好的介绍:PaddleOCR为何这么轻?

        普通的图片走完ocr流程的时间在0.3s左右,如果文字较多,时间会相对增加。可以下载开源模型,然后根据场景更改一下调用流程、阈值、前处理后处理。

1.4 半自动标注工具PPOCRLabel

        要解析一些证件,自研和百度的都不能定位得特别好,想试一下半自动标注能不能偷懒一点。

        安装:过程参考PPOCRLable下的README即可,不再赘述,一种不行就换一种,我最后是通过python脚本运行成功的,报需要啥安装包就装啥,有安装包冲突就搜索一下,安装别人推荐的版本,包括什么“Class RunLoopModeTracker is implemented in both”报错闪退。内网环境下不了模型就手动操作,改改代码的download部分,加个if os.path.exists。

        平时都是labelme标,这个界面用起来有点别扭,为啥要这么搞呢?堪比单个脚踏板的汽车。包括:

1、图片后缀要是正常的图片后缀,不能没后缀

2、不接受指定文件夹下的循环嵌套文件夹,只能是对应文件夹下的图片

3、不能设置标签路径,和图片放一起了

4、多个文件的标签放到了一个标注文件中,如果我想把数据放在几个文件夹中标,就很难受

5、进入标注状态后不能esc,必须让我标注

6、自动标注后的标签是矩形,进行矩形标注会显示4个点,我想改成4点的标注都不知道从哪改,Label.txt里没有显式形状的说明。

7、有旋转90方向,但太隐蔽,要用3个jia4组成快捷键,不好用。

        这个工具目前最大价值在于半自动标注了,其他真的不咋好用。

1.5训练

        用自己的数据更新定位识别模型

1.5.1训练定位模型

python tools/train.py -x config/det/***/***.yml

      yml文件需指定:预训练模型路径pretrained_model、数据集图片路径data_dir、标签文件路径label_file_list。如果没有共享内存,在loadel中增加use_shared_memory: False。

        这里需要关注的是标签格式,paddle自定义了一个怪怪的标签文件格式,从yml中的label_file_list可以看出标签文件label.txt是个txt文件。每一行对应一张图片及其所有定位框的标签。定位和识别yml的dataset的name都为SimpleDataset,不同的是使用了不同的dataset的transforms。

        关注SimpleDataset、transformers中的DetLabelEncode,

# ppocr/data/simple_dataset.py
class SimpleDataSet(Dataset):
    ...
    def get_ext_data(self):
        ext_data_num = 0
        for op in self.ops:
            if hasattr(op, 'ext_data_num'):
                ext_data_num = getattr(op, 'ext_data_num')
                break
        load_data_ops = self.ops[:self.ext_op_transform_idx]
        ext_data = []

        while len(ext_data) < ext_data_num:
            file_idx = self.data_idx_order_list[np.random.randint(self.__len__(
            ))]
            data_line = self.data_lines[file_idx]
            data_line = data_line.decode('utf-8')
            #  self.delimiter = dataset_config.get('delimiter', '\t') 分割符是\t
            # 每行是一个标签
            substr = data_line.strip("\n").split(self.delimiter)
            file_name = substr[0]
            file_name = self._try_parse_filename_list(file_name)
            label = substr[1]
            img_path = os.path.join(self.data_dir, file_name)
            data = {'img_path': img_path, 'label': label}
            if not os.path.exists(img_path):
                continue
            with open(data['img_path'], 'rb') as f:
                img = f.read()
                data['image'] = img
            # transform将 label 进行了转化,所以后面才有polys
            data = transform(data, load_data_ops)

            if data is None:
                continue
            if 'polys' in data.keys():
                if data['polys'].shape[1] != 4:
                    continue
            ext_data.append(data)
        return ext_data
# ppocr/data/imaug/label_ops.py

class DetLabelEncode(object):
    def __init__(self, **kwargs):
        pass

    def __call__(self, data):
        label = data['label']
        label = json.loads(label)
        nBox = len(label)
        boxes, txts, txt_tags = [], [], []
        for bno in range(0, nBox):
            box = label[bno]['points']   # 点坐标
            txt = label[bno]['transcription']   # 文本
            boxes.append(box)
            txts.append(txt)
            if txt in ['*', '###']:   # 这2种是要ignore的
                txt_tags.append(True)
            else:
                txt_tags.append(False)
        if len(boxes) == 0:
            return None
        boxes = self.expand_points_num(boxes)   # 不断复制最后一个box至最大点数一致
        boxes = np.array(boxes, dtype=np.float32)
        txt_tags = np.array(txt_tags, dtype=np.bool_)

        data['polys'] = boxes
        data['texts'] = txts
        data['ignore_tags'] = txt_tags
        return data

再参考labelme标注的json文件转为paddleOCR提供的标注文件格式_labelme标注文件转为 paddleocr训练文件格式_月夜竹清的博客-CSDN博客

可知,标签文件格式为

aaa.jpg    [{"transcription":"xxx","points":[[x0,y0],[x1,y1],[x2,y2],[x3,y3]],...}]

问题1: 在读取文件的时候老是报错"Expecting property name enclosed in double quotes",查了下说是因为json.loads必须是双引号,老改不对,在ppocr中查了一下,正确写法如下,原来[{},{}]这种数据也能json.dumps成字符串!

# ppocr/utils/gen_label.py
out_file.write(img_path+'\t'+json.dumps(label, ensure_ascii=False)+'\n')

问题2:自训练的模型转infer模型后效果不好,这么小的模型,这么大的数据,而且主要是图像中最后一个长切片不好,为什么呢?一开始怀疑是crop的问题,数据增强的时候是否容易把下面的切掉,实验固定crop(0,0,w,h)后还是不行,推翻了这个推测。后来怀疑是参数的问题,网上很多人自训练检测模型转inference模式后,效果不行,需要指定det_limit_side_len=736、det_limit_type='min',默认的参数和训练时eval的不一致。这个解决方案很行!效果差距巨大,为何paddleocr要这样!而且还很隐蔽不易发现。深入研究后,发现,开源模型指不指定这2个参数差距不大,甚至不一定是正效果(某个场景,v2指定是正效果,v3v4是负效果),但是自训练模型会影响很大,整体影响1%,某个字段影响6%。

        官方说法是训练时为了进行评估,resize到XXX,推理时为了速度又将长边限制在YYY。见以下FAQ。

https://github.com/PaddlePaddle/PaddleOCR/blob/dygraph/doc/doc_ch/detection.md#5-faq

        简单来说,原来默认infer时把输入默认为,最长边960(det_limit_side_len=960、det_limit_type='max'),但训练时默认又是最短边736(见DetResizeForTest类的默认处理方法)。

        如果不确定自己是否修改到位,可以分别试一下 tools/infer/predict_det.py和tools/infer_det.py的效果。以下2个函数都会生成可视化结果及box的text。

# 基于训练模型,加载checkpoint模型
python tools/infer_det.py -c config/*** -o Global.infer_img='xxx.jpg'

# 基于转成的推理模型
python tools/infer/predict_det.py --miage_dir='xxx.jpg'  --det_model_dir="xxx" --det_limit_type=min --det_limit_side_len=736

训练检测模型,同样遭遇的人:

https://github.com/PaddlePaddle/PaddleOCR/issues/9116

https://github.com/PaddlePaddle/PaddleOCR/issues/2080

问题3:自训练的db模型,边框太贴字了,导致识别效果不好,尤其是第一个和最后一个字

这就要联系到db的原理,推理时自动扩大区域,查看yml中的PostProcess参数,里面有个unclip_ratio默认值1.5,增大该值可以扩大缩放程度,改成2.2还不错。

关于Paddle OCR检测器检测框偏小的解决方法_det_db_unclip_ratio_AI浩的博客-CSDN博客

问题4:训练v4的检测模型时,有cml、teacher、student3种yml文件,分别是什么意思?

cml是蒸馏后的小模型,约5M,用cml蒸馏方法训练,训出1个teacher2个student;teacher是用在server端的大模型,用dml方法训练,训出2个teacher,约110M;student我的理解是在小模型上finetine

v3开始引入了一堆互学习等的概念,具体参考https://zhuanlan.zhihu.com/p/511564666

大致概念如下

DML(Deep Mutual Learning):教师模型互学习策略、蒸馏策略。通过两个结构相同的模型互相学习,可以有效提升文本检测模型的精度。教师模型采用DML策略,同时训练2个Teacher出来
UDML(Unified-Deep Mutual Learning):联合互学习策略,。在PP-OCRv3中,针对两个不同的SVTR_LCNet和Attention结构,对他们之间的PP-LCNet的特征图、SVTR模块的输出和Attention模块的输出同时进行监督训练。
CML(Collaborative Mutual Learning) 协同互学习文本检测蒸馏策略,针对学生模型的训练方式,基于Teacher和2个student,同时训练2个Student出来

问题5:这么多花里胡哨的yml文件,那该如何训练呢?

先说结论,可以训练v2、v3,但训不了v4的det模型。

PP-OCRv3有官方训练教程,和教程稍微有点不同的是现在在模型已经有student模型了,不用从best_accuracy转出来

https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.6/doc/doc_ch/PP-OCRv3_det_train.md

自己瞎捣鼓v4 似乎训练过程有点问题,cml训练时PPLCNetNew找不到对应模型,改成PPLCNetv3报别的错,如下Question,也没有官方解决回复。student的训练下了配置文件中的预训练模型,又是找不到各种参数,私以为配置文件里的那个权重是不全的只有backbone没有neck和head,应该下载README.md里的文件,但是下不了没对应文件可下。teacher模型太大,不想用,没必要。

PPLCNetNew找不到對應模型 · Issue #10685 · PaddlePaddle/PaddleOCR · GitHub

参考v3的finetune训练教程,尝试训练PP-OCRv4的检测模型,失败了,等官方吧。

a、首先从README.md中找到对应训练模型ch_PP-OCRv4_det_distill_train.tar,下载后提取Student参数。。

百度快修一修吧T_T

问题6:训练v4的student检测模型时,“The pretrained params backbone.xxx not in model”。

对应配置文件为ch_pp-ocrv4_det_student.yml,下载对应的预训练模型后训练,产生如题报错。

vim ppocr/utils/save_load.py 后发现待训练模型参数有905层,预训练模型参数只有836层,且还有5层不在待训练模型中。

强行训练后,缺失大量参数导致检测效果很不好。

这个人也有同样的问题在微调模型的时候,“ppocr WARNING: The pretrained params conv**** not in model”正常吗? · Issue #6684 · PaddlePaddle/PaddleOCR · GitHub

问题7:paddle中db如何调参

训练后,准召都在0.9附近,出现了很明显的漏检,而且有大切片的漏检。

db主要是调后处理参数,参数在配置文件yml的PostProcess中,除了上文unclip_ratio外,还有几个重要参数thresh=0.3、box_thresh=0.6、max_candidates=1000。查看DBPostProcess方法可知,thresh过滤了小于该值的map位置,box_thresh过滤了小于该得分的box、max_candidates限制了切片数量。打印中间过程后,我将box_thresh调小到了0.35。此外,在后处理类中,还限定了最小面积min_size=3

1.5.2训练识别模型

识别模型的标签文件比较简单,间隔符为\t,每一行为

aaa.jpg    text

除上述格式外,还有另一种格式用于处理相同标签的数据增强数据集,请参考如下链接。此外,该文章还提到在训练V3识别模型时“ 由于预训练模型提供的是蒸馏模型,需先将Student模型的参数提取出

【官方】十分钟完成 PP-OCRv3 识别全流程实战_AI Studio的博客-CSDN博客

训练识别模型也可能遇到参数不对齐的问题,主要关注resize、字典

训练识别模型,同样遭遇的人:

https://github.com/PaddlePaddle/PaddleOCR/pull/2470

1.6 基于不同paddle的模型进行推理

想比较下v2、v3、v4在自己场景中的推理能力,同样的代码,v2推理有问题。经排查,推理老的模型需要指定ocr_version

经确认,v4>v3>v2

from paddleocr import PaddleOCR
import cv2

ocr_engine = PaddleOCR(
    cls_model ='XXX_cls_infer',
    use_angel_cls=True,
    
    det_model_dir = 'XXX_det_infer',
    rec_model_dir='XXX_rec_infer',

    ocr_version="PP-OCRv2",

    # 检测模型重要参数
    det_limit_type="min",
    det_limit_side_len=736,)

img_path='xxx.jpg'
res = ocr_engine.ocr(img=cv2.imread(img_path))

1.7 安装环境

        之所以在这里还要提一下paddle的安装,是因为,paddle安装总是常看常新,在不同环境下想要把paddlepaddle-gpu搭建起来,真是丰富多彩的奇妙之旅。

        首先要知道,paddlepaddle是cpu版本,paddlepaddle-gpu才即支持gpu又支持cpu。paddle框架比pytorch对cuda、cudnn更为敏感,换句话说,就是兼容性不好。所以一旦cuda不兼容,会有各种报错。

1.7.1 查看cuda版本

        这里可以分别查看nvidia-smi和nvcc -V显示的cuda版本,分别表示cuda的driver API和runtime API。目前我的安装都是基于nvcc -V显示的cuda版本,nvidia-smi尚未试验过(即尚未实验过没有单独安装cuda下,只有显卡驱动显示cuda的情况下安装gpu版本的paddle)。

【精选】cuda 的driver API 和 runtime API_driver api version: 12.2, runtime api version: 11._Be long的博客-CSDN博客

https://blog.csdn.net/weixin_44015965/article/details/122176244

1.7.2 方案一:基于nvcc-v 显示的cuda版本选择正确的paddlepaddle-gpu版本

这里习惯性地用pip进行安装,但是!pip显示的并不全,比较惨的时候恰好和你的cuda都不匹配。这时候需要自行下载whl进行安装,paddlepaddle-gpu==2.5.2.post117表示cuda版本为117,paddlepaddle-gpu版本为2.5.2

下载网址如下:

https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html

以下官方教程还给了个 “包含 cuDNN 动态链接库的 PaddlePaddle”的下载地址,暂不知道具体含义

Linux 下的 PIP 安装-使用文档-PaddlePaddle深度学习平台

1.7.3 方案二:基于pip安装cuda驱动及paddle

    有时并没有单独安装的cuda及cudann,即nvcc -V无结果,/usr/local 也没cuda文件夹。实际上可以不单独安装cuda,pytorch安装时可以顺带安装cuda及cudann,paddle同理。不同的是paddle需要使用pip来安装cuda,而不是安装paddle时自带。在GPU服务器上安装步骤如下:

# 基于pip安装cuda及paddle
pip install nvidia-cuda-runtime-cu11
pip install nvidia-cublas-cu11
pip install nvidia-cuda-nvrtc-cu11
pip install nvidia-cudnn-cu11
pip install paddlepaddle-gpu=2.4.1.post112
# 软链接示例
ln -s /usr/local/lib/python3.7/site-packages/nvidia/cublas/lib/libcublas.so.11 /usr/local/lib/python3.7/site-packages/nvidia/cublas/lib/libcublas.so
# 动态链接库
export LD_LIBRARY_PATH =patha/lib:pathb/lib:pathc/lib

import paddle时,如提示缺失某个so文件,安装并添加到LD_LIBRARY_PATH 即可,例如提示缺失了libcudart.so.11.0,这个在cuda_runtime时会安装

1.7.4确认安装成功

import paddle
paddle.utils.run_check()

无需模型即可验证paddle是否安装成功,安装失败的表现包括:

  • 运行run_check提示cuda相关报错
  • 运行ocr模型识别文字乱码
  • 运行检测模型输出的box异常
  • segment错误
  • 程序卡死,需外部中断

二、paddle lite

       用于移动端部署,有完备的demo文件,训练好的文件转为nb格式即可。具体模型是否适合各种芯片,需要deploy、FastDeploy等处的说明。

https://github.com/PaddlePaddle/FastDeploy

2.1模型转换

训练模型——推理infer模型——nb模型

第一步:

python tools/export_model.py -c config/***/*.yml -o weight=***

转为pdmodel和pdiparams文件

第二步:

paddle_lite_opt --optimize_out=*** --optimize_out_type=naive_buffer --valid_targets=arm --model_file=***.pdmodel --param_file=***.pdiparams

转为nb文件

三、paddle其他组件

        除去paddleocr外,百度还有几个专用场景套件,如PaddleClas分类、PaddleDetection检测、PaddleSeg分割、PaddleGAN、PaddleVideo、ERNIEKit语义、PLSC海量分类、ElasticCTR推荐、Parakeet语音合成、PGL图学习、PARL强化学习、Paddle Quantum量桨、PaddleHelix生物计算。

        其中我今后比较会用到的组件有:paddlenlp、UIE、文心大模型。

2.1PaddleNLP

2.2文心大模型

        ERNIE 是百度基于transformer研发的,可视为一个比较强的中文transformer,backbone结构没什么特殊,主要是设计了一些特殊的预训练任务,有基于mask的预训练,也有迁移到不同任务上的预训练。

2.3UIE

        另一个文本方面的大统一思想是UIE,本质是基于ERNIE的双指针解码(仅谈paddle的实现)

UIE教程(搜索 五条标注数据搞定快递单信息抽取):

优点:教程给的是“北京市海淀区上地十街10号18888888888张三”,改成“北京市海淀区上地十街10号18888888888交款人:李白百”后还是能把名字抽出来。

from paddlenlp import Taskflow

schema = ["姓名", "省份", "城市", "县区", "电话", "详细地址"]
ie = Taskflow("information_extraction", schema=schema)
res=ie("交款人:李白百 北京市海淀区上地十街10号18888888888")
print(res)
# [{'姓名': [{'text': '李白百', 'start': 4, 'end': 7, 'probability': 0.9335348137713595}], '县区': [{'text': '海淀区', 'start': 11, 'end': 14, 'probability': 0.9149133074831752}]}]

uie的其他信息:

《Unified Structure Generation for Universal Information Extraction》

    Yaojie Lu等人提出了开放域信息抽取的统一框架,这一框架在实体抽取、关系抽取、事件抽取、情感分析等任务上都有着良好的泛化效果。开放域信息抽取可以实现零样本(zero-shot)或者少样本(few-shot)抽取

(杂谈)关于UIE的一点感想_常鸿宇的博客-CSDN博客_uie原理

  

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐