目录
- Motivation
- How to Attack
- Example of Attack
- How to Attack (White Box Attack)
- Black Box Attack
- One pixel attack
- Universal Adversarial Attack
- Attack in the Physical World
- “Backdoor” in Model
- The attack is so easy! Why?
- How to Defend
- Passive Defense
- Adversarial Training - 对抗训练 (Proactive Defense)
- Adversarial Training in NLP
- Generating Adversarial Sentences is Difficult
- Adversarial perturbations in embeddings
- Fast Gradient Method (FGM) 的 PyTorch 实现
- Projected Gradient Descent (PGD) 的 PyTorch 实现
- FreeAT
- YOPO (You Only Propogate Once)
- FreeLB
- References
Motivation
- Are networks robust to the inputs that are built to fool them?
- Useful for spam classification, malware detection, network intrusion detection, etc.
How to Attack
Example of Attack
- 下面以图像分类为例。benign image 表示未经修改的原图像,识别结果为 tiger cat。攻击的目的就是给 benign image 加上一个小噪声,使得分类器输出的类别不为 “猫”
- 而攻击类型也可以分为 Non-targeted 和 Targeted 两种
- Non-targeted: 让分类器输出任何非 “猫” 的类别
- Targeted: 让分类器输出指定的非 “猫” 类别 (e.g. Star fish)
上图中,加入的噪声甚至是人眼不可分辨的,分类器对 Benign image 进行分类时 Tiger Cat 的置信度都只有 0.64,但对 Acttacked Image 进行分类时 Star Fish 的置信度却达到了 1.00
How to Attack (White Box Attack)
- Non-targeted:
x
0
x^0
x0 为 benign image,
x
x
x 为 attacked image,在固定网络参数的情况下,我们想要使分类器输出的概率分布尽量远离 cat 的概率分布
进而得到如下的优化目标 (约束
d
(
x
0
,
x
)
≤
ε
d(x^0,x)\leq \varepsilon
d(x0,x)≤ε 保证了加入的 noise 不会被人眼察觉;
e
e
e 可以为 cross entropy):
- Targeted: 相比 Non-targeted,在优化目标中增加了一项,使得 attacked image 对应的概率分布尽量接近目标类别的概率分布
Non-perceivable
- 那么我们应该如何表示
d
d
d 来使得人眼无法感知到我们加入的 noise 呢?下图对比了 L2 norm 和 L-infinity norm,发现使用 L-infinity norm 更加合理
Fast Gradient Sign Method (FGSM) and Fast Gradient Method (FGM)
- paper: Explaining and Harnessing Adversarial Examples、Adversarial Training Methods for Semi-Supervised Text Classification
- 我们的目标是解如下的优化问题:
x
∗
=
arg min
d
(
x
0
,
x
)
≤
ε
L
(
x
)
x^*=\argmin_{d(x^0,x)\leq\varepsilon} L(x)
x∗=d(x0,x)≤εargminL(x) - Goodfellow 在 15 年的 ICLR 中提出了 FGSM。FGSM 只更新一次
x
x
x,可以保证更新后的
x
x
x 满足约束
- 随后,在 17 年的 ICLR 中,Goodfellow 对 FGSM 中计算扰动的部分做了一点简单的修改,提出了 FGM,也就是取消了符号函数,转而用 F-norm 作 scale:
x
t
←
x
t
−
1
−
ε
g
∣
∣
g
∣
∣
2
x^t\leftarrow x^{t-1}-\varepsilon \frac{g}{||g||_2}
xt←xt−1−ε∣∣g∣∣2g
Projected Gradient Descent (PGD)
- paper: Towards Deep Learning Models Resistant to Adversarial Attacks
- 在 FGM 中,对抗样本的生成采用了粗暴的 “一步到位” 的方法,而一步更新可能并不足以到达约束内的最优点。为此,Madry 在 18 年的 ICLR 中提出了 PGD,简单的说,就是 “小步走,多走几步”,如果走出了扰动半径为
ϵ
\epsilon
ϵ 的空间,就映射回 “球面” 上,以保证扰动不要过大。下面介绍其具体思路:
- 可以先不考虑约束条件,直接用梯度下降法求解
x
x
x:
- 那么现在如何满足该约束条件呢?我们可以直接简单粗暴的在每次更新后,都检查更新后的
x
x
x 是否满足约束,如果不满足,就强行将其修改回满足约束的情况:
事实上,在更新时,PGM 还会用梯度的 F-norm 作一个 scale,一次迭代的过程可以写为如下形式:
g
(
x
t
)
=
∇
x
L
(
θ
,
x
t
,
y
)
x
t
+
1
=
Π
x
+
S
(
x
t
+
α
g
(
x
t
)
/
∥
g
(
x
t
)
∥
2
)
\begin{aligned} g\left(x_{t}\right) &=\nabla_{x} L\left(\theta, x_{t}, y\right) \\ x_{t+1} &=\Pi_{x+\mathcal{S}}\left(x_{t}+\alpha g\left(x_{t}\right) /\left\|g\left(x_{t}\right)\right\|_{2}\right) \end{aligned}
g(xt)xt+1=∇xL(θ,xt,y)=Πx+S(xt+αg(xt)/∥g(xt)∥2)其中,
S
=
{
r
∣
r
∈
R
d
,
∣
∣
r
∣
∣
2
≤
ϵ
}
\mathcal S=\{r|r\in \R^d,||r||_2\leq\epsilon\}
S={r∣r∈Rd,∣∣r∣∣2≤ϵ} 为扰动的约束空间,
r
r
r 为增加的扰动,
α
\alpha
α 为步长,
Π
x
+
S
\Pi_{x+\mathcal{S}}
Πx+S 用于将扰动投影到
ϵ
\epsilon
ϵ-ball 上 - 由于 PGD 需要进行
T
T
T 次迭代,因此生成一个对抗样本就需要进行
T
T
T 次正向传播 + 反向传播,速度还是非常慢的。但 PGD 生成的对抗样本质量是非常高的,在论文中,作者称 PGD 为 “一阶最强对抗”。也就是说,只要能搞定 PGD 对抗,别的一阶对抗就不在话下了
Iterative FGSM
- paper: Adversarial examples in the physical world
Virtual Adversarial Training
- paper: Distributional Smoothing with Virtual Adversarial Training
- 除了监督训练,对抗训练还可以用在半监督任务中,尤其对于 NLP 任务来说,很多时候输入的无监督文本多的很,但是很难大规模地进行标注,那么就可以参考 Virtual Adversarial Training 进行半监督训练
- 首先,我们抽取一个随机标准正态扰动
d
∼
N
(
0
,
I
)
∈
R
d
d\sim N(0,I)\in\R^d
d∼N(0,I)∈Rd,加到输入上,并用 KL 散度计算梯度 (在监督学习中,我们使用交叉熵损失计算梯度,而在半监督学习中,我们使用 KL 散度计算梯度):
x
′
=
x
+
ξ
d
g
=
∇
x
′
D
K
L
(
p
(
⋅
∣
x
;
θ
)
∥
p
(
⋅
∣
x
′
;
θ
)
)
\begin{aligned} x^{\prime} &=x+\xi d \\ g &=\nabla_{x^{\prime}} D_{K L}\left(p(\cdot \mid x ; \theta) \| p\left(\cdot \mid x^{\prime} ; \theta\right)\right) \end{aligned}
x′g=x+ξd=∇x′DKL(p(⋅∣x;θ)∥p(⋅∣x′;θ))然后,用得到的梯度,计算对抗扰动,并进行对抗训练:
x
∗
=
x
+
ε
g
/
∥
g
∥
2
min
θ
D
K
L
(
p
(
⋅
∣
x
;
θ
)
∥
p
(
⋅
∣
x
∗
;
θ
)
)
\begin{aligned} x^{*}&=x+\varepsilon g /\|g\|_{2} \\ \min _{\theta}\ &D_{K L}\left(p(\cdot \mid x ; \theta) \| p\left(\cdot \mid x^{*} ; \theta\right)\right) \end{aligned}
x∗θmin =x+εg/∥g∥2DKL(p(⋅∣x;θ)∥p(⋅∣x∗;θ))
Black Box Attack
- In the previous attack, we know the network parameters
θ
\theta
θ. This is called White Box Attack.
- Are we safe if we do not release model? - No, because Black Box Attack is possible.
Proxy network
- If you have the training data of the target network: Train a proxy network yourself. Using the proxy network to generate attacked objects
- What if we do not know the training data? - 那就直接给目标网络 input,然后得到网络的 output 以获取输入-输出对,直接将其作为 proxy network 的训练数据即可
- 黑箱攻击的效果 (比较适合 non-targeted attack):
- paper: Delving into Transferable Adversarial Examples and Black-box Attacks
对角线为白箱攻击,非对角线为黑箱攻击
- 提高黑箱攻击的效果: Ensemble Attack
上表中,非对角线为白箱攻击,对角线为黑箱攻击。
i
i
i 行
j
j
j 列表示使用骗过了
i
i
i 行外所有模型的 attacked image 去攻击
j
j
j 列模型得到的准确率
One pixel attack
- paper: One pixel attack for fooling deep neural networks
- vider: [TA 補充課] More about Adversarial Attack (1/2) (由助教黃冠博同學講授)
Universal Adversarial Attack
- paper: Universal adversarial perturbations
- 一般的攻击会为每张图片都找出一个特制的 noise,但也有可能找到一个 noise,使每张图片加上该 noise 后都能被攻击成功 (Black Box Attack is also possible!)
Attack in the Physical World
攻击人脸识别系统
- paper: Accessorize to a Crime: Real and Stealthy Attacks on State-of-the-Art Face Recognition
- 攻击时的注意点:
- An attacker would need to find perturbations that generalize beyond a single image.
- Extreme differences between adjacent pixels in the perturbation are unlikely to be accurately captured by cameras. (攻击的花纹会受到相机分辨率的限制)
- It is desirable to craft perturbations that are comprised mostly of colors reproducible by the printer. (人眼看到的颜色与图像中的颜色存在差异性)
- 左边的男人带上特制的眼镜后,就会在各个角度被识别为右边的女人
攻击路牌识别系统
- Model Hacking ADAS to Pave Safer Roads for Autonomous Vehicles
- 仅仅将 “3” 中间的横线拉长一些,就能使路牌识别系统将其识别为限速 85
“Backdoor” in Model
- Attack happens at the training phase: 可以在训练数据集中加入 attacked image 来攻击模型
- be careful of unknown dataset ……
The attack is so easy! Why?
- paper: Adversarial Examples Are Not Bugs, They Are Features (容易被攻击并不是模型的问题,而是因为训练数据本身的特征) (just an idea)
在上图中,将图片看作一个高维向量,蓝色区域为识别为小丑鱼的区域,横轴为容易被攻击成功的移动方向
How to Defend
Passive Defense
- 在模型前加上 filter
Smoothing
Image Compression
- paper:
- Feature Squeezing: Detecting Adversarial Examples in Deep Neural Networks
- Shield: Fast, Practical Defense and Vaccination for Deep Learning using JPEG Compression
Generator
- paper: Defense-GAN: Protecting Classifiers Against Adversarial Attacks Using Generative Models
Randomization
- Mitigating Adversarial Effects Through Randomization
- 如果攻击者知道了 passive defense 的防御手段并且将 filter 当作模型的第一层加以攻击的话,passive defense 就失效了。因此,我们可以随机采用不同的 passive defense 的防御方法
Adversarial Training - 对抗训练 (Proactive Defense)
- Adversarial Training: Training a model that is robust to adversarial attack.
Trick: Use both clean and adversarial loss:
arg
min
θ
[
E
(
x
,
y
)
∼
D
(
L
(
θ
,
x
,
y
)
+
max
ϵ
∈
S
L
(
θ
,
x
+
ϵ
,
y
)
)
]
\underset{\theta}{\arg \min }\left[\mathbb{E}_{(x, y) \sim \mathbb{D}}\left(L(\theta, x, y)+\max _{\epsilon \in S} L(\theta, x+\epsilon, y)\right)\right]
θargmin[E(x,y)∼D(L(θ,x,y)+ϵ∈SmaxL(θ,x+ϵ,y))]
- 值得注意的是,对抗训练不仅可以提高模型应对恶意对抗样本时的鲁棒性,还可以作为一种正则化方法,缓解过拟合现象,提高模型的泛化能力
- Problem: 不一定能挡住新的攻击方式
Min-Max 公式
- paper: Towards Deep Learning Models Resistant to Adversarial Attacks
- 上述对抗训练的步骤可以被凝练为如下公式:
min
θ
E
(
x
,
y
)
∼
D
[
max
r
a
d
v
∈
S
L
(
θ
,
x
+
r
a
d
v
,
y
)
]
\min _{\theta} \mathbb{E}_{(x, y) \sim \mathcal{D}}\left[\max _{r_{a d v} \in \mathcal{S}} L\left(\theta, x+r_{a d v}, y\right)\right]
θminE(x,y)∼D[radv∈SmaxL(θ,x+radv,y)]内部 max 是为了找到 worst-case 的扰动,也就是攻击,其中,
L
L
L 为损失函数,
S
\mathcal{S}
S 为扰动的范围空间。外部 min 是为了基于该攻击方式,找到最鲁棒的模型参数,也就是防御,其中
D
\mathcal{D}
D 是输入样本的分布
相对清晰、相对可解释的梯度
- paper: Robustness May Be at Odds with Accuracy
- 值得一提的是,如果将 loss 对 input pixels 的梯度进行可视化就会发现,没有经过对抗训练的模型梯度是比较杂乱的,而经过了对抗训练的模型梯度能够捕捉到图片中物体的边缘,更加关注输入图像的形状和全局特征而非局部的纹理特征,这与人的感知更加接近,也从一定程度上解释了为什么对抗训练可以使模型更加鲁棒
Adversarial Examples Improve Image Recognition
- paper: Adversarial Examples Improve Image Recognition
- 从经验上来看,在 CV 领域使用对抗训练,模型在提升鲁棒性的同时也会损失精度,但这篇 paper 提出了一种新的对抗训练方法,能够提升模型在干净样本测试集上的准确率。具体而言,由于干净样本和对抗样本具有不同的分布,论文在对抗训练时,对干净样本和对抗样本分别采用了不同的 Batch Normalization,而在预测时,只使用干净样本对应的 Batch Normalization 参数,最终在 ImageNet 数据集上将 Efficient Net 的 TOP1 准确率最高提升了 0.7 个百分点,并且在数据越大的情况下提升越明显。这给我们提供了一个使用对抗训练来提升模型准确率的方式
Adversarial Training in NLP
Generating Adversarial Sentences is Difficult
- 之前提到的对抗训练还是在 CV 领域,那么可否将对抗训练迁移到 NLP 上呢?不同于 CV,在 NLP 领域,生成新的对抗样本时,需要保证新的样本不会改变原来样本的语义,因此如果想要构造出真实的对抗样本是很难的
- 一般最常用的方法是把句子中的一些词替换成它的近义词。对扰动也采用一种梯度上升的方式,具体做法是通过替换后的 embedding 向量和原句子的 embedding 向量求得差向量后,再求与梯度向量求夹角,夹角越小说明对损失函数的增加越多。一般情况下都是通过这种方式结合近义词替换的约束来构造一些对抗样本,但是这种近义词替换与上下文关系密切,所以有时候也存在一些不合理的情况。例如下图中,gift 意思为天赋,如果将其替换为 present 就不太合理
因此,采用这种词替换的方式时还需要结合其他约束方式来过滤掉那些不合理的对抗样本,比如使用 back-translation scores,但是这种方式需要使用额外的机器翻译模型,在每次生成对抗样本时检测一次,效率非常低 (cannot gaurantee to filter out invalid adversaries, but somehow works)。另一种方式就是找一些通用的语言规则,比如缩写 (what is 替换成 what’s),名词换成指示代词 (noun 替换成 this/that/it) 等语义等价对抗规则 (semantically equivalent adversarial rules) 来生成对抗样本 (paper: Semantically Equivalent Adversarial Rules for Debugging NLP Models)
这种语义等价对抗规则产生的对抗样本基本上保留了原义,但不是一种非常有效的攻击方式 (reliable, but inefficient),并不能有效降低模型的准确率,基于该方法的对抗训练的准确率表现也一般
Adversarial perturbations in embeddings
- Goodfellow 在 Adversarial Training Methods for Semi-Supervised Text Classification 提出可以在连续的 word embedding 而不是 one-hot vector 上做扰动
- 乍一思考,觉得这个解决方案似乎特别完美。然而,对比图像领域中直接在原始输入加扰动的做法,在 embedding 上加扰动会带来这么一个问题:这个被构造出来的 “对抗样本” 并不能对应到某个单词,因此,对手也没有办法通过修改原始输入得到这样的对抗样本。我们在上面提到,对抗训练有两个作用,一是提高模型对恶意攻击的鲁棒性,二是提高模型的泛化能力。在 CV 任务,根据经验性的结论,对抗训练往往会使得模型在非对抗样本上的表现变差,然而神奇的是,在 NLP 任务中,模型的泛化能力反而变强了。如 Enhanced Adversarial Training for Language Understanding 中所述:
- While adversarial training boosts the robustness, it is widely accepted by computer vision researchers that it is at odds with generalization, with classification accuracy on non-corrupted images dropping as much as 10% on CIFAR-10, and 15% on Imagenet. Surprisingly, people observe the opposite result for language models, showing that adversarial training can improve both generalization and robustness.
- 因此,在 NLP 任务中,对抗训练的角色不再是为了防御基于梯度的恶意攻击,反而更多的是作为一种 regularization,提高模型的泛化能力
Fast Gradient Method (FGM) 的 PyTorch 实现
下面的代码搬运自【炼丹技巧】功守道:NLP 中的对抗训练 + PyTorch 实现
- FGM 在用于 NLP 时,
∣
∣
g
∣
∣
2
||g||_2
∣∣g∣∣2 计算的是每个样本的输入词向量序列组成的
T
×
H
T\times H
T×H 矩阵的 L2 范数 (
H
H
H 为词向量的维数)
下面的实现其实是对一个批次的数据,即
N
×
T
×
H
N\times T\times H
N×T×H 的矩阵计算 L2 范数,这样实现更方便,同时由于本来 norm 也只是一个 scale 的作用,影响不大
import torch
class FGM():
def __init__(self, model):
self.model = model
self.backup = {}
def attack(self, epsilon=1., emb_name='emb.'):
for name, param in self.model.named_parameters():
if param.requires_grad and emb_name in name:
self.backup[name] = param.data.clone()
norm = torch.norm(param.grad)
if norm != 0 and not torch.isnan(norm):
r_at = epsilon * param.grad / norm
param.data.add_(r_at)
def restore(self, emb_name='emb.'):
for name, param in self.model.named_parameters():
if param.requires_grad and emb_name in name:
assert name in self.backup
param.data = self.backup[name]
self.backup = {}
fgm = FGM(model)
for batch_input, batch_label in data:
loss = model(batch_input, batch_label)
loss.backward()
fgm.attack()
loss_adv = model(batch_input, batch_label)
loss_adv.backward()
fgm.restore()
optimizer.step()
model.zero_grad()
Projected Gradient Descent (PGD) 的 PyTorch 实现
import torch
class PGD():
def __init__(self, model):
self.model = model
self.emb_backup = {}
self.grad_backup = {}
def attack(self, epsilon=1., alpha=0.3, emb_name='emb.', is_first_attack=False):
for name, param in self.model.named_parameters():
if param.requires_grad and emb_name in name:
if is_first_attack:
self.emb_backup[name] = param.data.clone()
norm = torch.norm(param.grad)
if norm != 0 and not torch.isnan(norm):
r_at = alpha * param.grad / norm
param.data.add_(r_at)
param.data = self.project(name, param.data, epsilon)
def restore(self, emb_name='emb.'):
for name, param in self.model.named_parameters():
if param.requires_grad and emb_name in name:
assert name in self.emb_backup
param.data = self.emb_backup[name]
self.emb_backup = {}
def project(self, param_name, param_data, epsilon):
r = param_data - self.emb_backup[param_name]
if torch.norm(r) > epsilon:
r = epsilon * r / torch.norm(r)
return self.emb_backup[param_name] + r
def backup_grad(self):
for name, param in self.model.named_parameters():
if param.requires_grad:
self.grad_backup[name] = param.grad.clone()
def restore_grad(self):
for name, param in self.model.named_parameters():
if param.requires_grad:
param.grad = self.grad_backup[name]
pgd = PGD(model)
K = 3
for batch_input, batch_label in data:
loss = model(batch_input, batch_label)
loss.backward()
pgd.backup_grad()
for t in range(K):
pgd.attack(is_first_attack=(t==0))
if t != K-1:
model.zero_grad()
else:
pgd.restore_grad()
loss_adv = model(batch_input, batch_label)
loss_adv.backward()
pgd.restore()
optimizer.step()
model.zero_grad()
FreeAT
- paper: Adversarial Training for Free!
- 如前所述,PGD / Iterative FGSM 的开销是非常大的,
T
T
T 次的 FP + BP 只取了最后一次的梯度进行梯度上升。但注意到,在
T
T
T 次的 FP + BP 的过程中,其实可以得到所有参数的梯度,FreeAT 就是利用了梯度上升过程中的参数梯度来更新参数,达到减少总的 forward-backward 次数的目的 (对输入每做一次梯度上升时,同时对参数
θ
θ
θ 做一次梯度下降,最终训练所需的 FP + BP 次数与正常训练相同):
由于 FreeAT 会在同一个样本附近更新多次参数,这种方法也成为 batch replay
按照伪代码,扰动
δ
\delta
δ 只在最开始初始化,每个 batch 的初始扰动
δ
0
\delta_0
δ0 是上一个 batch 更新
m
m
m 次之后的最终扰动,但按常理难道不应该是在每个 batch 开始时重新初始化扰动为 0 来进行 clean data 的梯度下降吗?
- FreeAT 中也存在一些问题,我们看到
δ
t
\delta_t
δt 更新的公式中,
δ
t
\delta_t
δt 是
θ
t
−
1
θ_{t-1}
θt−1 的函数,就是每次更新时仍然使用了上一步骤中的参数,参数存在滞后性,所以产生的对抗效果不够强 (“expired grad”)
YOPO (You Only Propogate Once)
- paper: You Only Propagate Once: Accelerating Adversarial Training via Maximal Principle
- 加速对抗训练 —— YOPO算法浅析
- 一文搞懂 NLP 中的对抗训练 FGSM / FGM / PGD / FreeAT / YOPO / FreeLB / SMART
FreeLB
- paper: FreeLB: Enhanced Adversarial Training for Natural Language Understanding
- PGD 只使用了最后一步
x
+
r
x+r
x+r 计算的梯度来更新参数,而 FreeLB 保留了每步对抗训练的梯度,并取它们的平均值来更新参数,相当于把输入看作一个
K
K
K 倍大的虚拟 batch,由
[
X
+
r
1
,
X
+
r
2
,
.
.
.
,
X
+
r
k
]
[X+r_1, X+r_2, ..., X+r_k]
[X+r1,X+r2,...,X+rk] 拼接而成。具体的公式为:
为了方便对比,再贴下论文中 PGD 的公式:
相对于 PGD 做
k
k
k 步对抗训练,才做一次网络参数更新。在 min-max 角度来说,相当于进行了多次内部 maximum,然后做一次 minimize。而 FreeLB 对每一步的 max 都做了 minmize。论文实验部分证明了这种做法,比起 PGD 鲁棒性和泛化能力更强。同时 FreeLB 在
K
K
K 步之后才更新参数,积累得到的对抗样本的梯度都是使用当前步骤下的参数,避免了 FreeAT 中 “参数过期” 的问题
注意到,FreeLB 并没有将总迭代次数 (模型参数更新次数) 减少到原来的
1
/
K
1/K
1/K,但这种方式相比于
K
K
K-PGD 仍然提高了梯度的利用率,并能够进一步提升模型性能 - 同时,论文中还指出了很重要的一点,就是如果要在对抗训练中使用 dropout,为了增加对抗的强度,就需要在
K
K
K 步中都使用同一个 dropout mask,这与 FreeAT 参数过期问题相似,因为如果网络每次迭代的结构都不相同,得到的输入梯度就会有很多噪声。如果从目标函数的角度来看,我们想优化不同 dropout mask 下损失函数的期望,损失函数在
K
K
K-step 里是所有样本损失之和,所以需要在每一步里的 dropout 保持相同
References
- 李宏毅 2021 ML 课程
- 【炼丹技巧】功守道:NLP 中的对抗训练 + PyTorch 实现
- 对抗训练——终极数据增强?
- 马里兰大学在读博士生朱晨:FreeLB — 适用于自然语言理解的对抗学习 (视频版)、长文回顾 | 马里兰大学在读博士生朱晨: FreeLB—适用于自然语言理解的对抗学习 (文字版)
- 论文分享 – > NLP – > FreeLB
- 一文搞懂 NLP 中的对抗训练 FGSM / FGM / PGD / FreeAT / YOPO / FreeLB / SMART
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)