Uma rede neural pode reconhecer primos?


26

fundo

Reconhecer a primalidade parece um ajuste inadequado para redes neurais (artificiais). Entretanto, o teorema da aproximação universal afirma que as redes neurais podem se aproximar de qualquer função contínua, portanto, em particular, deve ser possível representar qualquer função com suporte finito que se deseja. Então, vamos tentar reconhecer todos os números primos entre o primeiro milhão de números.

Mais precisamente, como este é um site de programação, vamos subir para 2 ^ 20 = 1.048.576. O número de primos abaixo deste limite é 82.025 ou aproximadamente 8%.

Desafio

Qual o tamanho de uma rede neural que classifica corretamente todos os números inteiros de 20 bits como primos ou não primos?

Para os propósitos deste desafio, o tamanho de uma rede neural é o número total de pesos e desvios necessários para representá-la.

Detalhes

O objetivo é minimizar o tamanho de uma única rede neural explícita.

A entrada para sua rede será um vetor de comprimento 20 contendo os bits individuais de um número inteiro, representados com 0s e 1s ou alternativamente com -1s e + 1s. A ordem destes pode ser o primeiro com o bit mais significativo ou o primeiro com o menos significativo.

A saída da sua rede deve ser um número único, de modo que, acima de algum ponto de corte, a entrada seja reconhecida como primária e abaixo do mesmo ponto de corte, a entrada seja reconhecida como não primária. Por exemplo, positivo pode significar primo (e negativo, não primo) ou, alternativamente, maior que 0,5 pode significar primo (e menor que 0,5, não primo).

A rede deve ser 100% precisa em todas as 2 ^ 20 = 1.048.576 entradas possíveis. Como mencionado acima, observe que existem 82.025 números primos nesse intervalo. (Daqui resulta que sempre a saída "not prime" seria 92% precisa.)

Em termos de terminologia de rede neural padrão, isso provavelmente seria chamado de ajuste excessivo . Em outras palavras, seu objetivo é super-ajustar perfeitamente os números primos. Outras palavras que se pode usar são que o "conjunto de treinamento" e o "conjunto de teste" são os mesmos.

Esse desafio não considera o número de parâmetros "treináveis" ou "aprendíveis". De fato, é provável que sua rede contenha pesos codificados e o exemplo abaixo é totalmente codificado. Em vez disso, todos os pesos e desvios são considerados parâmetros e são contados.

O comprimento do código necessário para treinar ou gerar sua rede neural não é relevante para sua pontuação, mas a publicação do código relevante é certamente apreciada.

Linha de base

Como linha de base, é possível "memorizar" todos os 82.025 primos com 1.804.551 pesos totais e desvios.

Observe que este código a seguir inclui muitas coisas: um exemplo de trabalho, código de teste de trabalho, uma definição funcional de rede neural usando uma biblioteca de rede neural conhecida, uma rede neural "codificada" (ou pelo menos não treinada), e uma medida de trabalho de pontuação.

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

O que é uma rede neural?

Para os propósitos deste desafio, podemos escrever uma definição estreita, mas precisa, de uma rede neural (artificial). Para algumas leituras externas, sugiro Wikipedia sobre rede neural artificial , rede neural feedforward , perceptron multicamada e função de ativação .

Uma rede neural feedforward é uma coleção de camadas de neurônios. O número de neurônios por camada varia, com 20 neurônios na camada de entrada, algum número de neurônios em uma ou mais camadas ocultas e 1 neurônio na camada de saída. (Deve haver pelo menos uma camada oculta porque os números primos e não primos não são linearmente separáveis ​​de acordo com seus padrões de bits.) No exemplo da linha de base acima, os tamanhos das camadas são [20, 82025, 1].

Os valores dos neurônios de entrada são determinados pela entrada. Como descrito acima, serão 0s e 1s correspondentes aos bits de um número entre 0 e 2 ^ 20 ou -1s e + 1s da mesma forma.

Os valores dos neurônios de cada camada seguinte, incluindo a camada de saída, são determinados previamente a partir da camada. Primeiro, uma função linear é aplicada, de maneira totalmente conectada ou densa . Um método para representar essa função é usar uma matriz de pesos . Por exemplo, as transições entre as duas primeiras camadas da linha de base podem ser representadas com uma matriz 82025 x 20. O número de pesos é o número de entradas nessa matriz, por exemplo, 1640500. Em seguida, cada entrada possui um termo de viés (separado) adicionado. Isso pode ser representado por um vetor, por exemplo, uma matriz 82025 x 1 em nosso caso. O número de desvios é o número de entradas, por exemplo, 82025. (Observe que os pesos e desvios juntos descrevem uma função linear afim .)

Um peso ou viés é contado, mesmo que seja zero. Para os fins desta definição restrita, os vieses contam como pesos, mesmo que sejam todos zero. Observe que no exemplo da linha de base, apenas dois pesos distintos (+1 e -1) são usados ​​(e apenas vieses ligeiramente mais distintos); no entanto, o tamanho é superior a um milhão, porque a repetição não ajuda na pontuação de forma alguma.

Finalmente, uma função não linear denominada função de ativação é aplicada de entrada no resultado dessa função linear afim. Para os fins desta definição restrita, as funções de ativação permitidas são ReLU , tanh e sigmoid . A camada inteira deve usar a mesma função de ativação.

No exemplo da linha de base, o número de pesos é 20 * 82025 + 82025 * 1 = 1722525 e o número de desvios é 82025 + 1 = 82026, para uma pontuação total de 1722525 + 82026 = 1804551. Como exemplo simbólico, se houver mais uma camada e os tamanhos das camadas eram [20, a, b, 1], então o número de pesos seria 20 * a + a * b + b * 1 e o número de vieses seria a + b + 1.

Essa definição de rede neural é bem suportada por muitas estruturas, incluindo Keras , scikit-learn e Tensorflow . Keras é usado no exemplo da linha de base acima, com o código essencialmente da seguinte maneira:

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 as matrizes de ponderação e polarização forem matrizes numpy , o numpy.size informará diretamente o número de entradas.

Existem outros tipos de redes neurais?

Se você deseja uma definição única e precisa de rede neural e pontuação para os propósitos deste desafio, use a definição na seção anterior. Se você acha que "qualquer função" vista da maneira correta é uma rede neural sem parâmetros , use a definição na seção anterior.

Se você é um espírito mais livre, encorajo você a explorar mais. Talvez sua resposta não conte para o desafio estreito , mas talvez você se divirta mais. Algumas outras idéias que você pode tentar incluem funções de ativação mais exóticas, redes neurais recorrentes (lendo um bit de cada vez), redes neurais convolucionais, arquiteturas mais exóticas, softmax e LSTMs (!). Você pode usar qualquer função de ativação padrão e qualquer arquitetura padrão. Uma definição liberal de recursos de rede neural "padrão" pode incluir qualquer coisa postada no arxiv antes da publicação desta pergunta.


Que tipo de tipos são esses pesos? Geralmente as pessoas usam carros alegóricos, podemos usar outros tipos numéricos? por exemplo, tipos de precisão menor, mais ou ilimitada.
Wheat Wizard

@ SriotchilismO'Zaic: Para os propósitos da definição restrita, acho que faz sentido restringir a flutuar e dobrar (números reais de ponto flutuante de precisão simples e dupla IEEE) para todos os pesos e valores intermediários. (Embora observe que algumas implementações podem usar outras quantidades de precisão - por exemplo, 80 bits - durante a avaliação).
A. Rex

Eu amo essa pergunta, mas estou desapontado por não haver uma rede neural muito menor com tempo de treinamento suficiente.
Anush

Respostas:


13

Divisão de avaliação: pontuação 59407, 6243 camadas, 16478 neurônios no total

Dado como um programa Python que gera e valida a rede. Veja os comentários trial_divisionpara obter uma explicação de como funciona. A validação é bastante lenta (como, tempo de execução medido em horas): eu recomendo usar PyPy ou Cython.

αmax(0 0,α)

O limite é 1: qualquer coisa acima do que é primo, qualquer coisa abaixo é composta ou zero, e a única entrada que fornece uma saída de 1 é o próprio 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)

Como um aparte, re

o teorema da aproximação universal afirma que as redes neurais podem aproximar qualquer função contínua

max(0 0,1-umaEu)max(0 0,1+(umaEu-1))mas só funcionará corretamente se for garantido que suas entradas sejam 0 ou 1 e pode gerar números inteiros maiores. Vários outros portões são possíveis em uma camada, mas o NOR por si só é completo em Turing, portanto não há necessidade de entrar em detalhes.


Além disso, comecei a trabalhar em um teste de Euler antes de tentar a divisão de testes, porque achava que seria mais eficiente, mas elevar um número (7 era o melhor candidato) a uma potência de (x- (x mod 2) ) exigiria 38 multiplicações seguidas pela redução mod x, e a melhor rede que encontrei para multiplicar números de 20 bits custa 1135, portanto, não será competitivo.
Peter Taylor

7

Pontuação 984314, 82027 camadas, 246076 neurônios no total

Podemos manter as coisas inteiramente nos números inteiros se usarmos a função de ativação ReLU, que simplifica a análise.

xx=uma

  1. geuma=(x-uma)+leuma=(-x+uma)+
  2. equma=(-geuma-leuma+1)+equma1x=uma0 0

x

ge2=(x-2)+le2=(-x+2)+

acumular2=(-ge2-le2+1)+ge3=(ge2-(3-2))+le3=(-ge2+(3-2))+

acumular3=(221acumular2-ge3-le3+1)+ge5=(ge3-(5-3))+le5=(-ge3+(5-3))+

acumular5=(221acumular3-ge5-le5+1)+ge7=(ge5-(7-5))+le7=(-ge5+(7-5))+

...

Camada 82026: saídas acumular1048571=(221acumular1048559-ge1048571-le1048571+1)+ge1048573=(ge1048571-(1048573-1048571))+le1048573=(-ge1048571+(1048573-1048571))+

acumular1048573=(221acumular1048571-ge1048573-le1048573+1)+

+

A pontuação é (82026 - 3) * 12 + 21 + 4 + 9 + 4.


Legal. Pelo que entendi, isso também "memoriza" os números primos, mas testa a igualdade "sequencialmente" e não em "paralelo". (Como alternativa, é como uma transposição da linha de base.) A primeira etapa é afastar-se imediatamente do padrão de bits e apenas trabalhar com o inteiro inteiro. Como resultado, não há uma penalidade de 20 vezes na verificação de igualdade. Obrigado pela sua submissão
A. Rex

O que é sobrescrito mais?
feersum 14/04

1
x+=max(0 0,x)
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.