深度学习中损失函数(loss function)介绍
在深度学习的宏伟城堡中,损失函数扮演着国王的角色,它决定了模型训练的方向和目标。损失函数,也被称为代价函数,是衡量模型预测与实际结果之间差异的函数。在深度学习的训练过程中,我们的目标就是最小化这个损失函数,就像是在一场游戏中,我们的目标是获得尽可能低的失误和丢分。
损失函数的选择对于模型的训练至关重要。不同的问题可能需要不同的损失函数。比如在图像识别中,我们可能需要一个能够处理大量类别的损失函数,这时候交叉熵损失就是一个很好的选择。而在预测房价等连续值的问题中,均方误差损失可能更为合适。
损失函数不仅是评价模型性能的标尺,也是指导模型学习的方向。在训练过程中,我们通过计算损失函数的梯度,并使用梯度下降算法来更新模型的权重,以此来减少损失函数的值。
1. 损失函数的作用
1-1. 损失函数作用
想象一下,你在玩一个投飞镖的游戏,而飞镖靶就是损失函数。你的每一次投掷,都对应着模型的一次预测。飞镖落在靶上的位置,就是预测结果与实际结果之间的误差。损失函数的作用就是告诉我们,我们的飞镖投得有多准,或者说,我们的模型预测得有多准确。
在深度学习中,损失函数计算模型输出(预测值)和真实标签(实际值)之间的差异。这个差异,或者说"误差",量化了模型的性能。训练深度学习模型的过程,本质上就是通过调整模型的参数来最小化损失函数的过程。损失函数通常被视为一种 优化目标 ,它在训练过程中不断变化,我们希望通过优化算法(如梯度下降)来不断 减小损失函数,使得模型更精确地拟合数据。
1-2. 为什么要让损失函数持续下降
损失函数的下降反映了模型的性能提高,因此我们希望通过调整模型参数使损失函数尽可能持续下降。具体来说:
- 反映模型拟合度:损失函数值越小,说明模型在训练数据上的拟合程度越高,预测误差越小。
- 优化目标 :在机器学习和深度学习中,我们的最终目标是训练出一个 尽可能准确 的模型,使得它对未见过的数据(测试数据或未来数据)具有较好的预测能力。为了达到这个目标,必须尽量 最小化损失函数,降低模型的误差。
- 梯度下降优化:优化过程的核心就是通过梯度下降法(或其变种)不断更新模型参数,以减少损失函数。每次更新时,都会使用损失函数来计算梯度,并沿着梯度的方向调整参数。
- 避免过拟合或欠拟合:通过不断调整模型参数(如网络层、权重等),让损失函数下降,可以防止模型在训练数据上产生过拟合或欠拟合。过拟合是指模型在训练数据上表现很好,但在新数据(测试数据)上表现差;欠拟合则是指模型的表达能力不足,无法有效地拟合训练数据。
模型的拟合度(Model Fit)指的是模型在训练数据上的表现程度,衡量模型能够多好地捕捉训练数据中的规律和模式。拟合度越高,表示模型能够更精确地预测训练数据中的目标变量。具体来说:
- 高拟合度 :模型能够准确地预测训练数据中的结果,误差较小。这通常意味着模型的复杂度较高,可以很好地表达数据中的模式和关系。然而,过高的拟合度可能导致过拟合,即模型过度依赖训练数据中的噪声,不能很好地推广到新数据。
- 低拟合度 :模型不能很好地拟合训练数据,误差较大。这通常表明模型过于简单,无法捕捉数据的复杂规律,可能存在欠拟合现象。欠拟合的模型通常表现出较高的训练误差和较低的泛化能力。
为了避免过拟合和欠拟合,常通过调整模型的复杂度、损失函数、训练数据的量和质量、正则化方法等,来优化模型的拟合度,使其既能在训练集上取得较好的结果,又能在测试集或实际应用中具有较好的预测能力。
梯度下降是通过不断调整参数来最小化损失函数,最终使模型学习到最优的参数,从而提高预测准确性。假设你正在训练一个神经网络,并且已经得到了初始的参数(例如权重)。你计算出当前参数下的损失函数值(比如MSE损失)。接下来,你计算每个参数的梯度(即每个权重对损失函数的影响)。然后,你根据这些梯度调整权重,使得损失函数逐渐减小,模型预测更加准确。这个过程重复进行,直到损失函数的值降到足够小为止。
1-3. 损失函数下降的目标
- 模型优化:每次调整参数时,我们希望通过梯度下降法(或其他优化算法)最小化损失函数。这是因为只有在训练过程中,模型的误差不断下降,才能有效地提高模型的预测性能。
- 收敛与稳定性:损失函数在训练过程中逐渐减少,通常是模型朝着最佳解(即最小化误差)收敛的标志。若损失函数在多个训练轮次中持续下降,表明模型已经开始稳定地找到最优的参数。
- 防止欠拟合和过拟合 :当损失函数 持续下降 到一个较低的值时,通常说明模型在训练集上拟合得较好。如果损失函数长时间不下降,可能表明模型无法有效拟合数据,产生了 欠拟合 。如果损失函数持续下降,并且在训练集和测试集上都表现良好,模型可能会很好地泛化,避免 过拟合。
1-4. 损失函数下降的具体作用
- 减少误差:损失函数的值反映了模型的预测值与真实值之间的误差。通过不断降低损失函数,我们希望减少模型的误差,使其预测更加精确。
- 训练收敛:训练中的优化目标是让损失函数趋于最小。随着训练轮次的增加,损失值应当逐渐减少,模型的参数也会越来越接近最优解。损失函数达到某个极小值时,表示训练收敛,模型已经学到了数据中的规律。
- 提高泛化能力 :一个低损失函数值通常意味着模型不仅在训练集上表现好,同时也在未见过的数据上(例如验证集或测试集)具有较好的预测能力。这有助于提升模型的 泛化能力。
1-5. 如何让损失函数持续下降
让损失函数持续下降通常依赖于以下几个因素:
- 合理的学习率(Learning Rate):学习率过大可能导致损失函数震荡或发散,学习率过小可能导致训练过程非常缓慢。通过调整学习率,可以使得梯度下降更加平稳,使得损失函数持续下降。
- 优化器选择:不同的优化器(如SGD、Adam、RMSprop等)会影响训练过程中的参数更新方式,进而影响损失函数的下降速度。Adam优化器通常能加速训练并使得损失函数下降得更快。
- 模型架构:模型的复杂度(层数、神经元数量等)也影响损失函数的下降速度。如果模型太简单,可能无法捕捉数据的复杂性,导致损失函数下降缓慢或者停滞。如果模型过于复杂,则可能导致过拟合,使损失函数在测试数据上不下降。
- 数据预处理:合适的数据处理方式(如标准化、归一化、去噪等)可以加速训练过程,让损失函数更快速地下降。
- 训练轮次(Epochs):适当增加训练轮次可以帮助模型有更多的机会去优化参数,从而让损失函数继续下降。
2. 常见的损失函数
-
均方误差损失(MSE) :
想象你在玩一个扔球进篮子的游戏,而篮子的大小就是均方误差损失。你每次扔球,球与篮子中心的距离平方就是你的得分。游戏的目标是尽可能让得分低,也就是让球尽可能接近篮子中心。在深度学习中,均方误差损失函数计算预测值与实际值之间差的平方的平均值,常用于回归问题。
-
交叉熵损失(Cross-Entropy Loss) :
想象你在玩一个猜谜游戏,每次猜测后,系统会告诉你猜测结果与正确答案之间的差异。交叉熵损失就是这样一个函数,它衡量的是模型输出的概率分布与真实标签的概率分布之间的差异。在分类问题中,当我们有多个类别时,交叉熵损失是首选的损失函数。
-
Hinge损失(Hinge Loss) :
想象你在玩一个挥剑砍靶的游戏,目标是让剑刃尽可能地砍进靶心。Hinge损失函数用于支持向量机(SVM)等最大间隔分类器,它鼓励模型产生更大的间隔,以提高分类的鲁棒性。
-
绝对值损失(Absolute Loss) :
想象你在玩一个射箭游戏,目标是射中靶心,而靶心的半径就是绝对值损失。你的箭与靶心的距离越小,得分越低。绝对值损失计算预测值与实际值之间差的绝对值的平均值,它对异常值的敏感度低于均方误差损失。
3. 损失函数代码使用示例
通过以下一个示例,我们不断的优化参数,调整训练方式,来逐步使得损失函数下降的方法
【注意】:每次的参数调整只是一个调整探索过程,并不是调整后的参数一定为最优解,通过示例为了体会这个调参的过程,如何逐渐的去使得损失函数下降
demo_reg.py 功能介绍:
1.数据获取与预处理:代码首先从UCI机器学习库(archive.ics.uci.edu)获取波士顿房价数据集,对数据进行清洗(去除多余空格),并将清洗后的数据保存到本地文件housing.data中。
2.数据加载与分割:使用numpy加载本地文件,并提取特征变量x和目标变量y,然后将数据分为训练集和测试集。
3.模型定义:定义一个简单的线性神经网络模型Net,包含一个线性层用于预测。
4.模型训练:使用均方误差损失函数(MSELoss)和随机梯度下降优化器(SGD)训练模型,共迭代10000次,并在每次迭代后打印损失值和部分预测值与实际值。
注意事项:
1.确保网络连接正常,以便成功从UCI机器学习库获取数据。
2.确保数据文件housing.data可以正确保存到本地,并且路径正确。
3.确保模型训练时的数据形状一致,以避免广播问题导致的警告。
完整代码如下:demo_reg.py
python
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# @Project :demo_create
# @File :demo_reg.py
# @Time :2024-12-12 18:15
# @Author :wangting_666
"""
pip install torch -i https://mirrors.aliyun.com/pypi/simple/
pip install requests -i https://mirrors.aliyun.com/pypi/simple/
pip install numpy -i https://mirrors.aliyun.com/pypi/simple/
"""
# 导入所需的库
import torch
import requests
import numpy as np
import re
# 指定数据文件的URL
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/housing/housing.data'
response = requests.get(url)
# 发送HTTP GET请求
if response.status_code == 200:
# 请求成功,获取网页内容
url_data = response.text
# 按行分割数据
lines = url_data.split('\n')
processed_lines = []
for line in lines:
# 去除每行的首尾空白字符
stripped_line = line.strip()
if stripped_line:
# 将多个连续空格替换为单个空格,并去除首尾空白字符
processed_line = re.sub(r'\s{2,}', ' ', line).strip()
processed_lines.append(processed_line)
# 将处理后的数据行合并为一个字符串,每行之间用换行符分隔
url_data = '\n'.join(processed_lines)
# 指定本地文件名
filename = 'housing.data'
# 将处理后的数据写入本地文件
with open(filename, 'w') as f:
f.write(url_data)
print('Data saved to {}'.format(filename))
else:
# 请求失败,打印错误信息
print('Failed to retrieve data')
# 使用numpy加载本地文件
data = np.loadtxt('housing.data')
# 506行,14列数据
print("data.shape: {}".format(data.shape))
# 提取目标变量y和特征变量x
# data 数组中提取最后一列数据,存储在变量 y 中。
y = data[:, -1]
# data 数组中提取除了最后一列之外的所有列,存储在变量 x 中。
x = data[:, 0:-1]
# 将数据分为训练集和测试集
x_train = x[0:496, ...]
y_train = y[0:496, ...]
x_test = x[496:, ...]
y_test = y[496:, ...]
# 打印训练集和测试集的形状
print("x_train.shape: {}".format(x_train.shape))
print("y_train.shape: {}".format(y_train.shape))
print("x_test.shape: {}".format(x_test.shape))
print("y_test.shape: {}".format(y_test.shape))
# 定义神经网络模型
class Net(torch.nn.Module):
def __init__(self, n_features, n_output):
super(Net, self).__init__()
# 定义一个线性层,用于预测
self.predict = torch.nn.Linear(n_features, n_output)
def forward(self, x):
# 前向传播
out = self.predict(x)
return out
# 实例化模型
net = Net(13, 1)
# 定义损失函数
loss_func = torch.nn.MSELoss()
# 定义优化器
optimizer = torch.optim.SGD(net.parameters(), lr=0.0001)
# 训练模型
for i in range(10000):
# 将训练数据转换为PyTorch张量
x_data = torch.tensor(x_train, dtype=torch.float32)
y_data = torch.tensor(y_train, dtype=torch.float32)
# 前向传播,计算预测值
pred = net.forward(x_data)
# 计算损失
loss = loss_func(pred, y_data) * 0.001
# 清空梯度
optimizer.zero_grad()
# 反向传播,计算梯度
loss.backward()
# 更新模型参数
optimizer.step()
# 每轮迭代后打印损失和部分预测值、实际值
print("item:{},loss:{}".format(i, loss.item()))
print(pred[0:10])
print(y_data[0:10])
最后item输出损失函数结果(11.82%):loss:0.11819077283143997
item:9999,loss:0.11819077283143997
tensor([[22.0104],
[20.1129],
[22.3382],
[22.9463],
[21.9336],
[21.3733],
[21.1441],
[17.4917],
[14.9874],
[18.2216]], grad_fn=<SliceBackward0>)
tensor([24.0000, 21.6000, 34.7000, 33.4000, 36.2000, 28.7000, 22.9000, 27.1000,
16.5000, 18.9000])
3-1. 调整学习率 (Learning Rate)
通过调整学习率来优化,目前使用的学习率lr=0.001,这可能会导致模型训练得较慢。可以尝试增大学习率来加速训练,或者使用更适合的值。可以尝试以下几种调整:
- 尝试
0.001
或0.01
。 - 过高的学习率可能会导致发散,可以逐步减小。
python
# optimizer = torch.optim.SGD(net.parameters(), lr=0.0001)
optimizer = torch.optim.SGD(net.parameters(), lr=0.001)
最后item输出损失函数结果(9.47%):item:9999,loss:0.09471820294857025
item:9999,loss:0.09471820294857025
tensor([[22.1652],
[21.6274],
[19.9424],
[19.1086],
[19.8691],
[20.0128],
[22.4789],
[24.8061],
[26.4139],
[23.9335]], grad_fn=<SliceBackward0>)
tensor([24.0000, 21.6000, 34.7000, 33.4000, 36.2000, 28.7000, 22.9000, 27.1000,
16.5000, 18.9000])
3-2. 调整损失函数权重
当前在计算损失时将结果乘以了一个常数 0.001
,这会影响损失的计算。可以尝试不同的乘法因子,看看是否能改善模型表现。比如,将 0.001
改为 0.0001
。
python
# 计算损失
# loss = loss_func(pred, y_data) * 0.001
loss = loss_func(pred, y_data) * 0.0001
最后item输出损失函数结果(1.07%):loss:0.0107608987018466
item:9999,loss:0.0107608987018466
tensor([[24.8059],
[21.2679],
[21.0506],
[20.6933],
[20.7666],
[21.0484],
[21.9299],
[22.2718],
[19.4872],
[21.5408]], grad_fn=<SliceBackward0>)
tensor([24.0000, 21.6000, 34.7000, 33.4000, 36.2000, 28.7000, 22.9000, 27.1000,
16.5000, 18.9000])
3-3. 增加训练轮数 (Epochs)
目前代码中训练轮数为 10000
次。如果你觉得模型在 10000
次迭代后仍未达到最优结果,可以适当增加训练轮数。例如,增加到 20000
或 30000
,并观察损失函数是否持续下降。
python
# for i in range(10000):
for i in range(30000):
最后item输出损失函数结果(0.93%):loss:0.00932362861931324
item:29999,loss:0.00932362861931324
tensor([[22.3280],
[22.3310],
[22.1053],
[20.9865],
[21.1635],
[21.2615],
[21.4697],
[22.1821],
[20.5868],
[21.7904]], grad_fn=<SliceBackward0>)
tensor([24.0000, 21.6000, 34.7000, 33.4000, 36.2000, 28.7000, 22.9000, 27.1000,
16.5000, 18.9000])
3-4. 使用更高级的优化器(优化器调整)
当前使用的是 SGD(随机梯度下降) ,尽管它是最基础的优化器,但它可能在某些问题上收敛较慢,尤其是当损失函数比较复杂时。你可以尝试使用 Adam 优化器,它通常比 SGD 更有效,能加速训练并避免过拟合。
SGD:标准的梯度下降方法,适用于较简单的任务,收敛速度较慢且容易陷入局部最小值。
Adam:结合了动量和自适应学习率调整,通常能在复杂问题中更快收敛,避免过拟合。
学习率过大会导致模型训练不稳定,过小则会导致收敛缓慢。调整学习率可以帮助模型快速收敛
python
# optimizer = torch.optim.SGD(net.parameters(), lr=0.0001)
optimizer = torch.optim.Adam(net.parameters(), lr=0.001)
最后item输出损失函数结果(0.86%):loss:0.008578178472816944
item:29999,loss:0.008578178472816944
tensor([[22.4775],
[22.4890],
[22.7133],
[22.8217],
[22.9005],
[22.6268],
[22.3341],
[22.4805],
[22.4585],
[22.4555]], grad_fn=<SliceBackward0>)
tensor([24.0000, 21.6000, 34.7000, 33.4000, 36.2000, 28.7000, 22.9000, 27.1000,
16.5000, 18.9000])
3-5. 使用更复杂的损失函数
尽管 MSELoss 是回归任务中的标准损失函数,但有时候可以根据任务的特性尝试其他损失函数。例如,Huber Loss 对于一些数据中的异常值更具鲁棒性,可以尝试替换掉 MSELoss
python
# loss_func = torch.nn.MSELoss()
loss_func = torch.nn.HuberLoss(delta=1.0)
最后item输出损失函数结果(0.06%):loss:0.0006132011767476797
item:29999,loss:0.0006132011767476797
tensor([[21.1054],
[21.1156],
[21.3443],
[21.4555],
[21.5352],
[21.2573],
[20.9581],
[21.1065],
[21.0816],
[21.0821]], grad_fn=<SliceBackward0>)
tensor([24.0000, 21.6000, 34.7000, 33.4000, 36.2000, 28.7000, 22.9000, 27.1000,
16.5000, 18.9000])
通过以上的示例,可以看到损失值降低的办法非常多,是一个持续优化的过程。