CTC相关知识

目录

 

简介

算法详解

1.1 对齐

1.2 损失函数

1.3 预测

1.3.1 Greedy Search

1.3.2 Beam Search

CTC的特征

参考知识


简介

在语音识别中,我们的数据集是音频文件和其对应的文本,不幸的是,音频文件和文本很难再单词的单位上对齐。除了语言识别,在OCR,机器翻译中,都存在类似的Sequence to Sequence结构,同样也需要在预处理操作时进行对齐,但是这种对齐有时候是非常困难的。如果不使用对齐而直接训练模型时,由于人的语速的不同,或者字符间距离的不同,导致模型很难收敛。

CTC(Connectionist Temporal Classification)是一种避开输入与输出手动对齐的一种方式,是非常适合语音识别或者OCR这种应用的。

                                                                                                                       图1:CTC用于OCR(左)和语音识别(右)

给定输入序列X=[x1,x2,...,xt]  以及对应的标签数据 Y = [y1,y2,...,yu]例如语音识别中的音频文件和文本文件。我们的工作是找到 X 到 Y 的一个映射,这种对时序数据进行分类的算法叫做Temporal Classification。

这种对时序数据进行分类的算法叫做Temporal Classification。

对比传统的分类方法,时序分类有如下难点:

  1. X 和  Y的长度都是变化的;
  2.  X和 Y 的长度是不相等的;
  3. 对于一个端到端的模型,我们并不希望手动设计X 和Y  的之间的对齐。

CTC提供了解决方案,对于一个给定的输入序列 X ,CTC给出所有可能的 Y 的输出分布。根据这个分布,我们可以输出最可能的结果或者给出某个输出的概率。

损失函数:给定输入序列 X,我们希望最大化Y的后验概率P(Y|X),P(Y|X)是可导的,测试:给定一个训练好的模型和输入序列X,我们希望输出概率最高的Y:

                                                                                                           Y^{*}=argmaxYP(Y|X)

算法详解

给定输入X,CTC输出每个可能输出及其条件概率,问题的关键是CTC的输出概率是如何考虑XY之间是如何对齐的,这种对齐也是构建损失函数的基础。所以,首先我们分析CTC的对齐方式,然后我们在分析CTC的损失函数的构造。

1.1 对齐

需要注意的是,CTC本身是不需要对齐的,但是我们需要知道X的输出路径和最终输出结果的对应关系,因为在CTC中,多个输出路径可能对应一个输出结果,举例来理解。例如在OCR的任务中,输入X是含有“CAT”的图片,输出 Y是文本[C, A, T]。将X分割成若干个时间片,每个时间片得到一个输出,一个最简答的解决方案是合并连续重复出现的字母,如图2.

                                                              

                                                                         图2:CTC的一种原始对齐策略

这个问题有两个缺点:

  1. 几乎不可能将X的每个时间片都和输出Y对应上,例如OCR中字符的间隔,语音识别中的停顿
  2. 不能处理有连续重复字符出现的情况,例如单词"HELLO",按照上面的算法,输出的是“”HELO"而非"HELLO"

为了解决上面的问题,CTC引入了空白字符\epsilon,例如OCR中的字符间距,语音识别中的停顿都可以表示为\epsilon。所以,CTC的对齐涉及去除重复字母和去除\epsilon两部分,如图3

                                                                    图三:CTC的对齐策略

这种对齐方式有三个特征

  1. X与Y之间的时间片映射是单调的,即如果X向前移动一个时间片,Y保持不动或者也向前移动一个时间片
  2. X与Y之间的映射是多对一的,即多个输出可能对应一个映射,反之则不成立,这样就有了特征三
  3. X的长度大于或者等于Y的长度。

1.2 损失函数

CTC的时间片的输出和输出序列的映射如图4:

                                                                                                  图4:CTC的流程

也就是说,对应标签Y,其关于输入X的后验概率可以表示为所有映射为Y的路径之和,我们的目标就是最大化Y关于x=y的后验概率P(Y|X)。假设每个时间片的输出是独立的,则路径的后延概率是每个时间片概率的累积,公式及其详细含义如图5.

                                           图5:CTC的公式及其详细含义

上面的CTC算法存在性能问题,对于一个时间片长度为T的N分类任务,所有可能的路径数为T^{N},,在很多情况下,这几乎是一个宇宙级别的数字,用于计算Loss几乎是不现实的。在CTC中采用了动态规划的思想来对查找路径进行剪枝,算法的核心思想是如果路径\pi 1\pi 2在时间片t之前的输出均相等,我们就可以提前合并他们,,如图6。

                                                                                   图6:CTC的动态规划计算输出路径

 其中,横轴的单位是X的时间片,纵轴的单位是Y插入\varepsilon的序列Z。例如对于单词“ZOO”,插入\epsilon后为:

Z={​{\epsilon ,Z,\epsilon,O,\epsilon,O}}

我们用\alpha s,t表示路径中已经合并的在横轴单位为t,纵轴单位为s的节点。根据CTC对齐方式的三个特征,输入有9个时间片,标签内容为"ZOO",P(Y|X)的所有可能的合法路径如下图所示

                                                       图7:CTC中单词ZOO的所有合法路径

上图分成两种情况

Case1:

 

 现在,我们已经可以高效的计算损失函数,下一步的工作便是计算梯度用于训练模型。由于P(Y|X)的计算只涉及加法和乘法,因此其一定是可导函数,进而我们可以使用SGD优化模型。

对于数据集D,模型的优化目标是最小化负对数似然\sum_{(X,Y)\varepsilon D}^{}-logp(Y|X)

1.3 预测

当我们训练好一个RNN模型时,给定一个输入序列X,

我们需要找到最可能的输出,也就是求解

Y* = argmaxp(Y|X)

求解最可能的输出有两种方案,一种是Greedy Search,第二种是beam search

1.3.1 Greedy Search

每个时间片均取该时间片概率最高的节点作为输出:

A^{*}=argmax\prod t = 1^{T}pt(at|X)

这个方法最大的缺点是忽略了一个输出可能对应多个对齐方式.

代码

def remove_blank(labels, blank=0):
import numpy as np


# 求每一列(即每个时刻)中最大值对应的softmax值
def softmax(logits):
	# 注意这里求e的次方时,次方数减去max_value其实不影响结果,因为最后可以化简成教科书上softmax的定义
	# 次方数加入减max_value是因为e的x次方与x的极限(x趋于无穷)为无穷,很容易溢出,所以为了计算时不溢出,就加入减max_value项
	# 次方数减去max_value后,e的该次方数总是在0到1范围内。
	max_value = np.max(logits, axis=1, keepdims=True)
	exp = np.exp(logits - max_value)
	exp_sum = np.sum(exp, axis=1, keepdims=True)
	dist = exp / exp_sum
	return dist


def remove_blank(labels, blank=0):
	new_labels = []
	# 合并相同的标签
	previous = None
	for l in labels:
		if l != previous:
			new_labels.append(l)
			previous = l
	# 删除blank
	new_labels = [l for l in new_labels if l != blank]

	return new_labels


def insert_blank(labels, blank=0):
	new_labels = [blank]
	for l in labels:
		new_labels += [l, blank]
	return new_labels


def greedy_decode(y, blank=0):
	# 按列取最大值,即每个时刻t上最大值对应的下标
	raw_rs = np.argmax(y, axis=1)
	# 移除blank,值为0的位置表示这个位置是blank
	rs = remove_blank(raw_rs, blank)
	return raw_rs, rs


np.random.seed(11)
y_test = softmax(np.random.random([30,10]))
label_have_blank, label_no_blank = greedy_decode(y_test)
print(label_have_blank)
print(label_no_blank)

1.3.2 Beam Search

Beam Search是寻找全局最优值和Greedy Search在查找时间和模型精度的一个折中。一个简单的beam search在每个时间片计算所有可能假设的概率,并从中选出最高的几个作为一组。然后再从这组假设的基础上产生概率最高的几个作为一组假设,依次进行,直到达到最后一个时间片,下图是beam search的宽度为3的搜索过程,红线为选中的假设

代码

import numpy as np


# 求每一列(即每个时刻)中最大值对应的softmax值
def softmax(logits):
    # 注意这里求e的次方时,次方数减去max_value其实不影响结果,因为最后可以化简成教科书上softmax的定义
    # 次方数加入减max_value是因为e的x次方与x的极限(x趋于无穷)为无穷,很容易溢出,所以为了计算时不溢出,就加入减max_value项
    # 次方数减去max_value后,e的该次方数总是在0到1范围内。
    max_value = np.max(logits, axis=1, keepdims=True)
    exp = np.exp(logits - max_value)
    exp_sum = np.sum(exp, axis=1, keepdims=True)
    dist = exp / exp_sum
    return dist


def remove_blank(labels, blank=0):
    new_labels = []
    # 合并相同的标签
    previous = None
    for l in labels:
        if l != previous:
            new_labels.append(l)
            previous = l
    # 删除blank
    new_labels = [l for l in new_labels if l != blank]

    return new_labels


def insert_blank(labels, blank=0):
    new_labels = [blank]
    for l in labels:
        new_labels += [l, blank]
    return new_labels


def beam_decode(y, beam_size=10):
    # y是个二维数组,记录了所有时刻的所有项的概率
    T, V = y.shape
    # 将所有的y中值改为log是为了防止溢出,因为最后得到的p是y1..yn连乘,且yi都在0到1之间,可能会导致下溢出
    # 改成log(y)以后就变成连加了,这样就防止了下溢出
    log_y = np.log(y)
    # 初始的beam
    beam = [([], 0)]
    # 遍历所有时刻t
    for t in range(T):
        # 每个时刻先初始化一个new_beam
        new_beam = []
        # 遍历beam
        for prefix, score in beam:
            # 对于一个时刻中的每一项(一共V项)
            for i in range(V):
                # 记录添加的新项是这个时刻的第几项,对应的概率(log形式的)加上新的这项log形式的概率(本来是乘的,改成log就是加)
                new_prefix = prefix + [i]
                new_score = score + log_y[t, i]
                # new_beam记录了对于beam中某一项,将这个项分别加上新的时刻中的每一项后的概率
                new_beam.append((new_prefix, new_score))
        # 给new_beam按score排序
        new_beam.sort(key=lambda x: x[1], reverse=True)
        # beam即为new_beam中概率最大的beam_size个路径
        beam = new_beam[:beam_size]

    return beam


np.random.seed(11)
y_test = softmax(np.random.random([30, 10]))
beam_chosen = beam_decode(y_test, beam_size=100)
for beam_string, beam_score in beam_chosen[:20]:
    print(remove_blank(beam_string), beam_score)

1.3.3 prefix Beam search

最简单的decode方式当然是最近拿每个frame的最大概率的token,但实际应用中这种方法字错率会颇高,且无法和语言模型结合。要与语言模型结合,必须有多个candidate,但也不能够穷尽每个frame每个token的组合,故有beam search。但beam search的candidate会有很多相同的部分,所以有了Prefix beam search。

缀束搜索(Prefix Beam Search)方法,可以在搜索过程中不断的合并相同的前缀。
具体较复杂,不过读者弄懂beam search后再想想prefix beam search的流程不是很难,主要弄懂probabilityWithBlank和probabilityNoBlank分别代表最后一个字符是空格和最后一个字符不是空格的概率即可。

 

import numpy as np
from collections import defaultdict

ninf = float("-inf")


# 求每一列(即每个时刻)中最大值对应的softmax值
def softmax(logits):
	# 注意这里求e的次方时,次方数减去max_value其实不影响结果,因为最后可以化简成教科书上softmax的定义
	# 次方数加入减max_value是因为e的x次方与x的极限(x趋于无穷)为无穷,很容易溢出,所以为了计算时不溢出,就加入减max_value项
	# 次方数减去max_value后,e的该次方数总是在0到1范围内。
	max_value = np.max(logits, axis=1, keepdims=True)
	exp = np.exp(logits - max_value)
	exp_sum = np.sum(exp, axis=1, keepdims=True)
	dist = exp / exp_sum
	return dist


def remove_blank(labels, blank=0):
	new_labels = []
	# 合并相同的标签
	previous = None
	for l in labels:
		if l != previous:
			new_labels.append(l)
			previous = l
	# 删除blank
	new_labels = [l for l in new_labels if l != blank]

	return new_labels


def insert_blank(labels, blank=0):
	new_labels = [blank]
	for l in labels:
		new_labels += [l, blank]
	return new_labels


def _logsumexp(a, b):
	'''
	np.log(np.exp(a) + np.exp(b))

	'''

	if a < b:
		a, b = b, a

	if b == ninf:
		return a
	else:
		return a + np.log(1 + np.exp(b - a))


def logsumexp(*args):
	'''
	from scipy.special import logsumexp
	logsumexp(args)
	'''
	res = args[0]
	for e in args[1:]:
		res = _logsumexp(res, e)
	return res


def prefix_beam_decode(y, beam_size=10, blank=0):
	T, V = y.shape
	log_y = np.log(y)
	# 最后一个字符是blank与最后一个字符为non-blank两种情况
	beam = [(tuple(), (0, ninf))]
	# 对于每一个时刻t
	for t in range(T):
		# 当我使用普通的字典时,用法一般是dict={},添加元素的只需要dict[element] =value即可,调用的时候也是如此
		# dict[element] = xxx,但前提是element字典里,如果不在字典里就会报错
		# defaultdict的作用是在于,当字典里的key不存在但被查找时,返回的不是keyError而是一个默认值
		# dict =defaultdict( factory_function)
		# 这个factory_function可以是list、set、str等等,作用是当key不存在时,返回的是工厂函数的默认值
		# 这里就是(ninf, ninf)是默认值
		new_beam = defaultdict(lambda: (ninf, ninf))
		# 对于beam中的每一项
		for prefix, (p_b, p_nb) in beam:
			for i in range(V):
				# beam的每一项都加上时刻t中的每一项
				p = log_y[t, i]
				# 如果i中的这项是blank
				if i == blank:
					# 将这项直接加入路径中
					new_p_b, new_p_nb = new_beam[prefix]
					new_p_b = logsumexp(new_p_b, p_b + p, p_nb + p)
					new_beam[prefix] = (new_p_b, new_p_nb)
					continue
				# 如果i中的这一项不是blank
				else:
					end_t = prefix[-1] if prefix else None
					# 判断之前beam项中的最后一个元素和i的元素是不是一样
					new_prefix = prefix + (i,)
					new_p_b, new_p_nb = new_beam[new_prefix]
					# 如果不一样,则将i这项加入路径中
					if i != end_t:
						new_p_nb = logsumexp(new_p_nb, p_b + p, p_nb + p)
					else:
						new_p_nb = logsumexp(new_p_nb, p_b + p)
					new_beam[new_prefix] = (new_p_b, new_p_nb)
					# 如果一样,保留现有的路径,但是概率上要加上新的这个i项的概率
					if i == end_t:
						new_p_b, new_p_nb = new_beam[prefix]
						new_p_nb = logsumexp(new_p_nb, p_nb + p)
						new_beam[prefix] = (new_p_b, new_p_nb)

		# 给新的beam排序并取前beam_size个
		beam = sorted(new_beam.items(), key=lambda x: logsumexp(*x[1]), reverse=True)
		beam = beam[:beam_size]

	return beam


np.random.seed(11)
y_test = softmax(np.random.random([30, 16]))
beam_test = prefix_beam_decode(y_test, beam_size=100)
for beam_string, beam_score in beam_test[:20]:
	print(remove_blank(beam_string), beam_score)

CTC的特征

  1. 条件独立:CTC的一个非常不合理的假设是其假设每个时间片都是相互独立的,这是一个非常不好的假设。在OCR或者语音识别中,各个时间片之间是含有一些语义信息的,所以如果能够在CTC中加入语言模型的话效果应该会有提升
  2. 单调对齐:CTC的另外一个约束是输入X与输出Y之间的单调对齐,OCR和语音识别中,这种约束是成立的。但是在一些场景中例如机器翻译,这个约束便无效了。
  3. 多对一映射:CTC的又一个约束是输入序列X的长度大于标签数据Y的长度,但是对于Y的长度大于X的长度的场景,CTC便失效了。

参考知识

CTC三种搜索方式

CTC详解

原出处

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
CTC(Connectionist Temporal Classification)分割是一种被广泛应用于序列学习任务中的分割方法。它最初被应用于语音识别领域,用于解码语音信号对应的文字序列。CTC分割的思想是将输入序列与输出序列对齐,使得输入序列上的每一个时间步都能对应一个输出标记。 在CTC分割中,输入序列被表示为一个矩阵,例如语音识别中的声学特征矩阵。输出序列是一个包含所有可能标记的序列,包括目标标记和空白标记。空白标记用于建立标记间的空隙,以便更好地适应输入序列与输出序列的对齐。 CTC分割的目标是找到最可能对应于输入序列的输出序列。这通过计算条件概率来实现,即给定输入序列的条件下,得到输出序列的概率。CTC分割使用动态规划算法来计算这一条件概率,通过对输入序列上的所有可能对齐路径求和来得到最终的输出序列概率。 CTC分割具有很好的鲁棒性,它可以处理输入序列与输出序列之间的对齐问题,即输入序列和输出序列长度不一致的情况。同时,CTC分割还可以处理同一输出序列上的多个相同标记的情况,这对于一些序列学习任务非常重要,例如语音识别中的连续重叠发音。 总的来说,CTC分割是一种有效的序列分割方法,可以被应用于多个领域的序列学习任务中,如语音识别、手写识别等。通过对输入序列与输出序列的对齐和概率计算,CTC分割能够找到最可能对应的输出序列,为序列学习任务提供了一种可靠且灵活的解决方案。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值