Usando dados criptografados para aprendizado de máquina sem descriptografá-los


Usando dados criptografados para aprendizado de máquina sem descriptografá-los
Este artigo discute técnicas criptográficas avançadas. Esta é apenas uma visão geral das pesquisas conduzidas por Julia Computing. Não use os exemplos dados aqui em aplicativos comerciais. Sempre consulte os criptografadores antes de aplicar a criptografia.

Aqui você pode baixar o pacote que implementa toda a magia, e aqui está o código discutido no artigo.

1. Introdução


Digamos que você acabou de desenvolver um novo modelo interessante de aprendizado de máquina (é claro, usando o Flux.jl ). E agora você deseja começar a implantá-lo para seus usuários. Como você fará isso? Provavelmente, a maneira mais fácil é fornecer o modelo aos usuários e deixá-lo executar localmente em seus dados. Mas essa abordagem tem desvantagens:

  1. Os modelos de aprendizado de máquina são grandes e os computadores dos usuários podem não ter recursos suficientes de computação ou disco.
  2. Os modelos de aprendizado de máquina geralmente são atualizados e pode não ser conveniente enviar regularmente grandes quantidades de dados pela rede.
  3. O desenvolvimento do modelo é demorado e requer uma grande quantidade de recursos de computação. E você pode querer uma compensação por isso na forma de uma taxa pelo uso do seu modelo.

Em seguida, eles lembram que o modelo pode ser fornecido na nuvem por meio da API. Nos últimos anos, muitos desses serviços apareceram; cada grande plataforma em nuvem oferece serviços semelhantes aos desenvolvedores corporativos. Mas os usuários em potencial enfrentam um dilema óbvio: agora seus dados são processados ​​em um servidor remoto, o que pode não ser confiável. Isso tem implicações éticas e legais claras que limitam o uso de tais serviços. Em setores regulamentados, especialmente serviços de saúde e financeiros, geralmente não é possível enviar dados de pacientes e clientes a terceiros para processamento.

Alguma outra opção?

Acontece que existe! Descobertas recentes em criptografia permitem a computação com dados sem decodificá-los . Por exemplo, um usuário envia dados criptografados (por exemplo, imagens) para uma API da nuvem que inicia um modelo de aprendizado de máquina e envia uma resposta criptografada. Em nenhum momento os dados são descriptografados, o provedor de nuvem não obtém acesso às imagens de origem e não pode descriptografar a previsão calculada. Como isso é possível? Vamos descobrir o exemplo da criação de um serviço para reconhecimento de manuscrito em imagens criptografadas do conjunto de dados MNIST.

Sobre criptografia homomórfica


A capacidade de executar cálculos com dados criptografados é comumente referida como "computação segura". Essa é uma grande área para pesquisa, com inúmeras abordagens à criptografia, dependendo de todos os tipos de cenários de aplicativos. Vamos nos concentrar em uma técnica chamada "criptografia homomórfica". Nesse sistema, as seguintes operações geralmente estão disponíveis para nós:

  • pub_key, eval_key, priv_key = keygen()
  • encrypted = encrypt(pub_key, plaintext)
  • decrypted = decrypt(priv_key, encrypted)
  • encrypted′ = eval(eval_key, f, encrypted)

As três primeiras operações são simples e familiares para todos que já usaram algoritmos de criptografia assimétrica (por exemplo, se você se conectou via TLS). Toda mágica acontece na última operação. Durante a criptografia, ele avalia a função f e retorna outro valor criptografado calculado de acordo com o resultado da avaliação de f no valor criptografado. Esse recurso deu seu nome à abordagem. A avaliação está relacionada à operação de criptografia:

 f(decrypt(priv_key, encrypted)) == decrypt(priv_key, eval(eval_key, f, encrypted)) 

Da mesma forma, usando um valor criptografado, podemos avaliar homomorfismos arbitrários f .

Quais funções f suportadas dependem de esquemas criptográficos e operações suportadas. Se apenas um f suportado (por exemplo, f = + ), o circuito será chamado de "parcialmente homomórfico". Se f pode ser qualquer conjunto completo de gateways, com base nos quais esquemas arbitrários podem ser criados, então, para um tamanho limitado de um esquema, isso é chamado de outro tipo de cálculo parcialmente homomórfico - "um tanto homomórfico" e para um tamanho ilimitado - cálculo "completamente homomórfico". Você pode transformar "de alguma forma" em uma criptografia completamente homomórfica usando a técnica de inicialização, mas isso está além do escopo de nosso artigo. A criptografia totalmente homomórfica é uma descoberta relativamente recente; o primeiro esquema de trabalho (embora impraticável) foi publicado por Craig Gentry em 2009 . Existem vários esquemas posteriores (e práticos) completamente homomórficos. Existem também pacotes de software que implementam qualitativamente esses esquemas. Na maioria das vezes, eles usam o Microsoft SEAL e PALISADE . Além disso, abri recentemente o código de implementação desses algoritmos Pure Julia . Neste artigo, usaremos a criptografia CKKS implementada nele.

Visão geral do CKS


O CKKS (pelos nomes dos autores do trabalho científico Cheon-Kim-Kim-Song, que propôs o algoritmo em 2016) é um esquema de criptografia homomórfica que permite a avaliação homomórfica das seguintes operações primitivas:

  • A adição elementar dos comprimentos de n vetores de números complexos.
  • Multiplicação por elemento dos comprimentos de n vetores complexos.
  • Gire (no contexto de circshift de circshift ) elementos em um vetor.
  • Emparelhamento integrado de elementos do vetor.

O parâmetro n depende do nível desejado de segurança e precisão, e geralmente é bastante alto. Em nosso exemplo, será igual a 4096 (um valor mais alto aumenta a segurança, mas também é mais difícil nos cálculos, é escalado aproximadamente como n log n ).

Além disso, os cálculos usando CKKS são barulhentos . Portanto, os resultados são aproximados e deve-se tomar cuidado para que os resultados sejam avaliados com precisão suficiente para não afetar a exatidão do resultado.

Por outro lado, essas restrições não são incomuns para os desenvolvedores de pacotes de aprendizado de máquina. Aceleradores especiais como a GPU também costumam operar com vetores numéricos. Além disso, para muitos desenvolvedores, os números de ponto flutuante às vezes parecem barulhentos devido à influência de algoritmos de seleção, multithreading etc. Quero enfatizar que a principal diferença aqui é que os cálculos aritméticos com números de ponto flutuante são inicialmente determinísticos, mesmo que isso não seja óbvio devido à complexidade da implementação, embora as primitivas do CKKS sejam realmente barulhentas. Mas talvez isso permita que os usuários entendam que o ruído não é tão assustador quanto pode parecer.

Agora vamos ver como você pode executar essas operações na Julia (nota: parâmetros muito inseguros são selecionados, com essas operações ilustramos apenas o uso da biblioteca no REPL).

 julia> using ToyFHE # Let's play with 8 element vectors julia> N = 8; # Choose some parameters - we'll talk about it later julia> ℛ = NegacyclicRing(2N, (40, 40, 40)) ℤ₁₃₂₉₂₂₇₉₉₇₅₆₈₀₈₁₄₅₇₄₀₂₇₀₁₂₀₇₁₀₄₂₄₈₂₅₇/(x¹⁶ + 1) # We'll use CKKS julia> params = CKKSParams(ℛ) CKKS parameters # We need to pick a scaling factor for a numbers - again we'll talk about that later julia> Tscale = FixedRational{2^40} FixedRational{1099511627776,T} where T # Let's start with a plain Vector of zeros julia> plain = CKKSEncoding{Tscale}(zero(ℛ)) 8-element CKKSEncoding{FixedRational{1099511627776,T} where T} with indices 0:7: 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im # Ok, we're ready to get started, but first we'll need some keys julia> kp = keygen(params) CKKS key pair julia> kp.priv CKKS private key julia> kp.pub CKKS public key # Alright, let's encrypt some things: julia> foreach(i->plain[i] = i+1, 0:7); plain 8-element CKKSEncoding{FixedRational{1099511627776,T} where T} with indices 0:7: 1.0 + 0.0im 2.0 + 0.0im 3.0 + 0.0im 4.0 + 0.0im 5.0 + 0.0im 6.0 + 0.0im 7.0 + 0.0im 8.0 + 0.0im julia> c = encrypt(kp.pub, plain) CKKS ciphertext (length 2, encoding CKKSEncoding{FixedRational{1099511627776,T} where T}) # And decrypt it again julia> decrypt(kp.priv, c) 8-element CKKSEncoding{FixedRational{1099511627776,T} where T} with indices 0:7: 0.9999999999995506 - 2.7335193113350057e-16im 1.9999999999989408 - 3.885780586188048e-16im 3.000000000000205 + 1.6772825551165524e-16im 4.000000000000538 - 3.885780586188048e-16im 4.999999999998865 + 8.382500573679615e-17im 6.000000000000185 + 4.996003610813204e-16im 7.000000000001043 - 2.0024593503998215e-16im 8.000000000000673 + 4.996003610813204e-16im # Note that we had some noise. Let's go through all the primitive operations we'll need: julia> decrypt(kp.priv, c+c) 8-element CKKSEncoding{FixedRational{1099511627776,T} where T} with indices 0:7: 1.9999999999991012 - 5.467038622670011e-16im 3.9999999999978817 - 7.771561172376096e-16im 6.00000000000041 + 3.354565110233105e-16im 8.000000000001076 - 7.771561172376096e-16im 9.99999999999773 + 1.676500114735923e-16im 12.00000000000037 + 9.992007221626409e-16im 14.000000000002085 - 4.004918700799643e-16im 16.000000000001346 + 9.992007221626409e-16im julia> csq = c*c CKKS ciphertext (length 3, encoding CKKSEncoding{FixedRational{1208925819614629174706176,T} where T}) julia> decrypt(kp.priv, csq) 8-element CKKSEncoding{FixedRational{1208925819614629174706176,T} where T} with indices 0:7: 0.9999999999991012 - 2.350516767363621e-15im 3.9999999999957616 - 5.773159728050814e-15im 9.000000000001226 - 2.534464540987068e-15im 16.000000000004306 - 2.220446049250313e-15im 24.99999999998865 + 2.0903753311370056e-15im 36.00000000000222 + 4.884981308350689e-15im 49.000000000014595 + 1.0182491378134327e-15im 64.00000000001077 + 4.884981308350689e-15im 

Tão simples! Um leitor atento pode perceber que o CSQ é um pouco diferente do texto cifrado anterior. Em particular, o texto cifrado tem "comprimento 3" e a escala é muito maior. Uma explicação do que é e do que é necessário está além do escopo deste artigo. Basta dizer que precisamos diminuir os valores antes de continuar com os cálculos; caso contrário, o "local" terminará no texto cifrado. Felizmente, podemos reduzir cada um dos dois valores aumentados:

 # To get back down to length 2, we need to `keyswitch` (aka # relinerarize), which requires an evaluation key. Generating # this requires the private key. In a real application we would # have generated this up front and sent it along with the encrypted # data, but since we have the private key, we can just do it now. julia> ek = keygen(EvalMultKey, kp.priv) CKKS multiplication key julia> csq_length2 = keyswitch(ek, csq) CKKS ciphertext (length 2, encoding CKKSEncoding{FixedRational{1208925819614629174706176,T} where T}) # Getting the scale back down is done using modswitching. julia> csq_smaller = modswitch(csq_length2) CKKS ciphertext (length 2, encoding CKKSEncoding{FixedRational{1.099511626783e12,T} where T}) # And it still decrypts correctly (though note we've lost some precision) julia> decrypt(kp.priv, csq_smaller) 8-element CKKSEncoding{FixedRational{1.099511626783e12,T} where T} with indices 0:7: 0.9999999999802469 - 5.005163520332181e-11im 3.9999999999957723 - 1.0468514951188039e-11im 8.999999999998249 - 4.7588542623100616e-12im 16.000000000023014 - 1.0413447889166631e-11im 24.999999999955193 - 6.187833723406491e-12im 36.000000000002345 + 1.860733715346631e-13im 49.00000000001647 - 1.442396043149794e-12im 63.999999999988695 - 1.0722489563648028e-10im 

Além disso, modswitching (abreviação de comutação de módulo, comutação de módulo) reduz o tamanho do módulo de texto cifrado, para que não possamos continuar fazendo isso indefinidamente (usamos um esquema de criptografia um tanto homomórfica):

 julia> ℛ # Remember the ring we initially created ℤ₁₃₂₉₂₂₇₉₉₇₅₆₈₀₈₁₄₅₇₄₀₂₇₀₁₂₀₇₁₀₄₂₄₈₂₅₇/(x¹⁶ + 1) julia> ToyFHE.ring(csq_smaller) # It shrunk! ℤ₁₂₀₈₉₂₅₈₂₀₁₄₄₅₉₃₇₇₉₃₃₁₅₅₃/(x¹⁶ + 1)</code>     —  (rotations).      keyswitch,       (evaluation key,     ): <source lang="julia">julia> gk = keygen(GaloisKey, kp.priv; steps=2) CKKS galois key (element 25) julia> decrypt(circshift(c, gk)) decrypt(kp, circshift(c, gk)) 8-element CKKSEncoding{FixedRational{1099511627776,T} where T} with indices 0:7: 7.000000000001042 + 5.68459112632516e-16im 8.000000000000673 + 5.551115123125783e-17im 0.999999999999551 - 2.308655353580721e-16im 1.9999999999989408 + 2.7755575615628914e-16im 3.000000000000205 - 6.009767921608429e-16im 4.000000000000538 + 5.551115123125783e-17im 4.999999999998865 + 4.133860996136768e-17im 6.000000000000185 - 1.6653345369377348e-16im # And let's compare to doing the same on the plaintext julia> circshift(plain, 2) 8-element OffsetArray(::Array{Complex{Float64},1}, 0:7) with eltype Complex{Float64} with indices 0:7: 7.0 + 0.0im 8.0 + 0.0im 1.0 + 0.0im 2.0 + 0.0im 3.0 + 0.0im 4.0 + 0.0im 5.0 + 0.0im 6.0 + 0.0im 

Abordamos o básico do uso da biblioteca HE. Mas antes de passar a usar essas primitivas para calcular previsões de redes neurais, vejamos o processo de aprendê-las.

Modelo de aprendizado de máquina


Se você não estiver familiarizado com o aprendizado de máquina ou a biblioteca Flux.jl, recomendo uma rápida pesquisa na documentação do Flux.jl ou consulte uma introdução gratuita ao aprendizado de máquina , porque discutiremos apenas as mudanças na aplicação do modelo aos dados criptografados.

Vamos começar usando a rede neural convolucional do zoológico Flux . Vamos realizar o mesmo ciclo de treinamento, com preparação de dados e assim por diante, apenas configurando um pouco o modelo. Aqui está:

 function reshape_and_vcat(x) let y=reshape(x, 64, 4, size(x, 4)) vcat((y[:,i,:] for i=axes(y,2))...) end end model = Chain( # First convolution, operating upon a 28x28 image Conv((7, 7), 1=>4, stride=(3,3), x->x.^2), reshape_and_vcat, Dense(256, 64, x->x.^2), Dense(64, 10), ) 

Esse é o mesmo modelo do trabalho “Computação matricial terceirizada segura e aplicativo para redes neurais” , que usa o mesmo esquema criptográfico com duas diferenças: 1) por uma questão de simplicidade, não criptografamos o modelo em si e 2) após cada camada que temos São utilizados vetores bayesianos (isso é feito por padrão no Flux), sem ter certeza se isso estava no trabalho mencionado. Talvez, devido ao segundo ponto, a precisão no conjunto de testes de nosso modelo tenha sido ligeiramente maior (98,6% versus 98,1%), mas diferenças hiperparamétricas também possam ser o motivo.

Incomum (para quem tem experiência em aprendizado de máquina) é a ativação de funções x.^2 . Na maioria dos casos, nesses casos, eles usam tanh , relu ou algo mais fantasioso. Porém, embora essas funções (especialmente relu ) sejam facilmente calculadas para valores de texto comuns, no entanto, elas podem exigir muitos recursos computacionais para avaliá-las em forma criptografada (geralmente estimamos a aproximação polinomial). Felizmente, neste caso, x.^2 funciona muito bem.

O restante do ciclo de aprendizado permaneceu o mesmo. Removemos o softmax do modelo para a função de perda através da logitcrossentropy (você pode deixá-lo e avaliar o softmax após a descriptografia no cliente). O código completo para o treinamento do modelo está no GitHub , ele é executado em alguns minutos em qualquer nova placa de vídeo.

Operações efetivas


Agora sabemos quais operações precisamos executar:

  • Coagulação.
  • Elemento quadrado.
  • Multiplicação de matrizes.

Ao quadrado tudo é simples, já o examinamos acima, portanto consideraremos duas outras operações. Assumimos que o comprimento do pacote de dados é 64 (você pode notar que os parâmetros do modelo e o tamanho do pacote são escolhidos de forma a tirar proveito do vetor de 4096 elementos que obtivemos como resultado de uma escolha realista dos parâmetros).

Coagulação


Lembre-se de como a coagulação funciona. Pegue uma janela (no nosso caso 7x7) da matriz de entrada original e cada elemento da janela é multiplicado por um elemento de máscara de convolução. Em seguida, movemos a janela para algum passo (no nosso caso, o passo é 3, ou seja, movemos 3 elementos) e repetimos o processo (com a mesma máscara de convolução). A animação do processo ( fonte ) para a convolução 3x3 com a etapa (2, 2) mostrada abaixo (matriz azul - entrada, verde - saída):


Além disso, realizamos a convolução em quatro "canais" diferentes (ou seja, repetimos a convolução mais três vezes com máscaras diferentes).

Agora sabemos o que fazer, resta entender como. Temos a sorte de que a convolução é a primeira operação em nosso modelo. Como resultado, para economizar recursos, podemos pré-processar os dados no cliente e depois criptografá-los (sem usar pesos). Vamos fazer o seguinte:

  • Primeiro, calculamos cada janela de convolução (ou seja, uma amostra 7x7 das imagens de origem), o que fornece 64 matrizes 7x7 para cada imagem de entrada. Observe que, para uma janela 7x7 em incrementos de 2, haverá janelas de convolução 8x8 para avaliar a imagem de entrada 28x28.
  • Vamos coletar em um vetor as mesmas posições em cada janela. Ou seja, para cada imagem, teremos um vetor de 64 elementos ou um vetor de 64x64 para um pacote de tamanho 64 (um total de 49 matrizes 64x64).
  • Vamos criptografar.

Então a coagulação simplesmente se transforma em multiplicação escalar de toda a matriz com o elemento de máscara correspondente. E resumindo mais tarde todos os 49 elementos, obtemos o resultado da dobragem. Aqui está a aparência da implementação dessa estratégia (em texto simples):

 function public_preprocess(batch) ka = OffsetArray(0:7, 0:7) # Create feature extracted matrix I = [[batch[i′*3 .+ (1:7), j′*3 .+ (1:7), 1, k] for i′=ka, j′=ka] for k = 1:64] # Reshape into the ciphertext Iᵢⱼ = [[I[k][l...][i,j] for k=1:64, l=product(ka, ka)] for i=1:7, j=1:7] end Iᵢⱼ = public_preprocess(batch) # Evaluate the convolution weights = model.layers[1].weight conv_weights = reverse(reverse(weights, dims=1), dims=2) conved = [sum(Iᵢⱼ[i,j]*conv_weights[i,j,1,channel] for i=1:7, j=1:7) for channel = 1:4] conved = map(((x,b),)->x .+ b, zip(conved, model.layers[1].bias)) 

Este (módulo para alterar a dimensão) (módulo - alterar a ordem dos tamanhos) fornece a mesma resposta que a operação model.layers[1](batch) .

Adicione operações de criptografia:

 Iᵢⱼ = public_preprocess(batch) C_Iᵢⱼ = map(Iᵢⱼ) do Iij plain = CKKSEncoding{Tscale}(zero(plaintext_space(ckks_params))) plain .= OffsetArray(vec(Iij), 0:(N÷2-1)) encrypt(kp, plain) end weights = model.layers[1].weight conv_weights = reverse(reverse(weights, dims=1), dims=2) conved3 = [sum(C_Iᵢⱼ[i,j]*conv_weights[i,j,1,channel] for i=1:7, j=1:7) for channel = 1:4] conved2 = map(((x,b),)->x .+ b, zip(conved3, model.layers[1].bias)) conved1 = map(ToyFHE.modswitch, conved2) 

Observe que a chave não é necessária aqui porque os pesos são públicos. Portanto, não aumentamos o comprimento do texto cifrado.

Multiplicação de matrizes


Passando para a multiplicação de matrizes, podemos usar a rotação de elementos no vetor para alterar a ordem dos índices de multiplicação. Vamos considerar o layout da linha dos elementos da matriz em um vetor. Se mudarmos o vetor por um múltiplo do tamanho da linha, obtemos o efeito da rotação da coluna, que é uma primitiva suficiente para implementar a multiplicação de matrizes (pelo menos matrizes quadradas). Vamos tentar:

 function matmul_square_reordered(weights, x) sum(1:size(weights, 1)) do k # We rotate the columns of the LHS and take the diagonal weight_diag = diag(circshift(weights, (0,(k-1)))) # We rotate the rows of the RHS x_rotated = circshift(x, (k-1,0)) # We do an elementwise, broadcast multiply weight_diag .* x_rotated end end function matmul_reorderd(weights, x) sum(partition(1:256, 64)) do range matmul_square_reordered(weights[:, range], x[range, :]) end end fc1_weights = model.layers[3].W x = rand(Float64, 256, 64) @assert (fc1_weights*x) ≈ matmul_reorderd(fc1_weights, x) 

Obviamente, para a multiplicação geral da matriz, é necessário algo mais complicado, mas por enquanto isso é suficiente.

Melhorando a técnica


Agora todos os componentes de nossa técnica funcionam. Aqui está o código inteiro (exceto para definir opções de seleção e coisas semelhantes):

 ek = keygen(EvalMultKey, kp.priv) gk = keygen(GaloisKey, kp.priv; steps=64) Iᵢⱼ = public_preprocess(batch) C_Iᵢⱼ = map(Iᵢⱼ) do Iij plain = CKKSEncoding{Tscale}(zero(plaintext_space(ckks_params))) plain .= OffsetArray(vec(Iij), 0:(N÷2-1)) encrypt(kp, plain) end weights = model.layers[1].weight conv_weights = reverse(reverse(weights, dims=1), dims=2) conved3 = [sum(C_Iᵢⱼ[i,j]*conv_weights[i,j,1,channel] for i=1:7, j=1:7) for channel = 1:4] conved2 = map(((x,b),)->x .+ b, zip(conved3, model.layers[1].bias)) conved1 = map(ToyFHE.modswitch, conved2) Csqed1 = map(x->x*x, conved1) Csqed1 = map(x->keyswitch(ek, x), Csqed1) Csqed1 = map(ToyFHE.modswitch, Csqed1) function encrypted_matmul(gk, weights, x::ToyFHE.CipherText) result = repeat(diag(weights), inner=64).*x rotated = x for k = 2:64 rotated = ToyFHE.rotate(gk, rotated) result += repeat(diag(circshift(weights, (0,(k-1)))), inner=64) .* rotated end result end fq1_weights = model.layers[3].W Cfq1 = sum(enumerate(partition(1:256, 64))) do (i,range) encrypted_matmul(gk, fq1_weights[:, range], Csqed1[i]) end Cfq1 = Cfq1 .+ OffsetArray(repeat(model.layers[3].b, inner=64), 0:4095) Cfq1 = modswitch(Cfq1) Csqed2 = Cfq1*Cfq1 Csqed2 = keyswitch(ek, Csqed2) Csqed2 = modswitch(Csqed2) function naive_rectangular_matmul(gk, weights, x) @assert size(weights, 1) < size(weights, 2) weights = vcat(weights, zeros(eltype(weights), size(weights, 2)-size(weights, 1), size(weights, 2))) encrypted_matmul(gk, weights, x) end fq2_weights = model.layers[4].W Cresult = naive_rectangular_matmul(gk, fq2_weights, Csqed2) Cresult = Cresult .+ OffsetArray(repeat(vcat(model.layers[4].b, zeros(54)), inner=64), 0:4095) 

Não parece muito legal, mas se você fez tudo isso, deve entender cada passo.
Agora vamos pensar sobre quais abstrações poderiam simplificar nossas vidas. Estamos saindo do campo da cartografia e do aprendizado de máquina e passando para a arquitetura da linguagem de programação, então vamos aproveitar o fato de que Julia permite que você use e crie abstrações poderosas. Por exemplo, você pode encapsular todo o processo de extração de convoluções no seu tipo de matriz:

 using BlockArrays """ ExplodedConvArray{T, Dims, Storage} <: AbstractArray{T, 4} Represents a an `nxmx1xb` array of images, but rearranged into a series of convolution windows. Evaluating a convolution compatible with `Dims` on this array is achievable through a sequence of scalar multiplications and sums on the underling storage. """ struct ExplodedConvArray{T, Dims, Storage} <: AbstractArray{T, 4} # sx*sy matrix of b*(dx*dy) matrices of extracted elements # where (sx, sy) = kernel_size(Dims) # (dx, dy) = output_size(DenseConvDims(...)) cdims::Dims x::Matrix{Storage} function ExplodedConvArray{T, Dims, Storage}(cdims::Dims, storage::Matrix{Storage}) where {T, Dims, Storage} @assert all(==(size(storage[1])), size.(storage)) new{T, Dims, Storage}(cdims, storage) end end Base.size(ex::ExplodedConvArray) = (NNlib.input_size(ex.cdims)..., 1, size(ex.x[1], 1)) function ExplodedConvArray{T}(cdims, batch::AbstractArray{T, 4}) where {T} x, y = NNlib.output_size(cdims) kx, ky = NNlib.kernel_size(cdims) stridex, stridey = NNlib.stride(cdims) kax = OffsetArray(0:x-1, 0:x-1) kay = OffsetArray(0:x-1, 0:x-1) I = [[batch[i′*stridex .+ (1:kx), j′*stridey .+ (1:ky), 1, k] for i′=kax, j′=kay] for k = 1:size(batch, 4)] Iᵢⱼ = [[I[k][l...][i,j] for k=1:size(batch, 4), l=product(kax, kay)] for (i,j) in product(1:kx, 1:ky)] ExplodedConvArray{T, typeof(cdims), eltype(Iᵢⱼ)}(cdims, Iᵢⱼ) end function NNlib.conv(x::ExplodedConvArray{<:Any, Dims}, weights::AbstractArray{<:Any, 4}, cdims::Dims) where {Dims<:ConvDims} blocks = reshape([ Base.ReshapedArray(sum(xx[i,j]*weights[i,j,1,channel] for i=1:7, j=1:7), (NNlib.output_size(cdims)...,1,size(x, 4)), ()) for channel = 1:4 ],(1,1,4,1)) BlockArrays._BlockArray(blocks, BlockArrays.BlockSizes([8], [8], [1,1,1,1], [64])) end 

Aqui, novamente usamos o BlockArrays para representar uma matriz 8x8x4x64 como quatro matrizes 8x8x1x64 como no código-fonte. Agora, a apresentação do primeiro estágio ficou muito mais bonita, pelo menos com matrizes não criptografadas:

 julia> cdims = DenseConvDims(batch, model.layers[1].weight; stride=(3,3), padding=(0,0,0,0), dilation=(1,1)) DenseConvDims: (28, 28, 1) * (7, 7) -> (8, 8, 4), stride: (3, 3) pad: (0, 0, 0, 0), dil: (1, 1), flip: false julia> a = ExplodedConvArray{eltype(batch)}(cdims, batch); julia> model(a) 10×64 Array{Float32,2}: [snip] 

Agora, como conectamos isso à criptografia? Para fazer isso, você precisa:

  1. Criptografe a estrutura ( ExplodedConvArray ) para obter o texto cifrado para cada campo. Operações com uma estrutura criptografada verificarão o que a função faria com a estrutura original e farão a mesma coisa homomorficamente.
  2. Intercepte determinadas operações para executá-las de maneira diferente em um contexto criptografado.

Felizmente, Julia nos fornece uma abstração para isso: um plug-in de compilador que usa o mecanismo Cassette.jl . Não vou lhe dizer o que é e como funciona, vou dizer brevemente que ele pode determinar o contexto, por exemplo, Encrypted e, em seguida, define as regras de funcionamento das operações nesse contexto. Por exemplo, você pode escrever isso para o segundo requisito:

 # Define Matrix multiplication between an array and an encrypted block array function (*::Encrypted{typeof(*)})(a::Array{T, 2}, b::Encrypted{<:BlockArray{T, 2}}) where {T} sum(a*b for (i,range) in enumerate(partition(1:size(a, 2), size(b.blocks[1], 1)))) end # Define Matrix multiplication between an array and an encrypted array function (*::Encrypted{typeof(*)})(a::Array{T, 2}, b::Encrypted{Array{T, 2}}) where {T} result = repeat(diag(a), inner=size(a, 1)).*x rotated = b for k = 2:size(a, 2) rotated = ToyFHE.rotate(GaloisKey(*), rotated) result += repeat(diag(circshift(a, (0,(k-1)))), inner=size(a, 1)) .* rotated end result end 

Como resultado, o usuário poderá escrever todos os itens acima com uma quantidade mínima de trabalho manual:

 kp = keygen(ckks_params) ek = keygen(EvalMultKey, kp.priv) gk = keygen(GaloisKey, kp.priv; steps=64) # Create evaluation context ctx = Encrypted(ek, gk) # Do public preprocessing batch = ExplodedConvArray{eltype(batch)}(cdims, batch); # Run on encrypted data under the encryption context Cresult = ctx(model)(encrypt(kp.pub, batch)) # Decrypt the answer decrypt(kp, Cresult) 

, . ( ℛ, modswitch, keyswitch ..) , . , , , , .

Conclusão


— . Julia . RAMPARTS ( paper , JuliaCon talk ) : Julia- - PALISADE. Julia Computing RAMPARTS Verona, . , . . , , .

, ToyFHE . , , , .

Source: https://habr.com/ru/post/pt478514/


All Articles