平行语料数据对齐,如果是机器翻译的平行语料,目前有很多资源,而且语料大多已经预处理好了,可以直接拿来用。就算没有对齐,也有一些工具可以使用。比如Tmxmall, 然而他是针对 翻译句的对齐工具,不支持对齐同一种语言。
如果你做文本复述(或者文本改写任务) 亦或是文本风格迁移任务,就可能需要 同一种语言的平行语料了。这样的话不免会遇到对齐语料的问题。具体地说:
问题:当你有两大段,同一种语言的平行语料 A、B,太长了,需要切分句子才能输入网络。这时,如何切分才能使 语料对应上呢。如果直接按 “。”切分,句子数量一般是对不上的。
这时候不难想到,先用句子级别的符号(或直接用逗号级别的符号)切分,同时遍历切分后的A、B。
遍历的索引分别为 IndexA IndexB
同时建立空对的语句对 数组 C,当前索引为 Index
不妨把简化成:A中第IndexA个句子,和B中第IndexB第去向:
- IndexA 是要 和 IndexB 组成一个新到语句对;并添加到 C的末尾,同时 Index += 1、IndexA += 1 、IndexB += 1
- IndexA 合并到 IndexA - 1, 修改C的末尾语句对。 IndexA += 1
- IndexA 合并 IndexA + 1 和 IndexB 组成新语句对,添加到C的末尾。 Index += 1、IndexA += 2
- IndexB 和 IndexB-1合并。IndexB += 1。IndexB += 1
- IndexB 合并IndexB+1 和 IndexA 组成新的语句对。 添加到C的末尾。 Index += 1, IndexB += 2
具体选择哪种情况,要看每种情况的得分。(注意这里没有考虑所有情况,也没有对在合并时 考虑合并后的长度)
如何决定得分,要看具体任务,对于文本复述任务,平行语料是十分相似的句子。这个时候直接用类似jaccard相似度就行了。
完整代码:https://github.com/XuhXie/NLPTools/tree/main/Dataprocess
1. 简单的Jaccard实现:
import re
import jieba
from collections import defaultdict
def jaccard(x, y):
x = set(x)
y = set(y)
return len(x & y) / len(x|y)
def countDic(x):
d = defaultdict(int)
for i in x:
d[i] = d[i] + 1
return d
def jaccardRepeated(a, b):
longDic = a if len(a) >= len(b) else b
shortDic = a if len(a) < len(b) else b
totalLen = len(a) + len(b)
longLen, shortLen = len(longDic), len(shortDic)
if totalLen == 0: return 1
longDic = countDic(longDic)
shortDic = countDic(shortDic)
num = 0
for key in shortDic.keys():
num = num + min(shortDic[key], longDic[key])
# 这里如果用总长度当分母会有问题:相似度永远不会到1
return num/(longLen + shortLen - num)
def jacSeten(x, y, repeat = True, tokenMode = 'jieba'):
if tokenMode == 'jieba':
x = list(jieba.cut(x))
y = list(jieba.cut(y))
else:
x = list(x)
y = list(y)
if (repeat): return jaccardRepeated(x, y)
return jaccard(x, y)
2. 上述算法思路实现 (有待优化 能用就行~)
## 根据相似度 合并前后句,使平行语料对应上; 还需要优化
## Needed to be improved
def merge(sen1, sen2, tokenMode=None, maxlen=256):
res1 = []
res2 = []
res1.append(sen1[0])
res2.append(sen2[0])
index1 = 1
index2 = 1
while ((index1 < len(sen1)) and (index2 < len(sen2))):
sim = [0, 0, 0, 0, 0]
# sim1_2, sim11_2, sim1_22, sim_11_2, sim_1_22 = 0, 0, 0, 0, 0
sim[0] = jacSeten(sen1[index1], sen2[index2], tokenMode=tokenMode)
sim[1] = jacSeten(res1[-1] + sen1[index1], res2[-1], tokenMode=tokenMode)
sim[2] = jacSeten(res1[-1], res2[-1] + sen2[index2], tokenMode=tokenMode)
if (index1 + 1 < len(sen1)):
sim[3] = jacSeten(sen1[index1] + sen1[index1 + 1], sen2[index2], tokenMode=tokenMode)
if (index2 + 1 < len(sen2)):
sim[4] = jacSeten(sen1[index1], sen2[index2] + sen2[index2 + 1], tokenMode=tokenMode)
maxIndex = sim.index(max(sim))
## and len(res1[-1]) + len(sen1[index1]) <= maxlen
if (maxIndex == 1 and len(res1[-1]) + len(sen1[index1]) <= maxlen):
res1[-1] = res1[-1] + sen1[index1]
index1 += 1
## and len(res2[-1]) + len(sen2[index2]) <= maxlen
elif (maxIndex == 2 and len(res2[-1]) + len(sen2[index2]) <= maxlen):
res2[-1] = res2[-1] + sen2[index2]
index2 += 1
## and (len(sen1[index1]) + len(sen1[index1+1])) <= maxlen
elif (maxIndex == 3 and (len(sen1[index1]) + len(sen1[index1 + 1])) <= maxlen):
res1.append(sen1[index1] + sen1[index1 + 1])
res2.append(sen2[index2])
index1 += 2
index2 += 1
## and (len(sen2[index2]) + len(sen2[index2+1])) <= maxlen
elif (maxIndex == 4 and (len(sen2[index2]) + len(sen2[index2 + 1])) <= maxlen):
res1.append(sen1[index1])
res2.append(sen2[index2] + sen2[index2 + 1])
index1 += 1
index2 += 2
else:
res1.append(sen1[index1])
res2.append(sen2[index2])
index1 += 1
index2 += 1
if (index1 < len(sen1)):
res1[-1] = res1[-1] + ''.join(sen1[index1:])
if (index2 < len(sen2)):
res2[-1] = res2[-1] + ''.join(sen2[index2:])
assert len(res1) == len(res2)
return res1, res2
事实上,如果想得到完整的句子,的语句对。一般一开始会用“。” 先切分。那这时,在 对齐 的时候不考虑长度,各种合并句子的话,就会出现超级长的句子。这时候,可以对超长对句子再进行‘,’级别对切分,然后再进行一遍上述对对齐过程。(不过最后还是有一些比较长对语句对,这时候就直接扔掉吧)
这一过程在上面提供对代码链接里已经实现,这里不过多赘述。
为什么不在对齐过程中就限制最大长度呢?我尝试过,效果一般。看具体情况了。
这个方法是前几天赶实验临时想的方法,而且刚转的NLP 有点小白。有更好方法或者改进思路的,欢迎讨论,哦 是万分希望能与我交流~