使用 Leo 的神经网络贷款决策

Justin | Aleo 中文
Aleo中文社区(非官方)
19 min readApr 28, 2023

这篇博文由 Aleo 社区成员 zk_tutorials 撰写

复制代码

在整个博客中,我们将引用代码片段。您可以通过单击框的右上角来复制这些片段。本文的完整源代码可以在 GitHub 上找到

function copy-> {
return “you can copy this code”
}

介绍

在我们之前的文章中,我们深入研究了 Leo 中的定点数和神经网络。现在,我们将探索神经网络在 zk-SNARKs 中的潜在应用,特别关注贷款决策。这些决定在 DeFi 应用程序中变得越来越重要,因为它们对于向借款人提供廉价贷款同时为贷方提供最大回报至关重要。

首先,我们将检查德国信用数据集,这是一种常用的机器学习信用数据集。使用 PyTorch 和 Python,我们将在此数据集上训练一个神经网络,以根据借款人的就业状况和信贷目的等各种参数来预测借款人是否会偿还贷款或违约。

成功训练神经网络后,我们将评估其有效性和实用性。一旦对结果感到满意,我们会将神经网络转移到 Leo 并评估其在该环境中的性能。

通过将神经网络集成到 zk-SNARKs 中,我们可以提高贷款决策的准确性,从而提高 DeFi 应用程序的效率和盈利能力。您可以在与我们上一篇文章的代码相同的GitHub 存储库中的两个应用程序文件夹中找到我们在本文中使用的代码。

德国信用数据集

德国信用数据集是机器学习领域著名的数据集,于 1994 年首次发布。它可公开下载,包含 1000 个实例,即 1000 个做出信用决策的数据案例。每个实例都包含有关信用情况以及在该特定情况下信用是否违约的数据。具体来说,每个实例包含20个属性,例如信用持续时间、申请人的就业状况、信用目的和申请人的信用记录。此外,数据集中的每个实例都标有一位信息,指示信用最终是否违约。

使用 PyTorch 和 Python 训练神经网络

为了创建神经网络,我们使用流行的深度学习库 PyTorch 和 Python 编程语言。我们使用多层感知器 (MLP) 前馈神经网络架构,我们也在上一篇文章中更深入地使用和解释了它。对于 20 个输入特征,我们在第一层创建 20 个输入神经元。在第三层,我们还需要 2 个输出神经元用于 2 个可能的输出类别,贷款偿还或违约。MLP 网络架构设计的一个经验法则是让一个隐藏层具有输入和输出层的平均神经元数量 — — 这为我们提供了隐藏(第二)层中的 11 个神经元。

以下代码首先加载数据集,然后创建一个子集,称为训练数据集。此外,它还创建了 MLP 神经网络架构。然后,它对训练数据进行归一化并训练神经网络。然后它将经过训练的神经网络和归一化参数存储在硬盘上。由于此处 CPU 的计算能力足够,我们使用 CPU 进行训练。对于更大的网络和数据集,可能需要 GPU。

import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
import pandas as pd
from sklearn.model_selection import train_test_split
import pickle


# load the german.data-numeric data set
data = pd.read_csv('german.data-numeric', delim_whitespace=True, header=None,
on_bad_lines='skip')


# define the neural network
class MLP(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(MLP, self).__init__()
self.fc1 = nn.Linear(input_size, hidden_size)
self.fc2 = nn.Linear(hidden_size, output_size)


def forward(self, x):
x = torch.relu(self.fc1(x))
x = self.fc2(x)
return x


X = data.iloc[:, 0:20]#df.iloc[:, :-1]#df.iloc[:, 0:6]#df.iloc[:, :-1]
y = data.iloc[:, -1] - 1


# split training and testing data
x_train, _, y_train, _ = train_test_split(X, y, test_size=0.2, random_state=0)


# normalize the data
x_train_mean = x_train.mean()
x_train_std = x_train.std()

x_train = (x_train - x_train_mean) / x_train_std


# convert pandas dataframes to tensors
x_train = torch.tensor(x_train.values, dtype=torch.float32)
y_train = torch.tensor(y_train.values, dtype=torch.long)


# combine the data into a dataset and dataloader
dataset = TensorDataset(x_train, y_train)


train_loader = DataLoader(dataset, batch_size=32, shuffle=True)


model = MLP(20, 10, 2)


# define the loss function and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)


# train the model
for epoch in range(100):
for inputs, labels in train_loader:
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()


# save model
torch.save(model.state_dict(), 'model.pt')


# save the mean and standard deviation using pickle
with open('mean_std.pkl', 'wb') as f:
pickle.dump((x_train_mean, x_train_std), f)

评估经过训练的神经网络

我们创建一个单独的文件来评估经过训练的神经网络。核心思想是在整个数据集的另一个子集上对其进行评估,该子集不同于用于训练的训练数据。我们将其称为测试数据集。然后我们加载存储的神经网络和归一化参数并评估神经网络。对于评估,我们使用接受者操作特征 (AUROC) 性能指标下的面积。该指标特别适用于不平衡的数据集,例如在我们的案例中,分类准确性等其他指标无济于事。在完美分类器的情况下,最大 AUROC 值为 1。以下代码执行这些步骤并计算 AUROC 指标。

import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
import pandas as pd
from sklearn.model_selection import train_test_split
import pickle


# load the data set
data = pd.read_csv('german.data-numeric', delim_whitespace=True,
header=None, on_bad_lines='skip')


# define the neural network
class MLP(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(MLP, self).__init__()
self.fc1 = nn.Linear(input_size, hidden_size)
self.fc2 = nn.Linear(hidden_size, output_size)


def forward(self, x):
x = torch.relu(self.fc1(x))
x = self.fc2(x)
return x


X = data.iloc[:, 0:20]#df.iloc[:, :-1]#df.iloc[:, 0:6]#df.iloc[:, :-1]
y = data.iloc[:, -1] - 1


# split training and testing data
_, x_test, _, y_test = train_test_split(X, y, test_size=0.2, random_state=0)


# open the pickle file to load the train mean and std
with open('mean_std.pkl', 'rb') as f:
[x_train_mean, x_train_std] = pickle.load(f)


x_test = (x_test - x_train_mean) / x_train_std

# load the model
model = MLP(20, 10, 2)
model.load_state_dict(torch.load('model.pt'))


# test the model


x_test = torch.tensor(x_test.values, dtype=torch.float)
y_test = torch.tensor(y_test.values, dtype=torch.float)
test_data = TensorDataset(x_test, y_test)
test_loader = DataLoader(test_data, batch_size=32, shuffle=False)


from sklearn.metrics import roc_auc_score
with torch.no_grad():
running_predicted_tensor = torch.tensor([])
for inputs, labels in test_loader:
outputs = model(inputs)
_, predicted = torch.max(outputs.data, 1)
running_predicted_tensor = torch.cat((running_predicted_tensor,
predicted), 0)
auc = roc_auc_score(y_test, running_predicted_tensor)
print('AUC: {}'.format(auc))

结果:AUROC:0.6897765905779504

我们获得了 ca 的 AUROC。.69,相当不错。虽然我们可以专注于进一步提高该价值,但其他工作已经完成了此类工作。结果对于我们的目的来说已经足够好了,我们现在专注于将神经网络转移到 Leo。

将神经网络转移到 Leo

要将神经网络从 PyTorch 模型转移到 Leo,我们可以使用我们在上一篇文章中开发的软件 — — 一个 Python 程序,它自动生成神经网络架构的代码,给定每层所需的神经元数量作为输入。Python 代码为 Leo 电路代码和输入参数生成一个 main.leo 和 input.in 文件。我们的参数是 20 个输入神经元、10 个隐藏神经元和 2 个输出神经元。对于 Leo,我们使用 i16 个变量,这意味着变量可以取正值和负值。因此,16 位中的一位保留用于符号。由于输入值是归一化的,其绝对值大多分布在 0 和 1 之间,因此我们需要高精度的小数位来表示和计算数字。因此,我们使用 2⁷ 的比例因子,

main.leo 代码可以在这里找到

然后,我们需要根据 Python 文件中的实际参数创建输入参数文件 — — 这意味着我们以定点格式提供神经网络输入参数和数据实例属性。在这种特定情况下,我们使用测试数据集中的第一个数据实例。对于提取,我们使用以下 Python 代码:

import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
import pandas as pd
from sklearn.model_selection import train_test_split
import pickle
import math


# load the data set
data = pd.read_csv('german.data-numeric', delim_whitespace=True,
header=None, on_bad_lines='skip')


# define the neural network
class MLP(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(MLP, self).__init__()
self.fc1 = nn.Linear(input_size, hidden_size)
self.fc2 = nn.Linear(hidden_size, output_size)


def forward(self, x):
x = torch.relu(self.fc1(x))
x = self.fc2(x)
return x


X = data.iloc[:, 0:20]#df.iloc[:, :-1]#df.iloc[:, 0:6]#df.iloc[:, :-1]
y = data.iloc[:, -1] - 1


# split training and testing data
_, x_test, _, y_test = train_test_split(X, y, test_size=0.2, random_state=0)


# open the pickle file to load the train mean and std
with open('mean_std.pkl', 'rb') as f:

[x_train_mean, x_train_std] = pickle.load(f)


x_test = (x_test - x_train_mean) / x_train_std


# load the model
model = MLP(20, 10, 2)
model.load_state_dict(torch.load('model.pt'))


for key in model.state_dict().keys():
print(key)
print(model.state_dict()[key])


str_list_inputs = []


str_inputs = ""


str_list_inputs.append("[main]\n")


for i, key in enumerate(model.state_dict().keys()):
if(i==0):
first_layer = model.state_dict()[key]
layer_name = ""
if('weight' in key):
layer_name = "w"
if('bias' in key):
layer_name = "b"

value = model.state_dict()[key]
for j, val in enumerate(value):
# get dimension of the value
if(len(val.shape) == 1):
for k, val2 in enumerate(val):
val_fixed_point = int(val2 * 2**7)

variable_line = layer_name + str(math.floor(i/2)+1) + str(k)
+ str(j) + ": " + "u32 = " + str(0) + ";\n"
str_list_inputs.append(variable_line)
else:
val_fixed_point = int(val * 2**7)
#variable_line = layer_name + str(math.floor(i/2)+1) + str(j) + ": "
#continued: + "u32 = " + str(val_fixed_point) + ";\n"
variable_line = layer_name + str(math.floor(i/2)+1) + str(j) +
": " + "u32 = " + str(0) + ";\n"
str_list_inputs.append(variable_line)


str_list_inputs.append("\n")


# load the data set
data = pd.read_csv('german.data-numeric', delim_whitespace=True, header=None,
on_bad_lines='skip')


X = data.iloc[:, 0:20]#df.iloc[:, :-1]#df.iloc[:, 0:6]#df.iloc[:, :-1]
y = data.iloc[:, -1] - 1


# split training and testing data
_, x_test, _, y_test = train_test_split(X, y, test_size=0.2, random_state=0)


# open the pickle file to load the train mean and std
with open('mean_std.pkl', 'rb') as f:
[x_train_mean, x_train_std] = pickle.load(f)


x_test = (x_test - x_train_mean) / x_train_std


x_test = torch.tensor(x_test.values, dtype=torch.float)
y_test = torch.tensor(y_test.values, dtype=torch.float)
test_data = TensorDataset(x_test, y_test)
test_loader = DataLoader(test_data, batch_size=32, shuffle=False)


for i in range(len((first_layer[0]))):
value = test_data[0][0][i]
val_fixed_point = int(value * 2**7)
str_list_inputs.append("input" + str(i) + ": u32 = " + str(val_fixed_point) + ";\n")


str_list_inputs.append("\n")
str_list_inputs.append("[registers]")
str_list_inputs.append("\n")
str_list_inputs.append("r0: [u32; 2] = [0, 0];")

with open("project.in", "w+") as file:
file.writelines(str_list_inputs)

我们现在运行代码并获取此输入文件

在 PyTorch 神经网络中评估数据实例时,我们从神经网络中获得以下输出向量:‍

张量([1.8403,-2.2161])

第一个值远高于第二个值的事实表明数据实例属于 0 类(贷款将被偿还),在查看数据集中数据实例的标签时确实如此。

评估 Leo 神经网络

当使用“leo run”运行 leo 电路时,我们在电路中获得以下输出向量:‍

222、-272

要解释十进制系统中的数字,我们需要将它们除以比例因子 128。因此,我们得到以下十进制结果:‍

1.73 -2.125

这非常接近上面在 Python 中的浮点计算!因此,基于结果的决定 — 授予贷款,因为第一个输出神经元比第二个输出神经元高得多 — 仍然适用。因此,我们决定使用高比例因子证明在拥有准确工作的定点神经网络方面是成功的。

我们现在进一步分析“leo run”命令的输出:

Build Starting...
Build Compiling main program... ("/home/user/Aleo Studio/project/src/main.leo")
Build Number of constraints - 2357872
Build Complete
Done Finished in 11838 milliseconds

Setup Starting...
Setup Saving proving key ("/home/user/Aleo Studio/project/outputs/project.lpk")
Setup Complete
Setup Saving verification key ("/home/user/Aleo Studio/project/outputs/project.lvk")
Setup Complete
Done Finished in 117657 milliseconds

Proving Starting...
Proving Saving proof... ("/home/user/Aleo Studio/project/outputs/project.proof")
Done Finished in 78358 milliseconds

Verifying Starting...
Verifying Proof is valid
Done Finished in 11 milliseconds

Leo 电路产生 2.3M 约束,这表明现在使用 Leo 在 zk-SNARKs 中运行这种规模甚至更复杂规模的神经网络是可行的。

结论

我们能够使用 LEO 编程语言以定点数运行 MLP 神经网络,即使对于贷款决策等关键应用,计算的准确性也很高。计算开销对于当代硬件来说是非常合理的,这表明该技术已经为实际应用做好了准备。我们使用机器学习信用数据集演示了这样一个用例。这对于将 AI 逻辑链接到智能合约中很有用,而零知识方面可以隐藏个人数据和专有机器学习模型。看到应用程序在未来的时间里不断发展,这将是一件很有趣的事情。您可以在这个Github 存储库中找到所有代码。

--

--