章相关引用及参考:映维网
记录手部动作并将其转换为文字
(映维网 2018年04月19日)意大利初创公司Limix开发了一种名为Talking Hands的手套,其可以记录手部动作并将其转换为文字,转发至智能手机设备,然后再通过语音合成器进行播放。他们希望以这种方式来弥合大多数聋障人士所面临的通信障碍。
Limix的创始人Francesco Pezzuoli说道:“Talking Hands不仅可以翻译每一个字母,而且它的可定制程度非常高:用户可以记录他/她的手语,然后Talking Hands就会翻译它们。这是一种可穿戴设备,不需要任何外部摄像头或计算机,因此用户随时随地都可以使用它。”
该项目始于一年前,当时是作为Limix,意大利国家聋人协会(ENS),以及专门教导聋哑学生的意大利公立高中I.S.I.S.S. Magarotto之间的合作产物。然而,人们告诉Pezzuoli和另一位联合创始人Dario Corona说这一概念不会成功,因为设备无法测量细微的手指动作。但他们坚持不懈,并最终研发出可行的技术。现在,他们正准备在2018年6月推出第一个工业原型。
他们的概念成为了今年The Chivas Venture竞赛的最终入围项目之一,并有机会竞逐100万美元的投资。在今年5月,最终入选者将乘飞机前往阿姆斯特丹参加最终的决赛。届时他们将有五分钟时间向专家评审团推销他们的项目。对于The Chivas Venture的风投,开发团队希望这可以为工业原型的生产和扩展提供资金支持。
Pezzuoli解释说:“主要的问题是,非手语者通常不太了解手语。他们可能甚至没有意识到美国手语(和其他手语,如英国手语和中国手语)是非常不同的语言,其有着自己的语法和措辞,而不是口头语言的单词重述。”
Talking Hands能够绕开这个问题,因为其允许每个用户通过应用程序创建和自定义他们自己的手语,并将其直接链接到智能手机。他们可以创建专用于不同情况的字典,例如在医疗紧急情况下需要与医生沟通的常用词语。
Pezzuoli声称:“从来没有人能够发现和实现像我们这样简单,低成本和有用的解决方案。Talking Hands的首次生产成本约为500欧元,包含一切,因此与市场上的其他手套相比,它的价格相对较低。”
具体操作:
用户将Talking Hands应用下载到移动设备上。
用户将应用的口头语言设置为英语和意大利语等语言,将其作为手语识别的输出语言。
然后,用户可以在穿戴手套时录制手语,然后系统会将其与字母,单词或整个短语相关联。
每当用户重复该手语时,智能手机都会代表用户说出相应的词语。
Talking Hands可以翻译世界各地使用的几种不同手语:ASL(美国手语),BSL(英国手语),CSL(中国手语)等等。
Talking Hands同时可以与增强现实系统进行交互,从而进行员工培训,与虚拟博物馆和游戏控制器交互。
意大利国立聋人高等教育研究所(National Institute for Higher Education of Deaf People)的Rita Antoniozzi教授解释说:“Talking Hands可以帮助聋哑人摆脱沉默,使他们能够与不懂手语的人进行第一范式的沟通。”她认为,这项技术将能为失聪用户提供支持,使他们能够实现更大程度的独立性和平等性。
但对Pezzuoli而言,平等性的好处在很大程度上是双向的:“我想证明技术可以成为统一和悦人的力量,使人成为中心,这样我们就可以创建并扩大这种关系,纳入残疾人,并与他们的世界和文化融合在一起。”他补充说,残疾人士看待世界的不同方式实际上是一项重要资产,它可以带来创造性思维和创新,因此赋予他们“声音”不仅对聋人社区有好处,其对整个社会都有很大的益处。
原文链接:https://yivian.com/news/44228.html
文共4732字,预计学习时长15分钟
现在的年轻人聊起天来都是一场场你来我往的表情包大战。稍有不慎,就会立马败下阵来。
你可能拥有数G个表情包存图,但总是苦于表情包太多太乱,每次挑选都是旷日持久。等好不容易终于选中一张满意的表情包,却发现对方早已切到下一回合。
要是有个功能可以把表情包一键分类就好了。这可能,会随着FER(面部表情识别技术)的发展成为现实。
表情识别vs人脸识别
面部表情识别技术源于1971年心理学家Ekman和Friesen的一项研究,他们提出人类主要有六种基本情感,每种情感以唯一的表情来反映当时的心理活动,这六种情感分别是愤怒(anger)、高兴(happiness)、悲伤 (sadness)、惊讶(surprise)、厌恶(disgust)和恐惧(fear)。
尽管人类的情感维度和表情复杂度远不是数字6可以量化的,但总体而言,这6种也差不多够描述了。
事实上,表情识别技术差不多可以算是在人脸识别技术的基础上发展而来的。因此,表情识别也需要依赖于人脸的特征点检测。
所谓的特征点,就是预先定义的一组脸部或五官轮廓的点。在人脸特征点检测中,通常我们比较关注的特征点一般位于眉毛、眼睛、嘴巴的位置,而鼻子在各种表情中的位置变化较不明显,因此很多研究中都忽略对鼻子位置的特征点进行检测。
图片来源:shutterlock
但两者还是有区别的,表情识别从技术上来说还是比人脸识别要更加复杂。人脸识别是一个静态识别问题,最经典的人脸识别案例就是输入两张人脸照片,然后让机器去判定两张脸是不是属于同一个人。而表情识别是给定一个人脸的连续动作帧,是一个时间段内表情变化的动态判定问题。
人脸识别实际上是个去表情的过程,不管作出什么表情,不管是哭还是笑,都要想办法去识别为同一个人。然而表情识别却是放大表情的过程,对于同一个人,通过观察表情变化来推断其情绪的起伏。
图片来源:shutterlock
相比于人脸识别,表情识别的动态不确定性成分更大,不仅需要做到精确,而且需要做到实时。如果愤怒被检测为悲伤,或者当前检测的表情已经是三秒前的表情状态了,那肯定都是没有办法满足实际应用需求的。
说到应用,人脸识别最常见的应用场景可能要数“身份验证”,而表情识别除了我们上面提到的能进行表情分类外,还可以广泛应用于多个领域。
比如用于电影制作,往后不再需要设计动画,只需要将真人的表情动作直接映射即可。用于产品投放前的反响测试,则可以通过分析被试者的表情来预测用户体验。用于公安的审问环节,可以通过观察受审者的细微表情变化来帮助判断其证词是否属实。当然,在未来,如果机器能够通过识别我们的表情来为我们提供个性化的服务,就能实现更好的人机交互。
图中右下角显示原始表情,根据表情来进行四川变脸。
当表情识别遇上深度学习
目前,深度学习已强势渗透进各个学科各个领域,大数据已成为这个时代最标志的特征之一。要用深度学习来做表情识别,第一步自然是选用一个数据丰富的表情库,目前比较常用的表情库主要有FER2013人脸数据集、日本ATR技术研究所建立的JAFFE日本女性表情数据库以及美国CMU机器人研究所和心理学系共同建立的CKACFEID人脸表情数据库。
JAFFE日本女性表情数据库
总体来说,基于深度学习的表情识别一般分为以下几个步骤:
1)图像获取:通过摄像头等来获得图像输入。
2)图像预处理:对图像中人脸识别子区域进行检测,从而分割出人脸并去掉背景和无关区域。然后,进一步对图像中的人脸进行标定。目前IntraFace是最常用的人脸标定方法,通过使用级联人脸关键特征点定位(SDM),可准确预测49个关键点。为了保证数据足够充分,可以采用旋转、翻转、缩放等图像增强操作,甚至可以利用现在大火的GAN来辅助生成更多的训练数据。
至此,准备工作可还没完事。由于图像中不同光照强度和头部姿态对表情识别的效果影响巨大,所以在开始正式工作前,还需要对光照和姿态做归一化处理。不必担心,站在巨人肩膀上的我们有很多现成的归一化方法,比如使用INFace工具箱对光照进行归一化,以及使用FF-GAN,TP-GAN,DR-GAN这些基于GAN的深度模型来矫正面部姿态从而生成正面人脸。
基于深度学习的面部表情识别系统
3)特征学习深度网络
传统表情识别技术和深度表情识别技术最大的区别就在于特征学习的方式不同。传统表情识别技术的特征提取方法主要有Gabor小波变换、局部二值模式(LBP)、局部线性嵌入(LLE)、梯度方向直方图(HOG)等。
近些年来,有越来越多的深度网络被用于FER,其中包括深度置信网络DBN、递归神经网络RNN以及卷积神经网络CNN等。
以CNN为例,面部表情识别的CNN框架如下图所示,与经典的卷积神经网络无甚差别,主要也是包含输入层、卷积层、全连接层和输出层。
面部表情识别CNN架构(改编自 埃因霍芬理工大学PARsE结构图)
其中,通过卷积操作来创建特征映射,将卷积核挨个与图像进行卷积,从而创建一组要素图,并在其后通过池化(pooling)操作来降维。
CNN表情识别网络中使用卷积和最大池化操作
通常,在致密层(又称为全连接层)的末端可以加上损失层,目的是修正正反向的传播误差,此后网络输出的直接就是每个输入样本的表情分类预测概率。当提供的数据越多,网络可以逐步进行微调,直至损失最小。看起来好像我们设置的网络节点越多,模型的表达能力就会越好,但这也同时会导致训练数据容易陷入过拟合状态。一般使用Dropout来解决过拟合问题,该方法不仅可以保证模型在训练期间的敏感性也可以保持框架的必要复杂度。
神经网络训练:前向传播(左)和后向传播(右)
输出层常用Sigmoid或者Softmax作为激活函数,通过激活函数将神经元的输入映射到输出端。实际上,激活函数并不是要激活什么,而是需要把激活的神经元特征保留并映射出来,其他的数据属于冗余就被剔掉了。在输出层我们就能直接得到每个表情所属的情绪类别及相应概率。
尽管这看起来并不难,但表情识别除面临光照变化、非正面头部姿态等带来的挑战之外,对低强度的表情识别也较为困难;并且,理想的表情数据库应该包含各个种族、各个年龄阶段的各种表情,除了数据获取的难度外,对大量复杂自然场景下的人脸进行精准标注也是一大难点。但随着数据的丰富和算法的改进,这些都将不会是什么大问题。
表情识别太高端?不,你也可以!
下面,我们将通过实例教你如何实现表情识别。
首先,请确保你的电脑上已经安装和配置好Keras、Flask和OpenCV。
然后,选择一个合适的表情库,在这个实例中,我们选择FER2013表情库。
这个数据集中大约包含36000张大小为48*48像素的灰度图像,并且都已自动调整过,基本每张人脸在图像中的位置和所占比例都差不多。除了心理学家Ekman和Friesen提出的6种情绪外,这里加入了一种新的情绪—中性(neutral)。每种情绪对应一个数字类别,0=Angry(愤怒), 1=Disgust(厌恶), 2=Fear(恐惧), 3=Happy(高兴), 4=Sad(悲伤), 5=Surprise(惊讶), 6=Neutral(中性)。
现在,正式工作开始。
原始数据由图像每个像素灰度值的数组数据组成,我们将数据转换为原始图像并将其拆分放进多个子文件中。其中,80%的数据放进训练集中,剩余20%的数据用于测试。
images/ train/ angry/ disgust/ fear/ happy/ neutral/ sad/ surprise/ validation/ angry/ disgust/ fear/ happy/ neutral/ sad/ surprise/
首先让我们看看我们的图像长什么样子:
# display some images for every different expression import numpy as np import seaborn as sns from keras.preprocessing.image import load_img, img_to_array import matplotlib.pyplot as plt import os # size of the image: 48*48 pixels pic_size=48 # input path for the images base_path="../input/images/images/" plt.figure(0, figsize=(12,20)) cpt=0 for expression in os.listdir(base_path + "train/"): for i in range(1,6): cpt=cpt + 1 plt.subplot(7,5,cpt) img=load_img(base_path + "train/" + expression + "/" +os.listdir(base_path + "train/" + expression)[i], target_size=(pic_size, pic_size)) plt.imshow(img, cmap="gray") plt.tight_layout() plt.show()
部分训练样本
你能猜出这些图像对应的表情么?
这对人类来说可能非常简单,但对于机器来说还是相当具有挑战性的。毕竟上面这些图像分辨率不高,脸也不在同一位置,一些图上还有文字,甚至很多图都有手对面部进行了遮挡。
但与此同时,越复杂多样的图片训练出的模型泛化能力就会越好。
# count number of train images for each expression for expression in os.listdir(base_path + "train"): print(str(len(os.listdir(base_path + "train/" + expression))) + " " + expression + " images")
数一下每种表情的数量,我们得到:
4103 fear images 436 disgust images 4982 neutral images 7164 happy images 3993 angry images 3205 surprise images 4938 sad images
不难看出,除了“厌恶”的表情,其他每种表情的数量基本是平衡的。
Keras的ImageDataGenerator类可以从路径中提供批量数据:
from keras.preprocessing.image import ImageDataGenerator # number of images to feed into the NN for every batch batch_size=128 datagen_train=ImageDataGenerator() datagen_validation=ImageDataGenerator() train_generator=datagen_train.flow_from_directory(base_path + "train", target_size=(pic_size,pic_size), color_mode="grayscale", batch_size=batch_size, class_mode='categorical', shuffle=True) validation_generator=datagen_validation.flow_from_directory(base_path + "validation", target_size=(pic_size,pic_size), color_mode="grayscale", batch_size=batch_size, class_mode='categorical', shuffle=False)
Found 28821 images belonging to 7 classes. Found 7066 images belonging to 7 classes.
训练集中共有28821张表情图片;验证集中共有7066张表情图片。值得一提的是,此处还可以在获取图像时执行数据增强(比如随机旋转和尺度缩放等)。上文代码中函数flow_from_directory()用于指定生成器以何种方式导入图像(路径,图像大小,颜色等)。
先来定义我们的CNN网络架构:
from keras.layers import Dense, Input, Dropout, GlobalAveragePooling2D, Flatten, Conv2D, BatchNormalization, Activation, MaxPooling2D from keras.models import Model, Sequential from keras.optimizers import Adam # number of possible label values nb_classes=7 # Initialising the CNN model=Sequential() # 1 - Convolution model.add(Conv2D(64,(3,3), padding='same', input_shape=(48, 48,1))) model.add(BatchNormalization()) model.add(Activation('relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Dropout(0.25)) # 2nd Convolution layer model.add(Conv2D(128,(5,5), padding='same')) model.add(BatchNormalization()) model.add(Activation('relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Dropout(0.25)) # 3rd Convolution layer model.add(Conv2D(512,(3,3), padding='same')) model.add(BatchNormalization()) model.add(Activation('relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Dropout(0.25)) # 4th Convolution layer model.add(Conv2D(512,(3,3), padding='same')) model.add(BatchNormalization()) model.add(Activation('relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Dropout(0.25)) # Flattening model.add(Flatten()) # Fully connected layer 1st layer model.add(Dense(256)) model.add(BatchNormalization()) model.add(Activation('relu')) model.add(Dropout(0.25)) # Fully connected layer 2nd layer model.add(Dense(512)) model.add(BatchNormalization()) model.add(Activation('relu')) model.add(Dropout(0.25)) model.add(Dense(nb_classes, activation='softmax')) opt=Adam(lr=0.0001) model.compile(optimizer=opt, loss='categorical_crossentropy', metrics=['accuracy'])
我们的CNN中包含4个卷积层和2个全连接层,卷积层负责从图像中提取相关特征,全连接层用于对图像进行分类。我们使用ReLU函数在CNN中引入非线性,并使用BN(批量标准化)来提高网络性能。此外,通过Dropout来减少过拟合。我们选用交叉熵作为损失函数,激活函数选用常用于多分类任务的Softmax函数。
现在我们已经定义了CNN,让我们正式开始训练吧!
训练模型
# number of epochs to train the NN epochs=50 from keras.callbacks import ModelCheckpoint checkpoint=ModelCheckpoint("model_weights.h5", monitor='val_acc', verbose=1, save_best_only=True, mode='max') callbacks_list=[checkpoint] history=model.fit_generator(generator=train_generator, steps_per_epoch=train_generator.n//train_generator.batch_size, epochs=epochs, validation_data=validation_generator, validation_steps=validation_generator.n//validation_generator.batch_size, callbacks=callbacks_list)
Epoch 1/50 225/225 [==============================] - 36s 161ms/step - loss: 2.0174 - acc: 0.2333 - val_loss: 1.7391 - val_acc: 0.2966 Epoch 00001: val_acc improved from -inf to 0.29659, saving model to model_weights.h5 Epoch 2/50 225/225 [==============================] - 31s 138ms/step - loss: 1.8401 - acc: 0.2873 - val_loss: 1.7091 - val_acc: 0.3311 Epoch 00002: val_acc improved from 0.29659 to 0.33108, saving model to model_weights.h5 ... Epoch 50/50 225/225 [==============================] - 30s 132ms/step - loss: 0.6723 - acc: 0.7499 - val_loss: 1.1159 - val_acc: 0.6384 Epoch 00050: val_acc did not improve from 0.65221
从迭代输出中不难看出,我们的模型能够达到的最高验证准确度为65%,这可能并不算高,但对于多分类任务来说,已经相当不错了。
我们将CNN的结构保存到文件中:
# serialize model structure to JSON model_json=model.to_json() with open("model.json", "w") as json_file: json_file.write(model_json)
我们通过训练过程中保存的数据来绘制训练集和验证集的损失和准确度演变曲线:
# plot the evolution of Loss and Acuracy on the train and validation sets import matplotlib.pyplot as plt plt.figure(figsize=(20,10)) plt.subplot(1, 2, 1) plt.suptitle('Optimizer : Adam', fontsize=10) plt.ylabel('Loss', fontsize=16) plt.plot(history.history['loss'], label='Training Loss') plt.plot(history.history['val_loss'], label='Validation Loss') plt.legend(loc='upper right') plt.subplot(1, 2, 2) plt.ylabel('Accuracy', fontsize=16) plt.plot(history.history['acc'], label='Training Accuracy') plt.plot(history.history['val_acc'], label='Validation Accuracy') plt.legend(loc='lower right') plt.show()
随着训练的迭代次数增加,损失和准确度的演变
除了损失和准确度演变曲线,我们还可以通过绘制混淆矩阵来帮助我们了解我们的模型是如何对图像进行分类的:
# show the confusion matrix of our predictions # compute predictions predictions=model.predict_generator(generator=validation_generator) y_pred=[np.argmax(probas) for probas in predictions] y_test=validation_generator.classes class_names=validation_generator.class_indices.keys() from sklearn.metrics import confusion_matrix import itertools def plot_confusion_matrix(cm, classes, title='Confusion matrix', cmap=plt.cm.Blues): cm=cm.astype('float') / cm.sum(axis=1)[:, np.newaxis] plt.figure(figsize=(10,10)) plt.imshow(cm, interpolation='nearest', cmap=cmap) plt.title(title) plt.colorbar() tick_marks=np.arange(len(classes)) plt.xticks(tick_marks, classes, rotation=45) plt.yticks(tick_marks, classes) fmt='.2f' thresh=cm.max() / 2. for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])): plt.text(j, i, format(cm[i, j], fmt), horizontalalignment="center", color="white" if cm[i, j] > thresh else "black") plt.ylabel('True label') plt.xlabel('Predicted label') plt.tight_layout() # compute confusion matrix cnf_matrix=confusion_matrix(y_test, y_pred) np.set_printoptions(precision=2) # plot normalized confusion matrix plt.figure() plot_confusion_matrix(cnf_matrix, classes=class_names, title='Normalized confusion matrix') plt.show()
从混淆矩阵的结果来看,我们的模型在预测“高兴”和“惊讶”的表情时表现非常优秀,但是在预测“恐惧”的表情时则比较头疼,因为容易和“悲伤”的表情相混淆。
不管怎样,随着研究进一步深入以及更广泛的资源获取,模型性能肯定能得到优化和改善。下面,是时候在真实场景下检测我们的模型性能了。我们将使用Flask,以便通过网络摄像头的视频输入进行表情的实时检测。
首先我们先创建一个类,它将为我们提供先前训练模型的预测:
from keras.models import model_from_json import numpy as np class FacialExpressionModel(object): EMOTIONS_LIST=["Angry", "Disgust", "Fear", "Happy", "Neutral", "Sad", "Surprise"] def __init__(self, model_json_file, model_weights_file): # load model from JSON file with open(model_json_file, "r") as json_file: loaded_model_json=json_file.read() self.loaded_model=model_from_json(loaded_model_json) # load weights into the new model self.loaded_model.load_weights(model_weights_file) self.loaded_model._make_predict_function() def predict_emotion(self, img): self.preds=self.loaded_model.predict(img) return FacialExpressionModel.EMOTIONS_LIST[np.argmax(self.preds)]
接下来我们将实现一个能够执行以下操作的类:
1) 从网络摄像头获取图像流
2) 使用OpenCV检测并框出人脸
3) 从我们的CNN网络获取预测结果并将预测标签添加到网络摄像头的图像流中
4) 返回处理后的图像流
import cv2 from model import FacialExpressionModel import numpy as np facec=cv2.CascadeClassifier('haarcascade_frontalface_default.xml') model=FacialExpressionModel("model.json", "model_weights.h5") font=cv2.FONT_HERSHEY_SIMPLEX class VideoCamera(object): def __init__(self): self.video=cv2.VideoCapture(0) def __del__(self): self.video.release() # returns camera frames along with bounding boxes and predictions def get_frame(self): _, fr=self.video.read() gray_fr=cv2.cvtColor(fr, cv2.COLOR_BGR2GRAY) faces=facec.detectMultiScale(gray_fr, 1.3, 5) for (x, y, w, h) in faces: fc=gray_fr[y:y+h, x:x+w] roi=cv2.resize(fc, (48, 48)) pred=model.predict_emotion(roi[np.newaxis, :, :, np.newaxis]) cv2.putText(fr, pred, (x, y), font, 1, (255, 255, 0), 2) cv2.rectangle(fr,(x,y),(x+w,y+h),(255,0,0),2) _, jpeg=cv2.imencode('.jpg', fr) return jpeg.tobytes()
你以为这就完了?
还没,我们将创建一个Flask应用程序,将我们的表情预测结果呈现到网页中。
from flask import Flask, render_template, Response from camera import VideoCamera app=Flask(__name__) @app.route('/') def index(): return render_template('index.html') def gen(camera): while True: frame=camera.get_frame() yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n\r\n') @app.route('/video_feed') def video_feed(): return Response(gen(VideoCamera()), mimetype='multipart/x-mixed-replace; boundary=frame') if __name__=='__main__': app.run(host='0.0.0.0', debug=True)
激动人心的时刻到了,下面让我们一起看看最终的检测效果吧!
学会它,你就又习得一项实(shua)用(shuai)新技能。下次群聊发啥表情包,直接甩个表情识别网页程序过去,分分钟实力carry秒杀全场。你就是人群中最靓的仔!
留言 点赞 关注
我们一起分享AI学习与发展的干货
欢迎关注全平台AI垂类自媒体 “读芯术”
年来,图像识别研究的精确度越来越高,其中“手写数字识别”作为图像识别中的一个经典问题,通过“卷积神经网络(CNN)”的方式,某些解决方案的精确度更是达到了99.1%。大家在网上也能搜索到相应的实现教程,但是大部分教程都比较简略,缺乏深入浅出的讲解,让初学者们难以上手。
而且常见的“手写数字识别”教程往往是仅仅介绍已有的公共数据里的样本数字的识别方法,这就和实际应用中的“识别自己提供的手写数字”差距很大。考虑到这些gap,本文就详细给大家介绍一个更加贴近“实战”的、更详细的教程:如何用Keras构建卷积神经网络识别“自己”的手写字。
图1:手写字识别示图
使用的编程语言:Python。
使用的依赖包/框架:TensorFlow,Keras。
使用的模型/方法:卷积神经网络(Convolutional Neural Network,即CNN)。
使用的数据集:MNIST数据集--28x28像素的灰度手写数字图片--包括60000个训练样本和10000个测试样本(该MNIST数据集为事先处理好的“一个数字一张图”的标准数据集,背景为纯黑色、即byte值为0,数字笔划为白色,即笔划的最高值为255)。另有自行上传的手写字体图片约共30个。
解决的问题/实现的功能:
1,基于MNIST数据集,训练一个能识别手写阿拉伯数字的模型,即能够将输入的手写数字的图和对应的阿拉伯数字进行匹配(识别)。
2,用户将自己创建的手写数字作为一个图片、上传至训练好的模型中做预测,让模型识别出对应的数字。
推荐使用的平台:达仁云主机【Linux + JupyterLab】公共镜像。
图2:达仁云主机示图
可以看到笔者推荐使用“达仁云主机”这个工具(来自于atlas.daren.io)来进行学习和练习,这是因为对于第一次试用CNN、TensorFlow或Keras的同学来说,往往不仅仅缺乏必要的编程和软件配置知识,也经常缺少合适的硬件主机,例如合适的GPU卡。这样很多同学一开始上手时需要投入的时间和预算成本就非常高,相当于一个和我们要学习的CNN基本知识和应用并不是密切相关、不必要的“高门槛”。
使用类似于“达仁云主机”这样的产品可以大大降低同学们一开始上手的难度。虽然同样是采用云上租用虚拟机的方式使用,“达仁云主机”的特殊之处在于是“立即可用”的“预设+托管”型云主机(managed cloud PC):“达仁云主机”是预先安装好基础软件(例如这里会用到的JupyterLab)、配置好软件和网络设置(例如立即可以在浏览器里直接登录访问这个云主机上的JupyterLab)、并且“自动化+智能”处理好镜像存储和数据持久化等一些云主机的常见问题。
用“达仁云主机”这种浏览器中立即可用的“预设+托管”型云主机来做CNN的学习和练习,就会更加容易上手、直接进入CNN知识和技能学习的“核心区”。
基于达仁云主机这样的简易设定,我们在网页上仅需不断点击“下一步”,就能启动一台所需环境和配置的云主机。“达仁云主机”其实就是把平时需要“看懂一整本教程书”以后才能用起来的“云主机”的那些复杂的设置和维护“智能化/傻瓜化”,让用户仅需点击几个按钮,就能在浏览器里直接启动一台“立即能用”的云主机。
特别方便的是“达仁云主机”通过一个普通浏览器即可访问,不需要安装任何软件或购买任何硬件。云主机启动后是按秒计费,停机就立刻停止计费,没有“包月”的门槛,“省心省钱”。“达仁云主机”更有百余种云主机配置可选,其中就包括很多款适合做CNN机器学习使用的、带GPU卡的云主机类型。
这些特色就让很多因为没有预算(适合做AI的GPU卡现在可是几千元甚至上万元一张)、或者被各种云计算教科书“望而却步”过的同学能够“省心省事”地立即开始用上合适的AI学习环境。
注意了,这个达仁云主机提供的开发环境和计算资源,是真正可以做开发、做项目的“实战”级环境,可不是一些常见的“只能做作业”的“假”的模拟开发环境哦~
好了,我们开始进入正题,笔者将本教程分为三部分
一、达仁云主机三分钟开启环境篇
1.1、创建虚拟硬盘;
1.2、选择公共镜像;
1.3、配置JupyterLab云主机;
1.4、从终端配置环境。
二、建模型篇:模型训练与评估
2.1、导入所需的依赖包与框架
2.2、如何对数据预处理;
2.3、如何训练模型;
2.4、如何评估模型。
三、用模型篇:使用模型识别输入的手写字体
3.1、尝试识别在便笺上写的数字;
3.2、尝试识别白纸作为背景的手写数字;
3.3、尝试识别手写字体网站截取的手写数字;
3.4、尝试识别在Atlas OS笔记功能里手写的数字。
注意:本教程涉及的数据、Python代码、对应结果,都在本文的图文教程中全部提供,并可以在https://gitee.com/diagnoa/public/tree/master/tutorial/CNN-general-tutorial-01下载到。
一、达仁云主机三分钟开启环境篇
1.1、创建虚拟硬盘
在浏览器中登录Atlas OS(atlas.daren.io),选择左侧导航栏中的达仁云主机,在达仁云主机界面创建虚拟硬盘,使用此硬盘作为主要的数据和运行目录。
图1. 启动云主机界面
1.2、选择公共镜像
达仁云主机为用户“预设配置好”的JupyterLab、RStudio、Eclipse等多种开发环境及Windows、Linux远程桌面,这里我们选择Linux + JupyterLab镜像。
图2.选择主机镜像界面
1.3、配置JupyterLab云主机
点击启动云主机后,我们依次选择GPU类型的云主机g4dn.xlarge,设置虚拟硬盘大小,设置合适的内存,为云主机命名,然后点击启动。
图3. 选择配置GPU实例界面
达仁云主机提供百余种配置可选(CPU : 1~96核、内存:0.5GB~3TB、硬盘:1GB~16TB),费用低至3分钱/小时起,可以随时开启和关闭。当云主机状态由“启动中”变为“运行中”时,便可在启动JupyterLab后进行后续操作。此教程提供从IP启动的例子。
图4. 云主机启动中状态界面
图5. 云主机运行中状态界面
图6. 使用IP启动云主机界面
此时浏览器会打开Jupyter的操作界面,输入系统分配的初始密码即可登陆。
图7. 输入密码登陆Jupyter
1.4、打开终端配置环境
图8. 使用终端界面
图9. 终端界面
输入密码登录后,在图9界面中键入如下代码安装程序运行所需环境
# 安装必要的系统环境,即所依赖的系统程序和编译工具
# 此镜像默认安装了conda环境,所有的操作可以在base这个环境中进行
sudo yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
sudo yum update -y
sudo yum -y install kernel-devel-$(uname -r) kernel-headers-$(uname -r)
sudo yum -y install gcc gcc-c++
sudo yum install libXext.x86_64 libXrender.x86_64 libXtst.x86_64 mesa-libGL.x86_64 -y
# 下载并安装GPU驱动
cd DataDrive/
wget https://cn.download.nvidia.com/tesla/450.119.04/NVIDIA-Linux-x86_64-450.119.04.run
sudo sh NVIDIA-Linux-*.run
# 使用conda安装Keras环境
conda install -c anaconda tensorflow-gpu keras matplotlib pillow -y
conda install -c conda-forge opencv -y
至此本次教程所需环境已经配置完毕,我们就可以使用Jupyter Notebook开始执行代码。
图10. 打开Jupyter Notebook界面
二、建模型篇:模型训练与评估
2.1、导入所需的依赖包与框架
# 在程序的开始,导入待使用的包
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
from keras.layers import Flatten
from keras.layers.convolutional import Conv2D
from keras.layers.convolutional import MaxPooling2D
from keras.optimizers import Adam
from keras.utils import np_utils
from PIL import Image
import numpy as np
import os
from tensorflow import keras
import cv2
import matplotlib.pyplot as plt
2.2、如何对数据做预处理
# 加载数据
# 注意:一般模型训练需要区分训练数据(train)和测试数据(test),测试数据不可以在训练过程中使用,以便在训练过程结束以后,能够用测试数据来评估所训练的模型的性能。
(X_train, y_train), (X_test, y_test) = mnist.load_data()
# 将输入数据reshape成CNN期望的格式(也就是将图像数据转成神经网络能识别的数组数据)
X_train = X_train.reshape(X_train.shape[0], X_train.shape[1], X_train.shape[2], 1).astype('float32')
X_test = X_test.reshape(X_test.shape[0], X_test.shape[1], X_test.shape[2], 1).astype('float32')
# 将输入数据归一化
# 注意1:一般CNN数据的归一化(Normalization)是将数据转化到[0, 1]这个区间里(把数值变小一些)。因为如果输入数据里的数据值过大(例如几百或上万),将会造成模型训练时的时间过长、轮数过多(造成这个现象的一个原因:一些小的初始化系数项需要经过很多轮的训练才能“适应”包含“大数值”的变量项),而且很有可能会导致训练失败。所以一般所有的机器学习前,都需要对数据进行预处理,例如归一化或标准化。
# 注意2:数据的归一化和标准化(Standardization)和中心化(Centering)是三种不同(但联系紧密)的操作。此处使用的是归一化。对于基本符合正态分布的数据,建议使用标准化的预处理。
X_train /= 255
X_test /= 255
# 将输出数据类别变为one hot编码 (one hot encoding)
# 为什么这里要做one hot编码呢?因为神经网络无法识别常规的分类变量(categorical data)。感兴趣的同学可以在这里看一看做one hot编码的详细背景和原因:https://machinelearningmastery.com/why-one-hot-encode-data-in-machine-learning/。
number_of_classes = 10
y_train = np_utils.to_categorical(y_train, number_of_classes)
y_test = np_utils.to_categorical(y_test, number_of_classes)
2.3、如何训练模型
# 创建模型
# 创建模型有时候可以说一种“艺术”,因为模型的选择,例如各个层的参数选择多种多样,而模型中层与层之间的关系更是可以“千奇百怪”,这里仅仅提供了一种常见的适合于MNIST数据的网络。感兴趣的同学们可以阅读Keras和PyTorch文档里关于网络中不同层的各种类型的作用和其配置参数影响的介绍,创建自己的网络结构,来试一试:
# https://keras.io/api/layers/convolution_layers/
# https://pytorch.org/docs/stable/nn.html#convolution-layers
# 注:给一个“模型”尝试不同结构和不同参数(即常常提到的一个模型的“超参数”,hyperparameter)设置,找到最优化的模型超参数,往往需要尝试成千上万种潜在的模型结构和配置,这也是为什么在机器学习实战中,模型的创建和优化是如此的消耗资源(在成千上万个机器节点上跑不同的模型,或者在一个机器上跑成千上万个不同的模型)。
num_classes = 10 # 代表0-9共10个数字,10种类别。
input_shape = (28, 28, 1) # 输入数据shape为28x28x1。
model = Sequential(
[
keras.Input(shape=input_shape), # 定义输入层,设置输入层为与训练数据相同的维度。
Conv2D(32, kernel_size=(3, 3), activation="relu"), # 定义卷积层网络,本层需要学习32个卷积核,卷积核大小为3x3。激活函数为Relu。
MaxPooling2D(pool_size=(2, 2)), # 叠加池化层,池化窗口形状为2×2的最大池化。
Conv2D(64, kernel_size=(3, 3), activation="relu"), # 叠加卷积层网络。
MaxPooling2D(pool_size=(2, 2)), # 叠加池化层。
Flatten(), # 将输入“压平”,即把多维的输入一维化,常用在从卷积层到全连接层的过渡。
Dropout(0.5),# Dropout将在训练过程中每次更新参数时按一定概率(0.5)随机断开输入神经元,可防止过拟合。
Dense(num_classes, activation="softmax"),# 全连接层,输出层,激活函数为softmax。
]
)
# 打印模型信息
model.summary()
运行上述代码,输出结果为:
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d (Conv2D) (None, 26, 26, 32) 320
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 13, 13, 32) 0
_________________________________________________________________
conv2d_1 (Conv2D) (None, 11, 11, 64) 18496
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 5, 5, 64) 0
_________________________________________________________________
flatten (Flatten) (None, 1600) 0
_________________________________________________________________
dropout (Dropout) (None, 1600) 0
_________________________________________________________________
dense (Dense) (None, 10) 16010
=================================================================
Total params: 34,826
Trainable params: 34,826
Non-trainable params: 0
___________________________________________________________
上述结果打印了模型的相关信息:从左至右依次为网络每一层的信息、每一层的输出结果的样式、每一层的参数个数。打印这些信息可以帮助我们更直观地了解所构建模型的结构。
# 训练模型
# 注:一般训练模型是最消耗时间的步骤,这里可能会需要等一段时间
batch_size = 128 # 设置batch size
epochs = 15 # 设置epoch。
model.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"])
# 模型的loss选择categorical_crossentropy, 优化器选择adam,metrics选择accuracy。
model.fit(X_train, y_train, batch_size=batch_size, epochs=epochs, validation_split=0.1) # 用训练数据训练模型,并按照0.1的比例抽取数据进行validation。
运行上述代码,输出结果省略中间部分,展示如下:
Epoch 1/15
422/422 [==============================] - 6s 14ms/step - loss: 0.3649 - accuracy: 0.8872 - val_loss: 0.0844 - val_accuracy: 0.9782
Epoch 2/15
422/422 [==============================] - 2s 4ms/step - loss: 0.1099 - accuracy: 0.9660 - val_loss: 0.0556 - val_accuracy: 0.9848
......
Epoch 15/15
422/422 [==============================] - 1s 4ms/step - loss: 0.0335 - accuracy: 0.9890 - val_loss: 0.0286 - val_accuracy: 0.9915
上述结果打印了模型训练过程中loss的变化。在模型训练时,选择了categorical cross entropy作为loss,来衡量原始数字类别和由模型预测的数字类别的差异。loss值越小,说明预测结果与原始结果越相似。打印结果中loss是训练过程产生的,val_loss是验证过程产生的,val_accuracy是指validation过程中的准确性。计算的准确性越高效果越好。
2.4、如何评估模型
# 用测试数据评估模型
score = model.evaluate(X_test, y_test, verbose=0) # 用测试数据评估模型,不输出中间过程
print("Test loss:", score[0]) # 打印loss值
print("Test accuracy:", score[1]) # 打印accuracy
# 运行上述代码,输出结果为:
Test loss: 0.027751104906201363 # 测试数据的loss
Test accuracy: 0.991100013256073# 测试数据类别的准确性达到99.1%
所以如果仅仅拿MNIST数据集里数据做评估的话,那么这个模型的准确性相当的高,达到了99.1%。但是如果拿来“实用”一下呢?我们来看看这个模型能不能识别我们自己手写的数字。
三、用模型篇:使用模型识别输入的手写字体
生活中的事一般都是“理想很丰满,现实很骨感”,真的要让一个模型把一个手写的数字给识别出来,其实还真不是那么容易的。我们一起来看一下吧:
3.1、尝试识别在便笺上写的数字
我们先试一试“随便”地在便签纸上写一个简单的数字,让这个模型来试一下吧。在一张最普通最常见的(就是浅黄色的那种)便笺上写一个数字(如我们图中的这个“扭曲的0”),然后拍照后存到电脑(或云主机)里,先试一下吧。
注:让模型识别一个普通图像里的数字可不是那么“简单/直接”的,需要很多个“把一般的彩色照片图像转换成模型可以读取的数组”的操作步骤。例如下面这整整15行的代码就是做这个的:
pic = '/home/diagnoa/DataDrive/WechatIMG1.jpeg' # 手写字原始图片
img = Image.open(pic) # 读取图片文件
ax = plt.subplot(1, 2, 1) # 选择画布1行2列区域的左边绘图
plt.imshow(img) # 在左边绘制原始手写图片
plt.gray() # 绘制灰度图
ax.get_xaxis().set_visible(False) # 隐藏X轴
ax.get_yaxis().set_visible(False) # 隐藏Y轴
ax = plt.subplot(1, 2, 2) # 选择画布1行2列区域的右边绘图
target_shape = (28,28) # 设置数据shape
img2 = cv2.imread(pic, cv2.IMREAD_GRAYSCALE) # 读取图片为灰度图
img2 = cv2.resize(img2, target_shape) # resize图片
plt.imshow(img2, cmap='gray') # 绘制灰度图
ax.get_xaxis().set_visible(False) # 隐藏X轴
ax.get_yaxis().set_visible(False) # 隐藏Y轴
plt.show() # 画图
因为MNIST训练的模型要求的输入图片是(28x28x1)的灰度图,且数值是在0-1之间并且背景反转的(即背景为黑色,byte值为0,笔划信号为正值,这样在转化出的数组中方便直观观察)。此处我们使用的手写数字的照片在经过了上述的预处理之后,我们并不确定还有多少有效信息得到了保留。因此为了能够尽可能地对比原始图片和输入模型的图片,上述代码对预处理之后的图片做了可视化,即上面的这一部分代码中的使用到plt函数的这些行。
注:此处做预处理的代码参考的网络资料来源:
*请认真填写需求信息,我们会在24小时内与您取得联系。