Кодирование арабских букв с их диакритическими знаками (если есть) - PullRequest
0 голосов
/ 23 января 2019

Я работаю над проектом глубокого обучения, в котором мы используем RNN. Я хочу закодировать данные, прежде чем они будут переданы в сеть. Входные данные - арабские стихи, в которых есть диакритические знаки, которые в Python рассматриваются как отдельные символы. Я должен кодировать / представлять символ с символом, следующим за ним, с номером , если символ, следующий за ним, является диакритическим, иначе я только кодирую символ .

Делая это для миллионов стихов, надеялся использовать lambda с map. Тем не менее, я не могу перебирать по два символа за раз, то есть надеялся:

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

Мое намерение, стоящее за этим вопросом, - найти самый быстрый способ выполнить вышеизложенное. Никаких ограничений на лямбда-функции нет, но ответы for loop - это не то, что я ищу.

Близкий пример для неарабцев, предположим, что вы хотите закодировать следующий текст:

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

Вы хотите закодировать букву после конкатенации с буквой, следующей за ней , если это специальный символ , иначе просто закодируйте только букву.

Выход:

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

Для арабов:

стих пример:

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

Диакритические знаки - это маленькие символы над буквой (т. Е., ّ, ْ)


[Обновить]

Диапазон диакритических знаков начинается с 64B HEX или 1611 INT и заканчивается на 652 HEX или 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 равно 3

Рассмотрим следующие кодировки:

{
'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

Именно поэтому я хочу сначала объединить диакритические знаки с буквами.


Примечание: Итерация по строке 2 (или n) символа за раз в Python и Итерация каждого символа в строке с использованием Python не дает ожидаемого ответа .

Ответы [ 3 ]

0 голосов
/ 23 января 2019

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), ...), вы действительно могли бы также использовать понимание списка, но я все равно проголосовал бы за цикл 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 и лямбду, но вам также нужно сначала filter, с другой лямбдой, что делает все это на несколько порядков уродливее и труднее для чтения.

0 голосов
/ 23 января 2019

Я собираюсь бросить свою шляпу на ринг с обалденной здесь. Вы можете преобразовать строку в пригодный для использования формат с помощью

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 , 0x0641-0x064A (26 + 10 = 36 букв) в младшие 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)

В натуральном выражении:

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 посмотрим, как все пойдет. Во-первых, вот другие реализации. Я преобразовываю все в массив или список целых чисел для честного сравнения. Я также внес небольшие изменения, чтобы функции возвращали списки с одинаковыми величинами для проверки точности:

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()

В качестве примечания, работа, которую я здесь сделал, вдохновила этот вопрос: Преобразование массива кодовых точек в и из строк

0 голосов
/ 23 января 2019

Вы не читаете два символа одновременно, и даже если бы вы были, 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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...