TransWikia.com

¿Usar un objeto zip unzip el contenido de forma permanente?

Stack Overflow en español Asked on January 6, 2021

Tengo dos conjuntos de datos:

x:  [(1, -14580.0, -3389.0), (1, -18403.0, 19052.0), (1, -31845.0, -39952.0), (1, -10634.0, -220.0), (1, -9180.0, 18411.0), (1, -27952.0, -8415.0), (1, -25686.0, 8325.0), (1, -22570.0, 8041.0), (1, -34124.0, -5791.0), (1, -20119.0, 2468.0), (1, -12164.0, 53333.0), (1, -14587.0, -66443.0), (1, -5928.0, -7212.0), (1, -25489.0, 5610.0), (1, -33283.0, -18741.0), (1, -18729.0, 1234.0), (1, -25495.0, -361.0), (1, -33315.0, -6149.0), (1, -28093.0, 11441.0), (1, -36642.0, -3513.0), (1, -26365.0, 8214.0), (1, -36366.0, 10423.0)] 
ts: 
 [131479.0, 128090.0, 147142.0, 107190.0, 106970.0, 125381.0, 116966.0, 125291.0, 133332.0, 127541.0, 130009.0, 183342.0, 116899.0, 109687.0, 115297.0, 96556.0, 97790.0, 97429.0, 91280.0, 102721.0, 99208.0, 107422.0, 117845.0, 168755.0, 110971.0, 84198.0, 82014.0, 77827.0, 72295.0, 64114.0, 63187.0, 66079.0, 72843.0, 71056.0]
value:  422080882435.25397

y quiero hacer una regresión lineal múltiple para predecir el beta.

import random

def dot(v,w):
  return sum(v_i*w_i for v_i, w_i in zip(v,w))

def predict(x_i, beta):
  """assumes that the first element of each x_i is one"""
  return dot(x_i, beta)

def error(x_i, y_i, beta):
  return y_i - predict(x_i, beta)

def squarred_error(x_i, y_i, beta):
  return error(x_i, y_i, beta)**2

def squarred_error_gradient(x_i, y_j, beta):
  return [-2 * x_ij * error(x_i, y_j, beta) 
          for x_ij in x_i]

def in_random_order(data):
  """generator that returns the elements if data in random order"""
  indexes = [i for i, _ in enumerate(data)] # create a list of indexes
  random.shuffle(indexes) # suffle them
  for i in indexes:
    # print("data[i]: ", data[i])
    return data[i]

def minimize_stochastic(target_fn, gradient_fn, x,y, theta_0, alpha_0=0.01):
  data = zip(x,y)
  theta = theta_0  #initial guess
  almpah = alpha_0  # initial step size
  min_theta, min_value = None, float('inf') # the minimum so far
  iterations_with_no_improvment = 0

  # if we ever go 100 iterations with no improvment, stop
  while iterations_with_no_improvment < 100:
    value = sum(target_fn(x_i, y_i, theta) for x_i, y_i in data)
    print("value: ", value)

    if value < min_value:
      # if we've found a new minimum, remember it
      # and go back to the original step size
      min_theta, min_value = theta, value
      iterations_with_no_improvment = 0
      alpha = alpha_0
    else:
      # otherwise we're not improving, so try shrinking the step size
      iterations_with_no_improvment +=1
      alpha *=0.9

    # and take a gradient step for each of the data points
    # print("data: ", [x for x in data])
    # print("data: ", data)
    for x_i, y_i in in_random_order(data):
      gradient_i = gradient_fn(x_i, y_i, theta)
      theta = vector_substract(theta, scalar_multiply(alpha_gradient_i))
  return min_theta

def estimate_beta(x,y):
  beta_initial = [random.random() for x_i in x[0]]
  return minimize_stochastic(squarred_error,
                             squarred_error_gradient,
                             x,y,
                             beta_initial,
                             0.001)

random.seed(0)
beta = estimate_beta(x, ts)

Sin embargo, parece que in_random_order(data) no funciona porque el data es un zip que esta None. En efecto obtengo:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-109-c0f3fa276b50> in <module>()
     69 
     70 random.seed(0)
---> 71 beta = estimate_beta(x, ts)

1 frames
<ipython-input-109-c0f3fa276b50> in minimize_stochastic(target_fn, gradient_fn, x, y, theta_0, alpha_0)
     54     # print("data: ", [x for x in data])
     55     # print("data: ", data)
---> 56     for x_i, y_i in in_random_order(data):
     57       gradient_i = gradient_fn(x_i, y_i, theta)
     58       theta = vector_substract(theta, scalar_multiply(alpha_gradient_i))

TypeError: 'NoneType' object is not iterable

One Answer

Qué significa el error?

Ya que el error salta en la línea que dice:

for x_i, y_i in in_random_order(data):

el problema es que in_random_order(data) ha generado como resultado None (o un iterable cuyo primer elemento es None en cuyo caso fallaría el desempaquetado con el mismo mensaje de error).

¿Por qué sucede?

¿Por qué estará devolviendo None esa la función? Veamos su código:


def in_random_order(data):
  """generator that returns the elements if data in random order"""
  indexes = [i for i, _ in enumerate(data)] # create a list of indexes
  random.shuffle(indexes) # suffle them
  for i in indexes:
    return data[i]

Vemos que debería estar devolviendo data[i], pero el data que le estás pasando se inicializa en esta otra línea, de otra función:

data = zip(x,y)

O sea que data no es una lista, sino el resultado de un zip(). Ocurre que este resultado, aunque es un iterable, no es indexable. No puedes usarlo como si fuera una lista para tratar de obtener su resultado i-ésimo, como puedes comprobar con este experimento:

>>> test = zip(x, y)
>>> test[1]
TypeError: 'zip' object is not subscriptable

Entonces la función debería haber generado esa excepción al llegar al return data[i]. Sin embargo no ha generado tal excepción sino que ha retornado None¿cómo pudo suceder esto?

Sólo hay una forma. No llegó nunca a ejecutar esa línea. Y ésto solo puede suceder si este bucle:

  for i in indexes:
    return data[i]

no itera ni una sola vez. Es decir, si el iterable indexes está vacío. Esto es de hecho lo que ocurre como puedes comprobar si añades un print(indexes) delante del bucle.

Esto significa por tanto que el valor de data que le llega es un objeto zip() agotado.

¿Qué es un iterable agotado?

Es un iterable por el que ya has iterado una vez. Los iterables en python, una vez llegas a su final, no puedes volver a iterar por ellos. Rápida demostración:

>>> a = zip(x, y)
>>> test = [ v for v in a ]
# La línea anterior no da problemas. `a` es un iterable, y podemos iterar por él
# Si imprimiéramos ahora el valor de `test` veríamos todos sus elementos
# No lo hago porque es largo

>>> len(test)
22

>>> test2 = [ v for v in a ]
# Esta línea tampoco daría problemas, pero ya que el objeto zip se ha agotado
# no se realizará niguna iteración. test2 resulta vacío

>>> len(test2)
0

Por tanto tu código itera sobre data en algún momento antes de pasarlo como parámetro a in_random_order().

Efectivamente vemos que lo hace en la línea:

    value = sum(target_fn(x_i, y_i, theta) for x_i, y_i in data)

Es aquí donde data queda agotado, y por tanto al pasarlo después como parámetro a in_random_order() ya no se obtienen más elementos al iterar sobre data.

¿Y si quiero iterar más veces? ¿no se puede?

Esto nos lleva a tu pregunta. Seguramente tú te sospechabas que estaba pasando algo de esto pues preguntabas "usar un objeto zip de forma permanente", que entiendo que viene a significar que quieres iterar sobre él varias veces.

Ya hemos visto que sobre un iterador sólo se puede iterar una vez. Pero si a partir del iterador construyes una lista, matas dos pájaros de un tiro ya que si data fuese una lista:

  • Podrías iterar múltiples veces por ella (porque cada vez que lo intentas Python construye para ella un nuevo iterador)
  • Podrías acceder a data[i], resolviendo otro problema que tenías antes.

Así pues, basta cambiar esta asignación:

data = zip(x, y)

por esta otra:

data = list(zip(x, y))

El constructor list() itera por los contenidos del objeto zip(), creando con ellos una lista.

Pero hay más errores

Con ese cambio ahora rompe por otra razón:

---> 57     for x_i, y_i in in_random_order(data):
     58       gradient_i = gradient_fn(x_i, y_i, theta)
     59       theta = vector_substract(theta, scalar_multiply(alpha_gradient_i))

ValueError: too many values to unpack (expected 2)

Ahora resulta que return data[i] tiene más valores de los que se esperaban (se esperaban 2, uno para x_i y otro para y_i. En vez de eso se obtienen tres. ¿Por qué?

En este caso el error es más sutil. Y es que tu función in_random_order() no hace en realidad lo que tú querías. Pretendías que devolviera los elementos de data en orden aleatorio, pero en realidad sólo te devuelve el primer elemento de data, debido a que en este bucle:

  for i in indexes:
    return data[i]

retornas inmediatamente en la primera iteración. Así que la función en realidad devuelve data[indexes[0]] y termina. Lo que devuelve será una tupla de este estilo: ((1, 2, 3), 4) con un elemento de x y otro de y.

El bucle for x_i, y_i in .... iterará sobre esa tupla, que tiene dos elementos, por tanto hará dos iteraciones. En la primera de ellas encontrará el valor (1,2,3) que intentará asignar a x_i, y_i y por eso rompe.

El problema aquí es que tu función in_random_order() debería ser un generador, pero al no contener instrucciones yield es una función normal.

Es decir, debes cambiar el return por un yield:

def in_random_order(data):
  """generator that returns the elements if data in random order"""
  indexes = [i for i, _ in enumerate(data)] # create a list of indexes
  random.shuffle(indexes) # suffle them
  for i in indexes:
    yield data[i]

Otra versión más simple sería que en lugar de ir devolviendo con yield los elementos cambiados de orden, directamente devuelva una lista completa con los elementos desordenados. Es decir, puedes sacar una copia de data, desordenarla, y devolverla:

def in_random_order(data):
  aux = data[:]
  random.shuffle(aux)
  return aux

Cualquiera de estas soluciones arregla todos los problemas que se pueden arreglar de momento, pues aparece uno nuevo `NameError: name 'vector_substract' is not defined, pero ese ya no lo puedo solucionar con el código que proporcionas.

Correct answer by abulafia on January 6, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP