一、项目说明

  给定数据集train.csv,要求使用卷积神经网络CNN,根据每个样本的面部图片判断出其表情。在本项目中,表情共分7类,分别为:(0)生气,(1)厌恶,(2)恐惧,(3)高兴,(4)难过,(5)惊讶和(6)中立(即面无表情,无法归为前六类)。所以,本项目实质上是一个7分类问题。

数据集介绍:

  (1)、CSV文件,大小为28710行X2305列;

  (2)、在28710行中,其中第一行为描述信息,即“label”和“feature”两个单词,其余每行内含有一个样本信息,即共有28709个样本;

  (3)、在2305列中,其中第一列为该样本对应的label,取值范围为0到6。其余2304列为包含着每个样本大小为48X48人脸图片的像素值(2304=48X48),每个像素值取值范围在0到255之间;

  (4)、数据集地址:https://pan.baidu.com/s/1hwrq5Abx8NOUse3oew3BXg ,提取码:ukf7 。

二、思路分析及代码实现

  给定的数据集是csv格式的,考虑到图片分类问题的常规做法,决定先将其全部可视化,还原为图片文件再送进模型进行处理。

  借助深度学习框架Pytorch1.0 CPU(穷逼)版本,搭建模型,由于需用到自己的数据集,因此我们需要重写其中的数据加载部分,其余用现成的API即可。

  作业要求使用CNN实现功能,因此基本只能在调参阶段自由发挥(不要鄙视调参,通过这次作业才发现,参数也不是人人都能调得好的,比如我)。

2.1 数据可视化

  我们需要将csv中的像素数据还原为图片并保存下来,在python环境下,很多库都能实现类似的功能,如pillow,opencv等。由于笔者对opencv较为熟悉,且opencv又是专业的图像处理库,因此决定采用opencv实现这一功能。

2.1.1 数据分离

  原文件中,label和人脸像素数据是集中在一起的。为了方便操作,决定利用pandas库进行数据分离,即将所有label 读出后,写入新创建的文件label.csv;将所有的像素数据读出后,写入新创建的文件data.csv。

  1. # 将label和像素数据分离
  2. import pandas as pd
  3.  
  4. # 修改为train.csv在本地的相对或绝对地址
  5. path = './/ml2019spring-hw3//train.csv'
  6. # 读取数据
  7. df = pd.read_csv(path)
  8. # 提取label数据
  9. df_y = df[['label']]
  10. # 提取feature(即像素)数据
  11. df_x = df[['feature']]
  12. # 将label写入label.csv
  13. df_y.to_csv('label.csv', index=False, header=False)
  14. # 将feature数据写入data.csv
  15. df_x.to_csv('data.csv', index=False, header=False)

  以上代码执行完毕后,在该代码脚本所在的文件夹下,就会生成两个新文件label.csv以及data.csv。在执行代码前,注意修改train.csv在本地的路径。

2.1.2 数据可视化

  将数据分离后,人脸像素数据全部存储在data.csv文件中,其中每行数据就是一张人脸。按行读取数据,利用opencv将每行的2304个数据恢复为一张48X48的人脸图片,并保存为jpg格式。在保存这些图片时,将第一行数据恢复出的人脸命名为0.jpg,第二行的人脸命名为1.jpg......,以方便与label[0]、label[1]......一一对应。

  1. import cv2
  2. import numpy as np
  3.  
  4. # 指定存放图片的路径
  5. path = './/face'
  6. # 读取像素数据
  7. data = np.loadtxt('data.csv')
  8.  
  9. # 按行取数据
  10. for i in range(data.shape[0]):
  11. face_array = data[i, :].reshape((48, 48)) # reshape
  12. cv2.imwrite(path + '//' + '{}.jpg'.format(i), face_array) # 写图片

  以上代码虽短,但涉及到大量数据的读取和大批图片的写入,因此占用的内存资源较多,且执行时间较长(视机器性能而定,一般要几分钟到十几分钟不等)。代码执行完毕,我们来到指定的图片存储路径,就能发现里面全部是写好的人脸图片。

  粗略浏览一下这些人脸图片,就能发现这些图片数据来源较广,且并不纯净。就前60张图片而言,其中就包含了正面人脸,如1.jpg;侧面人脸,如18.jpg;倾斜人脸,如16.jpg;正面人头,如7.jpg;正面人上半身,如55.jpg;动漫人脸,如38.jpg;以及毫不相关的噪声,如59.jpg。放大图片后仔细观察,还会发现不少图片上还有水印。种种因素均给识别提出了严峻的挑战。

2.2 在pytorch下创建数据集

  现在我们有了图片,但怎么才能把图片读取出来送给模型呢?

  最简单粗暴的方法就是直接用opencv将所有图片读取出来,以numpy中array的数据格式直接送给模型。如果这样做的话,会一次性把所有图片全部读入内存,占用大量的内存空间,且只能使用单线程,效率不高,也不方便后续操作。

  其实在pytorch中,有一个类(torch.utils.data.Dataset)是专门用来加载数据的,我们可以通过继承这个类来定制自己的数据集和加载方法。以下为基本流程。

2.2.1 创建data-label对照表

  首先,我们需要划分一下训练集和验证集。在本次作业中,共有28709张图片,取前24000张图片作为训练集,其他图片作为验证集。新建文件夹train和val,将0.jpg到23999.jpg放进文件夹train,将其他图片放进文件夹val。

  在继承torch.utils.data.Dataset类定制自己的数据集时,由于在数据加载过程中需要同时加载出一个样本的数据及其对应的label,因此最好能建立一个data-label对照表,其中记录着data和label的对应关系(“data-lable对照表”并非官方名词,这个技术流程是笔者参考了他人的博客后自己摸索的,这个名字也是笔者给命的名)。

  有童鞋看到这里就会提出疑问了:在人脸可视化过程中,每张图片的命名不都和label的存放顺序是一一对应关系吗,为什么还要多此一举,再重新建立data-label对照表呢?笔者在刚开始的时候也是这么想的,按顺序(0.jpg, 1.jpg, 2.jpg......)加载图片和label(label[0], label[1], label[2]......),岂不是方便、快捷又高效?结果在实际操作的过程中才发现,程序加载文件的机制是按照文件名首字母(或数字)来的,即加载次序是0,1,10,100......,而不是预想中的0,1,2,3......,因此加载出来的图片不能够和label[0],label[1],lable[2],label[3]......一一对应,所以建立data-label对照表还是相当有必要的。

  建立data-label对照表的基本思路就是:指定文件夹(train或val),遍历该文件夹下的所有文件,如果该文件是.jpg格式的图片,就将其图片名写入一个列表,同时通过图片名索引出其label,将其label写入另一个列表。最后利用pandas库将这两个列表写入同一个csv文件。 

  执行这段代码前,注意修改相关文件路径。代码执行完毕后,会在train和val文件夹下各生成一个名为dataset.csv的data-label对照表。 

  1. import os
  2. import pandas as pd
  3.  
  4. def data_label(path):
  5. # 读取label文件
  6. df_label = pd.read_csv('label.csv', header = None)
  7. # 查看该文件夹下所有文件
  8. files_dir = os.listdir(path)
  9. # 用于存放图片名
  10. path_list = []
  11. # 用于存放图片对应的label
  12. label_list = []
  13. # 遍历该文件夹下的所有文件
  14. for file_dir in files_dir:
  15. # 如果某文件是图片,则将其文件名以及对应的label取出,分别放入path_list和label_list这两个列表中
  16. if os.path.splitext(file_dir)[1] == ".jpg":
  17. path_list.append(file_dir)
  18. index = int(os.path.splitext(file_dir)[0])
  19. label_list.append(df_label.iat[index, 0])
  20.  
  21. # 将两个列表写进dataset.csv文件
  22. path_s = pd.Series(path_list)
  23. label_s = pd.Series(label_list)
  24. df = pd.DataFrame()
  25. df['path'] = path_s
  26. df['label'] = label_s
  27. df.to_csv(path+'\\dataset.csv', index=False, header=False)
  28.  
  29. def main():
  30. # 指定文件夹路径
  31. train_path = 'F:\\0gold\\ML\\LHY_class\\FaceData\\train'
  32. val_path = 'F:\\0gold\\ML\\LHY_class\\FaceData\\val'
  33. data_label(train_path)
  34. data_label(val_path)
  35.  
  36. if __name__ == "__main__":
  37. main()

  OK,代码执行完毕,让我们来看一看data-label对照表里面具体是什么样子吧!

2.2.2 重写Dataset类

  首先介绍一下Pytorch中Dataset类:Dataset类是Pytorch中图像数据集中最为重要的一个类,也是Pytorch中所有数据集加载类中应该继承的父类。其中父类中的两个私有成员函数getitem()和len()必须被重载,否则将会触发错误提示。其中getitem()可以通过索引获取数据,len()可以获取数据集的大小。在Pytorch源码中,Dataset类的声明如下:

  1. class Dataset(object):
  2. """An abstract class representing a Dataset.
  3.  
  4. All other datasets should subclass it. All subclasses should override
  5. ``__len__``, that provides the size of the dataset, and ``__getitem__``,
  6. supporting integer indexing in range from 0 to len(self) exclusive.
  7. """
  8.  
  9. def __getitem__(self, index):
  10. raise NotImplementedError
  11.  
  12. def __len__(self):
  13. raise NotImplementedError
  14.  
  15. def __add__(self, other):
  16. return ConcatDataset([self, other])

  我们通过继承Dataset类来创建我们自己的数据加载类,命名为FaceDataset。

  1. import torch
  2. from torch.utils import data
  3. import numpy as np
  4. import pandas as pd
  5. import cv2
  6.  
  7. class FaceDataset(data.Dataset):

  首先要做的是类的初始化。之前的data-label对照表已经创建完毕,在加载数据时需用到其中的信息。因此在初始化过程中,我们需要完成对data-label对照表中数据的读取工作。

  通过pandas库读取数据,随后将读取到的数据放入list或numpy中,方便后期索引。

  1. # 初始化
  2. def __init__(self, root):
  3. super(FaceDataset, self).__init__()
  4. # root为train或val文件夹的地址
  5. self.root = root
  6. # 读取data-label对照表中的内容
  7. df_path = pd.read_csv(root + '\\dataset.csv', header=None, usecols=[0]) # 读取第一列文件名
  8. df_label = pd.read_csv(root + '\\dataset.csv', header=None, usecols=[1]) # 读取第二列label
  9. # 将其中内容放入numpy,方便后期索引
  10. self.path = np.array(df_path)[:, 0]
  11. self.label = np.array(df_label)[:, 0]

  接着就要重写getitem()函数了,该函数的功能是加载数据。在前面的初始化部分,我们已经获取了所有图片的地址,在这个函数中,我们就要通过地址来读取数据。

  由于是读取图片数据,因此仍然借助opencv库。需要注意的是,之前可视化数据部分将像素值恢复为人脸图片并保存,得到的是3通道的灰色图(每个通道都完全一样),而在这里我们只需要用到单通道,因此在图片读取过程中,即使原图本来就是灰色的,但我们还是要加入参数从cv2.COLOR_BGR2GARY,保证读出来的数据是单通道的。读取出来之后,可以考虑进行一些基本的图像处理操作,如通过高斯模糊降噪、通过直方图均衡化来增强图像等(经试验证明,在本次作业中,直方图均衡化并没有什么卵用,而高斯降噪甚至会降低正确率,可能是因为图片分辨率本来就较低,模糊后基本上什么都看不清了吧)。读出的数据是48X48的,而后续卷积神经网络中nn.Conv2d() API所接受的数据格式是(batch_size, channel, width, higth),本次图片通道为1,因此我们要将48X48 reshape为1X48X48。

  1. # 读取某幅图片,item为索引号
  2. def __getitem__(self, item):
  3. face = cv2.imread(self.root + '\\' + self.path[item])
  4. # 读取单通道灰度图
  5. face_gray = cv2.cvtColor(face, cv2.COLOR_BGR2GRAY)
  6. # 高斯模糊
  7. # face_Gus = cv2.GaussianBlur(face_gray, (3,3), 0)
  8. # 直方图均衡化
  9. face_hist = cv2.equalizeHist(face_gray)
  10. # 像素值标准化
  11. face_normalized = face_hist.reshape(1, 48, 48) / 255.0 # 为与pytorch中卷积神经网络API的设计相适配,需reshape原图
  12. # 用于训练的数据需为tensor类型
  13. face_tensor = torch.from_numpy(face_normalized) # 将python中的numpy数据类型转化为pytorch中的tensor数据类型
  14. face_tensor = face_tensor.type('torch.FloatTensor') # 指定为'torch.FloatTensor'型,否则送进模型后会因数据类型不匹配而报错
  15. label = self.label[item]
  16. return face_tensor, label

  最后就是重写len()函数获取数据集大小了。self.path中存储着所有的图片名,获取self.path第一维的大小,即为数据集的大小。

  1. # 获取数据集样本个数
  2. def __len__(self):
  3. return self.path.shape[0]

  完整代码:

  1. class FaceDataset(data.Dataset):
  2. # 初始化
  3. def __init__(self, root):
  4. super(FaceDataset, self).__init__()
  5. self.root = root
  6. df_path = pd.read_csv(root + '\\dataset.csv', header=None, usecols=[0])
  7. df_label = pd.read_csv(root + '\\dataset.csv', header=None, usecols=[1])
  8. self.path = np.array(df_path)[:, 0]
  9. self.label = np.array(df_label)[:, 0]
  10.  
  11. # 读取某幅图片,item为索引号
  12. def __getitem__(self, item):
  13. face = cv2.imread(self.root + '\\' + self.path[item])
  14. # 读取单通道灰度图
  15. face_gray = cv2.cvtColor(face, cv2.COLOR_BGR2GRAY)
  16. # 高斯模糊
  17. # face_Gus = cv2.GaussianBlur(face_gray, (3,3), 0)
  18. # 直方图均衡化
  19. face_hist = cv2.equalizeHist(face_gray)
  20. # 像素值标准化
  21. face_normalized = face_hist.reshape(1, 48, 48) / 255.0 # 为与pytorch中卷积神经网络API的设计相适配,需reshape原图
  22. # 用于训练的数据需为tensor类型
  23. face_tensor = torch.from_numpy(face_normalized) # 将python中的numpy数据类型转化为pytorch中的tensor数据类型
  24. face_tensor = face_tensor.type('torch.FloatTensor') # 指定为'torch.FloatTensor'型,否则送进模型后会因数据类型不匹配而报错
  25. label = self.label[item]
  26. return face_tensor, label
  27.  
  28. # 获取数据集样本个数
  29. def __len__(self):
  30. return self.path.shape[0]

2.2.3 数据集的使用

  到此为止,我们已经成功地写好了自己的数据集加载类。那么这个类该如何使用呢?下面笔者将以训练集(train文件夹下的数据)加载为例,讲一下整个数据集加载类在模型训练过程中的使用方法。

  首先,我们需要将这个类实例化。

  1. # 数据集实例化(创建数据集)
  2. train_dataset = FaceDataset(root='E:\\WSD\\HW3\\FaceData\\train')

  train_dataset即为我们实例化的训练集,要想加载其中的数据,还需要DataLoader类的辅助。DataLoader类总是配合Dataset类一起使用,DataLoader类可以帮助我们分批次读取数据,也可以通过这个类选择读取数据的方式(顺序 or 随机乱序),还可以选择并行加载数据等,这个类并不要我们重写。

  1. # 载入数据并分割batch
  2. train_loader = data.DataLoader(train_dataset, batch_size)

  最后,我们就能直接从train_loader中直接加载出数据和label了,而且每次都会加载出一个批次(batch)的数据和label。

  1. for images, labels in train_loader:
  2. '''
  3. 通过images和labels训练模型
  4. '''

2.3 网络模型搭建

  通过Pytorch搭建基于卷积神经网络的分类器。刚开始是自己设计的网络模型,在训练时发现准确度一直上不去,折腾一周后走投无路,后来在github上找到了一个做表情识别的开源项目,用的是这个项目的模型结构,但还是没能达到项目中的精度(acc在74%)。下图为该开源项目中公布的两个模型结构,笔者用的是Model B ,且只采用了其中的卷积-全连接部分,如果大家希望进一步提高模型的表现能力,可以考虑向模型中添加Face landmarks + HOG features 部分。

  可以看出,在Model B 的卷积部分,输入图片shape为48X48X1,经过一个3X3X64卷积核的卷积操作,再进行一次2X2的池化,得到一个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层,取概率最高的类别为分类结果。

  我们可以通过继承nn.Module来定义自己的模型类。以下代码实现了上述的模型结构。需要注意的是,在代码中,数据经过最后含7个神经元的线性层后就直接输出了,并没有经过softmax层。这是为什么呢?其实这和Pytorch在这一块的设计机制有关。因为在实际应用中,softmax层常常和交叉熵这种损失函数联合使用,因此Pytorch在设计时,就将softmax运算集成到了交叉熵损失函数CrossEntropyLoss()内部,如果使用交叉熵作为损失函数,就默认在计算损失函数前自动进行softmax操作,不需要我们额外加softmax层。Tensorflow也有类似的机制。

  1. class FaceCNN(nn.Module):
  2. # 初始化网络结构
  3. def __init__(self):
  4. super(FaceCNN, self).__init__()
  5.  
  6. # 第一次卷积、池化
  7. self.conv1 = nn.Sequential(
  8. # 输入通道数in_channels,输出通道数(即卷积核的通道数)out_channels,卷积核大小kernel_size,步长stride,对称填0行列数padding
  9. # input:(bitch_size, 1, 48, 48), output:(bitch_size, 64, 48, 48), (48-3+2*1)/1+1 = 48
  10. nn.Conv2d(in_channels=1, out_channels=64, kernel_size=3, stride=1, padding=1), # 卷积层
  11. nn.BatchNorm2d(num_features=64), # 归一化
  12. nn.RReLU(inplace=True), # 激活函数
  13. # output(bitch_size, 64, 24, 24)
  14. nn.MaxPool2d(kernel_size=2, stride=2), # 最大值池化
  15. )
  16.  
  17. # 第二次卷积、池化
  18. self.conv2 = nn.Sequential(
  19. # input:(bitch_size, 64, 24, 24), output:(bitch_size, 128, 24, 24), (24-3+2*1)/1+1 = 24
  20. nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=1),
  21. nn.BatchNorm2d(num_features=128),
  22. nn.RReLU(inplace=True),
  23. # output:(bitch_size, 128, 12 ,12)
  24. nn.MaxPool2d(kernel_size=2, stride=2),
  25. )
  26.  
  27. # 第三次卷积、池化
  28. self.conv3 = nn.Sequential(
  29. # input:(bitch_size, 128, 12, 12), output:(bitch_size, 256, 12, 12), (12-3+2*1)/1+1 = 12
  30. nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, stride=1, padding=1),
  31. nn.BatchNorm2d(num_features=256),
  32. nn.RReLU(inplace=True),
  33. # output:(bitch_size, 256, 6 ,6)
  34. nn.MaxPool2d(kernel_size=2, stride=2),
  35. )
  36.  
  37. # 参数初始化
  38. self.conv1.apply(gaussian_weights_init)
  39. self.conv2.apply(gaussian_weights_init)
  40. self.conv3.apply(gaussian_weights_init)
  41.  
  42. # 全连接层
  43. self.fc = nn.Sequential(
  44. nn.Dropout(p=0.2),
  45. nn.Linear(in_features=256*6*6, out_features=4096),
  46. nn.RReLU(inplace=True),
  47. nn.Dropout(p=0.5),
  48. nn.Linear(in_features=4096, out_features=1024),
  49. nn.RReLU(inplace=True),
  50. nn.Linear(in_features=1024, out_features=256),
  51. nn.RReLU(inplace=True),
  52. nn.Linear(in_features=256, out_features=7),
  53. )
  54.  
  55. # 前向传播
  56. def forward(self, x):
  57. x = self.conv1(x)
  58. x = self.conv2(x)
  59. x = self.conv3(x)
  60. # 数据扁平化
  61. x = x.view(x.shape[0], -1)
  62. y = self.fc(x)
  63. return y

2.4 训练模型

  有了模型,就可以通过数据的前向传播和误差的反向传播来训练模型了。在此之前,还需要指定优化器(即学习率更新的方式)、损失函数以及训练轮数、学习率等超参数。

  在本次作业中,我们采用的优化器是SGD,即随机梯度下降,其中参数weight_decay为正则项系数;损失函数采用的是交叉熵;可以考虑使用学习率衰减。

  1. def train(train_dataset, batch_size, epochs, learning_rate, wt_decay):
  2. # 载入数据并分割batch
  3. train_loader = data.DataLoader(train_dataset, batch_size)
  4. # 构建模型
  5. model = FaceCNN()
  6. # 损失函数
  7. loss_function = nn.CrossEntropyLoss()
  8. # 优化器
  9. optimizer = optim.SGD(model.parameters(), lr=learning_rate, weight_decay=wt_decay)
  10. # 学习率衰减
  11. # scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.8)
  12. # 逐轮训练
  13. for epoch in range(epochs):
  14. # 记录损失值
  15. loss_rate = 0
  16. # scheduler.step() # 学习率衰减
  17. model.train() # 模型训练
  18. for images, labels in train_loader:
  19. # 梯度清零
  20. optimizer.zero_grad()
  21. # 前向传播
  22. output = model.forward(images)
  23. # 误差计算
  24. loss_rate = loss_function(output, labels)
  25. # 误差的反向传播
  26. loss_rate.backward()
  27. # 更新参数
  28. optimizer.step()

2.5 模型的保存与加载

  我们训练的这个模型相对较小,因此可以直接保存整个模型(包括结构和参数)。

  1. # 模型保存
  2. torch.save(model, 'model_net1.pkl')
  1. # 模型加载
  2. model_parm = 'model_net1.pkl'
  3. model = torch.load(net_parm)

三、源码分享

3.1 源代码

  代码在CPU上跑起来较慢,视超参数和机器性能不同,一般跑完需耗时几小时到几十小时不等。代码执行时,每轮输出一次损失值,每5轮输出一次在训练集和验证集上的正确率。有条件的可以在GPU上尝试。

  1. import torch
  2. import torch.utils.data as data
  3. import torch.nn as nn
  4. import torch.optim as optim
  5. import numpy as np
  6. import pandas as pd
  7. import cv2
  8.  
  9. # 参数初始化
  10. def gaussian_weights_init(m):
  11. classname = m.__class__.__name__
  12. # 字符串查找find,找不到返回-1,不等-1即字符串中含有该字符
  13. if classname.find('Conv') != -1:
  14. m.weight.data.normal_(0.0, 0.04)
  15.  
  16. # 人脸旋转,尝试过但效果并不好,本次并未用到
  17. def imgProcess(img):
  18. # 通道分离
  19. (b, g, r) = cv2.split(img)
  20. # 直方图均衡化
  21. bH = cv2.equalizeHist(b)
  22. gH = cv2.equalizeHist(g)
  23. rH = cv2.equalizeHist(r)
  24.  
  25. # 顺时针旋转15度矩阵
  26. M0 = cv2.getRotationMatrix2D((24,24),15,1)
  27. # 逆时针旋转15度矩阵
  28. M1 = cv2.getRotationMatrix2D((24,24),15,1)
  29. # 旋转
  30. gH = cv2.warpAffine(gH, M0, (48, 48))
  31. rH = cv2.warpAffine(rH, M1, (48, 48))
  32. # 通道合并
  33. img_processed = cv2.merge((bH, gH, rH))
  34. return img_processed
  35.  
  36. # 验证模型在验证集上的正确率
  37. def validate(model, dataset, batch_size):
  38. val_loader = data.DataLoader(dataset, batch_size)
  39. result, num = 0.0, 0
  40. for images, labels in val_loader:
  41. pred = model.forward(images)
  42. pred = np.argmax(pred.data.numpy(), axis=1)
  43. labels = labels.data.numpy()
  44. result += np.sum((pred == labels))
  45. num += len(images)
  46. acc = result / num
  47. return acc
  48.  
  49. class FaceDataset(data.Dataset):
  50. # 初始化
  51. def __init__(self, root):
  52. super(FaceDataset, self).__init__()
  53. self.root = root
  54. df_path = pd.read_csv(root + '\\dataset.csv', header=None, usecols=[0])
  55. df_label = pd.read_csv(root + '\\dataset.csv', header=None, usecols=[1])
  56. self.path = np.array(df_path)[:, 0]
  57. self.label = np.array(df_label)[:, 0]
  58.  
  59. # 读取某幅图片,item为索引号
  60. def __getitem__(self, item):
  61. # 图像数据用于训练,需为tensor类型,label用numpy或list均可
  62. face = cv2.imread(self.root + '\\' + self.path[item])
  63. # 读取单通道灰度图
  64. face_gray = cv2.cvtColor(face, cv2.COLOR_BGR2GRAY)
  65. # 高斯模糊
  66. # face_Gus = cv2.GaussianBlur(face_gray, (3,3), 0)
  67. # 直方图均衡化
  68. face_hist = cv2.equalizeHist(face_gray)
  69. # 像素值标准化
  70. face_normalized = face_hist.reshape(1, 48, 48) / 255.0
  71. face_tensor = torch.from_numpy(face_normalized)
  72. face_tensor = face_tensor.type('torch.FloatTensor')
  73. label = self.label[item]
  74. return face_tensor, label
  75.  
  76. # 获取数据集样本个数
  77. def __len__(self):
  78. return self.path.shape[0]
  79.  
  80. class FaceCNN(nn.Module):
  81. # 初始化网络结构
  82. def __init__(self):
  83. super(FaceCNN, self).__init__()
  84.  
  85. # 第一次卷积、池化
  86. self.conv1 = nn.Sequential(
  87. # 输入通道数in_channels,输出通道数(即卷积核的通道数)out_channels,卷积核大小kernel_size,步长stride,对称填0行列数padding
  88. # input:(bitch_size, 1, 48, 48), output:(bitch_size, 64, 48, 48), (48-3+2*1)/1+1 = 48
  89. nn.Conv2d(in_channels=1, out_channels=64, kernel_size=3, stride=1, padding=1), # 卷积层
  90. nn.BatchNorm2d(num_features=64), # 归一化
  91. nn.RReLU(inplace=True), # 激活函数
  92. # output(bitch_size, 64, 24, 24)
  93. nn.MaxPool2d(kernel_size=2, stride=2), # 最大值池化
  94. )
  95.  
  96. # 第二次卷积、池化
  97. self.conv2 = nn.Sequential(
  98. # input:(bitch_size, 64, 24, 24), output:(bitch_size, 128, 24, 24), (24-3+2*1)/1+1 = 24
  99. nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=1),
  100. nn.BatchNorm2d(num_features=128),
  101. nn.RReLU(inplace=True),
  102. # output:(bitch_size, 128, 12 ,12)
  103. nn.MaxPool2d(kernel_size=2, stride=2),
  104. )
  105.  
  106. # 第三次卷积、池化
  107. self.conv3 = nn.Sequential(
  108. # input:(bitch_size, 128, 12, 12), output:(bitch_size, 256, 12, 12), (12-3+2*1)/1+1 = 12
  109. nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, stride=1, padding=1),
  110. nn.BatchNorm2d(num_features=256),
  111. nn.RReLU(inplace=True),
  112. # output:(bitch_size, 256, 6 ,6)
  113. nn.MaxPool2d(kernel_size=2, stride=2),
  114. )
  115.  
  116. # 参数初始化
  117. self.conv1.apply(gaussian_weights_init)
  118. self.conv2.apply(gaussian_weights_init)
  119. self.conv3.apply(gaussian_weights_init)
  120.  
  121. # 全连接层
  122. self.fc = nn.Sequential(
  123. nn.Dropout(p=0.2),
  124. nn.Linear(in_features=256*6*6, out_features=4096),
  125. nn.RReLU(inplace=True),
  126. nn.Dropout(p=0.5),
  127. nn.Linear(in_features=4096, out_features=1024),
  128. nn.RReLU(inplace=True),
  129. nn.Linear(in_features=1024, out_features=256),
  130. nn.RReLU(inplace=True),
  131. nn.Linear(in_features=256, out_features=7),
  132. )
  133.  
  134. # 前向传播
  135. def forward(self, x):
  136. x = self.conv1(x)
  137. x = self.conv2(x)
  138. x = self.conv3(x)
  139. # 数据扁平化
  140. x = x.view(x.shape[0], -1)
  141. y = self.fc(x)
  142. return y
  143.  
  144. def train(train_dataset, val_dataset, batch_size, epochs, learning_rate, wt_decay):
  145. # 载入数据并分割batch
  146. train_loader = data.DataLoader(train_dataset, batch_size)
  147. # 构建模型
  148. model = FaceCNN()
  149. # 损失函数
  150. loss_function = nn.CrossEntropyLoss()
  151. # 优化器
  152. optimizer = optim.SGD(model.parameters(), lr=learning_rate, weight_decay=wt_decay)
  153. # 学习率衰减
  154. # scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.8)
  155. # 逐轮训练
  156. for epoch in range(epochs):
  157. # 记录损失值
  158. loss_rate = 0
  159. # scheduler.step() # 学习率衰减
  160. model.train() # 模型训练
  161. for images, labels in train_loader:
  162. # 梯度清零
  163. optimizer.zero_grad()
  164. # 前向传播
  165. output = model.forward(images)
  166. # 误差计算
  167. loss_rate = loss_function(output, labels)
  168. # 误差的反向传播
  169. loss_rate.backward()
  170. # 更新参数
  171. optimizer.step()
  172.  
  173. # 打印每轮的损失
  174. print('After {} epochs , the loss_rate is : '.format(epoch+1), loss_rate.item())
  175. if epoch % 5 == 0:
  176. model.eval() # 模型评估
  177. acc_train = validate(model, train_dataset, batch_size)
  178. acc_val = validate(model, val_dataset, batch_size)
  179. print('After {} epochs , the acc_train is : '.format(epoch+1), acc_train)
  180. print('After {} epochs , the acc_val is : '.format(epoch+1), acc_val)
  181.  
  182. return model
  183.  
  184. def main():
  185. # 数据集实例化(创建数据集)
  186. train_dataset = FaceDataset(root='E:\\WSD\\HW3\\FaceData\\train')
  187. val_dataset = FaceDataset(root='E:\\WSD\\HW3\\FaceData\\val')
  188. # 超参数可自行指定
  189. model = train(train_dataset, val_dataset, batch_size=128, epochs=100, learning_rate=0.1, wt_decay=0)
  190. # 保存模型
  191. torch.save(model, 'model_net1.pkl')
  192.  
  193. if __name__ == '__main__':
  194. main()

3.2 说明

  这是台湾大学李宏毅老师机器学习课程(2019年春季)第三次作业。在该数据集上,只用卷积神经网络和其他辅助手段,能达到的最高分类正确率在75%左右。我前后折腾了近3周,一方面因为能力有限,无人交流指导,另一方面是因为算力有限(穷逼一个,没有GPU),最终正确率也仅有63%。上面的源代码不是我的最终模型,一是因为我的模型本来就不好,过拟合有点严重;二是因为我希望大家能自己动手体验一波调参的乐趣。在此抛砖引玉,要是有哪个小伙伴有好的改进方法,欢迎来和我交流鸭~

参考资料:

  本次作业发布地址:https://ntumlta2019.github.io/ml-web-hw3/

  面部表情识别GitHub地址:https://github.com/amineHorseman/facial-expression-recognition-using-cnn

  Pytorch制作数据集:https://ptorch.com/news/215.html

            https://blog.csdn.net/Teeyohuang/article/details/79587125

基于卷积神经网络的面部表情识别(Pytorch实现)----台大李宏毅机器学习作业3(HW3)的更多相关文章

  1. Pytorch实现基于卷积神经网络的面部表情识别(详细步骤)

    文章目录 一.项目背景 二.数据处理 1.标签与特征分离 2.数据可视化 3.训练集和测试集 三.模型搭建 四.模型训练 五.完整代码 一.项目背景数据集cnn_train.csv包含人类面部表情的图 ...

  2. 基于卷积神经网络的人脸识别项目_使用Tensorflow-gpu+dilib+sklearn

    https://www.cnblogs.com/31415926535x/p/11001669.html 基于卷积神经网络的人脸识别项目_使用Tensorflow-gpu+dilib+sklearn ...

  3. 深度学习项目——基于卷积神经网络(CNN)的人脸在线识别系统

    基于卷积神经网络(CNN)的人脸在线识别系统 本设计研究人脸识别技术,基于卷积神经网络构建了一套人脸在线检测识别系统,系统将由以下几个部分构成: 制作人脸数据集.CNN神经网络模型训练.人脸检测.人脸 ...

  4. 硕毕论文_基于 3D 卷积神经网络的行为识别算法研究

    论文标题:基于 3D 卷积神经网络的行为识别算法研究 来源/作者机构情况: 中  国  地  质  大  学(北京),计算机学院,图像处理方向 解决问题/主要思想贡献: 1. 使用张量CP分解的原理, ...

  5. 卷积神经网络概念及使用 PyTorch 简单实现

    卷积神经网络 卷积神经网络(CNN)是深度学习的代表算法之一 .具有表征学习能力,能够按其阶层结构对输入信息进行平移不变分类,因此也被称为“平移不变人工神经网络”.随着深度学习理论的提出和数值计算设备 ...

  6. 【RS】Automatic recommendation technology for learning resources with convolutional neural network - 基于卷积神经网络的学习资源自动推荐技术

    [论文标题]Automatic recommendation technology for learning resources with convolutional neural network ( ...

  7. 完全基于卷积神经网络的seq2seq

    本文参考文献: Gehring J, Auli M, Grangier D, et al. Convolutional Sequence to Sequence Learning[J]. arXiv ...

  8. 基于卷积神经网络CNN的电影推荐系统

    本项目使用文本卷积神经网络,并使用MovieLens数据集完成电影推荐的任务. 推荐系统在日常的网络应用中无处不在,比如网上购物.网上买书.新闻app.社交网络.音乐网站.电影网站等等等等,有人的地方 ...

  9. 使用卷积神经网络CNN训练识别mnist

    算的的上是自己搭建的第一个卷积神经网络.网络结构比较简单. 输入为单通道的mnist数据集.它是一张28*28,包含784个特征值的图片 我们第一层输入,使用5*5的卷积核进行卷积,输出32张特征图, ...

随机推荐

  1. proxysql 学习一 proxysql docker 运行试用

    proxysql 是一个比较强大的mysql proxy 服务,支持动态mysql 实例调整,查询重写,查询cache,监控,数据镜像,读写分离 以及ha,最近已经发布了2.0 ,很值得试用下 环境准 ...

  2. canvas做动画

    一.绘制图片 ①加载图片 <!DOCTYPE html> <html lang="en"> <head> <meta charset=&q ...

  3. tensorflow学习(一)

    今天开始学习tensorflow框架,从极客学院下载了官方中文教程(15年翻译的),第一天开始学习第一章ng基本流程和原理,作为前奏.然后写了代码,验证一下,准确率确实非常高,非常好用.把代码上传,作 ...

  4. 记录python循环引用带来的MemoryError错误解决

    在以前的python中,没有遇到过这样的错误,上来之后还是很蒙蔽的,问了几个业内的人,他们 都给我说让我改服务器里面配置的东西, 因为是要给大家用的,服务器要保证长久运行,不能临时去修改这个,导致在大 ...

  5. 微信小程序之如何定义页面标题

    效果图: 这个标题是在哪里定义的呢?type.js核心代码如下(通常这段代码放在onLoad函数体内): wx.setNavigationBarTitle({ title: "支出类型列表& ...

  6. 范仁义html+css课程---5、列表

    范仁义html+css课程---5.列表 一.总结 一句话总结: 学会基本的使用有序列表.无序列表.定义列表,设置样式的话尽量通过css而不是属性 1.无序列表基本形式(实例)? ul标签包裹li标签 ...

  7. Can not add Newtonsoft.Json.Linq.JValue to Newtonsoft.Json.Linq.JObject.

    https://blog.csdn.net/zhouyingge1104/article/details/83307637 C#项目中使用NewtonSoft.json,报错提示: Can not a ...

  8. pg执行计划分析小笔记

    开发同事问,为什么一个标量子查询,放在where子句后进行大小判断,比不放在where子句后进行判断大小运行的更快?按道理加了一次判断,不是应该变慢么? 把语句拿过来,看了一下两个语句的执行计划: 语 ...

  9. odoo开发笔记--一个模块显示两个一级菜单

    场景描述: 在已启动开发的模块中,odoo顶部一级菜单只有一个“会员管理”,需求是:在同一级顶部菜单,增加新菜单“产品管理”.举例如图:       处理方式: 按照odoo的机制,实现这种效果,可以 ...

  10. 数据分析入门——pandas数据处理

    1,处理重复数据 使用duplicated检测重复的行,返回一个series,如果不是第一次出现,也就是有重复行的时候,则为True: 对应的,可以使用drop_duplicates来删除重复的行: ...