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

And*_*uib 11 python lambda python-3.x

我正在开展一个深度学习项目,我们使用RNN.我想在数据馈送到网络之前对数据进行编码.输入是阿拉伯语经文,其变音符号在Python中被视为单独的字符.如果跟随它的字符是变音符号,我应编码/表示带有数字的字符,否则我只编码字符.

这样做对数以百万计的诗句,希望能使用lambdamap.但是,我不能一次迭代两个字符,即希望:

map(lambda ch, next_ch: encode(ch + next_ch) if is_diacritic(next_ch) else encode(ch), verse)
Run Code Online (Sandbox Code Playgroud)

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

非阿拉伯人的一个接近的例子,假设你想编码以下文字:

 XXA)L_I!I%M<LLL>MMQ*Q
Run Code Online (Sandbox Code Playgroud)

如果它是一个特殊字符,你希望在将字母与后面的字母连接后对其进行编码,否则只需对字母进行编码.

输出:

['X', 'X', 'A)', 'L_', 'I!', 'I%', 'M<', 'L', 'L', 'L>', 'M', 'M', 'Q*', 'Q']
Run Code Online (Sandbox Code Playgroud)

对于阿拉伯人:

诗节示例:

"قفانبكمنذكرىحبيبومنزلبسقطاللوىبينالدخولفحومل"

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


[更新]

变音符号范围64B HEX或1611 INT开始,到652 HEX或1618 INT结束.

字母621 HEX - 1569 INT63A HEX - 1594 INT641 HEX - 1601 INT64A HEX - 1610 INT

一封信最多可以有一个变音符号.


额外的信息:

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

例如,假设这节经文如下,变音符号是特殊字符:

X+Y_XX+YYYY_
Run Code Online (Sandbox Code Playgroud)

字母表的不同组合是:

['X', 'X+', 'X_', 'Y', 'Y+', 'Y_']  
Run Code Online (Sandbox Code Playgroud)

因此我需要3比特(至少)来表示这些6字符,所以number of bits needed3

请考虑以下编码:

{
'X' : 000,
'X+': 001,
'X_': 010,
'Y':  011,
'Y+': 100,
'Y_': 101,
}
Run Code Online (Sandbox Code Playgroud)

我可以在矩阵中表示示例(二进制表示是垂直的):

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
Run Code Online (Sandbox Code Playgroud)

这就是为什么我希望首先将变音符号与字母组合在一起.


注意:在Python中一次迭代字符串2(或n)个字符,使用Python迭代字符串中的每个字符,但不提供预期的答案.

Mad*_*ist 5

我要在这里用 numpy 把我的帽子扔进戒指。您可以使用以下命令将字符串转换为可用格式

arr = np.array([verse]).view(np.uint32)
Run Code Online (Sandbox Code Playgroud)

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

mask = np.empty(arr.shape, dtype=np.bool)
np.bitwise_and((arr[1:] > lower), (arr[1:] < upper), out=mask[:-1])
mask[-1] = False
Run Code Online (Sandbox Code Playgroud)

在这里,范围[upper, lower]是检查变音符号的一种组合方式。随心所欲地实施实际检查。在这个例子中,我使用了bitwise_andwith的完整形式empty来避免最后一个元素的潜在昂贵的附加。

现在,如果您有一种将代码点编码为数字的数值方法,我相信您可以对其进行矢量化,您可以执行以下操作:

combined = combine(letters=arr[mask], diacritics=arr[1:][mask[:-1]])
Run Code Online (Sandbox Code Playgroud)

要获得剩余的未组合字符,您必须同时删除变音符号和它们绑定的字符。我能想到的最简单的方法是将面具涂抹在右侧并使其无效。同样,我假设您也有一个矢量化方法来对单个字符进行编码:

smeared = mask.copy()
smeared[1:] |= mask[:-1]
single = encode(arr[~smeared])
Run Code Online (Sandbox Code Playgroud)

将结果组合成最终数组在概念上很简单,但需要几个步骤。结果将是np.count_nonzeros(mask)比输入短的元素,因为正在删除变音符号。我们需要按索引量移动所有掩码元素。这是一种方法:

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
Run Code Online (Sandbox Code Playgroud)

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

建议实施

我一直在思考你的问题,并决定尝试一些时间和可能的实现。我的想法是将0x0621-0x063A0x0641-0x064A(26 + 10 = 36 个字母)中的 unicode 字符映射到 a 的低 6 位uint16,并将字符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)
Run Code Online (Sandbox Code Playgroud)

用麻木的话来说:

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
Run Code Online (Sandbox Code Playgroud)

您可以选择进一步编码以稍微缩短表示,但我不建议这样做。这种表示的优点是与经文无关,因此您可以比较不同经文的部分,而且不必担心根据一起编码的经文的数量会得到哪种表示。您甚至可以屏蔽所有代码的最高位以比较原始字符,而无需使用变音符号。

因此,假设您的诗句是这些范围内随机生成的数字的集合,随机生成的变音符号最多跟随一个字母。出于比较目的,我们可以很容易地生成一串长度约为百万的字符串:

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())
Run Code Online (Sandbox Code Playgroud)

该数据具有完全随机分布的字符,任何字符后跟变音符号的几率约为 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
Run Code Online (Sandbox Code Playgroud)

基准

现在让我们%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)]
Run Code Online (Sandbox Code Playgroud)

现在是时间:

%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)
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

我在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()
Run Code Online (Sandbox Code Playgroud)

作为旁注,我在这里所做的工作启发了这个问题:将代码点的 numpy 数组转换为字符串和从字符串转换