ctc_loss 公式推导与C++实现

CTC简介

本文不介绍CTC的背景与具体应用,有关CTC的基本知识可以通过以下文章了解:
Hannun Awni经典博客.

总体思路

初始时我们得到一个输入矩阵,它的行代表时间步,长度为T,列代表不同的字符在不同时间片下的概率,长度为alphabet_size(包含空字符)。同时我们有一个lebel序列,它代表我们的正确的输出。
我们需要做的是根据label序列利用该矩阵前向计算一遍概率,并把每个字符在每个时间步下的概率保存在矩阵α(t,u)中。然后反向计算一遍概率,存储在矩阵β(t,u)中。最后利用我们推导出的公式计算每一个输入的梯度。
在本文中,我们的实现只以一个样本为例。

准备工作

1. softmax

我们初始得到的输入矩阵并不表示概率,需要对该矩阵的每一行进行softmax。
softmax公式: S i = e v i ∑ j e v j S_i=\dfrac{e^{v_i}}{\sum_je^{v_j}} Si=jevjevi

2.label的扩展

CTC的算法要求我们对原laebl进行扩展,具体是在laebl的开头,结尾以及每两个字符中间加上空符号。如原label为 {1,3,5,5,7} 则扩展后的符号为:
{0,1,0,3,0,5,0,5,0,7,0} 注:这里的数字对应的符号在符号表中的下标,0默认为空字符。

3.取对数

由于在CTC_loss的计算过程中有大量的概率相乘,这些浮点数往往较小,极有可能发生underflow,为了数值的稳定,我们往往先对这些概率取对数,将概率的加法与乘法转化为对数的计算:
概率乘法转化: log ⁡ ( a ⋅ b ) = log ⁡ ( a ) + log ⁡ ( b ) \log(a\cdot b)=\log(a)+\log(b) log(ab)=log(a)+log(b)
概率加法转化: log ⁡ ( a + b ) = log ⁡ ( 1 + e ( log ⁡ ( b ) − log ⁡ ( a ) ) ) \log(a+b)=\log(1+e^{(\log(b)-\log(a))}) log(a+b)=log(1+e(log(b)log(a))

需要特别注意,由于log(0)无法表示,
而我们在计算中会涉及大量的log(0)的运算,
所以我们需要定义一个-INFINITY,
在初始化时需要将所有初始化为-INFINITY,它表示log(0)double log_sum(double a, double b)
{
	//log(a+b)=log(a)+log(1+exp(logb-loga));
	if (a == -INFINITY)
		return b;
	if (b == -INFINITY)
		return a;

	return a + log(1 + exp(b - a));
}

double log_prod(double a,double b)
{
	if (a == -INFINITY && b == -INFINITY)
		return -INFINITY;
	if (a == -INFINITY)
		return b;
	if (b == -INFINITY)
		return a;

	return a + b;
}

4.相关代码

在具体实现中,我主要参考了百度的工作,他们在16年的时候开源了warp_ctc的代码: warp_ctc.

数据结构(类)设计:

class cpu_ctc {
public:
	cpu_ctc(vector<int> labels, vector<vector<double>> Prob);
	void compute();
	double compute_alphas();
	double compute_betas();
	void compute_gradients();
	
private:
	int* s_inc;
	int* e_inc;
	int blank_label = 0;   //空字符在字符表里对应的序号 
						 //默认为0; 
	double prob_;        //前向传播计算出的概率(取对数);
	int L,S,T;
	//L表示标签序列的长度,S表示扩展后的序列长度,T表示输入的时间步数量。
	int repeats;  //表示标签序列中紧接着的重复的标签数量。
	
	vector<vector<double>> alphas;  //计算前向概率; 
	vector<vector<double>> betas;   //计算后向概率
	vector<vector<double>> gradients;   //计算梯度; 
	vector<vector<double>> Prob;
	vector<vector<double>> tmp_alphas;
	vector<vector<double>> tmp_betas;
	vector<vector<double>> tmp_gradients;

	int* labels_with_blanks;
	//注:出现repeats后相当于原label需要扩充一个空! 
	int prepare(vector<int> labels, vector<vector<double>> Prob);
};

未加注释的设计的具体意义将在后文中详细介绍。

初始化工作:

//在构造函数中进行一些数据的初始化工作;
//写的较为朴素;
cpu_ctc::cpu_ctc(vector<int> labels, vector<vector<double>> Prob)
{
	for (int i = 0; i < Prob.size(); i++)  //初始化输入概率
	{
		vector<double> single;
		for (int j = 0; j < Prob[i].size(); j++)
			single.push_back(Prob[i][j]);
		this->Prob.push_back(single);
	}

	for (int i = 0; i < Prob.size(); i++) //alpha数组和beta数组
	//为正常的概率计算数组,全初始化为0.
	{
		vector<double> tmp;
		for (int j = 0; j < 2 * labels.size() + 1; j++)
			tmp.push_back(0);

		this->alphas.push_back(tmp);
		this->betas.push_back(tmp);
		//this->gradients.push_back(tmp);
	}
	
	//tmp_alphas和tmp_betas为log域下概率数组,全部初始化为-INFINITY
	for (int i = 0; i < Prob.size(); i++)
	{
		vector<double> tmp;
		for (int j = 0; j < 2 * labels.size() + 1; j++)
			tmp.push_back(-INFINITY);

		this->tmp_alphas.push_back(tmp);
		this->tmp_betas.push_back(tmp);
		//this->gradients.push_back(tmp);
	}
//初始化梯度数组;
	for (int i = 0; i < Prob.size(); i++)
	{
		vector<double> tmp;
		vector<double> tmp_;
		for (int j = 0; j < Prob[0].size(); j++)
		{
			tmp.push_back(0);
			tmp_.push_back(-INFINITY);
		}
			
		this->tmp_gradients.push_back(tmp_);
		this->gradients.push_back(tmp);
	}
	L = labels.size();
	T = Prob.size();
	repeats = this->prepare(labels, Prob);
}

prepare函数:
prepare函数的目的主要有两个:
1.生成s_int和e_int数组; 这两个数组在计算前向和反向概率中有用。
2.用blank符号扩充labels;

//主要模仿的是百度warp-ctc的开源代码;
int cpu_ctc::prepare(vector<int> labels, vector<vector<double>> Prob)
{
	S = 2 * labels.size() + 1;
	s_inc = (int*)malloc(sizeof(int) * (labels.size() * 2 + 1));
	e_inc = (int*)malloc(sizeof(int) * (labels.size() * 2 + 1));
	labels_with_blanks = (int*)malloc(sizeof(int) * (labels.size() * 2 + 1));

	int s_counter = 0;
	int e_counter = 0;

	this->s_inc[s_counter++] = 1;  //remain=0,start+=1;   
	int repeats = 0;

	for (int i = 1; i < labels.size(); i++)
	{
		if (labels[i - 1] == labels[i])  //repeat; 
		{
			repeats++;
			s_inc[s_counter++] = 1;
			s_inc[s_counter++] = 1;
			e_inc[e_counter++] = 1;
			e_inc[e_counter++] = 1;
		}
		else
		{
			this->s_inc[s_counter++] = 2;
			this->e_inc[e_counter++] = 2;
		}
	}
	this->e_inc[e_counter++] = 1;

	for (int i = 0; i < labels.size(); i++)
	{
		this->labels_with_blanks[2 * i] = this->blank_label;
		this->labels_with_blanks[2 * i + 1] = labels[i];
	}

	labels_with_blanks[2 * labels.size()] = this->blank_label;
	return repeats;
}

具体理解可以参考这两篇文章:
CTC实现——compute ctc loss(1).
CTC实现——compute ctc loss(2).

前向计算

原理

在这里插入图片描述

前向传播主要有两个任务,一是在给定输入x的情况下,计算得到label l的概率,即 p ( l ∣ x ) p(l|x) p(lx)。而一个序列 l l l可能通过多条路径映射得到,随着 l l l长度的增加,相应路径的数量成指数增加,所以我们需要一种高效的算法帮助我们计算。
设原label的长度为U,经过扩展后长度变为2U+1,设扩展后的序列为 l ′ l' l。对于一个特定的序列 l l l,我们定义前向变量 α ( t , u ) = ∑ π ∈ V ( t , u ) ∏ i = 1 t y π i \alpha(t,u)=\sum_{\pi\in{V(t,u)}}\prod_{i=1}^ty_{\pi}^i α(t,u)=πV(t,u)i=1tyπi
其中 V ( t , u ) V(t,u) V(t,u)代表所有经过映射之后为序列 l l l,且长度为t的序列,且在第t步输出为 l u ′ l_{u}^{'} lu的集合。
由于 α ( t , u ) \alpha(t,u) α(t,u)的每一个后续状态一定依赖于前面的状态转移,可以借助动态规划算法来求解,且 α \alpha α数组大小为 ( 2 U + 1 ) ⋅ T (2U+1)\cdot T (2U+1)T
初始状态:
所有label的正确开头都只能是空或者第一个字符,所以初始化状态为:
α ( 1 , 1 ) = y 0 1 α ( 1 , 2 ) = y 1 1 α ( 1 , u ) = 0 u > 2 \alpha(1,1)=y_{0}^1\\ \alpha(1,2)=y_{1}^1 \\ \alpha(1,u)=0 \quad u>2 α(1,1)=y01α(1,2)=y11α(1,u)=0u>2
递推关系:
α ( t , u ) = y l u ′ t ∑ i = f ( u ) u α ( t − 1 , i ) \alpha(t,u)=y_{l_{u}^{'}}^{t}\sum_{i=f(u)}^{u}\alpha(t-1,i) α(t,u)=yluti=f(u)uα(t1,i)
其中:
f ( u ) = { u − 1 i f l u ′ = b l a n k ∣ l u − 2 ′ = l u ′ u − 2 o t h e r w i s e f(u)=\left\{ \begin{array}{rcl} u-1 & if l_{u}^{'}=blank | l_{u-2}^{'}=l_{u}{'}\\ u-2 &otherwise \end{array} \right. f(u)={u1u2iflu=blanklu2=luotherwise
即当当前字符是空或者与上一个字符相同(这样必须通过一个空字符过渡),当前状态依赖于上一个时间步的前两个状态,即通过当前字符和当前的前一个字符转移而来。
而其他情况都可以由三个状态转移而来。
最终 p ( l ∣ x ) = ∑ i = 0 2 u + 1 α ( T − 1 , i ) p(l|x)=\sum_{i=0}^{2u+1}\alpha(T-1,i) p(lx)=i=02u+1α(T1,i)

具体实现

特别注意,这里既有正常的概率计算,即alphas数组所对应的计算,也有log域下的计算,即tmp_alphas所对应的计算。
依旧参考了百度的逻辑。

double cpu_ctc::compute_alphas()
{
	int start = ((L + repeats - T) < 0) ? 0 : 1,
		end = (L * 2 + 1) > 1 ? 2 : 1;
	
	for (int i = start; i < end; i++)  
	{
		alphas[0][i] = Prob[0][labels_with_blanks[i]];      
		tmp_alphas[0][i] = log(Prob[0][labels_with_blanks[i]]);                        
	}

	for (int t = 1; t < T; ++t)   //模拟时间  
	{
		int remain = L + repeats - (T - t);   //label数量和剩余时间的差; 
		if (remain >= 0)                     
			start += this->s_inc[remain];      

		if (t <= L + repeats)	//若t>L,则已到末尾;       
			end += e_inc[t - 1];          
		                         
		int startloop = start;            
		if (start == 0)                              
		{
			alphas[t][0] = alphas[t-1][0]*Prob[t][blank_label];  //概率相乘; 
			tmp_alphas[t][0] = tmp_alphas[t - 1][0]+log(Prob[t][blank_label]);

			startloop += 1;
		}

		for (int i = startloop; i < end; i++)
		{
			double prev_sum = alphas[t - 1][i] + alphas[t - 1][i - 1];
			double tmp_sum = log_sum(tmp_alphas[t - 1][i], tmp_alphas[t - 1][i - 1]);

			if (labels_with_blanks[i] != blank_label && i != 1 && labels_with_blanks[i - 2] != labels_with_blanks[i])
			{
				prev_sum += alphas[t - 1][i - 2];
				tmp_sum = log_sum(tmp_sum,tmp_alphas[t-1][i-2]);
			}
			alphas[t][i] = prev_sum*Prob[t][labels_with_blanks[i]];
			tmp_alphas[t][i] = tmp_sum + log(Prob[t][labels_with_blanks[i]]);
		}
	}
	double score = 0;
	for (int i = start; i < end; i++)
	{
		score += alphas[T-1][i];
	}
	double tmp_score = -INFINITY;
	for (int i = start; i < end; i++)
	{
		tmp_score = log_sum(tmp_score, tmp_alphas[T - 1][i]);
	}
	prob_ = log(score);
	return score;
}

反向计算

原理

反向计算的主要任务是计算存储 β ( t , u ) \beta(t,u) β(t,u)数组,并将最终求得的概率与前向求得的概率相验证。
其中: β ( t , u ) = ∑ π ∈ W ( t , u ) ∏ i = 1 T − t − 1 y π i + t \beta(t,u)=\sum_{\pi\in{W(t,u)}}\prod_{i=1}^{T-t-1}y_{\pi}^{i+t} β(t,u)=πW(t,u)i=1Tt1yπi+t
W ( t , u ) W(t,u) W(t,u)的含义是所有在t时刻输出为 l u ′ l_{u}^{'} lu的合法路径的从t开始至结尾的部分。
β ( t , u ) \beta(t,u) β(t,u)就是从t+1时刻起一直到序列结尾的所有合法路径(映射后为 l l l)的概率之和(不包含时间t)。
这样 α ( t , u ) ⋅ β ( t , u ) \alpha(t,u) \cdot \beta(t,u) α(t,u)β(t,u)的值就是,在t时间步输出 l u ′ l_{u}^{'} lu的所有合法路径的概率之和。
反向数组的求解可以理解为前向的逆过程,其总体思路与前向相似。
初始化:
β ( T − 1 , U ′ ) = β ( T − 1 , U ′ − 1 ) = 1 β ( T − 1 , u ) = 0 u < U ′ − 1 \beta(T-1,U')=\beta(T-1,U'-1)=1\\ \beta(T-1,u)=0\quad u<U'-1 β(T1,U)=β(T1,U1)=1β(T1,u)=0u<U1
其递推公式为: β ( t , u ) = y l u ′ t ∑ i = u g ( u ) β ( t + 1 , i ) \beta(t,u)=y_{l_{u}^{'}}^{t}\sum_{i=u}^{g(u)}\beta(t+1,i) β(t,u)=yluti=ug(u)β(t+1,i)
其中:
g ( u ) = { u + 1 i f l u ′ = b l a n k ∣ l u + 2 ′ = l u ′ u + 2 o t h e r w i s e g(u)=\left\{ \begin{array}{rcl} u+1 & if l_{u}^{'}=blank | l_{u+2}^{'}=l_{u}{'}\\ u+2 &otherwise \end{array} \right. g(u)={u+1u+2iflu=blanklu+2=luotherwise

具体实现

反向计算的实现与前向计算极为相似,相当于前向计算的逆过程。
实现了正常概率与log域下的计算:

double cpu_ctc::compute_betas()
{
	int start = S > 1 ? (S - 2) : 0,
		end = (T > (S / 2) + repeats) ? S : S - 1;

	for (int i = start; i < end; i++)
	{
		betas[T - 1][i] = Prob[T - 1][labels_with_blanks[i]];
		tmp_betas[T - 1][i] = log(Prob[T - 1][labels_with_blanks[i]]);
	}

	for (int t = T - 2; t >= 0; t--)
	{
		int remain = (S / 2) + repeats - (T - t);
		if (remain >= -1)
			start -= s_inc[remain + 1];
		if (t < (S / 2) + repeats)
			end -= e_inc[t];
		int endloop = end;
		for (int i = start; i < endloop; ++i) {
			double next_sum=0;
			double tmp_sum = -INFINITY;
			if (i == S - 1)
			{
				next_sum = betas[t + 1][i];
				tmp_sum = log(betas[t + 1][i]);
			}
				
			else
			{
				next_sum = betas[t + 1][i] + betas[t + 1][i + 1];
				tmp_sum = log_sum(tmp_betas[t + 1][i], tmp_betas[t + 1][i + 1]);
			}

			if (labels_with_blanks[i] != blank_label && i != (S - 2) && labels_with_blanks[i] != labels_with_blanks[i + 2]) {
				next_sum += betas[t + 1][i + 2];
				tmp_sum = log_sum(tmp_sum, tmp_betas[t + 1][i + 2]);
			}
	
			betas[t][i] = next_sum * Prob[t][labels_with_blanks[i]];
			tmp_betas[t][i] = tmp_sum + log(Prob[t][labels_with_blanks[i]]);
		}
	}

	double score = 0;
	double tmp_score = -INFINITY;
	for (int i = start; i < end; i++)
	{
		score += betas[0][i];
		tmp_score = log_sum(tmp_score, tmp_betas[0][i]);
	}

	for (int i = 0; i < tmp_betas.size(); i++)
		for (int j = 0; j < tmp_betas[0].size(); j++)
			if (tmp_betas[i][j] != -INFINITY)
				tmp_betas[i][j] = (tmp_betas[i][j]- log(Prob[i][labels_with_blanks[j]]));
	for (int i = 0; i < betas.size(); i++)
		for (int j = 0; j < betas[i].size(); j++)
			betas[i][j] = betas[i][j] / Prob[i][labels_with_blanks[j]];
	
	return score;
}

需要特别注意,在计算出beta数组后,我们将其除以Prob[i][labels_with_blanks[j]],这是因为alpha数组和beta数组相乘时多计算了一次 y k t y_{k}^{t} ykt自身的概率。

损失函数

CTC的损失函数定义如下:
L ( S ) = − l n ∏ ( x , z ) ∈ S p ( z ∣ x ) = − ∑ ( x , z ) ∈ S l n p ( z ∣ x ) L(S)=-ln\prod_{(x,z)\in S}p(z|x)=-\sum_{(x,z)\in S}lnp(z|x) L(S)=ln(x,z)Sp(zx)=(x,z)Slnp(zx)
其中S为训练集。损失函数可以解释为:给定标签序列和输入,最终输出正确序列的概率,为了方便计算,我们将这个概率取负对数。而我们将取负对数之后的loss最小化就是将输出概率最大化。
而我们在前文提到过,对于给定的 t t t u u u,%= α ( t , u ) β ( t , u ) \alpha(t,u) \beta(t,u) α(t,u)β(t,u)有具体的意义。具体写成公式为:
α ( t , u ) β ( t , u ) = ∑ π ∈ X ( t , u ) ∏ t = 1 T y π t t \alpha(t,u) \beta(t,u)=\sum_{\pi \in{X(t,u)}} \prod_{t=1}^{T}y_{\pi_{t}}^{t} α(t,u)β(t,u)=πX(t,u)t=1Tyπtt
X ( t , u ) X(t,u) X(t,u)表示在t时刻经过 l u ′ l_{u}^{'} lu所有路径的集合。
进一步可以转换为
α ( t , u ) β ( t , u ) = ∑ π ∈ X ( t , u ) p ( π ∣ x ) \alpha(t,u) \beta(t,u)=\sum_{\pi \in{X(t,u)}} p(\pi|x) α(t,u)β(t,u)=πX(t,u)p(πx)
所以给定任意时刻的t,我们的概率可以这么计算:
p ( z ∣ x ) = ∑ u = 0 ∣ z ′ − 1 ∣ α ( t , u ) β ( t , u ) p(z|x)=\sum_{u=0}^{|z^{'}-1|}\alpha(t,u) \beta(t,u) p(zx)=u=0z1α(t,u)β(t,u)
其意义是任意时刻,从扩展后的序列的第一个位置直到最后一个位置的 α β \alpha\beta αβ乘积之和就是 p ( z ∣ x ) p(z|x) p(zx)的概率,只要理解了上述所说的所有公式的含义,这个式子也是可以直观理解的。在任意t时刻,如果合法的路径有经过 l u ′ l_{u}^{'} lu的,乘积一定不为0,而不经过一定为0.所有乘积加和一定是最终概率。这也很容易通过程序验证。

梯度计算

公式推导

相信大家在看前向过程和反向过程的时候都是一知半解,搞懂了这些计算的过程,但并不明白计算这些中间结果到底有什么用。其实,前面所做的都是为最终求输入的梯度做准备的。
假设经softmax后的网络输出 y k t y_{k}^{t} ykt代表第t个时间片第k个字符的输出概率。则损失函数对网络输出 y k t y_{k}^{t} ykt的偏导数为:
∂ L ( x , y ) ∂ y k t = − ∂ ln ⁡ p ( x ∣ z ) ∂ y k t = − 1 p ( x ∣ z ) ∂ p ( x ∣ z ) ∂ y k t \frac{\partial L(x,y)}{\partial y_{k}^{t}}=-\frac{\partial \ln p(x|z)}{\partial y_{k}^{t}}=-\frac{1}{p(x|z)}\frac{\partial p(x|z)}{\partial y_{k}^{t}} yktL(x,y)=yktlnp(xz)=p(xz)1yktp(xz)
关于p(z|x)的意义已经在前文中解释过。
在这里,我们再引入一个集合 B ( z , k ) = { u : z u ′ = k } B(z,k)=\{u: z_{u}^{'}=k\} B(z,k)={u:zu=k},它的意义是,k出现在序列z’中的所有路径的集合(注:k可以在路径中任意位置出现)。这样,我们很容易得到:
∂ α ( t , u ) β ( t , u ) ∂ y k t = { α ( t , u ) β ( t , u ) y k t i f k o c c u r s i n z ′ 0 o t h e r w i s e \frac{\partial \alpha(t,u) \beta(t,u)}{\partial y_{k}^{t}}=\left\{ \begin{array}{rcl} \frac{ \alpha(t,u) \beta(t,u)}{y_{k}^{t}} & if \quad k \quad occurs \quad in \quad z'\\ 0 &otherwise \end{array} \right. yktα(t,u)β(t,u)={yktα(t,u)β(t,u)0ifkoccursinzotherwise
因此,损失函数关于输出的偏导数可以写为:
∂ L ( x , y ) ∂ y k t = − 1 p ( x ∣ z ) ∂ p ( x ∣ z ) ∂ y k t = − 1 p ( x ∣ z ) y k t ∑ u ∈ B ( z , k ) α ( u , t ) β ( u , t ) \frac{\partial L(x,y)}{\partial y_{k}^{t}}=-\frac{1}{p(x|z)}\frac{\partial p(x|z)}{\partial y_{k}^{t}}=-\frac{1}{p(x|z)y_{k}^{t}}\sum_{u\in B(z,k)}\alpha(u,t)\beta(u,t) yktL(x,y)=p(xz)1yktp(xz)=p(xz)ykt1uB(z,k)α(u,t)β(u,t)
最后,由于该输出是经过softmax之后的结果,我们需要得到损失函数对于softmax之前的网络输出的 α k t \alpha_{k}^{t} αkt的偏导数。
∂ L ( x , y ) ∂ a k t = − ∑ k ′ ∂ L ( x , y ) ∂ y k ′ t ∂ y k ′ t a k t \frac{\partial L(x,y)}{\partial a_{k}^{t}}=-\sum_{k'}\frac{\partial L(x,y)}{\partial y_{k'}^{t}}\frac{\partial y_{k'}^{t}}{a_{k}^{t}} aktL(x,y)=kyktL(x,y)aktykt
其中, y k t = e a k t ∑ k ′ e a k ′ t y_{k}^{t}=\frac{e^{a_{k}^{t}}}{\sum_{k'}e^{a_{k'}^{t}}} ykt=keakteakt
针对未经softmax的网络层的梯度推导较为复杂,具体推导如下:
∂ y k ′ t ∂ a k t = ∂ e a k t ∑ k ′ e a k ′ t ∂ a k t \frac{\partial y_{k'}^{t}}{\partial a_{k}^{t}}=\frac{\partial \frac {e^{a_{k}^{t}}}{\sum _{k'}e^{a_{k'}^{t}}}}{\partial a_{k}^{t}} aktykt=aktkeakteakt
这里需要特别注意,当 y k ′ t y_{k'}^{t} ykt中的k’与 a k t a_{k}^{t} akt中的k相同与不相同时求导结果是不同的,当 k ′ = k k'=k k=k时:
∂ e a k t ∑ k ′ e a k ′ t ∂ a k t = e a k t ( ∑ k ′ e a k t − e a k t ) ( ∑ k ′ e a k t ) 2 \frac{\partial \frac {e^{a_{k}^{t}}}{\sum _{k'}e^{a_{k'}^{t}}}}{\partial a_{k}^{t}}=\frac{e^{a_{k}^{t}}(\sum_{k'}e^{a_{k}^{t}}-e^{a_{k}^{t}})}{(\sum_{k'}e^{a_{k}^{t}})^{2}} aktkeakteakt=(keakt)2eakt(keakteakt)
注意:这里的 e a k t ∑ k ′ e a k ′ t \frac{e^{a_{k}^{t}}}{\sum_{k'}e^{a_{k'}^{t}}} keakteakt可以提取为 y k t y_{k}^{t} ykt,所以原式可化简为:
= y k t ∑ k ′ e a k t − e a k t ∑ k ′ e a k ′ t = y k t ( 1 − y k t ) =y_{k}^{t} \frac{\sum_{k'}e^{a_{k}^{t}}-e^{a_{k}^{t}}}{\sum_{k'}e^{a_{k'}^{t}}}\\ =y_{k}^{t}(1-y_{k}^{t}) =yktkeaktkeakteakt=ykt(1ykt)
同理,当 k ′ ! = k k'!=k k!=k时:
∂ y k ′ t ∂ a k t = − e k ′ t e k t ( ∑ k ′ e a k t ) 2 = − y k t y k t \frac{\partial y_{k'}^{t}}{\partial a_{k}^{t}}=\frac {-e_{k'}^{t}e_{k}^{t}}{(\sum_{k'}e^{a_{k}^{t}})^{2}}\\ =-y_{k}^{t}y_{k}^{t} aktykt=(keakt)2ektekt=yktykt
k ′ = k k'=k k=k时(所有),有:
∂ L ( x , y ) ∂ a k t = y k t − 1 p ( z ∣ x ) ∑ u ∈ B ( z , k ) α ( u , t ) β ( u , t ) \frac{\partial L(x,y)}{\partial a_{k}^{t}}=\frac {y_{k}^{t}-1}{p(z|x)}\sum_{u\in B(z,k)}\alpha(u,t)\beta(u,t) aktL(x,y)=p(zx)ykt1uB(z,k)α(u,t)β(u,t)
k ′ ! = k k'!=k k!=k时(k’!=k的所有情况),有:
∂ L ( x , y ) ∂ a k t = y k t p ( z ∣ x ) ∑ k ′ ∑ u ∈ B ( z , k ′ ) α ( u , t ) β ( u , t ) \frac{\partial L(x,y)}{\partial a_{k}^{t}}=\frac {y_{k}^{t}}{p(z|x)}\sum_{k'}\sum_{u\in B(z,k')}\alpha(u,t)\beta(u,t) aktL(x,y)=p(zx)yktkuB(z,k)α(u,t)β(u,t)
相加得:
∂ L ( x , y ) ∂ a k t = y k t p ( z ∣ x ) ∑ k ∑ u ∈ B ( z , k ) α ( u , t ) β ( u , t ) − 1 p ( z ∣ x ) ∑ u ∈ B ( z , k ) α ( u , t ) β ( u , t ) \frac{\partial L(x,y)}{\partial a_{k}^{t}}=\frac {y_{k}^{t}}{p(z|x)}\sum_{k}\sum_{u\in B(z,k)}\alpha(u,t)\beta(u,t)-\frac{1}{p(z|x)}\sum_{u\in B(z,k)}\alpha(u,t)\beta(u,t) aktL(x,y)=p(zx)yktkuB(z,k)α(u,t)β(u,t)p(zx)1uB(z,k)α(u,t)β(u,t)
特别注意:
∑ k ∑ u ∈ B ( z , k ) α ( u , t ) β ( u , t ) \sum_{k}\sum_{u\in B(z,k)}\alpha(u,t)\beta(u,t) kuB(z,k)α(u,t)β(u,t)就是 p ( z ∣ x ) p(z|x) p(zx)(可以从公式的实际意义出发,该式意义就是所有可以映射为z序列的序列之和)
于是,最终的式子可以化简为:
y k t − 1 p ( z ∣ x ) ∑ u ∈ B ( z , k ) α ( u , t ) β ( u , t ) y_{k}^{t}-\frac{1}{p(z|x)}\sum_{u\in B(z,k)}\alpha(u,t)\beta(u,t) yktp(zx)1uB(z,k)α(u,t)β(u,t)
这就是我们最终推导出的相对于未经softmax的网络层的梯度。

具体实现

梯度的计算主要难在公式的推到上,将最终的公式推导出后具体实现较为简单,就是对公式的简单模拟。

void cpu_ctc::compute_gradients()   //将梯度改为log域!!!
{
 	for (int t = 0; t < T; t++)
	{
		for (int k = 0; k < Prob[0].size(); k++)  //a{k,t} 表示在t时刻k字符的梯度; 
		{
			double prob_sum = 0;
			double tmp_sum = -INFINITY;
			
			for (int i = 0; i < 2 * L + 1; i++)
			{
				if (labels_with_blanks[i] == k)
				{
					prob_sum += alphas[t][i] * betas[t][i];
					tmp_sum = log_sum(tmp_sum, log_prod(tmp_alphas[t][i],tmp_betas[t][i]));
				}
			}

			gradients[t][k] = Prob[t][k] - prob_sum / exp(prob_);
			if(tmp_sum!=-INFINITY)
				tmp_gradients[t][k] = Prob[t][k] -  exp(tmp_sum - prob_);
			else
				tmp_gradients[t][k] = Prob[t][k];
		}
	}
}
  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值