Чи може нейронна мережа розпізнавати праймери?


26

Фон

Визнання первинності здається поганим пристосуванням для (штучних) нейронних мереж. Однак теорема про універсальне наближення стверджує, що нейронні мережі можуть наближати будь-яку безперервну функцію, тому, зокрема, слід мати можливість представляти будь-яку бажану функцію, що підтримується кінцево. Тож спробуємо розпізнати всі прими серед перших мільйонів чисел.

Точніше, оскільки це веб-сайт з програмування, давайте до 2 ^ 20 = 1,048,576. Кількість простих розмірів нижче цього порогу становить 82,025 або приблизно 8%.

Виклик

Наскільки мало нейронної мережі можна визначити, що правильно класифікує всі 20-бітні цілі числа як прості чи непрості?

Для цілей цієї задачі розмір нейронної мережі - це загальна кількість ваг і ухилів, необхідних для її представлення.

Деталі

Мета - мінімізувати розмір єдиної явної нейронної мережі.

Вхід до вашої мережі буде вектором довжиною 20, що містить окремі біти цілого числа, представлені або 0s і 1s, або альтернативно -1s і +s. Упорядкування цих може бути найзначнішим бітом першим або найменш значущим бітом першим.

Вихід вашої мережі повинен бути єдиним числом, таким чином, що над деяким відсіканням вхід розпізнається як простий, а нижче одного і того ж відсікання вхід розпізнається як не основний. Наприклад, позитивний може означати простий (а негативний не простий), або, альтернативно, більше 0,5 може означати простий (і менше 0,5 не простий).

Мережа повинна бути на 100% точною на всіх 2 ^ 20 = 1,048,576 можливих входів. Як було сказано вище, зауважте, що в цьому діапазоні є 82 025 прайменів. (Звідси випливає, що виведення завжди "не просто" було б 92% точним.)

З точки зору стандартної термінології нейронної мережі, це, ймовірно, можна назвати надмірним . Іншими словами, ваша мета полягає в тому, щоб ідеально подолати праймери. Інші слова, які можна використати, - це те, що «навчальний набір» і «тестовий набір» однакові.

Цей виклик не враховує кількість параметрів, що «навчаються» або «навчаються». Дійсно, ваша мережа, ймовірно, містить жорсткі ваги, і наведений нижче приклад є повністю жорстким. Натомість всі ваги та ухили вважаються параметрами та підраховуються.

Довжина коду, необхідного для тренування або генерування вашої нейронної мережі, не відповідає вашому результату, однак розміщення відповідного коду, безумовно, цінується.

Базова лінія

В якості базової лінії можна "запам'ятати" всі 82,025 прайметів з загальною вагою та зміщеннями 1804,551 .

Зауважте, що цей код, який випливає, включає багато речей: робочий приклад, робочий тестовий код, робоче визначення нейронної мережі з використанням відомої бібліотеки нейронної мережі, "жорстко кодовану" (або, принаймні, не "навчену") нейронну мережу, і робоче вимірювання балів.

import numpy as np

bits = 20

from keras.models import Sequential
from keras.layers import Dense

from sympy import isprime

# Hardcode some weights
weights = []
biases  = []
for n in xrange(1<<bits):
    if not isprime(n):
        continue
    bit_list = [(n / (1 << i))%2 for i in xrange(bits)]
    weight = [2*bit - 1 for bit in bit_list]
    bias   = - (sum(bit_list) - 1)
    weights.append(weight)
    biases .append(bias)
nprimes = len(biases)
weights1 = np.transpose(np.array(weights))
biases1  = np.array(biases )
weights2 = np.full( (nprimes,1), 1 )
biases2  = np.array( [0] )

model = Sequential()
model.add(Dense(units=nprimes, activation='relu', input_dim=bits, weights=[weights1, biases1]))
model.add(Dense(units=1, activation='relu', weights=[weights2, biases2]))
print "Total weights and biases: {}".format( np.size(weights1) + np.size(weights2) + np.size(biases1) + np.size(biases2) )

# Evaluate performance
x = []
y = []
for n in xrange(1<<bits):
    row = [(n / (1 << i))%2 for i in xrange(bits)]
    x.append( row )
    col = 0
    if isprime(n):
        col = 1
    y.append( col )
x = np.array(x)
y = np.array(y)

model.compile(loss='binary_crossentropy', optimizer='sgd', metrics=['accuracy'])

loss, accuracy = model.evaluate(x, y, batch_size=256)
if accuracy == 1.0:
    print "Perfect fit."
else:
    print "Made at least one mistake."

Що таке нейромережа?

Для цілей цього виклику ми можемо записати вузьке, але точне визначення (штучної) нейронної мережі. Для деякого зовнішнього читання я пропоную Вікіпедію про штучну нейронну мережу , нейронну мережу подачі , багатошаровий перцептрон і функцію активації .

Попередженням нейронна мережа являє собою сукупність шарів нейронів. Кількість нейронів на шарі змінюється, з 20 нейронів у вхідному шарі, деяка кількість нейронів в одному або декількох прихованих шарах та 1 нейрон у вихідному шарі. (Повинно бути принаймні один прихований шар, тому що прайми та непримітки не є лінійно відокремленими відповідно до їх бітових шаблонів.) У наведеному вище прикладі розміри шарів [20, 82025, 1].

Значення вхідних нейронів визначаються на вході. Як описано вище, це будуть або 0s і 1s, відповідні бітам числа між 0 і 2 ^ 20, або -1s і + 1s аналогічно.

Значення нейронів кожного наступного шару, включаючи вихідний шар, попередньо визначають із шару. Спочатку застосовується лінійна функція, повністю пов'язана або щільна . Один із способів подання такої функції - використання матриці ваг . Наприклад, переходи між першими двома шарами базової лінії можуть бути представлені матрицею 82025 x 20. Кількість ваг - це кількість записів у цій матриці, наприклад 1640500. Тоді до кожного запису додається (окремий) термін зміщення. Це може бути представлено вектором, наприклад, матрицею 82025 x 1 у нашому випадку. Кількість ухилів - це кількість записів, наприклад, 82025. (Зверніть увагу, що ваги та ухили разом описують афінну лінійну функцію .)

Вага або ухил рахуються, навіть якщо вони дорівнюють нулю. Для цілей цього вузького визначення ухили вважають вагами, навіть якщо всі вони дорівнюють нулю. Зауважимо, що в базовому прикладі використовуються лише два чіткі ваги (+1 і -1) (і лише трохи більш чіткі відхилення); тим не менше, розмір становить понад мільйон, тому що повторення жодним чином не допомагає складати рахунок.

Нарешті, нелінійна функція, яка називається функцією активації , застосовується вхідним шляхом до результату цієї афінної лінійної функції. Для цілей цього вузького визначення, дозволені функції активації є РЕЛУ , гіперболічний тангенс і сигмовидної . Весь шар повинен використовувати одну і ту ж функцію активації.

У базовому прикладі кількість ваг становить 20 * 82025 + 82025 * 1 = 1722525, а кількість ухилів - 82025 + 1 = 82026, для загальної оцінки 1722525 + 82026 = 1804551. Як символічний приклад, якби ще один шар і розміри шарів були замість цього [20, a, b, 1], тоді кількість ваг складе 20 * a + a * b + b * 1, а кількість зміщення буде a + b + 1.

Це визначення нейронної мережі добре підтримується багатьма механізмами, включаючи Keras , scikit вчитися і Tensorflow . Керас використовується в наведеному вище прикладі базового сценарію з кодом по суті наступним чином:

from keras.models import Sequential
model = Sequential()
from keras.layers import Dense
model.add(Dense(units=82025, activation='relu', input_dim=20, weights=[weights1, biases1]))
model.add(Dense(units=1, activation='relu', weights=[weights2, biases2]))
score = numpy.size(weights1) + numpy.size(biases1) + numpy.size(weights2) + numpy.size(biases2)

Якщо матриці ваги та зміщення - це масивні масиви, то numpy.size безпосередньо повідомить вам кількість записів.

Чи існують інші види нейронних мереж?

Якщо ви хочете отримати єдине, точне визначення нейронної мережі та оцінка для цілей цього виклику, то будь ласка, використовуйте визначення в попередньому розділі. Якщо ви вважаєте, що "будь-яка функція" виглядала правильно, це нейронна мережа без параметрів , то будь ласка, використовуйте визначення в попередньому розділі.

Якщо ви більш вільний дух, то я закликаю вас далі досліджувати. Можливо, ваша відповідь не враховуватиме вузький виклик, але, можливо, вам буде веселіше. Деякі інші ідеї, які ви можете спробувати, включають більш екзотичні функції активації, періодичні нейронні мережі (читання по одному біту), звивисті нейронні мережі, більш екзотичні архітектури, софтмакс та LSTM (!). Ви можете використовувати будь-яку стандартну функцію активації та будь-яку стандартну архітектуру. Ліберальне визначення "стандартних" функцій нейронної мережі може включати будь-що, розміщене в арксиві до початку публікації цього питання.


Що це за типи ваг? Зазвичай люди використовують поплавці, чи можемо ми використовувати інші числові типи? наприклад, типи меншої, більшої або необмеженої точності.
Пшеничний майстер

@ SriotchilismO'Zaic: Для цілей вузького визначення, я думаю, має сенс обмежитися плаванням і подвійним (реальними числами з однозначною і двоточною плаваючою точкою IEEE) для всіх ваг і проміжних значень. (Хоча зауважте, що деякі реалізації можуть використовувати інші величини точності - наприклад, 80-бітну - під час оцінювання.)
А. Рекс,

Мені подобається це питання, але я розчарований, що не дуже багато нейронної мережі знайти з достатньою кількістю часу на тренування.
Ануш

Відповіді:


13

Пробний поділ: оцінка 59407, 6243 шарів, 16478 нейронів

Дана програма Python, яка генерує та перевіряє мережу. Дивіться коментарі в trial_divisionдля пояснення того, як це працює. Перевірка досить повільна (як, наприклад, час роботи, виміряний в годинах): я рекомендую використовувати PyPy або Cython.

αмакс(0,α)

Поріг дорівнює 1: усе вище, ніж основне, все, що нижче, є складеним або нульовим, і єдиний вхід, який дає вихід 1, - це 1 саме.

#!/usr/bin/python3

import math


def primes_to(n):
    ps = []
    for i in range(2, n):
        is_composite = False
        for p in ps:
            if i % p == 0:
                is_composite = True
                break
            if p * p > i:
                break
        if not is_composite:
            ps.append(i)
    return ps


def eval_net(net, inputs):
    for layer in net:
        inputs.append(1)
        n = len(inputs)
        inputs = [max(0, sum(inputs[i] * neuron[i] for i in range(n))) for neuron in layer]
    return inputs


def cost(net):
    return sum(len(layer) * len(layer[0]) for layer in net)


def trial_division(num_bits):
    # Overview: we convert the bits to a single number x and perform trial division.
    # x is also our "is prime" flag: whenever we prove that x is composite, we clear it to 0
    # At the end x will be non-zero only if it's a unit or a prime, and greater than 1 only if it's a prime.
    # We calculate x % p as
    #     rem = x - (x >= (p << a) ? 1 : 0) * (p << a)
    #     rem -= (rem >= (p << (a-1)) ? 1) : 0) * (p << (a-1))
    #     ...
    #     rem -= (rem >= p ? 1 : 0) * p
    #
    # If x % p == 0 and x > p then x is a composite multiple of p and we want to set it to 0

    N = 1 << num_bits
    primes = primes_to(1 + int(2.0 ** (num_bits / 2)))

    # As a micro-optimisation we exploit 2 == -1 (mod 3) to skip a number of shifts for p=3.
    # We need to bias by a multiple of 3 which is at least num_bits // 2 so that we don't get a negative intermediate value.
    bias3 = num_bits // 2
    bias3 += (3 - (bias3 % 3)) % 3

    # inputs: [bit0, ..., bit19]
    yield [[1 << i for i in range(num_bits)] + [0],
           [-1] + [0] * (num_bits - 1) + [1],
           [0] * 2 + [-1] * (num_bits - 2) + [1],
           [(-1) ** i for i in range(num_bits)] + [bias3]]

    for p in primes[1:]:
        # As a keyhole optimisation we overlap the cases slightly.
        if p == 3:
            # [x, x_is_even, x_lt_4, x_reduced_mod_3]
            max_shift = int(math.log((bias3 + (num_bits + 1) // 2) // p, 2))
            yield [[1, 0, 0, 0, 0], [0, 1, -1, 0, 0], [0, 0, 0, 1, 0], [0, 0, 0, -1, p << max_shift]]
            yield [[1, -N, 0, 0, 0], [0, 0, 1, 0, 0], [0, 0, 0, -1, 1]]
            yield [[1, 0, 0, 0], [0, 1, -p << max_shift, 0]]
        else:
            # [x, x % old_p]
            max_shift = int(num_bits - math.log(p, 2))
            yield [[1, 0, 0], [1, -N, -p_old], [-1, 0, p << max_shift]]
            yield [[1, -N, 0, 0], [0, 0, -1, 1]]
            yield [[1, 0, 0], [1, -p << max_shift, 0]]

        for shift in range(max_shift - 1, -1, -1):
            # [x, rem]
            yield [[1, 0, 0], [0, 1, 0], [0, -1, p << shift]]
            yield [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, -1, 1]]
            yield [[1, 0, 0, 0], [0, 1, -p << shift, 0]]
        # [x, x % p]
        p_old = p

    yield [[1, 0, 0], [1, -N, -p]]
    yield [[1, -N, 0]]


def validate_primality_tester(primality_tester, threshold):
    num_bits = len(primality_tester[0][0]) - 1
    primes = set(primes_to(1 << num_bits))
    errors = 0
    for i in range(1 << num_bits):
        expected = i in primes
        observed = eval_net(primality_tester, [(i >> shift) & 1 for shift in range(num_bits)])[-1] > threshold
        if expected != observed:
            errors += 1
            print("Failed test case", i)
        if (i & 0xff) == 0:
            print("Progress", i)

    if errors > 0:
        raise Exception("Failed " + str(errors) + " test case(s)")


if __name__ == "__main__":
    n = 20

    trial_div = list(trial_division(n))
    print("Cost", cost(trial_div))
    validate_primality_tester(trial_div, 1)

Як осторонь, повторно

теорема про універсальне наближення зазначає, що нейронні мережі можуть наближати будь-яку безперервну функцію

макс(0,1-аi)макс(0,1+(аi-1))але працює правильно, лише якщо гарантовано, що його входи дорівнюють 0 або 1 і можуть видавати більші цілі числа. В один шар можливі різні інші ворота, але NOR сам по собі є повним Тюрінгом, тому не потрібно вникати в деталі.


В подальшому я почав працювати над тестом Ейлера, перш ніж спробувати пробний поділ, тому що вважав, що це буде більш ефективно, але збільшити число (7 - найкращий кандидат) до сили (x- (x mod 2) ) вимагало б 38 множень з подальшим скороченням mod x, а найкраща мережа, яку я знайшов для множення 20-бітних чисел, коштує 1135, тому вона не буде конкурентоспроможною.
Пітер Тейлор

7

Оцінка 984314, 82027 шарів, 246076 нейронів

Ми можемо зберігати речі цілими числами, якщо використовувати функцію активації ReLU, яка спрощує аналіз.

хх=а

  1. геа=(х-а)+леа=(-х+а)+
  2. еква=(-геа-леа+1)+еква1х=а0

х

ге2=(х-2)+ле2=(-х+2)+

акам2=(-ге2-ле2+1)+ге3=(ге2-(3-2))+ле3=(-ге2+(3-2))+

акам3=(221акам2-ге3-ле3+1)+ге5=(ге3-(5-3))+ле5=(-ге3+(5-3))+

Шар 5: виходи акам5=(221акам3-ге5-ле5+1)+ге7=(ге5-(7-5))+ле7=(-ге5+(7-5))+ . Вартість (3 + 1) * 3 = 12.

...

акам1048571=(221акам1048559-ге1048571-ле1048571+1)+ге1048573=(ге1048571-(1048573-1048571))+ле1048573=(-ге1048571+(1048573-1048571))+. Вартість (3 + 1) * 3 = 12.

Шар 82027: виходи акам1048573=(221акам1048571-ге1048573-ле1048573+1)+. Вартість (3 + 1) * 1 = 4.

Поріг дорівнює 0. Якщо ви працюєте з подвійними, перелийте до + цілком можливо, але це, здається, ідеально відповідає правилам.

Оцінка становить (82026 - 3) * 12 + 21 + 4 + 9 + 4.


Класно. Як я розумію, це також "запам'ятовує" праймери, але воно перевіряє рівність "послідовно", а не "паралельно". (Крім того, це як перенесення базової лінії.) Перший крок - негайно відійти від бітового шаблону і просто працювати з власне цілим числом. Як результат, у перевірці рівності немає 20-кратного штрафу. Дякуємо за ваше подання
А. Рекс,

Що є надрисовим плюсом?
feersum

1
@feersum, це позначення зі сторінки Вікіпедії на ReLU, пов’язані у питанні. х+=макс(0,х)
Пітер Тейлор
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.