代码之家  ›  专栏  ›  技术社区  ›  ndrwnaguib Nikkolai Fernandez

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

  •  10
  • ndrwnaguib Nikkolai Fernandez  · 技术社区  · 6 年前

    我正在进行一个深度学习项目,我们在其中使用RNN。我想在数据输入网络之前对其进行编码。输入的是阿拉伯诗句,其中的音调符号在python中被视为单独的字符。我应该用跟在后面的字符和数字来编码/表示字符 如果后面的字符是音调符号,否则我只对字符进行编码。 .

    为了数百万诗句,我希望 lambda 具有 map . 但是,我不能一次使用两个字符进行迭代,即希望:

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

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

    非阿拉伯国家的一个很好的例子,假设您希望对以下文本进行编码:

     XXA)L_I!I%M<LLL>MMQ*Q
    

    您希望在将字母与后面的字母连接后对其进行编码 如果它是一个特殊的角色 ,否则只对字母进行编码。

    输出:

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

    阿拉伯人:

    诗句范例:

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

    发音符号是字母上方的小符号(即“符号”)。


    [更新]

    Range of diacritics 开始于 64B HEX or 1611 INT 结束于 652 HEX or 1618 INT .

    和信件 621 HEX - 1569 INT 63A HEX - 1594 INT 641 HEX - 1601 INT 64A HEX - 1610 INT

    一封信最多只能有一个音调符号。


    额外信息:

    我正在做的一个类似的编码方法是将诗歌的二进制形式表示为一个有形状的矩阵。 (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 Iterating each character in a string using Python 不要给出预期的答案。

    3 回复  |  直到 6 年前
        1
  •  3
  •   Mad Physicist    6 年前

    我要把我的帽子扔到戒指里,让我麻木。可以将字符串转换为可使用的格式

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

    您可以屏蔽以下字符为音调符号的位置:

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

    这里,范围 [upper, lower] 是检查音调符号的一种组合方式。不管您喜欢什么,都要执行实际检查。在这个例子中,我使用了 bitwise_and 具有 empty 以避免最后一个元素的附加成本可能很高。

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

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

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

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

    将结果组合成最终数组在概念上很简单,但需要几个步骤。结果是 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
    

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

    建议实施

    我一直在思考您的问题,并决定考虑一些时间安排和可能的实现。我的想法是将Unicode字符映射到 0x0621-0x063A , 0x064 1-0x064 (26+10=36个字母)输入 uint16 以及角色 0x064 B-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比其他实现快10倍,但它并没有提供巨大的优势。

    译码

    为了完整起见,这里有一个函数来解码您的结果:

    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

        2
  •  3
  •   tobias_k    6 年前

    map 似乎不是合适的工作工具。您不希望将字符映射到其他字符,而是将它们组合在一起。相反,你可以尝试 reduce (或) functools.reduce 在Python 3中)。在这里,我用 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 循环,即使您明确要求其他内容:

    res = []
    for x in verse:
        if not is_diacritic(x):
            res.append(x)
        else:
            res[-1] += x
    

    通过迭代连续字符对,例如使用 zip(verse, verse[1:]) (即 (1,2), (2,3),... 不是 (1,2), (3,4), ... ,你也可以使用列表理解,但我仍然会投票支持 对于 可读性循环。

    >>> [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']
    

    能够 甚至用同样的方法 地图 还有lambda,但你也需要 filter 首先,使用另一个lambda,使整个事物的数量级变得更丑,更难阅读。

        3
  •  2
  •   FHTMitchell    6 年前

    你一次不会读两个字,即使你读了, 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