情绪识别CNN模型训练中损失函数不下降的问题解决
问题
我是神经网络开发新手,正在构建用于情绪识别的CNN模型。所使用的数据集结构为:DatasetName -> train -> 0、1、2、3、4、5、6、7,每个数字对应存储单一情绪图片的文件夹。数据集包含37553张尺寸不一的彩色图片,已统一调整为48x48大小。
训练代码如下:
# Imports import torch import torch.nn as nn import torch.optim as optim import torchvision.transforms as transforms import torchvision import torch.nn.functional as F import os from skimage import io from torch.utils.data import Dataset, DataLoader from torch.autograd import Variable # Custom dataset class EmotionDataset(Dataset): def __init__(self, root_dir, transform, train): self.root_dir = root_dir self.transform = transform self.train = train self.zero = sorted(os.listdir(os.path.join(self.root_dir, str(0)))) self.one = sorted(os.listdir(os.path.join(self.root_dir, str(1)))) self.two = sorted(os.listdir(os.path.join(self.root_dir, str(2)))) self.three = sorted(os.listdir(os.path.join(self.root_dir, str(3)))) self.four = sorted(os.listdir(os.path.join(self.root_dir, str(4)))) self.five = sorted(os.listdir(os.path.join(self.root_dir, str(5)))) self.six = sorted(os.listdir(os.path.join(self.root_dir, str(6)))) self.seven = sorted(os.listdir(os.path.join(self.root_dir, str(7)))) self.zerol = len(self.zero) self.onel = len(self.one) self.twol = len(self.two) self.threel = len(self.three) self.fourl = len(self.four) self.fivel = len(self.five) self.sixl = len(self.six) self.sevenl = len(self.seven) self.data = [self.zero, self.one, self.two, self.three, self.four, self.five, self.six, self.seven] self.length = self.zerol + self.onel + self.twol + self.threel + self.fourl + self.fivel + self.sixl + self.sevenl def __len__(self): return self.length def sub_folder(self, index): if index < self.zerol: return 0, index elif index >= self.zerol and index < self.zerol + self.onel: return 1, index - self.zerol elif index >= self.zerol + self.onel and index < self.zerol + self.onel + self.twol: return 2, index - (self.zerol + self.onel) elif index >= self.zerol + self.onel + self.twol and index < self.zerol + self.onel + self.twol + self.threel: return 3, index - (self.zerol + self.onel + self.twol) elif index >= self.zerol + self.onel + self.twol + self.threel and index < self.zerol + self.onel + self.twol + self.threel + self.fourl: return 4, index - (self.zerol + self.onel + self.twol + self.threel) elif index >= self.zerol + self.onel + self.twol + self.threel + self.fourl and index < self.zerol + self.onel + self.twol + self.threel + self.fourl + self.fivel: return 5, index - (self.zerol + self.onel + self.twol + self.threel + self.fourl) elif index >= self.zerol + self.onel + self.twol + self.threel + self.fourl + self.fivel and index < self.zerol + self.onel + self.twol + self.threel + self.fourl + self.fivel + self.sixl: return 6, index - (self.zerol + self.onel + self.twol + self.threel + self.fourl + self.fivel) else: return 7, index - (self.zerol + self.onel + self.twol + self.threel + self.fourl + self.fivel + self.sixl) def __getitem__(self, index): subfolder, mod_index = self.sub_folder(index) img_path = os.path.join(self.root_dir, str(subfolder), self.data[subfolder][mod_index]) image = io.imread(img_path) target = torch.tensor(int(subfolder)) if self.transform: image = self.transform(image) t = torchvision.transforms.Resize((48, 48)) image = t(image) #image = image.expand(3, -1, -1) return image, target # Set device device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # Convolutional Neural Network class ConvolutionalNeuralNetwork(nn.Module): def __init__(self, in_channels, num_classes): super(ConvolutionalNeuralNetwork, self).__init__() self.conv1 = nn.Conv2d(in_channels=in_channels, out_channels=8, kernel_size=(3,3), stride=(1,1), padding=(1,1)) #keeps size self.pool = nn.MaxPool2d(kernel_size=(2,2), stride =(2,2)) #cuts size in half self.conv2 = nn.Conv2d(in_channels=8, out_channels=16, kernel_size=(3,3), stride=(1,1), padding=(1,1)) #keeps size self.conv3 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=(3,3), stride=(1,1), padding=(1,1)) #keeps size self.conv4 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) # keeps size self.fc1 = nn.Linear(64*6*6, num_classes) def forward(self, x): x = F.relu((self.conv1(x))) x = self.pool(x) x = F.relu(self.conv2(x)) x = F.relu(self.conv3(x)) x = self.pool(x) x = F.relu(self.conv4(x)) x = self.pool(x) x = x.reshape(x.shape[0], -1) x = self.fc1(x) return x # Saving model def save_model(state, filename = "saved_model1.pth.tar"): print("Saving model") torch.save(state, filename) # Hyperparameters in_channels = 3 num_classes = 8 learning_rate = 0.01 batch_size = 64 num_epochs = 11 # Load Data train_set = EmotionDataset(root_dir = os.path.join("DatasetName", "train"), transform = transforms.ToTensor(), train=True) test_set = EmotionDataset(root_dir = os.path.join("DatasetName", "test"), transform = transforms.ToTensor(), train=False) train_loader = DataLoader(dataset=train_set, batch_size=batch_size, shuffle=True) test_loader = DataLoader(dataset=test_set, batch_size=batch_size, shuffle=False) # Init network model = ConvolutionalNeuralNetwork(in_channels=in_channels, num_classes=num_classes).to(device) # Loss - cost function criterion = nn.CrossEntropyLoss() # Learning algorithm optimizer = optim.Adam(model.parameters(), lr = learning_rate) # Training def train_model(): print("Training Model") for epoch in range(num_epochs): losses = [] for batch_idx, (data, targets) in enumerate(train_loader): # get data in cpu data = data.to(device=device) targets = targets.to(device=device) # forward scores = model(data) loss = criterion(scores, targets) losses.append(loss.item()) # backward optimizer.zero_grad() loss.backward() # gradient descent or adam step optimizer.step() print(f"Cost at epoch {epoch} is {sum(losses) / len(losses)}") # Testing def check_accuracy(loader, model): num_correct = 0 num_samples = 0 model.eval() with torch.no_grad(): if loader.dataset.train: print("Checking accuracy on training data") else: print("Checking accuracy on test data") for x, y in loader: x = x.to(device = device) y = y.to(device=device) scores = model(x) _, predictions = scores.max(1) num_correct += (predictions == y).sum() num_samples += predictions.size(0) print(f'Got {num_correct} / {num_samples} with accuracy {float(num_correct)/float(num_samples)*100:.2f}') model.train() if __name__ == "__main__": # train and save model train_model() saved_model = {'state_dict': model.state_dict(), 'optimizer': optimizer.state_dict()} save_model(saved_model) check_accuracy(train_loader, model) check_accuracy(test_loader, model)
我尝试调整了learning rate、batch_size、num_epochs等超参数,但训练时损失函数初始值后几乎不再下降;更换其他数据集后损失虽下降,但模型准确率极低。请问如何修改该CNN网络,使损失函数正常下降?
解决方案
1. 修正学习率设置
当前learning_rate=0.01对于Adam优化器来说过高,Adam默认推荐学习率为1e-3(0.001),过高的学习率会导致参数在最优值附近震荡,无法收敛。直接修改:
learning_rate = 1e-3 # 替换原来的0.01
2. 优化网络结构,添加正则化与批归一化
原网络特征提取能力不足且缺少防过拟合组件,做以下调整:
- 加入批归一化(BatchNorm2d):稳定训练过程,加速收敛
- 加入Dropout层:抑制过拟合
- 提升卷积层通道数,增加全连接中间层增强特征映射能力
修改后的网络代码:
class ConvolutionalNeuralNetwork(nn.Module): def __init__(self, in_channels, num_classes): super(ConvolutionalNeuralNetwork, self).__init__() # 卷积块1 self.conv1 = nn.Conv2d(in_channels=in_channels, out_channels=16, kernel_size=(3,3), stride=(1,1), padding=(1,1)) self.bn1 = nn.BatchNorm2d(16) self.pool = nn.MaxPool2d(kernel_size=(2,2), stride=(2,2)) # 卷积块2 self.conv2 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=(3,3), stride=(1,1), padding=(1,1)) self.bn2 = nn.BatchNorm2d(32) # 卷积块3 self.conv3 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=(3,3), stride=(1,1), padding=(1,1)) self.bn3 = nn.BatchNorm2d(64) # 卷积块4 self.conv4 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=(3,3), stride=(1,1), padding=(1,1)) self.bn4 = nn.BatchNorm2d(128) # 全连接层 self.fc1 = nn.Linear(128*3*3, 256) self.dropout = nn.Dropout(0.5) self.fc2 = nn.Linear(256, num_classes) def forward(self, x): x = self.pool(F.relu(self.bn1(self.conv1(x)))) x = self.pool(F.relu(self.bn2(self.conv2(x)))) x = self.pool(F.relu(self.bn3(self.conv3(x)))) x = self.pool(F.relu(self.bn4(self.conv4(x)))) x = x.reshape(x.shape[0], -1) x = F.relu(self.fc1(x)) x = self.dropout(x) x = self.fc2(x) return x
注:48x48图片经过4次MaxPool2d(每次尺寸减半)后,最终特征图尺寸为3x3,因此全连接层输入维度为
128*3*3
3. 完善数据预处理与增强
原预处理仅做了ToTensor(),缺乏归一化和数据增强,导致训练不稳定、泛化能力差:
# 训练集预处理(含增强) train_transform = transforms.Compose([ transforms.ToTensor(), transforms.Resize((48, 48)), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), # 适配彩色图的均值方差 transforms.RandomHorizontalFlip(p=0.5), # 随机水平翻转 transforms.RandomRotation(degrees=10) # 随机旋转 ]) # 测试集预处理(仅归一化和Resize) test_transform = transforms.Compose([ transforms.ToTensor(), transforms.Resize((48, 48)), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) # 加载数据集时替换transform train_set = EmotionDataset(root_dir=os.path.join("DatasetName", "train"), transform=train_transform, train=True) test_set = EmotionDataset(root_dir=os.path.join("DatasetName", "test"), transform=test_transform, train=False)
同时删除EmotionDataset类__getitem__方法中的手动Resize代码,避免重复操作:
# 删掉以下两行 t = torchvision.transforms.Resize((48, 48)) image = t(image)
4. 添加学习率调度器,优化训练逻辑
学习率随训练进程衰减,能帮助模型在后期更精细地收敛:
# 初始化optimizer后添加调度器 optimizer = optim.Adam(model.parameters(), lr=learning_rate) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5) # 每5个epoch学习率减半 # 修改train_model函数,每个epoch结束后更新学习率 def train_model(): print("Training Model") for epoch in range(num_epochs): losses = [] for batch_idx, (data, targets) in enumerate(train_loader): data = data.to(device=device) targets = targets.to(device=device) scores = model(data) loss = criterion(scores, targets) losses.append(loss.item()) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() # 更新学习率 print(f"Cost at epoch {epoch} is {sum(losses) / len(losses)}")
5. 简化数据集类实现
原sub_folder方法逻辑冗余,简化为更高效的方式:
class EmotionDataset(Dataset): def __init__(self, root_dir, transform, train): self.root_dir = root_dir self.transform = transform self.train = train self.image_paths = [] self.labels = [] # 遍历所有类别文件夹,收集图片路径和标签 for label in range(8): folder_path = os.path.join(root_dir, str(label)) for img_name in os.listdir(folder_path): self.image_paths.append(os.path.join(folder_path, img_name)) self.labels.append(label) def __len__(self): return len(self.image_paths) def __getitem__(self, index): img_path = self.image_paths[index] image = io.imread(img_path) target = torch.tensor(self.labels[index]) if self.transform: image = self.transform(image) return image, target
这种方式避免了索引计算错误,代码更简洁易维护。
内容的提问来源于stack exchange,提问作者HKG
相关产品推荐
相关产品推荐

