ASR流式翻译开发总结

ASR流式翻译方法及实践总结

本文版权归Pennyyu0214所有,如需转载本文或引用、复制文中原创内容的,请注明出处

介绍

流式翻译即流数据形式的翻译系统,区别于普通NMT系统在于流式数据是实时的、不完整的,一般流式数据的来源是语音转文字、对话系统反馈或其他流式翻译系统的反馈,算法需要在源源不断的数据流中利用合理的规则决定暂存/翻译/回退等操作,能够高质量、低延迟地给予译文。以下是流式场景的一个举例:

文化
文化兴则国家
文化兴则国家兴文化
文化兴则国家兴,文化强则国家强。回顾
文化兴则国家兴,文化强则国家强。回顾历史
文化兴则国家兴,文化强则国家强。回顾历史,人类社会的
文化兴则国家兴,文化强则国家强。回顾历史,人类社会的每一次跃进与
文化兴则国家兴,文化强则国家强。回顾历史,人类社会的每一次跃进与升华,都伴随着文化的
文化兴则国家兴,文化强则国家强。回顾历史,人类社会的每一次跃进与升华,都伴随着文化的历史性进步。

目标

  1. 在数据到达后能够很快地反馈相应译文,即低延迟;
  2. 在文本句子不完整的情况下能够确保实时文本的翻译质量。

相关技术

1. 松弛匹配

流式数据生成的过程可以分为两步:①生成文字;②打标点,首先由流式生成算法/模型读取原始流数据,并生成相应文字,然后送给标点模型添加标点。例如上文示例中第3行的“国家兴”与“文化”之间没有标点,到下一行就被打上了标点。一般情况下标点需要在文字产生后的下一步才能正确添加,但也有文字生成的同时将标点打上的,例如第7行到第8行的情况。
首先假设文字生成是递进的,及文字逐步的生成过程是不改变已有内容的,而标点可能随着内容的增补而改变,例如”全新的界面设计 ,将会带来全新的写作”→”全新的界面设计 ,将会带来全新的写作体验”,后一步在前一步的基础上增加了“体验”,因此属于递进式的;而”全新的界面设计 ,将会带来全新的写作”→”全新的界面设计 ,将会带来全新的创作体验”,后一步在增加了“体验”的同时也修改了“创作”,则此情况属于非递进类型。
对于递进类型的变标点流式数据,我们设计了松弛匹配算法(return_added()),思想是在匹配的过程中忽略标点。举例如下”全新的界面设计 ,将会带来全新的写作”→”全新的界面设计,将会带来全新的,写作体验”,如果使用严格匹配的话会因为后一步“全新的”后面的逗号而匹配失败,因为前一步的对应位置是没有逗号的,而松弛匹配在遇到逗号时会直接跳过,正常完成字的捕捉。匹配完成后将剩余的部分作为新片段返回。
子算法:字符串简化
松弛匹配涉及到对原始串的去标点操作remove_punc(),以获得纯净的文字串,描述如下:

输入:原字符串x,标点列表p
输出:删除所有p中符号的x'
初始化x’为''
for each(i) in x do
   if not i in p then
   	x’ = concat(x’, i)       #字符串拼接操作,返回拼接后的结果
   end if
end for 
return x’

松弛匹配算法描述如下:

输入:原字符串x,当前字符串y
输出:从y中删除x部分的新片段y',且不受标点变化的影响
x^ ← remove_punc(x)
y^ ← y
j ← 0    #累积位置
for each(i) in x^ do
	if i in y then
		pos ← getIndex(y, i) + 1    #getIndex(y, i)表示从串y中获得左起第一个i的位置
		rmn ← y^从开头到pos-1的部分
		if rmn中的不全是标点 then
		    *****抛出异常,原因是多了不应有的文字*****
		end if
		y^ ← y^从pos开始到末尾的部分
		j ← j + pos
	else
	    *****抛出异常,原因是少了应有的文字i*****
	end if
end for 
if j < length(y) and y[j]为标点 then    #length(y)表示取y的长度
    y' ← y从j+1到末尾的部分              #如果j的后一个位置是标点,则执行去除,防止返回的新片段以标点开头。
else
   	y' ← y从j到末尾的部分
end if
return 	y'
2.回退

前文讲过,松弛匹配适用于递进类型的流模式,而对于文字可能随时改变的非递进类型,松弛匹配过程会抛出异常,因此在此基础上引入回退算法,即对于去除标点的历史片段 x ^ \hat x x^ 以分段形式保存,执行过程为:如果当前状态的 x ^ \hat x x^在松弛匹配的过程中抛出异常,则删除 x ^ \hat x x^的最后一个元素,使用剩下的继续匹配,直到 x ^ \hat x x^为空,此时返回整句作为新增片段。过程描述如下:

输入:历史简化片段x^,当前字符串y
输出:从y中删除x^部分的新片段y',且不受标点和文字变化的影响
while 1 do
	try
	    y' ← return_added(x^, y)
	if 抛出异常 then
		x^ ← x^的起始到末尾-1部分
	else
	    break
	end if
	end try
end while 
return y'
3、延迟算法

延迟是评估流式翻译系统实时性的一个指标,它代表着翻译系统的响应速度,对具体落地的产品体验有着很大影响。在流式场景中,延迟的计算对应某一数据流(可以精确到单字)到达时间,即在屏幕上的出现时间起,到该句话(即该句中的每个字)的译文到达时间,即译文在屏幕上的显示时间之间的间隔。例如在某一流式场景 S = ( s 1 , s 2 , . . . s N ) S=(s_1, s_2, ... s_N) S=(s1,s2,...sN)中,对应的到达时间分别为 T = ( t 1 , t 2 , . . . t N ) T=(t_1, t_2, ... t_N) T=(t1,t2,...tN),而不同时刻执行翻译操作所耗费的时间记作 W = ( w 1 , w 2 , . . . w N ) W=(w_1, w_2, ... w_N) W=(w1,w2,...wN),则对于 t m t_m tm时刻到达、 t m + α t_{m+\alpha} tm+α时刻开始翻译的子句 e e e,其延迟计算如下: d e = t m + α − t m + w m + α d_e= t_{m+\alpha }-t_m + w_{m+\alpha} de=tm+αtm+wm+α
说明如下:当 e e e t m t_m tm时刻到达时,由于条件不足而没有立即被翻译,等到 t m + α t_{m+\alpha} tm+α时开始翻译,这时延迟已经达到了 t m + α − t m t_{m+\alpha }-t_m tm+αtm,此时还需再加上 m + α m+\alpha m+α时刻执行翻译的时间 w m + α w_{m+\alpha} wm+α,即得上式。用图例说明如下:
在这里插入图片描述

改进:

上述延迟算法应用在翻译的高响应速度情况下,即对于每一步,其翻译响应时间都低于当前步的流间隔,例如对于上图,每一个时间步下ASR的到达间隔(即上面蓝条)总是大于当前译文解码的耗时(即下面橘黄条)。而对于更一般、更实际的复杂场景中,由于各种原因(模型运算、通信等)翻译时间可能会更长一些,由此会造成当前步的翻译延迟大于ASR流间隔的情况,因此翻译会拖慢 e e e的译文反馈速度,需要改进上述延迟计算方法。

  • 首先分析较为简单的情况,就是“立即反馈”情况,即在 e e e的到达时刻 m m m就具备条件翻译,则时间 d e d_e de为: d e = w m d_e= w_m de=wm,也就是 m m m时刻的翻译时间;

  • 对于“下一步反馈”情况,即 e e e的到达时刻 m m m的下一步 m + 1 m+1 m+1具备翻译条件,则时间为 m m m时刻的延迟和 m m m的翻译相应时间中的较大值,与 m + 1 m+1 m+1时刻的翻译响应值,即 d e = w m + 1 + m a x ( t m + 1 − t m , w m ) d_e= w_{m+1} + max(t_{m+1} - t_m, w_m) de=wm+1+max(tm+1tm,wm)图片说明如下:

  • 在这里插入图片描述

  • 复杂的场景来了,如果 e e e的到达时刻是 m m m,而翻译时刻却要等到 m + α m+\alpha m+α怎么办呢?前文我们说到如果翻译相应延迟会存在翻译耗时大于流间隔的情况,由此也势必会影响下一步的翻译启动时间,因此我们定义翻译启动延迟 G = ( g 1 , g 2 , . . . g N ) G =(g_1, g_2, ... g_N) G=(g1,g2,...gN),其中 g i g_i gi代表 i i i时刻的翻译动作相比流到达时间 t i t_i ti延迟了多少,默认设 g 1 = 0 g_1=0 g1=0,因为第一时刻流数据到达前翻译模型一般都处于空闲状态,可以立即执行。对于后续情况,以上图为例:首先分析左图,由于所有时刻的翻译响应都比流间隔要低,因此不会耽误下一刻的翻译启动, g 2 g_2 g2 g 3 g_3 g3都为0;而对于右图的 g 3 g_3 g3,由于翻译的超时导致 w 2 w_2 w2大于 t 2 t_2 t2 t 3 t_3 t3的间隔,也使得时刻3的翻译延迟了,因此 g 3 = w 2 − ( t 3 − t 2 ) g_3=w_2-(t_3-t_2) g3=w2(t3t2) 对于上两种情况,求 g 3 g_3 g3的更一般的公式为 g 3 = m a x ( 0 , w 2 − ( t 3 − t 2 ) ) g_3=max(0, w_2-(t_3-t_2)) g3=max(0,w2(t3t2))
    递推形式:由于 g g g的累积特性,前一时刻如果发生超时,则延迟会传播到当前时刻中,因此 g 3 g_3 g3的递推形式为 g 3 = g 2 + m a x ( 0 , w 2 − ( t 3 − t 2 ) ) g_3=g_{2}+max(0, w_2-(t_3-t_2)) g3=g2+max(0,w2(t3t2)). 对于通用的 g i g_i gi,计算公式如下: g i = { 0 , if  i = 0 g i − 1 + m a x ( 0 , w i − 1 − ( t i − t i − 1 ) ) , else g_i= \begin{cases} 0, & \text{if $i=0$} \\ g_{i-1}+max(0, w_{i-1}-(t_i-t_{i-1})), & \text{else} \end{cases} gi={0,gi1+max(0,wi1(titi1)),if i=0else
    因此对于前文的问题,在 m m m时刻到达、 m + α m+\alpha m+α时刻翻译的片段 e e e,其翻译延迟 d e d_e de的计算为 d e = t m + α − t m + w m + α + g m + α d_e=t_{m+\alpha}-t_m+w_{m+\alpha}+g_{m+\alpha} de=tm+αtm+wm+α+gm+α

算法描述

实现细节

history_streams变量:储存已经读入并翻译的前若干个子句;
标点划分:以标点为标志进行句子划分,因为标点区分的句子翻译效果更好。

算法流程

流翻译算法如下(不使用回退):

输入:流数据S, 标点列表P
输出:对应于S中每个时刻的翻译结果T,完整翻译句子C
T ← 空列表
C ← 空字符串
history_streams ← 空列表
for each(i) in S do
	i_rmn ← return_added(i, all_cat(history_streams))    #all_cat将history_streams列表中所有字符串拼接并返回
    punc_found ← findAll(P,i_rmn)    #findAll召回包含在rmn中的标点,并返回标点列表
    trans_this ← 空字符串    #记录当前时刻的译文
   	fin_segs ← 空字符串     #记录当前时刻翻译过的原文
    for each(p) in punc_found do
        pos_p ← getIndex(i_rmn, p)
        i_rmn ← i_rmn从pos_p+1到末尾的部分
        seg4trans ← i_rmn从开头到pos_p-1的部分
        seg_trans ← translate(seg4trans)    #translate()执行翻译并返回译文
        seg_trans ← post_proc(seg_trans, p)    #post_proc()对字符串进行后处理,主要是去除末尾标点,并追加标点p,因为输入的句子是没有标点的,防止模型随意打不相关的标点
        trans_this ← concat(trans_this, seg_trans)
        fin_segs ← concat(fin_segs , seg4trans)
    history_streams ← append(history_streams, remove_punc(fin_segs))    #append()将元素追加到history_streams中
    C ← concat(C, trans_this)
    T ← append(T, trans_this)    
end for
if notNull(i_rmn) then     #notNull判断字符串是否为非空
    trans_last_rmn ← translate(i_rmn)    #将剩余的未翻译部分处理
    T[-1] ← concat(T[-1], trans_last_rmn )     #剩余部分译文追加到最后时刻的译文后面
end if
return T, C

为应对非递进情况,增加回退机制,算法描述如下:

输入:流数据S, 标点列表P
输出:对应于S中每个时刻的翻译结果T,完整翻译句子C
T ← 空列表
C ← 空字符串
history_streams ← 空列表
for each(i) in S do
	while True do
	    try        #增加回退机制
		    i_rmn ← return_added(i, all_cat(history_streams))    #all_cat将history_streams列表中所有字符串拼接并返回
		if 抛出异常 then
			history_streams ← history_streams从起始到len(history_streams)-1的位置
		else
		    break
		end if
		en try
	end while
	
    punc_found ← findAll(P,i_rmn)    #findAll召回包含在rmn中的标点,并返回标点列表
    trans_this ← 空字符串    #记录当前时刻的译文
   	fin_segs ← 空字符串     #记录当前时刻翻译过的原文
    for each(p) in punc_found do
        pos_p ← getIndex(i_rmn, p)
        i_rmn ← i_rmn从pos_p+1到末尾的部分
        seg4trans ← i_rmn从开头到pos_p-1的部分
        seg_trans ← translate(seg4trans)    #translate()执行翻译并返回译文
        seg_trans ← post_proc(seg_trans, p)    #post_proc()对字符串进行后处理,主要是去除末尾标点,并追加标点p,因为输入的句子是没有标点的,防止模型随意打不相关的标点
        trans_this ← concat(trans_this, seg_trans)
        fin_segs ← concat(fin_segs , seg4trans)
    history_streams ← append(history_streams, remove_punc(fin_segs))    #append()将元素追加到history_streams中
    C ← concat(C, trans_this)
    T ← append(T, trans_this)    
end for
if notNull(i_rmn) then     #notNull判断字符串是否为非空
    trans_last_rmn ← translate(i_rmn)    #将剩余的未翻译部分处理
    T[-1] ← concat(T[-1], trans_last_rmn )     #剩余部分译文追加到最后时刻的译文后面
end if
return T, C
时间戳机制

时间戳的粒度是字级别和句级别的,即需要同时记录单个字和短句的延迟,使用times和sub_times变量分别记录,并设置辅助变量chars和steps,分别记录已有的字列表和每个字对应的时间步序号。如果算法中加入回退机制,则回溯触发时上述变量都需要改变。首先定义子函数sum_lenl(list),它接受字符串列表list,并返回list的所有字符串总长度,伪代码如下:

输入:包含字符串类型的L
输出:L所有元素长度和total_len
total_len ← 0
for each(i) in L do
	total_len ← total_len + len(i)
end for
return total_len

加入时间戳后的伪代码如下:

输入:流数据S, 标点列表P
输出:对应于S中每个时刻的翻译结果T,完整翻译句子C,字级别延迟D,子句级别延迟D'
T ← 空列表
C ← 空字符串
D ← 空列表
D' ← 空列表
chars ← 空列表    #辅助记录当前时刻读取的所有字
time_segs ← 空列表    #将Chars按时刻划分保存
steps ← 空列表    #辅助记录Chars中每个位置对应的时刻
times_come ← 空列表    #辅助记录每一步的到达时刻
history_streams ← 空列表
stream_start = getTime()    #获得当前时间作为流起始
for each(i) in stream(S) do    #模拟流数据的到达场景,不是常规的for循环,每条数据都有一定延迟的
	step = getStep()    #获得当前流的步数(第一句为0,后面每个时刻+1)
	start = getTime()    #获得当前时间作为翻译起始
	times_come ← append(times_come, start - stream_start)
	try_time ← len(time_segs)
	while True do
		try        #为流记录增加回退机制
		    time_add ← return_added(i, all_cat(time_segs)) 
		if 抛出异常 then
			trt_time ← try_time -1
		else
		    break
		end if
		end try
	end while
	len_retain = sum_len(time_segs[:try_time])    #获取当前流与time_segs共同字前缀的长度,这里操作太繁琐,直接借用Python的列表生成式方法
	chars ← chars从起始到的位置到len_retain的部分
	D ← D从起始到的位置到len_retain的部分
	steps ← steps从起始到的位置到len_retain的部分
	
	chars ← extend(chars, remove_punc(time_add))
	D ← extend(D, [-1] * len(remove_punc(time_add))
	steps ← extend(steps, [step] * len(remove_punc(time_add))
	time_segs ← time_segs从起始到try_time - 1的部分
	time_segs ← append(time_segs, remove_punc(time_add))
	
	try_time ← len(history_streams)
    try        #子句级回退机制
	    i_rmn ← return_added(i, all_cat(history_streams))
	if 抛出异常 then
		try_time ← try_time - 1
		history_streams ← history_streams从起始到try_time-1的位置
	else
	    break
	end if
	end try
	
    punc_found ← findAll(P,i_rmn)    #findAll召回包含在rmn中的标点,并返回标点列表
    trans_this ← 空字符串    #记录当前时刻的译文
   	fin_segs ← 空字符串     #记录当前时刻翻译过的原文
    for each(p) in punc_found do
        pos_p ← getIndex(i_rmn, p)
        i_rmn ← i_rmn从pos_p+1到末尾的部分
        seg4trans ← i_rmn从开头到pos_p-1的部分
        seg_trans ← translate(seg4trans)    #translate()执行翻译并返回译文
        seg_trans ← post_proc(seg_trans, p)    #post_proc()对字符串进行后处理,主要是去除末尾标点,并追加标点p,因为输入的句子是没有标点的,防止模型随意打不相关的标点
        trans_this ← concat(trans_this, seg_trans)
        fin_segs ← concat(fin_segs , seg4trans)
    history_streams ← append(history_streams, remove_punc(fin_segs))    #append()将元素追加到history_streams中
    C ← concat(C, trans_this)
    T ← append(T, trans_this) 
    end ← getTime()  
    for each(n) in range(sum_len(history_streams)-len(remove_punc(fin_segs)), sum_len(history_streams)) do
    	times[n] = end - start + times_come[step] - times_come[step[n]]   #时间计算方法:当前字w的耗时=(当前时刻的翻译结束时间 - 当前时刻的翻译起始时间)+(当前流到达时间 - w到达时间)
    end for
end for
if notNull(i_rmn) then     #notNull判断字符串是否为非空
    trans_last_rmn ← translate(i_rmn)    #处理剩余的未翻译部分
    T[-1] ← concat(T[-1], trans_last_rmn )     #剩余部分译文追加到最后时刻的译文后面
  	for each(n) in range(sum_len(history_streams), len(times)) do
    	times[n] = end - start + times_come[step] - times_come[step[n]]    #为剩余部分添加时间记录
    end for
end if

for each(n, h) in enumerate(history_streams) do
	D'← append(sub_sent_times, mean(times[sum_len(history_streams[:n]): sum_len(history_streams[:n])+len(h)]))    #mean()用于计算均值
end for
return T, C, D, D'
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值