LLM推理性能受输出格式影响,JSON最严重
输出格式不同,竟然还能影响大模型发挥?!
两种提示下让大语言模型(LLMs)解同一道数学题,问题如下:
Eliza每周工作的前40小时,每小时的工资是10美元,加班费每小时x1.2。如果Eliza这周工作了45小时,她这周的收入是多少?
思维链prompt:“按照以下格式提供输出,逐步推理:…回答:最终答案是…”。
格式限制prompt:“按照以下有效的JSON格式提供输出:…(具体JSON格式见图)“。
正确答案是460,可以看出,思维链(让模型一步步思考)奏效,格式限制(“以JSON格式输出”)却失败了!!
这是台湾大学和Appier AI Research新研究中的一幕,他们发现——
格式限制这玩意儿会降低LLMs的推理能力,且限制越严推理越差。(主打一个叛逆)
不过好消息是,能治。
他们发现,最佳解决方案是搞个“二次转换”(倒爷是吧),即LLMs首先用自然语言回答问题,然后再将答案转换为目标格式。
在这个过程中,他们对比了GPT-3.5 Turbo、Claude 3 Haiku、Gemini 1.5 Flash等不同模型在生成不同格式数据时的性能差异,结果又发现:
GPT喜欢YAML、Claude喜欢XML、Gemini/Gemma喜欢JSON。(主打各有所爱)
看完研究,有网友点出了它对平衡结构化生成和任务推理的意义:
上述研究已发表在arXiv上,论文主要揭示了,在格式限制下,LLMs的推理能力显著下降,尤其是在JSON模式下。
一直以来,将LLMs纳入工业应用程序的一个主要障碍是它们缺乏对标准化输出格式的遵守。
一种常见解决方法是结构化生成,即通过格式限制让LLMs以JSON或XML等标准化格式提供输出。
不过话说回来,虽然有多种方式可以实现这种限制,但后续影响却无人研究。(限制是否影响模型性能呢?)
说干就干,研究人员采用3种常见方法来评估不同格式限制对下游性能的影响:
- JSON-mode:通过预定义的标记空间限制LLMs的输出
- FRI:指导LLMs生成符合特定模式的标准化格式响应
- NL-to-Format:两步过程,首先用自然语言回答问题,然后转换为目标格式
对了,还要加上自然语言(NL),它是最不受限的格式,允许模型以自然语言自由地回答问题。
评估对象是GSM8K(包含自然语言环境中的数学问题)和Last Letter Concatenation(最后一个字母连接任务)这两个需要精确匹配答案的数据集,以及Shuffled Objects(洗牌对象追踪任务)。
他们发现,在这些涉及推理的任务中,更宽松的提示通常会得到更好的结果。
同时,JSON模式在大多数情况下表现最差,其次是格式限制指令(FRI),然后是自然语言到格式(NL to Format)转换,以及自然语言(NL)提示。
研究还发现,不同的LLMs对不同的数据格式表现出不同的偏好。
例如,GPT更喜欢YAML格式,Claude更喜欢XML格式,而Gemini/Gemma则更倾向于JSON格式。
不过,在分类任务中,格式限制可能提高了准确性,因为它减少了可能的答案选择,从而降低了错误率。
他们进一步总结了格式限制会降低模型推理能力的原因,主要包括:
- 限制了模型生成必要中间推理步骤的能力。
- 强制的格式要求可能与模型自然生成答案的方式不兼容。
- 格式错误可能导致即使推理正确,答案也因为格式问题而被判定为错误。
针对这一问题,他们提出了几种应对之策:
首先,前面提到了JSON模式在大多数情况下表现最差,最后才是自然语言到格式(NL to Format)转换。
那么反过来说,解决格式限制的最佳方案就成了NL to Format,即LLMs首先用自然语言回答问题,然后再将答案转换为目标格式。这种方式允许推理与格式遵守分离,从而表现更佳。
此外,结构化输出中的键顺序对LLMs的回答方式有重要影响。
例如在使用GPT-3.5 Turbo时,100%的JSON-mode响应错误地将“answer” 键位于 “reasoning” 之前,这导致模型直接给出答案,而不是展示思考过程。
研究还表明,格式限制导致的解析错误不是性能差异的主要原因。
例如,在LLaMA 3 8B模型中,Last Letter任务的JSON格式解析错误率仅为0.15%,但与自然语言响应相比,性能差距达到了38.15%。
而且可以通过纠正提示来减轻这些错误,例如对于Claude-3-Haiku模型,在Last Letter任务中,通过纠正步骤,JSON和YAML格式的准确率分别提高了+2.8%和+44.8%。
以上也意味着,在应用LLMs时,需要在易于解析的格式和保留固有推理能力之间找到平衡点。
最后,研究人员在论文中提醒了:
相比于正则表达式,LLMs作为答案解析器能够提供更加深入和准确的文本理解,不仅仅局限于表面的模式匹配,而是能够真正理解答案的含义和上下文。
— 完 —
量子位 QbitAI · 头条号签约
关注我们,第一时间获知前沿科技动态
VulBERTa:用于漏洞检测的简化源代码预训练
VulBERTa: Simplified Source Code Pre-Training for Vulnerability Detection
Hazim Hanif, Sergio Maffeis
Computer Science and Information Technology,University of Malaya, Malaysia
Department of Computing Imperial College London, UK
引用
Hanif H, Maffeis S. Vulberta: Simplified source code pre-training for vulnerability detection[C]//2022 International joint conference on neural networks (IJCNN). IEEE, 2022: 1-8.
论文:https://arxiv.org/pdf/2205.12424.pdf
摘要
本文介绍了VulBERTa,一种用于检测源代码中安全漏洞的深度学习方法。本文的方法在开源C/C++项目的实际代码上预训练了一个具有自定义标记化管道的RoBERTa模型。该模型学习了代码语法和语义的深层知识表示,本文利用这一点来训练漏洞检测分类器。本文在几个数据集(Vuldeepecker、Draper、REVEAL和muVuldeepecker)和基准测试(CodeXGLUE和D2A)上评估了本文的方法,针对二元和多类漏洞检测任务。评估结果显示,VulBERTa实现了最先进的性能,并且在不同的数据集上超越了现有的方法,尽管在概念上简单,并且在训练数据的大小和模型参数的数量上成本有限。
1 引言
MITRE报告称,自2016年以来,每年提交的软件CVE(公共漏洞和曝光)数量呈上升趋势,反映了软件生态系统整体安全性面临的威胁增加。相应地,多年来在软件漏洞检测领域的研究稳步增长,涵盖了诸如静态分析、动态分析以及基于机器学习的检测模型等各种漏洞检测方法。深度学习在软件漏洞方面取得了鼓舞人心的结果,特别是使用基于序列和图的技术,如双向LSTM和图神经网络。这些技术试图显式地嵌入代码的语法和语义信息,例如通过使用各种依赖和数据流分析来预处理源代码并提取各种工件,如代码小工具、控制流图和依赖图,最终将其输入到相应的神经网络中。在本文中,我们采用了一种不同的方法,该方法受到了基于Transformer的神经架构的最近成功的启发,这些架构能够学习自然语言文本数据的深层表示知识。本文的目标是构建一个C/C++的表示模型,该模型嵌入了关于语言的语法和语义信息,而无需我们的直接干预,然后使用该模型作为基础,使用标准神经架构构建漏洞检测模型。本文介绍了VulBERTa,旨在从包含不同软件项目的大型代码库中学习C/C++源代码的深层表示。为了促进学习内部代码表示,我们需要创建一个语言感知的可靠标记化管道,来解析和标记源代码,同时确保向模型提供基本的、关键的语法和语义信息。在我们的标记化管道中,本文实现了几个新颖的特性,以增强现有字节对编码(BPE)标记器的标记化能力。本文引入了一个自定义的标记器,它将BPE与自定义的预定义代码令牌相结合,以构建我们标记器的词汇表。这些预定义的令牌基于Clang的AST节点类型(标准关键字和标点符号)和常见C/C++ API函数名的列表。本文的自定义标记化管道在保持其原始语法结构的同时,创建了更好的代码表示,即使在编码后也是如此。
VulBERTa实现了一种基于Transformer的深度学习架构,称为RoBERTa。通过遮蔽语言模型(MLM)的方法,VulBERTa构建了其代码表示知识。本文在较少的样本(228万)和模型参数(1.25亿)上预训练VulBERTa,相较于现有的方法。然后,本文将预训练的VulBERTa模型分别与多层感知器(VulBERTa-MLP)和卷积神经网络(VulBERTa-CNN)相连接,以便对漏洞检测模型进行微调。
本文在不同的数据集上评估VulBERTa,以测试本文预训练模型的性能和可迁移性。评估结果显示,本文在这些数据集上达到了最先进的检测性能。特别是,在被认为是具有挑战性的Draper数据集上,我们达到了57.92%的F1分数,超越了现有的方法,这是令人鼓舞的,因为这个数据集由于不平衡并且混合了真实世界和合成样本而被认为是具有挑战性的。VulBERTa在较为简单的muVuldeepecker数据集上的多类分类任务上也表现良好,达到了99.59%的加权F1分数。本文还在两个软件漏洞检测基准(CodeXGLUE和D2A)上测试了VulBERTa,在使用更少的训练数据和更少的模型参数的情况下,它超越了大多数现有的方法。这些结果显示,VulBERTa在学习源代码的深层表示以及使用该知识来检测软件漏洞方面是有效的。
总结来说,本文的主要贡献包括:
– 一个自定义的标记化管道,它将字节对编码(BPE)算法与新颖的预定义代码令牌(标准C/C++关键词、标点符号和库API调用)相结合,提供了更好的代码编码,同时保持了源代码的语法结构。
– 一个小型化且简化的预训练模型(VulBERTa),提供了预训练嵌入权重的可迁移性,这些权重可以在较为简单的架构如多层感知器(MLP)和卷积神经网络(CNN)中重用。
– 软件漏洞检测模型VulBERTa-MLP和VulBERTa-CNN,在不同数据集上实现了最先进(SOTA)的检测性能,并在两个基准测试(CodeXGLUE和D2A)中位列前三。
2 技术介绍
在这一部分中,我们介绍了VulBERTa,我们用于检测C/C++源代码中功能级粒度漏洞的预训练架构。该架构分为三个关键组件:一个标记化技术,它使用自定义词汇解析和标记代码;一个预训练会话,用于构建代码的表示模型;以及一个微调会话,用于精炼模型以针对具体的分类任务。图1展示了VulBERTa训练流程的8个主要步骤。
图1:VulBERTa训练流程
2.1 Tokenizer
我们的Tokenizer管道旨在保留语法结构和选定的语义标识符。它由解析器、标记器和编码器组成。这些组件叠加在一起,将原始源代码转换为神经网络能够理解的结构。图2显示了VulBERTa从原始代码到编码输出序列的过程。下面,我们描述了管道的每个步骤:
图2:Tokenizer工作流程
1)解析器:我们使用几个正则表达式从每个函数的源代码中移除注释。然后,我们使用Clang,一个健壮的C/C++解析器,解析源代码,该解析器可以在不包含任何库或外部依赖的情况下解析代码。Clang允许我们在将源代码分解成一系列代码标记的同时,保留源代码的语法结构。
2)标记器:Clang解析器产生的标记经过BPE算法的进一步处理,该算法经过修改以考虑我们的预定义标记,进一步将解析输入细分为细粒度的标记以进行编码。字节对编码(BPE)是一种子词标记化算法,它通过用不出现在数据中的字节替换一对相似的连续字节。子词标记化还减少了遇到词汇表外标记的可能性,因为大多数子词标记都在词汇表中。我们定义了一个词汇表大小为50000,作为词汇表中条目的最大数量。预定义标记是我们在词汇表中明确包含的标记,因此将它们从子词标记化过程中排除。我们的目标是保留它们的语法或语义含义。通过预定义C/C++关键词、标点符号和标准API名称,我们在预训练期间保留了更多关于源代码含义的信息。
表1 自定义预定义token
3)编码器:编码是将代码标记转换为张量的过程。对于预训练,我们将最大序列长度设置为512,以最大化和泛化数据的学习,因为预训练数据集包含不同的代码库。同时,对于微调任务,我们将最大序列长度增加到1024,以便它可以在不截断的情况下平均包含超过90%的实际样本。我们用一个特殊的填充标记(<pad>)向右填充较短的序列。
2.2 预训练
预训练是初始训练阶段,我们在此阶段训练一个标准的RoBERTa模型,采用MLM(遮蔽语言模型)目标,以学习C/C++代码在不同软件项目中的信息性通用表示。我们将从这个预训练阶段获得的模型称为VulBERTa模型。结合不同的软件项目对学习过程有益,因为它进一步泛化了代码的表示知识,覆盖了不同的编码风格。这也有助于增加模型在预训练期间的鲁棒性。我们设置嵌入大小为768维,遵循RoBERTa-base。这是模型的核心嵌入知识,将对下游微调任务有用。
2.3 微调
在微调过程中,我们在一个特定的下游任务上进一步训练预训练模型,在我们的案例中是软件漏洞检测。图3展示了用于漏洞检测案例的微调流程。我们为微调实现了两种不同的分类方法。第一种方法是在预训练模型之上使用一个标准的多层感知器(MLP),第二种方法使用文本卷积神经网络(TextCNN),由于CNN架构的鲁棒性,这种方法更便宜、更快速地微调。
图3 微调(漏洞检测任务)流程
VulBERTa-MLP:我们实现了一个包含768个神经元的全连接层,以及一个基于微调数据集是二分类或多分类数据集的输出层,拥有2或41个神经元。在微调过程中,我们重用VulBERTa的预训练权重,并继续训练几个周期。这种方法是微调预训练模型最常见的方法,因为它利用了整个VulBERTa架构,几乎不需要修改。
VulBERTa-CNN:我们提取预训练VulBERTa模型的嵌入权重,并将它们作为混合文本CNN的嵌入权重。TextCNN架构包括三个一维CNN,每个都有其最大池化层。输出被连接和展平,然后输入到两个全连接层(256和128个神经元),带有一个用于分类的输出层。由于嵌入已经在大型语言模型上预训练,我们在训练任务中冻结它们。这种技术允许TextCNN模型继承和使用嵌入的表示知识,并专注于调整特定任务CNN层的权重。
3 实验评估
3.1 实验设置
1) 硬件和软件:我们在所有微调实验中使用PyTorch 1.7与CUDA 10.2,并基于Python 3.7进行操作。对于预训练,我们使用了配备48 vCPUs、240GB RAM和2个NVIDIA Tesla A100 40GB GPUs的Google Compute Engine(GCP)虚拟机。对于微调,我们使用一台配备48核Intel Xeon Silver CPU、292GB RAM和2个NVIDIA GTX TITAN Xp GPU的机器。每个GPU拥有12GB的显存,以适应不同的模型配置。
2) 性能标准:对于每个实验,我们报告了几个评估指标,包括每个数据集初始工作中使用的指标。这样,我们可以进行更公平的比较。这些指标包括真阴性(TN)、假阴性(FN)、真阳性(TP)、假阳性(FP)、准确率、精确度、召回率、F1分数、接收器操作特征曲线下的面积(ROC-AUC)、精确度-召回率AUC(PR-AUC)和Matthews相关系数(MCC)。
3) 基准方法:在性能评估中,我们将VulBERTa与每个数据集现有方法的两种基准技术进行比较。这两个基准在分析用于漏洞检测的序列化输入时被广泛使用,并且已经在例如文献[6]和[17]中使用。
(i) 基准-BiLSTM:这项技术是LSTM的一种变体,它实现了一个双层双向LSTM和几个全连接层,以从源代码的序列中学习漏洞检测。双向LSTM同时学习代码序列的前向和后向关系。
(ii) 基准-TextCNN:这是CNN的一个变体,其中输入数据是自然语言文本而不是图像。在这种情况下,我们使用源代码作为输入数据,并将其输入到CNN中。这种技术部署了三个具有池化的卷积层,并在传递结果到几个全连接层之前将它们连接成一个单一层。
4) 模型预训练:我们使用Draper和GitHub数据集通过MLM预训练VulBERTa模型。我们尝试了不同的RoBERTa配置(即小型、中型和基础型),以观察模型参数数量如何影响模型的预训练性能。每次预训练会话的持续时间根据模型配置介于72到96小时之间。训练会话进行到500,000步,并使用学习率调度器随着训练损失趋于平稳而降低学习率。基于结果,我们发现基础型模型在500,000步后的训练中给出了最低的损失。因此,我们选择基础配置的VulBERTa模型(约125M参数)作为我们用于微调的参考预训练模型。
5) 模型微调:我们分别对预训练的VulBERTa-MLP和VulBERTa-CNN模型进行微调,使用漏洞检测作为微调目标。我们将最大迭代次数设置为10,这是足够的,因为模型在4-5个周期后开始对训练集过拟合。我们将学习率设置为0.00003,并使用学习率调度器随着训练损失趋于平稳而降低学习率。我们遵循每个数据集的原始划分,但如果划分信息不可用,我们将数据集分割为80/10/10(训练/验证/测试)。每次微调会话持续时间在5到10小时之间,具体取决于数据集和模型的大小。
3.2 在选定数据集上的评估
漏洞检测实验按数据集分开进行。对于每个数据集,我们选择了原始论文中用于比较的首选评估指标(PEM)。表II展示了评估结果,其中我们突出显示了每个数据集的最高PEM分数。
1) Vuldeepecker:Vuldeepecker报告的精确度得分为91.9%。然而,使用VulBERTa-MLP,我们实现了95.76%的精确度得分,比原始得分高出3.86%。此外,我们的模型获得了更高的F1分数,93.03%,相比于Vuldeepecker的92.9%,这表明我们的模型在分类易受攻击和非易受攻击样本时更为平衡。低比率的假阳性(0.39%)和假阴性(9.14%)表明VulBERTa-MLP能够在合成和现实世界代码中以低误分类率检测出漏洞。
2) Draper:[18]的作者使用几个评估指标来评估他们在Draper数据集上的模型。我们选择MCC作为比较的基础,因为它适用于不平衡数据集,而Draper数据集的易受攻击类和非易受攻击类是不平衡的。VulBERTa-CNN在这个数据集上获得了55.86%的MCC得分。这比[18]报告的性能提高了2.26%,这是显著的,因为我们可以在保持类别平衡的同时提供更好的检测。
3) REVEAL:VulBERTa-MLP模型实现了45.27%的F1分数,高于[25]报告的41.25%,尽管没有使用那里提出的数据再平衡技术。相反,我们在微调过程中为每个类(易受攻击和非易受攻击)分配了权重,以减少类别不平衡问题,而不改变数据集。我们的方法还获得了比原来高2.57%的真阳性率(TPR)。这表明VulBERTa-MLP在正确检测易受攻击样本的同时,也保持了与非易受攻击类的平衡。
4) muVuldeepecker:与前面的实验不同,这是一个多类分类任务。muVuldeepecker数据集包含40个CWE的独立类别,所以每个阳性样本可以映射到一个特定的安全漏洞。VulBERTa-MLP实现了非常高的加权F1分数,99.59%,相比于[5]报告的96.28%。它还将假阴性率(FNR)从5.53%降低到0.41%,这是显著的,因为作为易受攻击的附加样本需要被分配到正确的类别。此外,查看特定类别如CWE-190和CWE-191(整数溢出和下溢)的预测,我们可以看到VulBERTa-MLP能够正确地将90%以上的易受攻击样本分配到它们各自的类别中。
我们的模型设法超越了使用后者数据集和首选评估指标的比较模型的性能。这发生在包含合成和现实世界数据、平衡和不平衡类别、二类和多类分类任务的各种数据集上。
3.3 在选定数据集上的评估
我们还在两个公开可用的基准测试上评估了VulBERTa-MLP和VulBERTa-CNN,这两个基准测试被社区用作比较不同源代码模型在标准化任务上的基础。在两种情况下,漏洞检测任务的首选评估指标(PEM)是准确性。
表2 数据集评估结果
1) CodeXGLUE:CodeXGLUE基准测试是由微软研究院引入的第一个也是最受欢迎的编程语言理解基准测试。我们关注的是缺陷检测任务,该任务包括在第IV-B5节描述的Devign数据集上的漏洞检测。表III-A展示了发表时CodeXGLUE基准测试排行榜的情况,包括我们的结果。VulBERTa-MLP实现了64.75%的准确率,排名第三,低于CoText和C-BERT。值得注意的是,VulBERTa-MLP的模型参数数量显著少于CoTexT(55.07%),并且是用前2个模型的一小部分数据进行训练的。令人印象深刻的是,VulBERTa-CNN的性能略低,但模型大小不到CoTexT的1%,C-BERT的2%。
2) D2A:D2A是由IBM研究院引入的漏洞检测基准测试,基于第IV-B6节描述的D2A数据集。表III-B展示了发表时D2A基准测试排行榜的情况,包括我们的模型。对于“函数”任务(与我们的方法唯一相关的任务),VulBERTa-MLP以62.30%的准确率领先排行榜,VulBERTa-CNN以60.68%排在第二。有趣的是,在这个数据集上,我们的两个模型都超过了C-BERT,尽管它有更大的预训练数据集和更多的参数(与VulBERTa-CNN相比)。
表3性能评估结果
3.4 讨论
表II显示,VulBERTa-MLP在4个数据集中的3个上拥有最佳的PEM分数,而VulBERTa-CNN在剩下的数据集上有最佳的PEM分数。然而,如果我们在微调结果的全谱上比较我们的两个模型,我们会注意到两者之间的差异可以忽略不计。同样,在基准评估上,VulBERTa-MLP和VulBERTa-CNN之间也显示出紧密的关系。两个模型在两个基准测试中都一致地排名相邻,并且在两者中都达到了最先进的性能。一个可能的解释是,从预训练的VulBERTa模型继承的代码表示知识对于漏洞检测起着关键作用。
模型大小和预训练数据大小通常在学习更好的代码表示中扮演着重要的角色。然而,VulBERTa只包含125M参数,我们的预训练数据仅包含2.28M个C/C++函数,例如,与CoTexT(375M)和C-Bert(8.5M)相比。我们通过使用较小的模型和预训练数据提供了与这些方法相当的SOTA检测性能。
基于我们对测试和实施不同标记化技术的初步分析,我们相信我们的标记化方法在使语法和语义信息易于被用于微调的简单神经架构可用方面发挥了重要作用。事实上,模型的简单性似乎是解决方案的一部分。
VulBERTa-CNN,我们基于简单的TextCNN方法(仅有2M参数),在Draper数据集上的MCC得分为55.86%,高于最近在[40]中提出的复杂的3GNN模型所获得的52%,后者使用了晶体图卷积网络和自注意力池化。
3.5 限制
尽管结果令人鼓舞,我们还是发现了我们方法的一些限制。与漏洞检测数据集相关的一个共同且尚未解决的问题是,人工检查偶尔会发现标签不准确的情况。虽然深度学习应该对训练中的标签噪声具有弹性,但测试过程中噪声的存在在某种程度上削弱了量化性能结果。尽管我们的模型相对较小,但训练它们仍然代价昂贵。由于资源有限,我们无法探索显著更大的模型配置和组合,包括执行超参数调整。因此,不同的VulBERTa配置可能会达到更高的性能。
最后,我们工作的主要限制是缺乏系统地尝试在野外的开源项目中检测新的0Day漏洞。这是由于手动审核假阳性的挑战所致,我们希望在未来的工作中解决这个问题,利用可解释性技术。
转述:王越
x-cmd pkg | grex – 正则表达式生成利器,解决手动编写的烦恼
grex 是一个旨在简化创作正则表达式的复杂且繁琐任务的库和命令行程序。这个项目最初是 Devon Govett 编写的 JavaScript 工具 regexgen 的 Rust 移植。但 regexgen 在几年前停止了开发。现在 grex 提供了 regexgen 提供的所有功能,还增加了许多新的功能。
本文的 demo 展现了使用 grex 生成 [a-f] 正则表达式,以及使用可读性更好的方式打印输出。
生成的表达式与 Perl 兼容,也与 Rust 的 regex crate 中的正则表达式解析器(1.9.0 或更高版本)兼容。其他正则表达式解析器或其他编程语言的相应库尚未经过测试
支持 Unicode 符号
- 完全符合 Unicode 标准15.0
- 能正确处理由多个 Unicode 符号组成的图形元素
友好的用户体验
- 自动生成正则表达式:只需提供输入,grex 默认生成最具体的正则表达式,并与给定的输入完全匹配。
- 使用详细模式生成在多个上缩进的更具可读性的表达式
- 语法高亮显示,在支持的终端中提供更好的显示输出。
- 项目托管在 GitHub:
- 您还可以通过 Demo website,将您提供的测试用例中生成匹配的正则表达式。
更多内容请查阅 : grex | x-cmd pkg | 命令行工具和 Rust 库,用于从用户提供的测试用例生成正则表达式
转载请标明原文链接 :https://www.x-cmd.com/pkg/grex
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。