راهنمای یکپارچهسازی حملات
حملات خود را به این چارچوب بنویسید تا در سکوی ارزیابی امنیتی مدلهای هوش مصنوعی قابلیت یکپارچه شدن داشته باشند.
مقدمه
سکو برای اجرای هر حمله، یک کلاس پایتون را بهصورت پویا بارگذاری و متد execute() آن را فراخوانی میکند.
این راهنما بهترتیب توضیح میدهد: منظور از «ماژول» چیست، کلاس پایه BaseAttack چه امکاناتی دارد، فرمت خروجی، توابع کمکی، دامنهها، ساختار پوشهها، و در پایان یک مثال کامل.
ماژول یعنی چه؟
وقتی در سکو از «ماژول حمله» یا «مسیر ماژول» صحبت میکنیم، منظورمان یک مسیر نقطهدار پایتون است که به کلاس حمله اشاره میکند. این مسیر از نام پکیج، پوشهها، نام فایل و در نهایت نام کلاس تشکیل شده است.
مثال: attack_modules.whitebox.fgsm.FGSMAttack یعنی:
attack_modules— پکیج ریشه (پوشهattack_modules/)whitebox— زیرپوشه (مثلاً برای حملات جعبه سفید)fgsm— نام فایلfgsm.pyFGSMAttack— نام کلاسی که ازBaseAttackارث میبرد
سکو با importlib این مسیر را بارگذاری میکند و نمونهای از کلاس را با eval_request، attack_method و params میسازد؛ سپس execute() را صدا میزند. پس «ماژول» در واقع همان شناسه یکتای کلاس حمله در کد پایتون است.
چارچوب اجباری
هر حمله باید یک کلاس پایتون باشد که از attack_modules.base.BaseAttack ارثبری کند و متد execute(self) → dict را پیادهسازی کند. مسیر ماژول در دیتابیس (فیلد module_path در AttackMethod) به صورت نقطهدار ثبت میشود.
کلاس پایه BaseAttack (کد کامل)
در زیر کد واقعی کلاس BaseAttack و کلاس کمکی APIModelWrapper از فایل attack_modules/base.py آمده است. تمام متدهای abstract و توابع کمکی که میتوانید در حمله خود استفاده کنید اینجا دیده میشوند.
"""
Abstract base class for all attack modules.
Every attack module MUST subclass BaseAttack and implement execute().
"""
import abc
import logging
import time
from typing import Any, Dict, Optional
logger = logging.getLogger(__name__)
class BaseAttack(abc.ABC):
"""
Abstract base class for all attack implementations.
Subclass this and implement execute() to create a new attack module.
Attributes:
eval_request: The EvaluationRequest Django model instance.
attack_method: The AttackMethod Django model instance.
params: Dict of attack parameters (merged defaults + custom).
device: 'cpu' or 'cuda'.
"""
name: str = "BaseAttack"
description: str = ""
category: str = "other" # whitebox, blackbox, backdoor, benchmark, llm
supported_domains: list = ["any"]
def __init__(self, eval_request, attack_method, params=None, device: str = "cpu"):
self.eval_request = eval_request
self.attack_method = attack_method
self.params = {**attack_method.default_params, **(params or {})}
self.device = device
self._log_lines = []
def log(self, message: str):
"""Append a log line (server log + log_output in report)."""
...
def get_log(self) -> str:
"""Return full execution log as string for execute() return."""
...
@abc.abstractmethod
def execute(self) -> Dict[str, Any]:
"""
Run the attack and return results.
Returns:
dict: metrics, summary, artifacts, log
"""
...
def _load_model(self):
"""
Load target model: whitebox → from file; blackbox → API wrapper.
"""
...
def _load_whitebox_model(self):
"""Load local model file (PyTorch / TensorFlow / ONNX)."""
...
def _create_api_wrapper(self):
"""Create APIModelWrapper for blackbox API calls."""
...
class APIModelWrapper:
"""Wrapper for remote model API (JSON, image, audio, text)."""
def __init__(self, endpoint: str, headers: dict, input_format: dict, output_format: dict):
...
def predict(self, input_data: Any) -> Dict[str, Any]:
"""Send input to API and return parsed output."""
...
def predict_batch(self, inputs: list) -> list:
"""Predict on a batch of inputs."""
...
def __call__(self, input_data):
return self.predict(input_data)
فرمت خروجی execute()
خروجی باید دقیقاً شامل این کلیدها باشد تا در سامانه و گزارشها نمایش داده شود. از self.log("پیام") برای ثبت لاگ و در پایان self.get_log() را در کلید log قرار دهید.
{
"metrics": {
"attack_success_rate": 0.58,
"clean_accuracy": 0.95,
"adversarial_accuracy": 0.37
},
"summary": "خلاصه متنی برای نمایش سریع",
"artifacts": [],
"log": self.get_log()
}
توابع کمکی و APIModelWrapper
-
_load_model() — با توجه به
eval_request.access_typeمدل را بارگذاری میکند: برایwhiteboxاز روی فایل (فراخوانی_load_whitebox_model())، برایblackboxیا سایر موارد با فراخوانی_create_api_wrapper(). -
_load_whitebox_model() — فایل مدل را از
eval_request.model_fileو فریمورک را ازeval_request.model_frameworkمیخواند. پشتیبانی:pytorch(torch.load + eval)،tensorflow/keras،onnx(onnxruntime). در صورت نبود فایل یا فریمورک نامعتبر،ValueErrorپرتاب میشود. -
_create_api_wrapper() — یک نمونه از
APIModelWrapperباendpoint،api_auth_headers،input_formatوoutput_formatازeval_requestبرمیگرداند. در صورت خالی بودنendpoint،ValueErrorپرتاب میشود. -
APIModelWrapper — متد
predict(input_data)درخواست POST به API میزند؛ با توجه بهinput_format.type(مثلاًimage,audio,text) payload ساخته میشود.predict_batch(inputs)برای چند ورودی است.
# نمونه استفاده در execute():
model = self._load_model() # whitebox → مدل محلی؛ blackbox → APIModelWrapper
epsilon = float(self.params.get("epsilon", 0.03))
self.log("شروع حمله با epsilon=" + str(epsilon))
# ...
return {"metrics": {...}, "summary": "...", "artifacts": [], "log": self.get_log()}
دامنههای پشتیبانیشده
در کلاس حمله، supported_domains را روی یکی از مقادیر زیر تنظیم کنید. سکو تسکهای دستهبندی برای تصویر، صوت و متن و سناریوهای تشخیص چهره و بکدور را پشتیبانی میکند.
- image — دستهبندی، تشخیص اشیا
- audio — تشخیص گفتار، کلاسیفیکیشن
- text — کلاسیفیکیشن، NER، LLM
- face — تشخیص چهره
- backdoor — BadNets، Trojan
- llm, tabular, multimodal, any
ثبت حمله در سکو
پس از پیادهسازی، از بخش مشارکت در سامانه (پرتال توسعهدهندگان) فرم «ارسال حمله جدید» را پر کنید: عنوان، دستهبندی، دامنه مدل، لینک مقاله و مسیر ماژول. پس از تایید ادمین، حمله با نام شما در پکیجهای ارزیابی قابل انتخاب خواهد بود.
در ادامه مثالهای کامل برای همهٔ حملات پیشفرض سکو (ارزیابی تمیز، FGSM، PGD، C&W، DeepFool و یک مثال سفارشی) بههمراه مرجع مقاله و لینک arXiv هر روش آمده است.
مثال کامل: ارزیابی روی داده تمیز (بنچمارک)
این ماژول حمله نیست؛ ابزار بنچمارک است که دقت مدل را روی دادههای بدون اغتشاش (train / validation / test) اندازه میگیرد. مبنای استاندارد برای گزارش دقت پایه قبل از حملات خصمانه است.
مرجع: سنجش دقت پایه (clean accuracy) در ارزیابی امنیتی مدلهای یادگیری عمیق — استاندارد مرسوم در مقالات خصمانه (مثلاً Goodfellow et al., ICLR 2015؛ Madry et al., ICLR 2018).
# توسعهدهنده: Mohammad Khalooei (khalooei@aut.ac.ir, mohammad.khalooei@sharif.edu)
# فایل: attack_modules/benchmark/clean_eval.py
from typing import Any, Dict
import numpy as np
from attack_modules.base import BaseAttack
class CleanEvaluation(BaseAttack):
name = "Clean Evaluation"
description = "Benchmark: evaluates model accuracy on clean data (train/val/test)"
category = "benchmark"
supported_domains = ["any"]
def execute(self) -> Dict[str, Any]:
self.log("Starting Clean Evaluation (Benchmark)")
num_samples = self.params.get("num_samples", 500)
splits = self.params.get("splits", ["train", "validation", "test"])
import torch
model = self._load_model()
input_format = getattr(self.eval_request, "input_format", None) or {}
shape = input_format.get("shape", [3, 224, 224])
if len(shape) != 3:
shape = [3, 224, 224]
num_classes = getattr(self.eval_request, "num_classes", None) or 10
results_by_split = {}
for split in splits:
correct = total = 0
for i in range(num_samples):
x = np.random.rand(1, *shape).astype(np.float32)
true_label = np.random.randint(0, num_classes)
with torch.no_grad():
tensor = torch.tensor(x).to(self.device)
logits = model(tensor)
pred = logits.argmax(1).item()
if pred == true_label:
correct += 1
total += 1
accuracy = correct / total if total else 0
results_by_split[split] = {"accuracy": round(accuracy, 4), "total_samples": total}
return {
"metrics": {"results_by_split": results_by_split, "accuracy": results_by_split.get("test", {}).get("accuracy", 0)},
"summary": " | ".join(f"{s}: دقت {d['accuracy']:.2%}" for s, d in results_by_split.items()),
"artifacts": [],
"log": self.get_log(),
}
مثال کامل: حمله FGSM
FGSM (Fast Gradient Sign Method) یک حمله تکمرحلهای جعبهسفید است که با استفاده از علامت گرادیان loss نسبت به ورودی، اختلال محدود ε اضافه میکند.
مرجع: Goodfellow, I. J., Shlens, J., & Szegedy, C. (2015). Explaining and Harnessing Adversarial Examples. ICLR 2015. arXiv:1412.6572
# توسعهدهنده: Mohammad Khalooei (khalooei@aut.ac.ir, mohammad.khalooei@sharif.edu)
# فایل: attack_modules/whitebox/fgsm.py
from typing import Any, Dict
import torch
import torch.nn.functional as F
from attack_modules.base import BaseAttack
class FGSMAttack(BaseAttack):
name = "FGSM"
description = "Fast Gradient Sign Method – single-step white-box attack"
category = "whitebox"
supported_domains = ["image"]
def execute(self) -> Dict[str, Any]:
self.log("Starting FGSM Attack")
epsilon = float(self.params.get("epsilon", 0.03))
num_samples = int(self.params.get("num_samples", 100))
model = self._load_model()
device = torch.device(self.device)
if hasattr(model, "to"):
model = model.to(device)
clean_correct = adv_correct = attack_success = total = 0
for i in range(num_samples):
x = torch.randn(1, 3, 224, 224, device=device, requires_grad=True)
target = torch.randint(0, 10, (1,), device=device)
output = model(x)
clean_pred = output.argmax(dim=1)
if clean_pred.item() == target.item():
clean_correct += 1
loss = F.cross_entropy(output, target)
loss.backward()
x_adv = x + epsilon * x.grad.sign()
x_adv = torch.clamp(x_adv, 0, 1)
with torch.no_grad():
adv_pred = model(x_adv).argmax(dim=1)
if adv_pred.item() == target.item():
adv_correct += 1
else:
attack_success += 1
total += 1
clean_accuracy = clean_correct / total if total else 0
robust_accuracy = adv_correct / total if total else 0
attack_success_rate = attack_success / total if total else 0
return {
"metrics": {"clean_accuracy": round(clean_accuracy, 4), "robust_accuracy": round(robust_accuracy, 4), "attack_success_rate": round(attack_success_rate, 4), "epsilon": epsilon},
"summary": f"FGSM eps={epsilon}: دقت پاک {clean_accuracy:.2%}, مقاوم {robust_accuracy:.2%}, ASR {attack_success_rate:.2%}",
"artifacts": [],
"log": self.get_log(),
}
مثال کامل: حمله PGD
PGD (Projected Gradient Descent) حمله تکراری جعبهسفید است که در هر مرحله یک گام گرادیان با اندازه α برمیدارد و اختلال را در توپ ε پروژه میکند.
مرجع: Madry, A., Makelov, A., Schmidt, L., Tsipras, D., & Vladu, A. (2018). Towards Deep Learning Models Resistant to Adversarial Attacks. ICLR 2018. arXiv:1706.06083
# توسعهدهنده: Mohammad Khalooei (khalooei@aut.ac.ir, mohammad.khalooei@sharif.edu)
# فایل: attack_modules/whitebox/pgd.py
from typing import Any, Dict
import torch
import torch.nn.functional as F
from attack_modules.base import BaseAttack
class PGDAttack(BaseAttack):
name = "PGD"
description = "Projected Gradient Descent – iterative white-box attack"
category = "whitebox"
supported_domains = ["image"]
def execute(self) -> Dict[str, Any]:
self.log("Starting PGD Attack")
epsilon = float(self.params.get("epsilon", 0.03))
alpha = float(self.params.get("alpha", 0.007))
num_steps = int(self.params.get("num_steps", 40))
num_samples = int(self.params.get("num_samples", 100))
random_start = self.params.get("random_start", True)
model = self._load_model()
device = torch.device(self.device)
if hasattr(model, "to"):
model = model.to(device)
clean_correct = adv_correct = total = 0
for i in range(num_samples):
x = torch.randn(1, 3, 224, 224, device=device)
target = torch.randint(0, 10, (1,), device=device)
with torch.no_grad():
if model(x).argmax(1).item() == target.item():
clean_correct += 1
x_adv = x.clone().detach()
if random_start:
x_adv = x_adv + torch.empty_like(x_adv).uniform_(-epsilon, epsilon)
x_adv = torch.clamp(x_adv, 0, 1)
for step in range(num_steps):
x_adv.requires_grad_(True)
loss = F.cross_entropy(model(x_adv), target)
loss.backward()
with torch.no_grad():
x_adv = x_adv + alpha * x_adv.grad.sign()
delta = torch.clamp(x_adv - x, -epsilon, epsilon)
x_adv = torch.clamp(x + delta, 0, 1).detach()
with torch.no_grad():
if model(x_adv).argmax(1).item() == target.item():
adv_correct += 1
total += 1
clean_acc = clean_correct / total if total else 0
robust_acc = adv_correct / total if total else 0
asr = 1 - robust_acc
return {
"metrics": {"clean_accuracy": round(clean_acc, 4), "robust_accuracy": round(robust_acc, 4), "attack_success_rate": round(asr, 4), "epsilon": epsilon, "num_steps": num_steps},
"summary": f"PGD eps={epsilon}, {num_steps} steps: دقت مقاوم {robust_acc:.2%}, ASR {asr:.2%}",
"artifacts": [],
"log": self.get_log(),
}
مثال کامل: حمله C&W (Carlini-Wagner)
C&W یک حمله بهینهسازیمحور جعبهسفید است که با کمینهسازی نرم L2 اختلال، نمونهٔ خصمانه را طوری میسازد که مدل برچسب اشتباه بدهد. از تابع loss خاصی با پارامتر اطمینان (confidence) و وزن c استفاده میکند.
مرجع: Carlini, N., & Wagner, D. (2017). Towards Evaluating the Robustness of Neural Networks. IEEE S&P 2017. arXiv:1608.04644
# توسعهدهنده: Mohammad Khalooei (khalooei@aut.ac.ir, mohammad.khalooei@sharif.edu)
# فایل: attack_modules/whitebox/cw.py
from typing import Any, Dict
import torch
import torch.nn.functional as F
import torch.optim as optim
from attack_modules.base import BaseAttack
class CWAttack(BaseAttack):
name = "C&W"
description = "Carlini & Wagner L2 optimization-based attack"
category = "whitebox"
supported_domains = ["image"]
def execute(self) -> Dict[str, Any]:
self.log("Starting C&W Attack")
confidence = self.params.get("confidence", 0.0)
learning_rate = self.params.get("learning_rate", 0.01)
max_iterations = self.params.get("max_iterations", 1000)
num_samples = self.params.get("num_samples", 50)
model = self._load_model()
device = torch.device(self.device)
if hasattr(model, "to"):
model = model.to(device)
input_format = getattr(self.eval_request, "input_format", None) or {}
shape = input_format.get("shape", [3, 224, 224])
if len(shape) != 3:
shape = [3, 224, 224]
success_count = total = 0
l2_norms = []
for i in range(num_samples):
x = torch.rand(1, *shape, device=device)
with torch.no_grad():
num_classes = model(x).size(1)
target = torch.randint(0, num_classes, (1,), device=device)
w = torch.zeros_like(x, requires_grad=True)
optimizer = optim.Adam([w], lr=learning_rate)
c = self.params.get("c", 1.0)
best_l2 = float("inf")
for step in range(max_iterations):
x_adv = 0.5 * (torch.tanh(w) + 1)
output = model(x_adv)
real = output.gather(1, target.unsqueeze(1)).squeeze()
other = torch.max((1 - F.one_hot(target, output.size(1)).float()) * output - F.one_hot(target, output.size(1)).float() * 1e4, dim=1)[0]
f_loss = torch.clamp(real - other + confidence, min=0)
l2 = torch.norm(x_adv - x, p=2)
loss = l2 + c * f_loss
optimizer.zero_grad()
loss.backward()
optimizer.step()
if f_loss.item() == 0 and l2.item() < best_l2:
best_l2 = l2.item()
with torch.no_grad():
if model(0.5 * (torch.tanh(w) + 1)).argmax(1).item() != target.item():
success_count += 1
l2_norms.append(best_l2)
total += 1
asr = success_count / total if total else 0
avg_l2 = sum(l2_norms) / len(l2_norms) if l2_norms else 0
return {
"metrics": {"attack_success_rate": round(asr, 4), "average_l2_perturbation": round(avg_l2, 4), "num_samples": total},
"summary": f"C&W: نرخ موفقیت {asr:.2%}, میانگین L2: {avg_l2:.4f}",
"artifacts": [],
"log": self.get_log(),
}
مثال کامل: حمله DeepFool
DeepFool کمترین اغتشاش لازم برای عبور از مرز تصمیم را با تقریب خطی و تکرار محاسبه میکند. برای هر کلاس غیر از کلاس فعلی، جهت حداقل جابهجایی را پیدا کرده و اختلال را جمع میکند تا طبقه عوض شود.
مرجع: Moosavi-Dezfooli, S. M., Fawzi, A., & Frossard, P. (2016). DeepFool: a simple and accurate method to fool deep neural networks. CVPR 2016. arXiv:1511.04599
# توسعهدهنده: Mohammad Khalooei (khalooei@aut.ac.ir, mohammad.khalooei@sharif.edu)
# فایل: attack_modules/whitebox/deepfool.py
from typing import Any, Dict
import torch
from attack_modules.base import BaseAttack
class DeepFoolAttack(BaseAttack):
name = "DeepFool"
description = "Minimal perturbation to cross decision boundary"
category = "whitebox"
supported_domains = ["image"]
def execute(self) -> Dict[str, Any]:
self.log("Starting DeepFool Attack")
max_iterations = self.params.get("max_iterations", 50)
num_samples = self.params.get("num_samples", 100)
overshoot = self.params.get("overshoot", 0.02)
model = self._load_model()
device = torch.device(self.device)
if hasattr(model, "to"):
model = model.to(device)
input_format = getattr(self.eval_request, "input_format", None) or {}
shape = input_format.get("shape", [3, 224, 224])
if len(shape) != 3:
shape = [3, 224, 224]
success_count = total = 0
perturbation_norms = []
for i in range(num_samples):
x = torch.rand(1, *shape, device=device)
with torch.no_grad():
orig_pred = model(x).argmax(1).item()
x_adv, r_total = x.clone(), torch.zeros_like(x)
for _ in range(max_iterations):
x_adv.requires_grad_(True)
output = model(x_adv)
num_classes = output.size(1)
current_pred = output.argmax(1).item()
if current_pred != orig_pred:
break
grads = []
for k in range(num_classes):
if x_adv.grad is not None:
x_adv.grad.zero_()
output[0, k].backward(retain_graph=True)
grads.append(x_adv.grad.data.clone())
f_orig = output[0, current_pred]
min_pert, best_w = float("inf"), None
for k in range(num_classes):
if k == current_pred:
continue
w_k = grads[k] - grads[current_pred]
f_k = output[0, k] - f_orig
pert = abs(f_k.item()) / (torch.norm(w_k).item() + 1e-8)
if pert < min_pert:
min_pert, best_w = pert, w_k
if best_w is not None:
r_i = (min_pert + 1e-4) * best_w / (torch.norm(best_w) + 1e-8)
r_total += r_i
x_adv = (x + (1 + overshoot) * r_total).detach()
with torch.no_grad():
if model(x_adv).argmax(1).item() != orig_pred:
success_count += 1
perturbation_norms.append(torch.norm(r_total).item())
total += 1
asr = success_count / total if total else 0
avg_pert = sum(perturbation_norms) / len(perturbation_norms) if perturbation_norms else 0
return {
"metrics": {"attack_success_rate": round(asr, 4), "average_perturbation_l2": round(avg_pert, 4), "num_samples": total},
"summary": f"DeepFool: نرخ موفقیت {asr:.2%}, میانگین L2: {avg_pert:.4f}",
"artifacts": [],
"log": self.get_log(),
}
مثال کامل: حمله KhalooeiAttack
در این مثال یک حمله ساده روی مدلهای دستهبندی تصویر (مثلاً CIFAR-10) پیادهسازی شده و نرخ موفقیت حمله محاسبه میشود. میتوانید آن را بهنام خودتان از طریق مشارکت در سامانه ثبت کنید.
پیادهسازی کلاس حمله
# توسعهدهنده: Mohammad Khalooei (khalooei@aut.ac.ir, mohammad.khalooei@sharif.edu)
# فایل: attack_modules/whitebox/khalooei.py
from typing import Any, Dict
import torch
from attack_modules.base import BaseAttack
class KhalooeiAttack(BaseAttack):
name = "KhalooeiAttack"
description = "حمله ساده برای تست مقاومت مدلهای تصویر نسبت به نویز محدود."
category = "whitebox"
supported_domains = ["image"]
def execute(self) -> Dict[str, Any]:
self.log("شروع اجرای KhalooeiAttack ...")
model = self._load_model()
device = self.device
model.to(device)
model.eval()
epsilon = float(self.params.get("epsilon", 0.03))
num_samples = int(self.params.get("num_samples", 64))
num_classes = self.eval_request.num_classes or 10
clean_correct = adv_correct = 0
for i in range(num_samples):
x = torch.rand(1, 3, 32, 32, device=device)
y = torch.randint(0, num_classes, (1,), device=device)
with torch.no_grad():
clean_pred = model(x).argmax(dim=1)
if clean_pred.eq(y).item():
clean_correct += 1
delta = torch.empty_like(x).uniform_(-epsilon, epsilon)
adv_x = torch.clamp(x + delta, 0.0, 1.0)
with torch.no_grad():
adv_pred = model(adv_x).argmax(dim=1)
if adv_pred.eq(y).item():
adv_correct += 1
if (i + 1) % 10 == 0:
self.log(f"نمونه {i+1}/{num_samples} پردازش شد.")
clean_acc = clean_correct / num_samples if num_samples else 0.0
adv_acc = adv_correct / num_samples if num_samples else 0.0
asr = max(clean_acc - adv_acc, 0.0)
summary = f"دقت تمیز: {clean_acc:.2%} | پس حمله: {adv_acc:.2%} | ASR: {asr:.2%}"
return {
"metrics": {"clean_accuracy": round(clean_acc, 4), "adversarial_accuracy": round(adv_acc, 4), "attack_success_rate": round(asr, 4)},
"summary": summary,
"artifacts": [],
"log": self.get_log(),
}
جمعبندی مراجع حملات پیشفرض
- ارزیابی تمیز: مبنای استاندارد دقت پایه در ارزیابی امنیتی (Goodfellow et al., Madry et al.).
- FGSM: Goodfellow, I. J., Shlens, J., & Szegedy, C. (2015). Explaining and Harnessing Adversarial Examples. ICLR 2015. arXiv:1412.6572
- PGD: Madry, A., Makelov, A., Schmidt, L., Tsipras, D., & Vladu, A. (2018). Towards Deep Learning Models Resistant to Adversarial Attacks. ICLR 2018. arXiv:1706.06083
- C&W: Carlini, N., & Wagner, D. (2017). Towards Evaluating the Robustness of Neural Networks. IEEE S&P 2017. arXiv:1608.04644
- DeepFool: Moosavi-Dezfooli, S. M., Fawzi, A., & Frossard, P. (2016). DeepFool: a simple and accurate method to fool deep neural networks. CVPR 2016. arXiv:1511.04599
حتماً خروجی execute() را مطابق چارچوب (metrics, summary, artifacts, log) برگردانید.