本文以MNIST手写数字识别任务为例,使用FPGA搭建了一个LSTM网络加速器,并选取MNIST数据集中的10张图片,通过vivado软件进行仿真验证。实验结果表明,本文设计的基于FPGA的LSTM网络加速器可以完成图片分类任务,其准确率为80%(20张图片,4张分类错误)。本文主要分为四部分,第一章为LSTM硬件加速器的原理介绍,第二章为软件部分的程序设计思路,第三章为FPGA硬件部分的设计思路。本文所设计的LSTM硬件加速器的完整的工程文件已上传,并在文末对工程文件进行了简单的介绍。
目录
基于FPGA的LSTM加速器设计流程如下所示:
本章主要对各流程中的原理部分进行介绍,本章尽可能的解释了FPGA实现LSTM硬件加速器所需的原理知识,详细的介绍可以参考文末给出的参考资料。
传统循环神经网络(Recuurent Nerual Network,RNN)通过将上一时间步的输出
h
t
h_{t}
ht作为输入的一部分,和当前输入
x
t
x_{t}
xt一起作为输入信息输入到网络中,从而能够捕获序列信号的特性。然而,传统RNN存在梯度消失和梯度下降的问题,
而LSTM通过引入记忆细胞机制缓解了传统循环神经网络(Recuurent Nerual Network,RNN)梯度消失和梯度爆炸的问题。
LSTM的表达式如下所示:

其中
i
t
i_{t}
it为输入门,取值范围为(0~1),表示更新记忆细胞的程度。
f
t
f_{t}
ft为遗忘门,取值范围为(0~1),表示上一时间步的记忆细胞
c
t
−
1
c_{t-1}
ct−1的剔除程度。
c
~
t
\tilde{c}_{t}
c~t表示待更新入记忆细胞的信息。
o
t
o_{t}
ot为输出门,决定记忆细胞与输出信息的关系。LSTM通过门控单元,能及时的剔除记忆细胞中的无用信息,并及时准确地更新信息,从而能缓解传统RNN的梯度消失和梯度爆炸的问题,但也因此引入了大量的参数,导致其难以直接运行在存储、计算资源受限的平台,例如FPGA。
由于神经网络具有很强的鲁棒性,即使被大幅度的压缩,也能保证其准确率。
E
L
S
T
M
[
1
]
ELSTM^{[1]}
ELSTM[1]中提出了一种top-
k
k
k剪枝方案。该剪枝方案将权值矩阵的每个相邻的c个权值分为一组,每组只保留前k个绝对值最大的非零权值,其余权值均设为0。

c=8,k=2的top-
k
k
k剪枝示意图如上所示。存储top-
k
k
k剪枝方案压缩后的权值矩阵时,只需要3bits(
l
o
g
2
c
=
3
log_{2}c = 3
log2c=3)即可表示该非零权值的位置信息。
除了通过剪枝压缩模型外,还可以通过量化来压缩模型。由于GPU训练的模型为32位浮点数,而FPGA处理数据大多是定点数,因此需要对模型进行量化。常用的量化方式用两种,一种是线性量化,一种是对数量化。
对数量化的表达式如下所示:

其中m,f分别代表量化后的整数位位数和小数位位数。这种量化方式量化后的数为2的幂次方,如0011就表示
2
−
3
2^{-3}
2−3,而与2的幂次方的乘法运算,可以用移位运算替代,如
a
∗
2
−
b
=
a
>
>
>
b
a * 2^{-b} = a >>> b
a∗2−b=a>>>b。

如图所示,蓝色线表示函数
y
=
l
o
g
Q
m
,
−
f
(
x
)
y = log Q_{m,-f}(x)
y=logQm,−f(x),橘色线条表示函数
y
=
x
y=x
y=x。由图可知,对数量化对数值较大的数,量化产生的误差较大。因此,只被用于量化LSTM网络的权值矩阵参数。
线性量化的表达式如下所示:

其中m,f分别代表量化后的整数位位数和小数位位数。

如图所示,蓝色线表示函数 y = Q m , f ( x ) y = Q_{m, f}(x) y=Qm,f(x),橘色线条表示函数 y = x y=x y=x。线性量化的误差较稳定,因此输入、输出、中间运算结果均采用线性量化。
LSTM中的
σ
\sigma
σ和
tanh
\tanh
tanh函数,均为带有
e
x
e^{x}
ex的指数运算函数,FPGA难以直接实现这种复杂函数。常用的解决方案是用分段线性函数替代这两个非线性激活函数。如下所示:

为了便于FPGA的实现,本文采用Ptanh函数替代
t
a
n
h
tanh
tanh,用
H
s
i
g
m
Hsigm
Hsigm替代
σ
\sigma
σ。
本章主要对软件部分的设计进行介绍,并对关键代码进行解释,完整代码参考工程文件。

本文搭建的LSTM网络模型包含一个输入维度为28,隐藏层维度28的单层单向的LSTM层,一个输入维度28,输出维度10的全连接层(Fully Connect,FC),以及一个4bit输出的分类器(输出最大值的位置信息,0~9)。
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.rnn = LSTM(28, 28, num_layers=1,
bias=True, return_sequences=True, grad_clip=None)
self.fc = nn.Linear(28, 10)
def forward(self, x):
zeros = Variable(torch.zeros(x.size()[0], 28, device=torch.device("cuda")))
initial_states = [(zeros, zeros)]
x = x.squeeze(1)
hidden, _ = self.rnn(x, initial_states) #LSTM层
x = self.fc(hidden[:, -1, :]) #全连接层
return x
net = Net()
outputs = net(images)
_, predicted = torch.max(outputs.data, 1) #分类器
由于官方未提供(也可能是我没找到)LSTM对数量化的API,因此本文选用的LSTM为自定义的程序文件(详见工程文件)。
训练好的网络模型难以直接运行在资源受限的平台,因此需要剪枝压缩。top- k k k剪枝压缩代码如下所式:
def topk(para, k):
c = torch.zeros(para.size()[0], para.size()[1],dtype = torch.int) #初始化一个和权值矩阵相同大小的掩膜矩阵
l = int(para.size()[1]/7) #将每行的每7个权值分为一组,l为分组的数量
parameter = torch.abs(para) #将权值矩阵取绝对值
_, b = torch.topk(parameter[:,:7], k, 1, largest = True) #b为0~6之间的k个数,表示该组最大的前k个权值的位置
for i in range(1,l):
_, b1 = torch.topk(parameter[:,i*7:(i+1)*7], k, 1, largest = True) #遍历每一组最大的前k个值的位置
b1 = b1 + i * 7 #得到每一行中保留的权值位置信息的绝对位值
b = torch.cat((b,b1),dim=1) #将每一段拼接起来
for j in range(c.size()[0]):
c[j, b[j, :]] = 1 #将c中,b中位置信息的对应的位置,置1(保留),其他部分为0
return c
由于信息维度为28,因此本文选取每7个权值为一组,每组保留k个权值。本文选取的k为4,通过函数topk生成掩膜矩阵,再根据pytorch提供的API进行剪枝训练。
c1 = topk(net.rnn.cell0.weight_ix, 4) #根据W^{i}生成掩膜矩阵
class FooBarPruningMethod1(prune.BasePruningMethod): #调用剪枝的API,更改掩膜矩阵
"""Prune every other entry in a tensor
"""
PRUNING_TYPE = 'unstructured'
def compute_mask(self, t, default_mask):
mask = c1
return mask
FooBarPruningMethod1.apply(net.rnn.cell0, 'weight_ix') #对权值矩阵W^{i}进行剪枝
权值矩阵 W i W^{i} Wi的剪枝程序设计如上所示,以此类推,对LSTM其他权值矩阵,及FC层的权值矩阵进行剪枝。
Ptanh和Hsigm函数如下所示,本文将剪枝后的网络中的非线性激活函数用这两个分段线性函数进行替代,并通过重训练恢复模型精度。
def Ptanh(inn):
we1 = inn < -2.5
we2 = (inn >= -2.5) & (inn < -0.5)
we3 = (inn >= -0.5) & (inn < 0.5)
we4 = (inn >= 0.5) & (inn < 2.5)
we5 = inn >= 2.5
out1 = -1
out2 = 0.25 * inn - 0.375
out3 = inn
out4 = 0.25 * inn + 0.375
out5 = 1
out = out1 * we1 + out2 * we2 + out3 * we3 + out4 * we4 + out5 * we5
return out
def Hsigm(inn)
out = torch.clip(0.25 * inn + 0.5, 0, 1, out=None)
return out
def log2_Q(a):
a = a.to('cpu')
b = a.detach().numpy()
e = np.sign(b)
b = np.clip(np.round(np.log2(np.fabs(b))+0.4),-7,0) #得到最接近原始a的2的幂次方,不改变a的其他属性,因此只使用data属性
b = np.power(2,b) * e
a.data = torch.from_numpy(b).data
return a
对数量化的函数如上所示,权值被量化为4bits数,详见[1],然而量化后的权值矩阵不能再训练,因此本文先量化的候选记忆细胞的权值矩阵( W c W^{c} Wc、 U c U^{c} Uc),之后对其他剩余的权值进行重新训练;再量化输出门和遗忘门的权值矩阵( W o W^{o} Wo、 U o U^{o} Uo、 W f W^{f} Wf、 U f U^{f} Uf),并对其他剩余的权值进行训练,最后再量化输入门的权值矩阵( W i W^{i} Wi、 U i U^{i} Ui)。
def Q(a): #输入训练参数,输出量化后的训练参数
a = a.to('cpu')
b = a.detach().numpy() #由于a是训练参数,requires_grad为True,因此不能直接用numpy函数操作,需转换
b = np.clip(b,-0.875,0.875) #0.875是1 - (1/2)^3
b = np.round(b * 8 + 0.5) / 8
a.data = torch.from_numpy(b).data #得到最接近原始a的定点数
return a
权值被量化为4bits数,详见[1],本文对FC层的权值矩阵进行线性量化,将其量化为4bits数。
由于FPGA中存储和运算为二进制补码形式,因此需要将权值转换为补码形式,转换程序如下所示:
def p_d2b(n, m, f): #将一个10进制正数转换为一个2进制数,保留m位整数,f位小数,首位符号位
b = []
x = 2
n = n * np.power(2, f)
n = int(n)
while True:
s = n // x
y = n % x
b = b + [y]
if s == 0:
break
n = s
b.reverse()
if(len(b) > (m+f)):
for i in range(m+f):
b[i] = 1
b = b[:m+f]
elif(len(b) < (m+f)):
for i in range(m+f-len(b)):
b.insert(0,0)
b.insert(0,0)
a = [str(i) for i in b ]
return a
def n_d2b(n, m, f): #求一个10进制负数转换为一个2进制补码形式,保留m位整数,f位小数,首位符号位
n = -1 * n
b = p_d2b(n, m, f)
b[0] = '1'
flag = 1
for i in range(len(b)-1,0,-1):
if b[i]== '1' and flag == 1:
b[i] = '1'
flag = 0
elif b[i] == '0' and flag == 1:
b[i] = '0'
flag = 1
elif b[i] == '0':
b[i] = '1'
else:
b[i] = '0'
a = [str(i) for i in b ]
return a
def d2b(n, m, f): #求一个数n的补码,保留m位整数,n位小数,首位符号位
if n < 0:
c = n_d2b(n, m, f)
else:
c = p_d2b(n, m, f)
return c
对数量化后的数的存储与线性量化不同,如0.125( 2 − 3 2^{-3} 2−3),应该存储为0011,表示右移3位即可完成乘法操作。对数量化后的数据的二进制生成函数如下所示:
def logd2b(n, f):
if n > 0:
n = np.log2(n)
n = np.floor(-1 * n + 0.5)
if n >= np.power(2,f):
n = np.power(2,f)
a = p_d2b(n,f,0)
else:
n = -1 * n
n = np.log2(n)
n = np.floor(-1 * n + 0.5)
if n >= np.power(2, f):
n = np.power(2, f)
a = p_d2b(n, f, 0)
a[0] = '1'
return a
通过以上方式生成FPGA的ROM可加载的coe文件,将GPU训练出的网络模型参数导入到FPGA中。
def output_file_log(weight,name):
name = 'coe/'+name
f1 = open(str(name)+"_data.coe","a")
f2 = open(str(name)+"_index.coe","a")
data =';\nmemory_initialization_radix = 2;\nmemory_initialization_vector='
f1.writelines(data)
f2.writelines(data)
para = weight.numpy()
for i in range(para.shape[0]):
f1.writelines('\n')
f2.writelines('\n')
for j in range(para.shape[1]):
if para[i,j]!=0:
data = logd2b(para[i,j], 3)
index = p_d2b(j%7,3,0)[1:] #topk剪枝后的非零权值需要3bit表示其在分组中的位置信息
f1.writelines(data)
f2.writelines(index)
f1.writelines(';')
f1.close()
f2.writelines(';')
f2.close()
def output_file_Q(weight,name):
name = 'coe/' + name
f1 = open(name + "_data.coe", "a")
f2 = open(name + "_index.coe", "a")
data = ';\nmemory_initialization_radix = 2;\nmemory_initialization_vector='
f1.writelines(data)
f2.writelines(data)
para = weight.numpy()
for i in range(para.shape[0]):
f1.writelines('\n')
f2.writelines('\n')
for j in range(para.shape[1]):
if para[i, j] != 0:
data = d2b(para[i, j], 2,13)
index = p_d2b(j % 7, 3, 0)[1:]#topk剪枝后的非零权值需要3bit表示其在分组中的位置信息
f1.writelines(data)
f2.writelines(index)
f1.writelines(';')
f1.close()
f2.writelines(';')
f2.close()
LSTM整体框架如图所示:
其中LSTM层的结构如下所示:

输入数据经过S-P-X转换为多维向量,和非零权值的位置信息一起送入KMUX单元进行筛选,筛选后的非零权值和输入信息在MVMs模块中完成矩阵乘加运算,并在EWU模块中完成激活,点乘等运算,计算出的记忆细胞的值存储在FIFO-C中,输出信息
h
t
h_{t}
ht则存储在S-P-H中,以作为下一个时间步的输入。全连接层和LSTM的架构大体相似。
输入‘4’的图片,仿真结果如图所示,结果正确。
硬件部分没有太多要说的,需要注意的有如下几点:
完整工程文件:基于FPGA的LSTM加速器设计(MNIST数据集为例)
我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数
有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳
我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_
无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD
导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵
本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01 客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02 数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit
文章目录一、概述简介原理模块二、配置Mysql使用版本环境要求1.操作系统2.mysql要求三、配置canal-server离线下载在线下载上传解压修改配置单机配置集群配置分库分表配置1.修改全局配置2.实例配置垂直分库水平分库3.修改group-instance.xml4.启动监听四、配置canal-adapter1修改启动配置2配置映射文件3启动ES数据同步查询所有订阅同步数据同步开关启动4.验证五、配置canal-admin一、概述简介canal是Alibaba旗下的一款开源项目,Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。Git地址:https://github.co
C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.