Una rete neurale può riconoscere i numeri primi?


26

sfondo

Riconoscere la primalità sembra inadeguato per le reti (artificiali) neurali. Tuttavia, il teorema di approssimazione universale afferma che le reti neurali possono approssimare qualsiasi funzione continua, quindi in particolare dovrebbe essere possibile rappresentare qualsiasi funzione supportata finitamente che si desidera. Quindi proviamo a riconoscere tutti i numeri primi tra i primi milioni di numeri.

Più precisamente, poiché si tratta di un sito Web di programmazione, passiamo a 2 ^ 20 = 1.048.576. Il numero di numeri primi al di sotto di questa soglia è 82.025 o circa l'8%.

Sfida

Quanto è piccola una rete neurale che classifica correttamente tutti i numeri interi a 20 bit come primi o non primi?

Ai fini di questa sfida, la dimensione di una rete neurale è il numero totale di pesi e distorsioni richiesti per rappresentarla.

Dettagli

L'obiettivo è ridurre al minimo le dimensioni di una singola rete neurale esplicita.

L'input per la tua rete sarà un vettore di lunghezza 20 contenente i singoli bit di un numero intero, rappresentato con 0 e 1 o in alternativa con -1 e + 1. L'ordinamento di questi può essere il primo bit più significativo o il primo bit meno significativo.

L'output della tua rete dovrebbe essere un singolo numero, in modo che al di sopra di un valore di soglia l'ingresso sia riconosciuto come primo e al di sotto dello stesso valore di soglia l'ingresso sia riconosciuto come non primo. Ad esempio, positivo potrebbe significare primo (e negativo non primo), o in alternativa maggiore di 0,5 potrebbe significare primo (e inferiore a 0,5 non primo).

La rete deve essere accurata al 100% su tutti i 2 ^ 20 = 1.048.576 ingressi possibili. Come accennato in precedenza, si noti che ci sono 82.025 numeri primi in questo intervallo. (Ne consegue che produrre sempre "non prime" sarebbe preciso al 92%.)

In termini di terminologia della rete neurale standard, questo verrebbe probabilmente chiamato overfitting . In altre parole, il tuo obiettivo è quello di adattare perfettamente i numeri primi. Altre parole che si potrebbero usare sono che "set di allenamento" e "set di test" sono gli stessi.

Questa sfida non considera il numero di parametri "allenabili" o "apprendibili". In effetti, è probabile che la tua rete contenga pesi codificati e l'esempio che segue è interamente codificato. Invece, tutti i pesi e le inclinazioni sono considerati parametri e vengono conteggiati.

La lunghezza del codice necessario per addestrare o generare la tua rete neurale non è rilevante per il tuo punteggio, ma la pubblicazione del relativo codice è sicuramente apprezzata.

Baseline

Come base, è possibile "memorizzare" tutti gli 82.025 numeri primi con 1.804.551 pesi e distorsioni totali.

Si noti che questo codice che segue comprende molte cose: un esempio funzionante, un codice di prova funzionante, una definizione funzionante di rete neurale che utilizza una libreria di rete neurale nota, una rete neurale "codificata" (o almeno non "addestrata"), e una misurazione funzionante del punteggio.

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."

Cos'è una rete neurale?

Ai fini di questa sfida, possiamo scrivere una definizione stretta ma precisa di una rete neurale (artificiale). Per alcune letture esterne, suggerisco Wikipedia su rete neurale artificiale , rete neurale feedforward , percettrone multistrato e funzione di attivazione .

Una rete neurale feedforward è una raccolta di strati di neuroni. Il numero di neuroni per strato varia, con 20 neuroni nello strato di input, un certo numero di neuroni in uno o più strati nascosti e 1 neurone nello strato di output. (Deve esserci almeno uno strato nascosto perché i numeri primi e non primi non sono separabili linearmente in base ai loro modelli di bit.) Nell'esempio di base sopra riportato, le dimensioni dei livelli sono [20, 82025, 1].

I valori dei neuroni di input sono determinati dall'input. Come descritto sopra, questo sarà 0s e 1s corrispondenti ai bit di un numero compreso tra 0 e 2 ^ 20, oppure -1s e + 1s in modo simile.

I valori dei neuroni di ogni strato successivo, incluso lo strato di output, sono determinati in anticipo dallo strato. Innanzitutto viene applicata una funzione lineare, in modo completamente connesso o denso . Un metodo per rappresentare una tale funzione sta usando una matrice di pesi . Ad esempio, le transizioni tra i primi due strati della linea di base possono essere rappresentate con una matrice 82025 x 20. Il numero di pesi è il numero di voci in questa matrice, ad esempio 1640500. Quindi ad ogni voce è stato aggiunto un termine di bias (separato). Questo può essere rappresentato da un vettore, ad esempio una matrice 82025 x 1 nel nostro caso. Il numero di distorsioni è il numero di voci, ad esempio 82025. (Notare che pesi e distorsioni descrivono insieme una funzione lineare affine ).

Viene conteggiato un peso o una distorsione anche se è zero. Ai fini di questa definizione ristretta, i pregiudizi contano come pesi anche se sono tutti zero. Si noti che nell'esempio di base vengono utilizzati solo due pesi distinti (+1 e -1) (e solo inclinazioni leggermente più distinte); tuttavia, la dimensione è superiore a un milione, perché la ripetizione non aiuta in alcun modo il punteggio.

Infine, una funzione non lineare chiamata funzione di attivazione viene applicata dal punto di vista dell'entrata al risultato di questa funzione lineare affine. Ai fini di questa definizione ristretta, le funzioni di attivazione consentite sono ReLU , tanh e sigmoid . L'intero livello deve utilizzare la stessa funzione di attivazione.

Nell'esempio di base, il numero di pesi è 20 * 82025 + 82025 * 1 = 1722525 e il numero di distorsioni è 82025 + 1 = 82026, per un punteggio totale di 1722525 + 82026 = 1804551. Come esempio simbolico, se ci fossero un altro livello e le dimensioni del livello erano invece [20, a, b, 1], quindi il numero di pesi sarebbe 20 * a + a * b + b * 1 e il numero di distorsioni sarebbe a + b + 1.

Questa definizione di rete neurale è ben supportata da molti framework, tra cui Keras , scikit-learn e Tensorflow . Keras viene utilizzato nell'esempio di base precedente, con il codice essenzialmente come segue:

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)

Se le matrici di pesi e di polarizzazione sono matrici intorpidite , numpy.size ti dirà direttamente il numero di voci.

Esistono altri tipi di reti neurali?

Se si desidera un'unica definizione precisa di rete neurale e punteggio ai fini di questa sfida, utilizzare la definizione nella sezione precedente. Se ritieni che "qualsiasi funzione" considerata nel modo giusto sia una rete neurale senza parametri , utilizza la definizione nella sezione precedente.

Se sei uno spirito più libero, ti incoraggio a esplorare ulteriormente. Forse la tua risposta non conta per la sfida ristretta , ma forse ti divertirai di più. Alcune altre idee che potresti provare includono funzioni di attivazione più esotiche, reti neurali ricorrenti (lettura di un bit alla volta), reti neurali convoluzionali, architetture più esotiche, softmax e LSTM (!). È possibile utilizzare qualsiasi funzione di attivazione standard e qualsiasi architettura standard. Una definizione liberale di funzionalità di rete neurale "standard" potrebbe includere qualsiasi cosa pubblicata su arxiv prima della pubblicazione di questa domanda.


Che tipo di tipi sono questi pesi? Di solito le persone usano i float, possiamo usare altri tipi numerici? ad es. tipi di precisione inferiore, maggiore o illimitata.
Wheat Wizard

@ SriotchilismO'Zaic: Ai fini della definizione ristretta, penso che abbia senso limitare a float e double (numeri reali IEEE in virgola mobile a precisione singola e doppia) per tutti i pesi e valori intermedi. (Sebbene si noti che alcune implementazioni possono utilizzare altri livelli di precisione, ad esempio 80 bit, durante la valutazione.)
A. Rex

Adoro questa domanda, ma sono deluso che non ci sia una rete neurale molto più piccola da trovare con abbastanza tempo di allenamento.
Anush,

Risposte:


13

Divisione di prova: punteggio 59407, 6243 strati, 16478 neuroni in totale

Dato come un programma Python che genera e convalida la rete. Vedi i commenti trial_divisionper una spiegazione di come funziona. La validazione è piuttosto lenta (come in, tempo di esecuzione misurato in ore): consiglio di usare PyPy o Cython.

Tutti i livelli usano ReLU ( ) come funzione di attivazione.αmax(0,α)

La soglia è 1: qualsiasi cosa al di sopra di ciò che è primo, qualsiasi cosa al di sotto è composita o zero, e l'unico input che dà un output di 1 è 1 stesso.

#!/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)

A parte questo, re

il teorema di approssimazione universale afferma che le reti neurali possono approssimare qualsiasi funzione continua

è facile dimostrare che una rete neurale che utilizza ReLU è Turing completa. Il gate logico più semplice da implementare in modo affidabile è NOR: un gate NOR con input n è . Dico con fermezza perché questo gate accetta input maggiori di 1 ma (purché gli input non siano compresi tra 0 e 1) emette sempre solo 0 o 1. Un gate AND a singolo strato è ma funziona correttamente solo se i suoi input sono garantiti su 0 o 1 e possono generare numeri interi più grandi. Diverse altre porte sono possibili in uno strato, ma NOR di per sé è Turing completo, quindi non è necessario entrare nei dettagli.max(0,1ai)max(0,1+(ai1))


Inoltre, ho iniziato a lavorare su un test di Euler prima di provare la divisione di prova, perché pensavo che sarebbe stato più efficiente, ma aumentando un numero (7 era il miglior candidato) a una potenza di (x- (x mod 2) ) richiederebbe 38 moltiplicazioni seguite da riduzione mod x, e la migliore rete che ho trovato per moltiplicare i numeri a 20 bit costa 1135, quindi non sarà competitiva.
Peter Taylor,

7

Punteggio 984314, 82027 strati, 246076 neuroni in totale

Possiamo tenere le cose interamente negli interi se utilizziamo la funzione di attivazione ReLU, che semplifica l'analisi.

Dato un input che è noto per essere un numero intero, possiamo verificare se con due strati e tre neuroni:xx=a

  1. Primo livello: output egea=(xa)+lea=(x+a)+
  2. Secondo livello: output . sarà se e altrimenti.eqa=(gealea+1)+eqa1x=a0

Livello 1: ridurre i 20 input a un valore con pesi 1, 2, 4, ... e polarizzazione 0. Costo: (20 + 1) * 1 = 21.x

Livello 2: output , . Costo (1 + 1) * 2 = 4.ge2=(x2)+le2=(x+2)+

Livello 3: output , , . Costo (2 + 1) * 3 = 9.accum2=(ge2le2+1)+ge3=(ge2(32))+le3=(ge2+(32))+

Livello 4: output , , . Costo (3 + 1) * 3 = 12.accum3=(221accum2ge3le3+1)+ge5=(ge3(53))+le5=(ge3+(53))+

Livello 5: output , , . Costo (3 + 1) * 3 = 12.accum5=(221accum3ge5le5+1)+ge7=(ge5(75))+le7=(ge5+(75))+

...

Livello 82026: output , , . Costo (3 + 1) * 3 = 12.accum1048571=(221accum1048559ge1048571le1048571+1)+ge1048573=(ge1048571(10485731048571))+le1048573=(ge1048571+(10485731048571))+

Livello 82027: output . Costo (3 + 1) * 1 = 4.accum1048573=(221accum1048571ge1048573le1048573+1)+

La soglia è 0. Se si lavora con i doppi, l'overflow a è del tutto possibile, ma sembra perfettamente in accordo con le regole.+

Il punteggio è (82026-3) * 12 + 21 + 4 + 9 + 4.


Freddo. A quanto ho capito, anche questo "memorizza" i numeri primi, ma verifica l'uguaglianza "in sequenza" piuttosto che in "parallelo". (In alternativa, è come una trasposizione della linea di base.) Il primo passo è allontanarsi immediatamente dal modello di bit e lavorare con l'intero numero reale stesso. Di conseguenza, non esiste una penalità di 20 volte nel controllo dell'uguaglianza. Grazie per la tua presentazione
A. Rex

Che cos'è superscript plus?
febbraio

1
@feersum, questa è la notazione dalla pagina Wikipedia su ReLU collegata alla domanda. x+=max(0,x)
Peter Taylor,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.