You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

情绪识别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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 09:52:02