本期分享来自 MindSpore 社区的龙泳旭同学带来的项目经验:基于MindSpore,使用DFCNN和CTC损失函数的声学模型实现。

项目信息

项目名称

《基于MindSpore,使用DFCNN和CTC损失函数的声学模型实现》

方案描述

本项目的目标是使用MindSpore实现DFCNN+CTC的声学模型,将一句语音转化成一张特定模式的图像作为输入,然后通过DFCNN+CTC结构,对整句语音进行建模,实现输出单元直接与最终的识别结果(音节)相对应。

项目背景

自动语音识别(ASR)技术的目的是让机器能够"听懂"人类的语音,将人类语音信息转化为可读的文字信息,是实现人机交互的关键技术,也是长期以来的研究热点。最近几年,随着深度神经网络的应用,加上海量大数据的使用和云计算的普及,语音识别取得了突飞猛进的进展,在多个行业突破了实用化的门槛,越来越多的语音技术产品进入了人们的日常生活,包括苹果的Siri、亚马逊的Alexa、讯飞语音输入法、叮咚智能音箱等都是其中的典型代表。

......

20世纪80年代后期,深度神经网络(deep neural network,DNN)的前身— — 人工神经网络(artificial neural network,ANN)也成为了语音识别研究的一个方向。科大讯飞在2016年提出了一种全新的语音识别框架,称为全序列卷积神经网络(deep fully convolutional neural network,DFCNN)。实验证明,DFCNN 比BLSTM 语音识别系统这个学术界和工业界最好的系统识别率提升了15%以上。

——王海坤,潘嘉,刘聪.语音识别技术的研究进展与展望[J].电信科学,2018(2):1-11.

DFCNN的模型结构图大概如下所示:

DFCNN先对时域的语音信号进行傅里叶变换得到语音的语谱图,DFCNN直接将一句语音转化成一张图像作为输入,输出单元则直接与最终的识别结果(比如音节或者汉字)相对应。DFCNN的结构中把时间和频率作为图像的两个维度,通过结合较多的卷积层和池化(pooling)层,构成比较深的神经网络,实现对整句语音的建模。

正如上面所说,我们建立DFCNN模型,是把语谱图视作带有特定模式的图像,而有经验的语音学专家能够从中看出里面说的内容,于是我们便想将机器训练成为这么一个“专家”。

那么,为什么需要构建一个那么深的神经网络呢?

#

从输入端看

传统语音识别系统的提取特征方式是在傅里叶变换后用各种类型的人工设计的滤波器,比如Log Mel-Filter Bank,造成在语音信号频域信息损失比较明显。另外,传统语音特征采用非常大的帧移来降低运算量,导致时域上的信息会有损失,当说话人语速较快的时候, 这个问题表现得更为突出。而借鉴了计算机视觉领域加深网络的指导思想,DFCNN通过加深模型,保证了语音的长时相关性,能够看到足够长的历史与未来的信息,因此在顽健性上表现得更好。

#

从输出端看

DFCNN也能比较灵活地与其他模型进行接合,

例如:连接时序分类模型

(connectionist temporal classification,CTC),

以实现端到端的模型训练。

而本项目正是希望借用Mindspore进行实现。

Mindspore 简介

深度学习是近年来发展得比较快的一个领域,国外的公司诸如谷歌(Google)和脸书(Facebook)都分别推出了自己的开源深度学习框架,分别是采用静态计算图的Tensorflow与采用动态计算图的Pytorch,受到了广大的欢迎与应用。国内由华为公司推出的MindSpore框架则结合了动静计算图,发挥了二者的优势。由于Tensorflow与Pytorch出现的时间更早、社区更完善,MindSpore在基于DFCNN+CTC模型的实现任务上并没有具体的实现。但多亏于MindSpore框架底层API的完整、高效与简明,使得实现这一任务成为可能。

Mindspore 生态

MindSpore作为全球AI开源社区

(https://gitee.com/mindspore/mindspore),

致力于进一步开发和丰富AI软硬件应用生态。

Mindspore 技术特点

#

自动微分

当前主流深度学习框架中有三种自动微分技术:

基于静态计算图的转换:

编译时将网络转换为静态数据流图,将链式法则

应用于数据流图,实现自动微分。

基于动态计算图的转换:

记录算子过载正向执行时网络的运行轨迹,对动

态生成的数据流图应用链式法则,实现自动微分。

基于源码的转换:

该技术是从功能编程框架演进而来,以即时编译

(Just-in-time Compilation,JIT)的形式对中

间表达式(程序在编译过程中的表达式)进行自

动差分转换,支持复杂的控制流场景、高阶函数

和闭包。

TensorFlow早期采用的是静态计算图,PyTorch采

用的是动态计算图。静态映射可以利用静态编译技

术来优化网络性能,但是构建网络或调试网络非常

复杂。动态图的使用非常方便,但很难实现性能的

极限优化。

MindSpore找到了另一种方法,即基于源代码转换

的自动微分。一方面,它支持自动控制流的自动微

分,因此像PyTorch这样的模型构建非常方便。

另一方面,MindSpore可以对神经网络进行静态编

译优化,以获得更好的性能。

MindSpore自动微分的实现可以理解为程序本身的

符号微分。MindSpore IR是一个函数中间表达式,

它与基础代数中的复合函数具有直观的对应关系。

复合函数的公式由任意可推导的基础函数组成。

MindSpore IR中的每个原语操作都可以对应基础

代数中的基本功能,从而可以建立更复杂的流控制。

#

自动并行

MindSpore自动并行的目的是构建数据并行、模型

并行和混合并行相结合的训练方法。该方法能够自

动选择开销最小的模型切分策略,实现自动分布并

行训练。

目前MindSpore采用的是算子切分的细粒度并行策

略,即图中的每个算子被切分为一个集群,完成并

行操作。在此期间的切分策略可能非常复杂,但

是作为一名Python开发者,您无需关注底层实现,

只要顶层API计算是有效的即可。

快速开始

相信很多同学在进行机器学习时,用的比较多的是诸如Pytorch、Tensorflow和Keras等框架,对于Mindspore的使用,是比较陌生的。但是只要心里有机器学习的基本流程,我相信借助于Mindspore官方文档

(https://www.mindspore.cn/docs/programming_guide/zh-CN/r1.3/index.html)的帮助,与Model_Zoo

(https://gitee.com/mindspore/mindspore/tree/master/model_zoo)

参考案例的指导,从其他框架迁移快速适应过来是不成问题的。

我将该项目核心的实现大致分为如下两步走:搭建模型->训练流程

搭建模型

Mindspore的API算子与Pytorch的API基本比较类似,而且有映射表(https://www.mindspore.cn/docs/note/zh-CN/r1.3/index.html#operator_api)

以进行参考,因此我可以比较快写出DFCNN的模型结构:

class DFCNN(nn.Cell):"""DFCNN model"""def __init__(self, num_classes, input_nc = 1, padding=1, pad_mode='pad', has_bias = False, use_dropout = False):super(DFCNN,self).__init__()if pad_mode=='pad':assert padding>=0,"when the pad_mode is 'pad', the padding must be greater than or equal to 0!"if pad_mode=='same' or pad_mode=='valid':assert padding==0,"when the pad_mode is 'same' or 'valid', the padding must be equal to 0!"self.use_dropout = use_dropout# structure# seq 1self.conv11 = nn.Conv2d(in_channels=input_nc, out_channels=64,kernel_size=3, stride=1, padding=padding, has_bias=has_bias,pad_mode=pad_mode)self.bn11 = nn.BatchNorm2d(64)self.relu11 = nn.ReLU()self.conv12 = nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, stride=1, padding=padding, has_bias=has_bias, pad_mode=pad_mode)self.bn12 = nn.BatchNorm2d(64)self.relu12 = nn.ReLU()self.maxpool1 = nn.MaxPool2d(kernel_size=2, stride=2, pad_mode='valid')# seq 2self.conv21 = nn.Conv2d(in_channels=64,out_channels=128,kernel_size=3, stride=1,padding=padding, has_bias=has_bias,pad_mode=pad_mode)self.bn21 = nn.BatchNorm2d(128)self.relu21 = nn.ReLU()self.conv22 = nn.Conv2d(in_channels=128, out_channels=128, kernel_size=3, stride=1, padding=padding, has_bias=has_bias, pad_mode=pad_mode)self.bn22 = nn.BatchNorm2d(128)self.relu22 = nn.ReLU()self.maxpool2 = nn.MaxPool2d(kernel_size=2, stride=2, pad_mode='valid')# seq 3self.conv31 = nn.Conv2d(in_channels=128,out_channels=256,kernel_size=3, stride=1,padding=padding, has_bias=has_bias,pad_mode=pad_mode)self.bn31 = nn.BatchNorm2d(256)self.relu31 = nn.ReLU()self.conv32 = nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, stride=1, padding=padding, has_bias=has_bias, pad_mode=pad_mode)self.bn32 = nn.BatchNorm2d(256)self.relu32 = nn.ReLU()self.conv33 = nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, stride=1, padding=padding, has_bias=has_bias, pad_mode=pad_mode)self.bn33 = nn.BatchNorm2d(256)self.relu33 = nn.ReLU()self.maxpool3 = nn.MaxPool2d(kernel_size=2, stride=2, pad_mode='valid')# seq 4self.conv41 = nn.Conv2d(in_channels=256,out_channels=512,kernel_size=3, stride=1,padding=padding, has_bias=has_bias,pad_mode=pad_mode)self.bn41 = nn.BatchNorm2d(512)self.relu41 = nn.ReLU()self.conv42 = nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=padding, has_bias=has_bias, pad_mode=pad_mode)self.bn42 = nn.BatchNorm2d(512)self.relu42 = nn.ReLU()self.conv43 = nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=padding, has_bias=has_bias, pad_mode=pad_mode)self.bn43 = nn.BatchNorm2d(512)self.relu43 = nn.ReLU()self.maxpool4 = nn.MaxPool2d(kernel_size=1, stride=1, pad_mode='valid')# seq 5self.conv51 = nn.Conv2d(in_channels=512,out_channels=512,kernel_size=3, stride=1,padding=padding, has_bias=has_bias,pad_mode=pad_mode)self.bn51 = nn.BatchNorm2d(512)self.relu51 = nn.ReLU()self.conv52 = nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=padding, has_bias=has_bias, pad_mode=pad_mode)self.bn52 = nn.BatchNorm2d(512)self.relu52 = nn.ReLU()self.conv53 = nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=padding, has_bias=has_bias, pad_mode=pad_mode)self.bn53 = nn.BatchNorm2d(512)self.relu53 = nn.ReLU()self.maxpool5 = nn.MaxPool2d(kernel_size=1, stride=1, pad_mode='valid')self.bn = nn.BatchNorm2d(512)if self.use_dropout:self.drop1 = nn.Dropout(0.8)self.drop2 = nn.Dropout(0.8)self.drop3 = nn.Dropout(0.8)self.drop4 = nn.Dropout(0.8)self.drop5 = nn.Dropout(0.8)self.drop_fc1 = nn.Dropout(0.5)self.drop_fc2 = nn.Dropout(0.5)self.fc1 = nn.Dense(25 * 512, 4096, activation='relu')self.fc2 = nn.Dense(4096, 4096, activation='relu')self.fc3 = nn.Dense(4096, num_classes, activation='relu')

训练流程

熟悉Pytorch的朋友都知道,其训练流程基本上是参考这样一个套路来的:

(https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html#sphx-glr-beginner-blitz-cifar10-tutorial-py)

for epoch in range(2):  # loop over the dataset multiple timesrunning_loss = 0.0for i, data in enumerate(trainloader, 0):# get the inputs; data is a list of [inputs, labels]inputs, labels = data# zero the parameter gradientsoptimizer.zero_grad()# forward + backward + optimizeoutputs = net(inputs)loss = criterion(outputs, labels)loss.backward()optimizer.step()# print statisticsrunning_loss += loss.item()if i % 2000 == 1999:    # print every 2000 mini-batchesprint('[%d, %5d] loss: %.3f' %(epoch + 1, i + 1, running_loss / 2000))running_loss = 0.0print('Finished Training')

而在使用Mindspore时,实则是需要通过mindspore.Model.train接口进行训练(参考)

(https://www.mindspore.cn/tutorials/zh-CN/master/quick_start.html):

model.train(epochs,train_loader,callbacks=callbacks,dataset_sink_mode=False)

当我第一次使用时,是有很多疑问的:如果我需要使用自定义的损失函数呢?如果我需要更加灵活的数据加载类呢?

参考了官方教程和Model Zoo,我才明白Mindspore的思路,即是将训练流程也作一个Cell来处理,一切的安排都在construct中做。按照这个思路出发,我自定义实现了一个训练网络:

class WithLossCell(nn.Cell):def __init__(self, backbone, loss_fn):super(WithLossCell, self).__init__(auto_prefix=False)self._backbone = backboneself._loss_fn = loss_fndef construct(self, img, label_indices, text, sequence_length):model_predict = self._backbone(img)return self._loss_fn(model_predict, label_indices, text, sequence_length)@propertydef backbone_network(self):return self._backbonedef predict(self, img):return self._backbone(img)

那么,在我有自定义损失函数的需求时,只需将其传入到WithLossCell再封装到Model即可:

# 模型
net = DFCNN(num_classes=len(label2idx), padding=padding, pad_mode=pad_mode ,has_bias=has_bias, use_dropout=use_dropout)
......
# 损失函数
criterion = CTCLoss()
......
# “打包”到一个Cell中
net = WithLossCell(net, criterion)
......
# 封装到Model中
model = Model(net)
......
# 进行训练
model.train(epochs,train_loader,callbacks=callbacks,dataset_sink_mode=False)

事实上在项目的深入过程中,很多需求也是接踵而至的,这些细节若是读者有兴趣,可以参考此处

(https://gitlab.summer-ospp.ac.cn/summer2021/210610338)。

问题处理经验分享

问题出现的背景:

在搭建好模型、训练网络之后,在进行实际运行时候,出现了loss值一直震荡的问题,或许这也即是机器学习中经常让人头疼的问题。

问题解决的途径:

接触过机器学习的开发者或许很大一部分人都或多或少地碰到过相似的问题。正如Mindspore官方文档里所说:

(https://www.mindspore.cn/docs/programming_guide/zh-CN/r1.3/accuracy_optimization.html)

模型精度问题和一般的软件问题不同,定位周期一般也更长。在通常的程序中,程序输出和预期不符意味着存在bug(编码错误)。但是对一个深度学习模型来说,模型精度达不到预期,有着更复杂的原因和更多的可能性。由于模型精度要经过长时间的训练才能看到最终结果,定位精度问题通常会花费更长的时间。

那么我根据文档与以往的经验,我从以下几个点来排除问题:

  • 检查超参数设置

  • 检查输入数据

  • 检查模型结构

为了能可视化地展示做出的调整是否有效果,我引入了Mindspore的配套工具:MindInsight(https://www.mindspore.cn/mindinsight/)。
这个工具十分强大,可以很好帮我定位问题、测验效果。通过这个工具,可以看到经过比较长时间的训练,我们网络确实存在着loss震荡的问题:

那么,接下来我便按照上述的几点问题进行修订,排除问题。

#

调整超参数

有时候一些bug往往是最简单的问题所导致的,

因此我打算从最简单的方向先进行试探。通过

实验不同的batch-size、学习率等等超参数,

loss值仍表现为震荡。因此可以暂时排除这个

问题。

#

检查数据输入

数据是模型训练的一个重要组成部分,如果数据

出现问题,模型也是必然出现问题的。因此,我

在网络前向传播前,利用matlibplot库输出进

入的张量的图形,进行检查:

基本可以判定进入网络的语谱图是正常处理的,因此也暂时排除了这个问题。

#

调整模型结构

问题出在模型上是对我而言,比较困难的一个方

向了。我试着从加深、加宽模型、调整参数初始

化等方式对模型进行修改,但loss值都表现出了

震荡。最终在一些机缘巧合上,我想从损失函数

上找问题。我认为可能是我对Mindspore官方文

档上对CTCLoss损失函数的介绍理解不够深刻,

导致我的模型没有办法很好的配合这个接口。于

是我开始阅读Mindspore的源码,试图从其中寻

找答案。终于在Mindspore底层的C++实现的

mindspore/ccsrc/backend/kernel_

compiler/cpu/ctcloss_cpu_

kernel.cc文件中找到了答案,在该文件的307

行,可以看到其对输入进行了一次Softmax处理。

void CTCLossCPUKernel::LaunchKernel(const std::vector<AddressPtr> &inputs, const std::vector<AddressPtr> &outputs) {......for (size_t b = 0; b < batch_size_; ++b) {std::vector<uint32_t> label_with_blank = labels_with_blank[b];// y_b [num_class, sequence_length]std::vector<std::vector<T>> y_b;std::vector<std::vector<T>> dy;std::vector<std::vector<T>> log_alpha_b;std::vector<std::vector<T>> log_beta_b;MatrixfromVector(num_class_, sequence_length_addr[b], &y_b, kLogZero_);MatrixfromVector(y_b.size(), y_b[0].size(), &dy, T(0));MatrixfromVector(label_with_blank.size(), sequence_length_addr[b], &log_alpha_b, kLogZero_);MatrixfromVector(label_with_blank.size(), sequence_length_addr[b], &log_beta_b, kLogZero_);//<<<<<<<<<<<<<<<<<<<<<<<<< 307 行 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<   InnerSoftMax(inputs_addr, &y_b, sequence_length_addr[b], num_class_, batch_size_, b);//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>CalculateFwdVar(label_with_blank, y_b, &log_alpha_b);CalculateBwdVar(label_with_blank, y_b, &log_beta_b);T log_pzx = kLogZero_;for (size_t u = 0; u < label_with_blank.size(); ++u) {log_pzx = LogSumExp(log_pzx, log_alpha_b[u][0] + log_beta_b[u][0]);}loss_addr[b] = -log_pzx;CalculateGrad(label_with_blank, y_b, log_alpha_b, log_beta_b, log_pzx, &dy);for (size_t t = 0; t < sequence_length_addr[b]; ++t) {for (size_t c = 0; c < num_class_; ++c) {gradient_addr[t * batch_size_ * num_class_ + b * num_class_ + c] = dy[c][t];}}}
}
.......
class DFCNN(nn.Cell):"""DFCNN model"""......def construct(self, x):x = self.feature(x) # [batch, 256, 200, 25] -> [batch, channels, max_time, fq]x = self.bn(x)x = x.transpose(0,2,1,3) # [batch, channels, max_time, fq] -> [batch, max_time, channels, fq]x = x.reshape(-1, x.shape[1], x.shape[2] * x.shape[3]) # [batch, max_time, channels*fq]x = self.fc1(x)if self.use_dropout:x = self.drop_fc1(x)x = self.fc2(x)if self.use_dropout:x = self.drop_fc2(x)x = self.fc3(x)#>>>>>>>>> 删除该层 >>>>>>>>>>>>x = self.softmax(x)#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<return x....

于是我不禁怀疑是否是我做了多余的操作,而使得损失函数计算不正常。于是我便将这一层删去了。最终,loss曲线表现出了正常的走势:

问题得以解决。

后续成果

项目代码已合入社区。

http://gitee.com/mindspore/course/tree/master/dfcnn

MindSpore官方资料

官方QQ群 : 486831414

官网:https://www.mindspore.cn/

Gitee : https : //gitee.com/mindspore/mindspore

GitHub : https://github.com/mindspore-ai/mindspore

论坛:https://bbs.huaweicloud.com/forum/forum-1076-1.html

项目经验分享:基于昇思MindSpore,使用DFCNN和CTC损失函数的声学模型实现相关推荐

  1. 基于昇思MindSpore Quantum,实现量子虚时演化算法

    01.关于昇思MindSpore项目介绍 1.项目名称 基于昇思MindSpore Quantum,实现量子虚时演化算法 2.项目链接 https://summer-ospp.ac.cn/#/org/ ...

  2. 基于昇思MindSpore的同元软控AI系列工具箱正式发布,大幅度降低产品研发成本

    随着智能时代的到来,同元软控与华为携手合作,以昇思MindSpore为框架底座,打造了MWORKS AI工具箱,并于2023年1月8号正式对外发布.基于昇思MindSpore的MWORKS AI工具箱 ...

  3. 项目经验分享:基于昇思MindSpore实现手写汉字识别

    项目信息Program Info 项目要求 基于MindSpore的实现在线手写汉字识别,主要包括手写汉字检测和手写汉字识别,能较准确的对标准字体的手写文字进行识别,识别后通过人工干预对文本进行适当修 ...

  4. 致AI开发者,昇思MindSpore发来“成长”邀请

    撰文 / 张贺飞 编辑 / 沈洁 2020年,应届毕业的蒋倩成了一名算法工程师,因为工作的原因,蒋倩接触到了刚刚开源的昇思MindSpore. 和许多开发者一样,蒋倩对人工智能和开源社区充满了好奇心, ...

  5. 昇思MindSpore全场景AI框架 1.6版本,更高的开发效率,更好地服务开发者

    本文分享自华为云社区<昇思MindSpore全场景AI框架 1.6版本,更高的开发效率,更好地服务开发者>,作者: 技术火炬手. 全新的昇思MindSpore全场景AI框架1.6版本已发布 ...

  6. 智能基座昇腾高校行 | 昇思MindSpore携手清华大学共同培养新时代科技人才

    智能基座昇腾高校行 计算是人类永恒的需求,是智能世界的源动力,人工智能的发展需要数字化人才的助力.在新一轮科技革命背景下,人工智能扮演着重要角色,伴随着产业的快速升级,也对教育的人才培养提出了新要求. ...

  7. AI科学计算领域的再突破,昇思MindSpore做“基石”的决心有多强?

    过去的十多年,人工智能技术越来越深刻地影响了人类社会,越来越多成熟的人工智能产品逐渐渗透到每一个人的生活.就在大家享受着人工智能带来各种便利的同时,AI也不断影响着最前沿的科学研究领域.过去的数百年来 ...

  8. 昇思MindSpore AI框架在知名度与使用率市场份额上处于第一梯队

    2023年2月6日,行业研究机构Omdia(Informa tech集团旗下国际信息与通信技术研究机构)发布了<中国人工智能框架市场调研报告>,深入分析了中国人工智能框架市场的竞争格局,产 ...

  9. 如何加速大模型开发?技术方案拆解来了:昇思MindSpore技术一览

    随着人工智能爆火出圈,狂飙之势从22年底持续到23年初,与以往的技术突破不同的是,此次的大模型不仅被技术界关注,而且备受投资界.产业界和大众消费者的追捧,使它成为历史上最快月活过亿的现象级应用,继而引 ...

最新文章

  1. java jdk 1.8 安装_下载、安装、配置 java jdk1.8
  2. 性能定位常用命令整理
  3. thrift - C#(CSharp)客户端连接池(ConnectionPool)
  4. CSS 伪类与伪元素
  5. Python IDE之Pydev: 基于Eclipse搭建python的编译环境(Eclipse+pydev)简介、安装、使用的详细攻略
  6. fastdfs 一个group内实现按照不同的项目,指定路径存储.
  7. FreeRTOS在STM32F429上移植
  8. [css] sass是怎么定义变量的?
  9. (JAVA)Integer类之基本数据类型之间的转换
  10. 论文浅尝 | How to Keep a Knowledge Base Synchronized
  11. 谷歌在华遭遇首例关键词官司
  12. js文件,同样的路径,拷贝过来的为什么不能访问
  13. C++ 的 allocator类 提供类型化的内存分配以及对象的分配和撤销
  14. navacate连接不上mysql_解决navicat连接不上mysql服务器
  15. 小米手机访问电脑共享文件_小米随身Wifi让手机共享电脑文件教程
  16. QCC3040---uart configuration
  17. PHP之Smarty
  18. 一文搞明白DNS缓存投毒
  19. 取得目录和取得操作系统盘符以及获取系统盘可用空间
  20. Docker容器学习笔记一

热门文章

  1. XMind7主题大变样
  2. 仓库管理系统————QT+SQLite实现
  3. Real6410移植linux-2.6.39.1记录(2)-LCD驱动移植
  4. 软件系统维护是一项不吸引人的工作_“京极接口大师——链接一切软件系统和设备【智能制造吧】...
  5. [国外博士后申请]导师对简历的几点建议
  6. Prezi 中文字体
  7. 什么是长尾关键词751
  8. 什么是TPM? Windows 11 是否会重蹈 Windows Vista 覆辙?
  9. 世界首富比尔·盖茨怎样对待金钱
  10. 苹果电脑的超强文件压缩软件——FastZip