奥胡斯大学密码学phd、datadog机器学习工程师morten dahl介绍了如何基于安全多方计算协议实现私密深度学习模型。
受最近一篇混合深度学习和同态加密的博客的启发(见基于numpy实现同态加密神经网络),我觉得使用安全多方计算(secure multi-party computation)替换同态加密实现深度学习会很有趣。
在本文中,我们将从头构建一个简单的安全多方计算协议,然后尝试基于它训练简单的神经网络进行基本布尔值计算。本文的相关代码可以通过github取得(mortendahl/privateml/simple-boolean-functions)。 假设有未串通的三方p0、p1、p2,愿意一起进行计算,即训练神经网络并使用它们进行预测;然而,出于一些理由,他们不愿意泄露学习好的模型。同时假定有些用户愿意在保持私密的前提下提供训练数据,有些用户也有兴趣在他们的输入保持私密的前提下使用学习好的模型。
为了能够做到这一点,我们需要安全地在特定精度下计算有理数;具体而言,对它们做加法和乘法。我们同时需要计算sigmoid函数1/(1+np.exp(-x)),这一函数的传统形式在安全设定下会导致惊人沉重的运算。因此,我们将依照基于numpy实现同态加密神经网络中的做法,使用多项式逼近sigmoid函数,不过我们会进行一点优化。
安全多方计算
同态加密(homomorphic encryption,he)和安全多方计算(secure multi-party computation,mpc)是现代密码学中密切相关的两个领域,常常互相使用对方的技术以便解决大致相同的问题:计算接受私密数据输入的函数而不泄露任何东西,除了(可选)最终输出。例如,在我们的私密机器学习设定下,两种技术可以用来训练我们的模型并进行预测(不过在he的情形下,如果数据来自使用不同加密钥的用户,需要做一些专门的技术处理)。
就其本身而言,从高层看,he经常可以用mpc替换,反之亦然。至少就今天而言,两者的区别大致是he不怎么需要交互,但是需要昂贵的计算,而mpc的计算很廉价,但需要大量交互。换句话说,mcp用两方或多方间的交互取代了昂贵的计算。
目前而言,这在实践中提供了更好的性能,以至于人们可以主张mcp是明显更成熟的技术——作为这一主张的依据,已经存在好几家公司提供基于mpc的服务。
定点数算术
运算将在一个有限域上进行,因此我们首先需要决定如何将有理数r表示为域元素,即取自0, 1, ..., q-1的整数x(q为质数)。我们将采用典型的做法,根据固定的精度放大每个有理数,比如,在6位精度的情形下,我们将放大10**6倍,然后将结果的整数部分作为定点数表示。例如,q = 10000019时,我们得到encode(0.5) == 500000和encode(-0.5) == 10000019 - 500000 == 9500019。
def encode(rational):
upscaled = int(rational * 10**6)
field_element = upscaled % q
return field_element
def decode(field_element):
upscaled = field_element if field_element <= q/2else field_element - q
rational = upscaled / 10**6
return rational
注意,在这一表示下,加法是直截了当的,(r * 10**6) + (s * 10**6) == (r + s) * 10**6,而乘法添加了额外的放大因子,我们需要处理掉以保持精度和避免爆掉数字:(r * 10**6) * (s * 10**6) == (r * s) * 10**6 * 10**6。
共享和重建数据
编码输入后,每个用户接着需要一种和他方共享数据的方式,以便用于计算,不过,数据需要保持私密。
为了达到这一点,我们需要的配料是秘密共享(secret sharing)。秘密共享将一个值以某种方式分成三份,任何见到少于三份数据的人,无法得知关于值的任何信息;然而,一旦见到所有三份,可以轻易地重建值。
出于简单性考虑,这里我们将使用复制秘密共享(replicated secret sharing),其中每方收到不止一份数据。具体而言,私密值x分成部分x0、x1、x2,满足x == x0 + x1 + x2。p0方收到(x0,x1),p1收到(x1,x2),p2收到(x2,x0)。不过本教程中这一点将是隐式的,本文会直接将共享的x储存为由三部分组成的向量[x0, x1, x2]。
def share(x):
x0 = random.randrange(q)
x1 = random.randrange(q)
x2 = (x - x0 - x1) % q
return [x0, x1, x2]
当两方以上同意将一个值表露给某人时,他们直接发送他们所有的部分,从而使重建得以进行。
def reconstruct(shares):
return sum(shares) % q
然而,如果部分是以下小节提到的一次或多次安全运算的结果,出于私密性考虑,我们在重建前进行一次再共享。
def reshare(xs):
y = [ share(xs[0]), share(xs[1]), share(xs[2]) ]
return [ sum(row) % q for row in zip(*y) ]
严格来说这是不必要的,但是进行这一步可以更容易地说明为什么协议是安全的;直观地,它确保分享的部分是新鲜的,不包含关于我们用于计算结果的数据的信息。
加法和减法
这样我们已经可以进行安全的加法和减法运算了:每方直接加减其拥有的部分,由于(x0 + x1 + x2) + (y0 + y1 + y2) == (x0 + y0) + (x1 + y1) + (x2 + y2),通过这一操作可以得到x + y的三部分(技术上说应该是reconstruct(x) + reconstruct(y),但是隐式写法更易读)。
def add(x, y):
return [ (xi + yi) % q for xi, yi in zip(x, y) ]
def sub(x, y):
return [ (xi - yi) % q for xi, yi in zip(x, y) ]
注意这不需要进行任何通讯,因为这些都是本地运算。
乘法
由于每方拥有两个部分,乘法可以通过类似上面提到的加法和减法的方式进行,即,每方基于已拥有的部分计算一个新部分。具体而言,对下面的代码中定义的z0、z1、z2而言,我们有x * y == z0 + z1 + z2(技术上说……)
然而,每方拥有两个部分的不变性没有满足,而像p1直接将z1发给p0这样的做法是不安全的。一个简单的修正是直接将每份zi当成私密输入共享;这样就得到了一个正确而安全的共享w(乘积)。
def mul(x, y):
# 本地运算
z0 = (x[0]*y[0] + x[0]*y[1] + x[1]*y[0]) % q
z1 = (x[1]*y[1] + x[1]*y[2] + x[2]*y[1]) % q
z2 = (x[2]*y[2] + x[2]*y[0] + x[0]*y[2]) % q
# 重共享和分发;这里需要通讯
z = [ share(z0), share(z1), share(z2) ]
w = [ sum(row) % q for row in zip(*z) ]
# 将双精度转回单精度
v = truncate(w)
return v
不过还有一个问题,如前所述,reconstruct(w)具有双精度:它编码时使用的放大因子是10**6 * 10**6,而不是10**6。在不安全设定下,我们本可以通过标准的除法(除以10**6)来修正这一点,然而,由于我们操作的是有限域中的秘密共享元素,这变得不那么直截了当了。
除以一个公开的常量,这里是10**6,足够简单:我们直接将部分乘以其域中的逆元素10**(-6)。对某v和u < 10**6,如果reconstruct(w) == v * 10**6 + u,那么乘以逆元素得到v + u * 10**(-6),那么v就是我们要找到的值。在不安全设定下,残值u * 10**(-6)足够小,可以通过取整消除。与此不同,在安全设定下,基于有限域元素,这一语义丢失了,我们需要通过其他方法摆脱残值。
一种方法是确保u == 0。具体而言,如果我们事先知道u,那么我们可以不对w作除法,而对w' == (w - share(u))作除法,接着我们就如愿以偿,得到v' == v和u' == 0,即,没有任何残值。
剩下的问题当然是如何安全地得到u,以便计算w'。具体细节见cs’10,不过基本的思路是首先在w上加上一个大的掩码,将掩码后的值表露给其中一方,使其得以计算掩码后的u。最后,共享和解掩码这一掩码后的值,然后计算w'。
def truncate(a):
# 映射到正值范围
b = add(a, share(10**(6+6-1)))
# 应用仅有p0知道的掩码,然后重建掩码后的b,发送给p1或p2
mask = random.randrange(q) % 10**(6+6+kappa)
mask_low = mask % 10**6
b_masked = reconstruct(add(b, share(mask)))
# 提取低位数字
b_masked_low = b_masked % 10**6
b_low = sub(share(b_masked_low), share(mask_low))
# 去除低位数字
c = sub(a, b_low)
# 除法
d = imul(c, inverse)
return d
注意上面的imul是本地操作,将每个共享部分乘以公开的常数,这里是10**6的域中逆元素。
安全数据类型
最后,我们将以上过程包裹进一个定制的抽象数据类型,这样我们之后表达神经网络的时候就可以使用numpy了。
classsecurerational(object):
def __init__(self, secret=none):
self.shares = share(encode(secret)) if secret isnotnoneelse []
return z
def reveal(self):
return decode(reconstruct(reshare(self.shares)))
def __repr__(self):
returnsecurerational(%f) % self.reveal()
def __add__(x, y):
z = securerational()
z.shares = add(x.shares, y.shares)
return z
def __sub__(x, y):
z = securerational()
z.shares = sub(x.shares, y.shares)
return z
def __mul__(x, y):
z = securerational()
z.shares = mul(x.shares, y.shares)
return z
def __pow__(x, e):
z = securerational(1)
for _ in range(e):
z = z * x
return z
基于这一类型,我们可以安全地对这样的值进行操作:
x = securerational(.5)
y = securerational(-.25)
z = x * y
assert(z.reveal() == (.5) * (-.25))
此外,需要调试的时候,我们可以切换为不安全类型而不需要修改其余(神经网络)代码。再比如,我们可以隔离计数器的使用,查看进行了多少次乘法,进而让我们模拟下需要多少通讯。
深度学习
这里用“深度学习”这个术语属于夸夸其谈,因为我们只是简单地摆弄了下基于numpy实现同态加密神经网络中的神经网络学习基本布尔值函数。
一个简单函数
第一个实验是训练网络以识别序列中的第一位。下面的代码中,x中的四行是输入的训练数据,y中相应的列是所需输出。
x = np.array([
[0,0,1],
[0,1,1],
[1,0,1],
[1,1,1]
])
y = np.array([[
0,
0,
1,
1
]]).t
我们将使用同样的双层网络,不过我们会将下面定义的sigmoid逼近函数作为参数。secure函数是一个简单的辅助函数,将所有值转换为我们的安全数据类型。
classtwolayernetwork:
def __init__(self, sigmoid):
self.sigmoid = sigmoid
def train(self, x, y, iterations=1000):
# 初始化权重
self.synapse0 = secure(2 * np.random.random((3,1)) - 1)
# 训练
for i in range(iterations):
# 前向传播
layer0 = x
layer1 = self.sigmoid.evaluate(np.dot(layer0, self.synapse0))
# 反向传播
layer1_error = y - layer1
layer1_delta = layer1_error * self.sigmoid.derive(layer1)
# 更新
self.synapse0 += np.dot(layer0.t, layer1_delta)
def predict(self, x):
layer0 = x
layer1 = self.sigmoid.evaluate(np.dot(layer0, self.synapse0))
return layer1
同时,我们将使用原文提出的sigmoid逼近,即标准麦克劳林/泰勒多项式的前五项。出于可读性考虑,我这里用了一个简单多项式演算,有待进一步优化,比如使用秦九韶算法减少乘法的数目。
classsigmoidmaclaurin5:
def __init__(self):
one = securerational(1)
w0 = securerational(1/2)
w1 = securerational(1/4)
w3 = securerational(-1/48)
w5 = securerational(1/480)
self.sigmoid = np.vectorize(lambda x: w0 + (x * w1) + (x**3 * w3) + (x**5 * w5))
self.sigmoid_deriv = np.vectorize(lambda x: (one - x) * x)
def evaluate(self, x):
return self.sigmoid(x)
def derive(self, x):
return self.sigmoid_deriv(x)
实现了这个之后我们就可以训练和演算网络了(细节见notebook),这里使用了10000次迭代。
# 设置随机数种子以获得可复现的结果
random.seed(1)
np.random.seed(1)
# 选择逼近
sigmoid = sigmoidmaclaurin5()
# 训练
network = twolayernetwork(sigmoid)
network.train(secure(x), secure(y), 10000)
# 演算预测
evaluate(network)
注意训练数据在输入网络之前是安全共享的,并且学习到的权重从未泄露。预测同理,只有网络的用户知道输入和输出。
error: 0.00539115
error: 0.0025606125
error: 0.00167358
error: 0.001241815
error: 0.00098674
error: 0.000818415
error: 0.0006990725
error: 0.0006100825
error: 0.00054113
error: 0.0004861775
layer0 weights:
[[securerational(4.974135)]
[securerational(-0.000854)]
[securerational(-2.486387)]]
prediction on [000]: 0 (0.50000000)
prediction on [001]: 0 (0.00066431)
prediction on [010]: 0 (0.49978657)
prediction on [011]: 0 (0.00044076)
prediction on [100]: 1 (5.52331855)
prediction on [101]: 1 (0.99969213)
prediction on [110]: 1 (5.51898314)
prediction on [111]: 1 (0.99946841)
从上面的演算来看,神经网络确实看起来学习到了所要求的函数,在未见输入上也能给出正确的预测。
稍微高级些的函数
在下一个实验中,神经网络无法像之前一样镜像三个组件的其中一个,从直观上说,需要计算第一位和第二位的异或(第三位是偏离)。
x = np.array([
[0,0,1],
[0,1,1],
[1,0,1],
[1,1,1]
])
y = np.array([[
0,
1,
1,
0
]]).t
如numpy实现神经神经网络:反向传播一文所解释的,使用双层神经网络只能给出无意义的结果,本质上是在说“让我们扔一枚硬币吧”。
error: 0.500000005
error: 0.5
error: 0.5000000025
error: 0.5000000025
error: 0.5
error: 0.5
error: 0.5
error: 0.5
error: 0.5
error: 0.5
layer0 weights:
[[securerational(0.000000)]
[securerational(0.000000)]
[securerational(0.000000)]]
prediction on [000]: 0 (0.50000000)
prediction on [001]: 0 (0.50000000)
prediction on [010]: 0 (0.50000000)
prediction on [011]: 0 (0.50000000)
prediction on [100]: 0 (0.50000000)
prediction on [101]: 0 (0.50000000)
prediction on [110]: 0 (0.50000000)
prediction on [111]: 0 (0.50000000)
提议的补救措施是在网络中引入另一层:
classthreelayernetwork:
def __init__(self, sigmoid):
self.sigmoid = sigmoid
def train(self, x, y, iterations=1000):
# 初始权重
self.synapse0 = secure(2 * np.random.random((3,4)) - 1)
self.synapse1 = secure(2 * np.random.random((4,1)) - 1)
# 训练
for i in range(iterations):
# 前向传播
layer0 = x
layer1 = self.sigmoid.evaluate(np.dot(layer0, self.synapse0))
layer2 = self.sigmoid.evaluate(np.dot(layer1, self.synapse1))
# 反向传播
layer2_error = y - layer2
layer2_delta = layer2_error * self.sigmoid.derive(layer2)
layer1_error = np.dot(layer2_delta, self.synapse1.t)
layer1_delta = layer1_error * self.sigmoid.derive(layer1)
# 更新
self.synapse1 += np.dot(layer1.t, layer2_delta)
self.synapse0 += np.dot(layer0.t, layer1_delta)
def predict(self, x):
layer0 = x
layer1 = self.sigmoid.evaluate(np.dot(layer0, self.synapse0))
layer2 = self.sigmoid.evaluate(np.dot(layer1, self.synapse1))
return layer2
然而,如果我们采用之前的方式训练网络,即使仅仅迭代100次,我们都将面临一个奇怪的现象:突然之间,误差、权重、预测分数爆炸了,给出混乱的结果。
error: 0.496326875
error: 0.4963253375
error: 0.50109445
error: 4.50917445533e+22
error: 4.20017387687e+22
error: 4.38235385094e+22
error: 4.65389939428e+22
error: 4.25720845129e+22
error: 4.50520005372e+22
error: 4.31568874384e+22
layer0 weights:
[[securerational(970463188850515564822528.000000)
securerational(1032362386093871682551808.000000)
securerational(1009706886834648285970432.000000)
securerational(852352894255113084862464.000000)]
[securerational(999182403614802557534208.000000)
securerational(747418473813466924711936.000000)
securerational(984098986255565992230912.000000)
securerational(865284701475152213311488.000000)]
[securerational(848400149667429499273216.000000)
securerational(871252067688430631387136.000000)
securerational(788722871059090631557120.000000)
securerational(868480811373827731750912.000000)]]
layer1 weights:
[[securerational(818092877308528183738368.000000)]
[securerational(940782003999550335877120.000000)]
[securerational(909882533376693496709120.000000)]
[securerational(955267264038446787723264.000000)]]
prediction on [000]: 1 (41452089757570437218304.00000000)
prediction on [001]: 1 (46442301971509056372736.00000000)
prediction on [010]: 1 (37164015478651618328576.00000000)
prediction on [011]: 1 (43504970843252146044928.00000000)
prediction on [100]: 1 (35282926617309558603776.00000000)
prediction on [101]: 1 (47658769913438164484096.00000000)
prediction on [110]: 1 (35957624290517111013376.00000000)
prediction on [111]: 1 (47193714919561920249856.00000000)
导致这一切的原因很简单,但也许乍看起来不是那么明显(至少对我而言)。尽管(前五项)麦克劳林/泰勒逼近sigmoid函数在前面的网络中表现良好,当我们进一步推进时,它完全崩塌了,产生的结果不仅不精确,而且数量级也不对。因此很快摧毁了我们可能使用的任何有穷数字表示,即使在非安全设定下也是如此,数字开始溢出了。
技术上说sigmoid函数演算的点积变得太大了,就我所知,这意味着神经网络变得非常自信。就此而言,问题在于我们的逼近不允许神经网络变得足够自信,否则精确度会非常糟糕。
我不清楚基于numpy实现同态加密神经网络是如何避免这一问题的,我最好的猜测是较低的初始权重和alpha更新参数使它可能在迭代次数较低的情形下绕过这个坑(看起来是少于300次迭代)。无比欢迎任何关于这方面的评论。
逼近sigmoid
既然是我们的sigmoid逼近阻碍了我们学习更高级的函数,那么很自然地,我们接下来尝试使用麦克劳林/泰勒多项式的更多项。
如下所示,加到第9项(而不是第5项)确实能稍微增加一点进展,但这点进展远远不够。此外,它塌得更快了。
error: 0.49546145
error: 0.4943132225
error: 0.49390536
error: 0.50914575
error: 7.29251498137e+22
error: 7.97702462371e+22
error: 7.01752029207e+22
error: 7.41001528681e+22
error: 7.33032620012e+22
error: 7.3022511184e+22
...
或者我们该转而使用更少的项以更好地牵制崩塌?比如,只加到第3项?这确实有点作用,能让我们在崩塌之前训练500次迭代而不是100次。
error: 0.4821573275
error: 0.46344183
error: 0.4428059575
error: 0.4168092675
error: 0.388153325
error: 0.3619875475
error: 0.3025045425
error: 0.2366579675
error: 0.19651228
error: 0.1748352775
layer0 weights:
[[securerational(1.455894) securerational(1.376838)
securerational(-1.445690) securerational(-2.383619)]
[securerational(-0.794408) securerational(-2.069235)
securerational(-1.870023) securerational(-1.734243)]
[securerational(0.712099) securerational(-0.688947)
securerational(0.740605) securerational(2.890812)]]
layer1 weights:
[[securerational(-2.893681)]
[securerational(6.238205)]
[securerational(-7.945379)]
[securerational(4.674321)]]
prediction on [000]: 1 (0.50918230)
prediction on [001]: 0 (0.16883382)
prediction on [010]: 0 (0.40589161)
prediction on [011]: 1 (0.82447640)
prediction on [100]: 1 (0.83164009)
prediction on [101]: 1 (0.83317334)
prediction on [110]: 1 (0.74354671)
prediction on [111]: 0 (0.18736629)
然而,误差和预测很糟糕,也没有多少空间供增加迭代次数了(大约在550次迭代处崩塌)。
插值
作为替代,我们可以放弃标准多项式逼近,转而尝试在区间上进行多项式插值。这里主要的参数是多项式的项数,我们希望它保持在一个较低的值,以提高效率。不过,系数的精度也是相关参数。
# 我们想要逼近的函数
f_real = lambda x: 1/(1+np.exp(-x))
# 我们想要优化的区间
interval = np.linspace(-10, 10, 100)
# 给定项数,进行多项式插值
degree = 10
coefs = np.polyfit(interval, f_real(interval), degree)
# 降低插值系数的精度
precision = 10
coefs = [ int(x * 10**precision) / 10**precision for x in coefs ]
# 逼近函数
f_interpolated = np.poly1d(coefs)
一同绘制标准逼近和插值多项式(红色曲线)的图像我们看到了改进的希望:我们无法避免在某点崩塌,但它的崩塌点显然要大很多。
当然,我们也可以尝试其他项数、精度、区间的组合,如下所示,不过对我们的应用而言,上面的参数组合看起来已经足够了。
现在让我们回到我们的三层网络,我们定义一个新的sigmoid逼近:
classsigmoidinterpolated10:
def __init__(self):
one = securerational(1)
w0 = securerational(0.5)
w1 = securerational(0.2159198015)
w3 = securerational(-0.0082176259)
w5 = securerational(0.0001825597)
w7 = securerational(-0.0000018848)
w9 = securerational(0.0000000072)
self.sigmoid = np.vectorize(lambda x: \
w0 + (x * w1) + (x**3 * w3) + (x**5 * w5) + (x**7 * w7) + (x**9 * w9))
self.sigmoid_deriv = np.vectorize(lambda x:(one - x) * x)
def evaluate(self, x):
return self.sigmoid(x)
def derive(self, x):
return self.sigmoid_deriv(x)
……然后开始训练:
# 设置随机数种子以获得可复现的结果
random.seed(1)
np.random.seed(1)
# 选择逼近
sigmoid = sigmoidinterpolated10()
# 训练
network = twolayernetwork(sigmoid)
network.train(secure(x), secure(y), 10000)
# 演算预测
evaluate(network)
现在,尽管我们运行了10000次迭代,没有发生崩塌,预测表现也提升了,只有一个预测错误([0 1 0])。
error: 0.0384136825
error: 0.01946007
error: 0.0141456075
error: 0.0115575225
error: 0.010008035
error: 0.0089747225
error: 0.0082400825
error: 0.00769687
error: 0.007286195
error: 0.00697363
layer0 weights:
[[securerational(3.208028) securerational(3.359444)
securerational(-3.632461) securerational(-4.094379)]
[securerational(-1.552827) securerational(-4.403901)
securerational(-3.997194) securerational(-3.271171)]
[securerational(0.695226) securerational(-1.560569)
securerational(1.758733) securerational(5.425429)]]
layer1 weights:
[[securerational(-4.674311)]
[securerational(5.910466)]
[securerational(-9.854162)]
[securerational(6.508941)]]
prediction on [000]: 0 (0.28170669)
prediction on [001]: 0 (0.00638341)
prediction on [010]: 0 (0.33542098)
prediction on [011]: 1 (0.99287968)
prediction on [100]: 1 (0.74297185)
prediction on [101]: 1 (0.99361066)
prediction on [110]: 0 (0.03599433)
prediction on [111]: 0 (0.00800036)
注意错误情形的分数不是完全偏离,某种程度上和正确预测的零值有所不同。再运行5000次迭代看起来不会改善这一点,这时我们已经快要崩塌了。
结语
本文重点介绍了一个简单的安全多方计算协议,而没有显式地论证开头提到的安全多方计算比同态加密更高效。我们看到,使用非常基本的操作实现私密机器学习确实是可能的。
也许更需要批评的是我们没有测量运行协议需要的通讯量,主要是每次乘法时所需交换的一些消息。基于这一简单的协议进行大量计算的话,显然让三方通过高速局域网连接会比较好。不过更高级的协议不仅减少了来回收发的数据量,同时改善了其他性质,比如回合数(像乱码电路(garbled circuits)就可以将回合数压到一个小常数)。
最后,本文基本上将协议和机器学习过程看成是正交的,让后者仅仅以一种黑盒的方式使用前者(除了sigmoid计算)。两者更好的配合需要两个领域的专门知识,但可能显著地提升总体性能。
e络盟加大投入产品线,进一步扩展测试与测量产品线
国内龙头电池片厂商积极扩产,三家光伏公司均以210大尺寸为主
OPPO R11前后2000万拍照更清晰,全国九城首发
三大运营商经营表现各异 通信行业格局即将生变
2017年手机销量排行榜上各大手机厂商的解析
基于安全多方计算协议实现私密深度学习模型
小米6plus什么时候上市?小米6plus最新消息:既生瑜何生亮,小米6Plus,再见!
空间光调制器LCOS-SLM的衍射效率
UCLA新型光学神经网络可立即识别物体
安森美半导体推出蓝牙5无线系统认证的单芯片
中国大陆PCB厂发展神速 台湾龙头地位开始不稳
基于LabVIEW8.2的虚拟数字电压表的设计和实现
基于可逻辑编辑器件实现串口通讯系统的设计
逆变器使用视频教程
日本东芝拟出售在美LNG业务,2018年闪存芯片价格指数已累计下滑57%
一周大事件:7月2日-3日,500+行业精英共话LED未来
泰国AIS为5G部署拨出了4.8亿美元
PID参数整定试凑法介绍
Embeded linux中的节拍驱动
科大讯飞推出了搭载全新A.I.技术的讯飞智能鼠标M210