前半部分:中期报告
项目介绍
本次参加了基于MAX78000的智能边缘应用设计大赛,使用的板卡为MAX78000FTHR开发板,非常的小巧,十分难以相信可以在这样一块小小的板子上便可实现与人工智能相关的功能
难得板子是关于边缘开发有关的,若不做点人工智能的东西,还是像单片机开发一样,就有点可惜了。本次项目希望实现通过语音识别来控制led灯的亮灭,并在屏幕上反馈的效果。
上手
由于并没有系统学习过机器学习相关的东西,所以我就先从官方给的例程开始,看看能不能根据官方给的资料,在对例程进行修改,从而实现自己的目标
可以看得出,官方的资料是给的十分俱全的
https://github.com/MaximIntegratedAI/MaximAI_Documentation
上面这个应该是官方资料的总章,其中有介绍了如何上手,这块板子
https://github.com/MaximIntegratedAI/MaximAI_Documentation/blob/master/MAX78000_Feather/README.md
拿到板子后需先对板子上调试工具的固件进行更新
该板子涉及到机器学习训练的部分一定是在linux平台上进行的,而涉及板子上外设开发,烧录调试什么的可以在linux上也可以在windows上进行。这里我选择在linux上进行模型训练,将训练好生成的3个关键文件拿到windows上,再进行下一步开发。
windows
官方的SDK我们可以通过官网下载一个下载工具,使用它便可下载官方的SDK开发包了
将全部下载下来后,可以看到,官方是eclipse为框架进行修改,形成了一个自己的ide工具。我们可以先导入hello world程序,体验下在这个板子上如何进行编译,烧录的。
上图中,两个箭头,分别是调试工具脚本的位置,和需要烧录的程序文件,大家可以根据自己的工具位置和生成的程序的名称来就行了
之后我们可以尝试着导入kws20_demo这个例子,在:\MaximSDK\Examples\MAX78000\CNN\kws20_demo。该例子好像也是官方为我们准备的关于语音识别的例子,我们也对它进行编译并烧录,在使用串口工具打印
效果如上,根据官方readme文档的描述,例程可以分辨出['up', 'down', 'left', 'right', 'stop', 'go', 'yes', 'no', 'on', 'off', 'one', 'two', 'three', 'four', 'five','six', 'seven', 'eight', 'nine', 'zero'] 这20个关键词的,其余的则是分辨为unknown。
好,在windows上单纯的试验使用就这样子了,其实若不涉及到人工智能的话,跟我们平时的单片机开发都差不多的,拿到库函数,面对库函数开发
linux(机器学习,模型训练)
在上面windows的kws20_demo的例程中,我们可以看到这3个文件,官方介绍模型训练后我们所拿到的就是这三个文件,到时候后放到例程里进行替换,再对main函数里进行小修改,就可以实现自己的语音识别功能了
官方建议是我们可以在windows上的wsl进行模型训练,所谓wsl就是适用于Linux的Windows子系统) ,使用它,我们可以非常方便的借用GPU的计算能力(如果有GPU的话),而且因为是在windows上,所以进行文件的移动也十分的方便,也不需要我们安装双系统什么的。
https://blog.csdn.net/u012806787/article/details/123074342
若想使用到GPU的计算能力,加速我们的模型训练(有没有GPU,差别十分大,当然我们可以选择只对少量数据进行模型训练,这样对只用cpu的同学来说也很方便)在我们的windows上得先安装最新的nvidia驱动,在安装好后,在命令框中敲入 nvidia-smi命令
在右上角我们可以获取到我们的电脑上可以支持的最高版本的cuda版本,到时候我们在wsl中安装的cuda版本号就不能高于这个哦(cuda就是显卡厂商NVIDIA推出的运算平台,模型加速会会用到它)。
接下来就是在linux中的操作了,先安装一个PyTorch,因为我们模型是使用PyTorch机型训练的,然后就是按照美信官方的安装一些系列的工具如penv(需要用到它的虚拟库功能),和两个库ai8x-training和ai8x-synthesis,前者与我们模型训练及模型评估相关,后者与量化模型,生成sdk开发中的3个文件相关,十分重要。整个流程跟着下面的文档来就行了,非常的清楚。
https://github.com/MaximIntegratedAI/ai8x-training
https://github.com/MaximIntegratedAI/ai8x-synthesis
sudo apt-get install -y make build-essential libssl-dev zlib1g-dev
sudo apt-get install -y make build-essential libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm
sudo apt-get install -y make build-essential libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev
sudo apt-get install -y make build-essential libsndfile-dev portaudio19-dev
tip:若类似上面这种,一大串的命令给你,却执行不成功,可以尝试一条一条执行,不要一起执行。
既然我们是打算做语音识别相关的,我们就可以重点关注kws20_demo的例子进行学习。
#!/bin/sh
python train.py --epochs 50 --optimizer Adam --lr 0.001 --wd 0 --deterministic --compress policies/schedule_kws20.yaml --model ai85kws20net --dataset KWS_20 --confusion --device MAX78000 "$@"
根据官方所说的,kws20是通过上面这条命令就行模型训练的(在ai8x-training\scripts),其中有用到kws20.py这个脚本文件(在\ai8x-training\datasets)
我们可以打开看看
import errno
import hashlib
import os
import tarfile
import time
import urllib
import warnings
import numpy as np
import torch
from torch.utils.model_zoo import tqdm
from torchvision import transforms
import librosa
import pytsmod as tsm
import ai8x
class KWS:
url = 'http://download.tensorflow.org/data/speech_commands_v0.02.tar.gz'
fs = 16000
class_dict = {'backward': 0, 'bed': 1, 'bird': 2, 'cat': 3, 'dog': 4, 'down': 5,
'eight': 6, 'five': 7, 'follow': 8, 'forward': 9, 'four': 10, 'go': 11,
'happy': 12, 'house': 13, 'learn': 14, 'left': 15, 'marvin': 16, 'nine': 17,
'no': 18, 'off': 19, 'on': 20, 'one': 21, 'right': 22, 'seven': 23,
'sheila': 24, 'six': 25, 'stop': 26, 'three': 27, 'tree': 28, 'two': 29,
'up': 30, 'visual': 31, 'wow': 32, 'yes': 33, 'zero': 34}
class KWS_20(KWS):
def KWS_get_datasets(data, load_train=True, load_test=True, num_classes=6):
"""
Load the folded 1D version of SpeechCom dataset
The dataset is loaded from the archive file, so the file is required for this version.
The dataset originally includes 30 keywords. A dataset is formed with 7 or 21 classes which
includes 6 or 20 of the original keywords and the rest of the
dataset is used to form the last class, i.e class of the others.
The dataset is split into training+validation and test sets. 90:10 training+validation:test
split is used by default.
Data is augmented to 3x duplicate data by random stretch/shift and randomly adding noise where
the stretching coefficient, shift amount and noise variance are randomly selected between
0.8 and 1.3, -0.1 and 0.1, 0 and 1, respectively.
"""
(data_dir, args) = data
transform = transforms.Compose([
ai8x.normalize(args=args)
])
if num_classes in (6, 20):
classes = next((e for _, e in enumerate(datasets)
if len(e['output']) - 1 == num_classes))['output'][:-1]
else:
raise ValueError(f'Unsupported num_classes {num_classes}')
augmentation = {'aug_num': 2}
quantization_scheme = {'compand': False, 'mu': 10}
if load_train:
train_dataset = KWS(root=data_dir, classes=classes, d_type='train',
transform=transform, t_type='keyword',
quantization_scheme=quantization_scheme,
augmentation=augmentation, download=True)
else:
train_dataset = None
if load_test:
test_dataset = KWS(root=data_dir, classes=classes, d_type='test',
transform=transform, t_type='keyword',
quantization_scheme=quantization_scheme,
augmentation=augmentation, download=True)
if args.truncate_testset:
test_dataset.data = test_dataset.data[:1]
else:
test_dataset = None
return train_dataset, test_dataset
def KWS_20_get_datasets(data, load_train=True, load_test=True):
def KWS_get_unquantized_datasets(data, load_train=True, load_test=True, num_classes=6):
def KWS_35_get_unquantized_datasets(data, load_train=True, load_test=True):
datasets = [
{
'name': 'KWS', # 6 keywords
'input': (512, 64),
'output': ('up', 'down', 'left', 'right', 'stop', 'go', 'UNKNOWN'),
'weight': (1, 1, 1, 1, 1, 1, 0.06),
'loader': KWS_get_datasets,
},
{
'name': 'KWS_20', # 20 keywords
'input': (128, 128),
'output': ('up', 'down', 'left', 'right', 'stop', 'go', 'yes', 'no', 'on', 'off', 'one',
'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'zero',
'UNKNOWN'),
'weight': (1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0.14),
'loader': KWS_20_get_datasets,
},
{
'name': 'KWS_35_unquantized', # 35 keywords (no unknown)
'input': (128, 128),
'output': ('backward', 'bed', 'bird', 'cat', 'dog', 'down',
'eight', 'five', 'follow', 'forward', 'four', 'go',
'happy', 'house', 'learn', 'left', 'marvin', 'nine',
'no', 'off', 'on', 'one', 'right', 'seven',
'sheila', 'six', 'stop', 'three', 'tree', 'two',
'up', 'visual', 'wow', 'yes', 'zero'),
'weight': (1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
'loader': KWS_35_get_unquantized_datasets,
},
]
好吧,东西有点多看不太懂,我们可以先对其进行一些小修改,验证我们的猜想(如何更改成自己的功能)如在最下面的name为KWS_20的那一个中括号中,修改output字段为right,left,unknown就这三项,修改weight为1,1,0.6。然后在ai8x-training目录下进入虚拟环境,执行命令进行模型训练。
./scripts/train_kws20.sh
在ai8x-synthesis的scripts/gen_kws20_max78000.sh脚本的作用下生成了所需要的3个文件。我们将其移动windows的kws20_demo的示例中,同时对main文件中的两个部分就行修改
#define NUM_OUTPUTS 2 // number of classes
/* Set of detected words */
const char keywords[NUM_OUTPUTS][10] = {
"right","left","Unknown"};
进行编译并烧录,打开串口,观察它现在确实只能够识别right,left这两个词了,对于其他的词都为unknown。
所以我猜想是不是模型是固定的,只要添加了自己的语音数据,并对其进行训练便可实现自定义的语音别功能。
那就得先去弄到自己的语音数据,既然是控制led灯相关的。那么就搜集“开灯”,“关灯”这两语音数据先。
根据官方所提示的我们需要给该模型的语音数据格式有特定的要求,
- 速率为16000hz
- 为16位小端PCM编码方式
- 单声道
- 文件格式为wav
我们使用audacity这个软件,开源免费
将这两个语音各录制10个,在ai8x-training\data\KWS\raw目录下创建两个文件夹kai,guan,分别放入
同时我们对kws20.py脚本文件就行修改,将KWS_20的中括号部分,修改为如下所示,还有上面KWS类中也做修改
class_dict = {'backward': 0, 'bed': 1, 'bird': 2, 'cat': 3, 'dog': 4, 'down': 5,
'eight': 6, 'five': 7, 'follow': 8, 'forward': 9, 'four': 10, 'go': 11,'guan':12,
'happy': 13, 'house': 14, 'kai':15, 'learn': 16, 'left': 17, 'marvin': 18, 'nine': 19,
'no': 20, 'off': 21, 'on': 22, 'one': 23, 'right': 24, 'seven': 25,
'sheila': 26, 'six': 27, 'stop': 28, 'three': 29, 'tree': 30, 'two': 31,
'up': 32, 'visual': 33, 'wow': 34, 'yes': 35, 'zero': 36}
{
'name': 'KWS_20', # 20 keywords
'input': (128, 128),
'output': ('kai', 'guan','UNKNOWN'),
'weight': (1, 1,0.06),
'loader': KWS_20_get_datasets,
},
重新训练,并生成所需的3分文件
放到kws20_demo例程中,若真如猜想一样,效果应该是能够分辨出我的开灯,及关灯语音
好吧,并没有达到预期的结果
以上便是我的中期报告,根据我想要做的项目,研究了kws20_demo运行流程及模型训练的流程,并对其就行修改验证。最后加入自己的数据,但并未实现期望的功能,感觉是我所添加的语音数据太少所导致,还有1个月,尽量实现它。
后半部分:终期报告
在中期报告后,花了些时间搞明白了如何训练自己的语音数据后,就开始策划该实现怎样的项目了,那就模拟一个智能家居吧。
硬件
一块8*8的led灯板,由两块74H595控制,使用者只需要3个引脚便能控制数量众多的led。
一块2.4寸触摸屏,驱动芯片为ILI9341,用来模拟显示智能家居的中控屏幕,同时显示语音识别的结果。
软件
模型训练
模型训练之前我们得需要先收集语音素材,用于我们的模型训练,我一共收集了5种语音素材,分别是“打开”,“关闭”,“开灯”,“关灯”,“测试”,共找了10个人帮忙,每个人每种音频各采集30次左右,这样的话,每种语音我大概有300多个
之后就是裁剪工作了,就像中期报告中的那样,使用audacity软件,对音频进行裁剪,特点的格式,当然这是一个比较耗时的工作。如下
将裁剪号的音频,分类分好,放一个文件夹中
将五个文件夹改英文名,并移到如下目录,data是放官方例程中下载的数据集的地方,而KWS则是语音唤醒词的例程,因为我的程序是直接在官方例程kws_demo上修改使用的,所以就放这里也行
同时我们也可以对这里的语音文件进行删减,因为有些我们用不着,删了可以减少语音转化的时间
再之后,则是对下图的文件进行修改,该文件用于给主目录下的train.py文件提供kws例程所含有的数据集及所想训练哪些语音
一处修改在文件开头,将此处的class_dict修改为刚刚raw文件夹中所包含的所有语音类型,要按照拼音的先后顺序,可以看到我的close,open,kai,guan,test,就在其中
class KWS:
"""
`SpeechCom v0.02 <http://download.tensorflow.org/data/speech_commands_v0.02.tar.gz>`
Dataset, 1D folded.
Args:
root (string): Root directory of dataset where ``KWS/processed/dataset.pt``
exist.
classes(array): List of keywords to be used.
d_type(string): Option for the created dataset. ``train`` or ``test``.
n_augment(int, optional): Number of augmented samples added to the dataset from
each sample by random modifications, i.e. stretching, shifting and random noise.
transform (callable, optional): A function/transform that takes in an PIL image
and returns a transformed version.
download (bool, optional): If true, downloads the dataset from the internet and
puts it in root directory. If dataset is already downloaded, it is not
downloaded again.
save_unquantized (bool, optional): If true, folded but unquantized data is saved.
"""
url = 'http://download.tensorflow.org/data/speech_commands_v0.02.tar.gz'
fs = 16000
//修改此处
class_dict = {'backward': 0,'close':1,'down': 2,'eight': 3, 'five': 4, 'forward': 5, 'four': 6, 'go': 7,'guan':8,
'kai':9, 'left': 10, 'nine': 11,'no': 12, 'off': 13, 'on': 14, 'one': 15,'open':16, 'right': 17, 'seven': 18,
'six': 19, 'stop': 20, 'test': 21, 'three': 22, 'two': 23,'up': 24, 'yes': 25}
def __init__(self, root, classes, d_type, t_type, transform=None, quantization_scheme=None,
augmentation=None, download=False, save_unquantized=False):
另一处的修改在最后面,这里修改为自己所想训练的语音,我这里则是训练了9个(不包括unknow)语音'backward','forward','open','close','kai','guan','test', 'left', 'right'。
def KWS_20_get_datasets(data, load_train=True, load_test=True):
"""
Load the folded 1D version of SpeechCom dataset for 20 classes
The dataset is loaded from the archive file, so the file is required for this version.
The dataset originally includes 35 keywords. A dataset is formed with 21 classes which includes
20 of the original keywords and the rest of the dataset is used to form the last class, i.e.,
class of the others.
The dataset is split into training+validation and test sets. 90:10 training+validation:test
split is used by default.
Data is augmented to 3x duplicate data by random stretch/shift and randomly adding noise where
the stretching coefficient, shift amount and noise variance are randomly selected between
0.8 and 1.3, -0.1 and 0.1, 0 and 1, respectively.
"""
return KWS_get_datasets(data, load_train, load_test, num_classes=9)//修改此处
def KWS_get_unquantized_datasets(data, load_train=True, load_test=True, num_classes=6):
"""
Load the folded 1D version of SpeechCom dataset without quantization and augmentation
"""
(data_dir, args) = data
transform = None
if num_classes in (6, 20):
classes = next((e for _, e in enumerate(datasets)
if len(e['output']) - 1 == num_classes))['output'][:-1]
elif num_classes == 35:
classes = next((e for _, e in enumerate(datasets)
if len(e['output']) == num_classes))['output']
else:
raise ValueError(f'Unsupported num_classes {num_classes}')
augmentation = {'aug_num': 0}
quantization_scheme = {'bits': 0}
if load_train:
train_dataset = KWS(root=data_dir, classes=classes, d_type='train',
transform=transform, t_type='keyword',
quantization_scheme=quantization_scheme,
augmentation=augmentation, download=True)
else:
train_dataset = None
if load_test:
test_dataset = KWS(root=data_dir, classes=classes, d_type='test',
transform=transform, t_type='keyword',
quantization_scheme=quantization_scheme,
augmentation=augmentation, download=True)
if args.truncate_testset:
test_dataset.data = test_dataset.data[:1]
else:
test_dataset = None
return train_dataset, test_dataset
def KWS_35_get_unquantized_datasets(data, load_train=True, load_test=True):
"""
Load the folded 1D version of unquantized SpeechCom dataset for 35 classes.
"""
return KWS_get_unquantized_datasets(data, load_train, load_test, num_classes=35)
datasets = [
{
'name': 'KWS', # 6 keywords
'input': (512, 64),
'output': ('up', 'down', 'left', 'right', 'stop', 'go', 'UNKNOWN'),
'weight': (1, 1, 1, 1, 1, 1, 0.06),
'loader': KWS_get_datasets,
},
{
'name': 'KWS_20', # 20 keywords
'input': (128, 128),
'output': ('backward','forward','open','close','kai','guan','test', 'left', 'right','UNKNOWN'),//修改此处
'weight': (1, 1,1,1,1,1,1,1,1,0.07),//修改此处
'loader': KWS_20_get_datasets,
},
最后一个要修改的文件在此处,该文件好像是专门为这里例程编写的模型配置文件
该文件要修改的地方就一处,改为我们刚刚所需要训练语音的数量,即10个(这里需要包括unknow)
class AI85KWS20Net(nn.Module):
"""
Compound KWS20 Audio net, starting with Conv1Ds with kernel_size=1
and then switching to Conv2Ds
"""
# num_classes = n keywords + 1 unknown
def __init__(
self,
num_classes=10,//修改此处
num_channels=128,
dimensions=(128, 1), # pylint: disable=unused-argument
fc_inputs=7,
bias=False,
**kwargs
):
super().__init__()
接下来就简单了,跟官方教程的一样,开始训练,训练后量化,生成c文件
训练量化生成c文件
另外,该文件的--test-dir 后面的路径为生成c文件的路径,可以修改
生成的文件中,这3个文件复制到自己的项目中,替换原有的进行使用
最后,对项目中此处修改为我们所训练的语音即可
const char keywords[NUM_OUTPUTS][10] = { "backward","forward","open","close","led on","led off","test", "left", "right","unknow" };//此处
至此,模型训练的部分就好了,接下来就是语音识别的具体应用方面了
移植LVGl
难得使用了屏幕,为了让屏幕显示更加精美,更多的信息,就移植个LVGL看看。在lvgl的github上下载最新版8.3版本,移到项目中
将屏幕初始化,及lvgl初始化准备好
void lv_port_disp_init(void)
{
/*-------------------------
* Initialize your display
* -----------------------*/
disp_init();
/* Example for 2) */
static lv_disp_buf_t draw_buf_dsc_2;
static lv_color_t draw_buf_2_1[LV_HOR_RES_MAX * 10]; /*A buffer for 10 rows*/
static lv_color_t draw_buf_2_2[LV_HOR_RES_MAX * 10]; /*An other buffer for 10 rows*/
lv_disp_buf_init(&draw_buf_dsc_2, draw_buf_2_1, draw_buf_2_2, LV_HOR_RES_MAX * 10); /*Initialize the display buffer*/
/*-----------------------------------
* Register the display in LVGL
*----------------------------------*/
lv_disp_drv_t disp_drv; /*Descriptor of a display driver*/
lv_disp_drv_init(&disp_drv); /*Basic initialization*/
/*Set up the functions to access to your display*/
/*Set the resolution of the display*/
disp_drv.hor_res = LV_HOR_RES_MAX;
disp_drv.ver_res = LV_VER_RES_MAX;
/*Used to copy the buffer's content to the display*/
disp_drv.flush_cb = disp_flush;
/*Set a display buffer*/
disp_drv.buffer = &draw_buf_dsc_2;
#if LV_USE_GPU
/*Optionally add functions to access the GPU. (Only in buffered mode, LV_VDB_SIZE != 0)*/
/*Blend two color array using opacity*/
disp_drv.gpu_blend_cb = gpu_blend;
/*Fill a memory array with a color*/
disp_drv.gpu_fill_cb = gpu_fill;
#endif
/*Finally register the driver*/
lv_disp_drv_register(&disp_drv);
}
/* Initialize your display and the required peripherals. */
static void disp_init(void)
{
mxc_gpio_cfg_t tft_reset_pin ={MXC_GPIO0,MXC_GPIO_PIN_19,MXC_GPIO_FUNC_OUT,MXC_GPIO_PAD_NONE,MXC_GPIO_VSSEL_VDDIOH};
mxc_gpio_cfg_t tft_blen_pin ={MXC_GPIO0,MXC_GPIO_PIN_9,MXC_GPIO_FUNC_OUT,MXC_GPIO_PAD_NONE,MXC_GPIO_VSSEL_VDDIOH};
/* Initialize TFT display */
MXC_TFT_Init(MXC_SPI0, 1, &tft_reset_pin, &tft_blen_pin);//官方的屏幕初始化程序
MXC_TFT_SetRotation(ROTATE_90);
}
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
uint32_t w = (area->x2 - area->x1 + 1);
uint32_t h = (area->y2 - area->y1 + 1);
MXC_TFT_WriteBufferRGB565(area->x1,area->y1,(uint8_t*)(color_p) ,w,h );
lv_disp_flush_ready(disp_drv);
}
遇到的问题
移植lvgl后,编译生成的固件太大了,溢出。
f:/maximsdk/tools/gnutools/10.3/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/bin/ld.exe: F:/MaximSDK/Examples/MAX78000/CNN/kws20_demo/build/kws20_demo.elf section `.bin_storage' will not fit in region `FLASH'
f:/maximsdk/tools/gnutools/10.3/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/bin/ld.exe: region `FLASH' overflowed by 13464 bytes
没办法,最后使用7.11版本的LVGL,就好了。
项目展示
总结
10月份开始,1月15号结束,时间过得真是快。虽然一开始拿到这个板子时一头的雾水,但在不断的摸索下,至少还是做出了像样的东西了,这种感觉真好。特别感谢群里的各位大佬们,在他们的聊天记录中总能发现避坑的点,要不然保准一步一跟头,还要感谢硬禾学堂能给我们带来这难得的项目经历,可以为自己的简历上丰富一笔。