第一部分:情绪识别模型训练部分,算法三种(CNN、VGG、ResNet)
1、基本表情介绍
人类的面部表情至少有21种,除了常见的高兴、吃惊、悲伤、愤怒、厌恶和恐惧6种,还有惊喜(高兴+吃惊)、悲愤(悲伤+愤怒)等15种可被区分的复合表情。详细介绍可以阅读下面博主的系列文章:
https://link.zhihu.com/target=https%3A//mp.weixin.qq.com/s%3F__biz%3DMzA3NDIyMjM1NA%3D%3D%26mid%3D2649039958%26idx%3D1%26sn%3Dbdbbb3690b8b23e107c56e14b72bf589%26chksm%3D87129a2bb065133d971793c1ca14b053737b593a5702c8375c5da5d328f0d422922ea7531b9b%26token%3D1054133372%26lang%3Dzh_CN%23rd
2、数据采集和训练
人脸情绪识别的训练数据有Kaggle和自行采集本地数据2种方式。
Fer2013人脸表情数据集由35886张人脸表情图片组成,其中,测试图(Training)28708张,公共验证图(PublicTest)和私有验证图(PrivateTest)各3589张,每张图片是由大小固定为48×48的灰度图像组成,共有7种表情,分别对应于数字标签0-6,具体表情对应的标签和中英文如下:
0 anger 生气;
1 disgust 厌恶;
2 fear 恐惧;
3 happy 开心;
4 sad 伤心;
5 surprised 惊讶;
6 normal 中性。
(数据集下载地址)
FER2013数据集
FER2013数据集由28709张训练图,3589张公开测试图和3589张私有测试图组成。每一张图都是像素为48*48的灰度图。FER2013数据库中一共有7中表情:愤怒,厌恶,恐惧,开心,难过,惊讶和中性。分别对应于数字标签0-6,具体表情对应的标签和中英文如下:0 anger 生气; 1 disgust 厌恶; 2 fear 恐惧; 3 happy 开心; 4 sad 伤心;5 surprised 惊讶; 6 normal 中性。
该数据库是2013年Kaggle比赛的数据,由于这个数据库大多是从网络爬虫下载的,存在一定的误差性。这个数据库的人为准确率是65% 士 5%。Kaggle的官网,数据、科学、机器学习比赛等。
3、数据清洗
在下载原文件中,emotion和pixels(像素)人脸像素数据是整合在一起的。为了方便操作,决定利用pandas库进行数据分离,即将所有emotion数据读出后,写入新创建的文件emotion.csv中去;将所有的像素数据读出后,写入新创建的文件pixels.csv文件中去。
pandas教程
-
关于.CSV文件,大小为28710行X2305列;
-
总计28710行中,其中第一行为描述信息,即“emotion”和“pixels”两个单词,其余每行内含有一个样本信息,即共有28709个样本;
-
在2305列中,其中第一列为该样本对应的emotion,取值范围为0-6。其余2304列为包含着每个样本大小为48X48人脸图片的像素值(2304=48X48),每个像素值取值范围在0到255之间;
实现代码如下:data_separation.py
# 将emotion和pixels像素数据分离
import pandas as pd
# 注意train.csv是在你电脑本地的相对或绝对路劲地址
path = 'dataset/train.csv'
# 读取数据
df = pd.read_csv(path)
# 提取emotion数据
df_y = df[['emotion']]
# 提取pixels数据
df_x = df[['pixels']]
# 将emotion写入emotion.csv
df_y.to_csv('dataset/emotion.csv', index=False, header=False)
# 将pixels数据写入pixels.csv
df_x.to_csv('dataset/pixels.csv', index=False, header=False)
此时,在dataset的文件夹下,就会生成两个新文件emotion.csv以及pixels.csv。在执行代码前,注意修改train.csv为你电脑上文件所在的相对或绝对路径。
4、将像素值数据还原为图片
-
给定的数据集是.csv格式的,考虑到图片分类问题的常规做法,决定先将其全部可视化,还原为图片文件后再进行图像数据处理。
-
在python环境下,将csv中的像素数据还原为图片并保存下来,有很多图片数据处理库都能实现类似的功能,如Pillow,opencv等。这里我采用的是用opencv来实现这一功能。
-
将数据分离后,人脸像素数据全部存储在pixels.csv文件中,其中每行数据就是一张人脸。按行读取数据,利用opencv将每行的2304个数据恢复为一张48X48的人脸图片,并保存为jpg格式。在保存这些图片时,将第一行数据恢复出的人脸命名为0.jpg,第二行的人脸命名为1.jpg…,以方便与label[0]、label[1]…一一对应。所以建立image-emotion映射关系表还是相当有必要的。
4.实现代码如下
import cv2
import numpy as np
# 指定存放图片的路径path = 'face_images'# 读取像素数据data = np.loadtxt('dataset/pixels.csv')
# 按行取数据for i in range(data.shape[0]):
face_array = data[i, :].reshape((48, 48)) # reshape
cv2.imwrite(path + '//' + '{}.jpg'.format(i), face_array) # 写图片
涉及到大量数据的读取和大批图片的写入,因此占用的内存资源较多,且执行时间较长(视机器性能而定,一般要几分钟到十几分钟不等)。代码执行完毕,我们来到指定的图片存储路径,就能发现里面全部是写好的人脸图片。
这里总共写入有28709张人脸图片数据
5、创建映射表
创建image图片名和对应emotion表情数据集的映射关系表。
- 我们需要划分一下训练集和验证集。在项目中,共有28709张图片,取前24000张图片作为训练集,其他图片作为验证集。在工程文件夹face_images下新建文件夹train_set和verify_set,将0.jpg到23999.jpg放进文件夹train_set,将其他图片放进文件夹verify_set。在继承torch.utils.data.Dataset类定制自己的数据集时,由于在数据加载过程中需要同时加载出一个样本的数据及其对应的emotion,因此最好能建立一个image的图片名和对应emotion表情数据的关系映射表,其中记录着image的图片名和其emotion表情数据的映射关系。
- 实现源码:image_emotion_mapping.py
def image_emotion_mapping(path):
# 读取emotion文件
df_emotion = pd.read_csv('dataset/emotion.csv', header = None)
# 查看该文件夹下所有文件
files_dir = os.listdir(path)
# 用于存放图片名
path_list = []
# 用于存放图片对应的emotion
emotion_list = []
# 遍历该文件夹下的所有文件
for file_dir in files_dir:
# 如果某文件是图片,则将其文件名以及对应的emotion取出,分别放入path_list和emotion_list这两个列表中
if os.path.splitext(file_dir)[1] == ".jpg":
path_list.append(file_dir)
index = int(os.path.splitext(file_dir)[0])
emotion_list.append(df_emotion.iat[index, 0])
# 将两个列表写进image_emotion.csv文件
path_s = pd.Series(path_list)
emotion_s = pd.Series(emotion_list)
df = pd.DataFrame()
df['path'] = path_s
df['emotion'] = emotion_s
df.to_csv(path+'\\image_emotion.csv', index=False, header=False)
def main():
# 指定文件夹路径
train_set_path = 'face_images/train_set'
verify_set_path = 'face_images/verify_set'
image_emotion_mapping(train_set_path)
image_emotion_mapping(verify_set_path)
代码执行完毕后,会在train_set和verify_set文件夹下各生成一个名为image-emotion.csv的关系映射表。
6、加载数据集
现在我们有了图片,但怎么才能把图片读取出来送给模型呢?一般在平常的时候,我们第一个想到的是将所有需要的数据聚成一堆一堆然后通过构建list去读取我们的数据:
假如我们编写了上述的图像加载数据集代码,在训练中我们就可以依靠get_training_data()这个函数来得到batch_size个数据,从而进行训练,乍看下去没什么问题,但是一旦我们的数据量超过1000:
将所有的图像数据直接加载到numpy数据中会占用大量的内存
由于需要对数据进行导入,每次训练的时候在数据读取阶段会占用大量的时间
只使用了单线程去读取,读取效率比较低下
拓展性很差,如果需要对数据进行一些预处理,只能采取一些不是特别优雅的做法
如果用opencv将所有图片读取出来,最简单粗暴的方法就是直接以numpy中array的数据格式直接送给模型。如果这样做的话,会一次性把所有图片全部读入内存,占用大量的内存空间,且只能使用单线程,效率不高,也不方便后续操作。
既然问题这么多,到底说回来,我们应该如何正确地加载数据集呢?
其实在pytorch中,有一个类(torch.utils.data.Dataset)是专门用来加载数据的,我们可以通过继承这个类来定制自己的数据集和加载方法。
Dataset类是Pytorch中图像数据集中最为重要的一个类,也是Pytorch中所有数据集加载类中应该继承的父类。其中父类中的两个私有成员函数必须被重载,否则将会触发错误提示:
def getitem(self, index):
def len(self):
其中__len__应该返回数据集的大小,而__getitem__应该编写支持数据集索引的函数,例如通过dataset[i]可以得到数据集中的第i+1个数据。
class Dataset(object):
"""An abstract class representing a Dataset.
All other datasets should subclass it. All subclasses should override
``__len__``, that provides the size of the dataset, and ``__getitem__``,
supporting integer indexing in range from 0 to len(self) exclusive.
"""
#这个函数就是根据索引,迭代的读取路径和标签。因此我们需要有一个路径和标签的 ‘容器’供我们读
def __getitem__(self, index):
raise NotImplementedError
#返回数据的长度
def __len__(self):
raise NotImplementedError
def __add__(self, other):
return ConcatDataset([self, other])
我们通过继承Dataset类来创建我们自己的数据加载类,命名为FaceDataset,完整代码如下请看代码:Dataset.py
模型训练
这里采用的是基于 CNN 的优化模型,这个模型是源于github一个做表情识别的开源项目,可惜即使借用了这个项目的模型结构,但却没能达到源项目中的精度(acc在74%)。下图为该开源项目中公布的两个模型结构,这里我采用的是 Model B ,且只采用了其中的卷积-全连接部分。CNN训练模型代码开源
这里:算法参考的论文原稿我已经下载文原版在参考文献文件夹下有。需要的(V我:ZQZ2551931023)
原文网址
CNN模型和算法原理实现
下图我们可以看出,在 Model B 的卷积部分,输入图片 shape 为 48X48X1,经过一个3X3X64卷积核的卷积操作,再进行一次 2X 2的池化,得到一个 24X24X64 的 feature map 1(以上卷积和池化操作的步长均为1,每次卷积前的padding为1,下同)。将 feature map 1经过一个 3X3X128 卷积核的卷积操作,再进行一次2X2的池化,得到一个 12X12X128 的 feature map 2。将feature map 2经过一个 3X3X256 卷积核的卷积操作,再进行一次 2X2 的池化,得到一个 6X6X256 的feature map 3。卷积完毕,数据即将进入全连接层。进入全连接层之前,要进行数据扁平化,将feature map 3拉一个成长度为 6X6X256=9216 的一维 tensor。随后数据经过 dropout 后被送进一层含有4096个神经元的隐层,再次经过 dropout 后被送进一层含有 1024 个神经元的隐层,之后经过一层含 256 个神经元的隐层,最终经过含有7个神经元的输出层。一般再输出层后都会加上 softmax 层,取概率最高的类别为分类结果。
通过数据的前向传播和误差的反向传播来训练模型了。在此之前,还需要指定优化器(即学习率更新的方式)、损失函数以及训练轮数、学习率等超参数。在本项目中,采用的优化器是SGD,即随机梯度下降,其中参数weight_decay为正则项系数;损失函数采用的是交叉熵;可以考虑使用学习率衰减。
实现代码:model_CNN.py
class FaceCNN(nn.Module):
# 初始化网络结构
def __init__(self):
super(FaceCNN, self).__init__()
# 第一次卷积、池化
self.conv1 = nn.Sequential(
# 输入通道数in_channels,输出通道数(即卷积核的通道数)out_channels,卷积核大小kernel_size,步长stride,对称填0行列数padding
# input:(bitch_size, 1, 48, 48), output:(bitch_size, 64, 48, 48), (48-3+2*1)/1+1 = 48
nn.Conv2d(in_channels=1, out_channels=64, kernel_size=3, stride=1, padding=1), # 卷积层
nn.BatchNorm2d(num_features=64), # 归一化
nn.RReLU(inplace=True), # 激活函数
# output(bitch_size, 64, 24, 24)
nn.MaxPool2d(kernel_size=2, stride=2), # 最大值池化
)
# 第二次卷积、池化
self.conv2 = nn.Sequential(
# input:(bitch_size, 64, 24, 24), output:(bitch_size, 128, 24, 24), (24-3+2*1)/1+1 = 24
nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(num_features=128),
nn.RReLU(inplace=True),
# output:(bitch_size, 128, 12 ,12)
nn.MaxPool2d(kernel_size=2, stride=2),
)
# 第三次卷积、池化
self.conv3 = nn.Sequential(
# input:(bitch_size, 128, 12, 12), output:(bitch_size, 256, 12, 12), (12-3+2*1)/1+1 = 12
nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(num_features=256),
nn.RReLU(inplace=True),
# output:(bitch_size, 256, 6 ,6)
nn.MaxPool2d(kernel_size=2, stride=2),
)
# 参数初始化
self.conv1.apply(gaussian_weights_init)
self.conv2.apply(gaussian_weights_init)
self.conv3.apply(gaussian_weights_init)
# 全连接层
self.fc = nn.Sequential(
nn.Dropout(p=0.2),
nn.Linear(in_features=256*6*6, out_features=4096),
nn.RReLU(inplace=True),
nn.Dropout(p=0.5),
nn.Linear(in_features=4096, out_features=1024),
nn.RReLU(inplace=True),
nn.Linear(in_features=1024, out_features=256),
nn.RReLU(inplace=True),
nn.Linear(in_features=256, out_features=7),
)
# 前向传播
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
# 数据扁平化
x = x.view(x.shape[0], -1)
y = self.fc(x)
return y
def train(train_dataset, val_dataset, batch_size, epochs, learning_rate, wt_decay):
# 载入数据并分割batch
train_loader = data.DataLoader(train_dataset, batch_size)
# 构建模型
model = FaceCNN()
# 损失函数
loss_function = nn.CrossEntropyLoss()
# 优化器
optimizer = optim.SGD(model.parameters(), lr=learning_rate, weight_decay=wt_decay)
# 学习率衰减
# scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.8)
# 逐轮训练
for epoch in range(epochs):
# 记录损失值
loss_rate = 0
# scheduler.step() # 学习率衰减
model.train() # 模型训练
for images, emotion in train_loader:
# 梯度清零
optimizer.zero_grad()
# 前向传播
output = model.forward(images)
# 误差计算
loss_rate = loss_function(output, emotion)
# 误差的反向传播
loss_rate.backward()
# 更新参数
optimizer.step()
# 打印每轮的损失
print('After {} epochs , the loss_rate is : '.format(epoch+1), loss_rate.item())
if epoch % 5 == 0:
model.eval() # 模型评估
acc_train = validate(model, train_dataset, batch_size)
acc_val = validate(model, val_dataset, batch_size)
print('After {} epochs , the acc_train is : '.format(epoch+1), acc_train)
print('After {} epochs , the acc_val is : '.format(epoch+1), acc_val)
return model
代码测试结果如下:
训练模型保存在文件下为:model_resnet.pkl
8、模型优化(VGG、ResNet)与算法改进
我们可以用理论部分提出的另外两种模型VGG模型和ResNet模型,对现有的项目进行优化,提升识别的准确度(三种算法已经实现),模型训练也已经完成。
此处省略----
9、本地数据采集[图像采集补充]
源码实现如下:
class FaceRecognition:
def __init__(self, master):
self.master = master
self.master.title('CNN人脸识别')
self.frame = Frame(self.master)
self.frame.pack(pady=10)
# 创建按钮
self.button1 = Button(self.frame, text='采集人脸数据', command=self.collect_face)
self.button1.pack(side=LEFT, padx=10)
self.button2 = Button(self.frame, text='训练模型', command=self.train_model)
self.button2.pack(side=LEFT, padx=10)
self.button3 = Button(self.frame, text='人脸识别', command=self.start_recognition)
self.button3.pack(side=LEFT, padx=10)
self.button4 = Button(self.frame, text='退出程序', command=self.master.quit)
self.button4.pack(side=LEFT, padx=10)
# 创建文本框用于显示结果
self.text = Text(self.master, width=30, height=10)
self.text.pack(pady=10)
# 加载模型
self.model = tf.keras.models.load_model(r'model/model.h5')
# 获取分类器
self.haar = cv2.CascadeClassifier(r'haarcascade_frontalface_default.xml')
# 获取人脸识别器
self.detector = dlib.get_frontal_face_detector()
# 创建显示图片的标签
self.img_label = Label(self.master)
self.img_label.pack()
# 创建关闭人脸识别按钮
self.close_button = Button(self.master, text='关闭人脸识别', command=self.close_recognition)
self.close_button.pack()
# 是否进行人脸识别的标志
self.recognition_flag = False
# 采集人脸数据
def collect_face(self):
out_dir = './my_faces'
if not os.path.exists(out_dir):
os.makedirs(out_dir)
# 改变亮度与对比度
def relight(img, alpha=1, bias=0):
w = img.shape[1]
h = img.shape[0]
# image = []
for i in range(0, w):
for j in range(0, h):
for c in range(3):
tmp = int(img[j, i, c] * alpha + bias)
if tmp > 255:
tmp = 255
elif tmp < 0:
tmp = 0
img[j, i, c] = tmp
return img
# 打开摄像头 参数为输入流,可以为摄像头或视频文件
camera = cv2.VideoCapture(0)
n = 1
while 1:
if (n <= 5000):
print('It`s processing %s image.' % n)
# 读帧
success, img = camera.read()
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
faces = self.haar.detectMultiScale(gray_img, 1.3, 5)
for f_x, f_y, f_w, f_h in faces:
face = img[f_y:f_y + f_h, f_x:f_x + f_w]
face = cv2.resize(face, (64, 64))
face = relight(face, random.uniform(0.5, 1.5), random.randint(-50, 50))
cv2.imshow('img', face)
cv2.imwrite(out_dir + '/' + str(n) + '.jpg', face)
n += 1
key = cv2.waitKey(30) & 0xff
if key == 27:
break
else:
break
10、人脸情绪识别系统设与实现
核心源码(Q:2551931023)
class EmotionClassifier:
def __init__(self):
self.face_cascade = cv2.CascadeClassifier(
r".\datasets\haarcascade_frontalface_default.xml")
self.model = Model()
self.model.load_state_dict(
torch.load(r'.\model\model_params.pkl', map_location='cpu'))
self.model.eval()
def get_emotion(self, inputs):
inputs = self.preprocess(inputs)
_, predicted = torch.max(self.model(inputs), 1)
probability = F.softmax((self.model(inputs)), dim=1).detach().numpy().flatten()
emotion = {
0: 'angry',
1: 'disgust',
2: 'fear',
3: 'happy',
4: 'sad',
5: 'surprised',
6: 'normal'
}
return emotion[predicted.item()], probability
def preprocess(self, inputs):
trans = transforms.Compose([
transforms.Grayscale(),
transforms.ToTensor(),
])
inputs = trans(inputs)
inputs = inputs.unsqueeze(0)
return inputs
class App:
def __init__(self, video_source=0):
self.window = Tk()
self.window.title('基于CNN的情绪识别系统')
self.video_source = video_source
self.emo_cls = EmotionClassifier()
self.canvas = Canvas(self.window, width=640, height=480)
self.canvas.pack(side=LEFT)
self.results_label = Label(self.window, text='情绪结果:', font=('Arial', 24), padx=20, pady=10)
self.results_label.pack(side=TOP)
self.bar_canvas = Canvas(self.window, width=320, height=240)
self.bar_canvas.pack(side=TOP)
self.quit_button = Button(self.window, text='退出系统', font=('Arial', 18), command=self.window.quit)
self.quit_button.pack(side=BOTTOM, padx=20, pady=10)
self.delay = 10
self.update()
self.window.mainloop()
def update(self):
ret, frame = cap.read()
if ret:
frame = cv2.flip(frame, 1)
img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
rendered_img = ImageTk.PhotoImage(img)
self.canvas.img = rendered_img
self.canvas.create_image(0, 0, anchor=NW, image=rendered_img)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
faces = self.emo_cls.face_cascade.detectMultiScale(gray, 1.3, 5)
for (x, y, w, h) in faces:
face = cv2.resize(frame[y:(y + h), x:(x + w)], (42, 42))
emotion, probability = self.emo_cls.get_emotion(Image.fromarray(face))
results_str = '情绪检测结果:{}'.format(emotion)
self.results_label.config(text=results_str)
self.bar_canvas.delete('all')
bar_height = self.bar_canvas.winfo_height()
bar_width = self.bar_canvas.winfo_width()
max_prob = np.max(probability)
prob_heights = [int(round(x / max_prob * bar_height)) for x in probability]
prob_colors = ['red', 'blue', 'green', 'yellow', 'purple', 'brown', 'orange']
for i in range(len(prob_heights)):
start_x = 0
start_y = bar_height / len(prob_heights) * i
end_x = prob_heights[i] / bar_height * bar_width
end_y = bar_height / len(prob_heights) * (i + 1)
# 拼接情绪标签、表情和百分比
emotion_text = ['愤怒', '厌恶', '恐惧', '高兴', '悲伤', '惊讶', '正常'][i]
emoji_text = ['Anger ',' disgust ', 'fear ',' happy ', 'sad ',' surprise ', 'normal'][i]
prob_text = '{:.2f}%'.format(probability[i] * 100)
label_text = '{} {} {}'.format(emotion_text, emoji_text, prob_text)
label_x = end_x
label_y = (start_y + end_y) / 2
self.bar_canvas.create_rectangle(start_x, start_y, end_x, end_y, fill=prob_colors[i])
self.bar_canvas.create_text(label_x, label_y, text=label_text, font=('Arial', 12), anchor=W)
self.window.after(self.delay, self.update)