Реализация БПФ над конечными полями

Я хотел бы реализовать умножение полиномов с помощью NTT. Я следовал теоретико-числовому преобразованию (целочисленное ДПФ), и кажется работать.

Теперь я хотел бы реализовать умножение полиномов над конечными полями Z_p[x], где p — произвольное простое число.

Изменяет ли что-нибудь то, что коэффициенты теперь ограничены p, по сравнению с прежним неограниченным случаем?

В частности, исходный NTT требовал найти простое число N в качестве рабочего модуля, превышающего (magnitude of largest element of input vector)^2 * (length of input vector) + 1, чтобы результат никогда не переполнялся. Если результат все равно будет ограничен этим p простым числом, то насколько малым может быть модуль? Обратите внимание, что p - 1 не обязательно должен иметь форму (some positive integer) * (length of input vector).

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

# 
# Number-theoretic transform library (Python 2, 3)
# 
# Copyright (c) 2017 Project Nayuki
# All rights reserved. Contact Nayuki for licensing.
# https://www.nayuki.io/page/number-theoretic-transform-integer-dft
#

import itertools, numbers

def find_params_and_transform(invec, minmod):
    check_int(minmod)
    mod = find_modulus(len(invec), minmod)
    root = find_primitive_root(len(invec), mod - 1, mod)
    return (transform(invec, root, mod), root, mod)

def check_int(n):
    if not isinstance(n, numbers.Integral):
        raise TypeError()

def find_modulus(veclen, minimum):
    check_int(veclen)
    check_int(minimum)
    if veclen < 1 or minimum < 1:
        raise ValueError()
    start = (minimum - 1 + veclen - 1) // veclen
    for i in itertools.count(max(start, 1)):
        n = i * veclen + 1
        assert n >= minimum
        if is_prime(n):
            return n

def is_prime(n):
    check_int(n)
    if n <= 1:
        raise ValueError()
    return all((n % i != 0) for i in range(2, sqrt(n) + 1))

def sqrt(n):
    check_int(n)
    if n < 0:
        raise ValueError()
    i = 1
    while i * i <= n:
        i *= 2
    result = 0
    while i > 0:
        if (result + i)**2 <= n:
            result += i
        i //= 2
    return result

def find_primitive_root(degree, totient, mod):
    check_int(degree)
    check_int(totient)
    check_int(mod)
    if not (1 <= degree <= totient < mod):
        raise ValueError()
    if totient % degree != 0:
        raise ValueError()
    gen = find_generator(totient, mod)
    root = pow(gen, totient // degree, mod)
    assert 0 <= root < mod
    return root

def find_generator(totient, mod):
    check_int(totient)
    check_int(mod)
    if not (1 <= totient < mod):
        raise ValueError()
    for i in range(1, mod):
        if is_generator(i, totient, mod):
            return i
    raise ValueError("No generator exists")

def is_generator(val, totient, mod):
    check_int(val)
    check_int(totient)
    check_int(mod)
    if not (0 <= val < mod):
        raise ValueError()
    if not (1 <= totient < mod):
        raise ValueError()
    pf = unique_prime_factors(totient)
    return pow(val, totient, mod) == 1 and all((pow(val, totient // p, mod) != 1) for p in pf)

def unique_prime_factors(n):
    check_int(n)
    if n < 1:
        raise ValueError()
    result = []
    i = 2
    end = sqrt(n)
    while i <= end:
        if n % i == 0:
            n //= i
            result.append(i)
            while n % i == 0:
                n //= i
            end = sqrt(n)
        i += 1
    if n > 1:
        result.append(n)
    return result

def transform(invec, root, mod):
    check_int(root)
    check_int(mod)
    if len(invec) >= mod:
        raise ValueError()
    if not all((0 <= val < mod) for val in invec):
        raise ValueError()
    if not (1 <= root < mod):
        raise ValueError()

    outvec = []
    for i in range(len(invec)):
        temp = 0
        for (j, val) in enumerate(invec):
            temp += val * pow(root, i * j, mod)
            temp %= mod
        outvec.append(temp)
    return outvec

def inverse_transform(invec, root, mod):
    outvec = transform(invec, reciprocal(root, mod), mod)
    scaler = reciprocal(len(invec), mod)
    return [(val * scaler % mod) for val in outvec]

def reciprocal(n, mod):
    check_int(n)
    check_int(mod)
    if not (0 <= n < mod):
        raise ValueError()
    x, y = mod, n
    a, b = 0, 1
    while y != 0:
        a, b = b, a - x // y * b
        x, y = y, x % y
    if x == 1:
        return a % mod
    else:
        raise ValueError("Reciprocal does not exist")

def circular_convolve(vec0, vec1):
    if not (0 < len(vec0) == len(vec1)):
        raise ValueError()
    if any((val < 0) for val in itertools.chain(vec0, vec1)):
        raise ValueError()
    maxval = max(val for val in itertools.chain(vec0, vec1))
    minmod = maxval**2 * len(vec0) + 1
    temp0, root, mod = find_params_and_transform(vec0, minmod)
    temp1 = transform(vec1, root, mod)
    temp2 = [(x * y % mod) for (x, y) in zip(temp0, temp1)]
    return inverse_transform(temp2, root, mod)

vec0 = [24, 12, 28, 8, 0, 0, 0, 0]
vec1 = [4, 26, 29, 23, 0, 0, 0, 0]

print(circular_convolve(vec0, vec1))

def modulo(vec, prime):
    return [x % prime for x in vec]

print(modulo(circular_convolve(vec0, vec1), 31))

Отпечатки:

[96, 672, 1120, 1660, 1296, 876, 184, 0]
[3, 21, 4, 17, 25, 8, 29, 0]

Однако, когда я меняю minmod = maxval**2 * len(vec0) + 1 на minmod = maxval + 1, он перестает работать:

[14, 16, 13, 20, 25, 15, 20, 0]
[14, 16, 13, 20, 25, 15, 20, 0]

Каково наименьшее значение minmod (N в приведенной выше ссылке), чтобы оно работало должным образом?


person minmax    schedule 11.09.2018    source источник
comment
@JohnColeman Есть два простых числа для работы: одно для многочлена, другое для NTT. Поскольку первый не обязательно должен быть в форме, необходимой для NTT, мой вопрос: насколько маленьким может быть второй?   -  person minmax    schedule 11.09.2018
comment
Я думаю, что по вашему p может смело играть роль того, что ссылка вызывает M. Если я правильно понимаю, единственная причина, по которой они тратят время на указание M, заключается в том, что они не хотят на самом деле получать ответы по модулю M, поэтому они хотят, чтобы M было настолько большим, чтобы окончательный вывод мог интерпретировать как обычные целые числа. Вам нужен выходной мод p, поэтому нет опасности, которую нужно было бы избежать при поиске безопасного M.   -  person John Coleman    schedule 11.09.2018
comment
@JohnColeman Меня интересуют N, M (по ссылке). N, по-видимому, должен иметь особую структуру (k*...), которой у p может и не быть.   -  person minmax    schedule 11.09.2018
comment
Форма N кажется важной. Кажется, что это должно быть похоже на вид kn+1, где n - количество элементов. Наименьшее такое простое число N больше p должно быть в порядке.   -  person John Coleman    schedule 11.09.2018
comment
@Spektre Когда я пробую, что ваш p ​​является простым, что p mod n == 1 и p›max, он перестает работать. Но может я неправильно понял?   -  person minmax    schedule 11.09.2018
comment
@Spektre Когда я прерываюсь на p>maxval + 1, вместо p>maxval**2 * len(vec0) + 1 печатается [14, 16, 13, 20, 25, 15, 20, 0] вместо [3, 21, 4, 17, 25, 8, 29, 0 ]. Итак, с одной стороны, p простое число, p mod n == 1 и p›max кажется недостаточным, с другой стороны, во всех текстах, которые я видел, это упоминается как критерий остановки.   -  person minmax    schedule 11.09.2018
comment
@minmax Я думаю, вы идете назад или слишком много думаете об этом. Почему бы не использовать наибольшее такое простое число, которое вписывается в ваше слово данных? Одно это может избавиться от модуля (заменить его одинарным вычитанием), а также некоторые такие простые числа могут иметь много сгруппированных нулей в двоичном виде для дополнительной оптимизации битовыми операциями. Также в качестве максимально возможного значения используется такое, если переполнение возможно, его нельзя избежать (если только не используется слово большего размера), так что не нужно ломать голову из-за этого. Использование минимального p всегда приведет к проблемам (если не используется произвольная точность)   -  person Spektre    schedule 12.09.2018
comment
@Spektre Потому что многочлены уже уменьшены по модулю некоторого несвязанного простого числа. Я не выбираю это простое число и все же хотел бы умножить их как можно эффективнее. Поэтому мне интересно, есть ли лучшая граница, чем p>maxval**2 * len(vec0) + 1? А еще, почему p>maxval + 1 не работает?   -  person minmax    schedule 12.09.2018
comment
@Spektre Конечно, это верно и для алгоритма школьных учебников, а не только для ДПФ. Мой вопрос в том, изменится ли что-нибудь при работе над конечным полем.   -  person minmax    schedule 12.09.2018
comment
@minmax Я преобразовал комментарии в ответ с дополнительной информацией ...   -  person Spektre    schedule 15.09.2018
comment
N должно иметь форму kn+1, потому что нам нужно найти подходящее ω. ω — корень n-й степени из единицы, который нам нужен для преобразования длины n.   -  person Nayuki    schedule 26.09.2018


Ответы (1)


Если введенные вами n целые числа привязаны к некоторому простому q (любое mod q, а не только простое число, будет одинаковым), вы можете использовать его как max value +1, но будьте осторожны, вы не можете использовать его как простое p для NTT потому что NTT prime p имеет особые свойства. Все они здесь:

таким образом, наше максимальное значение каждого входа равно q-1, но во время вычисления вашей задачи (свертка по 2 результатам NTT) величина результатов первого слоя может возрасти до n.(q-1), но поскольку мы выполняем свертку для них, входная величина конечного iNTT вырастет до:

m = n.((q-1)^2)

Если вы выполняете другие операции с NTT, уравнение m может измениться.

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

p mod n == 1
p > m

и существует 1 <= r,L < p таких, что:

p mod (L-1) = 0
r^(L*i) mod p == 1 // i = { 0,n }
r^(L*i) mod p != 1 // i = { 1,2,3, ... n-1 }

Если все это выполняется, то p является корнем n-й степени из единицы и может использоваться для NTT. Чтобы найти такое простое число, а также r,L, посмотрите ссылку выше (есть код C++, который находит такое).

Например, во время умножения строк мы берем 2 строки, делаем над ними NTT, затем сворачиваем результат и iNTT возвращаем результат (то есть сумму обоих входных размеров). Так, например:

                                99999999999999999999999999999999
                               *99999999999999999999999999999999
----------------------------------------------------------------
9999999999999999999999999999999800000000000000000000000000000001

q = 10 и оба операнда равны 9^32, поэтому n=32, следовательно, m = 9*9*32 = 2592, а найденное простое число равно p = 2689. Как видите, результат совпадает, поэтому переполнения не происходит. Однако, если я использую любое меньшее простое число, которое по-прежнему соответствует всем остальным условиям, результат не будет совпадать. Я использовал это специально, чтобы максимально растянуть значения NTT (все значения q-1, а размеры равны одной и той же степени 2)

Если ваш NTT быстрый и n не является степенью двойки, вам необходимо дополнить нулями до ближайшей большей или равной степени 2 размера для каждого NTT. Но это не должно влиять на значение m, так как заполнение нулями не должно увеличивать величину значений. Мое тестирование доказывает это, поэтому для свертки вы можете использовать:

m = (n1+n2).((q-1)^2)/2

где n1,n2 — размеры необработанных входных данных до нуля.

Для получения дополнительной информации о реализации NTT вы можете ознакомиться с моей статьей на C++ (значительно оптимизированной):

Итак, отвечая на ваши вопросы:

  1. да, вы можете воспользоваться тем, что ввод равен mod q, но вы не можете использовать q как p !!!

  2. Вы можете использовать minmod = n * (maxval + 1) только для одного NTT (или первого уровня NTT), но, поскольку вы связываете их в цепочку со сверткой во время использования NTT, вы не можете использовать это для финальной стадии INTT !!!

Однако, как я упоминал в комментариях, проще всего использовать максимально возможное p, которое соответствует типу данных, который вы используете, и может использоваться для всех поддерживаемых размеров входных данных 2.

Что в основном делает ваш вопрос неактуальным. Единственный случай, о котором я могу думать, когда это невозможно/нежелательно, - это числа с произвольной точностью, где нет максимального предела. Существует много проблем с производительностью, связанных с переменной p, так как поиск p очень медленный (может быть даже медленнее, чем сам NTT), а также переменная p отключает многие оптимизации производительности модульной арифметики, необходимые для создания < strong>NTT очень медленный.

person Spektre    schedule 15.09.2018