01  数据分析与加载

1.1 数据分析

1. 数据分析与加载

数据概览:7000 多条酒店评论数据,5000 多条正向评论,2000 多条负向评论

推荐实验:情感/观点/评论 倾向性分析

数据来源:携程网

原数据集:ChnSentiCorp_htl,由 谭松波 老师整理的一份数据集

加工处理:构造平衡数据,即正向评论与负向评论数量接近,各2000多条。

数据集详细信息:

https://github.com/SophonPlus/ChineseNlpCorpus/blob/master/datasets/ChnSentiCorp_htl_all/intro.ipynb
https://raw.githubusercontent.com/SophonPlus/ChineseNlpCorpus/master/datasets/ChnSentiCorp_htl_all/ChnSentiCorp_htl_all.csv

1.2 划分训练、开发、测试数据集


data_list_path="./data"

with open(os.path.join(data_list_path, 'eval.txt'), 'w', encoding='utf-8') as f_eval:
    f_eval.seek(0)
    f_eval.truncate()
    
with open(os.path.join(data_list_path, 'train.txt'), 'w', encoding='utf-8') as f_train:
    f_train.seek(0)
    f_train.truncate() 

with open(os.path.join(data_list_path, 'test.txt'), 'w', encoding='utf-8') as f_test:
    f_test.seek(0)
    f_test.truncate()

with open(os.path.join(data_list_path, 'all.txt'), 'r', encoding='utf-8') as f_data:
    lines = f_data.readlines()

i = 0
with open(os.path.join(data_list_path, 'eval.txt'), 'a', encoding='utf-8') as f_eval,open(os.path.join(data_list_path, 'test.txt'), 'a', encoding='utf-8') as f_test,open(os.path.join(data_list_path, 'train.txt'), 'a', encoding='utf-8') as f_train:
    for line in lines:
        words = line.split('\t')[-1].replace('\n', '')
        label = line.split('\t')[0]
        labs = ""
        # 划分验证集
        if i % 10 == 1:
            labs = label + '\t' + words + '\n'
            f_eval.write(labs)
        # 划分测试集
        elif i % 10 == 2:
            labs = label + '\t' + words + '\n'
            f_test.write(labs)
        # 划分训练集
        else:
            labs = label + '\t' + words + '\n'
            f_train.write(labs)
        i += 1

1.3 准备好的数据分析

从本地文件创建数据集,根据本地数据集的格式给出读取 function 并传入  load_dataset() 中创建数据集。

def read(data_path):
    with open(data_path, 'r', encoding='utf-8') as f:
        # 跳过列名
        next(f)
        for line in f:
            words, labels = line.strip('\n').split('\t')
            words = words.split('\002')
            labels = labels.split('\002')
            yield {'text': words[0], 'label': labels[0]}

train_ds = load_dataset(read, data_path='./data/train.txt',splits='train',lazy=False)
dev_ds = load_dataset(read, data_path='./data/eval.txt',splits='dev',lazy=False)
test_ds = load_dataset(read, data_path='./data/test.txt',splits='test',lazy=False)

看前两个样例

print("训练集数据:{}\n".format(train_ds[0:2]))
print("验证集数据:{}\n".format(dev_ds[0:2]))
print("测试集数据:{}\n".format(test_ds[0:2]))

print("训练集样本个数:{}".format(len(train_ds)))
print("验证集样本个数:{}".format(len(dev_ds)))
print("测试集样本个数:{}".format(len(test_ds)))

输出结果:

训练集数据:[{'text': '宾馆在小街道上,不大好找,但还好北京热心同胞很多~宾馆设施跟介绍的差不多,房间很小,确实挺小,但加上低价位因素,还是无超所值的;环境不错,就在小胡同内,安静整洁,暖气好足-_-||。。。呵还有一大优势就是从宾馆出发,步行不到十分钟就可以到梅兰芳故居等等,京味小胡同,北海距离好近呢。总之,不错。推荐给节约消费的自助游朋友~比较划算,附近特色小吃很多~', 'label': '1'}, {'text': 'CBD中心,周围没什么店铺,说5星有点勉强.不知道为什么卫生间没有电吹风', 'label': '1'}]

验证集数据:[{'text': '早餐很丰富,服务也热情,早上很早退房时,前台值此人员办理手续也非常快.', 'label': '1'}, {'text': '沈阳市政府的酒店,比较大气,交通便利,出门往左就是北陵公园,环境好。', 'label': '1'}]

测试集数据:[{'text': '这次是308的行政大床,总体感觉非常不错,就是价格稍许高了点,旁边有个五星的豪华客房才398。估计小天鹅也只有这个房型以上的,看得过去,以前住过的房间实在是很差。以后大家如果要住这里,还是选这个行政大床吧!', 'label': '1'}, {'text': '政府酒店感觉很气派,而且很干净,整个酒店的房间布局也很整齐.5月份入住的,由于第一天房间不能上网,和前台协调又换了一间可以上网的,还帮忙调试了笔记本,服务很周到.正好住的两天都有人结婚,感觉酒店一楼的宴会厅很适合婚礼', 'label': '1'}]

训练集样本个数:3910
验证集样本个数:488
测试集样本个数:488

统计训练数据的正负样例

spam = 0
label_count = [0, 0] 
for data in train_ds:
    if data['label'] == '0':
        label_count[0] += 1
    elif data['label'] == '1':
        label_count[1] += 1
    else:
        pass

print(label_count)

输出:

[1956, 1954]

02  PaddleNLP 预训练模型加载与模型 finetune

本示例展示了以 ERNIE (Enhanced Representation through Knowledge Integration)代表的预训练模型如何 Finetune 完成中文文本分类任务。

2.1 简介

本项目针对中文文本分类问题,采用 PaddleNLP 中的文本分类模型作为与训练模型来进行 finetune,

PaddleNLP 是飞桨自然语言处理开发库开源了一系列模型:

- BERT(Bidirectional Encoder Representations from Transformers)中文模型,简写 bert-base-chinese, 其由12层 Transformer 网络组成。

- ERNIE(Enhanced Representation through Knowledge Integration),支持ERNIE 1.0中文模型(简写ernie-1.0)和ERNIE Tiny中文模型(简写ernie-tiny)。其中 ernie 由12层Transformer网络组成,ernie-tiny 由3层 Transformer 网络组成。

- RoBERTa (A Robustly Optimized BERT Pretraining Approach),支持24层Transformer 网络的 roberta-wwm-ext-large 和12层 Transformer 网络的 roberta-wwm-ext。

以下是本项目主要代码结构及说明(该结构也是 PaddleNLP 官方示例的结构):​​​​​​​

pretrained_models/├── deploy # 部署│   └── python│       ├── onnxruntime_predict.py  #│       ├── openvino_predict.py  #│       └── predict.py  # python预测部署示例├── export_model.py # 动态图参数导出静态图参数脚本├── predict.py # 预测脚本├── README.md # 使用说明└── train.py # 训练评估脚本

2.2 预训练模型加载

完整的训练代码在 train.py 文件中​​​​​​​

mport paddlenlp as ppnlp

model = ppnlp.transformers.ErnieForSequenceClassification.from_pretrained('ernie-1.0', num_classes=2)
tokenizer = ppnlp.transformers.ErnieTokenizer.from_pretrained('ernie-1.0')

2.3 模型训练

$ python pretrained_models/train.py --device cpu --save_dir ./checkpoints

可支持配置的参数:​​​​​​​

* save_dir:可选,保存训练模型的目录;默认保存在当前目录checkpoints文件夹下。
* max_seq_length:可选,ERNIE/BERT模型使用的最大序列长度,最大不能超过512, 若出现显存不足,请适当调低这一参数;默认为128。
* batch_size:可选,批处理大小,请结合显存情况进行调整,若出现显存不足,请适当调低这一参数;默认为32。
* learning_rate:可选,Fine-tune的最大学习率;默认为5e-5。
* weight_decay:可选,控制正则项力度的参数,用于防止过拟合,默认为0.00。
* epochs: 训练轮次,默认为5。
* warmup_proption:可选,学习率warmup策略的比例,如果0.1,则学习率会在前10%训练step的过程中从0慢慢增长到learning_rate, 而后再缓慢衰减,默认为0.1。
* init_from_ckpt:可选,模型参数路径,热启动模型训练;默认为None。
* seed:可选,随机种子,默认为1000.
* device: 选用设备进行训练,可选cpu或gpu。如使用gpu训练则参数gpus指定GPU卡号。
```
代码示例中使用的预训练模型是ERNIE,如果想要使用其他预训练模型如BERT,RoBERTa,Electra等,只需更换model 和 tokenizer即可。

代码示例中使用的预训练模型是 ERNIE,如果想要使用其他预训练模型如 BERT,RoBERTa,Electra 等,只需更换 model  和 tokenizer 即可。

(paddle_env) dd@dd:~/workspace/nlp_space/openvino_paddle_nlp$ python pretrained_models/train.py --device cpu
[2021-11-21 23:59:42,057] [    INFO] - Already cached /home/dd/.paddlenlp/models/ernie-1.0/ernie_v1_chn_base.pdparams
[2021-11-21 23:59:49,301] [    INFO] - Already cached /home/dd/.paddlenlp/models/ernie-1.0/vocab.txt
global step 10, epoch: 1, batch: 10/123, loss: 0.64730, accu: 0.58750, speed: 0.33 step/s
global step 20, epoch: 1, batch: 20/123, loss: 0.62281, accu: 0.62500, speed: 0.32 step/s
global step 30, epoch: 1, batch: 30/123, loss: 0.39669, accu: 0.67292, speed: 0.32 step/s
global step 40, epoch: 1, batch: 40/123, loss: 0.29887, accu: 0.69141, speed: 0.32 step/s
global step 50, epoch: 1, batch: 50/123, loss: 0.43363, accu: 0.70937, speed: 0.31 step/s
global step 60, epoch: 1, batch: 60/123, loss: 0.76214, accu: 0.71667, speed: 0.32 step/s
global step 70, epoch: 1, batch: 70/123, loss: 0.54669, accu: 0.72366, speed: 0.32 step/s
global step 80, epoch: 1, batch: 80/123, loss: 0.33911, accu: 0.73086, speed: 0.32 step/s
global step 90, epoch: 1, batch: 90/123, loss: 0.44415, accu: 0.73542, speed: 0.31 step/s
global step 100, epoch: 1, batch: 100/123, loss: 0.56388, accu: 0.73969, speed: 0.31 step/s
global step 110, epoch: 1, batch: 110/123, loss: 0.43487, accu: 0.74432, speed: 0.31 step/s
global step 120, epoch: 1, batch: 120/123, loss: 0.43901, accu: 0.74661, speed: 0.31 step/s
eval loss: 0.39934, accu: 0.81557
global step 130, epoch: 2, batch: 7/123, loss: 0.45157, accu: 0.85034, speed: 0.22 step/s
global step 140, epoch: 2, batch: 17/123, loss: 0.39118, accu: 0.84528, speed: 0.32 step/s
global step 150, epoch: 2, batch: 27/123, loss: 0.35139, accu: 0.83940, speed: 0.32 step/s
global step 160, epoch: 2, batch: 37/123, loss: 0.31550, accu: 0.83652, speed: 0.32 step/s
global step 170, epoch: 2, batch: 47/123, loss: 0.19468, accu: 0.84180, speed: 0.32 step/s
global step 180, epoch: 2, batch: 57/123, loss: 0.19839, accu: 0.84530, speed: 0.31 step/s
global step 190, epoch: 2, batch: 67/123, loss: 0.27514, accu: 0.84417, speed: 0.32 step/s
global step 200, epoch: 2, batch: 77/123, loss: 0.37271, accu: 0.84294, speed: 0.31 step/s
global step 210, epoch: 2, batch: 87/123, loss: 0.54216, accu: 0.83952, speed: 0.31 step/s
global step 220, epoch: 2, batch: 97/123, loss: 0.37325, accu: 0.83680, speed: 0.31 step/s
global step 230, epoch: 2, batch: 107/123, loss: 0.41819, accu: 0.83887, speed: 0.32 step/s
global step 240, epoch: 2, batch: 117/123, loss: 0.27075, accu: 0.83797, speed: 0.32 step/s
eval loss: 0.38315, accu: 0.80943
global step 250, epoch: 3, batch: 4/123, loss: 0.34273, accu: 0.85714, speed: 0.22 step/s
global step 260, epoch: 3, batch: 14/123, loss: 0.21802, accu: 0.88111, speed: 0.29 step/s
global step 270, epoch: 3, batch: 24/123, loss: 0.11573, accu: 0.88972, speed: 0.31 step/s
global step 280, epoch: 3, batch: 34/123, loss: 0.14647, accu: 0.89872, speed: 0.31 step/s
global step 290, epoch: 3, batch: 44/123, loss: 0.16664, accu: 0.90025, speed: 0.32 step/s
global step 300, epoch: 3, batch: 54/123, loss: 0.19257, accu: 0.90285, speed: 0.32 step/s
global step 310, epoch: 3, batch: 64/123, loss: 0.48683, accu: 0.90289, speed: 0.31 step/s
global step 320, epoch: 3, batch: 74/123, loss: 0.39716, accu: 0.90371, speed: 0.32 step/s
global step 330, epoch: 3, batch: 84/123, loss: 0.22350, accu: 0.89979, speed: 0.32 step/s
global step 340, epoch: 3, batch: 94/123, loss: 0.21223, accu: 0.89887, speed: 0.32 step/s
global step 350, epoch: 3, batch: 104/123, loss: 0.23067, accu: 0.89926, speed: 0.32 step/s
global step 360, epoch: 3, batch: 114/123, loss: 0.37244, accu: 0.89748, speed: 0.32 step/s
eval loss: 0.41436, accu: 0.81967
global step 370, epoch: 4, batch: 1/123, loss: 0.27889, accu: 0.90816, speed: 0.22 step/s
global step 380, epoch: 4, batch: 11/123, loss: 0.16075, accu: 0.92671, speed: 0.32 step/s
global step 390, epoch: 4, batch: 21/123, loss: 0.07455, accu: 0.93148, speed: 0.32 step/s
global step 400, epoch: 4, batch: 31/123, loss: 0.06919, accu: 0.93381, speed: 0.32 step/s
global step 410, epoch: 4, batch: 41/123, loss: 0.10401, accu: 0.93456, speed: 0.32 step/s
global step 420, epoch: 4, batch: 51/123, loss: 0.08641, accu: 0.93295, speed: 0.32 step/s
global step 430, epoch: 4, batch: 61/123, loss: 0.18302, accu: 0.93812, speed: 0.32 step/s
global step 440, epoch: 4, batch: 71/123, loss: 0.10652, accu: 0.93962, speed: 0.31 step/s
global step 450, epoch: 4, batch: 81/123, loss: 0.09389, accu: 0.93868, speed: 0.32 step/s
global step 460, epoch: 4, batch: 91/123, loss: 0.09252, accu: 0.94203, speed: 0.32 step/s
global step 470, epoch: 4, batch: 101/123, loss: 0.19676, accu: 0.94333, speed: 0.31 step/s
global step 480, epoch: 4, batch: 111/123, loss: 0.06313, accu: 0.94337, speed: 0.32 step/s
eval loss: 0.56647, accu: 0.80123
global step 490, epoch: 4, batch: 121/123, loss: 0.08893, accu: 0.95000, speed: 0.21 step/s
global step 500, epoch: 5, batch: 8/123, loss: 0.15692, accu: 0.96906, speed: 0.34 step/s
global step 510, epoch: 5, batch: 18/123, loss: 0.06714, accu: 0.96146, speed: 0.32 step/s
global step 520, epoch: 5, batch: 28/123, loss: 0.07108, accu: 0.96332, speed: 0.32 step/s
global step 530, epoch: 5, batch: 38/123, loss: 0.20807, accu: 0.96633, speed: 0.32 step/s
global step 540, epoch: 5, batch: 48/123, loss: 0.08361, accu: 0.96938, speed: 0.32 step/s
global step 550, epoch: 5, batch: 58/123, loss: 0.04429, accu: 0.97154, speed: 0.31 step/s
global step 560, epoch: 5, batch: 68/123, loss: 0.18210, accu: 0.97080, speed: 0.31 step/s
global step 570, epoch: 5, batch: 78/123, loss: 0.00777, accu: 0.97127, speed: 0.32 step/s
global step 580, epoch: 5, batch: 88/123, loss: 0.06833, accu: 0.97101, speed: 0.32 step/s
global step 590, epoch: 5, batch: 98/123, loss: 0.03139, accu: 0.97167, speed: 0.32 step/s
global step 600, epoch: 5, batch: 108/123, loss: 0.05358, accu: 0.97195, speed: 0.31 step/s
eval loss: 0.63819, accu: 0.80123
global step 610, epoch: 5, batch: 118/123, loss: 0.10503, accu: 0.97813, speed: 0.21 step/s

训练结束后:


├── checkpoint
│   ├── model_120
│   │   ├── model_config.json
│   │   ├── model_state.pdparams
│   │   ├── tokenizer_config.json
│   │   └── vocab.txt
│   ├── model_240
│   │   ├── model_config.json
│   │   ├── model_state.pdparams
│   │   ├── tokenizer_config.json
│   │   └── vocab.txt
│   ├── model_360
│   │   ├── model_config.json
│   │   ├── model_state.pdparams
│   │   ├── tokenizer_config.json
│   │   └── vocab.txt
│   ├── model_480
│   │   ├── model_config.json
│   │   ├── model_state.pdparams
│   │   ├── tokenizer_config.json
│   │   └── vocab.txt
│   └── model_600
│       ├── model_config.json
│       ├── model_state.pdparams
│       ├── tokenizer_config.json
│       └── vocab.txt

03  模型导出与 Paddle 部署预测

3.1 模型导出

使用动态图训练结束之后,还可以将动态图参数导出成静态图参数,具体代码见 export_model.py。静态图参数保存在 output_path 指定路径中。运行方式:

python pretrained_models/export_model.py --
params_path=./checkpoint/model_600/model_state.
pdparams --output_path=./static_graph_params

其中params_path是指动态图训练保存的参数路径,output_path 是指静态图参数导出路径。

├── static_graph_params.pdiparams
├── static_graph_params.pdiparams.info
└── static_graph_params.pdmodel

​​​​​​​3.2  Paddle 部署预测

导出模型之后,可以用于部署,deploy/python/predict.py 文件提供了 python 部署预测示例。运行方式:​​​​​​​

python pretrained_models/deploy/python/predict.py --model_file=static_graph_params.pdmodel --params_file=static_graph_params.pdiparams --device 'cpu'

04  ONNX 转换与 ONNXRuntime 部署

使用 ONNXRunTime 加载转换的 ONNX 模型,对 test.txt 中随机选取的评论进行分类。

环境准备​​​​​​​

python -m pip install paddle2onnx onnx onnxruntime -i https://pypi.tuna.tsinghua.edu.cn/simple

Paddle2ONNX 静态图模型导出

将 Paddle 模型的参数保存在一个单独的二进制文件中(combined)模式:​​​​​​​

paddle2onnx --model_dir . --model_filename static_graph_params.pdmodel --params_filename static_graph_params.pdiparams --save_file model.onnx --opset_version 11

输出

model.onnx

ONNX 部署使用

(paddle_env) dd@dd:~/workspace/nlp_space/openvino_paddle_nlp$ python pretrained_models/deploy/python/onnxruntime_predict.py --model_file model.onnx
--device 'cpu'
[2021-11-22 01:23:05,213] [    INFO] - Already cached /home/dd/.paddlenlp/models/ernie-1.0/vocab.txt
原始输出: [array([[ 2.6874619, -2.9990964],
       [-4.0758886,  3.3297772]], dtype=float32)]
softmax转换后的预测概率: [[9.9662024e-01 3.3797831e-03]
 [6.0742983e-04 9.9939251e-01]]
原始输出: [array([[-3.7910347,  2.9819536]], dtype=float32)]
softmax转换后的预测概率: [[0.00114296 0.99885696]]
Data: 房间脏乱,服务态度不好!不过网络不太好,总是断线;早餐一般般       Label: 负面评论
Data: 3月住了4晚,感觉不错,比翠怡要好5月还来      Label: 正面评论
Data: 房间虽小,但很干净的!有点家的感觉,以后还会住!   Label: 正面评论
预测时间:966.909 ms

(paddle_env) dd@dd:~/workspace/nlp_space/openvino_paddle_nlp$ python pretrained_models/deploy/python/onnxruntime_predict.py --model_file model.onnx --device 'cpu'
[2021-11-22 01:23:12,093] [    INFO] - Already cached /home/dd/.paddlenlp/models/ernie-1.0/vocab.txt
原始输出: [array([[ 2.6874619, -2.9990964],
       [-4.0758886,  3.3297772]], dtype=float32)]
softmax转换后的预测概率: [[9.9662024e-01 3.3797831e-03]
 [6.0742983e-04 9.9939251e-01]]
原始输出: [array([[-3.7910347,  2.9819536]], dtype=float32)]
softmax转换后的预测概率: [[0.00114296 0.99885696]]
Data: 房间脏乱,服务态度不好!不过网络不太好,总是断线;早餐一般般       Label: 负面评论
Data: 3月住了4晚,感觉不错,比翠怡要好5月还来      Label: 正面评论
Data: 房间虽小,但很干净的!有点家的感觉,以后还会住!   Label: 正面评论
预测时间:280.334 ms

上述两次的时间相差较大

05  OpenVINO 部署模型

用 ONNX 模型作为中转,转换到 IR,实现 OpenVINO 部署,对 test.txt 中随机选取的评论进行分类。

(paddle_env) dd@dd:~/workspace/nlp_space/openvino_paddle_nlp$ python pretrained_models/deploy/python/openvino_predict.py --model_file model.onnx --device 'cpu'
[2021-11-22 01:32:25,707] [    INFO] - Already cached /home/dd/.paddlenlp/models/ernie-1.0/vocab.txt
原始输出: {'linear_147.tmp_1': array([[ 2.6874619, -2.999095 ],
       [-4.075888 ,  3.329778 ]], dtype=float32)}
概率输出: [[9.9662024e-01 3.3797878e-03]
 [6.0742983e-04 9.9939251e-01]]
原始输出: {'linear_147.tmp_1': array([[-3.791035 ,  2.9819534]], dtype=float32)}
概率输出: [[0.00114296 0.99885696]]
Data: 房间脏乱,服务态度不好!不过网络不太好,总是断线;早餐一般般       Label: 负面评论
Data: 3月住了4晚,感觉不错,比翠怡要好5月还来      Label: 正面评论
Data: 房间虽小,但很干净的!有点家的感觉,以后还会住!   Label: 正面评论
预测时间:194.682 ms

(paddle_env) dd@dd:~/workspace/nlp_space/openvino_paddle_nlp$ python pretrained_models/deploy/python/openvino_predict.py --model_file model.onnx --device 'cpu'
[2021-11-22 01:32:33,518] [    INFO] - Already cached /home/dd/.paddlenlp/models/ernie-1.0/vocab.txt
原始输出: {'linear_147.tmp_1': array([[ 2.6874619, -2.999095 ],
       [-4.075888 ,  3.329778 ]], dtype=float32)}
概率输出: [[9.9662024e-01 3.3797878e-03]
 [6.0742983e-04 9.9939251e-01]]
原始输出: {'linear_147.tmp_1': array([[-3.791035 ,  2.9819534]], dtype=float32)}
概率输出: [[0.00114296 0.99885696]]
Data: 房间脏乱,服务态度不好!不过网络不太好,总是断线;早餐一般般       Label: 负面评论
Data: 3月住了4晚,感觉不错,比翠怡要好5月还来      Label: 正面评论
Data: 房间虽小,但很干净的!有点家的感觉,以后还会住!   Label: 正面评论
预测时间:581.982 ms

速度方面:虽然测试的数据量较少,但是OpenVINO和 OnnxRuntime 推理时间还是少于 Paddle Inference 的,多测试几次,会发现 OpenVINO 和 OXNNX 的预测时间不太稳定,有待下一步研究。

上述模型由于没采用 GPU 训练,因此训练数据不够,在测试集上只到80%左右。训练不够充分。

06  总结

一开始使用4类情感的文本分析数据在 ’ernie‘上进行微调,初始时 acc 在30%左右,需要迭代的更多 epochs,后续会研究下多情感的分析。

环境,因为是使用自己的笔记本,GPU没配起来,做实验就慢了很多,只能在小数据集上完成整个

过程

本次在 win11 的 WSL2 中完成的,环境搭建,在搭环境的过程和使用 OpenVINO 的过程中,

OpenVINO 的官网提供的非常好的Demo学习。后续会针对 OpenVINO 与 kaldi 的语音识别的模型部署进行继续研究。


英特尔 OpenVINO开发工具套件高级课程,原价99元,限时免费学习,点击立即报名icon-default.png?t=M276https://bss.csdn.net/m/topic/intel_openvino/index/2/0?utm_source=wenzhang2​​​​​​​

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐