首先是AlexNet网络
一切都在图中
接下来说说改进方法:
(
1
)加入不同尺寸的卷积核。如上图
所示,
AlexNet
网络中
5
个卷积层 都使用了多个相同尺寸的卷积核来完成每个通道中的卷积任务。输入数据在经 过卷积操作后,生成与卷积核个数相同的特征。这些特征组合在一起构成了特 征图,作为下一个网络层的输入。为了获得更加充分的特征信息,对 AlexNet 网络进行修改。通过在一个卷积层中使用多个大小不一的卷积核,进行更加多
样化的卷积操作,一个卷积层中生成多个卷积通道。相当于对原始卷积操作进 行了扩展,这样能够得到包含更加充分特征信息的特征图。

利用四个不同尺寸的卷积核来替换原来的单尺寸卷积核。在第一个卷积层 中卷积核数量减少为 64
个,而第二个卷积层中需保留
256
个卷积核,所以对 5 *5 卷积核进行替换的的卷积核各有 64
个。在多尺寸卷积过程中,为了保证 卷积核卷积前后的尺寸不变以及方便卷积锚点(卷积核基准置的确定,将 卷积核尺寸设置为卷积操作中常见的 1*1 、
3 *3 、
5 *5 和
7 *7 四个尺寸。通过 四个不同尺寸的卷积核同时进行卷积操作,在一个卷积层中实现了多通道卷积。 改进后的第二个卷层与单尺寸卷积核相比,能够获得更加多样的信息。
(2)数据分布变化。网络的训练过程中,改变前层参数可能导致后层的 数据分布发生改变,为了避免这种情况,可以采取批量归一化(BN
)算法来确保所有层的输出数据均属于相同的分布。BN
算法处理后的数据均值为
0
,方差为 1
。数据归一化计算公式如下:

(3)引入全局平均池化层。就分类问题来说,经过多层卷积后,将最后 一个卷积层的特征图进行池化处理,并将其输入到全连接层。这些数据结构具 有二维多通道特性,可以用来进行降维和分类。降维操作是将特征图拉伸到一 维空间,分类操作是在 Softmax
层实现的。由于全连接层中参数量很多, Alexnet 的训练时间大部分都消耗在全连接层。通过全局平均池化,可以将特 征图中的所有特征的值进行统一处理,获得的各个特征均值可以直接对应 Softmax 层的分类概率,获得的数据数量也与分类数目相同。也就是说,经过对各个通道特征的全局平均池化处理,获得的平均值可以直接输入 Softmax
进 行分类。

原始的AlexNet可见我另一篇博客:基于AlexNet的人脸表情识别_JAFFE-CSDN博客
基于上述三点,基于AlexNet改进的多尺寸卷积网络代码如下:
import torch
import torch.nn as nn
class AlexNet_pro(nn.Module):
def __init__(self, num_classes=7):
super(AlexNet_pro, self).__init__()
# 第一阶段卷积
self.conv1 = nn.Sequential(
nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=2), # [N,3,227,227] -> [N,96,55,55]
nn.ReLU(inplace=True),
nn.BatchNorm2d(96),
nn.MaxPool2d(kernel_size=3, stride=2) # [N,96,27,27]
)
# 多分支卷积层(并行结构)
self.branch_conv = nn.ModuleDict({
'1x1': nn.Conv2d(96, 64, kernel_size=1),
'3x3': nn.Conv2d(96, 64, kernel_size=3, padding=1),
'5x5': nn.Conv2d(96, 64, kernel_size=5, padding=2),
'7x7': nn.Conv2d(96, 64, kernel_size=7, padding=3)
})
# 合并后的处理
self.post_branch = nn.Sequential(
nn.BatchNorm2d(256), # 4 branches * 64 = 256
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2) # [N,256,13,13]
)
# 后续卷积层
self.conv3 = nn.Sequential(
nn.Conv2d(256, 384, kernel_size=3, padding=1),
nn.BatchNorm2d(384),
nn.ReLU(inplace=True),
nn.Conv2d(384, 384, kernel_size=3, padding=1),
nn.BatchNorm2d(384),
nn.ReLU(inplace=True),
nn.Conv2d(384, 256, kernel_size=3, padding=1),
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2) # [N,256,6,6]
)
# 分类器
self.classifier = nn.Sequential(
nn.AdaptiveAvgPool2d(1), # 全局平均池化
nn.Dropout(0.5),
nn.Flatten(),
nn.Linear(256, 4096),
nn.BatchNorm1d(4096),
nn.ReLU(inplace=True),
nn.Dropout(0.5),
nn.Linear(4096, 1024),
nn.ReLU(inplace=True),
nn.Linear(1024, num_classes)
)
# 初始化权重
self._initialize_weights()
def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
nn.init.constant_(m.bias, 0)
def forward(self, x):
x = self.conv1(x)
# 并行处理四个分支
branch1 = self.branch_conv['1x1'](x)
branch2 = self.branch_conv['3x3'](x)
branch3 = self.branch_conv['5x5'](x)
branch4 = self.branch_conv['7x7'](x)
# 合并分支
x = torch.cat([branch1, branch2, branch3, branch4], dim=1)
x = self.post_branch(x)
x = self.conv3(x)
x = self.classifier(x)
return x
接下来要确定图像的预处理:
图像预处理采用灰度调整的方法,改变图像中的灰度分布。灰度调整过程 将彩色图像中的 R
、
G
、
B
三个通道合并,通过对像素点进行转换,形成一幅 新的图像。利用加权平均法,根据 R
、
G
、B 分量对人眼亮度敏感度的不同, 分配不同的权重,实现更好的图像灰度表示

为了改善图像的可观性,使用直方图平衡技术,提高图像的清晰度和对比 度,使得每个灰度级的数量基本相同。直方图均衡化的公式如下:

其次,针对Fer2013数据集样本分布不均匀的问题,调整损失函数,由原先的交叉熵损失函数修改为改进的焦点损失函数
焦点损失由何凯明于
2017
年提出,最早用于提高物体检测效果。通过引 入焦点参数,交叉熵损失函数可以有效地减少容易被分类的样本的权重,从而 提升稀有样本的权重,这将大大提升网络在特征学习中对于复杂样本的处理能 力。焦点损失函数公式如下:

p
i
计算方法如下:

焦点损失函数解决了样本数量不均衡问题并且调整了稀有样本在损失度量 中所起的作用。但当样本数据中存在少量误标注或者一些非分类目标的噪声数 据时,这些本应该在学习过程中被忽略掉的噪声反而会被焦点参数放大。网络会从噪声数据中获取到更多的信息,从而导致网络的性能下降。
然后介绍下我们使用的数据集Fer2013
FER-2013(Facial Expression Recognition 2013)是一个广泛应用于面部表情识别研究的数据集。它由 Pierre-Luc Carrier 和 Aaron Courville 在 2013 年创建,并通过 Kaggle 平台发布。FER-2013 数据集因其规模较大、多样性丰富而成为面部表情识别领域的基准数据集之一。
-
样本数量:
-
FER-2013 包含 35,887 张灰度图像。
-
图像分为 训练集(28,709 张)、验证集(3,589 张)和 测试集(3,589 张)。
-
-
表情类别:
-
7 种基本表情类别:
-
Anger(生气)
-
Disgust(厌恶)
-
Fear(恐惧)
-
Happy(高兴)
-
Sad(悲伤)
-
Surprise(惊讶)
-
Neutral(中性)
-
-
-
图像格式:
-
图像为灰度图,分辨率为 48x48 像素。
-
每张图像以像素值的形式存储在一个 CSV 文件中。
-
-
数据来源:
-
图像是通过 Google 图像搜索 API 收集的,涵盖了不同年龄、性别和种族的人群。
-
数据集具有较高的多样性,但也包含一些噪声(如标注错误、模糊图像等)。
-
由于其是CSV文件,图像没有直观的展示,可使用如下代码查看:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# 加载 FER2013 数据集
def load_fer2013(file_path):
data = pd.read_csv(file_path)
pixels = data['pixels'].tolist()
emotions = data['emotion'].tolist()
return pixels, emotions
# 将像素字符串转换为图像数组
def string_to_image(pixel_string):
pixel_list = list(map(int, pixel_string.split()))
image_array = np.array(pixel_list).reshape(48, 48)
return image_array
# 显示图像
def show_image(image_array, emotion):
plt.imshow(image_array, cmap='gray')
plt.title(f'Emotion: {emotion}')
plt.axis('off')
plt.show()
file_path = 'fer2013.csv' # 替换为你的文件路径
pixels, emotions = load_fer2013(file_path)
# 显示前 100 张图片
for i in range(100):
image_array = string_to_image(pixels[i])
show_image(image_array, emotions[i])
最后就是基于上述内容,进行图像预处理,损失函数的改进,模型训练代码的编写:
import numpy as np
import pandas as pd
from PIL import Image
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms
# 自定义数据集类
class Fer2013Dataset(Dataset):
def __init__(self, csv_file, usage='Training', transform=None):
"""
Args:
csv_file (str): Fer2013 数据集的 CSV 文件路径。
usage (str): 数据集用途,可选 'Training', 'PublicTest', 'PrivateTest'。
transform (callable, optional): 数据预处理方法。
"""
self.data = pd.read_csv(csv_file)
self.data = self.data[self.data['Usage'] == usage] # 根据 Usage 筛选数据
self.transform = transform
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
# 获取图像数据(字符串形式)
image_data = self.data.iloc[idx, 1] # 第二列是像素数据
# 将字符串转换为 numpy 数组
image = np.array([int(pixel) for pixel in image_data.split()], dtype=np.uint8)
image = image.reshape(48, 48) # Fer2013 图像尺寸为 48x48
# 转换为 PIL 图像
image = Image.fromarray(image)
# 获取标签
label = self.data.iloc[idx, 0] # 第一列是标签
# 数据增强和预处理
if self.transform:
image = self.transform(image)
return image, label
import torch.nn as nn
import torch.nn.functional as F
# 焦点损失函数实现
class FocalLoss(nn.Module):
def __init__(self, alpha=0.25, gamma=2.0, reduction='mean'):
"""
Args:
alpha (float/tensor): 类别权重,可设为标量(平衡因子)或各类别权重的张量
gamma (float): 难易样本调节因子
reduction (str): 损失聚合方式 ('mean', 'sum', 'none')
"""
super(FocalLoss, self).__init__()
self.alpha = alpha
self.gamma = gamma
self.reduction = reduction
def forward(self, inputs, targets):
# 计算交叉熵损失
ce_loss = F.cross_entropy(inputs, targets, reduction='none') # [N]
# 计算概率 p_t
p = torch.exp(-ce_loss) # p_t = exp(-CE)
# 计算焦点损失
focal_loss = (self.alpha * (1 - p) ** self.gamma * ce_loss) # [N]
# 聚合方式
if self.reduction == 'mean':
return focal_loss.mean()
elif self.reduction == 'sum':
return focal_loss.sum()
else:
return focal_loss
# 数据预处理管道
transform_train = transforms.Compose([
transforms.Grayscale(num_output_channels=3), # 转为 3 通道
transforms.Resize((227, 227)), # 调整尺寸为 227x227
transforms.RandomHorizontalFlip(), # 随机水平翻转
transforms.RandomRotation(10), # 随机旋转
transforms.ToTensor(), # 转为 Tensor
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) # 标准化
])
transform_test = transforms.Compose([
transforms.Grayscale(num_output_channels=3), # 转为 3 通道
transforms.Resize((227, 227)), # 调整尺寸为 227x227
transforms.ToTensor(), # 转为 Tensor
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) # 标准化
])
# 加载数据集
train_dataset = Fer2013Dataset(csv_file='../fer2013.csv', usage='Training', transform=transform_train)
val_dataset = Fer2013Dataset(csv_file='../fer2013.csv', usage='PublicTest', transform=transform_test)
test_dataset = Fer2013Dataset(csv_file='../fer2013.csv', usage='PrivateTest', transform=transform_test)
# 创建数据加载器
batch_size = 64
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
import torch
import torch.optim as optim
from tqdm import tqdm
from AlexNET import AlexNet_pro
# 设备配置
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 初始化模型
model = AlexNet_pro(num_classes=7).to(device)
# 定义损失函数和优化器
criterion = FocalLoss(alpha=0.25, gamma=2.0).to(device)
optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)
# 训练参数
num_epochs = 30
best_acc = 0.0
# 训练循环
for epoch in range(num_epochs):
model.train()
running_loss = 0.0
correct = 0
total = 0
# 训练阶段
for images, labels in tqdm(train_loader, desc=f"Epoch {epoch + 1}/{num_epochs}"):
images = images.to(device)
labels = labels.to(device)
# 前向传播
outputs = model(images)
loss = criterion(outputs, labels)
# 反向传播和优化
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 统计损失和准确率
running_loss += loss.item()
_, predicted = outputs.max(1)
total += labels.size(0)
correct += predicted.eq(labels).sum().item()
# 计算训练损失和准确率
train_loss = running_loss / len(train_loader)
train_acc = 100.0 * correct / total
# 验证阶段
model.eval()
val_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for images, labels in val_loader:
images = images.to(device)
labels = labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
val_loss += loss.item()
_, predicted = outputs.max(1)
total += labels.size(0)
correct += predicted.eq(labels).sum().item()
# 计算验证损失和准确率
val_loss /= len(val_loader)
val_acc = 100.0 * correct / total
# 打印结果
print(f"Epoch [{epoch + 1}/{num_epochs}] "
f"Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}% | "
f"Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.2f}%")
# 保存最佳模型
if val_acc > best_acc:
best_acc = val_acc
torch.save(model.state_dict(), "model_Fer2013.pth")
# 更新学习率
scheduler.step()
# 测试阶段
model.load_state_dict(torch.load("model_Fer2013.pth"))
model.eval()
test_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for images, labels in test_loader:
images = images.to(device)
labels = labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
test_loss += loss.item()
_, predicted = outputs.max(1)
total += labels.size(0)
correct += predicted.eq(labels).sum().item()
# 计算测试损失和准确率
test_loss /= len(test_loader)
test_acc = 100.0 * correct / total
print(f"Test Loss: {test_loss:.4f}, Test Acc: {test_acc:.2f}%")
print(f"Training complete. Best validation accuracy: {best_acc:.2f}%")
训练时间略久,1h左右,因GPU而异
在Fer2013上的表现其实感觉一般,可以考虑用JAFFE横向对比,看看是否有改进。
以下代码是在 JAFFE上训练测试:
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import transforms, datasets, models
from torch.utils.data import DataLoader, random_split
import os
from AlexNET import AlexNet_pro
# 设备配置
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
import cv2
import numpy as np
from PIL import Image
# 定义图像预处理方法
def preprocess_image(image):
# 1. 将图像转为灰度
image_gray = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2GRAY)
# 2. 进行直方图均衡化
image_eq = cv2.equalizeHist(image_gray)
# 3. 转换回三通道,复制灰度值到R, G, B通道
image_eq_rgb = cv2.cvtColor(image_eq, cv2.COLOR_GRAY2RGB)
# 将图像转换为PIL格式
return Image.fromarray(image_eq_rgb)
import torch.nn as nn
import torch.nn.functional as F
# 焦点损失函数实现
class FocalLoss(nn.Module):
def __init__(self, alpha=0.25, gamma=2.0, reduction='mean'):
"""
Args:
alpha (float/tensor): 类别权重,可设为标量(平衡因子)或各类别权重的张量
gamma (float): 难易样本调节因子
reduction (str): 损失聚合方式 ('mean', 'sum', 'none')
"""
super(FocalLoss, self).__init__()
self.alpha = alpha
self.gamma = gamma
self.reduction = reduction
def forward(self, inputs, targets):
# 计算交叉熵损失
ce_loss = F.cross_entropy(inputs, targets, reduction='none') # [N]
# 计算概率 p_t
p = torch.exp(-ce_loss) # p_t = exp(-CE)
# 计算焦点损失
focal_loss = (self.alpha * (1 - p) ** self.gamma * ce_loss) # [N]
# 聚合方式
if self.reduction == 'mean':
return focal_loss.mean()
elif self.reduction == 'sum':
return focal_loss.sum()
else:
return focal_loss
# 图像预处理管道
transform = transforms.Compose([
transforms.Resize(256), # 图像调整为 256*256
transforms.CenterCrop(227), # 中心裁剪 227*227
transforms.Lambda(lambda x: preprocess_image(x)), # 自定义预处理方法
transforms.ToTensor(), # 转为Tensor
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) # 标准化
])
# 加载数据集
dataset_path = "../jaffe"
full_dataset = datasets.ImageFolder(root=dataset_path, transform=transform)
# 划分训练集和验证集 (85-15)
train_size = int(0.85 * len(full_dataset))
val_size = len(full_dataset) - train_size
train_dataset, val_dataset = random_split(full_dataset, [train_size, val_size])
# 创建数据加载器
batch_size = 64
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)
# 初始化模型
model = AlexNet_pro(num_classes=7).to(device)
# 定义损失函数和优化器
criterion = FocalLoss(alpha=0.25, gamma=2.0).to(device)
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 训练参数
num_epochs = 100
best_acc = 0.0
# 训练循环
for epoch in range(num_epochs):
# 训练阶段
model.train()
running_loss = 0.0
for images, labels in train_loader:
images = images.to(device)
labels = labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item() * images.size(0)
epoch_loss = running_loss / len(train_dataset)
# 验证阶段
model.eval()
correct = 0
total = 0
with torch.no_grad():
for images, labels in val_loader:
images = images.to(device)
labels = labels.to(device)
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
acc = 100 * correct / total
print(f"Epoch [{epoch + 1}/{num_epochs}] "
f"Train Loss: {epoch_loss:.4f} "
f"Val Acc: {acc:.2f}%")
# 保存最佳模型
if acc > best_acc:
best_acc = acc
torch.save(model.state_dict(), "model_JAFFE.pth")
print(f"Training complete. Best validation accuracy: {best_acc:.2f}%")
对比原始的AlexNet,有显著提升!!