在之前进行实验的时候发现:调用 Pytorch 中的 Loss 函数之前如果对其没有一定的了解,可能会影响实验效果和调试效率。以
CrossEntropyLoss
为例,最初设计实验的时候没有注意到该函数默认返回的是均值,以为是总和,于是最后计算完 Loss 之后,手动做了个均值,导致实际 Loss 被错误缩放,实验效果不佳,在后来 Debug 排除代码模型架构问题的时候才发觉这一点,着实花费了不少时间。所以闲暇时准备写一下 Pytorch 中 Loss 函数相关的知识,希望能对初入深度学习的学子们有所帮助,少踩点坑。
这篇文章是用于后续理解的前置知识,在之后有提到新的专业名词时会进行补充。
文章大多以分类模型为例进行叙述。
文章目录
- [什么是 Logits?](#什么是 Logits?)
-
- [Logits 和 Softmax](#Logits 和 Softmax)
- [什么是 One-Hot 编码?](#什么是 One-Hot 编码?)
什么是 Logits?
Logits 是指神经网络的最后一个线性层(全连接层)的未经过任何激活函数(例如 softmax 或 sigmoid)处理的输出,可以是任意实数,在分类的任务中,logits 通常是在进行多类别分类任务时的原始输出。
Logits 和 Softmax
在多类别分类问题中,logits 通常会被传递给 softmax 函数,softmax 函数将这些 logits 转换为概率分布:将任意实数的 logits 转换为 [0, 1] 之间的概率值,并且这些概率值的和为 1。
代码示例
为了更好地理解 logits 和 softmax 之间的关系,下面是一个简单的代码示例:
python
import torch
import torch.nn.functional as F
# 样例:分类神经网络,便于对照理解
class Classifier(nn.Module):
def __init__(self, input_size, hidden_size, num_classes=3):
super(Classifier, self).__init__()
self.fc1 = nn.Linear(input_size, hidden_size) # 输入层到隐藏层
self.fc2 = nn.Linear(hidden_size, num_classes) # 隐藏层到输出层
def forward(self, x):
out = self.fc1(x)
out = F.relu(out) # ReLU 激活函数
logits = self.fc2(out) # 输出层,不经过 softmax
return logits
# 假设这是分类神经网络的输出 logits
logits = torch.tensor([[2.0, 1.0, 0.1], [1.0, 3.0, 0.2]])
# 使用 softmax 函数将 logits 转换为概率分布
probabilities = F.softmax(logits, dim=1)
print("Logits:")
print(logits)
print("\nProbabilities after applying softmax:")
print(probabilities)
python
>>> Logits:
>>> tensor([[2.0000, 1.0000, 0.1000],
>>> [1.0000, 3.0000, 0.2000]])
>>> Probabilities after applying softmax:
>>> tensor([[0.6590, 0.2424, 0.0986],
>>> [0.1131, 0.8360, 0.0508]])
输出解释
- Logits :
[[2.0, 1.0, 0.1], [1.0, 3.0, 0.2]]
是神经网络的输出,未经过 softmax 处理。 - Softmax: softmax 函数将 logits 转换为概率分布,每个分布的概率值和为 1。
什么是 One-Hot 编码?
初入深度学习领域的人大多都会有这个疑问:这些所说的类别,究竟是怎么表示成向量的?
One-Hot 是一个很直观的形容,但我当时看到并猜测到相应概念的时候,还是不敢确定,因为太直白了,总觉得编码成向量的过程应该没有这么简单,然而 One-Hot 就是如此,深度学习不是一蹴而就的,看似复杂的概念最初也是由一个个直白的想法发展得来。
具体来说,One-Hot 编码对于每个类别,使用一个与类别数相同长度 的二进制向量 ,每个位置对应一个类别。其中,只有一个位置的值为 1(这就是 "One-Hot" 的含义),表示属于该类别,其余位置的值为 0。
例如,对于三个类别的分类问题(类别 A、B 和 C),使用 One-Hot 编码可得:
- 类别 A: [1, 0, 0]
- 类别 B: [0, 1, 0]
- 类别 C: [0, 0, 1]
代码示例
python
import torch
# 假设我们有三个类别:0, 1, 2
num_classes = 3
# 样本标签
labels = torch.tensor([0, 2, 1, 0])
# 将标签转换为 One-Hot 编码
one_hot_labels = torch.nn.functional.one_hot(labels, num_classes)
print("Labels:")
print(labels)
print("\nOne-Hot Encoded Labels:")
print(one_hot_labels)
plaintext
>>> Labels:
>>> tensor([0, 2, 1, 0])
>>> One-Hot Encoded Labels:
>>> tensor([[1, 0, 0],
>>> [0, 0, 1],
>>> [0, 1, 0],
>>> [1, 0, 0]])
输出解释
- Labels :
[0, 2, 1, 0]
是我们初始的类别标签。 - One-Hot Encoded Labels :
[[1, 0, 0], [0, 0, 1], [0, 1, 0], [1, 0, 0]]
是将标签转换为 One-Hot 编码后的结果。每个向量中只有一个位置的值为 1(One-Hot)。
类别不是整数怎么办?
看了代码示例,可能会有一个疑问:类别大多不会是整数而是字符,应该怎么编码?或许你心中已经有了一个很直白的答案:那就做一个映射,将类别用整数编码,然后再将这些整数标签转换为 One-Hot 编码。
的确可以这样。
代码示例
python
import torch
# 类别映射:A -> 0, B -> 1, C -> 2
category_map = {'A': 0, 'B': 1, 'C': 2}
# 样本类别标签
labels = ['A', 'C', 'B', 'A']
# 将类别标签转换为整数标签
integer_labels = torch.tensor([category_map[label] for label in labels])
# 将整数标签转换为 One-Hot 编码
num_classes = len(category_map)
one_hot_labels = torch.nn.functional.one_hot(integer_labels, num_classes)
print("Labels:")
print(labels)
print("\nInteger Labels:")
print(integer_labels)
print("\nOne-Hot Encoded Labels:")
print(one_hot_labels)
python
>>> Labels:
>>> ['A', 'C', 'B', 'A']
>>> Integer Labels:
>>> tensor([0, 2, 1, 0])
>>> One-Hot Encoded Labels:
>>> tensor([[1, 0, 0],
>>> [0, 0, 1],
>>> [0, 1, 0],
>>> [1, 0, 0]])
解释
- Labels :
['A', 'C', 'B', 'A']
是我们初始的类别标签。 - Integer Labels :
[0, 2, 1, 0]
是将类别标签映射到整数后的结果。A
对应 0,B
对应 1,C
对应 2。 - One-Hot Encoded Labels :
[[1, 0, 0], [0, 0, 1], [0, 1, 0], [1, 0, 0]]
是将整数标签转换为 One-Hot 编码后的结果。每个向量中只有一个位置的值为 1,表示该样本的类别,其余位置的值为 0。