信号为E时,如何让语音识别脱“网”而出?
欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~
一般没有网络时,语音识别是这样的
▽
而同等环境下,嵌入式语音识别,是这样的
▽
不仅可以帮您边说边识、出口成章,有个性化名字的时候也难不倒它。
这就是嵌入式语音识别的魅力。
本文将从微信智聆的嵌入式语音识别引擎的实现和优化,
介绍嵌入式语音识别的技术选型。
01
语音识别,大体是这么来的
语音识别,能让机器“听懂”人类的语音,把说话内容识别为对应文本。
开始于上世纪50年代
从最初的小词量孤立识别系统
到如今的大词量连续识别系统
语音识别系统的发展,性能得到显著的提升,主要得利于以下几个方面:
大数据时代的到来
深度神经网络在语音识别中的应用
GPU硬件的发展
因此,语音识别逐步走向实用化和产品化
语音输入法,语音智能助手,语音车载交互系统……
可以说,语音识别是人类征服人工智能的前沿阵地,是目前机器翻译、自然语言理解、人机交互等的奠基石。
然而,性能的提升基于服务端CPU/GPU高计算能力和大内存,没有网络的时候将无法享受语音识别的便利。
为了解决这个问题,微信智聆针对嵌入式语音识别进行研发。嵌入式语音识别,也称为嵌入式LVCSR(或离线LVCSR,Large Vocabulary Continuous Speech Recognition),指全程运行在手机端的语音识别,而不依赖于服务端强大的计算能力。
在一些网络不稳的特殊场景(车载、境外等等),嵌入式语音识别可“曲线救国”。
那么,实现一个嵌入式语音识别,存在哪些难点呢?
语音识别的基本流程
主流的语音识别算法当中,包括声学和语言两大模型。声学模型得利于近十年深度学习的发展,从GMM(高斯模型)到DNN(深度神经网络),再从DNN到LSTM RNN(循环神经网络),识别率不断提升的同时,计算量也不断地飞涨。而语言模型常用的n-gram算法,阶数越高性能越好,常用的模型多达数十G的内存。
所以综合起来,嵌入式语音识别有以下几个难点:
\1. 深度学习运算复杂,仅仅对模型进行裁剪性能损失大,需寻找挽回性能的方法;
\2. 裁剪模型不可避免,在模型训练环节如何避免小模型训练易陷入局部最优的问题;
\3. 如何计算的更快,满足嵌入式的CPU环境;
\4. 如何组织语言模型存储,能在有限的内存下存储更多的语言信息。
本文将以语音识别的技术原理出发,浅谈微信智聆嵌入式的实现技术。
内容将分为四个部分:
\1. 回顾语音识别的基本概念;
\2. 简单介绍在速度和内存优化上我们做的部分工作,侧重于工程应用实现;
\3. 说一说为了更好的性能我们做了哪些事,侧重于算法研究介绍;
\4. 我们进行实验对比,最后我们进行总结。
02
语音识别的各个组件
语音识别“黑盒”
语音识别从输入录音输出文字,黑盒子处理经过特征提取、声学模型、发音词典、语言模型等流程,笔者认为可以把语音识别比作一台计算机。
特征提取相当于是路由器,作为领头羊给后续环节提供源源不断的数据来源。
声学模型相当于语音识别的心脏——CPU,他将最直接影响着识别的准确性能。
语言模型相当于语音识别的硬盘,大量的词汇组合信息存储于此。
发音词典相当于内存条,能有效组织声学模型与语言模型的关系。
除此之外,语音识别包含一个解码器,他如同计算机的操作系统,有效地组织着各个环节。
接下来,我们基于每个“部件”简介其基本概念,以便后续介绍如何在这些“部件”上对嵌入式ASR工作的展开。
1.特征提取
音识别特征提取包括预加重、分帧、加窗、FFT(Fast Fourier Transform)等一系列流程,常用的特征有PLP、MFCC、FBANK等等。一般来说,语音识别把一秒语音分成100段(之间有互相重叠),而特征提取能把每段语音数据转化为一个向量(常见的有39维MFCC特征)。
为了关联上下文信息,特征作为声学模型的输入时,常将相邻帧拼凑一起。比如以39维特征为例,前后各取5帧信息,那么总共有11帧,输入的向量维度为11*39=429。一般地,语音识别的性能与取帧宽度是正相关的。
作为语音识别的路由器,特征提取环节的运算量并不大。然而其作为声学模型拓扑结构的输入,间接影响着深度学习的运算量,是我们在嵌入式ASR中要考虑的问题。
2.帧率抖动
5s统计一次直播流视频帧率,1min计算一次帧率方差,方差过大,视为推流帧率抖动.
3.声学模型(acoustic model)
声学模型作为语音识别的CPU,其重要性不言自喻。
一般地,它占据着语音识别大部分的运算开销,直接影响着语音识别系统的性能。传统语音识别系统普遍基于GMM-HMM的声学模型,其中GMM对语音声学特征的分布进行建模,HMM则用于对语音信号的时序性进行建模。
2006年深度学习兴起以后,深度神经网络(DNN,Deep Neural Networks)被应用于声学模型。
近十多年,声学模型的上深度学习的发展一路高歌,各种CNN、RNN、TDNN的拓扑结构如雨后春笋一一冒出,关于深度学习在声学模型的更多介绍见文。
对于嵌入式LVCSR来说,选择合适的DNN拓扑结构,并用合理的优化在手机实现结构的运算,是声学模型在其的核心诉求。
4.语言模型(language model)
语言模型,NLP从业者相对更为熟悉。在语音识别里,语言模型用来评估一个句子(即图2的词语序列)出现的概率高低。
在语言模型的实现算法中,最常见的为n-gram模型(n-gram models),利用当前词前面的n个词来计算其概率,是一个上下文有关模型。几年来,神经语言模型(Neural language models)使用词汇Embedding来预测,也得到广泛的发展与应用。
在嵌入式ASR中,由于计算资源要留予声学模型,所以语言模型采用的依旧是n-gram的思想。那么在有限的内存中,如何最大化存储语言模型,是嵌入式ASR要解决的问题。
5.发音词典
发音词典,是语音识别的内存条。内存能将硬盘的数据读入,并使用cpu进行运算。同样的,发音词典,能将语言模型的词条序列转化为音素序列,并用声学模型进行分数评估运算。
发音词典是连接声学模型和语言模型的桥梁,他的大小直接影响声学模型和语言模型的发挥空间。
在嵌入式ASR中,发音词典的大小,与语言模型的规模互相共鸣,所以要解决的问题可以与语言模型归为一谈。
6.解码器
解码器,估计这个词的来自英文decoder的直译,笔者认为更恰当的名字应称为识别器。之所以叫解码器,还有另外一个比较形象的原因。以16bit语音数据为例,计算机的存储是一堆我们看不懂的short类型数字,如同密码一般。语音识别能破解这些密码,将明文展示在我们面前。
所以通俗来讲,解码器就是将语音识别各个流程串联的代码工程。一般云端采用与WFST(带权优有限状态自动机)搭档的静态解码器,可以更方便地综合处理语音识别的各个环节。而嵌入式为了节省语言模型的内存开支,采用特定的动态解码器。
03
开始优化这些组件——速度和内存优化
为了优化这些“部件”占用的时间与内存,我们做了一系列工作:
neon计算优化,奇异值分解优化,哈夫曼编码优化。
1.neon优化声学模型计算
neon的计算优化,已是广大工程师们的老生常谈,机器学习相关的T族们更是耳熟能详。在嵌入式ASR引擎中,我们对核心高频运算的函数进行了neon优化,采用了汇编语言进行编写,最终有效提高了25%的计算速度。
接下来,本文现以实现char类型向量乘的介绍优化的实现,分三版本来介绍:
A. 优化前的朴素版
B. neon c版
C. neon汇编版
首先,我们将要实现的函数是:
/**
* 实现两个char类型向量乘
* start_a: 向量A
* start_b: 向量B
* cnt:向量元素个数
* result:向量乘返回存储变量
*/
void vector_product_neon(const char * start_a, const char * start_b, int & result,
const int cnt);
A. 优化前朴素版
void vector_product_neon(const char * start_a, const char * start_b, int & result,
const int cnt) {
int res = 0;
for(int j = 0; j < cnt; j++) {
res += int(*start_a) * int(*start_b);
start_a++;
start_b++;
}
result = res;
}
B. neon c版
Neon寄存器能实现128位空间的并行运算,对于char类型的向量乘而言,两两相乘的结果在short类型范围内,故可8个为一组实现。以下代码,8个元素一组,一次循环处理两组。在我们的深度学习运算中,隐层的向量长度保证为16倍数,实现代码如下:
void vector_product_neon(const char * start_a, const char * start_b, int & result,
const int cnt) {
int res = 0;
int32x4_t neon_sum = vdupq_n_s32(0);
int8x8_t neon_vector1;
int8x8_t neon_vector2;
for(int j = 0; j < cnt / 16; j++) {
neon_vector1 = vld1_s8((char *)start_a);
neon_vector2 = vld1_s8((char *)start_b);
int16x8_t neon_tmp = vmull_s8(neon_vector1, neon_vector2);
start_a += 8;
start_b += 8;
neon_vector1 = vld1_s8((char *)start_a);
neon_vector2 = vld1_s8((char *)start_b);
neon_tmp = vmlal_s8(neon_tmp, neon_vector1, neon_vector2);
neon_sum = vaddw_s16(neon_sum, vget_low_s16(neon_tmp));
neon_sum = vaddw_s16(neon_sum, vget_high_s16(neon_tmp));
start_a += 8;
start_b += 8;
}
for(int j = 0; j < 4; j++)
res += vgetq_lane_s32(neon_sum, j);
result = res;
}
C. neon汇编版
汇编版本的neon代码编写与维护成本高,但速度比c版本更快。秉着精益求精的态度,我们实现了汇编代码:
void vector_product_neon(const char * start_a, const char * start_b, int & result,
const int cnt) {
int res = 0;
asm volatile(
"vmov.s32 q2, #0" "\n\t"
"lsr %[cnt], %[cnt], #4" "\n\t"
".charloop:"
"vld1.s8 {d0}, [%[vec1]]!" "\n\t"
"vld1.s8 {d1}, [%[vec2]]!" "\n\t"
"vmull.s8 q1, d0, d1" "\n\t"
"vld1.s8 {d0}, [%[vec1]]!" "\n\t"
"vld1.s8 {d1}, [%[vec2]]!" "\n\t"
"vmlal.s8 q1, d0, d1" "\n\t"
"vaddw.s16 q2, q2, d2" "\n\t"
"vaddw.s16 q2, q2, d3" "\n\t"
"subs %[cnt], %[cnt], #1" "\n\t"
"bne .charloop" "\n\t"
"vadd.s32 d4, d4, d5" "\n\t"
"vmov.s32 r4, d4[0]" "\n\t"
"add %[sum], r4" "\n\t"
"vmov.s32 r4, d4[1]" "\n\t"
"add %[sum], r4" "\n\t"
: [sum]"+r"(res)
: [vec1]"r"(start_a),
[vec2]"r"(start_b),
[cnt]"r"(cnt)
: "r4", "cc", "memory"
);
result = res;
}
2.奇异值分解优化声学模型运算量
为了降低乘加运算的次数,我们决定利用奇异值分解来对DNN进行重构,通过裁剪掉最小的奇异值及其相对应的特征向量,来达到减少乘加运算数量的目标。奇异值分解将任意矩阵Wm×n(不失一般性,假设m≤n)分解成3个矩阵相乘:Wm×n =Um×mΣm×mVm×n。
其中:Σm×m 为对角矩阵,即Σm×m =diag(σ1,σ2,…,σm),它的对角元素即为Wm×n的奇异值;Um×m 为单位正交矩阵,其列向量为与奇异值对应的特征向量;Vm×n中的行向量是互相单位正交的,也是与奇异值对应的特征向量。
下图是我们以DNN模型其中一层网络作为例子,阐述我们在重构DNN中的模型转化,其中原始DNN模型为图中上方子图(a),新重构DNN模型在下方子图(b)所示:
a:原始DNN模型的一层结构
(b)新DNN模型的两层对应结构
利用SVD对声学模型计算量优化大致分为3个步骤
(1)训练初始DNN神经网络;
(2)对权重矩阵进行奇异值分解;
(3)对重构后的DNN模型重新训练。
通过基于SVD的模型压缩方法,我们可以在稍微降低模型性能的前提下,将声学模型计算量减少30%。
3.哈夫曼优化语言模型内存
一般地,n-gram语言模型可以用一张有向图存储便于介绍存储空间以及快速查询,这张图上的边要存储词汇信息。我们知道以汉语为例,不同词语的出现频率相差极大,如果所有词汇的label id都用int类型存储,那空间的利用率较为低下。
以“我”“要”“吃饭”为例,假设语言模型的词汇频率:我>要>吃饭,那么我们可以构建图3的哈夫曼树,则四个字使用的编号码分别为:我(0),要(10),吃饭(110)
二叉哈夫曼
十六叉哈夫曼树
然而,采用图4的二叉树数据结构,一次只能处理1bit效率较低,也不便于工程实现。所以在工程实现的时候,我们按4bits编码为单位,对词汇进行分类存储处理。
我们使用一棵16叉树的哈夫曼树结构,每层树节点的编号总量是上一层的16倍。树中的所有编号为0的子节点用于储存词汇,越高频的词汇储存于深度越低的节点位置。
通过哈夫曼优化,我们的引擎最终成功降低了25%的内存占用,同时引擎是资源文件也得到50%左右的优化。
04
识别性能的优化
1.基于TDNN优化声学模型
近几年,TDNN(Time-Delay Neural Network,延时神经网络)【5】的拓扑结构被应用于语音识别。事实上,该结构于1989年被提出,随着近几年技术的发展,重新进入了大家的视线。
DNN结构
DNN的拓扑网络仅针对单一特征时刻点建模。
TDNN结构
TDNN的隐层结构,对语音特征多个时刻点进行抽象建模,拥有更强的建模能力。除此之外,TDNN结构的多时刻建模参数是共享的(图中红、绿、紫用的是同样的拓扑矩阵传播)。
所以,TDNN虽然在训练的时候,比DNN需要更多的BP运算。而在语音识别时,由于参数共享的原因,隐层的计算结果可以复用,每一帧仅需对所有参数进行一次运算,大大节省了计算量。最后,我们基于TDNN结构,引擎在保持计算量一致的前提下,识别率提升了相对20%的准确率。
2.基于多任务训练优化性能
采用多任务联合训练,能有效提高声学训练的鲁棒性,避免过早陷入局部最优。在嵌入式的模型中,模型输出目标比较少,训练容易陷入局部最优。所以我们,同时用目标多的大模型联合训练,让训练的隐层结构更为鲁棒。
声学模型多任务训练
在训练的时候,我们网络同时拥有输出1和输出2两个,多任务训练时,逆向迭代需要残差协调,我们采用以下公式分配残差,其中λ权衡两个模型的训练权重:
最终我们采用多任务训练优化性能,对语音识别率带来了一定提升,接下来所有的性能提升我们将在下一章结实验给出。
3.基于区分性训练(Discriminative Training)性能优化
声学模型区分性训练是针对MLE训练的不足而提出的。DT训练通常定义一个目标函数(Objective Function),或者说是准则函数(Criterion Function),来近似一个与分类代价相关的度量。通过区分性训练,我们可以从一定程度上弱化模型假设错误所带来的影响。
同时,由于区分性训练致力于优化与识别效果好坏相关的度量,因此也就为提高识别器性能提供了更直接的途径。形象的说,MLE训练告诉模型“这是椅子,那是桌子”,而区分性训练则告诉模型“这是桌子而不是椅子,那是椅子而不是桌子”。MLE训练更重视调整模型参数以反映训练数据的概率分布,而区分性训练则更重视调整模型之间的分类面,以更好的根据设定的准则对训练数据进行分类。
DT的目标函数是这样的:
对DT的目标函数用一次贝叶斯公司可以得到:
分子正是ML的目标函数;而分母则是所有文本(包括训练文本和它的所有竞争者)产生训练语音的概率的(按语言模型加权的)和。由于分母上要枚举所有可能的文本并不现实,所以实际中,一般是用一个已有的ML训练的语音系别系统对训练语音做一次解码,得到n-best list或lattice,用这里面的文本来近似分母上的求和。n-best list或lattice中包含了训练文本的足够接近的竞争者。
4.基于互信息的新词发现
对于语音识别系统来说,语言模型对结果影响至关重要;而对于语言模型来讲,语言模型的词典是关键。一个好的分词词典,对于得到鲁棒的语言模型是至关重要的,如果才能选出合理正确的“词”所组成的词典,首先最关键的一步就是基于现有语料的新词挖掘。
由于嵌入式系统性能有限,因此选择合适大小的词表,并对语言模型进行适当剪枝头,可以压缩安装包大小、限制内存消耗、提高识别性能。压缩词表可以筛选高频词,并通过一定的模型来识别筛掉截断词,如“新功”、“嘉年”、“扛生”、“鹅卵”、“刘德”、“利亚”等半个高频词。
一个简单而又有效的新词发现和筛选方案可以采用互信息和左右信息熵的计算方法,计算二元的信息熵的分数由三个对应部分组成: 1)点间互信息:点间互信息越高,内部聚合程度越高; 2)两个单词片段信息熵 h_r_l 和 h_l_r 的最小值:这个数值越大,则意味着两个单词一起出现的可能性越小; 3)单词左右信息熵的最小值:这个数值越大就表示着候选词出现的语境越多,越有可能成词因此,分数越高表示成词的可能性越大。
计算完二元的信息熵后,可以依次计算三元、四元的信息熵,三元的新词发现和筛选是将二元替换原有的两个单字做为一个单字继续进行,候选集可以取左信息熵或者右信息熵为0的候选集,四元、五元以此类推。 另外,语言模型直接关系到识别结果输出,因此选与应用场景相对应的语料进行统计尤为重要。
05
实验对比
第二章节和第三章节,介绍了一些我们完成的工作,本章节将分为两部分。首先,我们通过实验对比验证工作的成果。其次,我们将引擎和行业竞品进行对比。
工作成果验证
目前总共有6个通用测试集,测试集大小分别为1220、6917、4069、2977、2946、2500条语音。其中测试集1是手机录制测试集,集2是命令类的录音,集3是麦克风录音涉及一般生活情景,4、5、6集都是线上实网数据,区别是 集4、5背景比较干净,集6背景带噪。
测试集
DNN
TDNN
TDNN优化版
1
10.4
8
6.9
2
13.7
11.3
9.3
3
22.9
18.3
15.6
4
15.8
13.3
12
5
15.3
12.2
10.5
6
22.6
20.3
17.8
在模型选取对比,我们针对DNN、TDNN、以及TDNN优化版(优化内容为第三章的2、3、4小结内容),总共设计出三个不同版本的嵌入式语音识别引擎进行对比。
三个版本的嵌入式语音识别引擎在6个通用测试集上的实验结果如表中所示。表中的数字表示字错误率,即100个字里面识别错字的数量。总体来看,TDNN对识别率带来了20%左右的提升,其他工作也带来了10%左右的提升。
从语音识别的基本概念,到语音识别速度和内存优化的介绍,以及沉淀的一些算法研究、实验结果验证,本文大体讲述了语音识别从原理到实践的基本过程。欢迎同样从事语音AI识别的小伙伴加入我们~
此文已由作者授权腾讯云+社区发布,更多原文请点击
搜索关注公众号「云加社区」,第一时间获取技术干货,关注后回复1024 送你一份技术课程大礼包!
海量技术实践经验,尽在云加社区!
信号为E时,如何让语音识别脱“网”而出?的更多相关文章
- 类似智能购票的demo--进入页面后默认焦点在第一个输入框,输入内容、回车、right时焦点自动跳到下一个,当跳到select时,下拉选项自动弹出,并且可以按上下键选择,选择完成后再跳到下一个。
要实现的效果:进入页面后默认焦点在第一个输入框,输入内容.回车.right时焦点自动跳到下一个,当跳到select时,下拉选项自动弹出,并且可以按上下键选择,选择完成后再跳到下一个. PS:自己模拟的 ...
- PyQt Designer中连接信号和槽时为什么只能连接控件自己的信号和槽函数?
老猿在学习ListView组件时,想实现一个在ListView组件中选中一个选择项后触发消息给主窗口,通过主窗口显示当前选中的项的内容. 进入QtDesigner后,设计一个图形界面,其中窗口界面使用 ...
- PyQt(Python+Qt)学习随笔:调用disconnect进行信号连接断开时的信号签名与断开参数的匹配要求
老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 在使用信号调用disconnect()方法断开信号和槽的连接时,信号可以带签名也可不带签名,参数可以 ...
- 面试时,问哪些问题能试出一个 Android 应用开发者真正的水平?【转自知乎】
这几年面过的各种Android开发也有三位数了,failed的不敢说,pass的基本都没有看走眼,来得晚了也想说说我的体会. 一般面试时间短则30分钟,多则1个小时,这么点时间要全面考察一个人难度很大 ...
- 面试时,问哪些问题能试出一个Android应用开发者真正的水平?
一般面试时间短则30分钟,多则1个小时,这么点时间要全面考察一个人难度很大,需要一些技巧,这里我不局限于回答题主的问题,而是分享一下我个人关于如何做好Android技术面试的一些经验: 面试前的准备 ...
- Item 22: 当使用Pimpl机制时,在实现文件中给出特殊成员函数的实现
本文翻译自<effective modern C++>,由于水平有限,故无法保证翻译完全正确,欢迎指出错误.谢谢! 博客已经迁移到这里啦 如果你曾经同过久的编译时间斗争过,那么你肯定对Pi ...
- [随想感悟] 面试时,问哪些问题能试出一个 Android 应用开发者真正的水平?【转自知乎】
这几年面过的各种Android开发也有三位数了,failed的不敢说,pass的基本都没有看走眼,来得晚了也想说说我的体会. 一般面试时间短则30分钟,多则1个小时,这么点时间要全面考察一个人难度很大 ...
- Maven打包时出现“Show Console View”错误弹出框,错误详情为“An internal error has occurred. java.lang.NullPointerException”的解决方法
今天为项目打包时出现了下面的错误提示: 打开Details里面写的是“An internal error has occurred. java.lang.NullPointerException”.在 ...
- JavaScript实现在textbox输入时自动去数据库匹配并找出类似值列出,选择后记得将值填入本textbox及下一个textbox
1. <script src='<%= Application["rootURL"] %>JS/jquery-1.4.1.min.js' type="t ...
随机推荐
- 《笨方法学Python》加分题6
types_of_people = 10 x = f"There are {types_of_people} types of peoples." binary = "b ...
- Linux 给文件夹或者文件增加权限
chmod -R 777 文件夹参数-R是递归的意思777表示开放所有权限 chmod 777 test.sh chmod +x 某文件 如果给所有人添加可执行权限:chmod a+x 文件名:如果给 ...
- C++ 提取网页内容系列之二
标 题: C++ 提取网页内容系列作 者: itdef链 接: http://www.cnblogs.com/itdef/p/4171203.html 欢迎转帖 请保持文本完整并注明出处 另外一种下载 ...
- leveldb 学习记录(五)SSTable格式介绍
本节主要记录SSTable的结构 为下一步代码阅读打好基础,考虑到已经有大量优秀博客解析透彻 就不再编写了 这里推荐 https://blog.csdn.net/tankles/article/det ...
- Delphi 域名解析为IP地址
花生壳:1.LJSZForm-Lable1-Caption改成 “IP地址或域名:”2.LJSZForm-BitBtn1Click-注释掉--else if IsIP(Trim(IPEdit.Text ...
- SpringMVC 学习 十一 springMVC控制器向jsp或者别的控制器传递参数的四种方法
以后的开发,大部分是发送ajax,因此这四种传递参数的方法,并不太常用.作为了解吧 第一种:使用原生 Servlet 在控制器的响应的方法中添加Servlet中的一些作用域:HttpRequestSe ...
- mui.fire()触发自定义事件
导读:添加自定义事件监听操作和标准js事件监听类似,可直接通过window对象添加,通过mui.fire()方法可触发目标窗口的自定义事件. 监听自定义事件 添加自定义事件监听操作和标准js事件监听类 ...
- ABP框架系列之十五:(Caching-缓存)
Introduction ASP.NET Boilerplate provides an abstraction for caching. It internally uses this cache ...
- 13. The Impact of New Technology on Business 新科技对商务的影响
13. The Impact of New Technology on Business 新科技对商务的影响 (1) New technology links the world as never b ...
- TensorFlow基本--张量
在TensorFlow中所有的数据都通过张量的形式表示,从功能上看张量可以被简单的理解为多维数据,其中零阶张量表示标量(一个数),第一阶张量为向量(一个一维数组),第n阶向量可以理解为一个n维数组. ...