歡迎大家前往騰訊雲+社群,獲取更多騰訊海量技術實踐乾貨哦~
長文預警: 在本文中,我們將訓練一個在訓練期間完全加密的神經網路(在未加密的資料上進行訓練)。這將為神經網路帶來兩個有益的特性:首先,神經網路的智慧可以受到更好的保護以免被他人盜取,消除了在不安全環境下訓練的有價值的AI被其他智慧盜取的風險;其次,網路可以只作加密的預測(這意味著在沒有金鑰的情況下,外界無法理解接受預測,從而網路對外界不會產生影響)。這在使用者和超人工智慧間建立了一個非常有價值的不對等權力。試想,如果AI是經過同態加密處理的,那麼從AI的角度來看,整個外部世界也是同態加密過的。而人類可以控制金鑰從而決定是解鎖AI自身(在外部世界釋出)還是隻解碼AI作出的決策(似乎顯得更安全)。
注: 如果你對訓練加密的神經網路感興趣,可以參考OpenMined的PySyft庫
超級智慧
很多人擔心超人工智慧總有一天會選擇傷害人類。之前,史蒂芬·霍金曾呼籲建立一個新的世界“政府”來管理我們給予人工智慧的能力以免其毀滅人類。這個陳述是很大膽的,我認為它反映了科學界以至全世界共同關心的問題。在本文中,我將用一些玩具性質的程式碼來演示針對這個問題的潛在技術解決方案。
我們的目標很樸實。我們像發明一個可以讓AI變得極度聰明(聰明到可以治癒癌症,解決全球飢餓問題)的技術,不過前提是這樣的智慧是在人類的控制下的,即它的應用是受限的。無止境地學習固然是好的,但是知識的無限制應用卻有著潛在的危險。
為了闡述這一觀點,我將首先快速講解兩個振奮人心的研究領域:深度學習和同態加密。
第1部分:什麼是深度學習?
深度學習可以理解為人工智慧的一套自動化工具,主要藉助與神經網路。作為電腦科學的領域之一,深度學習在很多智慧任務的完成質量上超越了之前的技術,這促使了AI技術的繁榮。在這一背景下,在擊敗世界圍棋冠軍的DeepMind的AlphaGo中深度學習也扮演了重要角色。
問題: 神經網路是怎樣學習的呢?
神經網路基於輸入進行預測。它通過不斷地試驗來高效地做到這一點。這個過程從一個預測開始(起初很大情況下是隨機的),之後通過接受到的“誤差訊號”來判斷它的預測是高了還是低了(通常預測的輸出為概率值)。在經過很多次嘗試之後,網路開始具有了辨識能力。關於神經網路工作的具體細節,可以參閱A Neural Network in 11 Lines of Python。
這裡的重點是上面提到的誤差訊號。如果說網路不知道自己的預測的水平如何,它就無法進行學習,,要牢記這一點。
第2部分:什麼是同態加密?
顧名思義,同態加密是一種加密形式。在不對稱的情況下,可以用“公鑰”將明文轉化為亂碼。關鍵的一點是,你可以用對應的“私鑰”將加密後的文字再次解碼為原始的明文。但是隻有在你有“私鑰”的情況下你才可以解碼混淆後的明文(理論上)。
同態加密只是一種特殊的加密方式。它支援某些使用者在不具有原有資訊讀許可權的情況下對加密資訊進行修改操作。舉個例子來說,加密的數字資訊可以在不解碼的情況下進行乘法和加法操作。下面給出一個簡單的例子:
現在同態加密的方案越來越多,每個方案都有著不同的特性。不過這還是一個成長中的領域,還有很多關鍵的問題仍待解決,我們後面會再回到這個問題上討論。
現在我們先延續上面的內容進行講解,從上圖可以看出我們可以在經過同態加密的資料上進行乘法和加法操作。此外由於公鑰可以進行單向加密,這使得我們可以用加密的數字和未加密的數字進行相應操作(對未加密的資料進行單向加密),正如上圖中的 2 * Cypher 一樣。(一些加密演算法甚至不需要這樣做,我們後面再談)
第三部分:我們可以把兩者結合使用嗎?
深度學習和同態加密最常見的結合場景主要體現再資料隱私方面。事實證明,當資料經過同態加密之後,雖然不能夠讀取其中的資訊,但是你仍然可以保持資料中大部分感興趣的統計學結構。這使得人們可以在加密資料上訓練模型(CryptoNets)。此外,初創對衝基金會Numer.ai加密了昂貴,專有的資料來提供給任何人訓練機器學習模型來預測股票市場。通常來說,他們是不能這樣做的,因為這構成了珍貴/私密資料的洩漏(而常規的加密手段又會使模型訓練變得不可能)。
不過,本文要做的是一個反向的過程,即對神經網路進行加密並在解碼的資料上進行訓練。
一個大型神經網路從整體上看複雜度是驚人的,但是將它們分解之後也只是一些簡單操作的重複而已。事實上,許多先進的神經網路通常只需要以下操作來建立:
既然這樣,我們在技術上是否可以加密同態神經網路本身嗎?事實證明我們可以通過一些近似做到這一點。
- 加法 - 開箱即用
- 乘法 - 開箱即用
- 除法 - 開箱即用? - 乘 (1 / 被乘數) 即可
- 減法 - 開箱即用? - 加一個負數即可
- Sigmoid - 嗯...也許有點難
- Tanh - 嗯...也許有點難
- 指數 - 嗯...也許有點難
看起來我們可以很輕鬆地完成除法和減法,但是完成一些複雜函式則比完成加法和乘法操作困難的多。為了對深度神經網路進行同態加密,我們需要一些“祕密配方”。
第四部分:泰勒級數展開
可能你還記得高中或者大學裡這部分的內容:泰勒級數允許我們使用無限項的加減乘除操作的組合來計算非線性函式。這個方法完美地解決了我們的問題!(除了需要無限項這一點)。幸運的是,如果你只能計算泰勒展開式的前幾項,你也可以得到一個近似的函式。下賣弄給出了一些常用函式的泰勒級數(來源):
其中有指數函式!可以看到展開式只是一些加減乘除操作,這是我們可以完成的。同樣地,我們可以像下圖所示一樣用Python實現我們需要的sigmoid函式的泰勒展開式(其展開式可以在Wolfram Alpha查閱)。我們可以只取前面的部分項來觀察近似結果和實際結果的差距。
import numpy as np
def sigmoid_exact(x):
return 1 / (1 + np.exp(-x))
# using taylor series
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("\nInput:" + str(lil_number))
print("Exact Sigmoid:" + str(sigmoid_exact(lil_number)))
print("Approx Sigmoid:" + str(sigmoid_approximation(lil_number)))
複製程式碼
Input:0.1
Exact Sigmoid:0.524979187479
Approx Sigmoid:0.5249791875
Input:0.2
Exact Sigmoid:0.549833997312
Approx Sigmoid:0.549834
Input:0.3
Exact Sigmoid:0.574442516812
Approx Sigmoid:0.5744425625
Input:0.4
Exact Sigmoid:0.598687660112
Approx Sigmoid:0.598688
Input:0.5
Exact Sigmoid:0.622459331202
Approx Sigmoid:0.6224609375
Input:0.6
Exact Sigmoid:0.645656306226
Approx Sigmoid:0.645662
Input:0.7
Exact Sigmoid:0.668187772168
Approx Sigmoid:0.6682043125
Input:0.8
Exact Sigmoid:0.689974481128
Approx Sigmoid:0.690016
Input:0.9
Exact Sigmoid:0.710949502625
Approx Sigmoid:0.7110426875
Input:1.0
Exact Sigmoid:0.73105857863
Approx Sigmoid:0.73125
複製程式碼
從結果可以看到,我們只取前四項時的計算結果就已經很接近無窮項的計算結果了。完成了複雜函式的問題之後,下面我們來選擇一個同態加密演算法。
第5部分:選擇加密演算法
同態加密是一個相對較新的領域,其里程碑事件是2009年克雷格·金特里(Craig Gentry)發明了第一個完全同態加密演算法。這個發現為後來的研究者提供了立足點。這一領域大部分振奮人心的研究是圍繞實現同態加密的圖靈完全的計算機展開的。相應地,人們需要尋求一個有效且安全的同態加密演算法方案,可以根據任意的計算輸入去完成相應的邏輯閘操作。人們普遍的希望是可以將工作安全地移植到雲上並且不用擔心傳送的資料被髮送者以外的人監聽。這是一個非常酷的想法,也取得了很大進展。
但是還有一個缺陷,就是大多數完全同態加密演算法在普通計算機上通常是非常慢的(不實用)。這引發了另一個有趣的研究方向,即只做部分同態加密的運算從而減小運算量。這種方案降低了靈活程度但提升了運算速度,是計算中常用的折衷手段。
我們從這一點出發尋找加密演算法。理論上我們需要的是一個在浮點運算的同態加密方案(但我們本文更多以整數為例),為什麼不是二進位制呢?二進位制加密固然可行,但是它不僅要求完全同態加密方案的靈活性(效能損失),還需要我們去管理二進位制表示和數學操作中的邏輯操作。相比之下,沒有那麼強大的針對浮點數定製的HE演算法更為合適。
儘管我們確定了這個限制條件,但是還有很多的演算法以供選擇。這裡有一些具有我們喜歡的特性的流行演算法:
- 基於整數向量的高效同態加密及其應用
- Yet Another Somewhat Homomorphic Encryption (YASHE)
- Somewhat Practical Fully Homomorphic Encryption (FV)
- Fully Homomorphic Encryption without Bootstrapping
以上方案中的最優方案應當是YASHE或FV。YASHE被應用與流行的CryptoNets(加密網路)演算法當中,對浮點操作的支援很好。但是這個方法很複雜,為了使本文更加簡單有趣,我們選擇了稍遜一籌的方案(基於整數向量的高效同態加密,安全性要差一點)。但是,你要注意即使你讀本文時出現了新的同態加密演算法,但是本文中的整數/浮點數加法和乘法同態加密的實現方案是通用的。我希望通過本文可以提高你對同態加密演算法應用的理解以便開發出更多的同態加密演算法去優化深度學習。
這個加密演算法Yu, Lai, and Paylor的工作裡也有一節專門提及,還有相應的程式碼實現,核心程式碼位於vhe.cpp
當中。下面我們將用Python實現相應的介面並做相應講解。你也可以選擇更高層面的實現方式或者移植到其他語言或場景下,因為這套實現方案是通用的(通用的函式名稱,變數名等等)。
第6部分:Python中的同態加密
首先介紹一下同態加密中的術語:
- 明文: 未加密的資料。這也被稱為“資訊”(message)。在我們的例子中,這將是代表我們神經網路的數值的集合。
- 密文: 加密後的資料。我們在密文做的數學操作會改變底層的明文,最初的圖示也提到了這一點。
- 公鑰: 偽隨機數序列,任何人可以用它加密資料。公鑰可以被傳播,因為拿到的人只能用它來加密資料(理論上)。
- 私鑰/金鑰: 偽隨機數序列,允許您解密通過公鑰加密的資料。私鑰不應該分享給他人,否則你加密的資料就會被他人解碼。
以上是使用中可以移動的元件。在不同的同態加密演算法中,會有一些標準變數,本文中有以下標準變數:
- S: 代表金鑰/私鑰的矩陣,你需要用它來解密資訊。
- M: 公鑰。你會用它來加密東西,並執行數學運算。雖然一些演算法中所有的數學運算都不需要公鑰,但是公鑰的使用還是相當廣泛。
- c: 加密資料向量,“密文”。
- x: 對應於你的資訊,或者說你的“明文”。有些論文使用變數“m”代替。
- w : 用於給我們的輸入資訊x重新分配權重的常量值。(使其總是更大或更小一點)。我們使用這個變數來幫助調整訊雜比。使訊號“變大”使得它在任何給定的操作中不易受到噪聲的影響。但是,如果資料太大,則會增加我們完全損壞資料的可能性。這是一個折衷後的值。
- E/e:通常指隨機噪聲。在某些情況下,公鑰加密的資料在加密之前被混入了噪聲。混入的噪聲使解密變得困難。但這也使的相同的明文在使用相同的公鑰時可以產生不同的密文,這增加了密文被破解的難度。請注意,根據演算法和實現,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)) # proving max(S) < w
return S
def encrypt(x,S,m,n,w):
assert len(x) == len(S)
e = (np.random.rand(m)) # proving 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 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
x = np.array([0,1,2,5])
m = len(x)
n = m
w = 16
S = generate_key(w,m,n)
複製程式碼
我在iPython notebook中執行這個程式碼後執行了以下操作(圖示有相應的輸出)。
關鍵的是下面兩個操作的結果,可以看到我們對明文做的基本算術操作改變了密文底層代表的明文。很優雅的操作,不是嗎?
第7部分:優化加密過程
引入: 我們再次審視一下解密的公式,如果金鑰S為單位矩陣,那麼密文c就只是重新加權後的明文混入一定的噪聲而已,只要具有一定量的樣本就可以發現。如果這段文字你不理解的話,你需要搜尋一下“單位矩陣”相關的知識再回來閱讀,否則下面的內容理解起來會很吃力。
上面的例子告訴了我們加密發生的過程。這項技術的發明者並沒有同時確定一對獨立的“公鑰”和”私鑰“,而是提出了“金鑰轉換”技術,藉助這一技術可以將私鑰S轉換為S'。更具體地說,這種金鑰轉換技術生成了可以將未加密的資料(單位矩陣作為金鑰)轉換為加密後的資料(隨機生成的,難以被猜到的金鑰)的矩陣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)) # proving max(S) < w
return S
def encrypt(x,S,m,n,w):
assert len(x) == len(S)
e = (np.random.rand(m)) # proving 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行列相同的矩陣來完成這個工作。
第8部分:建立異或神經網路
現在我們已經知道了如何對資訊進行加密和解密(並實現了基本的加法和乘法),是時候擴充套件剩餘的操作來實現一個簡單的XOR神經網路了。雖然從技術上來說神經網路只是一系列簡單操作的集合,但為了方便我們需要將一些常用的組合操作封裝為函式。下面我會描述我們需要的操作以及我們實現這些操作使用的一些高階技巧,然後會列出相關的程式碼實現。如果想了解更多細節可以參閱Angel Yu, Wai Lok Lai, James Payor的研究。
- 浮點數: 我們將通過簡單地將浮點數放大為整數來實現。這樣即時輸入為浮點數我們也可以作為整數處理。比方說,我們選擇的放大倍數為1000。放大前0.2 0.5 = 0.1。放大後為200 500 = 100000.注意我們進行兩次乘法運算之後必須縮放2次,即100000 /(1000 * 1000)= 0.1。這是一個你需要習慣的技巧。由於同態加密方案最後會舍入到最近的整數,所以放大也可以幫助你控制網路的精度。
- 向量矩陣乘法: 這是我們要完成的基本功能。事實上,將一個金鑰轉換為另一個金鑰的M矩陣的過程實際上就是一種線性變換。
- 點積: 在正確的上下文環境中,上面的線性變換也可以寫作點積。
- Sigmoid: 由於我們可以做向量矩陣乘法,我們可以藉助足夠的乘式來評估擬合任意多項式。由於我們知道sigmoid的泰勒級數多項式,我們可以擬合一個近似的sigmoid函式!
- 矩陣元乘法: 這個操作的效率極低。我們必須做一個向量矩陣乘法或一系列的點積。
- 外積: 我們可以通過矩陣的裁剪和外積來實現。
首先宣告這裡可能還有其它更有效的方法來實現以上操作,但是為了不破壞當前的同態加密方案我只好使用了論文中提供的函式(實現sigmoid可用的擴充套件)。現在讓我們看看如何在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))。因此預先計算這些值是很有幫助的。下面給出相應程式碼:
# HAPPENS ON SECURE SERVER
l = 100
w = 2 ** 25
aBound = 10
tBound = 10
eBound = 10
max_dim = 10
scaling_factor = 1000
# keys
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函式時需要用到的矩陣。:)最後,我們用下面的程式碼來訓練我們的神經網路。如果你對神經網路的概念還不清楚,可以自行搜尋或者查閱作者的博文A Neural Network in 11 Lines of Python,這裡的異或神經網路基本上是參考這篇文章的,只是根據上面的加密操作函式做了相應替換。
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 in syn1_t:
syn1.append(one_way_encrypt_vector(row,scaling_factor).astype('int64'))
syn0 = list()
for row in syn0_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 Iter:" + str(iter) + " Encrypted Loss:" + str(encrypted_error) + " Decrypted Loss:" + str(decrypted_error) + " Alpha:" + str(alpha))
# 使輸出更美觀
if(iter % 10 == 0):
print()
# 加密誤差達到指定值後停止訓練
if(encrypted_error < 25000000):
break
print("\nFinal Prediction:")
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("True Pred:" + str(output_dataset[row_i]) + " Encrypted Prediction:" + str(layer_2) + " Decrypted Prediction:" + str(s_decrypt(layer_2) / scaling_factor))
複製程式碼
Iter:0 Encrypted Loss:84890656 Decrypted Loss:2.529 Alpha:0.015
Iter:10 Encrypted Loss:69494197 Decrypted Loss:2.071 Alpha:0.015
Iter:20 Encrypted Loss:64017850 Decrypted Loss:1.907 Alpha:0.015
Iter:30 Encrypted Loss:62367015 Decrypted Loss:1.858 Alpha:0.015
Iter:40 Encrypted Loss:61874493 Decrypted Loss:1.843 Alpha:0.015
Iter:50 Encrypted Loss:61399244 Decrypted Loss:1.829 Alpha:0.015
Iter:60 Encrypted Loss:60788581 Decrypted Loss:1.811 Alpha:0.015
Iter:70 Encrypted Loss:60327357 Decrypted Loss:1.797 Alpha:0.015
Iter:80 Encrypted Loss:59939426 Decrypted Loss:1.786 Alpha:0.015
Iter:90 Encrypted Loss:59628769 Decrypted Loss:1.778 Alpha:0.015
Iter:100 Encrypted Loss:59373621 Decrypted Loss:1.769 Alpha:0.015
Iter:110 Encrypted Loss:59148014 Decrypted Loss:1.763 Alpha:0.015
Iter:120 Encrypted Loss:58934571 Decrypted Loss:1.757 Alpha:0.015
Iter:130 Encrypted Loss:58724873 Decrypted Loss:1.75 Alpha:0.0155
Iter:140 Encrypted Loss:58516008 Decrypted Loss:1.744 Alpha:0.015
Iter:150 Encrypted Loss:58307663 Decrypted Loss:1.739 Alpha:0.015
Iter:160 Encrypted Loss:58102049 Decrypted Loss:1.732 Alpha:0.015
Iter:170 Encrypted Loss:57863091 Decrypted Loss:1.725 Alpha:0.015
Iter:180 Encrypted Loss:55470158 Decrypted Loss:1.653 Alpha:0.015
Iter:190 Encrypted Loss:54650383 Decrypted Loss:1.629 Alpha:0.015
Iter:200 Encrypted Loss:53838756 Decrypted Loss:1.605 Alpha:0.015
Iter:210 Encrypted Loss:51684722 Decrypted Loss:1.541 Alpha:0.015
Iter:220 Encrypted Loss:54408709 Decrypted Loss:1.621 Alpha:0.015
Iter:230 Encrypted Loss:54946198 Decrypted Loss:1.638 Alpha:0.015
Iter:240 Encrypted Loss:54668472 Decrypted Loss:1.63 Alpha:0.0155
Iter:250 Encrypted Loss:55444008 Decrypted Loss:1.653 Alpha:0.015
Iter:260 Encrypted Loss:54094286 Decrypted Loss:1.612 Alpha:0.015
Iter:270 Encrypted Loss:51251831 Decrypted Loss:1.528 Alpha:0.015
Iter:276 Encrypted Loss:24543890 Decrypted Loss:0.732 Alpha:0.015
Final Prediction:
True Pred:[0] Encrypted Prediction:[-3761423723.0718255 0.0] Decrypted Prediction:[-0.112]
True Pred:[1] Encrypted Prediction:[24204806753.166267 0.0] Decrypted Prediction:[ 0.721]
True Pred:[1] Encrypted Prediction:[23090462896.17028 0.0] Decrypted Prediction:[ 0.688]
True Pred:[0] Encrypted Prediction:[1748380342.4553354 0.0] Decrypted Prediction:[ 0.052]
複製程式碼
以上是我訓練這個網路時得到的輸出。因為加密噪聲和低精度的問題模型調優會更困難一些,同時模型的訓練也很慢,這是由於加密和解密操作的開銷。雖然我也想做一些更簡單的樣例,但是從我們的主題和概念出發,首先要保證的是方案的安全性。
重點:
- 網路的權重全部是加密的。
- 訓練資料是未加密的。
- 在訓練之後,網路可以被解密以提高效能或二次訓練(或切換到不同的加密金鑰)。
- 訓練損失和輸出的預測都是加密的值。我們必須解碼它們才能解釋網路的行為。
第9部分:情感分類
為了提供一些更真實的場景,這裡提供了基於優達學城的奈米學位中的網路實現的情感分類網路,網路是在IMDB的評價上訓練的。你可以在這裡找到完整的程式碼
import time
import sys
import numpy as np
# 讓我們調整之前的網路來適配這個問題
class SentimentNetwork:
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):
return 1 / (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'):
return 1
else:
return 0
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') # round to nearest integer
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%
複製程式碼
第10部分:資料加密的優勢
與本文類似的一種實現方式是讓網路在加密資料上進行訓練並輸出加密的預測值。這是一個很好的想法,但是仍有一些缺陷。首先,加密的資料意味著這些資料對於沒有私鑰的人來說是沒有任何意義的這使得個人來源的資料無法在相同的深度學習網路上訓練。但是大多數的商業應用又有這個需求,需要彙總消費者的資料。從理論上講我們希望每個消費者有屬於自己的一份私鑰,但同態加密要求所有人共用一份私鑰。
但是,加密網路就不會受到這種限制了。
用上面的方法,首先你經過一段時間訓練得到了一個合格的神經網路,然後加密之後將公鑰傳送給A(A可以用他的資料繼續訓練網路一段時間),之後你拿回神經網路,用不同的金鑰再次加密之後發給B方,B同樣可以用他的資料訓練網路。由於網路本身是加密的,你可以擁有網路智慧的完全控制權.A和B無法知道他們用的是不是相同的網路,一切過程都是在他們看不到或者無法使用網路的前提下進行的。由此你可以保持對神經網路智慧財產權的控制,每個使用者又可以保持自身資料的隱私性。
第11部分:未來的工作
會有更加安全更加快速的同態加密演算法。我認為將工作移植到YASHE會是一個正確的選擇。也許一個框架更適合使用者使用進行加密,因為經過了系統的複雜抽象。大體來說,要使這些想法達到生產級的水平,同態加密必須要更快一點。不過這方面的進展也很迅速,我相信不久的將來就可以實現。
第12部分:潛在應用
去中心化的AI: 公司可以直接在現場部署模型進行訓練或使用而無需擔心網路被盜取的風險。
保護消費者隱私: 上面提到的應用為使用者掌控自己的資料提供了可能性,他們可以"選擇加入"某些網路的訓練而不是把自己的資料全部傳送至某地。公司也無法利用網路下方對智慧財產權造成的風險來拒絕這一要求。資料是有著強大的力量,但它應當迴歸到使用者手中。
受控的超級智慧: 網路可以變得很聰明,但是隻要它沒有私鑰,它做的預測就無法對外界造成影響。
問答
相關閱讀
此文已由作者授權騰訊雲+社群釋出,更多原文請點選
搜尋關注公眾號「雲加社群」,第一時間獲取技術乾貨,關注後回覆1024 送你一份技術課程大禮包!
海量技術實踐經驗,盡在雲加社群!