联邦学习原理-分类-python代码实现

1、介绍

联邦学习其设计目标是在保障大数据交换时的信息安全、保护终端数据和个人数据隐私、保证合法合规的前提下,在多参与方或多计算结点之间开展高效率的机器学习。

优势:
1、数据隔离,数据不会泄露到外部,满足用户隐私保护和数据安全的需求;
2、能够保证模型质量无损,不会出现负迁移,保证联邦模型比割裂的独立模型效果好;
3、参与者地位对等,能够实现公平合作;
4、能够保证参与各方在保持独立性的情况下,进行信息与模型参数的加密交换,并同时获得成长。

联邦学习系统主要由两部分构成:
1、加密样本对齐。由于各个data owners的用户群体并非完全重合,需要通过用户样本对齐技术在 不公开各自数据的前提下确认共有用户(且不暴露独有用户),随后就可以联合这些用户的特征进行建模。
2、加密模型训练。在确定共有用户群体后,就可以利用这些数据训练机器学习模型。为保证训练过程中数据的保密性,需要借助第三方协作者进行加密训练。①协作者把公钥分发给各个data owners,用以对训练过程中需要交换的数据进行加密;②data owners之间以加密形式交互中间结果用于计算梯度;③各data owners分别基于加密的梯度值进行计算,同时根据标签数据计算损失,并把这些结果汇总给第三方协作者,由第三方协作者通过汇总结果计算总梯度并将其解密;④第三方协作者将解密后的梯度回传给各data owners,让其根据梯度更新各自模型的参数。

迭代上述步骤直至损失函数收敛,这样就完成了整个训练过程。在样本对齐及模型训练过程中,各个data owners的数据始终保留在本地,且训练中的数据交互也不会导致数据隐私泄露。因此,联邦学习可实现多方合作训练模型。
在这里插入图片描述

2、分类

2.1 横向联邦学习

横向联邦学习,适用于参与者的数据特征重叠较多,而样本ID重叠较少的情况。横向联邦学习也称为特征对齐的联邦学习,即横向联邦学习的参与者的数据特征是对齐的,如图所示,联合多个参与者的具有相同特征的多行样本进行联邦学习,即各个参与者的训练数据是横向划分的,称为横向联邦学习。横向联邦使训练样本的总数量增加。
在这里插入图片描述
在这里插入图片描述

横向联邦学习以数据的特征维度为导向,取出参与方特征相同而用户不完全相同的部分进行联合训练。在此过程中,通过各参与方之间的样本联合,扩大了训练的样本空间,从而提升了模型的准确度和泛化能力。

步骤:
1、参与者在本地计算训练梯度,使用加密,差分隐私或秘密共享技术加密梯度的更新,并将加密的结果发送到服务器;
2、服务器在不了解有关任何参与者的信息的情况下,聚合各用户的梯度更新模型参数;
3、服务器将汇总结果模型发回给各参与者;
4、各参与者使用解密的梯度更新各自的模型。

2.1 横向联邦学习

纵向联邦学习也称为样本对齐的联邦学习,即纵向联邦学习的参与者的训练样本是对齐的。
如图所示,联合多个参与者的共同样本的不同数据特征进行联邦学习,即各个参与者的训练数据是纵向划分的,称为纵向联邦学习。纵向联邦学习需要先做样本对齐,即找出参与者拥有的共同的样本。只有联合多个参与者的共同样本的不同特征进行纵向联邦学习。纵向联邦使训练样本的特征维度增多。
在这里插入图片描述
在这里插入图片描述
纵向联邦学习是以共同用户为数据的对齐导向,取出参与方用户相同而特征不完全相同的部分进行联合训练。因此,在联合训练时,需要先对各参与方数据进行样本对齐,获得用户重叠的数据,然后各自在被选出的数据集上进行训练。

步骤:
第一步:第三方C加密样本对齐。是在系统级做这件事,因此在企业感知层面不会暴露非交叉用户。
第二步:对齐样本进行模型加密训练:
1、合作者C创建加密对,将公钥发送给A和B;
2、A和B分别计算和自己相关的特征中间结果,并加密交互,用来求得各自梯度和损失;
3、A和B分别计算各自加密后的梯度并添加掩码发送给C,同时B计算加密后的损失发送给C;
4、C解密梯度和损失后回传给A和B,A、B去除掩码并更新模型。

2.3 迁移联邦学习

当参与者间特征和样本重叠都很少时可以考虑使用联邦迁移学习,如不同地区的银行和商超间的联合。主要适用于以深度神经网络为基模型的场景。

利用数据、任务、或模型之间的相似性,将在源领域学习过的模型,应用于目标领域的一种学习过程。迁移学习主要分为三类:基于实例的迁移、基于特征的迁移和基于模型的迁移。
在这里插入图片描述

3、代码实现(PyTorch)

这里主要实现Fed Avg算法,以下是算法伪代码:
在这里插入图片描述

实验结果图:

在这里插入图片描述
在这里插入图片描述

代码文件

1、首先我们要为每个客户端分配数据,在实际场景中,每个客户端有自己独有的数据,这里为了模拟场景,手动划分数据集给每个客户端。(!!!注意需要修改路径!!!)
数据集采用的是MNIST数据集,百度网盘下载地址可点击链接

# _*_ coding : utf-8 _*_
# @Time : 2022/11/16 10:36
# @Author : 小刘同学home
# @File : data handle
# @Project : 2022s

import numpy as np
import struct

from PIL import Image
import os

data_file = 'D:/postgraduate/MNIST_data/train-images.idx3-ubyte'  # 需要修改的路径

data_file_size = 47040016
data_file_size = str(data_file_size - 16) + 'B'

data_buf = open(data_file, 'rb').read()

magic, numImages, numRows, numColumns = struct.unpack_from(
    '>IIII', data_buf, 0)
datas = struct.unpack_from(
    '>' + data_file_size, data_buf, struct.calcsize('>IIII'))
datas = np.array(datas).astype(np.uint8).reshape(
    numImages, 1, numRows, numColumns)

label_file = 'D:/postgraduate/MNIST_data/train-labels.idx1-ubyte'  # 需要修改的路径

# It's 60008B, but we should set to 60000B
label_file_size = 60008
label_file_size = str(label_file_size - 8) + 'B'

label_buf = open(label_file, 'rb').read()

magic, numLabels = struct.unpack_from('>II', label_buf, 0)
labels = struct.unpack_from(
    '>' + label_file_size, label_buf, struct.calcsize('>II'))
labels = np.array(labels).astype(np.int64)

datas_root = './data/image_turn/'  # 需要修改的路径
if not os.path.exists(datas_root):
    os.mkdir(datas_root)

for i in range(10):
    file_name = datas_root + os.sep + str(i)
    if not os.path.exists(file_name):
        os.mkdir(file_name)

for ii in range(numLabels):
    img = Image.fromarray(datas[ii, 0, 0:28, 0:28])
    label = labels[ii]
    file_name = datas_root + os.sep + str(label) + os.sep + \
                'mnist_train_' + str(ii) + '.png'
    img.save(file_name)

import numpy as np
import struct

from PIL import Image
import os

data_file = 'D:/postgraduate/MNIST_data/t10k-images.idx3-ubyte'  # 需要修改的路径

data_file_size = 7840016
data_file_size = str(data_file_size - 16) + 'B'

data_buf = open(data_file, 'rb').read()

magic, numImages, numRows, numColumns = struct.unpack_from(
    '>IIII', data_buf, 0)
datas = struct.unpack_from(
    '>' + data_file_size, data_buf, struct.calcsize('>IIII'))
datas = np.array(datas).astype(np.uint8).reshape(
    numImages, 1, numRows, numColumns)

label_file = 'D:/postgraduate/MNIST_data/t10k-labels.idx1-ubyte'  # 需要修改的路径

# It's 10008B, but we should set to 10000B
label_file_size = 10008
label_file_size = str(label_file_size - 8) + 'B'

label_buf = open(label_file, 'rb').read()

magic, numLabels = struct.unpack_from('>II', label_buf, 0)
labels = struct.unpack_from(
    '>' + label_file_size, label_buf, struct.calcsize('>II'))
labels = np.array(labels).astype(np.int64)

datas_root = './data/image_test_turn/'  # 需要修改的路径

if not os.path.exists(datas_root):
    os.mkdir(datas_root)

for i in range(10):
    file_name = datas_root + os.sep + str(i)
    if not os.path.exists(file_name):
        os.mkdir(file_name)

for ii in range(numLabels):
    img = Image.fromarray(datas[ii, 0, 0:28, 0:28])
    label = labels[ii]
    file_name = datas_root + os.sep + str(label) + os.sep + \
                'mnist_test_' + str(ii) + '.png'
    img.save(file_name)

2、FedAvg.py
我这里设置的迭代次数为1000,若觉得代码运行时间太久,可设置低一点。

# _*_ coding : utf-8 _*_
# @Time : 2022/11/16 10:26
# @Author : 小刘同学home
# @File : FedAvg
# @Project : 2022s


import argparse
import torch
import os
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
from torch.autograd import Variable
from PIL import Image
import torch
import copy
import pandas as pd
import random
import time
import sys
import re
import matplotlib.pyplot as plt


name = str(sys.argv[0])

home_path = "./"


class MyDataset(torch.utils.data.Dataset):  # 创建自己的类:MyDataset,这个类是继承的torch.utils.data.Dataset
    def __init__(self, root, data, label, transform=None, target_transform=None):  # 初始化一些需要传入的参数
        super(MyDataset, self).__init__()
        imgs = []  # 创建一个名为img的空列表
        self.img_route = root
        for i in range(len(data)):
            imgs.append((data[i], int(label[i])))
        self.imgs = imgs
        self.transform = transform
        self.target_transform = target_transform

    def __getitem__(self, index):  # 这个方法是必须要有的,用于按照索引读取每个元素的具体内容
        fn, label = self.imgs[index]  # fn是图片path #fn和label分别获得imgs[index]也即是刚才每行中word[0]和word[1]的信息
        route = self.img_route + str(label) + "/" + fn
        img = Image.open(route)  # 按照path读入图片from PIL import Image # 按照路径读取图片
        if self.transform is not None:
            img = self.transform(img)  # 是否进行transform
        return img, label  # return很关键,return回哪些内容,那么我们在训练时循环读取每个batch时,就能获得哪些内容

    def __len__(self):  # 这个函数也必须要写,它返回的是数据集的长度,也就是多少张图片,要和loader的长度作区分
        return len(self.imgs)


filePath = home_path + 'data/image_turn/'
train_data = []
train_label = []
for i in range(10):
    train_data.append(os.listdir(filePath + str(i)))
    train_label.append([i] * len(train_data[i]))
filePath = home_path + 'data/image_test_turn/'
test_data = []
test_label = []
for i in range(10):
    test_data.append(os.listdir(filePath + str(i)))
    test_label.append([i] * len(test_data[i]))
test_ori = []
test_label_ori = []
for x in range(10):
    test_ori += test_data[x]
    test_label_ori += test_label[x]
test_data = MyDataset(home_path + "data/image_test_turn/", test_ori, test_label_ori,
                      transform=transforms.ToTensor())
test_loader = DataLoader(dataset=test_data, batch_size=64)


# 搭建卷积神经网络
class MyConvNet(nn.Module):
    def __init__(self):
        super(MyConvNet, self).__init__()

        # 定义第一个卷积层
        self.conv1 = nn.Sequential(
            nn.Conv2d(
                in_channels=1,   # 输入的feature map,输入通道数
                out_channels=16,  # 输出的feature map,输出通道数
                stride=1,  # 卷积核步长
                kernel_size=3,  # 卷积核尺寸
                padding=1,  # 进行填充
            ),
            nn.ReLU(),  # 激活函数
            nn.AvgPool2d(
                kernel_size=2, # 平均值池化层,使用2*2
                stride=2)
        )   # 池化后:(16*28*28) ->(16*14*14)

        # 定义第二个卷积层
        self.conv2 = nn.Sequential(
            nn.Conv2d(16, 32, 3, 1, 0),
            nn.ReLU(),
            nn.MaxPool2d(2, 2),
        )
        self.classifier = nn.Sequential(
            nn.Linear(32 * 6 * 6, 256),
            nn.ReLU(),
            nn.Linear(256, 128),
            nn.ReLU(),
            nn.Linear(128, 10)
        )

    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        x = x.view(x.size(0), -1)
        output = self.classifier(x)
        return output


def train_model(model, traindataloader, criterion, optimizer, batch_max, num_epochs):
    train_loss_all = []
    train_acc_all = []
    for epoch in range(num_epochs):
        train_loss = 0.0
        train_corrects = 0
        train_num = 0
        temp = random.sample(traindataloader, batch_max)  #随机选取客户端
        for (b_x, b_y) in temp:
            model.train()  # 设置模式为训练模式
            if (torch.cuda.is_available()):
                b_x = b_x.cuda()
                b_y = b_y.cuda()
            output = model(b_x)
            pre_lab = torch.argmax(output, 1)
            loss = criterion(output, b_y)
            optimizer.zero_grad()
            loss.backward()  # 将损失loss 向输入侧进行反向传播
            optimizer.step()  # 优化器对x的值进行更新
            train_loss += loss.item() # .item()获得张量中的元素值
            train_corrects += torch.sum(pre_lab == b_y.data) # 将预测值与标签值相等的数累加
            train_num += b_x.size(0) # b_x.size(0):取出第一个维度的数字
        train_loss_all.append(train_loss / train_num)
        train_acc_all.append(train_corrects.double().item() / train_num)
        print("Train Loss:{:.4f}  Train Acc: {:.4f}".format(train_loss_all[-1], train_acc_all[-1]))
    return model



def local_train(local_convnet_dict, traindataloader, epochs, batch_max):
    if (torch.cuda.is_available()):
        local_convnet = MyConvNet().cuda()
    else:
        local_convnet = MyConvNet()
    local_convnet.load_state_dict(local_convnet_dict)
    optimizer = optim.Adam(local_convnet.parameters(), lr=0.01, weight_decay=5e-4)
    criterion = nn.CrossEntropyLoss()  # 交叉熵
    local_convnet = train_model(local_convnet, traindataloader, criterion, optimizer, batch_max, epochs)
    minus_convnet_dict = MyConvNet().state_dict()
    for name in local_convnet.state_dict():
        minus_convnet_dict[name] = local_convnet_dict[name] - local_convnet.state_dict()[name]
    return minus_convnet_dict



def Central_model_update(Central_model, minus_convnet_client):
    weight = 1

    model_dict = Central_model.state_dict()
    for name in Central_model.state_dict():
        for local_dict in minus_convnet_client:
            model_dict[name] = model_dict[name] - weight * local_dict[name] / len(minus_convnet_client)
    Central_model.load_state_dict(model_dict)
    return Central_model



def train_data_loader(client_num, ClientSort1):
    global train_data
    global train_label
    train_loaders = []
    for i in range(client_num):
        train_ori = []
        label_ori = []
        for j in range(10):
            train_ori += train_data[j]
            label_ori += train_label[j]
        train_datas = MyDataset(home_path + "data/image_turn/", train_ori, label_ori,
                                transform=transforms.ToTensor())
        train_loader = DataLoader(dataset=train_datas, batch_size=100, shuffle=True)
        train_list = []
        for step, (b_x, b_y) in enumerate(train_loader):
            train_list.append((b_x, b_y))
        train_loaders.append(train_list)
    return train_loaders


def test_accuracy(Central_model):
    global test_loader
    test_correct = 0
    for data in test_loader:
        Central_model.eval()  # 设置模式为评估模式
        inputs, lables = data
        if (torch.cuda.is_available()):
            inputs = inputs.cuda()
        inputs, lables = Variable(inputs), Variable(lables)
        outputs = Central_model(inputs)
        if (torch.cuda.is_available()):
            outputs = outputs.cpu()
        id = torch.max(outputs.data, 1)
        test_correct += torch.sum(id == lables.data)
        test_correct = test_correct
    print("correct:%.3f%%" % (100 * test_correct / len(test_ori)))
    return 100 * test_correct / len(test_ori)


############################
#      中央共享模型
############################
if (torch.cuda.is_available()):
    Central_model = MyConvNet().cuda()
else:
    Central_model = MyConvNet()
local_client_num = 10  # 局部客户端数量
ClientSort1 = 10
# Central_model.load_state_dict(torch.load('F:/params.pkl'))
global_epoch = 1000
# print(test_accuracy(Central_model))



train_loaders = train_data_loader(local_client_num, ClientSort1)
result = []
count = 0
for i in range(global_epoch):
    count += 1
    minus_model = []
    for j in range(local_client_num):
        minus_model.append(local_train(Central_model.state_dict(), train_loaders[j], 1, 1))
    Central_model = Central_model_update(Central_model, minus_model)
    print("epoch: ", count, "\naccuracy:")
    result.append(float(test_accuracy(Central_model)))



plt.xlabel('round')
plt.ylabel('accuracy')
plt.plot(range(0, len(result)), result, color='r', linewidth='1.0', label='同步FedAvg')
plt.savefig(home_path + name + ".jpg")
filename = open(home_path + name + ".txt", mode='w')
for namet in result:
    filename.write(str(namet))
    filename.write('\n')
filename.close()
torch.save(Central_model.state_dict(), filePath + name + '.pkl')

以上代码详细解释可查看:PyTorch 实现联邦学习FedAvg (详解)

  • 0
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
联邦学习(Federated Learning)是一种新兴的机器学习方法,它将数据存储在本地设备上,通过联合学习算法在本地设备上进行模型训练,然后将权重平均值发送到服务器进行汇总,从而实现模型的更新。联邦学习算法可以在不泄露数据的情况下对模型进行训练,可以保护用户的隐私,同时也可以减少数据传输的成本。 下面是一个简单的联邦学习代码实现,其中假设有多个本地设备(clients)和一个服务器(server): 1. 客户端代码: ```python import torch import torch.nn as nn import torch.optim as optim class Client(object): def __init__(self, data, target): self.model = nn.Linear(2, 1) self.data = data self.target = target def train(self): criterion = nn.MSELoss() optimizer = optim.SGD(self.model.parameters(), lr=0.01) for epoch in range(10): optimizer.zero_grad() output = self.model(self.data) loss = criterion(output, self.target) loss.backward() optimizer.step() return self.model.state_dict() ``` 2. 服务器端代码: ```python import torch class Server(object): def __init__(self, clients): self.clients = clients def train(self): models = [] for client in self.clients: model = client.train() models.append(model) avg_model = self._average_models(models) return avg_model def _average_models(self, models): avg_model = {} for key in models[0].keys(): avg_model[key] = torch.stack([model[key] for model in models]).mean(0) return avg_model ``` 3. 主函数代码: ```python import torch from client import Client from server import Server def main(): # generate data data = torch.randn(10, 2) target = torch.randn(10, 1) # create clients clients = [] for i in range(3): client_data = data[i*3:(i+1)*3] client_target = target[i*3:(i+1)*3] client = Client(client_data, client_target) clients.append(client) # create server server = Server(clients) # train model avg_model = server.train() print(avg_model) if __name__ == '__main__': main() ``` 上述代码中,客户端随机生成了一个2维的数据和目标值,并使用一个简单的线性模型进行训练。服务器通过调用每个客户端的train方法,获取每个客户端的模型权重,并对这些权重进行平均,最终返回平均模型权重。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值