Hace aproximadamente un año,
Intel Movidius lanzó un dispositivo para la inferencia eficiente de redes neuronales convolucionales: Movidius Neural Compute Stick (NCS). Este dispositivo permite el uso de redes neuronales para el reconocimiento o detección de objetos en condiciones de consumo de energía limitado, incluso en tareas de robótica. NCS tiene una interfaz USB y no consume más de 1 vatio. En este artículo, hablaré sobre la experiencia de usar NCS con la Raspberry Pi para la tarea de detectar rostros en video, incluyendo tanto la capacitación del detector Mobilenet-SSD como el lanzamiento en Raspberry.
Todo el código se puede encontrar en mis dos repositorios:
capacitación de detectores y
demostraciones de detección de rostros .
En mi primer artículo, ya escribí sobre la detección de rostros usando NCS: luego estábamos hablando de un detector
YOLOv2 , que convertí del formato
Darknet al formato
Caffe , y luego lo lancé en NCS. El proceso de conversión resultó no ser trivial: dado que estos dos formatos definen la última capa del detector de diferentes maneras, la salida de la red neuronal tuvo que analizarse por separado, en la CPU, utilizando un código de Darknet. Además, este detector no me satisfizo tanto en velocidad (hasta 5.1 FPS en mi computadora portátil) como en precisión; más tarde me convencí de que debido a su sensibilidad a la calidad de imagen era difícil obtener un buen resultado en Raspberry Pi.
Al final, decidí entrenar mi detector. La elección recayó en el detector
SSD con el codificador
Mobilenet : la convolución ligera de Mobilenet le permite alcanzar alta velocidad sin pérdida de calidad, y el detector SSD no es inferior a YOLO y funciona en el NCS de fábrica.
Cómo funciona el detector Mobilenet-SSDComencemos con Mobilenet. En esta arquitectura esta completa
la convolución (en todos los canales) se reemplaza por dos convoluciones ligeras: primero
por separado para cada canal y luego completar
convolución Después de cada convolución, se
usan BatchNorm y Non-linearity (ReLU). La primera convolución de red que recibe una imagen como entrada generalmente se deja completa. Esta arquitectura puede reducir significativamente la complejidad de los cálculos debido a una ligera disminución en la calidad de las predicciones. Hay una
opción más avanzada , pero aún no la he probado.
SSD (Single Shot Detector) funciona así: en las salidas de varios codificadores de convolución se cuelgan en dos
capa convolucional: uno predice las probabilidades de las clases, el otro, las coordenadas del cuadro delimitador. Hay una tercera capa que proporciona las coordenadas y las posiciones de los cuadros predeterminados en el nivel actual. El significado es: la salida de cualquier capa se divide naturalmente en celdas; más cerca del final de la red neuronal, se están volviendo más pequeños (en este caso, debido a la convolución con
stride=2
), y el campo de visión de cada célula aumenta. Para cada celda en cada una de varias capas seleccionadas, establecemos varios marcos predeterminados de diferentes tamaños y con diferentes relaciones de aspecto, y utilizamos capas convolucionales adicionales para corregir coordenadas y predecir las probabilidades de clase para cada uno de esos marcos. Por lo tanto, un detector SSD (como YOLO) siempre considera el mismo número de cuadros. El mismo objeto se puede detectar en diferentes capas: durante el entrenamiento, la señal se envía a todos los cuadros que se cruzan con el objeto con bastante fuerza, y durante la aplicación, las detecciones se combinan utilizando la supresión no máxima (NMS). La capa final combina las detecciones de todas las capas, considera sus coordenadas completas, corta el umbral de probabilidad y produce NMS.
Entrenamiento de detectores
Arquitectura
El código para entrenar el detector se encuentra
aquí .
Decidí usar el
detector Mobilenet-SSD listo para usar entrenado en el
PASCAL VOC0712 y entrenarlo para detectar rostros. En primer lugar, ayuda a entrenar la red mucho más rápido y, en segundo lugar, no tiene que reinventar la rueda.
El proyecto original incluía el script
gen.py
, que literalmente recopilaba el archivo de modelo
.prototxt
, sustituyendo los parámetros de entrada. Lo transferí a mi proyecto, ampliando un poco la funcionalidad. Este script le permite generar cuatro tipos de archivos de configuración:
- tren : en la entrada - una base de entrenamiento LMDB, en la salida - una capa con el cálculo de la función de pérdida y sus gradientes, hay BatchNorm
- prueba : en la entrada, la base de prueba LMDB, en la salida, la capa con el cálculo de la calidad (precisión media promedio), hay BatchNorm
- desplegar : en la entrada - la imagen, en la salida - la capa con predicciones, falta BatchNorm
- deploy_bn : en la entrada, la imagen, en la salida, la capa con predicciones, hay BatchNorm
Agregué la última opción más tarde, para que en las secuencias de comandos pueda cargar y convertir la cuadrícula de BatchNorm sin tocar la base de datos LMDB; de lo contrario, en ausencia de la base de datos, nada funcionaría. (En general, me parece extraño que en Caffe la fuente de datos esté configurada en la arquitectura de red; esto al menos no es muy práctico).
¿Cómo se ve la arquitectura de red (breve)- Iniciar sesión:
- Full conv conv conv : 32 canales,
stride=2
- Convolución Mobilenet conv1 - conv11 : 64, 128, 128, 256, 256, 512 ... 512 canales, algunos tienen
stride=2
- Capa de detección:
- Convolución Mobilenet conv12, conv13 : 1024 canales, conv12 tiene
stride=2
- Capa de detección:
- Convolución completa conv14_1, conv14_2 : 256, 512 canales, el primer
kernel_size=1
, el segundo stride=2
- Capa de detección:
- Convolución completa conv15_1, conv15_2 : 128, 256 canales, el primer
kernel_size=1
, el segundo stride=2
- Capa de detección:
- Convoluciones conv16_1, conv16_2 completas: 128, 256 canales, el primer
kernel_size=1
, el segundo stride=2
- Capa de detección:
- Convolución completa conv17_1, conv17_2 : 64, 128 canales, el primer
kernel_size=1
, el segundo stride=2
- Capa de detección:
- Salida de detección de capa final
He corregido ligeramente la arquitectura de la red. Lista de cambios:
- Obviamente, el número de clases ha cambiado a 1 (sin contar el fondo).
- Limitaciones en la relación de aspecto de los parches de corte durante el entrenamiento: cambiado de en (Decidí simplificar un poco la tarea y no aprender de imágenes demasiado estiradas).
- De los marcos predeterminados, solo quedaron los cuadrados, dos para cada celda. Reduje considerablemente sus tamaños, ya que las caras son significativamente más pequeñas que los objetos en el clásico problema de detección de objetos.
Caffe calcula los tamaños de fotograma predeterminados de la siguiente manera: tener un tamaño de fotograma mínimo
y máximo
, crea un marco pequeño y grande con dimensiones
y
. Como quería detectar rostros lo más pequeños posible, calculé el
stride
completo para cada capa de detección y equiparé el tamaño mínimo de fotograma. Con estos parámetros, los pequeños marcos predeterminados se ubicarán uno cerca del otro y no se intersecarán. Entonces, al menos tenemos una garantía de que la intersección con el objeto existirá para algún tipo de marco. Establezco el tamaño máximo el doble. Para las capas
conv16_2, conv17_2, configuro las dimensiones en el ojo para que sean las mismas. De esta manera
para todas las capas fueron:
Cómo se ven algunos marcos predeterminados (ruido para mayor claridad) Datos
Usé dos
conjuntos de datos :
WIDER Face y
FDDB . WIDER contiene muchas imágenes con caras muy pequeñas y borrosas, y FDDB está más inclinado a imágenes grandes de caras (y un orden de magnitud menor que WIDER). El formato de anotación es ligeramente diferente en ellos, pero estos ya son detalles.
Para el entrenamiento, no utilicé todos los datos: arrojé caras demasiado pequeñas (menos de seis píxeles o menos del 2% del ancho de la imagen), arrojé todas las imágenes con una relación de aspecto de menos de 0.5 o más de 2, arrojé todas las imágenes marcadas como "borrosas" en el conjunto de datos WIDER, dado que correspondían en su mayor parte a personas muy pequeñas, y tuve que alinear al menos de alguna manera la proporción de caras pequeñas y grandes. Después de eso, hice todos los cuadros cuadrados, expandiendo el lado más pequeño: decidí que no estaba muy interesado en las proporciones faciales, y la tarea para la red neuronal se simplificó un poco. También descarté todas las imágenes en blanco y negro, de las cuales había pocas, y en las que el script de compilación de la base de datos falla.
Para usarlos para entrenamiento y pruebas, necesita armar una base LMDB a partir de ellos. Cómo hacerlo:
- Para cada imagen, el marcado se crea en formato
.xml
. - Se
train.txt
archivo train.txt
con líneas de la forma "path/to/image.png path/to/labels.xml"
, lo mismo se crea para la prueba. - Se
test_name_size.txt
archivo test_name_size.txt
con líneas de la forma "test_image_name height width"
- Crea un archivo
labelmap.prototxt
con coincidencias de labelmap.prototxt
numéricas
Se
ssd-caffe/scripts/create_annoset.py
(ejemplo del Makefile):
python3 /opt/movidius/ssd-caffe/scripts/create_annoset.py --anno-type=detection \ --label-map-file=$(wider_dir)/labelmap.prototxt --min-dim=0 --max-dim=0 \ --resize-width=0 --resize-height=0 --check-label --encode-type=jpg --encoded \ --redo $(wider_dir) \ $(wider_dir)/trainval.txt $(wider_dir)/WIDER_train/lmdb/wider_train_lmdb ./data
labelmap.prototxt item { name: "none_of_the_above" label: 0 display_name: "background" } item { name: "face" label: 1 display_name: "face" }
Ejemplo de marcado .xml <?xml version="1.0" ?> <annotation> <size> <width>348</width> <height>450</height> <depth>3</depth> </size> <object> <name>face</name> <bndbox> <xmin>161</xmin> <ymin>43</ymin> <xmax>241</xmax> <ymax>123</ymax> </bndbox> </object> </annotation>
El uso de dos conjuntos de datos al mismo tiempo solo significa que debe fusionar cuidadosamente los archivos correspondientes en pares, sin olvidar registrar correctamente las rutas, así como barajar el archivo para el entrenamiento.
Después de eso, puedes comenzar a entrenar.
Entrenamiento
El código para la capacitación modelo se puede encontrar en mi
Colab Notebook .
Realicé la capacitación en Google Colaboratory, porque mi computadora portátil apenas realizó las pruebas de la red y, en general, colgó la capacitación. La colaboración me permitió entrenar la red lo suficientemente rápido y gratis. El único inconveniente es que tuve que escribir un script de compilación SSD-Caffe para el Colaboratorio (que incluye cosas tan extrañas como la recompilación de impulso y la edición de la fuente), que toma alrededor de 40 minutos. Se pueden encontrar más detalles
en mi publicación anterior .
El Colaboratorio tiene una característica más: después de 12 horas, el automóvil muere, borrando permanentemente todos los datos. La mejor manera de evitar la pérdida de datos es montar su disco de Google en el sistema y ahorrar pesos de red allí cada 500-1000 iteraciones de entrenamiento.
En cuanto a mi detector, en una sesión en Colaboratory logró desaprender 4.500 iteraciones, y se entrenó por completo en dos sesiones.
La calidad de las predicciones (precisión promedio promedio) en el conjunto de datos de prueba que destaqué (fusionó WIDER y FDDB con las restricciones enumeradas anteriormente) fue de aproximadamente 0.87 para el mejor modelo. Para medir mAP en las escalas guardadas durante el entrenamiento, hay un script
scripts/plot_map.py
.
El detector funciona en un ejemplo (muy extraño) de un conjunto de datos:
Lanzamiento en NCS
Una demostración de detección de rostros está
aquí .
Para compilar una red neuronal para el Neural Compute Stick, necesita
Movidius NCSDK : contiene utilidades para compilar y perfilar redes neuronales, así como las API de C ++ y Python. Vale la pena señalar que la segunda versión fue lanzada recientemente, que no es compatible con la primera: todas las funciones de API fueron renombradas por alguna razón, el formato interno de las redes neuronales cambió, FIFO fue agregado para interactuar con NCS y (finalmente) la conversión automática de flotante de 32 bits a float 16 bit, que faltaba tanto en C ++. Actualicé todos mis proyectos a la segunda versión, pero dejé un par de muletas para compatibilidad con la primera.
Después de entrenar el detector, vale la pena fusionar capas BatchNorm con convoluciones adyacentes para acelerar la red neuronal. El script
merge_bn.py
esto desde aquí , que también tomé prestado del proyecto Mobilenet-SSD.
Luego debe llamar a la utilidad
mvNCCompile
, por ejemplo:
mvNCCompile -s 12 -o graph_ssd -w ssd-face.caffemodel ssd-face.prototxt
Hay un objetivo
graph_ssd
para esto en el Makefile del proyecto. El archivo
graph_ssd
resultante es una descripción de red neuronal en un formato comprendido por NCS.
Ahora sobre cómo interactuar con el dispositivo en sí. El proceso no es muy complicado, pero requiere una cantidad bastante grande de código. La secuencia de acciones es aproximadamente la siguiente:
- Obtenga el descriptor del dispositivo por número de serie
- Dispositivo abierto
- Lea el archivo de red neuronal compilado en el búfer (como un archivo binario)
- Crear un gráfico de cálculo vacío para NCS
- Coloque el gráfico en el dispositivo usando los datos del archivo y seleccione FIFO para él en la entrada / salida; ahora se puede liberar el búfer con el contenido del archivo
- Detector de inicio:
- Obtenga la imagen de la cámara (o de cualquier otra fuente)
- Procesarlo: escalar al tamaño deseado, convertir a float32 y lanzar al rango [-1,1]
- Subir imagen al dispositivo y solicitar inferencia
- Solicitar un resultado (el programa se bloqueará hasta que se reciba el resultado)
- Analiza el resultado, selecciona los cuadros de los objetos (sobre el formato - más adelante)
- Mostrar predicciones
- Libere todos los recursos: elimine FIFO y el gráfico de cálculo, cierre el dispositivo y quite su asa
Casi todas las acciones con NCS tienen su propia función separada, y en C ++ se ve muy engorroso, y debe monitorear cuidadosamente la liberación de todos los recursos. Para no sobrecargar el código, creé
una clase de contenedor para trabajar con NCS . En él, todo el trabajo de inicialización se oculta en el constructor y la función
load_file
, y en la liberación de recursos, en el destructor, y trabajar con NCS se reduce a llamar a métodos de clase 2-3. Además, hay una función conveniente para explicar los errores que han ocurrido.
Cree un contenedor pasando el tamaño de entrada y el tamaño de salida (número de elementos) al constructor:
NCSWrapper NCS(NETWORK_INPUT_SIZE*NETWORK_INPUT_SIZE*3, NETWORK_OUTPUT_SIZE);
Cargamos el archivo compilado con la red neuronal, inicializando simultáneamente todo lo que necesitamos:
if (!NCS.load_file("./models/face/graph_ssd")) { NCS.print_error_code(); return 0; }
Convertimos la imagen a float32 (la
image
es
cv::Mat
en el formato
CV_32FC3
) y la descargamos al dispositivo:
if(!NCS.load_tensor_nowait((float*)image.data)) { NCS.print_error_code(); break; }
Obtenemos el resultado (el
result
es un puntero de
float
libre, el contenedor de resultados es compatible con el contenedor); hasta el final de los cálculos, el programa está bloqueado:
if(!NCS.get_result(result)) { NCS.print_error_code(); break; }
De hecho, el contenedor también tiene un método que le permite cargar datos y obtener el resultado al mismo tiempo:
load_tensor((float*)image.data, result)
. Me negué a usarlo por una razón: usando métodos separados, puede acelerar ligeramente la ejecución del código. Después de cargar la imagen, la CPU permanecerá inactiva hasta que llegue el resultado de la ejecución con NCS (en este caso es de aproximadamente 100 ms), y en este momento puede hacer un trabajo útil: leer un nuevo marco y convertirlo, así como mostrar detecciones anteriores . Así es exactamente como se implementa el programa de demostración, en mi caso aumenta ligeramente el FPS. Puede ir más allá e iniciar el procesamiento de imágenes y el detector de caras de forma asincrónica en dos flujos diferentes; esto realmente funciona y le permite acelerar un poco más, pero no está implementado en el programa de demostración.
Como resultado, el detector devuelve una matriz flotante de tamaño
7*(keep_top_k+1)
. Aquí,
keep_top_k
es el parámetro especificado en el archivo
.prototxt
del modelo y muestra cuántas detecciones (en el orden de disminución de la confianza) deben devolverse. Este parámetro, así como el parámetro responsable de filtrar las detecciones por el valor de confianza mínimo, y los parámetros de supresión no máxima se pueden configurar en el archivo
.prototxt
del modelo en la última capa. Vale la pena señalar que si Caffe devuelve tantas detecciones como se encontraron en la imagen, entonces NCS siempre devuelve
keep_top_k
detecciones para que el tamaño de la matriz sea constante.
El conjunto de resultados en sí está
keep_top_k+1
siguiente manera: si lo consideramos como una matriz con
keep_top_k+1
filas y 7 columnas, entonces en la primera fila, en el primer elemento habrá el número de detecciones, y a partir de la segunda fila habrá las detecciones en el formato
"garbage, class_index, class_probability, x_min, y_min, x_max, y_max"
. Las coordenadas se especifican en el rango [0,1], por lo que deberán multiplicarse por el alto / ancho de la imagen. Los elementos restantes de la matriz serán basura. En este caso, la supresión no máxima se realiza automáticamente, incluso antes de que se obtenga el resultado (parece, justo en el NCS).
Detector de análisis void get_detection_boxes(float* predictions, int w, int h, float thresh, std::vector<float>& probs, std::vector<cv::Rect>& boxes) { int num = predictions[0]; float score = 0; float cls = 0; for (int i=1; i<num+1; i++) { score = predictions[i*7+2]; cls = predictions[i*7+1]; if (score>thresh && cls<=1) { probs.push_back(score); boxes.push_back(Rect(predictions[i*7+3]*w, predictions[i*7+4]*h, (predictions[i*7+5]-predictions[i*7+3])*w, (predictions[i*7+6]-predictions[i*7+4])*h)); } } }
Raspberry Pi Características de lanzamiento
El programa de demostración en sí se puede ejecutar en una computadora o computadora portátil normal con Ubuntu, o en una Raspberry Pi con un Raspbian Stretch. Estoy usando un Raspberry Pi 2 modelo B, pero la demostración también debería funcionar en otros modelos. El archivo MAKE del proyecto contiene dos objetivos para cambiar de modo:
make switch_desk
para la computadora / laptop y
make switch_rpi
para la Raspberry Pi. La diferencia fundamental en el código del programa es que, en el primer caso, OpenCV se usa para leer datos de la cámara y, en el segundo caso, la biblioteca
RaspiCam . Para ejecutar la demostración en Raspberry, debe compilarla e instalarla.
Ahora un punto muy importante: instalar NCSDK. Si sigue las instrucciones de instalación estándar en la Raspberry Pi, no terminará en nada bueno: el instalador intentará arrastrar y compilar SSD-Caffe y Tensorflow. En cambio, el NCSDK debe
compilarse en modo solo API . En este modo, solo estarán disponibles las API de C ++ y Python (es decir, no será posible compilar y perfilar gráficos de redes neuronales). Esto significa que el gráfico de la red neuronal primero debe compilarse en una computadora normal y luego copiarse a Raspberry. Por conveniencia, agregué dos archivos compilados al repositorio, para YOLO y para SSD.
Otro punto interesante es la conexión puramente física de NCS a Raspberry. Parece que no es difícil conectarlo a un conector USB, pero debe recordar que su carcasa bloqueará los otros tres conectores (es bastante saludable, ya que actúa como un radiador). La salida más fácil es conectarlo a través de un cable USB.
También vale la pena tener en cuenta que la velocidad de ejecución variará para diferentes versiones de USB (para esta red neuronal particular: 102 ms para USB 3.0, 92 ms para USB 2.0).
Ahora sobre el poder de la NCS. Según la documentación, consume hasta 1 vatio (a 5 voltios en el conector USB será de hasta 200 ma; en comparación: la cámara Raspberry consume hasta 250 ma). Cuando funciona con un cargador regular de 5 voltios, 2 amperios, todo funciona muy bien. Sin embargo, intentar conectar dos o más NCS a Raspberry puede causar problemas. En este caso, se recomienda utilizar un divisor USB con posibilidad de alimentación externa.
En Raspberry, la demostración es más lenta que en una computadora / laptop: 7.2 FPS versus 10.4 FPS. Esto se debe a varios factores: en primer lugar, es imposible deshacerse de los cálculos en la CPU, pero se realizan mucho más lentamente; en segundo lugar, la velocidad de transferencia de datos afecta (para USB 2.0).
Además, para comparar, intenté ejecutar un detector de rostros en Raspberry YOLOv2 desde mi primer artículo, pero funcionó muy mal: a una velocidad de 3.6 FPS, pierde muchas caras incluso en cuadros simples. Aparentemente, es muy sensible a los parámetros de la imagen de entrada, cuya calidad en el caso de la cámara Raspberry está lejos de ser ideal. SSD funciona mucho más estable, aunque tuve que ajustar un poco la configuración de video en la configuración de RapiCam. a veces también extraña las caras en el marco, pero rara vez lo hace. Para aumentar la estabilidad en aplicaciones reales, puede agregar un
rastreador centroide simple.
Por cierto: lo mismo se puede reproducir en Python, hay un
tutorial sobre PyImageSearch (Mobilenet-SSD se usa para la tarea de detección de objetos).
Otras ideas
También probé un par de ideas para acelerar la red neuronal en sí:
Primera idea: puede dejar solo la detección de las capas
conv11
y
conv13
, y eliminar todas las capas adicionales. Obtendrá un detector que detecta solo caras pequeñas y funciona un poco más rápido. En general, no vale la pena.
La segunda idea fue interesante, pero no funcionó: intenté lanzar convoluciones desde la red neuronal con pesos cercanos a cero, con la esperanza de que fuera más rápido. Sin embargo, hubo pocas convoluciones de este tipo, y su eliminación solo ralentizó ligeramente la red neuronal (el único presentimiento: esto se debe al hecho de que el número de canales ha dejado de ser una potencia de dos).
Conclusión
Pensé en detectar rostros en Raspberry durante mucho tiempo, como una subtarea de mi proyecto robótico. No me gustaron los detectores clásicos en términos de velocidad y calidad, y decidí probar métodos de redes neuronales, al mismo tiempo que probaba el Neural Compute Stick, como resultado de lo cual había dos proyectos en GitHub y tres artículos sobre Habré (incluido el actual). En general, el resultado me conviene, lo más probable es que use este detector en mi robot (tal vez haya otro artículo al respecto). Vale la pena señalar que mi solución puede no ser óptima; sin embargo, este es un proyecto de capacitación, hecho en parte por curiosidad por el NCS. Sin embargo, espero que este artículo sea útil para alguien.