- 一、损失函数
- 二、损失函数的创建和运行机制
- 二、各种损失函数
- 01.
nn.CrossEntropyLoss()
- 02.
nn.NLLLoss()
- 03.
nn.BCELoss()
- 04.
nn.BCEWithLogitsLoss()
- 05.
nn.L1Loss()
和nn.MSELoss()
- 06.
nn.SmoothL1Loss()
- 06.
nn.PoissonNLLLoss()
- 07.
nn.KLDivLoss()
- 08.
nn.MarginRankingLoss()
- 09.
nn.MultiLabelMarginLoss()
- 10.
nn.SoftMarginLoss()
- 11.
nn.MultiLabelSoftMarginLoss()
- 12.
nn.MultiMarginLoss()
- 13.
nn.TripletMarginLoss()
- 14.
nn.HingeEmbeddingLoss()
- 15.
nn.CosineEmbeddingLoss()
- 16.
nn.CTCLoss()
- 01.
- 总结
- 损失函数概念
- 损失函数的创建和运行机制
- 各种损失函数
在前几篇文章中我们学习了模型模块中一些知识。我们了解了如何构建模型,然后对模型进行一个初始化。今天我们就开始进入下一个模块 损失函数。
一、损失函数
首先我们来看一下什么是损失函数。损失函数是衡量模型输出与真实标签之间的一个差异。
我们先来看上面这个示意图,就是一元线性回归的拟合过程。图中的绿色点是训练样本,蓝色的线是训练好的一个模型。在图中可以看到模型并没有很好去拟合到每一个数据点,也就是说我们的每一个数据点并没有在模型上。所以数据点会产生一个loss值。
通常我们在谈损失函数、loss的时候经常出现这三个概念。三者之间又有什么关系?他们的区别在哪里呢?
- 损失函数(Loss Function)
- 计算一个样本与预测结果的差异
- 代价函数(Cost Function)
- 计算整个样本集Loss的平均值
- 目标函数(Objective Function)
- 代价函数 + 正则项
在我们继续学习模型训练当中,目标函数是我们最终的一个目标。通常目标函数包含代价函数和正则项。代价函数衡量模型输出与真实标签之间的差异,也是希望模型的输出和真实标签差异要更小一些,要更接近真实标签。那么是不是我们这个代价函数越小越好了。其实并不是,因为有时候会过拟合了。
假如我们来看上面示意图,假如有这么一个模型很好的拟合每一个数据点,所以代价函数已经很小了,达到最小值是0,但是这个模型并不是个好的模型。这就是模型太复杂,导致模型过拟合。
所以我们在追求模型输出与真实标签之间差异比较小的时候,同时也要对这个模型做一些限制,做一些约束。
而在机器学习中约束的项,我们就称之为正则项 regulation
。通常我们会采用了L1、L2两个正则项加载代价函数之后就构成了我们整个目标函数。所以通常我们的目标函数是包含了两项:一项是我们希望模型的输出与真实标签之间差异要小一些;第二项是这种模型进行一定的约束,常用的约束有L1、L2约束,或是稀疏约束等等。
这就是损失函数,代价函数和目标函数的一个关系。在这里我们不失一般性的,我们后面都会用损失函数来代替代价函数。也就是我们去衡量模型输出与真实标签之间的差异的时候,我们都通通称之为loss称为损失函数。
class _Loss(Module):
def __init__(self, size_average=None, reduce=None,reduction='mean'):
super(_Loss, self).__init__()
if size_average is not None or reduce is not None:
self.reduction = _Reduction.legacy_get_string(
size_average, reduce)
else:
self.reduction = reduction
PyTorch
中的 _Loss类
继承 Module类
,所以说loss可以相当于是一个网络层。 __init__
初始化函数当中有3个参数,其中 size_average
和 reduce
是会被舍弃了两个参数。所以大家千万不要再去使用这两个参数。因为它俩的功能在 reduction
当中完全可以实现 none
、sum
、mean
三种模式。首先我们来看一下 __init__函数
的内部实现,其实只是去构建了 self.reduction
。size_average
和 reduce
不需要再去关注。
二、损失函数的创建和运行机制
下面通过代码演示交叉熵损失函数是怎么创建的、及其使用流程和机制。
import os
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
import torchvision.transforms as transforms
import torch.optim as optim
from matplotlib import pyplot as plt
from model.lenet import LeNet
from tools.my_dataset import RMBDataset
from tools.common_tools import transform_invert, set_seed
set_seed(1) # 设置随机种子
rmb_label = {"1": 0, "100": 1}
# 参数设置
MAX_EPOCH = 10
BATCH_SIZE = 16
LR = 0.01
log_interval = 10
val_interval = 1
# ============================ step 1/5 数据 ============================
train_dir = "H:/PyTorch_From_Zero_To_One/data/rmb_split/train"
valid_dir = "H:/PyTorch_From_Zero_To_One/data/rmb_split/valid"
norm_mean = [0.485, 0.456, 0.406]
norm_std = [0.229, 0.224, 0.225]
train_transform = transforms.Compose([
transforms.Resize((32, 32)),
transforms.RandomCrop(32, padding=4),
transforms.RandomGrayscale(p=0.8),
transforms.ToTensor(),
transforms.Normalize(norm_mean, norm_std),
])
valid_transform = transforms.Compose([
transforms.Resize((32, 32)),
transforms.ToTensor(),
transforms.Normalize(norm_mean, norm_std),
])
# 构建MyDataset实例
train_data = RMBDataset(data_dir=train_dir, transform=train_transform)
valid_data = RMBDataset(data_dir=valid_dir, transform=valid_transform)
# 构建DataLoder
train_loader = DataLoader(dataset=train_data, batch_size=BATCH_SIZE, shuffle=True)
valid_loader = DataLoader(dataset=valid_data, batch_size=BATCH_SIZE)
# ============================ step 2/5 模型 ============================
net = LeNet(classes=2)
net.initialize_weights()
# ============================ step 3/5 损失函数 ============================
loss_functoin = nn.CrossEntropyLoss() # 选择损失函数
# ============================ step 4/5 优化器 ============================
optimizer = optim.SGD(net.parameters(), lr=LR, momentum=0.9) # 选择优化器
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1) # 设置学习率下降策略
# ============================ step 5/5 训练 ============================
train_curve = list()
valid_curve = list()
for epoch in range(MAX_EPOCH):
loss_mean = 0.
correct = 0.
total = 0.
net.train()
for i, data in enumerate(train_loader):
# forward
inputs, labels = data
outputs = net(inputs)
# backward
optimizer.zero_grad()
loss = loss_functoin(outputs, labels)
loss.backward()
# update weights
optimizer.step()
# 统计分类情况
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).squeeze().sum().numpy()
# 打印训练信息
loss_mean += loss.item()
train_curve.append(loss.item())
if (i+1) % log_interval == 0:
loss_mean = loss_mean / log_interval
print("Training:Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format(
epoch, MAX_EPOCH, i+1, len(train_loader), loss_mean, correct / total))
loss_mean = 0.
scheduler.step() # 更新学习率
# validate the model
if (epoch+1) % val_interval == 0:
correct_val = 0.
total_val = 0.
loss_val = 0.
net.eval()
with torch.no_grad():
for j, data in enumerate(valid_loader):
inputs, labels = data
outputs = net(inputs)
loss = loss_functoin(outputs, labels)
_, predicted = torch.max(outputs.data, 1)
total_val += labels.size(0)
correct_val += (predicted == labels).squeeze().sum().numpy()
loss_val += loss.item()
valid_curve.append(loss_val)
print("Valid:\t Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format(
epoch, MAX_EPOCH, j+1, len(valid_loader), loss_val, correct / total))
train_x = range(len(train_curve))
train_y = train_curve
train_iters = len(train_loader)
valid_x = np.arange(1, len(valid_curve)+1) * train_iters*val_interval # 由于valid中记录的是epochloss,需要对记录点进行转换到iterations
valid_y = valid_curve
plt.plot(train_x, train_y, label='Train')
plt.plot(valid_x, valid_y, label='Valid')
plt.legend(loc='upper right')
plt.ylabel('loss value')
plt.xlabel('Iteration')
plt.show()
# ============================ inference ============================
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
test_dir = os.path.join(BASE_DIR, "test_data")
test_data = RMBDataset(data_dir=test_dir, transform=valid_transform)
valid_loader = DataLoader(dataset=test_data, batch_size=1)
for i, data in enumerate(valid_loader):
# forward
inputs, labels = data
outputs = net(inputs)
_, predicted = torch.max(outputs.data, 1)
rmb = 1 if predicted.numpy()[0] == 0 else 100
img_tensor = inputs[0, ...] # C H W
img = transform_invert(img_tensor, train_transform)
plt.imshow(img)
plt.title("LeNet got {} Yuan".format(rmb))
plt.show()
plt.pause(0.5)
plt.close()
还是使用二分类模型代码,在58行和83行设置断点。
debug代码,第58行停止,来到 CrossEntropyLoss类
,是继承的一个 _WeightedLoss类
。
class CrossEntropyLoss(_WeightedLoss):
__constants__ = ['ignore_index', 'reduction']
def __init__(self, weight=None, size_average=None, ignore_index=-100,
reduce=None, reduction='mean'):
super(CrossEntropyLoss, self).__init__(weight, size_average, reduce, reduction)
self.ignore_index = ignore_index
def forward(self, input, target):
return F.cross_entropy(input, target, weight=self.weight,
ignore_index=self.ignore_index, reduction=self.reduction)
继续step into,来到了 _WeightedLoss类
,该类继承了 _Loss类
class _WeightedLoss(_Loss):
def __init__(self, weight=None, size_average=None, reduce=None, reduction='mean'):
super(_WeightedLoss, self).__init__(size_average, reduce, reduction)
self.register_buffer('weight', weight)
继续step into,来到了 _Loss类
,该类继承了 Module类
,所以loss函数是一个Module,同样拥有8个有序字典
class _Loss(Module):
def __init__(self, size_average=None, reduce=None, reduction='mean'):
super(_Loss, self).__init__()
if size_average is not None or reduce is not None:
self.reduction = _Reduction.legacy_get_string(size_average, reduce)
else:
self.reduction = reduction
step over 到第4行代码,这时候构建了交叉熵损失
之后一步步step out,就构建完成了一个交叉熵损失
构建完成交叉熵损失之后,我们是在训练过程中,模型forward得到output之后,采用lossfunction来衡量output和label之间的差异,直接运行到83行代码。继续step into查看交叉熵损失函数的运行机制。
刚从交叉熵损失函数的构建过程中发现,交叉熵损失函数是一个module,一个模型模块必须进行forward操作,所以step into之后,到达 module.py
中的 Module类
中的 __call__函数
来调用 forward
操作。
class Module(object):
...
def __call__(self, *input, **kwargs):
for hook in self._forward_pre_hooks.values():
result = hook(self, input)
if result is not None:
if not isinstance(result, tuple):
result = (result,)
input = result
if torch._C._get_tracing_state():
result = self._slow_forward(*input, **kwargs)
else:
result = self.forward(*input, **kwargs)
for hook in self._forward_hooks.values():
hook_result = hook(self, input, result)
if hook_result is not None:
result = hook_result
if len(self._backward_hooks) > 0:
var = result
while not isinstance(var, torch.Tensor):
if isinstance(var, dict):
var = next((v for v in var.values() if isinstance(v, torch.Tensor)))
else:
var = var[0]
grad_fn = var.grad_fn
if grad_fn is not None:
for hook in self._backward_hooks.values():
wrapper = functools.partial(hook, self)
functools.update_wrapper(wrapper, hook)
grad_fn.register_hook(wrapper)
return result
运行到13行之后,step into到 CrossEntropyLoss类
中的 forward函数
class CrossEntropyLoss(_WeightedLoss):
__constants__ = ['ignore_index', 'reduction']
def __init__(self, weight=None, size_average=None, ignore_index=-100,
reduce=None, reduction='mean'):
super(CrossEntropyLoss, self).__init__(weight, size_average, reduce, reduction)
self.ignore_index = ignore_index
def forward(self, input, target):
return F.cross_entropy(input, target, weight=self.weight,
ignore_index=self.ignore_index, reduction=self.reduction)
step into 到 functional.py
文件中的 cross_entropy
函数,在这里进行了交叉熵损失的计算。
def cross_entropy(input, target, weight=None, size_average=None, ignore_index=-100,
reduce=None, reduction='mean'):
if not torch.jit.is_scripting():
tens_ops = (input, target)
if any([type(t) is not Tensor for t in tens_ops]) and has_torch_function(tens_ops):
return handle_torch_function(
cross_entropy, tens_ops, input, target, weight=weight,
size_average=size_average, ignore_index=ignore_index, reduce=reduce,
reduction=reduction)
if size_average is not None or reduce is not None:
reduction = _Reduction.legacy_get_string(size_average, reduce)
return nll_loss(log_softmax(input, 1), target, weight, None, ignore_index, None, reduction)
然后一步步的返回,会得到一个loss损失值。
以上就是Loss Function 的构建和运行过程。
二、各种损失函数
01. nn.CrossEntropyLoss()
nn.CrossEntropyLoss(
weight=None,
size_average=None, # 放弃使用
ignore_index=-100,
reduce=None, # 放弃使用
reduction='mean')
功能:nn.LogSoftmax()
与 nn.NLLLoss()
结合,进行交叉熵计算
主要参数:
weight
:各类别的loss设置权值ignore_index
:忽略某个类别reduction
:计算模式,可为none
/sum
/mean
none
- 逐个元素计算sum
- 所有元素求和,返回标量mean
- 加权平均,返回标量
下面我们来详细的学习 CrossEntropyLoss
。我们首先来看一下它的功能是怎么样的。功能:nn.LogSoftmax()
与 nn.NLLLoss()
结合,进行交叉熵计算。这里需要注意的是,这一个函数呢并不是真正意义上的一个交叉熵函数计算,而是有一些不同之处。不同之处就是在于他采用了一个 softmax
对我们的数据进行了归一化,把我们的数据值归一化到一个概率输出的形式0~1分布。这是因为交叉熵损失函数常用于分类任务当中,分类任务当中我们的输出通常是以概率值为主的。所以我们交叉熵其实他是衡量两个概率分布之间的差异,所以交叉熵值越低,也就是表示两个分布越近。
那为什么交叉熵值越低,这两个概率越相似呢?这就要从他与相对熵之间的关系说起。要提到相对熵与交叉熵,那就不得不提到信息熵。下面我们来分析交叉熵、相对熵和信息熵这三者之间的关系。这三者之间关系是:交叉熵 = 信息熵 + 相对熵。
下面我们一步步来看 交叉熵 = 信息熵 + 相对熵。首先我们先来看最基本的熵的概念,熵准确来说应该叫做信息熵。他是由信息论之父香农是从热力学的概念当中借鉴而来的一个名词。熵是用来描述一个事件的不确定性。有个事件越不确定他熵值越大,比如说 明天会下雨
这个事件的熵就会比 明天太阳会升起
这个事件的熵要大。下面我们来看信息熵的计算公式是这样的:
熵:
它是自信息的一个期望。所以我们再来了解什么是自信息。自信心是用于衡量单个输出单个事件的不确定性。它的公式如下:
自信息:
所以他是对概率取 负的log()
。明天下雨的概率是 0.3
,那么明天下雨的自信息就是 -log0.3
。
而信息熵是整个概率分布的一个不确定性,它是用来描述整个概率的分布,所以要对自信息求期望。我们看到信息熵公式如下:
熵:
为了更好理解一个信息熵的大小的关系与事件不确定性的关系,我们来看一个示意图。
这是一个伯努利两点分布的一个信息熵。当事件的概率值是 0.5
的时候,其信息熵最大。也就是在概率是 0.5
的时候,他的不确定性是最大的,最大值应该是在 0.69
。如果训练过二分类模型的同学应该知道 0.69
这个数字,会在loss值中经常碰到该数字。因为有时候模型训练坏了的时候,我们的模型不管怎样去迭代loss值都恒定在 0.69
。还有在模型第一次初始化,第一个 iteration
的时候, loss
值也很可能就是 0.69
。该值表示模型当前是不具备任何判别能力的,因为该模型对任意输入,输出概率值都为0.5。这就是信息熵的一个概念,用来描述信息的不确定性,事件越不确定,熵值就越大。
下面我们来看相对熵,又被称为KL散度,它是用来衡量两个分布之间的差异,也就两个分布之间的距离。但是我们需要注意,它虽然是可以计算两个分布之间的距离,但它不是一个距离的函数,距离函数他有一个对称性。比如是p到q的距离,要等于q到p的距离,这才是一个距离函数。但是我们的相对熵不具备这个对称性。我们可以看一下它的公式。P
是真实的分布,而 Q
是模型输出的分布。这个我们需要用 Q
的分布去拟合、去逼近 P
的真实分布,所以它这是不具备一个对称性的。
而现在我们来看一下交叉熵的公式
交叉熵:
下面我们对相对熵的公式进行推导变化。
我们再来观察这一个公式。 P
是真实的一个概率分布,也就是训练集中我们的样本的分布;而 Q
是模型输出的一个分布。所以在机器学习模型当中,优化交叉熵等价于优化相对熵。为什么呢?我们看到等号右边这个也就是 P
的信息熵,因为训练集是固定的,所以 P
的信息熵是一个常数,因为这个概率分布是固定的,所以他的信息熵是一个常数,所以我们在做优化的时候,常数是可以忽略掉的。所以等号两边做优化,优化交叉熵等价于优化相对熵。
公式如下:
交叉熵 = 信息熵 + 相对熵
交叉熵:
自信息:
熵:
相对熵:
交叉熵是衡量两个分布之间这个距离一个差异。所以我们就应该知道为什么要采用 softmax
。因为 softmax
可以将输出值,将数据值转换、归一化到一个概率取值范围,也就是0到1之间。然后再通过 log
以及 NLLLoss
来计算的交叉熵。下面我们来看一下这个交叉熵中的计算公式是这样的。
loss接受的是x(输出的概率值)和 class 类别值。第二行公式中的第二个括号中就是softmax操作,将这一个神经元的输出值归一化到0~1的概率取值区间,然后在取负号进行log操作,完成交叉熵损失计算。
对比上图中第一行公式(交叉熵定义),还应该有,在公式计算的时候却没有。因为我们这个样本已经取出来的,所以我们样本的概率为1,这一项是等于1的,就可以省略计算。
我们在训练过程中只是计算一个样本的loss,所以也不需要计算。所以最终我们公式就变成了。因为我们的模型输出不会服从概率分布的形式,所以我们需要用一个 softmax
把输出值归一化到0~1的概率取值区间。
主要参数:
weight
:设置各类别的loss设置权值
02. nn.NLLLoss()
nn.NLLLoss(
weight=None,
size_average=None,
ignore_index=-100,
reduce=None,
reduction='mean')
功能:实现负对数似然函数中的负号功能
主要参数:
weight
:各类别的loss设置权值ignore_index
:忽略某个类别reduction
:计算模式,可为none
/sum
/mean
none
- 逐个元素计算sum
- 所有元素求和,返回标量mean
-加权平均,返回标量
03. nn.BCELoss()
nn.BCELoss(
weight=None,
size_average=None,
reduce=None,
reduction='mean’)
功能:二分类交叉熵
计算公式:
注意事项:输入值取值在[0,1]
主要参数:
weight
:各类别的loss设置权值ignore_index
:忽略某个类别reduction
:计算模式,可为none
/sum
/mean
none
- 逐个元素计算sum
- 所有元素求和,返回标量mean
- 加权平均,返回标量
04. nn.BCEWithLogitsLoss()
nn.BCEWithLogitsLoss(
weight=None,
size_average=None,
reduce=None, reduction='mean',
pos_weight=None)
功能:结合Sigmoid与二分类交叉熵
计算公式:
注意事项:网络最后不加sigmoid函数
主要参数:
pos_weight
:正样本的权值weight
:各类别的loss设置权值ignore_index
:忽略某个类别reduction
:计算模式,可为none
/sum
/mean
reduction
:计算模式,可为none
/sum
/mean
reduction
:计算模式,可为none
/sum
/mean
log_input
:输入是否为对数形式,决定计算公式- True:loss(input, target) = exp(input) - target * input
- False:loss(input, target) = input - target * log(input+eps)
full
:计算所有loss,默认为Falseeps
:修正项,避免log(input)为nan
07. nn.KLDivLoss()
nn.KLDivLoss(size_average=None, reduce=None, reduction='mean'
功能:计算KLD(divergence),KL散度,相对熵
注意事项:需提前将输入计算log-probabilities
,如通过nn.logsoftmax()
计算公式:
主要参数:
reduction
:none/sum/mean/batchmeanmargin
:边界值,x1与x2之间的差异值reduction
:计算模式,可为none/sum/mean
当y = 1时,希望x1比x2大,当x1>x2时,不产生loss
当y = -1时,希望x2比x1大,当x2>x1时,不产生loss
09. nn.MultiLabelMarginLoss()
nn.MultiLabelMarginLoss(size_average=None, reduce=None, reduction='mean')
功能:多标签边界损失函数
举例:四分类任务,样本x属于0类和3类,
标签:[0, 3, -1, -1] , 不是[1, 0, 0, 1]
计算公式:
主要参数:
reduction
:计算模式,可none
/sum
/mean
10.
nn.SoftMarginLoss()
nn.SoftMarginLoss(size_average=None, reduce=None, reduction='mean')
功能:计算二分类的logistic损失
计算公式:
主要参数:reduction
:计算模式,可为none
/sum
/mean
11.
nn.MultiLabelSoftMarginLoss()
nn.MultiLabelSoftMarginLoss(weight=None, size_average=None, reduce=None, reduction='mean')
功能:SoftMarginLoss多标签版本
计算公式:
主要参数:weight
:各类别的loss设置权值reduction
:计算模式,可为none
/sum
/mean
12.
nn.MultiMarginLoss()
nn.MultiMarginLoss(p=1, margin=1.0, weight=None, size_average=None, reduce=None, reduction='mean')
功能:计算多分类的折页损失
计算公式:
主要参数:p
:可选1或2weight
:各类别的loss设置权值margin
:边界值reduction
:计算模式,可为none
/sum
/mean
13.
nn.TripletMarginLoss()
nn.TripletMarginLoss( margin=1.0, p=2.0, eps=1e-06, swap=False, size_average=None, reduce=None, reduction='mean')
功能:计算三元组损失,人脸验证中常用
计算公式:
主要参数:p
:范数的阶,默认为2margin
:边界值reduction
:计算模式,可为none
/sum
/mean
14.
nn.HingeEmbeddingLoss()
nn.HingeEmbeddingLoss(margin=1.0, size_average=None, reduce=None, reduction='mean’)
功能:计算两个输入的相似性,常用于非线性embedding和半监督学习
特别注意:输入x应为两个输入之差的绝对值
计算公式:
主要参数:margin
:边界值reduction
:计算模式,可为none
/sum
/mean
15.
nn.CosineEmbeddingLoss()
nn.CosineEmbeddingLoss(margin=0.0, size_average=None, reduce=None, reduction='mean')
功能:采用余弦相似度计算两个输入的相似性
计算公式:
主要参数:margin
:可取值[-1, 1] , 推荐为[0, 0.5]reduction
:计算模式,可为none
/sum
/mean
16.
```python torch.nn.CTCLoss(blank=0, reduction=’mean’, zero_infinity=False)nn.CTCLoss()
```
功能:计算CTC损失,解决时序类数据的分类
Connectionist Temporal Classification
主要参数:
blank
:blank labelzero_infinity
:无穷大的值或梯度置0reduction
:计算模式,可为none
/sum
/mean
参考文献: A. Graves et al.: Connectionist Temporal Classification: Labelling Unsegmented Sequence Data with Recurrent Neural Networks
总结
- nn.CrossEntropyLoss
- nn.NLLLoss
- nn.BCELoss
- nn.BCEWithLogitsLoss
- nn.L1Loss
- nn.MSELoss
- nn.SmoothL1Loss
- nn.PoissonNLLLoss
- nn.KLDivLoss
- nn.MarginRankingLoss
- nn.MultiLabelMarginLoss
- nn.SoftMarginLoss
- nn.MultiLabelSoftMarginLoss
- nn.MultiMarginLoss
- nn.TripletMarginLoss
- nn.HingeEmbeddingLoss
- nn.CosineEmbeddingLoss
- nn.CTCLoss