在分布式AI環境下,同態加密神經網絡有助于保護商業公司知識產權和消費者隱私。讓我們和DeepMind數據科學家、Udacity深度學習導師Andrew Trask一起,來看看如何基于Numpy實現同態加密神經網絡吧。
TLDR:在這篇文章中,我們將訓練一個在訓練階段完全加密的神經網絡(在未加密的數據上訓練)。得到的神經網絡將具備兩個有益的性質。首先,保護神經網絡的智能免遭竊取,使有價值的AI可以在不安全的環境中加以訓練而不用冒智能遭竊的風險。其次,網絡只能進行加密預測(大概對外部世界毫無影響,因為在沒有密鑰的情況下,外部世界無法理解預測)。這在用戶和超智能間構成了一個有價值的權力失衡。如果AI是同態加密的,那么在AI看來,整個外部世界也是同態加密的。一個控制密鑰的人類可以選擇解鎖AI本身(將AI釋放到世界中)或僅僅解密AI做出的單個預測(看起來更安全)。
超智能
很多人都擔憂超智能有一天會選擇傷害人類。史蒂芬·霍金曾呼吁建立新的世界政府來管理我們賦予人工智能的能力,以防人工智能最終摧毀人類。這些是相當大膽的主張,我認為它們反映了科學界和整個世界對這一問題的普遍擔憂。本文將是一篇介紹解決這一問題的潛在技術方案的教程,我將通過一些玩具樣例代碼來演示這一方法。
目標很簡單。我們想要創建未來會變得非常智能的AI技術(智能到可以解決治愈癌癥、終結世界上的饑餓等問題),但是這樣的智能受人類的控制(基于密鑰),因而其智能的應用是受限的。不受限的學習是很棒的,但知識的不受限的應用可能具有潛在危險性。
為了介紹這一想法,讓我先簡要介紹兩個非常激動人心的研究領域:深度學習和同態加密。
一、什么是深度學習?
深度學習是用于自動化智能的工具套件,主要基于神經網絡。這一計算機科學的領域,是最近AI技術爆發的主要動力,因為深度學習在許多智能任務上超越了先前的表現記錄。例如,他是DeepMind的AlphaGo系統的主要組成部分。
神經網絡基于輸入做出預測。它通過試錯法學習做出有效的預測。剛開始,它做出一個預測(起初基本上是隨機預測),接著接收一個“錯誤信號”,該信號表明它的預測過高或過低(通常是概率)。在這一周期重復數百萬次后,網絡開始搞明白情況。想要了解更多神經網絡如何工作的細節,請參考基于Numpy實現神經網絡:反向傳播一文。
這里最神奇的是錯誤信號。如果不告知預測的表現有多好,神經網絡無法學習。牢記這一點。
二、什么是同態加密?
顧名思義,同態加密是一種加密的形式。在不對稱情形下,它可以接受完全可讀的文本,然后基于“公鑰”將其轉變為亂碼。更重要的是,它可以基于“私鑰”將亂碼轉回同樣的文本。然而,除非你有“私鑰”,(理論上)你無法解碼加密后的亂碼。
同態加密是一種特殊形式的加密。它允許某人在無法閱讀信息的前提下以特定的方式修改加密信息。例如,同態加密可以應用于數字上,讓加密過的數字可以進行乘法和加法運算而無需解密數字。下面是一些玩具樣例。
現在出現了越來越多的同態加密方案,各有不同的性質。這是一個相對年輕的領域,仍有一些明顯的問題有待解決,不過我們將這些內容留待以后討論。
就目前而言,讓我們從整數公鑰加密方案開始。整數公鑰加密方案是一種乘法和加法上的同態加密,允許進行上圖的操作。不僅如此,由于公鑰允許“單向”加密,你甚至可以進行未加密數字和加密數字間的運算(通過單向加密),上圖的2 * Cypher A就是一個例子。(某些加密方案甚至不要求這一點……不過同樣……我們以后討論這個。)
三、我們可以結合這兩者嗎?
也許深度學習和同態加密之間最頻繁的互動體現在數據隱私上。當你同態加密數據時,你無法讀取數據但仍然可以保持大多數有趣的統計學結構。這讓人們得以在加密數據上訓練模型(CryptoNets)。甚至有一家名為Numer.ai的初創對沖基金加密昂貴的專有數據,允許任何人嘗試訓練機器學習模型預測股票市場。通常這不可能辦到,因為會導致放棄極為昂貴的信息(不可能基于通常的加密數據訓練模型)。
然而,本文將反其道而行,加密神經網絡,然后在解密信息上加以訓練。
復雜度驚人的神經網絡,事實上可以劃分成很少(少得驚人)幾種組件,這些組件不斷重復以構成神經網絡。其實,僅僅基于如下操作,就可以創建很多最先進的神經網絡:
加法
乘法
除法
減法
Sigmoid
Tanh
指數函數
那么,讓我們提出這一明顯的技術問題,我們能否同態加密神經網絡本身?我們會想這么做嗎?結果發現,基于一些保守的逼近,這是可以辦到的。
加法 —— 開箱即用
乘法 —— 開箱即用
除法 —— 開箱即用?只是乘法的倒數
加法 —— 開箱即用?只是加上負數
Sigmoid —— 嗯……也許有點難度
Tanh —— 嗯……也許有點難度
指數函數 —— 嗯……也許有點難度
看起來實現除法和減法會是相當微不足道的事情,但那些更復雜的函數就……好吧……比簡單的加法和乘法更復雜。為了嘗試同態加密一個深度神經網絡,我們還需要一個秘密原料。
四、泰勒級數展開
也許你在小學學過,泰勒級數允許我們使用無限項加法、減法、乘法、除法來計算一個復雜(非線性)函數。這很完美?。ǔ藷o限部分)。幸運的是,如果你早早地停止了計算精確的泰勒級數展開,你仍然能得到手頭的函數的一個逼近值。下面是通過泰勒級數逼近一些流行函數的例子(來源)。
等下!這里有指數!別擔心。指數不過是反復相乘。下面是使用泰勒級數逼近sigmoid函數的python實現(相關公式見Wolfram Alpha)。我們將選取級數的開始幾項,看看能逼近到什么程度。
import numpy as np
def sigmoid_exact(x):
return1 / (1 + np.exp(-x))
# 使用泰勒級數
def sigmoid_approximation(x):
return (1 / 2) + (x / 4) - (x**3 / 48) + (x**5 / 480)
for lil_number in [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0]:
print("\n輸入:" + str(lil_number))
print("精確的Sigmoid值:" + str(sigmoid_exact(lil_number)))
print("逼近Sigmoid:" + str(sigmoid_approximation(lil_number)))
結果:
輸入:0.1
精確的Sigmoid值:0.52497918747894
逼近Sigmoid:0.5249791874999999
輸入:0.2
精確的Sigmoid值:0.549833997312478
逼近Sigmoid:0.549834
輸入:0.3
精確的Sigmoid值:0.574442516811659
逼近Sigmoid:0.5744425624999999
輸入:0.4
精確的Sigmoid值:0.598687660112452
逼近Sigmoid:0.598688
輸入:0.5
精確的Sigmoid值:0.6224593312018546
逼近Sigmoid:0.6224609375000001
輸入:0.6
精確的Sigmoid值:0.6456563062257954
逼近Sigmoid:0.6456620000000001
輸入:0.7
精確的Sigmoid值:0.6681877721681662
逼近Sigmoid:0.6682043125000001
輸入:0.8
精確的Sigmoid值:0.6899744811276125
逼近Sigmoid:0.690016
輸入:0.9
精確的Sigmoid值:0.7109495026250039
逼近Sigmoid:0.7110426875
輸入:1.0
精確的Sigmoid值:0.7310585786300049
逼近Sigmoid:0.73125
僅僅使用了泰勒級數的前4項,我們已經相當逼近sigmoid函數了。既然我們已經具備了通用的策略,是時候選擇一個同態加密算法了。
五、選擇加密算法
同態加密是一個相對較新的領域,其中的主要里程碑是Craig Gentry在2009年發現的第一個全同態加密算法。這一里程碑為許多后來者建立了據點。大部分關于同態加密的激動人心的研究圍繞開發圖靈完備的同態加密計算機展開。因此,對全同態加密方案的需求讓人們試圖找到一個算法,使得進行任意計算所需的多種邏輯門都可以通這一算法高效而安全地計算。大體的希望是人們能夠安全地將工作放到云端,而不必冒發送到云端的數據被發送者以外的人讀取的風險。這是一個非??岬南敕?,也取得了很多進展。
然而,這一角度存在一些缺陷。一般而言,相比普通電腦,大多數全同態加密方案慢得讓人懷疑人生(目前還不實用)。這鼓舞了一系列有趣的研究,將操作種類限制為某種程度上同態,這樣至少可以進行某些操作。不那么靈活,但是更快,這是常見的計算上的折衷。
這是我們想要開始查看的地方。理論上,我們想要一個操作浮點數的同態加密方案(不過很快我們將看到,最終我們選擇了操作整數的方案),而不是操作二進制值的方案。二進制可以工作,但它不僅要求全同態加密的靈活性(以性能為代價),還要求我們管理二進制表示和我們想要計算的數學運算之間的邏輯。一個不那么強大,為浮點運算定制的HE(HE為同態加密Homomorphic Encryption的縮寫)算法會更合適。
盡管加上了這一限制,仍有非常多的選擇。下面是一些具備我們需要的特性的流行算法:
Efficient Homomorphic Encryption on Integer Vectors and Its Applications(基于整數向量的高效同態加密及其應用)
Yet Another Somewhat Homomorphic Encryption (YASHE)(又一個某種程度上的同態加密)
Somewhat Practical Fully Homomorphic Encryption (FV)(某種程度上實用的全同態加密)
Fully Homomorphic Encryption without Bootstrapping(非自舉的全同態加密)
最佳的選擇可能是YASHE或FV。YASHE是流行的CryptoNet使用的算法,對浮點運算的支持很棒。然而,它相當復雜。為了讓這篇文章容易閱讀、便于嘗試,我們將選擇稍微不那么高級(可能也不那么安全)的Efficient Integer Vector Homomorphic Encryption(高效整數向量同態加密)。然而,我認為非常值得指出的是,在你閱讀本文的時候,更多新的HE算法正在開發之中,同時本文展示的想法通用于任何在整數或浮點數的加法和乘法上同態加密的方案。甚至說,我的愿望是引起對HE的這一應用的關注,以便更多的為深度學習優化的HE算法能被開發出來。
Yu、Lai、Paylor的論文Efficient Integer Vector Homomorphic Encryption詳細描述了這一算法,相應的實現可以在GitHub上獲?。╦amespayor/vector-homomorphic-encryption)。主要部分在C++文件vhe.cpp中。下面我們引導讀者閱讀代碼的一個python移植,說明代碼是干什么的。如果你選擇實現一個更高級的方案,這也會很有幫助,因為有一些主題相對而言是通用的(一般函數名,變量名,等等)。
六、Python中的同態加密
首先是一些同態加密術語:
明文(plaintext)未加密數據。也叫“消息”。在我們的例子中,這將是一些表示神經網絡的數字。
密文(cyphertext)加密數據。我們將在密文之上進行數學運算,這些運算會改變底層的明文。
公鑰(public key)偽隨機數字序列,讓任何人得以加密數據??梢院蛣e人分享,因為(理論上)公鑰只能用于加密。
私鑰/密鑰(private/secret key)偽隨機數字序列,讓你解密被公鑰加密的數據。你不想和別人分享私鑰。否則,別人可以解密你的消息。
對應的變量名(不同的同態加密技術都傾向于使用這些標準變量名):
S表示密鑰/私鑰的矩陣。用于解密。
M公鑰。用于加密和進行數學運算。在有些算法中,不是所有數學運算都需要公鑰。但這一算法非常廣泛地使用公鑰。
c加密數據向量,密文。
x消息,即明文。有些論文使用m作明文的變量名。
w單個“加權(weighting)”標量變量,用于重加權輸入消息x(讓它一致地更長或更短)。這一變量用于調節信噪比。加強信號后,對于給定的操作而言,消息較不容易受噪聲影響。然而,過于加強信號,會增加完全毀壞數據的概率。這是一個平衡。
E或e一般指隨機噪聲。在某些情形下,指用公鑰加密數據前添加的噪聲。一般而言,噪聲使解密更困難。噪聲使同一消息的兩次加密可以不一樣,在讓消息難以破解方面,這很重要。注意,取決于算法和實現,這可能是一個向量,也可能是一個矩陣。在其他情形下,指隨操作積累的噪聲,詳見后文。
和許多數學論文的慣用法一樣,大寫字母對應矩陣,小寫字母對應向量,斜體小寫對應標量。我們關注同態加密的四種操作:公私鑰對生成,單向加密,解密,數學運算。讓我們從解密開始。
左邊的公式描述了密鑰S和消息x的一般關系。右邊的公式顯示了如何使用密鑰解密消息。不知道你注意到沒有,右邊的公式并不包含e?;旧?,同態加密技術一般引入足夠多的噪聲使沒有密鑰的情況下難以破解出原始消息,但是引入的噪聲的量又足夠少,當你確實具有密鑰時噪聲可以通過取整忽略。右邊的公式中的框表示“取整到最接近的整數”。其他同態加密算法使用不同的取整。模數運算幾乎普遍存在。而加密則生成使上述關系為真的c. 如果S是一個隨機矩陣,那么c很難解密。一個簡單的、非對稱的生成加密鑰的方式是找到密鑰的逆矩陣。讓我們看下相應的Python代碼。
import numpy as np
def generate_key(w,m,n):
S = (np.random.rand(m,n) * w / (2 ** 16)) # 可證明 max(S) < w
return S
def encrypt(x,S,m,n,w):
assert len(x) == len(S)
e = (np.random.rand(m)) # 可證明 max(e) < w / 2
c = np.linalg.inv(S).dot((w * x) + e)
return c
def decrypt(c,S,w):
return (S.dot(c) / w).astype('int')
x = np.array([0,1,2,5])
m = len(x)
n = m
w = 16
S = generate_key(w,m,n)
你可以在Jupyter Notebook中試著運行上面的代碼,進行一些操作:
注意,我們可以對密文進行一些基本的運算,這些運算改動了相應的明文。很優雅,不是嗎?
七、優化加密
重要一課:回顧一下之前的公式。如果密鑰S是一個單位矩陣,那么c不過是輸入x的一個重加權的、略帶噪聲的版本。如果你不明白上面的話,請Google“單位矩陣教程”。限于篇幅,這里就不詳細介紹單位矩陣了。
這引導我們思考加密是如何進行的。論文作者沒有顯式地分配一對獨立的“公鑰”和“私鑰”,相反,提出了一種“鑰交換”技術,將私鑰S替換為S'。更具體地,這一私鑰交換技術涉及生成一個可以進行該變換的矩陣M。由于M具備將消息從未加密狀態(單位矩陣密鑰)轉換為加密狀態(隨機而難以猜測的密鑰),這個M矩陣正好可以用作我們的公鑰!
上面一段話包含許多信息,我們也許講得太快了。讓我們重新概括一下。
發生了什么……
基于上面兩個公式,如果密鑰是一個單位矩陣,那么消息是未加密的。
基于上面兩個公式,如果密鑰是一個隨機矩陣,那么消息是加密的。
我們構造一個矩陣M將一個密鑰轉換為另一個私鑰。
當矩陣M將單位矩陣轉換為一個隨機密鑰時,根據定義,它使用單向加密方式加密了消息。
由于M充當了“單向加密”的角色,我們稱它為“公鑰”,并且可以像公鑰一樣分發它,因為它無法用于解密。
好了,不多拖延了,讓我們看下這一切是如何通過Python實現的。
import numpy as np
def generate_key(w,m,n):
S = (np.random.rand(m,n) * w / (2 ** 16)) # 可證明 max(S) < w
return S
def encrypt(x,S,m,n,w):
assert len(x) == len(S)
e = (np.random.rand(m)) # 可證明 max(e) < w / 2
c = np.linalg.inv(S).dot((w * x) + e)
return c
def decrypt(c,S,w):
return (S.dot(c) / w).astype('int')
def get_c_star(c,m,l):
c_star = np.zeros(l * m,dtype='int')
for i in range(m):
b = np.array(list(np.binary_repr(np.abs(c[i]))),dtype='int')
if(c[i] < 0):
b *= -1
c_star[(i * l) + (l-len(b)): (i+1) * l] += b
return c_star
def switch_key(c,S,m,n,T):
l = int(np.ceil(np.log2(np.max(np.abs(c)))))
c_star = get_c_star(c,m,l)
S_star = get_S_star(S,m,n,l)
n_prime = n + 1
S_prime = np.concatenate((np.eye(m),T.T),0).T
A = (np.random.rand(n_prime - m, n*l) * 10).astype('int')
E = (1 * np.random.rand(S_star.shape[0],S_star.shape[1])).astype('int')
M = np.concatenate(((S_star - T.dot(A) + E),A),0)
c_prime = M.dot(c_star)
return c_prime,S_prime
def get_S_star(S,m,n,l):
S_star = list()
for i in range(l):
S_star.append(S*2**(l-i-1))
S_star = np.array(S_star).transpose(1,2,0).reshape(m,n*l)
return S_star
def get_T(n):
n_prime = n + 1
T = (10 * np.random.rand(n,n_prime - n)).astype('int')
return T
def encrypt_via_switch(x,w,m,n,T):
c,S = switch_key(x*w,np.eye(m),m,n,T)
return c,S
x = np.array([0,1,2,5])
m = len(x)
n = m
w = 16
S = generate_key(w,m,n)
上面的代碼的基本思路是讓S大體上是單位矩陣,然后在其之上連接一個隨機向量T。因此T具備所有密鑰所需的信息,不過我們仍然需要構建一個尺寸為S的矩陣使得一切可以工作。
八、創建一個XOR神經網絡
既然我們已經知道如何加密和解密消息(以及進行基本的加法和乘法計算),是時候嘗試擴展剩余的運算,以便構建一個簡單的XOR神經網絡。盡管從技術上說,神經網絡不過是一系列非常簡單的操作,我們還是需要一些操作的組合以實現便利的功能。下面我將描述我們需要的每項操作,以及在一個較高的抽象層次上,我們是如何實現這些操作的(基本上是我們將使用的加法和乘法的序列)。接著我會向你展示代碼。關于一些細節,請參考前面提到的論文。
浮點數我們將簡單地scale浮點數到整數。我們將在整數上訓練我們的網絡(把整數當成浮點數)。比如,假設scale=1000,0.2 * 0.5 = 0.1就是200 * 500 = 100000。還原時,100000 / (1000 * 1000) = 0.1(因為我們使用了乘法,所以需要除以1000的平方)。初看起來這很有技巧性,但你會適應的。由于我們使用的HE方案取整到最接近的整數,這也讓我們得以控制神經網絡的精度。
向量矩陣乘法這是我們的黃油面包(最基本的操作)。事實上,轉換密鑰的矩陣M是一種線性變換的方式。
內積在合適的背景下,上述線性變換可能是內積。
sigmoid由于我們可以進行向量矩陣乘法運算,基于足夠的乘法,我們可以演算任意多項式的值。因為我們已經知道了對應sigmoid的泰勒級數多項式,我們可以演算sigmoid的逼近值!
逐元素矩陣乘法這一操作驚人地低效。我們需要進行向量矩陣乘法或一系列內積運算。
外積我們可以通過掩碼和內積完成這一運算。
聲明一下,可能存在完成這些運算的更高效的方法,但我不想冒打破同態加密方案完整性的風險。所以某種程度上我是通過論文中提供的函數來反推如何完成上述運算的(除了算法容許的sigmoid擴展)?,F在,讓我們看看完成這些的Python代碼:
def sigmoid(layer_2_c):
out_rows = list()
for position in range(len(layer_2_c)-1):
M_position = M_onehot[len(layer_2_c)-2][0]
layer_2_index_c = innerProd(layer_2_c,v_onehot[len(layer_2_c)-2][position],M_position,l) / scaling_factor
x = layer_2_index_c
x2 = innerProd(x,x,M_position,l) / scaling_factor
x3 = innerProd(x,x2,M_position,l) / scaling_factor
x5 = innerProd(x3,x2,M_position,l) / scaling_factor
x7 = innerProd(x5,x2,M_position,l) / scaling_factor
xs = copy.deepcopy(v_onehot[5][0])
xs[1] = x[0]
xs[2] = x2[0]
xs[3] = x3[0]
xs[4] = x5[0]
xs[5] = x7[0]
out = mat_mul_forward(xs,H_sigmoid[0:1],scaling_factor)
out_rows.append(out)
return transpose(out_rows)[0]
def load_linear_transformation(syn0_text,scaling_factor = 1000):
syn0_text *= scaling_factor
return linearTransformClient(syn0_text.T,getSecretKey(T_keys[len(syn0_text)-1]),T_keys[len(syn0_text)-1],l)
def outer_product(x,y):
flip = False
if(len(x) < len(y)):
flip = True
tmp = x
x = y
y = tmp
y_matrix = list()
for i in range(len(x)-1):
y_matrix.append(y)
y_matrix_transpose = transpose(y_matrix)
outer_result = list()
for i in range(len(x)-1):
outer_result.append(mat_mul_forward(x * onehot[len(x)-1][i],y_matrix_transpose,scaling_factor))
if(flip):
return transpose(outer_result)
return outer_result
def mat_mul_forward(layer_1,syn1,scaling_factor):
input_dim = len(layer_1)
output_dim = len(syn1)
buff = np.zeros(max(output_dim+1,input_dim+1))
buff[0:len(layer_1)] = layer_1
layer_1_c = buff
syn1_c = list()
for i in range(len(syn1)):
buff = np.zeros(max(output_dim+1,input_dim+1))
buff[0:len(syn1[i])] = syn1[i]
syn1_c.append(buff)
layer_2 = innerProd(syn1_c[0],layer_1_c,M_onehot[len(layer_1_c) - 2][0],l) / float(scaling_factor)
for i in range(len(syn1)-1):
layer_2 += innerProd(syn1_c[i+1],layer_1_c,M_onehot[len(layer_1_c) - 2][i+1],l) / float(scaling_factor)
return layer_2[0:output_dim+1]
def elementwise_vector_mult(x,y,scaling_factor):
y =[y]
one_minus_layer_1 = transpose(y)
outer_result = list()
for i in range(len(x)-1):
outer_result.append(mat_mul_forward(x * onehot[len(x)-1][i],y,scaling_factor))
return transpose(outer_result)[0]
有一點我之前沒有告訴你。為了節省時間,我預計算了一些鑰、向量、矩陣,并對它們作了排序。這包括完全由1組成的向量,不同長度的one-hot編碼向量。這有助于上面的掩碼操作,以及其他我們希望可以做到的簡單操作。例如,sigmoid的導數是sigmoid(x) * (1 - sigmoid(x))。因此,預計算這些變量會很方便。下面是預計算步驟。
# 在安全的服務端進行
l = 100
w = 2 ** 25
aBound = 10
tBound = 10
eBound = 10
max_dim = 10
scaling_factor = 1000
# 鑰
T_keys = list()
for i in range(max_dim):
T_keys.append(np.random.rand(i+1,1))
# 單向加密變換
M_keys = list()
for i in range(max_dim):
M_keys.append(innerProdClient(T_keys[i],l))
M_onehot = list()
for h in range(max_dim):
i = h+1
buffered_eyes = list()
for row in np.eye(i+1):
buffer = np.ones(i+1)
buffer[0:i+1] = row
buffered_eyes.append((M_keys[i-1].T * buffer).T)
M_onehot.append(buffered_eyes)
c_ones = list()
for i in range(max_dim):
c_ones.append(encrypt(T_keys[i],np.ones(i+1), w, l).astype('int'))
v_onehot = list()
onehot = list()
for i in range(max_dim):
eyes = list()
eyes_txt = list()
for eye in np.eye(i+1):
eyes_txt.append(eye)
eyes.append(one_way_encrypt_vector(eye,scaling_factor))
v_onehot.append(eyes)
onehot.append(eyes_txt)
H_sigmoid_txt = np.zeros((5,5))
H_sigmoid_txt[0][0] = 0.5
H_sigmoid_txt[0][1] = 0.25
H_sigmoid_txt[0][2] = -1/48.0
H_sigmoid_txt[0][3] = 1/480.0
H_sigmoid_txt[0][4] = -17/80640.0
H_sigmoid = list()
for row in H_sigmoid_txt:
H_sigmoid.append(one_way_encrypt_vector(row))
如果你仔細查看了上面的代碼,你會注意到H_sigmoid矩陣是我們需要的用于演算sigmoid多項式的矩陣。最后,我們使用如下代碼訓練我們的神經網絡。如果不明白神經網絡的部分,你可以溫習下基于Numpy實現神經網絡:反向傳播一文。我基本上使用了文中的XOR網絡,使用適當的工具函數替換了其中一些操作,以加密權重。
np.random.seed(1234)
input_dataset = [[],[0],[1],[0,1]]
output_dataset = [[0],[1],[1],[0]]
input_dim = 3
hidden_dim = 4
output_dim = 1
alpha = 0.015
# 使用公鑰單向加密訓練數據(可就地進行)
y = list()
for i in range(4):
y.append(one_way_encrypt_vector(output_dataset[i],scaling_factor))
# 生成權重
syn0_t = (np.random.randn(input_dim,hidden_dim) * 0.2) - 0.1
syn1_t = (np.random.randn(output_dim,hidden_dim) * 0.2) - 0.1
# 單向加密權重
syn1 = list()
for row insyn1_t:
syn1.append(one_way_encrypt_vector(row,scaling_factor).astype('int64'))
syn0 = list()
for row insyn0_t:
syn0.append(one_way_encrypt_vector(row,scaling_factor).astype('int64'))
# 開始訓練
for iter in range(1000):
decrypted_error = 0
encrypted_error = 0
for row_i in range(4):
if(row_i == 0):
layer_1 = sigmoid(syn0[0])
elif(row_i == 1):
layer_1 = sigmoid((syn0[0] + syn0[1])/2.0)
elif(row_i == 2):
layer_1 = sigmoid((syn0[0] + syn0[2])/2.0)
else:
layer_1 = sigmoid((syn0[0] + syn0[1] + syn0[2])/3.0)
layer_2 = (innerProd(syn1[0],layer_1,M_onehot[len(layer_1) - 2][0],l) / float(scaling_factor))[0:2]
layer_2_delta = add_vectors(layer_2,-y[row_i])
syn1_trans = transpose(syn1)
one_minus_layer_1 = [(scaling_factor * c_ones[len(layer_1) - 2]) - layer_1]
sigmoid_delta = elementwise_vector_mult(layer_1,one_minus_layer_1[0],scaling_factor)
layer_1_delta_nosig = mat_mul_forward(layer_2_delta,syn1_trans,1).astype('int64')
layer_1_delta = elementwise_vector_mult(layer_1_delta_nosig,sigmoid_delta,scaling_factor) * alpha
syn1_delta = np.array(outer_product(layer_2_delta,layer_1)).astype('int64')
syn1[0] -= np.array(syn1_delta[0]* alpha).astype('int64')
syn0[0] -= (layer_1_delta).astype('int64')
if(row_i == 1):
syn0[1] -= (layer_1_delta).astype('int64')
elif(row_i == 2):
syn0[2] -= (layer_1_delta).astype('int64')
elif(row_i == 3):
syn0[1] -= (layer_1_delta).astype('int64')
syn0[2] -= (layer_1_delta).astype('int64')
# 如果有安全性要求,可以將加密的損失發送到別處解密。
encrypted_error += int(np.sum(np.abs(layer_2_delta)) / scaling_factor)
decrypted_error += np.sum(np.abs(s_decrypt(layer_2_delta).astype('float')/scaling_factor))
sys.stdout.write("\r 迭代" + str(iter) + " 加密損失:" + str(encrypted_error) + " 解密損失:" + str(decrypted_error) + " Alpha:" + str(alpha))
# 讓日志好看一點
if(iter % 10 == 0):
print()
# 加密誤差達到一定水平后停止訓練
if(encrypted_error < 25000000):
break
print("\n最終預測:")
for row_i in range(4):
if(row_i == 0):
layer_1 = sigmoid(syn0[0])
elif(row_i == 1):
layer_1 = sigmoid((syn0[0] + syn0[1])/2.0)
elif(row_i == 2):
layer_1 = sigmoid((syn0[0] + syn0[2])/2.0)
else:
layer_1 = sigmoid((syn0[0] + syn0[1] + syn0[2])/3.0)
layer_2 = (innerProd(syn1[0],layer_1,M_onehot[len(layer_1) - 2][0],l) / float(scaling_factor))[0:2]
print("真預測:" + str(output_dataset[row_i]) + " 加密預測:" + str(layer_2) + " 解密預測:" + str(s_decrypt(layer_2) / scaling_factor))
迭代0 加密損失:84890656 解密損失:2.529Alpha:0.015
迭代10 加密損失:69494197 解密損失:2.071Alpha:0.015
迭代20 加密損失:64017850 解密損失:1.907Alpha:0.015
迭代30 加密損失:62367015 解密損失:1.858Alpha:0.015
迭代40 加密損失:61874493 解密損失:1.843Alpha:0.015
迭代50 加密損失:61399244 解密損失:1.829Alpha:0.015
迭代60 加密損失:60788581 解密損失:1.811Alpha:0.015
迭代70 加密損失:60327357 解密損失:1.797Alpha:0.015
迭代80 加密損失:59939426 解密損失:1.786Alpha:0.015
迭代90 加密損失:59628769 解密損失:1.778Alpha:0.015
迭代100 加密損失:59373621 解密損失:1.769Alpha:0.015
迭代110 加密損失:59148014 解密損失:1.763Alpha:0.015
迭代120 加密損失:58934571 解密損失:1.757Alpha:0.015
迭代130 加密損失:58724873 解密損失:1.75Alpha:0.0155
迭代140 加密損失:58516008 解密損失:1.744Alpha:0.015
迭代150 加密損失:58307663 解密損失:1.739Alpha:0.015
迭代160 加密損失:58102049 解密損失:1.732Alpha:0.015
迭代170 加密損失:57863091 解密損失:1.725Alpha:0.015
迭代180 加密損失:55470158 解密損失:1.653Alpha:0.015
迭代190 加密損失:54650383 解密損失:1.629Alpha:0.015
迭代200 加密損失:53838756 解密損失:1.605Alpha:0.015
迭代210 加密損失:51684722 解密損失:1.541Alpha:0.015
迭代220 加密損失:54408709 解密損失:1.621Alpha:0.015
迭代230 加密損失:54946198 解密損失:1.638Alpha:0.015
迭代240 加密損失:54668472 解密損失:1.63Alpha:0.0155
迭代250 加密損失:55444008 解密損失:1.653Alpha:0.015
迭代260 加密損失:54094286 解密損失:1.612Alpha:0.015
迭代270 加密損失:51251831 解密損失:1.528Alpha:0.015
迭代276 加密損失:24543890 解密損失:0.732Alpha:0.015
最終預測:
真實預測:[0] 加密預測:[-3761423723.07182550.0] 解密預測:[-0.112]
真實預測:[1] 加密預測:[24204806753.1662670.0] 解密預測:[ 0.721]
真實預測:[1] 加密預測:[23090462896.170280.0] 解密預測:[ 0.688]
真實預測:[0] 加密預測:[1748380342.45533540.0] 解密預測:[ 0.052]
以上是我訓練神經網絡時看到的輸出。加密噪聲的某種組合和低精度導致某種程度上笨重的學習,因此調優具有一定的技巧性。訓練也相當慢。這些很大程度上是因為轉置運算非常昂貴。我比較確定本可以通過更簡單的操作完成轉置運算,但是,如前所述,像這樣證明概念可行的代碼,我更偏向安全性。
小小的總結
網絡的權重都是加密的。
數據解密為1和0.
經過訓練后,可以解密網絡,以提高性能或進行進一步的訓練(或者轉用不同的加密鑰)。
訓練損失和輸出預測同樣是加密過的值。我們需要解碼之后才能解讀網絡表現。
九、情感分類
下面是一個真實一些的例子,我們在IMDB評論情感數據上訓練同態加密的網絡,網絡基于Udacity的深度學習課程。完整代碼發布在GitHub上。
import time
import sys
import numpy as np
# 調整之前的網絡以建模這些現象
classSentimentNetwork:
def __init__(self, reviews,labels,min_count = 10,polarity_cutoff = 0.1,hidden_nodes = 8, learning_rate = 0.1):
np.random.seed(1234)
self.pre_process_data(reviews, polarity_cutoff, min_count)
self.init_network(len(self.review_vocab),hidden_nodes, 1, learning_rate)
def pre_process_data(self,reviews, polarity_cutoff,min_count):
print("Pre-processing data...")
positive_counts = Counter()
negative_counts = Counter()
total_counts = Counter()
for i in range(len(reviews)):
if(labels[i] == 'POSITIVE'):
for word in reviews[i].split(" "):
positive_counts[word] += 1
total_counts[word] += 1
else:
for word in reviews[i].split(" "):
negative_counts[word] += 1
total_counts[word] += 1
pos_neg_ratios = Counter()
for term,cnt in list(total_counts.most_common()):
if(cnt >= 50):
pos_neg_ratio = positive_counts[term] / float(negative_counts[term]+1)
pos_neg_ratios[term] = pos_neg_ratio
for word,ratio in pos_neg_ratios.most_common():
if(ratio > 1):
pos_neg_ratios[word] = np.log(ratio)
else:
pos_neg_ratios[word] = -np.log((1 / (ratio + 0.01)))
review_vocab = set()
for review in reviews:
for word in review.split(" "):
if(total_counts[word] > min_count):
if(word in pos_neg_ratios.keys()):
if((pos_neg_ratios[word] >= polarity_cutoff) or (pos_neg_ratios[word] <= -polarity_cutoff)):
review_vocab.add(word)
else:
review_vocab.add(word)
self.review_vocab = list(review_vocab)
label_vocab = set()
for label in labels:
label_vocab.add(label)
self.label_vocab = list(label_vocab)
self.review_vocab_size = len(self.review_vocab)
self.label_vocab_size = len(self.label_vocab)
self.word2index = {}
for i, word in enumerate(self.review_vocab):
self.word2index[word] = i
self.label2index = {}
for i, label in enumerate(self.label_vocab):
self.label2index[label] = i
def init_network(self, input_nodes, hidden_nodes, output_nodes, learning_rate):
# 設置輸入層、隱藏層、輸出層節點數
self.input_nodes = input_nodes
self.hidden_nodes = hidden_nodes
self.output_nodes = output_nodes
print("Initializing Weights...")
self.weights_0_1_t = np.zeros((self.input_nodes,self.hidden_nodes))
self.weights_1_2_t = np.random.normal(0.0, self.output_nodes**-0.5,
(self.hidden_nodes, self.output_nodes))
print("Encrypting Weights...")
self.weights_0_1 = list()
for i,row in enumerate(self.weights_0_1_t):
sys.stdout.write("\rEncrypting Weights from Layer 0 to Layer 1:" + str(float((i+1) * 100) / len(self.weights_0_1_t))[0:4] + "% done")
self.weights_0_1.append(one_way_encrypt_vector(row,scaling_factor).astype('int64'))
print("")
self.weights_1_2 = list()
for i,row in enumerate(self.weights_1_2_t):
sys.stdout.write("\rEncrypting Weights from Layer 1 to Layer 2:" + str(float((i+1) * 100) / len(self.weights_1_2_t))[0:4] + "% done")
self.weights_1_2.append(one_way_encrypt_vector(row,scaling_factor).astype('int64'))
self.weights_1_2 = transpose(self.weights_1_2)
self.learning_rate = learning_rate
self.layer_0 = np.zeros((1,input_nodes))
self.layer_1 = np.zeros((1,hidden_nodes))
def sigmoid(self,x):
return1 / (1 + np.exp(-x))
def sigmoid_output_2_derivative(self,output):
return output * (1 - output)
def update_input_layer(self,review):
# 清除之前的狀態,重置層至全0
self.layer_0 *= 0
for word in review.split(" "):
self.layer_0[0][self.word2index[word]] = 1
def get_target_for_label(self,label):
if(label == 'POSITIVE'):
return1
else:
return0
def train(self, training_reviews_raw, training_labels):
training_reviews = list()
for review in training_reviews_raw:
indices = set()
for word in review.split(" "):
if(word in self.word2index.keys()):
indices.add(self.word2index[word])
training_reviews.append(list(indices))
layer_1 = np.zeros_like(self.weights_0_1[0])
start = time.time()
correct_so_far = 0
total_pred = 0.5
for i in range(len(training_reviews_raw)):
review_indices = training_reviews[i]
label = training_labels[i]
layer_1 *= 0
for index in review_indices:
layer_1 += self.weights_0_1[index]
layer_1 = layer_1 / float(len(review_indices))
layer_1 = layer_1.astype('int64') # 取整至最接近的整數
layer_2 = sigmoid(innerProd(layer_1,self.weights_1_2[0],M_onehot[len(layer_1) - 2][1],l) / float(scaling_factor))[0:2]
if(label == 'POSITIVE'):
layer_2_delta = layer_2 - (c_ones[len(layer_2) - 2] * scaling_factor)
else:
layer_2_delta = layer_2
weights_1_2_trans = transpose(self.weights_1_2)
layer_1_delta = mat_mul_forward(layer_2_delta,weights_1_2_trans,scaling_factor).astype('int64')
self.weights_1_2 -= np.array(outer_product(layer_2_delta,layer_1)) * self.learning_rate
for index in review_indices:
self.weights_0_1[index] -= (layer_1_delta * self.learning_rate).astype('int64')
# 我們將即時解密,以便查看發生了什么
total_pred += (s_decrypt(layer_2)[0] / scaling_factor)
if((s_decrypt(layer_2)[0] / scaling_factor) >= (total_pred / float(i+2)) and label == 'POSITIVE'):
correct_so_far += 1
if((s_decrypt(layer_2)[0] / scaling_factor) < (total_pred / float(i+2)) and label == 'NEGATIVE'):
correct_so_far += 1
reviews_per_second = i / float(time.time() - start)
sys.stdout.write("\rProgress:" + str(100 * i/float(len(training_reviews_raw)))[:4] + "% Speed(reviews/sec):" + str(reviews_per_second)[0:5] + " #Correct:" + str(correct_so_far) + " #Trained:" + str(i+1) + " Training Accuracy:" + str(correct_so_far * 100 / float(i+1))[:4] + "%")
if(i % 100 == 0):
print(i)
def test(self, testing_reviews, testing_labels):
correct = 0
start = time.time()
for i in range(len(testing_reviews)):
pred = self.run(testing_reviews[i])
if(pred == testing_labels[i]):
correct += 1
reviews_per_second = i / float(time.time() - start)
sys.stdout.write("\rProgress:" + str(100 * i/float(len(testing_reviews)))[:4] \
+ "% Speed(reviews/sec):" + str(reviews_per_second)[0:5] \
+ "% #Correct:" + str(correct) + " #Tested:" + str(i+1) + " Testing Accuracy:" + str(correct * 100 / float(i+1))[:4] + "%")
def run(self, review):
# 輸入層
# 隱藏層
self.layer_1 *= 0
unique_indices = set()
for word in review.lower().split(" "):
if word in self.word2index.keys():
unique_indices.add(self.word2index[word])
for index in unique_indices:
self.layer_1 += self.weights_0_1[index]
# 輸出層
layer_2 = self.sigmoid(self.layer_1.dot(self.weights_1_2))
if(layer_2[0] >= 0.5):
return"POSITIVE"
else:
return"NEGATIVE"
Progress:0.0% Speed(reviews/sec):0.0#Correct:1 #Trained:1 Training Accuracy:100.%0
Progress:0.41% Speed(reviews/sec):1.978#Correct:66 #Trained:101 Training Accuracy:65.3%100
Progress:0.83% Speed(reviews/sec):2.014#Correct:131 #Trained:201 Training Accuracy:65.1%200
Progress:1.25% Speed(reviews/sec):2.011#Correct:203 #Trained:301 Training Accuracy:67.4%300
Progress:1.66% Speed(reviews/sec):2.003#Correct:276 #Trained:401 Training Accuracy:68.8%400
Progress:2.08% Speed(reviews/sec):2.007#Correct:348 #Trained:501 Training Accuracy:69.4%500
Progress:2.5% Speed(reviews/sec):2.015#Correct:420 #Trained:601 Training Accuracy:69.8%600
Progress:2.91% Speed(reviews/sec):1.974#Correct:497 #Trained:701 Training Accuracy:70.8%700
Progress:3.33% Speed(reviews/sec):1.973#Correct:581 #Trained:801 Training Accuracy:72.5%800
Progress:3.75% Speed(reviews/sec):1.976#Correct:666 #Trained:901 Training Accuracy:73.9%900
Progress:4.16% Speed(reviews/sec):1.983#Correct:751 #Trained:1001 Training Accuracy:75.0%1000
Progress:4.33% Speed(reviews/sec):1.940#Correct:788 #Trained:1042 Training Accuracy:75.6%
....
十、相比數據加密的優勢
和這一做法最相似的是加密訓練數據,然后在加密數據上訓練神經網絡(接受加密輸入并預測加密輸出)。這是一個出色的想法。然而,其實它有一些缺陷。首先也是最重要的,加密數據意味著對任何不具有加密數據的私鑰的人而言,該神經網絡完全無用。這樣就不可能在不同的私有數據源上訓練同一深度學習模型了。大部分商業應用有這樣的需求,需要匯總消費者的數據。理論上,我們本來想要讓每個消費者用他們自己的密鑰保護自己的數據,然而同態加密數據要求所有人使用相同的鑰。
而加密網絡則沒有這個限制。
基于上述方法,你可以訓練一個平常的、解密的神經網絡一段時間,加密它,將它和相應的公鑰發給A方(A方可以基于其所有的數據訓練網絡一段時間……A方保留數據)。接著,你可以收回這個網絡,解密它,用另一個鑰加密網絡,然后發給B方,B方在其所有的數據上進行一些訓練。由于網絡自身被加密了,你可以完全控制全過程中你刻畫的智能。A方和B方將無法知道他們各自收到的是同一個網絡,也無法知道之前見過這個網絡,或在自己的數據上用過這個網絡。你的公司保留對神經網絡中的知識產權的控制,而每個用戶保留對他們自己的數據的控制。
十一、以后的工作
存在更快、更安全的同態加密算法。我相信將本項工作移植到YASHE會是一個正確的方向。由于一些系統復雜性,也許開發一個能讓用戶更簡單地進行加密的框架會是一個好主意。一般而言,為了達到生產環境要求的質量,HE需要變得更快。然而,這方面的進展十分迅速。我確信我們會在不久的將來達到這一點。
十二、潛在應用
分布式AI商業公司可以分布式地部署它們的模型(用于訓練或使用),而無需冒智能被竊的風險。
保護消費者隱私之前的應用提供了這樣的可能性:消費者保留他們的數據,選擇不同的模型在自己的設備上訓練,不用將數據發送到別處。如果商業公司的智能在分布式場景中沒有被竊的風險,那么他們不尊重消費者隱私的借口就會少很多。數據就是力量,它需要回歸到人們手中。
受控超智能網絡可以充分發展其智能,不過除非它具有密鑰,否則它只能預測亂碼。
-
深度學習
+關注
關注
73文章
5260瀏覽量
120117 -
同態加密
+關注
關注
1文章
5瀏覽量
1907
原文標題:基于Numpy實現同態加密神經網絡
文章出處:【微信號:jqr_AI,微信公眾號:論智】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論