IGNG - Algoritmo incremental de gas neuronal incremental


Al escribir un artículo sobre el desarrollo de un detector de anomalías, implementé uno de los algoritmos llamado Gas neuronal de crecimiento incremental.


En Literatura soviética En el segmento ruso de Internet, este tema está cubierto bastante mal, y solo hubo un artículo , e incluso entonces, con la aplicación de este algoritmo.


Entonces, ¿qué es exactamente un algoritmo de gas neuronal en crecimiento incremental?


Introduccion


IGNG, como GNG , es un algoritmo de agrupamiento adaptativo.
El algoritmo en sí mismo se describe en un artículo de Prudent y Ennadji para 2005 .


Al igual que GNG, hay muchos vectores de datos. X o función generadora f(t) , que proporciona vectores a partir de datos distribuidos aleatoriamente (parámetro t - hora o número de muestra en la muestra).


El algoritmo no impone restricciones adicionales en estos datos.
Pero por dentro es muy diferente de GNG.


Este algoritmo también es interesante porque es un poco más preciso que los modelos GNG de neurogénesis .


Descripción del algoritmo


El algoritmo divide muchos datos en grupos.
En comparación con GNG, su ventaja es una mayor tasa de convergencia.


Ideas en las que se basa el algoritmo:


  • Teoría de resonancia adaptativa : Primero, se busca la neurona más cercana, y si la diferencia no excede el umbral (el "parámetro de vigilancia"), se ajustan los pesos o, de lo contrario, se cambian las coordenadas de las neuronas en el espacio de datos. Si no se ha superado el umbral, se crean nuevas neuronas que se aproximan mejor al valor de la muestra de datos.
  • Tanto las conexiones como las neuronas tienen un parámetro de edad (GNG solo tiene conexiones), que al principio es cero, pero aumenta a medida que aprende.
  • Una neurona no aparece inmediatamente: primero aparece un embrión (o neurona germinal), cuya edad aumenta con cada iteración, hasta que madura. Después del entrenamiento, solo las neuronas maduras participan en la clasificación .

Ciclo principal


El trabajo comienza con un gráfico vacío. Parámetro  sigma Inicializado por la desviación estándar de la muestra de entrenamiento:


 sigma= sqrt frac1N sum limitsNi=1 left(xi barx right)2


Donde:  barx - el promedio entre las coordenadas de la muestra.


El bucle principal en cada paso disminuye el valor  sigma , que es el umbral de proximidad, y calcula la diferencia entre el nivel anterior de calidad de agrupación y el nivel que se obtuvo después de la agrupación mediante el procedimiento IGNG .



Código gráfico
@startuml start :TrainIGNG(S); :<latex>\sigma = \sigma_S,x,y \in S</latex>; :<latex>IGNG(1, \sigma, age_{mature}, S)</latex>; :<latex>old = 0</latex>; :<latex>calin = CHI()</latex>; while (<latex>old - calin \leq 0</latex>) :<latex>\sigma=\sigma - \sigma / 10</latex>; :<latex>IGNG(1, \sigma, age_{mature}, S)</latex>; :<latex>old = calin</latex>; :<latex>calin = CHI()</latex>; endwhile stop @enduml 

CHI es el índice Kalinsky-Kharabaz, que muestra la calidad de la agrupación:


CHI= fracB/(c1)W/(nc)


Donde:


  • n - el número de muestras de datos.
  • c - el número de grupos (en este caso, el número de neuronas).
  • B - matriz de dispersión interna (la suma de las distancias al cuadrado entre las coordenadas de las neuronas y el promedio de todos los datos).
  • W - matriz de dispersión externa (la suma de las distancias al cuadrado entre todos los datos y la neurona más cercana).

Cuanto mayor sea el valor del índice, mejor, porque si la diferencia entre los índices después de la agrupación y antes de que sea negativa, es posible suponer que el índice se volvió positivo y excedió el anterior, es decir. agrupación completada con éxito.


Procedimiento IGNG


Este es el procedimiento básico del algoritmo.


Se divide en tres etapas mutuamente excluyentes:


  • No se encontraron neuronas.
  • Se encontró una neurona satisfactoria.
  • Encontramos dos que satisfacen las condiciones de la neurona.

Si una de las condiciones pasa, los otros pasos no se realizan.


Primero, se busca en una neurona la mejor muestra de datos aproximados:


c1=min(dist( xi, omegac))


Aqui dist(x omega,x xi) - función de cálculo de distancia, que suele ser una métrica euclidiana .


Si no se encuentra la neurona, o está demasiado lejos de los datos, es decir no cumple el criterio de proximidad dist( xi, omegac) leq sigma , se crea una nueva neurona embrionaria con coordenadas iguales a las coordenadas de la muestra en el espacio de datos.



Si la verificación de proximidad ha pasado, se busca una segunda neurona de la misma manera y se verifica la proximidad a la muestra de datos.
Si no se encuentra la segunda neurona, se crea.



Si se encuentran dos neuronas que satisfacen la condición de proximidad a la muestra de datos, sus coordenadas se corrigen de acuerdo con la siguiente fórmula:


 epsilon(t)hc,ci= begincases epsilonb, ifc=ci epsilonn, ifhaylaconexiónentrec=ci0,inothercase endcases

ó


 Delta omegac= epsilon(t)hc,c1 parallel xi omegac parallel omegac= omegac+ Delta omegac


Donde:


  •  epsilon(t) - paso de adaptación.
  • ci Es el número de la neurona.
  • hc,c1 - Función de Neuron Neighborhood c con la neurona ganadora (en este caso, devolverá 1 para vecinos directos, 0 de lo contrario, porque el paso de adaptación, para el cálculo  omega será distinto de cero solo para vecinos directos).

En otras palabras, la coordenada (peso) de la neurona ganadora se cambia a  epsilonb Delta omegai , y todos sus vecinos directos (aquellos conectados con él por un borde del gráfico) en  epsilonn Delta omegai donde  omegai - la coordenada de la neurona correspondiente antes del cambio.


Luego se crea una conexión entre las dos neuronas ganadoras, y si ya se ha creado, su edad se restablece.
La edad de todas las demás relaciones está aumentando.


Todas las comunicaciones cuya edad excedió la constante agemax son eliminados
Después de eso, se eliminan todas las neuronas maduras aisladas (aquellas que no tienen conexión con otras).


La edad de las neuronas inmediatas vecinas a la neurona ganadora está aumentando.
Si la edad de cualquiera de las neuronas de la línea germinal excede agemature Se convierte en una neurona madura.


El gráfico final contiene solo neuronas maduras.


Una condición para completar el procedimiento IGNG a continuación puede considerarse un número fijo de ciclos.


El algoritmo se muestra a continuación (se puede hacer clic en la imagen):



Código gráfico
 @startuml skinparam nodesep 10 skinparam ranksep 20 start :IGNG(age, sigma, <latex>a_{mature}</latex>, S); while (  ) is () -[#blue]-> :   e  S; :   c<sub>1</sub>; if (  \n<latex>dist(\xi, \omega_{c_1}) \leq \sigma</latex>) then () :     <latex>\omega_{new} = \xi</latex>; else () -[#blue]-> :   ; if (     \n <latex>dist(\xi, \omega_{c_2}) \leq \sigma</latex>) then () :     <latex>\omega_{new} = \xi</latex>; :   <latex>c_1</latex>  <latex>c_2</latex>; note     ,      end note else () -[#blue]-> :   ,\n  <latex>c_1</latex>; :<latex>\omega_{c_1} = \omega_c + \epsilon_b(\xi - \omega_{c_1})</latex>; :<latex>\omega_n = \omega_n + \epsilon_n(\xi - \omega_n)</latex>; note n -     <latex>c_1</latex> (..     ) end note if (c<sub>1</sub>  c<sub>2</sub> ) then () :  : <latex>age_{c_1 -> c_2} = 0</latex>; else () -[#blue]-> :   c<sub>1</sub>  c<sub>2</sub>; endif :  \n  c<sub>1</sub>; note ,    ,   . end note endif repeat if (<latex>age(c) \geq a_{mature}</latex>) then () :  $<!-- math>c</math -->$  ; else () -[#blue]-> endif repeat while (  ?) endif : ,    ; :   ; note          IGNG,   ,     GNG.     . endnote endwhile () stop @enduml 

Implementación


La red se implementó en Python usando la biblioteca de gráficos NetworkX . A continuación, se muestra el corte del código del prototipo en el artículo anterior . También hay breves explicaciones para el código.


Si alguien está interesado en el código completo, aquí hay un enlace al repositorio .


Un ejemplo del algoritmo:



La mayor parte del código
 class NeuralGas(): __metaclass__ = ABCMeta def __init__(self, data, surface_graph=None, output_images_dir='images'): self._graph = nx.Graph() self._data = data self._surface_graph = surface_graph # Deviation parameters. self._dev_params = None self._output_images_dir = output_images_dir # Nodes count. self._count = 0 if os.path.isdir(output_images_dir): shutil.rmtree('{}'.format(output_images_dir)) print("Ouput images will be saved in: {0}".format(output_images_dir)) os.makedirs(output_images_dir) self._start_time = time.time() @abstractmethod def train(self, max_iterations=100, save_step=0): raise NotImplementedError() def number_of_clusters(self): return nx.number_connected_components(self._graph) def detect_anomalies(self, data, threshold=5, train=False, save_step=100): anomalies_counter, anomaly_records_counter, normal_records_counter = 0, 0, 0 anomaly_level = 0 start_time = self._start_time = time.time() for i, d in enumerate(data): risk_level = self.test_node(d, train) if risk_level != 0: anomaly_records_counter += 1 anomaly_level += risk_level if anomaly_level > threshold: anomalies_counter += 1 #print('Anomaly was detected [count = {}]!'.format(anomalies_counter)) anomaly_level = 0 else: normal_records_counter += 1 if i % save_step == 0: tm = time.time() - start_time print('Abnormal records = {}, Normal records = {}, Detection time = {} s, Time per record = {} s'. format(anomaly_records_counter, normal_records_counter, round(tm, 2), tm / i if i else 0)) tm = time.time() - start_time print('{} [abnormal records = {}, normal records = {}, detection time = {} s, time per record = {} s]'. format('Anomalies were detected (count = {})'.format(anomalies_counter) if anomalies_counter else 'Anomalies weren\'t detected', anomaly_records_counter, normal_records_counter, round(tm, 2), tm / len(data))) return anomalies_counter > 0 def test_node(self, node, train=False): n, dist = self._determine_closest_vertice(node) dev = self._calculate_deviation_params() dev = dev.get(frozenset(nx.node_connected_component(self._graph, n)), dist + 1) dist_sub_dev = dist - dev if dist_sub_dev > 0: return dist_sub_dev if train: self._dev_params = None self._train_on_data_item(node) return 0 @abstractmethod def _train_on_data_item(self, data_item): raise NotImplementedError() @abstractmethod def _save_img(self, fignum, training_step): """.""" raise NotImplementedError() def _calculate_deviation_params(self, distance_function_params={}): if self._dev_params is not None: return self._dev_params clusters = {} dcvd = self._determine_closest_vertice dlen = len(self._data) #dmean = np.mean(self._data, axis=1) #deviation = 0 for node in self._data: n = dcvd(node, **distance_function_params) cluster = clusters.setdefault(frozenset(nx.node_connected_component(self._graph, n[0])), [0, 0]) cluster[0] += n[1] cluster[1] += 1 clusters = {k: sqrt(v[0]/v[1]) for k, v in clusters.items()} self._dev_params = clusters return clusters def _determine_closest_vertice(self, curnode): """.""" pos = nx.get_node_attributes(self._graph, 'pos') kv = zip(*pos.items()) distances = np.linalg.norm(kv[1] - curnode, ord=2, axis=1) i0 = np.argsort(distances)[0] return kv[0][i0], distances[i0] def _determine_2closest_vertices(self, curnode): """Where this curnode is actually the x,y index of the data we want to analyze.""" pos = nx.get_node_attributes(self._graph, 'pos') l_pos = len(pos) if l_pos == 0: return None, None elif l_pos == 1: return pos[0], None kv = zip(*pos.items()) # Calculate Euclidean distance (2-norm of difference vectors) and get first two indexes of the sorted array. # Or a Euclidean-closest nodes index. distances = np.linalg.norm(kv[1] - curnode, ord=2, axis=1) i0, i1 = np.argsort(distances)[0:2] winner1 = tuple((kv[0][i0], distances[i0])) winner2 = tuple((kv[0][i1], distances[i1])) return winner1, winner2 class IGNG(NeuralGas): """Incremental Growing Neural Gas multidimensional implementation""" def __init__(self, data, surface_graph=None, eps_b=0.05, eps_n=0.0005, max_age=5, a_mature=1, output_images_dir='images'): """.""" NeuralGas.__init__(self, data, surface_graph, output_images_dir) self._eps_b = eps_b self._eps_n = eps_n self._max_age = max_age self._a_mature = a_mature self._num_of_input_signals = 0 self._fignum = 0 self._max_train_iters = 0 # Initial value is a standard deviation of the data. self._d = np.std(data) def train(self, max_iterations=100, save_step=0): """IGNG training method""" self._dev_params = None self._max_train_iters = max_iterations fignum = self._fignum self._save_img(fignum, 0) CHS = self.__calinski_harabaz_score igng = self.__igng data = self._data if save_step < 1: save_step = max_iterations old = 0 calin = CHS() i_count = 0 start_time = self._start_time = time.time() while old - calin <= 0: print('Iteration {0:d}...'.format(i_count)) i_count += 1 steps = 1 while steps <= max_iterations: for i, x in enumerate(data): igng(x) if i % save_step == 0: tm = time.time() - start_time print('Training time = {} s, Time per record = {} s, Training step = {}, Clusters count = {}, Neurons = {}, CHI = {}'. format(round(tm, 2), tm / (i if i and i_count == 0 else len(data)), i_count, self.number_of_clusters(), len(self._graph), old - calin) ) self._save_img(fignum, i_count) fignum += 1 steps += 1 self._d -= 0.1 * self._d old = calin calin = CHS() print('Training complete, clusters count = {}, training time = {} s'.format(self.number_of_clusters(), round(time.time() - start_time, 2))) self._fignum = fignum def _train_on_data_item(self, data_item): steps = 0 igng = self.__igng # while steps < self._max_train_iters: while steps < 5: igng(data_item) steps += 1 def __long_train_on_data_item(self, data_item): """.""" np.append(self._data, data_item) self._dev_params = None CHS = self.__calinski_harabaz_score igng = self.__igng data = self._data max_iterations = self._max_train_iters old = 0 calin = CHS() i_count = 0 # Strictly less. while old - calin < 0: print('Training with new normal node, step {0:d}...'.format(i_count)) i_count += 1 steps = 0 if i_count > 100: print('BUG', old, calin) break while steps < max_iterations: igng(data_item) steps += 1 self._d -= 0.1 * self._d old = calin calin = CHS() def _calculate_deviation_params(self, skip_embryo=True): return super(IGNG, self)._calculate_deviation_params(distance_function_params={'skip_embryo': skip_embryo}) def __calinski_harabaz_score(self, skip_embryo=True): graph = self._graph nodes = graph.nodes extra_disp, intra_disp = 0., 0. # CHI = [B / (c - 1)]/[W / (n - c)] # Total numb er of neurons. #ns = nx.get_node_attributes(self._graph, 'n_type') c = len([v for v in nodes.values() if v['n_type'] == 1]) if skip_embryo else len(nodes) # Total number of data. n = len(self._data) # Mean of the all data. mean = np.mean(self._data, axis=1) pos = nx.get_node_attributes(self._graph, 'pos') for node, k in pos.items(): if skip_embryo and nodes[node]['n_type'] == 0: # Skip embryo neurons. continue mean_k = np.mean(k) extra_disp += len(k) * np.sum((mean_k - mean) ** 2) intra_disp += np.sum((k - mean_k) ** 2) return (1. if intra_disp == 0. else extra_disp * (n - c) / (intra_disp * (c - 1.))) def _determine_closest_vertice(self, curnode, skip_embryo=True): """Where this curnode is actually the x,y index of the data we want to analyze.""" pos = nx.get_node_attributes(self._graph, 'pos') nodes = self._graph.nodes distance = sys.maxint for node, position in pos.items(): if skip_embryo and nodes[node]['n_type'] == 0: # Skip embryo neurons. continue dist = euclidean(curnode, position) if dist < distance: distance = dist return node, distance def __get_specific_nodes(self, n_type): return [n for n, p in nx.get_node_attributes(self._graph, 'n_type').items() if p == n_type] def __igng(self, cur_node): """Main IGNG training subroutine""" # find nearest unit and second nearest unit winner1, winner2 = self._determine_2closest_vertices(cur_node) graph = self._graph nodes = graph.nodes d = self._d # Second list element is a distance. if winner1 is None or winner1[1] >= d: # 0 - is an embryo type. graph.add_node(self._count, pos=copy(cur_node), n_type=0, age=0) winner_node1 = self._count self._count += 1 return else: winner_node1 = winner1[0] # Second list element is a distance. if winner2 is None or winner2[1] >= d: # 0 - is an embryo type. graph.add_node(self._count, pos=copy(cur_node), n_type=0, age=0) winner_node2 = self._count self._count += 1 graph.add_edge(winner_node1, winner_node2, age=0) return else: winner_node2 = winner2[0] # Increment the age of all edges, emanating from the winner. for e in graph.edges(winner_node1, data=True): e[2]['age'] += 1 w_node = nodes[winner_node1] # Move the winner node towards current node. w_node['pos'] += self._eps_b * (cur_node - w_node['pos']) neighbors = nx.all_neighbors(graph, winner_node1) a_mature = self._a_mature for n in neighbors: c_node = nodes[n] # Move all direct neighbors of the winner. c_node['pos'] += self._eps_n * (cur_node - c_node['pos']) # Increment the age of all direct neighbors of the winner. c_node['age'] += 1 if c_node['n_type'] == 0 and c_node['age'] >= a_mature: # Now, it's a mature neuron. c_node['n_type'] = 1 # Create connection with age == 0 between two winners. graph.add_edge(winner_node1, winner_node2, age=0) max_age = self._max_age # If there are ages more than maximum allowed age, remove them. age_of_edges = nx.get_edge_attributes(graph, 'age') for edge, age in iteritems(age_of_edges): if age >= max_age: graph.remove_edge(edge[0], edge[1]) # If it causes isolated vertix, remove that vertex as well. #graph.remove_nodes_from(nx.isolates(graph)) for node, v in nodes.items(): if v['n_type'] == 0: # Skip embryo neurons. continue if not graph.neighbors(node): graph.remove_node(node) def _save_img(self, fignum, training_step): """.""" title='Incremental Growing Neural Gas for the network anomalies detection' if self._surface_graph is not None: text = OrderedDict([ ('Image', fignum), ('Training step', training_step), ('Time', '{} s'.format(round(time.time() - self._start_time, 2))), ('Clusters count', self.number_of_clusters()), ('Neurons', len(self._graph)), (' Mature', len(self.__get_specific_nodes(1))), (' Embryo', len(self.__get_specific_nodes(0))), ('Connections', len(self._graph.edges)), ('Data records', len(self._data)) ]) draw_graph3d(self._surface_graph, fignum, title=title) graph = self._graph if len(graph) > 0: draw_graph3d(graph, fignum, clear=False, node_color=(1, 0, 0), title=title, text=text) mlab.savefig("{0}/{1}.png".format(self._output_images_dir, str(fignum))) #mlab.close(fignum) 

Source: https://habr.com/ru/post/es414209/


All Articles