Formación Módulo 5 de 11

M05

Python mínimo para DeepRacer

Todo el Python que necesitas para escribir tu recompensa

  • Duración: 60 min
  • Todos
  • Vídeo + test de 5 preguntas

Al terminar sabrás…

  • Usar variables, números (int y float), booleanos y comentarios en Python
  • Tomar decisiones con if, elif y else combinando and, or y not
  • Escribir una función que reciba params y devuelva un float con return
  • Leer datos de diccionarios y listas, como params['speed'] o waypoints[closest_waypoints[1]]
  • Detectar los errores típicos y probar una función antes de entrenar con Validate

Vídeo: Python mínimo para DeepRacer en 5 minutos

Python mínimo para DeepRacer en 5 minutosEl vídeo de este módulo está en preparación. Mientras tanto, tienes el guion completo justo debajo.
Guion del vídeo (texto)

¿Por qué Python?. Para enseñar a tu coche a conducir tendrás que escribir una función de recompensa, y se escribe en Python. Tranquilidad: no hace falta ser programador. Con variables, decisiones, funciones, diccionarios y listas tienes de sobra. En este vídeo vas a ver justo eso, con ejemplos sacados de las funciones oficiales de DeepRacer, para que cuando las abras en la plataforma entiendas cada línea.

Variables y tipos de datos. Una variable es como una caja con una etiqueta: guardas un valor y luego lo usas por su nombre. Los números enteros se llaman int y los que tienen decimales se llaman float. Ojo, los decimales se escriben con punto, no con coma. Los booleanos solo pueden valer verdadero o falso, y en Python se escriben con la primera letra en mayúscula. Lo que va detrás de la almohadilla es un comentario y Python lo ignora.

Decisiones con if, elif y else. Para decidir usamos if, que significa si, elif, que significa si no pero si, y else, que significa en cualquier otro caso. Python mira las condiciones de arriba abajo y se queda con la primera que se cumple. Fíjate en dos detalles: cada condición termina con dos puntos, y lo que va dentro está desplazado cuatro espacios. Esa sangría no es decoración, es parte del lenguaje.

Combinar condiciones. A veces una condición no basta. Con and pides que se cumplan las dos a la vez, por ejemplo ir rápido y además con el volante recto. Con or te vale cualquiera de las dos. Y not le da la vuelta a un valor: si el coche no tiene todas las ruedas en la pista, la condición se cumple y le damos una recompensa casi nula.

Funciones y return. Una función es como una máquina de zumos: le metes algo, trabaja y te devuelve un resultado. Se crea con def, se le pone un nombre y entre paréntesis lo que recibe. En DeepRacer recibe un paquete de datos llamado params. La palabra return devuelve el resultado y termina la función. Este es el esqueleto oficial, y por eso todos los ejemplos acaban convirtiendo la recompensa en un número decimal.

Diccionarios, la ficha del coche. Un diccionario es como la ficha de un personaje de videojuego: cada dato tiene un nombre y un valor. El diccionario params trae la velocidad, el ángulo del volante, la distancia al centro y muchos datos más. Para leer uno escribes params, abres corchetes y pones el nombre entre comillas, exactamente igual que en la documentación. Si te equivocas en una sola letra, la función falla.

Listas e índices. Una lista es una fila de valores en orden, y lo más importante es que se empieza a contar en cero. El parámetro closest waypoints es una lista con dos números: la posición del punto de referencia que queda detrás del coche y la del que queda delante. Si lo usas dentro de la lista de waypoints, obtienes las coordenadas del siguiente punto. Se lee de dentro hacia fuera, como abrir una muñeca rusa.

Errores típicos y Validate. Casi todos los errores de principiante son los mismos: la sangría descolocada, olvidar los dos puntos, usar un igual cuando querías comparar, escribir true en minúscula o terminar sin return. Antes de gastar horas de entrenamiento, pulsa el botón Validate del editor. La plataforma revisa tu código y te avisa si hay un error de sintaxis o una importación no permitida. Arreglarlo ahí cuesta segundos.

Tu coche aprende a conducir gracias a una función de recompensa que escribes tú, y esa función se escribe en Python. La buena noticia es que no necesitas aprender todo Python: con media docena de ideas puedes leer y modificar cualquier ejemplo oficial de DeepRacer. Este módulo es exactamente esa media docena, con ejercicios para practicar. Si ya programas, échale un vistazo rápido a los errores típicos y salta a la actividad.

Variables, números y comentarios

Una variable es una caja con una etiqueta. Guardas un valor dentro y después lo usas por su nombre.

# Todo lo que va detrás de la almohadilla es un comentario: Python lo ignora
vueltas = 3            # int: número entero
velocidad = 1.5        # float: número con decimales
en_pista = True        # booleano: solo puede ser True o False
casi_cero = 1e-3       # float escrito en notación científica: 0.001

Tres cosas que conviene grabarse:

  • Los decimales se escriben con punto: 1.5, nunca 1,5.
  • True y False van con la primera letra en mayúscula. true es un error.
  • 1e-3 significa "1 por 10 elevado a menos 3", es decir, 0.001. Los ejemplos oficiales lo usan como "recompensa casi nula" porque, según su propio comentario, el cero se reserva para cuando el coche se sale o choca.

Con los números puedes hacer las operaciones de siempre: +, -, * y /. Hay tres que verás en los ejemplos oficiales:

Escribes Significa Ejemplo
reward *= 0.5 Multiplica la variable por 0,5 y guárdala Si reward valía 1.0, ahora vale 0.5
reward += 10.0 Súmale 10 a la variable Si valía 1.0, ahora vale 11.0
steps % 100 Resto de dividir entre 100 300 % 100 da 0; 250 % 100 da 50

Los comentarios no son un adorno: dentro de dos semanas no recordarás por qué pusiste 0.8 y no 0.7. Escríbelo.

Comparaciones y decisiones

Las comparaciones dan como resultado un booleano (True o False):

Operador Pregunta que hace
< y > ¿Es menor? ¿Es mayor?
<= y >= ¿Es menor o igual? ¿Mayor o igual?
== ¿Son iguales? (dos signos de igual)
!= ¿Son distintos?

Para tomar decisiones se usan if (si), elif (si no, pero si...) y else (en cualquier otro caso). Piensa en las notas de un examen: si sacas 9 o más es sobresaliente; si no, pero sacas 7 o más, notable; en otro caso, "a seguir trabajando".

distance_from_center = 0.2
track_width = 0.6

if distance_from_center <= 0.1 * track_width:
    reward = 1.0
elif distance_from_center <= 0.5 * track_width:
    reward = 0.5
else:
    reward = 1e-3

Python mira las condiciones de arriba abajo y ejecuta solo el primer bloque cuya condición se cumple. Aquí 0.1 * 0.6 es 0,06 y 0.5 * 0.6 es 0,3; como 0,2 no es menor que 0,06 pero sí que 0,3, reward vale 0.5. El orden importa: si pones la condición más amplia primero, las demás nunca se ejecutarán.

Fíjate también en los dos puntos al final de cada if, elif y else, y en la sangría (cuatro espacios) de las líneas de dentro. La sangría le dice a Python qué líneas pertenecen a cada bloque.

and, or y not

Operador Se cumple cuando... Ejemplo
and se cumplen las dos condiciones speed > 1.0 and all_wheels_on_track
or se cumple al menos una is_offtrack or is_crashed
not la condición NO se cumple not all_wheels_on_track

El ejemplo oficial "quedarse entre los dos bordes" usa and y paréntesis para agrupar:

all_wheels_on_track = True
track_width = 0.6
distance_from_center = 0.2

if all_wheels_on_track and (0.5*track_width - distance_from_center) >= 0.05:
    reward = 1.0

Funciones y return

Una función es una máquina de zumos: le metes algo, hace su trabajo y te devuelve un resultado.

def doble(numero):
    resultado = numero * 2
    return resultado

print(doble(4))   # muestra 8
  • def crea la función; doble es su nombre y numero es lo que recibe.
  • Todo lo que pertenece a la función va sangrado.
  • return devuelve el resultado y termina la función en ese momento. Lo que haya debajo de un return ejecutado ya no se ejecuta.

DeepRacer llama a tu función en cada paso de la simulación, le pasa un diccionario llamado params y espera que le devuelvas un número decimal. Este es el esqueleto oficial:

def reward_function(params) :

    reward = ...

    return float(reward)

float(reward) convierte el resultado en float aunque hayas escrito reward = 1. Por eso casi todos los ejemplos oficiales terminan así: es un seguro barato.

Diccionarios y listas: cómo leer params

Diccionarios

Un diccionario guarda pares de clave y valor, como la ficha de un personaje de videojuego (nombre, vida, nivel...). Se escribe entre llaves y se lee con corchetes y el nombre de la clave entre comillas:

params = {
    'speed': 1.2,
    'all_wheels_on_track': True,
    'steering_angle': -12.0,
    'closest_waypoints': [7, 8],
}

speed = params['speed']                      # 1.2
on_track = params['all_wheels_on_track']     # True

La plataforma te da un params con más de veinte claves (las verás todas en el módulo de la función de recompensa). El nombre de la clave tiene que ser exacto: params['Speed'] o params['velocidad'] fallan.

La costumbre de los ejemplos oficiales es leer todo al principio de la función y guardarlo en variables con el mismo nombre. Así el resto del código queda limpio.

Listas e índices

Una lista es una fila de valores en orden, entre corchetes. Cada valor tiene una posición o índice, y se empieza a contar en cero:

colores = ['rojo', 'verde', 'azul']
print(colores[0])    # rojo
print(colores[2])    # azul
print(colores[-1])   # azul: el -1 es el último

En DeepRacer hay dos listas que se usan juntas:

  • waypoints: los puntos de referencia de la línea central de la pista, cada uno con sus coordenadas (x, y).
  • closest_waypoints: dos índices, el del waypoint que queda detrás del coche y el del que queda delante.
waypoints = [(0.0, 0.0), (1.0, 0.0), (2.0, 0.5), (3.0, 1.5)]
closest_waypoints = [1, 2]

next_point = waypoints[closest_waypoints[1]]
print(next_point)       # (2.0, 0.5)
print(next_point[0])    # 2.0, la x
print(next_point[1])    # 0.5, la y

waypoints[closest_waypoints[1]] se lee de dentro hacia fuera, como abrir una muñeca rusa: primero closest_waypoints[1] vale 2, y después waypoints[2] es (2.0, 0.5).

Matemáticas mínimas: abs, atan2 y degrees

abs devuelve el número sin signo. En DeepRacer el ángulo de dirección es negativo al girar a la derecha y positivo a la izquierda. Si solo te importa cuánto gira, y no hacia dónde, usas abs:

steering_angle = -22.0
abs_steering = abs(steering_angle)   # 22.0

math.atan2 y math.degrees sirven para calcular hacia dónde apunta la pista. Imagina una flecha que va del waypoint de atrás al de delante: math.atan2(dy, dx) te da el ángulo de esa flecha en radianes, y math.degrees lo pasa a grados para poder compararlo con heading, la orientación del coche. Estas líneas salen del ejemplo oficial de waypoints:

import math

prev_point = (1.0, 0.0)
next_point = (2.0, 1.0)

track_direction = math.atan2(next_point[1] - prev_point[1], next_point[0] - prev_point[0])
track_direction = math.degrees(track_direction)
print(track_direction)   # 45.0

import math va fuera de la función, al principio del archivo. Las librerías permitidas en la plataforma son math, random, numpy, scipy y shapely.

Errores típicos y cómo probar tu función

Este código tiene cinco errores. Intenta encontrarlos antes de leer la lista:

def reward_function(params)
    speed = params['speed']
    if speed = 1.0:
        reward = 1.0
      else:
        reward = 0.5
    on_track = true
  1. Faltan los dos puntos tras def reward_function(params).
  2. if speed = 1.0 usa un igual; para comparar hacen falta dos (==).
  3. El else tiene una sangría distinta de la del if: deben estar alineados.
  4. true va con mayúscula: True.
  5. No hay return: la función devolvería None, que no es un número.

Otros clásicos: mezclar tabuladores y espacios, olvidar cerrar un paréntesis o un corchete, escribir mal una clave de params y poner el import dentro de la función.

Probar en un intérprete de Python online

Cualquier intérprete de Python online (o el que tengas en tu ordenador) sirve para comprobar la lógica. Pega tu función, inventa un params de prueba y muestra el resultado:

def reward_function(params):
    if not params['all_wheels_on_track']:
        return float(1e-3)
    return float(1.0)

prueba = {'all_wheels_on_track': False}
print(reward_function(prueba))   # 0.001

Cambia los valores de prueba y comprueba que la función responde como esperas en cada caso.

Probar en la plataforma con Validate

En el último paso del asistente de creación de modelos, Customize reward function (personalizar la función de recompensa), hay un editor de Python con tres botones: Reward function examples (funciones de ejemplo), Validate (validar) y Reset (restablecer). Al pulsar Validate, la plataforma revisa tu código en un contenedor aislado y sin red. Si algo va mal, te indica un SYNTAX_ERROR (error de escritura), un IMPORT_ERROR (librería no permitida) o un TEST_FAILURE (la función falla al ejecutarse).

Actividad práctica

Necesitas unos 25 minutos, un intérprete de Python online y tu cuenta de la plataforma DeepRacer on AWS.

  1. Resuelve los ejercicios de abajo en papel o en el intérprete. Comprueba tus respuestas con las soluciones.
  2. Copia en el intérprete tu solución del ejercicio 5 y pruébala con al menos tres params de prueba distintos.
  3. Entra en la plataforma y ve a Your models (tus modelos) y después a Create model (crear modelo). Avanza por el asistente hasta el paso Customize reward function.
  4. Pega tu función del ejercicio 5 en el editor y pulsa Validate.
  5. Borra a propósito los dos puntos de una línea y vuelve a pulsar Validate. Lee el mensaje de error.
  6. Pulsa Reset para recuperar la función por defecto. Si no vas a entrenar ahora, sal del asistente sin pulsar Train your model.

Ejercicios

  1. ¿De qué tipo es cada valor: 3, 3.0, True, 1e-3?
  2. Con speed = 1.4, ¿qué valor tendrá reward tras este código?
speed = 1.4
if speed < 1.0:
    reward = 0.2
elif speed < 1.5:
    reward = 0.6
else:
    reward = 1.0
  1. ¿Qué devuelve abs(params['steering_angle']) si params['steering_angle'] vale -18.5?
  2. Con waypoints = [(0, 0), (2, 0), (4, 1), (5, 3)] y closest_waypoints = [2, 3], ¿qué vale waypoints[closest_waypoints[0]]? ¿Y waypoints[closest_waypoints[1]][1]?
  3. Escribe una reward_function completa que devuelva 1e-3 si el coche no tiene las cuatro ruedas en la pista, 0.5 si gira más de 20 grados (hacia cualquier lado) y 1.0 en el resto de casos.

Soluciones

  1. 3 es int, 3.0 es float, True es booleano y 1e-3 es float (vale 0,001).
  2. 0.6. La primera condición (1.4 < 1.0) es falsa; la segunda (1.4 < 1.5) es verdadera, así que se ejecuta ese bloque y el else ya no se mira.
  3. 18.5.
  4. waypoints[2] es (4, 1). waypoints[3] es (5, 3) y su elemento [1] (la y) es 3.
  5. Una solución posible:
def reward_function(params):
    # Leer los parámetros que necesitamos
    all_wheels_on_track = params['all_wheels_on_track']
    abs_steering = abs(params['steering_angle'])

    if not all_wheels_on_track:
        reward = 1e-3
    elif abs_steering > 20:
        reward = 0.5
    else:
        reward = 1.0

    return float(reward)

Resumen

  • Una variable guarda un valor; los float llevan punto decimal y los booleanos son True o False.
  • if, elif y else se evalúan de arriba abajo, con dos puntos al final y sangría dentro; and, or y not combinan condiciones.
  • Tu función se llama reward_function, recibe params y termina con return float(reward).
  • params['clave'] lee un dato del diccionario y las listas empiezan en el índice 0: waypoints[closest_waypoints[1]] es el siguiente punto de la pista.
  • Antes de entrenar, prueba la lógica en un intérprete y pulsa Validate en la plataforma.

Test final

Comprueba lo que has aprendido. Necesitas 4 aciertos de 5 para superar el módulo.