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)={ u1u

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对于CTC Loss算法的设计图,我们可以参考以下步骤: 1. 首先,我们需要进行前向计算,这可以参考文章"CTC实现——compute ctc loss(1)"和"CTC实现——compute ctc loss(2)"。在前向计算中,我们使用的是CTC Loss公式,其中包含了字符集合以及一个特殊的空白标记(blank)。这个公式通过计算路径的概率来确定预测文本和目标文本之间的差异。 2. 在实际的代码实现中,我们可以使用PyTorch中提供的CTCLoss函数来计算CTC Loss。引用和引用中的代码片段展示了如何使用nn.CTCLoss函数,其中设置了空白标记的索引以及reduction参数为'mean',表示对所有样本的CTC Loss进行平均。 综上所述,CTC Loss算法的设计图主要包括前向计算和使用CTCLoss函数进行计算。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [ctc_loss 公式推导C++实现](https://blog.csdn.net/Robbery07/article/details/119639033)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [pytorch的torch.nn.CTCLoss方法](https://blog.csdn.net/benben044/article/details/125130411)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值