Modelado del lenguaje Bigram desde cero.
El modelado del lenguaje tiene que ver con cómo las computadoras entienden y generan el lenguaje humano. Es una parte clave para crear sistemas de inteligencia artificial que puedan comunicarse con nosotros de manera efectiva. En los últimos años, ChatGPT ha sido un tema de discusión destacado en artículos de tecnología. Chat – Generative T ransformer preentrenado ha permitido a los usuarios generales hacer uso de un modelo de lenguaje altamente competente para responder sus consultas . ¿Cómo logra ChatGPT esta tarea? En primer lugar, está preentrenado en una parte sólida de Internet, lo que, cuando se combina con un preprocesamiento, capacitación y ajuste cuidadosos, puede hacer maravillas. En segundo lugar, funciona según el principio de encontrar el siguiente fragmento, dada una oración de entrada. Este mecanismo de atención combinado permite a ChatGPT crear párrafos completos y coherentes.
En este artículo, analizaremos un modelo de lenguaje similar, mucho menos potente. Nuestro modelo será un modelo a nivel de carácter en lugar de un modelo a nivel de palabra/fragmento y predecirá el siguiente carácter dado un carácter anterior.
El código de este artículo se puede encontrar en el siguiente cuaderno J upyter.
modelo bigrama
La esencia del modelo de bigramas en el modelado del lenguaje es aproximar la probabilidad de una secuencia de palabras considerando la probabilidad de cada palabra dada su predecesora inmediata.
La probabilidad de una secuencia de palabras (W = w_1, w_2,…, w_n) se representa de la siguiente manera:
P (W) = P (w_1, w_2, ..., w_n) ≈ P (w_1) * P (w_2 | w_1) * P (w_3 | w_2) * ... * P (w_n | w_{n- 1 })
Dónde:
P(w_1)
es la probabilidad de la primera palabra de la secuencia.
P(w_i | w_{i-1})
es la probabilidad condicional de la palabra w_i
dado que la palabra anterior es w_{i-1}
.
La probabilidad condicional P(w_i | w_{i-1})
de un bigrama se estima a partir de un corpus de texto de la siguiente manera:
P(w_i | w_{i-1}) = Contar(w_{i-1}, w_i) / Contar(w_{i-1})
Count(w_{i-1}, w_i)
representa el número de veces que la palabra w_i
sigue a la palabra w_{i-1}
en el corpus.
Count(w_{i-1})
es el número de veces que aparece la palabra w_{i-1}
en el corpus.
Implementación del modelo Bigram
Para el corpus de entrada, usaremos todas las cadenas ennames.txt
palabras = abrir ( 'nombres.txt' , 'r' ).read().splitlines()
palabras[: 10 ]
[ 'emma' ,
'olivia' ,
'ava' ,
'isabella' ,
'sophia' ,
'charlotte' ,
'mia' ,
'amelia' ,
'harper' ,
'evelyn' ]
print ( f"Número de palabras en el corpus { len (palabras)} " )
print ( f"Nombre más corto en el corpus { min ( len (w) para w en palabras)} " )
print ( f"Nombre más largo en el corpus { max ( len (w) para w en palabras)} " )
Número de palabras en el corpus 32033 Nombre
más corto en el corpus 2 Nombre
más largo en el corpus 15
A continuación, crearemos pares de bigramas y almacenaremos su recuento en un diccionario. Agregaremos un token/carácter especial para indicar el inicio y el final de la palabra.
bg_pairs = dict ()
para palabra en palabras:
palabra = [ '.' ] + lista (palabra) + [ '.' ]
para ch1, ch2 en zip (palabra, palabra[ 1 :]):
bg_pair = (ch1, ch2)
bg_pairs[bg_pair] = bg_pairs.get(bg_pair, 0 ) + 1
Impresión de los 10 pares de bigramas principales según su frecuencia
ordenado (bg_pairs.items(), clave = lambda kv: -kv[ 1 ])[: 10 ]
[(( 'n' , '.' ), 6763),
(( 'a' , '.' ), 6640),
(( 'a' , 'n' ), 5438),
(( '.' , ' a' ), 4410),
(( 'e' , '.' ), 3983),
(( 'a' , 'r' ), 3264),
(( 'e' , 'l' ), 3248),
( ( 'r' , 'i' ), 3033),
(( 'n' , 'a' ), 2977),
(( '.' , 'k' ), 2963)]
Bigram (n, .)
es el par más frecuente, lo que significa que los nombres terminan con mayor frecuencia n
en nuestro conjunto de datos. Intentemos visualizar los datos que tenemos. Usaremos matplotlib, pero como no reconoce texto, convertiremos nuestros caracteres en una representación entera.
lista_caracteres = ordenado ( lista ( set ( '' .join(palabras))))
lista_caracteres
[ 'a' ,
'b' ,
'c' ,
'd' ,
'e' ,
'f' ,
'g' ,
'h' ,
'i' ,
'j' ,
'k' ,
'l' ,
' m' ,
'n' ,
'o' ,
'p' ,
'q' ,
'r' ,
's' ,
't' ,
'u' ,
'v' ,
'w' ,
'x' ,
'y' ,
'z' ]
stoi = {s:i+ 1 for i,s in enumerate (character_list)} # Agregar 1 a cada índice para que se pueda asignar un carácter especial al índice 0
stoi[ '.' ] = 0
itos = {i:s for s,i in stoi.items()} # Crear mapeo inverso también
stoi
{ 'a' : 1,
'b' : 2,
'c' : 3,
'd' : 4,
'e' : 5,
'f' : 6,
'g' : 7,
'h' : 8,
' i' : 9,
'j' : 10,
'k' : 11,
'l' : 12,
'm' : 13,
'n' : 14,
'o' : 15,
'p' : 16,
'q' : 17,
'r' : 18,
's' : 19,
't' : 20,
'u' : 21,
'v' : 22,
'w' : 23,
'x' : 24,
'y' : 25 ,
'z' : 26,
'.' : 0}
Ahora que tenemos el diccionario para asignar los caracteres a números enteros, usemos un tensor de PyTorch para almacenar la frecuencia de bg_pairs.
importar antorcha
N = torch.zeros(( 27 , 27 ), dtype=torch.int32)
para palabra en palabras:
palabra = [ '.' ] + lista (palabra) + [ '.' ]
para ch1, ch2 en zip (palabra, palabra[ 1 :]):
idx1 = stoi[ch1]
idx2 = stoi[ch2]
N[idx1, idx2] += 1
importar matplotlib.pyplot como plt
importar numpy como np
%matplotlib en línea
plt.figure(figsize=( 16 , 16 ))
plt.imshow(N, cmap= 'Blues' )
para i en el rango ( 27 ):
para j en el rango ( 27 ):
chstr = itos[i] + itos[j]
plt.text(j, i, chstr, ha= "centro" , va= "abajo" , color= 'gris' )
plt.text(j, i, N[i, j].item(), ha= "centro" , va= "arriba" , color= 'gris' )
plt.axis( 'apagado' );
Distribución de frecuencia de Bigram
En el diagrama anterior, el valor correspondiente a cada par (ch1, ch2)
indica cuántas veces ha aparecido ch1 antes de ch2 . Los pares como (a, .)
muestran cuántos nombres terminaron con a
y de manera similar los pares como (., a)
muestran el recuento de nombres que comienzan con a
.
Ahora necesitaremos convertir la distribución de frecuencia anterior en una distribución probabilística. Veamos cómo se ve eso para la primera fila.
norte[ 0 ]
tensor([ 0 , 4410 , 1306 , 1542 , 1690 , 1531 , 417 , 669 , 874 , 591 , 2422 , 2963 , 1572 , 2538 ,
1146 , 394 , 515 , 92 , 9 , 2055 , 1308 , 78 , 376 , 307 , 134 , 535 , 929 ], dtype=antorcha.int32)
pag = norte[0]. float () # Convertir a flotante para que la probabilidad no se redondee a 0
p /= N[0]. suma ()
p
tensor ( [ 0,0000 , 0,1377 , 0,0408 , 0,0481 , 0,0528 , 0,0478 , 0,0130 , 0,0209 , 0,0273 , 0,0184 ,
0,0756 , 0,0925 , 0,0491 , 0,0792 , 0,0358 , 0,0123 , 0,0161 , 0,0029 , 0,0512 , 0,0642 , 0,0408 , 0,0024 , 0,0117 , 0,0096 , 0,0042 , 0,0167 , 0,0290 ])
Esto nos da la distribución probabilística. Para el ejemplo anterior, para indexado por filas 0
tenemos esta distribución de probabilidad. Para muestrear un índice a partir de una distribución de probabilidad, podemos hacer uso detorch.multinomial
gen = torch.Generator ( ) .manual_seed ( 2147483647 ) # Usaremos un generador con una semilla para que este experimento sea determinista
[ itos [ ix.item ( ) ] para ix en torch.multinomial ( p, num_samples = 3 , reemplazo = Verdadero , generador = gen ) ] # Muestreo de 3 caracteres de la distribución de probabilidad p
[ 'm' , 's' , 'n' ]
Ahora sabemos cómo llegar num_samples
a partir de una distribución de probabilidad p
. A continuación convertiremos el tensor N
de distribución de frecuencia a distribución de probabilidad. Dado que N
es un tensor de forma 2D (27, 27)
, para normalizarlo necesitaremos dividir cada elemento N[i, j]
porSUM(N[i])
P = (norte). float ()
P /= N. sum ( 1 , keepdims= True ) # 1 denota suma a lo largo de idx 1, que es columna.
forma de p
antorcha .Tamaño ( [27, 27] )
Radiodifusión
En el código anterior utilizamos N.sum(1, keepdims=True)
. Profundicemos en esta expresión para comprender los matices de la transmisión en pytorch. La forma de N.sum(1) es torch.Size([27])
mientras que la forma de N.sum(1, keepdims=True) estorch.Size([27, 1])
N.forma
antorcha .Tamaño ( [27, 27] )
N. suma (1).forma
antorcha .Tamaño ( [27] )
Cuando intentamos dividir la forma (27, 27)
por (27)
, la forma se convierte (1, 27)
y luego se extiende a lo largo de cada fila para crear la forma (27, 27)
. Esto significa que al dividir obtendremosN[i, j]/ sum of N[:, j]
N. suma ( 1 , keepdims= True ).forma
antorcha .Tamaño ( [27, 1] )
Cuando intentamos dividir la forma (27, 27)
entre (27, 1)
, la forma se extiende a lo largo de cada columna para crear la forma (27, 27)
. Lo que significa que al dividir obtendremos N[i, j]/ sum of N[i, :]
lo que esperamos.
Probabilidad
Ahora, calculemos la probabilidad que nuestro modelo asigna a algunas de las palabras de nuestro corpus. Lo ideal sería que esta probabilidad se diera 1
porque esas palabras están presentes en el conjunto de datos.
gen = torch.Generator().manual_seed( 2147483647 )
para i en el rango ( 10 ):
out = []
ix = 0
mientras que True:
p = P[ix]
ix = torch.multinomial(p, num_samples= 1 , replacement= Verdadero, generador=gen).item()
out .append(itos[ix])
if ix == 0 :
break
print ( '' . join ( out ))
junide.
janasah.
pag.
conejo.
a.
nn.
Kohin.
toliano.
jue.
ksahnaauranilevias.
El resultado anterior puede parecer un galimatías, para convencernos de que nuestro modelo realmente está aprendiendo algo, probemos esto
para palabra en palabras[: 3 ]:
palabra = [ '.' ] + lista (palabra) + [ '.' ]
para ch1, ch2 en zip (palabra, palabra[ 1 :]):
idx1 = stoi[ch1]
idx2 = stoi[ch2]
print ( f" {ch1} {ch2} {P[idx1, idx2]: .4 f } " )
. e 0,0478
em 0,0377
mm 0,0253
m a 0,3899
a . 0,1960
. o 0,0123
o l 0,0780
l yo 0,1777
i v 0,0152
v i 0,3541
i a 0,1381
a . 0,1960
. a 0,1377
a v 0,0246
v a 0,2495
a . 0,1960
Aquí vemos la probabilidad de cada par, ahora si el modelo no estuviera aprendiendo ningún patrón, cada uno de estos habría sido igualmente probable, es decir 1/27 ~ 0.037
, pero los modelos tienen mayor probabilidad (probabilidad) para algunos pares y menos probabilidad (probabilidad) para otros.
Evaluación de la calidad del modelo utilizando log-verosimilitud negativa
En nuestro caso, la probabilidad es simplemente otro término para la probabilidad de un elemento o bigrama. Dado que las probabilidades pueden ir de 0 a 1, para tener una función de pérdida continua y uniforme, tiene sentido tener un registro de probabilidad o un registro de verosimilitud.
x = np.linspace( 0.000001 , 1 , 100 )
y = np. log (x)
plt.plot(x, y, etiqueta= 'y = log(x)' )
probabilidad de registro
Calcular la probabilidad logarítmica de los pares
log_likelihood = 0
para palabra en palabras[: 3 ]:
palabra = [ '.' ] + lista (palabra) + [ '.' ]
para ch1, ch2 en zip (palabra, palabra[ 1 :]):
idx1 = stoi[ch1]
idx2 = stoi[ch2]
prob = P[idx1, idx2]
logprob = torch.log(prob)
log_likelihood += logprob
print ( f" {ch1} {ch2} {prob: .4 f} {logprob: .4 f} " )
imprimir ( f" {log_likelihood=} " )
negativo_log_likelihood = -log_likelihood
imprimir ( f" {negative_log_likelihood=} " )
. mi 0,0478 -3,0408
em 0,0377 -3,2793 mm 0,0253 -3,6772 m 0,3899 -0,9418 a . 0,1960-1,6299 . o 0,0123 -4,3982 o l 0,0780 -2,5508 l yo 0,1777 -1,7278 i v 0,0152 -4,1867 v i 0,3541 -1,0383 i a 0,1381 -1,9796 a . 0,1960-1,6299 . a 0,1377 -1,9829 a v 0,0246 -3,7045 v a 0,2495 -1,3882 a . 0.1960 -1.6299 log_likelihood=tensor(-38.7856) negativo_log_likelihood=tensor(38.7856)
Podemos ver que para números con mayor probabilidad, como m a
loglihelihood, está más cerca de 0, mientras que para pares, como . e
alcanza el valor -3.0408
. Para evaluar el modelo, nos gustaría tener un número que represente la eficiencia del modelo. Para ello, podemos sumar las probabilidades logarítmicas de cada palabra. Lo que nos da -38.7856
.
El valor más bajo de la probabilidad logarítmica significa que el modelo está funcionando mal, lo que va en contra de la intuición de la definición de la función de pérdida, por lo que lo que queremos es tener una probabilidad logarítmica negativa y tratar de minimizarla. También podemos hacer uso de promediar el valor de la probabilidad logarítmica negativa. Ejecutando esto para todo el conjunto de datos ahora
log_likelihood = 0.0
n = 0
para w en palabras:
chs = [ '.' ] + lista (w) + [ '.' ]
para ch1, ch2 en zip (chs, chs[ 1 :]):
ix1 = stoi[ch1]
ix2 = stoi[ch2]
prob = P[ix1, ix2]
logprob = torch.log(prob)
log_likelihood += logprob
n += 1
imprimir ( f' {log_likelihood=} ' )
nll = -log_likelihood
imprimir ( f' {nll=} ' )
imprimir ( f' {nll/n} ' )
log_likelihood=tensor(-559891.7500)
nll=tensor(559891.7500)
2.454094171524048
Inferencia
Intentemos obtener algunas recomendaciones de nombres ingresando solo su longitud y el carácter inicial.
ch = 'a'
para i en el rango ( 10 ):
out = [ch]
mientras len (out) < 6 :
ix = stoi[ch]
p = P[ix]
ix = torch.multinomial(p, num_samples= 1 , reemplazo= Verdadero , generador=gen).item()
si itos[ix] == '.' : continuar # ignorar '.'
out.append(itos[ix])
print ( f" {i} : { '' .join(out)} " )
0: anlylr
1: ailnyh
2: amnrir
3: arrylr
4: asgirn
5: adrhyi
6: amnrkn
7: anvjyn
8: asnihd
9: ahhuln
Usando la red neuronal
https://cs231n.github.io/assets/nn1/neural_net2.jpeg
En el método anterior, creamos un modelo de bigrama extrayendo información sobre la frecuencia de bigrama del corpus y convirtiéndola en una distribución de probabilidad. Podemos mirar el problema desde una perspectiva diferente modelándolo usando una red neuronal. En mi artículo anterior sobre redes neuronales desde cero, nos encontramos con la expresión: W*X+b, donde W es el peso de una neurona individual, X es la entrada a esa neurona y b es el sesgo. Intentemos convertir los datos que tenemos en algo que pueda ser utilizado por una red neuronal. Usaremos un diagrama de una sola palabra para que sea más fácil entender lo que está pasando.
Separaremos los datos de entrenamiento en xey. Tal que y[i] es el carácter esperado para la entrada x
## Preparando el conjunto de datos de entrenamiento
Xs=[]
Ys=[]
para w en palabras[: 1 ]:
chs = [ '.' ] + lista (w) + [ '.' ]
para ch1, ch2 en zip (chs, chs[ 1 :]):
ix1 = stoi[ch1]
ix2 = stoi[ch2]
Xs.append(ix1)
Ys.append(ix2)
Xs = torch.tensor(Xs)
Ys = antorcha.tensor(Ys)
Xs, Ys
(tensor([ 0 , 5 , 13 , 13 , 1 ] ), tensor([ 5 , 13 , 13 , 1 , 0 ]))
Las redes neuronales no entienden los índices, por lo que convertiremos los índices en un vector de codificación activa.
Una codificación en caliente
Una codificación activa toma el número entero y el número de clases como entrada y crea un vector de dimensión (1 x número de clases) con un conjunto de un solo bit.
importar torch.nn.functional como F
F.one_hot(Xs[ 1 ], num_classes= 27 )
tensor ([ 0 , 0 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
0 , 0 , 0 ])
Haremos lo mismo para todo X.
Xenc = F.one_hot(Xs, num_classes=27). flotador ()
plt.imshow(Xenc)
Codificación one-hot
La imagen de arriba muestra el bit establecido en codificación one-hot para cada uno de los 5 índices de entrada [0, 5, 13, 13, 1]. Usando la expresión de red neuronal que vimos arriba W*X +b , creemos un tensor W . Para fines de demostración, tengamos una sola neurona. Esta neurona tomaría una entrada de forma (1,27) y generaría un tensor de forma (1,1) .
Formulación de redes neuronales como modelo de Bigram
Usaremos una red neuronal muy simple: una única capa lineal y sin sesgos.
Como ya estamos familiarizados con la expresión de redes neuronales.
Y = W*X + b
Inicializaremos w con valores aleatorios y tomaremos un producto escalar con tensor de entrada.
w = torch.randn((27, 1))
Xenc @ w # Producto escalar de matriz
tensor ( [[0,3378] ,
[1,0748] ,
[0,1643] ,
[0,1643] ,
[-1,3647] ])
Para cada una de las 5 Xs[i] obtenemos un tensor de salida, ahora aumentemos el número de neuronas a 27 , creando así la forma de W (27,27) y la forma de salida (1, 27).
W = torch.randn(( 27 , 27 ))
logits = Xenc @ W
logits
tensor ( [[ 1.6538 , -0.8016 , -1.0628 , 0.5207 , -0.8133 , 1.8273 , 1.5478 , -0.9953 , 0.5832 ,
0.9147 , -0.3186 , 1.9207 , 2.1282 , 1,2778 , 0,4173 , -1,0522 , 0,8507 , 0,0043 , -0,4466 , 0,9321 , -0.9775 , 0.5061 , 1.1806 , 1.1374 , 0.3909 , 0.6887 , -0.5974 ], [ 0.3734 , -0.9440 , 2.1173 , 0.4307 , -0.8386 , 0.2973 , .1508 , -1.1140 , 1.5063 , 0.4548 , 0.3725 , 0.3407 , -0.0376 , 1,6323 , -0,7899 , -0,9287 , 1,0687 , 1,3284 , 0,2154 , 0,1661 , -0,4255 , -1,4457 , 0,0080 , 1,1457 , 0,1350 , 0,6581 , 3 ] , [ -0.3007 , -0.1158 , 1.0404 , 0.3564 , -0.3480 , 0.1582 , - 0.5061 , 0.5499 , -0.1747 , -1.1983 , -0.1389 , -1.2891 , 0.8116 , -1.0019 , 1.2577 , 1.1648 , -0.0425 , 0.9556 , -0.8966 , 1 , -0,8577 , -0,2066 , -0,1065 , 0,1099 , -0,4243 , - 1,3913 , -1,1660 ] , [ -0,3007 , -0,1158 , 1,0404
, 0.3564 , -0.3480 , 0.1582 , -0.5061 , 0.5499 ,
-0.1747 , -1.1983 , -0.1389 , -1.2891 , 0.8116 , -1.0019 , 1.2577 , 1.1648 , 425 , 0,9556 , -0,8966 , 1,4691 , -0,8577 , -0,2066 , -0,1065 , 0,1099 , -0,4243 , -1,3913 , -1,1660 ] , [ 0,4654 , -0,5457 , 0,0731 , 1,3982 , -0,6943 , -0,3172 , 0,6360 , -2,0058 , -1 . 7574 , 0,1172 , 0,2438 , -0,4918 , -0,7632 , 0,4355 , 0.0921 , 1.0822 , 0.6615 , 0.2039 , -0.2937 , 0.9257 , -0.1299 , -0.1696 , -0.8557 , 0.3851 , -1.4590 , -0.7883 , -1.2211 ] ])
En el ejemplo anterior, Xenc @ W[i, j] es la activación/salida de la j-ésima neurona para la i-ésima entrada. Los valores que obtenemos de las neuronas se denominan logits o log-counts. Para convertirlos a valores parecidos a recuentos, podemos tomar exp de los logits y normalizarlos para obtener probabilidades. Toda esta operación de convertir logits en probabilidades también se conoce como operación Softmax.
recuentos = logits.exp()
problemas = recuentos/cuentas. suma ( 1 , keepdim= True )
problemas[ 0 ], problemas[ 0 ]. suma ()
(tensor([ 0,0816 , 0,0070 , 0,0054 , 0,0263 , 0,0069 , 0,0970 , 0,0734 , 0,0058 , 0,0280 ,
0,0389 , 0,0113 , 0,1065 , 0,1311 , 0,0560 , 0,0237 , 0,0054 , 0,0365 , 0,0157 , 0,0100 , 0,0396 , 0,0059 , 0,0259 , 0,0508 , 0,0487 , 0,0231 , 0,0311 , 0,0086 ]), tensor ( 1. ))
Evaluación del modelo para los 5 pares de bigramas anteriores calculando la probabilidad logarítmica y la probabilidad logarítmica negativa.
nlls = torch.zeros( 5 )
para i en el rango ( 5 ):
ix = Xs[i].item()
iy = Ys[i].item()
print ( f"Entrada a la red neuronal {itos[ix]} " )
print ( f"Salida esperada de la red neuronal {itos[iy]} " )
probabilidad_y = probs[i, iy]
print ( f"Probabilidad de {itos[iy]} : {probabilidad_y} " )
log_likelihood_y = probabilidad_y.log ()
print ( f"log probabilidad {log_likelihood_y} " )
negativo_log_likelihood_y = -log_likelihood_y
print ( f"log probabilidad negativa {negative_log_likelihood_y} " )
nlls[i] = negativo_log_likelihood_y
pérdida = nlls.mean()
print ( f"Promedio logarítmico negativo {pérdida} " )
Entrada a la red neuronal. Salida
esperada de la red neuronal e Probabilidad de e: 0,09700655937194824 probabilidad logarítmica -2,3329765796661377 probabilidad logarítmica negativa 2,3329765796661377 Entrada a la red neuronal e Salida esperada de la red neuronal m Probabilidad de m: 0,10330255329608917 probabilidad logarítmica -2,27 00932025909424 probabilidad logarítmica negativa 2.2700932025909424 Entrada a la red neuronal m Salida esperada de la red neuronal m Probabilidad de m: 0,01063988171517849 probabilidad logarítmica -4,543146133422852 probabilidad logarítmica negativa 4,543146133422852 Entrada a la red neuronal m Salida esperada de la red neuronal a Probabilidad de a: 0,025809239596128464 probabilidad logarítmica -3,657022 714614868 probabilidad logarítmica negativa 3.657022714614868 Entrada a la red neuronal a Salida esperada de la red neuronal netork. Probabilidad de .: 0,051668521016836166 probabilidad logarítmica -2,9629065990448 probabilidad logarítmica negativa 2,9629065990448 Probabilidad logarítmica negativa promedio 3,153228998184204
Obtenemos una probabilidad logarítmica promedio de 3,15, lo cual no es muy bueno. Intentemos mejorar este modelo y entrenarlo en todo el conjunto de datos.
Usando todo el conjunto de datos
Creando conjunto de datos
# crear el conjunto de datos
xs, ys = [], []
para w en palabras:
chs = [ '.' ] + lista (w) + [ '.' ]
para ch1, ch2 en zip (chs, chs[ 1 :]):
ix1 = stoi[ch1]
ix2 = stoi[ch2]
xs.append(ix1)
ys.append(ix2)
xs = torch.tensor(xs)
ys = torch.tensor(ys)
num = xs.nelement()
print ( 'número de ejemplos: ' , num)
# inicializa la 'red'
g = torch.Generator().manual_seed( 2147483647 )
W = torch.randn(( 27 , 27 ), generador=g, requiere_grad= Verdadero )
número de ejemplos : 228146
Bucle de entrenamiento
Reuniendo lo que discutimos anteriormente
- Pase directo para convertir el índice de caracteres de entrada a una codificación activa, calcular el producto escalar con pesos de redes neuronales, realizar softmax para obtener probabilidades de logits y calcular nll
- Pase hacia atrás que implica establecer el gradiente en Ninguno y realizar propagación hacia atrás a través de la red neuronal
- Actualización del parámetro wrt a los gradientes
# descenso de gradiente
para k en el rango ( 50 ):
# pase hacia adelante
xenc = F.one_hot(xs, num_classes= 27 ). float () # entrada a la red: codificación one-hot
logits = xenc @ W # predecir recuentos de registros
= logits.exp() # recuentos, equivalente a N
probs = recuentos/cuentas. sum ( 1 , keepdims= True ) # probabilidades de la siguiente
pérdida de carácter = -probs[torch.arange(num), ys].log().mean() + 0.01 *(W** 2 ).mean()
print ( loss.item())
# paso hacia atrás
W.grad = Ninguno # establece en cero el gradiente
loss.backward()
# actualiza
W.data += - 50 * W.grad
3.7686190605163574
3.3788065910339355
3.16109037399292
3.0271859169006348
2.9344840049743652
2.867231607437134
2.8166539669036865
2.777146100997925
2.745253801345825
2.7188303470611572
2.696505308151245
2.6773722171783447
2.6608052253723145
2.6463513374328613
2.633665084838867
2.622471570968628
2.6125476360321045
2.6037068367004395
2.595794916152954
2.5886809825897217
2.5822560787200928
2.5764293670654297
2.5711236000061035
2.566272735595703
2.5618226528167725
...
2.5137410163879395
2.512698173522949
2.511704444885254
2.5107581615448
Una cosa interesante a tener en cuenta en el código anterior es
pérdida = -probs[torch.arange(num), ys].log().mean() + 0.01*(W**2).mean()
especialmente el 0.01*(W**2).mean() . Esto también se denomina factor de regularización y se utiliza para suavizar el modelo. Esencialmente, lo que controlamos es la suavidad de los pesos del modelo; al mantener la constante grande, forzamos que la media de W**2 sea pequeña y tenga valores más cercanos entre sí para reducir la pérdida, y al mantener la constante pequeña, reducir la contribución de los pesos de W, permitiendo así que W también tenga pesos mayores.
Inferencia
# finalmente, muestra del modelo de 'red neuronal'
g = torch.Generator().manual_seed( 2147483647 )
ch = 'a'
para i en el rango ( 10 ):
out = [ch]
ix = stoi[ch]
while len ( fuera) < 6 :
xenc = F.one_hot(torch.tensor([ix]), num_classes= 27 ). float ()
logits = xenc @ W # predecir recuentos de registros
recuentos = logits.exp() # recuentos, equivalente a N
p = recuentos/cuentas. suma ( 1 , keepdims= True ) # probabilidades para el siguiente carácter
ix = torch.multinomial(p, num_samples= 1 , replacement= True , generador=g).item()
if ix == 0 :
continuar
out.append(itos[ ix])
imprimir ( f" {i} : { '' .unirse(fuera)} " )
0 : asonde
1 : adiana
2 : asahpx
3 : anayan
4 : ankohi
5 : antoli
6 : araste
7 : azzada
8 : aheiau
9 : ayanil
Conclusión
Ahí tienes. Un modelo de bigram que aprende de la lista de entrada de nombres y se evalúa a sí mismo utilizando el método de probabilidad logarítmica. Dado que el método Bigram sólo tiene en cuenta las relaciones locales entre pares y, a su vez, ignora el contexto de la palabra en su conjunto, es un excelente punto de partida para aprender a modelar el lenguaje. Con el enfoque anterior de usar una red neuronal, logramos una pérdida de 2,5107581615448, que no es mejor que la que obtuvimos usando un modelo probabilístico en el que contamos las ocurrencias. Esto es de esperarse debido a la naturaleza del problema de bigram. Dado que, en cualquier momento, tenemos información muy limitada (carácter anterior), el enfoque manual de contar y usar la frecuencia para predecir el siguiente carácter resulta ser lo mismo que usar métodos basados en gradientes para optimizar los pesos de la red neuronal. Por tanto, el resultado de ambos métodos sigue siendo el mismo. Una cosa a tener en cuenta aquí es que, aunque ambos modelos funcionan igual de mal, el modelo de red neuronal es muy flexible en términos de entrada. Podemos ampliar la entrada para incluir un montón de caracteres si queremos; por otro lado, el método de distribución de frecuencia no escala bien con el aumento en la cantidad de caracteres de entrada.
Fuente: https://pub.towardsai.net/language-modeling-from-scratch-e2a336e092fa
Debe estar conectado para enviar un comentario.