用变音符号编码阿拉伯字母(如果存在)

2024-05-17 02:55:19 发布

您现在位置:Python中文网/ 问答频道 /正文

我正在做一个深度学习项目,我们使用RNN。我想在数据传输到网络之前对其进行编码。输入是阿拉伯语的韵文,在Python中,这些韵文被视为单独的字符。如果后面的字符是变音符号,我应该用数字来编码/表示字符,否则我只编码字符。在

这样做的数百万诗句,是希望使用lambda和{}。但是,我不能一次迭代两个字符,即希望:

map(lambda ch, next_ch: encode(ch + next_ch) if is_diacritic(next_ch) else encode(ch), verse)

我在这个问题背后的意图是找到实现上述目标的最快方法。对lambda函数没有限制,但是for循环的答案不是我想要的。在

对于非阿拉伯人来说,这是一个很好的例子,假设您要对以下文本进行编码:

^{pr2}$

如果字母是一个特殊字符,则需要在将字母与其后面的字母串联后对其进行编码,否则只对字母进行编码。在

输出:

['X', 'X', 'A)', 'L_', 'I!', 'I%', 'M<', 'L', 'L', 'L>', 'M', 'M', 'Q*', 'Q']

对于阿拉伯人:

诗歌示例:

另一方面则是“ق。”الدّڒولفحَومل”

变音符号是字母上方的小符号(即ّ,ْ)


[更新]

Range of diacritics64B HEX or 1611 INT开始,到652 HEX or 1618 INT结束。在

字母621 HEX - 1569 INT63A HEX - 1594 INT,从641 HEX - 1601 INT到{a7}

一个字母最多只能有一个音调符号。


额外信息:

与我所做的类似的编码方法是将韵文的二进制形式表示为具有形状(number of bits needed, number of characters in a verse)的矩阵。在我们将每个字母与其变音符号(如果存在)合并后,会计算位数和字符数。在

例如,假设诗句如下,变音符号是特殊字符:

X+Y_XX+YYYY_

字母表的不同组合是:

['X', 'X+', 'X_', 'Y', 'Y+', 'Y_']  

因此,我需要3(至少)来表示这些6字符,因此number of bits needed是{}

考虑以下编码:

{
'X' : 000,
'X+': 001,
'X_': 010,
'Y':  011,
'Y+': 100,
'Y_': 101,
}

我把矩阵中的例子表示为(二进制表示是垂直的)

X+     Y_    X    X+    Y    Y    Y    Y_
0      1     0    0     0    0    0    1
0      0     0    0     1    1    1    0
1      1     0    1     1    1    1    1

这就是为什么我要先把发音和字母结合起来。在


注意:Iterate over a string 2 (or n) characters at a time in Python和{a9}没有给出预期的答案。


Tags: oroflambdanumber编码诗句字母符号
3条回答

我要把我的帽子扔到拳击台上和纽比在一起。可以使用将字符串转换为可用的格式

arr = np.array([verse]).view(np.uint32)

您可以屏蔽以下字符变音符的位置:

^{pr2}$

在这里,范围[upper, lower]是检查音调符号的一种临时方法。按照你喜欢的方式进行实际检查。在这个例子中,我使用了^{}^{}的完整形式,以避免对最后一个元素进行可能昂贵的附加。

现在,如果你有一种将代码点编码为一个数字的数值方法,我相信你可以向量化,你可以做如下事情:

combined = combine(letters=arr[mask], diacritics=arr[1:][mask[:-1]])

要获取剩余的未组合字符,您必须同时删除音调符号和它们绑定到的字符。我能想到的最简单的方法就是把面具涂在右边,然后否定它。同样,我假设您有一个矢量化方法来编码单个字符:

smeared = mask.copy()
smeared[1:] |= mask[:-1]
single = encode(arr[~smeared])

将结果组合成一个最终数组在概念上很简单,但需要几个步骤。结果是^{}个元素比输入短,因为音调符号被删除了。我们需要根据它们的索引量来移动所有的掩码元素。有一种方法可以做到:

ind = np.flatnonzero(mask)
nnz = ind.size
ind -= np.arange(nnz)

output = np.empty(arr.size - nnz, dtype='U1')
output[ind] = combined

# mask of unmodified elements
out_mask = np.ones(output.size, dtype=np.bool)
out_mask[ind] = False
output[out_mask] = single

我建议numpy的原因是它应该能够在几秒钟内处理几百万个字符。将输出作为字符串返回应该很简单。

建议实施

我一直在考虑你的问题,并决定考虑一些时间安排和可能的实现。我的想法是将0x0621-0x063A0x0641-0x064A(26+10=36个字母)中的unicode字符映射到auint16的低6位,并将0x064B-0x0652(8个音调符号)映射到下一个更高的3位,假设这些实际上是您需要的唯一音调符号:

def encode_py(char):
    char = ord(char) - 0x0621
    if char >= 0x20:
        char -= 5
    return char

def combine_py(char, diacritic):
    return encode_py(char) | ((ord(diacritic) - 0x064A) << 6)

用新潮的术语来说:

def encode_numpy(chars):
    chars = chars - 0x0621
    return np.subtract(chars, 5, where=chars > 0x20, out=chars)

def combine_numpy(chars, diacritics):
    chars = encode_numpy(chars)
    chars |= (diacritics - 0x064A) << 6
    return chars

您可以选择进一步编码来稍微缩短表示,但我不建议这样做。这种表示法的优点是与韵文无关,因此您可以比较不同诗句的部分,也不必担心根据编码在一起的诗句数,您将得到哪种表示形式。你甚至可以屏蔽掉所有代码的前几位来比较原始字符,而不使用音调符号。

所以我们假设你的诗句是在这些范围内随机产生的数字的集合,音调符号随机生成,最多跟在一个字母后面。为了进行比较,我们可以很容易地生成一个长度约为百万的字符串:

import random

random.seed(0xB00B5)

alphabet = list(range(0x0621, 0x063B)) + list(range(0x0641, 0x064B))
diactitics = list(range(0x064B, 0x0653))

alphabet = [chr(x) for x in alphabet]
diactitics = [chr(x) for x in diactitics]

def sample(n=1000000, d=0.25):
    while n:
        yield random.choice(alphabet)
        n -= 1
        if n and random.random() < d:
            yield random.choice(diactitics)
            n -= 1

data = ''.join(sample())

这些数据完全是随机分布的字符,大约25%的概率是任何字符后面跟一个变音符号。它只需要几秒钟就可以在我的笔记本电脑上生成。

numpy转换如下所示:

def convert_numpy(verse):
    arr = np.array([verse]).view(np.uint32)
    mask = np.empty(arr.shape, dtype=np.bool)
    mask[:-1] = (arr[1:] >= 0x064B)
    mask[-1] = False

    combined = combine_numpy(chars=arr[mask], diacritics=arr[1:][mask[:-1]])

    smeared = mask.copy()
    smeared[1:] |= mask[:-1]
    single = encode_numpy(arr[~smeared])

    ind = np.flatnonzero(mask)
    nnz = ind.size
    ind -= np.arange(nnz)

    output = np.empty(arr.size - nnz, dtype=np.uint16)
    output[ind] = combined

    # mask of unmodified elements
    out_mask = np.ones(output.size, dtype=np.bool)
    out_mask[ind] = False
    output[out_mask] = single

    return output

基准

现在让我们%timeit来看看它是怎么回事。首先,这里是其他实现。我将所有内容转换为numpy数组或整数列表以进行公平比较。我还做了一些小修改,使函数返回相同数量的列表,以验证准确性:

from itertools import tee, zip_longest
from functools import reduce

def is_diacritic(c):
    return ord(c) >= 0x064B

def pairwise(iterable, fillvalue):
    """ Slightly modified itertools pairwise recipe
    s -> (s0,s1), (s1,s2), (s2, s3), ... 
    """
    a, b = tee(iterable)
    next(b, None)
    return zip_longest(a, b, fillvalue=fillvalue)

def combine_py2(char, diacritic):
    return char | ((ord(diacritic) - 0x064A) << 6)

def convert_FHTMitchell(verse):
    def convert(verse):
        was_diacritic = False  # variable to keep track of diacritics   stops us checking same character twice

        # fillvalue will not be encoded but ensures last char is read
        for this_char, next_char in pairwise(verse, fillvalue='-'):
            if was_diacritic:  # last next_char (so this_char) is diacritic
                was_diacritic = False
            elif is_diacritic(next_char):
                yield combine_py(this_char, next_char)
                was_diacritic = True
            else:
                yield encode_py(this_char)

    return list(convert(verse))

def convert_tobias_k_1(verse):
    return reduce(lambda lst, x: lst + [encode_py(x)] if not is_diacritic(x) else lst[:-1] + [combine_py2(lst[-1], x)], verse, [])

def convert_tobias_k_2(verse):
    res = []
    for x in verse:
        if not is_diacritic(x):
            res.append(encode_py(x))
        else:
            res[-1] = combine_py2(res[-1], x)
    return res

def convert_tobias_k_3(verse):
    return [combine_py(x, y) if y and is_diacritic(y) else encode_py(x) for x, y in zip_longest(verse, verse[1:], fillvalue="") if not is_diacritic(x)]

现在是时间安排:

%timeit result_FHTMitchell = convert_FHTMitchell(data)
338 ms ± 5.09 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit result_tobias_k_1 = convert_tobias_k_1(data)
Aborted, took > 5min to run. Appears to scale quadratically with input size: not OK!

%timeit result_tobias_k_2 = convert_tobias_k_2(data)
357 ms ± 4.94 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit result_tobias_k_3 = convert_tobias_k_3(data)
466 ms ± 4.62 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit result_numpy = convert_numpy(data)
30.2 µs ± 162 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

对结果数组/列表的比较表明它们也是相等的:

np.array_equal(result_FHTMitchell, result_tobias_k_2)  # True
np.array_equal(result_tobias_k_2, result_tobias_k_3)   # True
np.array_equal(result_tobias_k_3, result_numpy)        # True

我在这里使用array_equal,因为它执行所有必要的类型转换来验证实际数据。

所以这个故事的寓意是,有很多方法可以做到这一点,解析几百万个字符本身不应该过于昂贵,直到你开始交叉引用和其他真正耗时的任务。最重要的是不要在列表中使用reduce,因为你将比你需要的更多地重新分配。即使是一个简单的for循环对你的目的来说很好。尽管numpy比其他实现快十倍,但它并没有带来巨大的优势。

解码

为了完整起见,下面是一个用于解码结果的函数:

def decode(arr):
    mask = (arr > 0x3F)
    nnz = np.count_nonzero(mask)
    ind = np.flatnonzero(mask) + np.arange(nnz)

    diacritics = (arr[mask] >> 6) + 41
    characters = (arr & 0x3F)
    characters[characters >= 27] += 5

    output = np.empty(arr.size + nnz, dtype='U1').view(np.uint32)
    output[ind] = characters[mask]
    output[ind + 1] = diacritics

    output_mask = np.zeros(output.size, dtype=np.bool)
    output_mask[ind] = output_mask[ind + 1] = True
    output[~output_mask] = characters[~mask]

    output += 0x0621

    return output.base.view(f'U{output.size}').item()

作为旁注,我在这里所做的工作启发了这个问题:Converting numpy arrays of code points to and from strings

map似乎不是该作业的正确工具。您不希望将字符映射到其他字符,而是将它们组合在一起。相反,您可以尝试reduce(或者python3中的functools.reduce)。在这里,我使用isalpha来测试它是什么类型的字符;您可能需要其他的东西。

>>> is_diacritic = lambda x: not x.isalpha()
>>> verse = "XXA)L_I!I%M<LLL>MMQ*Q"
>>> reduce(lambda lst, x: lst + [x] if not is_diacritic(x) else lst[:-1] + [lst[-1]+x], verse, [])
['X', 'X', 'A)', 'L_', 'I!', 'I%', 'M<', 'L', 'L', 'L>', 'M', 'M', 'Q*', 'Q']

然而,这几乎不可读,而且还创建了大量中间列表。最好使用一个无聊的旧for循环,即使您明确要求其他东西:

^{pr2}$

通过迭代连续字符对,例如使用zip(verse, verse[1:])(即(1,2), (2,3),...,而不是(1,2), (3,4), ...),您确实可以使用列表理解,但我还是会投票给for循环的可读性。

>>> [x + y if is_diacritic(y) else x
...  for x, y in zip_longest(verse, verse[1:], fillvalue="")
...  if not is_diacritic(x)]
...
['X', 'X', 'A)', 'L_', 'I!', 'I%', 'M<', 'L', 'L', 'L>', 'M', 'M', 'Q*', 'Q']

你甚至可以使用map和lambda来做同样的事情,但是你还需要先使用另一个lambda来filter,这样整个事情的数量级就会变得更难看,更难阅读。

您不会一次读取两个字符,即使是这样,map也不会将它们拆分为lambda的两个参数。

from itertools import tee, zip_longest

def pairwise(iterable, fillvalue):
    """ Slightly modified itertools pairwise recipe
    s -> (s0,s1), (s1,s2), (s2, s3), ... 
    """
    a, b = tee(iterable)
    next(b, None)
    return zip_longest(a, b, fillvalue=fillvalue)

def encode_arabic(verse):

    was_diacritic = False  # variable to keep track of diacritics   stops us checking same character twice

    # fillvalue will not be encoded but ensures last char is read
    for this_char, next_char in pairwise(verse, fillvalue='-'):

        if was_diacritic:  # last next_char (so this_char) is diacritic
            was_diacritic = False

        elif is_diacritic(next_char):
            yield encode(this_char + next_char)
            was_diacritic = True

        else:
            yield this_char

encode_arabic(verse)  # returns a generator like map   wrap in list / string.join / whatever

相关问题 更多 >