神经网络负损失不降?原因分析与解决策略
2025-01-23 13:48:04
神经网络负损失不下降的排查与解决
训练神经网络时,观察到损失函数(loss function)的值呈现出正值部分下降,负值部分增大的反常现象,这表明训练过程可能存在一些问题。理想的训练过程是整体损失值逐步减小,即无论是正损失还是负损失都应趋向于零。负损失值的出现通常指示网络在某些样本上的预测结果与实际值差距较大,而正损失则代表其他样本预测较为接近。本篇文章会分析这种现象背后可能的成因并提供相应的解决策略。
常见原因分析
-
不平衡的数据集: 数据集中若存在显著的不平衡,例如,某些类别的样本远多于其他类别,可能会导致网络在多数类别上获得较低的损失,而在少数类别上损失较大甚至反向增大。这意味着模型更偏向于拟合数据量较大的类别,而忽略了少数类别的重要性。
-
不合适的激活函数: 激活函数的选择直接影响了神经网络的表现力与收敛速度,例如sigmoid激活函数容易在输出两端出现梯度消失现象,使得网络权重难以更新,无法使正负损失同步下降。
-
学习率设置不当: 过大的学习率可能导致损失值在最小值附近跳跃,而过小的学习率则会导致训练过程过于缓慢甚至陷入局部最小值。这两种情况都会使得负损失值无法下降。
-
初始化权重问题: 不合适的权重初始化,可能使网络陷入一个糟糕的起始状态,导致损失值出现振荡,难以稳定收敛。这可能导致部分数据出现过大或者负的误差。
-
梯度计算或更新错误: 反向传播过程中梯度计算有误,或是权重更新过程出现错误(如更新方向错误),都会使模型在某些样本上的损失值非但没下降反而增大。 这通常与代码的bug有关,例如维度不匹配。
解决方案
以下方法可以帮助我们排查和解决负损失值不下降的问题。
1. 数据集平衡处理
针对数据集不平衡,可以考虑以下措施:
- 数据增强: 通过对少数类别的样本进行旋转、平移、缩放等操作增加数据量。
- 重采样: 使用过采样 (重复少数类样本) 或欠采样 (减少多数类样本) 技术平衡数据。
- 使用带权重的损失函数: 对不同类别的损失赋予不同的权重,通常给予少数类别更大的权重。
- 使用更高级的平衡技术 :例如SMOTE(Synthetic Minority Oversampling Technique)或使用focal loss。
代码示例(数据增强, 使用OpenCV):
import cv2
import numpy as np
def augment_image(image, angle, scale):
rows, cols = image.shape[:2]
M = cv2.getRotationMatrix2D((cols / 2, rows / 2), angle, scale)
rotated_image = cv2.warpAffine(image, M, (cols, rows))
return rotated_image
# 假设有一个不平衡的数据集 image是样本, label是类别标签。
minority_class_images = [] #存放少数类图像
for i in range(len(minority_class_images)):
augmented_image = augment_image(minority_class_images[i], 15, 1.1)
#将增强后的图像添加到训练集中。
操作步骤:
1. 找出数据集中样本不平衡的类别。
2. 对少数类别的样本应用增强策略或者使用重采样。
3. 使用修改后的数据集重新训练神经网络。
4. 如果是带权重的loss,确保每个类别的损失权重都已经合理的配置。
2. 激活函数优化
sigmoid函数容易在两端梯度消失,可以尝试:
- ReLU 及其变种 (如 Leaky ReLU、ELU): 这些激活函数能更好地解决梯度消失的问题。
- 适当调整激活函数的参数: 例如调整Leaky ReLU 的泄露参数。
代码示例 (替换激活函数):
def leaky_relu(z, alpha=0.01):
return np.maximum(alpha*z, z)
def leaky_relu_derivative(z, alpha = 0.01):
dz = np.ones_like(z)
dz[z < 0] = alpha
return dz
# Feed Forward 中修改
def feedForward(x):
global z1
a1 = np.dot(x, w1)
z1 = leaky_relu(a1) # 将sigmoid改为relu 或 leaky relu等。
a2 = np.dot(z1, w2)
z2 = sigmoidActivation(a2) #第二层可以使用sigmoid函数或relu等函数,根据实际情况而定。
return z2
#backpropogation中修改。
def backPropogation(receivedOpt, actualOpt):
global z1
global dw2
global dw1
# Calculate delta2 , dz2 and dw2
delta2 = actualOpt.reshape(-1 , 1) - receivedOpt
dz2 = delta2 * sigmoidDerivative(receivedOpt).reshape(-1, 1)
dw2 = (1 / inputs) * np.dot(z1.T, dz2)
# Calculate delta1 , dz1 and dw1
delta1 = np.dot(dz2, w2.T)
dz1 = delta1 * leaky_relu_derivative(z1) # 根据feedforward的修改进行调整。
dw1 = (1 / inputs) * np.dot(inp.T, dz1)
操作步骤:
1. 根据您的神经网络结构选择合适的激活函数,例如对于隐层尝试 ReLU 系列。
2. 替换神经网络中对应的激活函数和他们的导数计算方法。
3. 再次运行神经网络进行训练,观察损失值的变化。
4. 若仍然不理想,可以尝试其他的激活函数进行微调,同时调整学习率。
3. 调整学习率
- 学习率衰减: 使用学习率衰减策略,使学习率随着训练次数增加逐渐减小,例如逐步衰减或指数衰减。
- 自适应学习率算法: 采用 Adam、RMSprop 等自适应学习率算法,这些算法会根据历史梯度调整学习率。
- 使用grid search,交叉验证等技术: 搜索一个比较合理的学习率。
代码示例 (使用Adam):
import tensorflow as tf
optimizer = tf.keras.optimizers.Adam(learning_rate=0.001)
def updateWeights():
global w1
global w2
global optimizer
gradients1 = tf.convert_to_tensor(dw1, dtype = tf.float32)
gradients2 = tf.convert_to_tensor(dw2, dtype = tf.float32)
#apply gradients for both w1 and w2
w1 = w1 - optimizer.apply_gradients(zip(gradients1,[tf.Variable(w1)]))
w2 = w2 - optimizer.apply_gradients(zip(gradients2,[tf.Variable(w2)]))
操作步骤:
- 选择合适的学习率优化方法(比如Adam)
- 替换模型中原有的学习率更新方式为新的方法,以及梯度计算(代码示例使用tf做了示例)。
- 进行多次试验和观察损失值的变化情况, 选出一个效果好的学习率或方法。
4. 权重初始化
- Xavier/Glorot 初始化: 根据输入和输出的维度调整权重的初始化范围。
- He 初始化: 特别适用于 ReLU 激活函数。
- 使用预训练模型: 从一个在大规模数据集上预训练好的模型开始,微调权重。
代码示例 (使用 Xavier 初始化):
def xavier_initialization(rows, cols):
limit = np.sqrt(6 / (rows + cols))
return np.random.uniform(-limit, limit, size=(rows, cols))
#初始化权重的地方替换为下面这种。
w1 = xavier_initialization(inputs, hiddenUnits)
w2 = xavier_initialization(hiddenUnits, outputs)
操作步骤:
- 选择合适的初始化方法(比如Xavier)。
- 将现有模型的权重初始化策略替换为新的初始化方法。
- 进行多次试验观察初始化后的效果。
- 使用不同的方法比较看看哪个对效果有提升,比如和均匀分布或正态分布初始化对比。
5. 检查梯度计算与更新
仔细检查反向传播和权重更新的代码,特别是:
- 维度匹配: 确保矩阵乘法和向量加减法的维度正确。
- 梯度计算公式: 核实激活函数的导数是否正确计算。
- 权重更新方向: 检查更新公式是否正确,权重是否是沿着梯度下降的方向更新。
代码示例(修正代码示例的维度问题)
def backPropogation(receivedOpt, actualOpt):
global z1
global dw2
global dw1
# Calculate delta2 , dz2 and dw2
delta2 = (receivedOpt - actualOpt.reshape(-1 , 1))
dz2 = delta2 * sigmoidDerivative(receivedOpt).reshape(-1, 1)
dw2 = (1 / inputs) * np.dot(z1.T, dz2)
# Calculate delta1 , dz1 and dw1
delta1 = np.dot(dz2, w2.T)
dz1 = delta1 * sigmoidDerivative(z1)
dw1 = (1 / inputs) * np.dot(inp.T, dz1)
操作步骤:
1. 使用print或debug查看梯度的数值变化和维度信息。
2. 与反向传播公式进行核对。
3. 核对每个梯度的维度是否符合预期,是否在对应的位置,更新参数的维数是否也匹配。
安全建议
- 小批量训练: 使用小批量梯度下降训练神经网络,不仅可以加速训练,也能避免陷入局部极小值,提升模型的泛化能力。
- 梯度裁剪: 在梯度更新之前,可以对其设置一个上限值,从而避免梯度爆炸。
在调整过程中需要不断进行观察和验证,找到最适合的配置。以上方案针对了导致负损失值不下降的多种原因提供了相应的解决策略,务必理解各个方案的原理并根据实际情况灵活运用。