Week8:CTC Loss

2021SC@SDUSC

1. CTC loss出现的背景

在图像文本识别、语言识别的应用中,所面临的一个问题是神经网络输出与ground truth的长度不一致,这样一来,loss就会很难计算,举个例子来讲,如果网络的输出是”-sst-aa-tt-e'', 而其ground truth为“state”,那么像之前经常用的损失函数如cross entropy便都不能使用了,因为这些损失函数都是在网络输出与ground truth的长度一致情况下使用的。除了长度不一致的情况之外,还有一个比较难的点在于有多种情况的输出都对应着ground truth,根据解码规则(相邻的重复字符合并,去掉blank), path1: "-ss-t-a-t-e-" 和path2: "--stt-a-tt-e"都可以解码成“state”,与ground truth对应, 也就是many-to-one。为了解决以上问题,CTC loss就产生了。

2.CTC loss原理

2.1 前序

在说明原理之前,首先要说明一下CTC计算的对象:softmax矩阵,通常我们在RNN后面会加一个softmax层,得到softmax矩阵,softmax矩阵大小是timestep*num_classes, timestep表示的是时间序列的维度,num_class表示类别的维度。
 

import numpy as np
ts = 12
num_classes = 26+1 #26 for the number of english character, 1 for blank
rnn_output = np.random.random((ts, 16))#16 for hidden node number
w = np.random.random((16,num_classes))
logits = np.matmul(rnn_output,w)#logits: ts*num_classes=[12,27]
#calculate softmax matrix
maxvalue = np.max(logits, axis=1, keepdims=True)
exp = np.exp(logits-maxvalue) #minus maxvalue for avoiding overflow
exp_sum = np.sum(exp, axis=1, keepdims=True)
y = softmax = exp/exp_sum #softmax:ts*num_classes=[12,27]

2.2 forward-backward计算

其实呢,整体过程可以看做是对输入的y也就是softmax做了相应的映射得到解码结果,在希望解码结果尽量正确的情况下(使用概率来衡量),对网络的参数进行梯度下降。

在接下来的说明中,我们使用​ x表示网络的输入,使用​y表示softmax矩阵,其大小为timestep*num_classes, ​表示的是在第t个timestep时,第k个类别的softmax值, 使用y_{k}^{t}表示路径, 路径中包含字符, 使用L^{'}=L\bigcup blank表示字符集,如果是英文的话,那么一共就有26+1=27类, 使用​表示总的timestep,即时序数。定义一个映射B:L^{'T}\rightarrow L ^{\leq T}​表示解码过程中many-to-one的映射,就如上面所说的B(”-ss -t -a-t -e-”)= B(”- - stt - a- tt -e ”) =“state"。

在给定输入​的情况下,输出路径​的概率可以表示为:

 

上述公式的一个假设是:每个时序的字符都是相互独立的,与上下文无关

以上述​"state"​为例子,可以通过​映射得到 l = ​"state"​的路径集合使用​ π 表示,那么

 

知道了之后,肯定希望它的概率越大越好,如果取反的话,可以作为损失函数来进行求导,从而反向传播,对参数进行更新啦。

在第t个timestep,对k个类别的softmax值求偏导,即为​,一个更具体的例子来说就是在第7个timestep, 对类别a的进行求导​,那么

只有在timestep=7时为a的路径才会使用​y_{a}^{7}进行路径的分数计算,所以求偏导的时候只对这部分路径求取就可以啦

path1:"-ss-t-a-t-e-" 第7个timestep为a, path2: "--stt-a-tt-e"第7个timestep也为a, 以a为中点,将这两条路径分别分成两段。

path1_forward: "-ss-t-" path1_backward: "-t-e-"

path2_forward: "--stt-" path2_backward: "-tt-e"

​你也会发现 path1_forward+"a"+path2_backward也能够解码成正确的”state", 我们使用path3来表示该路径 , 同样的, path2_forward+"a"+path1_backward也可以解码成正确的“state",我们使用path4表示该路径

在下式中我们考虑​中仅仅包含path1,path2, path3, path4

上面的公式特别复杂,想用符号来表示forward和backward, 用​α来表示forward的部分,\beta表示backward的部分吧

\alpha _{t}(s)表示将1-t个timestep解码成​中的1-s个字符, 

\beta _{t}(s)表示将t-T个timestep解码成​中的s-\left | l \right | 个字符,​ 

其中​表示的是解码后​的长度。先看forward部分。

2.2.1 forward部分

​ 

这个公式计算的是所有能够解码成​的概率,

  

上面三个式子是说第一个timestep的解码成”blank“的概率是y_{b}^{1}​, 解码成l中第一个字符的概率是y_{l_{1}}^{1}, 其他的字符的概率为0, 可以这样理解,如果路径能够解码成正确的”state", 那么第一个timestep的肯定是blank或者"s", 只有这样才能解码正确。在前向和后向计算中,CTC会将blank插入到输出字符串,比如“state”就会变成“-s-t-a-t-e-", 使用l^{'}表示。 根据上面的叙述,可以得到如下递推式:

​ 

举个例子好啦,先看上面的那种情况,也就是特殊情况下的递推公式:

假设在第t个timestep解码成 “-s-t-"(l_{5}^{'} = blank),在第t-1个timestep中,当前的解码只可能是 “-s-t-" (l_{1:5}^{'}) ​或者“-s-t" (​ l_{1:4}^{'}), 只有这样才能正确解码。“-s-t-" 加入第t个timestep中的blank,会变成”-s-t--", 合并两个相邻的blank变成“-s-t-"。 “-s-t"加入第t个timestep中的blank会变成”-s-t-"。两者去掉空格都可以变成正确的“st"。

假设在第t个timestep解码成 “-s-e-e"( l_{6}^{'}=l_{4}^{'} ​),在第t-1个timestep中,当前的解码只可能是 “-s-e-"(l_{1:5}^{'})或者“-s-e" (l_{1:4}^{'}), 只有这样才能正确解码。“-s-e-" 加入第t个timestep中的"e",会变成”-s-e-e", 去掉blank会解码成正确的”see“。 “-s-e"加入第t个timestep中的"e"会变成”-s-ee", 去掉blank也会解码成正确的”see“。

接着看公式(9)下面 的情况,也就是普通情况下的递推公式:

假设在第t个timestep解码成 “-s-t-a"(​ l_{1:6}^{'} ),在第t-1个timestep中,当前的解码只可能是 “-s-t-a" (l_{1:6}^{'}) ​或者“-s-t-" (l_{1:5}^{'}​)再或者“-s-t" (l_{1:4}^{'}), 只有这样才能正确解码。“-s-t-a" 加入第t个timestep中的“a“,会变成”-s-t-aa", 合并两个相邻的"a"变成“-s-t-a", 去掉blank可以解码成“sta"。 “-s-t-"加入第t个timestep中的a会变成”-s-t-a",去掉blank可以解码成“sta" 。“-s-t"加入第t个timestep中的a会变成”-s-ta",去掉blank可以解码成“sta"。

def forward(y, labels):
    T,C = y.shape #T: timestep  
    L = len(labels)
    alpha=np.zeros([T,L])
    alpha[0,0]=y[0,labels[0]]
    alpha[0,1]=y[0,labels[1]]
    for t in range(1,T):
        for i in range(L):
            s= labels[i]
            a = alpha[t-1,i]
            if i-1>=0:
                a += alpha[t-1,i-1]
            if i-2>=0 and s!=0 and s!=labels[i-2]:
                a +=alpha[t-1,i-2]
            alpha[t,i]=a*y[t,s]
    return alpha
  
labels = [0, 19, 0, 20, 0, 1, 0, 20, 0, 5, 0]
alpha = forward(y,labels)

 就像刚刚所说,末尾带有blank和不带有blank都是正确的,“-s-t-a-t-e-"和"-s-t-a-t-e"都可以正确解码,所以

p = alpha[-1,lables[-1]]+alpha[-1,lables[-2]]

​

2.2.2 backward部分

 这个公式计算的是所有能够解码成​的概率,

上面三个式子是说第T个timestep的解码成”blank“的概率是y_{b}^{T}, 解码成​中第一个字符的概率是y_{l_{\left | l \right |}}^{T}, 其他的字符的概率为0, 可以这样理解,如果路径能够解码成正确的”state", 那么第T个timestep的肯定是blank或者"e", 只有这样才能解码正确。 我们可以得到与forward相似的递推式:

def backward(y, labels):
    T,C = y.shape #T: timestep  
    L = len(labels)
    beta=np.zeros([T,L])
    beta[-1,-1]=y[-1,labels[-1]]
    beta[-1,-2]=y[-1,labels[-2]]
    for t in range(T-2,-1,-1):
        for i in range(L):
            s= labels[i]
            b = beta[t+1,i]
            if i+1<L:
                b += beta[t+1,i+1]
            if i+2<L and s!=0 and s!=labels[i+2]:
                b +=beta[t+1,i+2]
            beta[t,i]=b*y[t,s]
    return beta
  
labels = [0, 19, 0, 20, 0, 1, 0]
beta = backward(y,labels)

2.3 梯度

求了上面的forward和backward之后,就可以求解梯度了

根据可以得到 

 

因为 

以对​求导的话, 仅有当​为类别k的那一项不为0, 其余项的偏导都为0

def gradient(y,labels):
    T,C = y.shape
    L = len(labels)
    alpha = forward(y,labels)
    beta = backward(y,labels)
    p = alpha[-1,-1]+alpha[-1,-2]
    gradient = np.zeros([T,V])
    for t in range(T):
        for c in range(C):
            lab = [idx for idx, item in enumerate(labels) if item == c]
            for i in lab:
                gradient[t, s] += alpha[t, i] * beta[t, i]
            gradient[t,c]/=-(y[t,c]**2)
    return gradient3
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值