April 2019

Band 34, Nummer 4

[Test Run]

Neuronale Anomalieerkennung mit PyTorch

Von James McCaffrey

James McCaffreyAnomalieerkennung (auch als Ausreißererkennung bezeichnet) ist der Vorgang, bei dem seltene Elemente in einem Dataset gefunden werden. Beispiele sind das Identifizieren von bösartigen Ereignissen in einer Serverprotokolldatei und das Auffinden von betrügerischer Onlinewerbung.

Ich empfehle Ihnen, sich das Demoprogramm in Abbildung 1 anzusehen. Dort erfahren Sie, um was es bei diesem Artikel geht. Das Demoprogramm analysiert eine 1.000 Elemente umfassende Teilmenge des bekannten Datasets des Modified National Institute of Standards and Technology (MNIST). Jedes Datenelement ist ein 28x28 Graustufenbild (784 Pixel) einer handgeschriebenen Ziffer von 0 bis 9. Das vollständige MNIST-Dataset verfügt über 60.000 Trainingsbilder und 10.000 Testbilder.

MNSIT-Bildanomalieerkennung unter Verwendung von Keras
Abbildung 1: MNSIT-Bildanomalieerkennung unter Verwendung von Keras

Das Demoprogramm erstellt und trainiert einen 784-100-50-100-784 Deep Neural Autoencoder mithilfe der PyTorch-Codebibliothek. Ein Autoencoder ist ein neuronales Netz, das lernt, seine Eingabe vorherzusagen. Nach dem Training durchsucht die Demo 1.000 Bilder und findet das eine Bild, das am anormalsten ist, wobei „am anormalsten“ den größten Rekonstruktionsfehler bedeutet. Die anormalste Ziffer ist eine 3, die auch eine 8 sein könnte.

Dieser Artikel geht davon aus, dass Sie über mittlere oder fortgeschrittene Programmierkenntnisse mit einer Sprache der C-Familie und eine grundlegende Vertrautheit mit Machine Learning verfügen, setzt aber nicht voraus, dass Sie etwas über Autoencoder wissen. Der gesamte Democode wird in diesem Artikel vorgestellt. Sie können den Code und die Daten auch aus dem zugehörigen Download abrufen. Die gesamte normale Fehlerprüfung wurde entfernt, um die Hauptideen so klar wie möglich darzustellen.

Installieren von PyTorch

PyTorch ist eine relativ einfache Codebibliothek zum Erstellen neuronaler Netze. In der Funktionalität ist PyTorch in etwa vergleichbar mit TensorFlow und CNTK. PyTorch ist in C++ geschrieben, verfügt aber über eine Python-Sprach-API zur einfacheren Programmierung.

Die Installation von PyTorch umfasst zwei Hauptschritte. Zunächst installieren Sie Python und mehrere benötigte Hilfspakete, z.B. NumPy und SciPy. Danach installieren Sie PyTorch als Python-Add-On-Paket. Obwohl es möglich ist, Python und die für die Ausführung von PyTorch erforderlichen Pakete separat zu installieren, ist es viel besser, eine Python-Distribution zu installieren, die eine Sammlung mit dem Python-Basisinterpreter und zusätzlichen Paketen ist, die miteinander kompatibel sind. Für mein Demo habe die ich die Anaconda3 5.2.0-Distribution installiert, die Python 3.6.5 enthält.

Nach der Installation von Anaconda bin ich zur pytorch.org-Website navigiert und habe die Optionen für das Windows-Betriebssystem, den Pip-Installer, Python 3.6 und keine CUDA GPU-Version ausgewählt. Auf diese Weise habe ich eine URL erhalten, die auf die entsprechende WHL-Datei (ausgesprochen "wheel") verweist, die ich auf meinen lokalen Computer heruntergeladen habe. Wenn Sie mit dem Python-Ökosystem noch nicht vertraut sind: Sie können sich eine WHL-Datei von Python ähnlich wie eine MSI-Datei von Windows vorstellen. In meinem Fall habe ich PyTorch Version 1.0.0.0 heruntergeladen. Ich habe eine Befehlsshell geöffnet, bin zu dem Verzeichnis mit der WHL-Datei navigiert und habe den folgenden Befehl eingegeben:

pip install torch-1.0.0-cp36-cp36m-win_amd64.whl

Das Demoprogramm

Das vollständige Demoprogramm (mit einigen kleinen Bearbeitungen, um Platz zu sparen) wird in Abbildung 2 gezeigt. Anstatt der üblichen vier Leerzeichen habe ich einen Einzug von zwei Leerzeichen verwendet, um Platz zu sparen. Beachten Sie, dass Python das Zeichen „\“ für die Zeilenfortsetzung verwendet. Ich habe den Editor verwendet, um mein Programm zu bearbeiten. Die meisten meiner Kollegen bevorzugen einen anspruchsvolleren Editor, aber ich mag die kompromisslose Einfachheit von Editor.

Abbildung 2: Das Demoprogramm für Anomalieerkennung

# auto_anom_mnist.py
# PyTorch 1.0.0 Anaconda3 5.2.0 (Python 3.6.5)
# autoencoder anomaly detection on MNIST
import numpy as np
import torch as T
import matplotlib.pyplot as plt
# -----------------------------------------------------------
def display(raw_data_x, raw_data_y, idx):
  label = raw_data_y[idx]  # like '5'
  print("digit/label = ", str(label), "\n")
  pixels = np.array(raw_data_x[idx])  # target row of pixels
  pixels = pixels.reshape((28,28))
  plt.rcParams['toolbar'] = 'None'
  plt.imshow(pixels, cmap=plt.get_cmap('gray_r'))
  plt.show() 
# -----------------------------------------------------------
class Batcher:
  def __init__(self, num_items, batch_size, seed=0):
    self.indices = np.arange(num_items)
    self.num_items = num_items
    self.batch_size = batch_size
    self.rnd = np.random.RandomState(seed)
    self.rnd.shuffle(self.indices)
    self.ptr = 0
  def __iter__(self):
    return self
  def __next__(self):
    if self.ptr + self.batch_size > self.num_items:
      self.rnd.shuffle(self.indices)
      self.ptr = 0
      raise StopIteration  # ugh.
    else:
      result = self.indices[self.ptr:self.ptr+self.batch_size]
      self.ptr += self.batch_size
      return result
# -----------------------------------------------------------
class Net(T.nn.Module):
  def __init__(self):
    super(Net, self).__init__()
    self.layer1 = T.nn.Linear(784, 100)  # hidden 1
    self.layer2 = T.nn.Linear(100, 50)
    self.layer3 = T.nn.Linear(50,100)
    self.layer4 = T.nn.Linear(100, 784)
    T.nn.init.xavier_uniform_(self.layer1.weight)  # glorot
    T.nn.init.zeros_(self.layer1.bias)
    T.nn.init.xavier_uniform_(self.layer2.weight) 
    T.nn.init.zeros_(self.layer2.bias)
    T.nn.init.xavier_uniform_(self.layer3.weight) 
    T.nn.init.zeros_(self.layer3.bias)
    T.nn.init.xavier_uniform_(self.layer4.weight) 
    T.nn.init.zeros_(self.layer4.bias)
  def forward(self, x):
    z = T.tanh(self.layer1(x))
    z = T.tanh(self.layer2(z))
    z = T.tanh(self.layer3(z))
    z = T.tanh(self.layer4(z))  # consider none or sigmoid
    return z
# -----------------------------------------------------------
def main():
  # 0. get started
  print("Begin autoencoder for MNIST anomaly detection")
  T.manual_seed(1)
  np.random.seed(1)
  # 1. load data
  print("Loading MNIST subset data into memory ")
  data_file = ".\\Data\\mnist_pytorch_1000.txt"
  data_x = np.loadtxt(data_file, delimiter=" ",
    usecols=range(2,786), dtype=np.float32)
  labels = np.loadtxt(data_file, delimiter=" ",
    usecols=[0], dtype=np.float32)
  norm_x = data_x / 255
  # 2. create autoencoder model
  net = Net()
  # 3. train autoencoder model
  net = net.train()  # explicitly set
  bat_size = 40
  loss_func = T.nn.MSELoss()
  optimizer = T.optim.Adam(net.parameters(), lr=0.01)
  batcher = Batcher(num_items=len(norm_x),
    batch_size=bat_size, seed=1)
  max_epochs = 100
  print("Starting training")
  for epoch in range(0, max_epochs):
    if epoch > 0 and epoch % (max_epochs/10) == 0:
      print("epoch = %6d" % epoch, end="")
      print("  prev batch loss = %7.4f" % loss_obj.item())
    for curr_bat in batcher:
      X = T.Tensor(norm_x[curr_bat])
      optimizer.zero_grad()
      oupt = net(X)
      loss_obj = loss_func(oupt, X)  # note X not Y
      loss_obj.backward()
      optimizer.step()
  print("Training complete")
  # 4. analyze - find item(s) with large(st) error
  net = net.eval()  # not needed - no dropout
  X = T.Tensor(norm_x)  # all input item as Tensors
  Y = net(X)            # all outputs as Tensors
  N = len(data_x)
  max_se = 0.0; max_ix = 0
  for i in range(N):
    curr_se = T.sum((X[i]-Y[i])*(X[i]-Y[i]))
    if curr_se.item() > max_se:
      max_se = curr_se.item()
      max_ix = i
  raw_data_x = data_x.astype(np.int)
  raw_data_y = labels.astype(np.int)
  print("Highest reconstruction error is index ", max_ix)
  display(raw_data_x, raw_data_y, max_ix)
  print("End autoencoder anomaly detection demo ")
# -----------------------------------------------------------
if __name__ == "__main__":
  main()

Das Demoprogramm beginnt mit dem Import der NumPy-, PyTorch- und Matplotlib-Pakete. Das Matplotlib-Paket wird verwendet, um die anormalste Ziffer, die das Modell gefunden hat, visuell darzustellen. Eine Alternative zum Import des gesamten PyTorch-Pakets besteht darin, nur die notwendigen Module zu importieren (z.B. torch.optim als Option).

Laden der Daten in den Arbeitsspeicher

Das Arbeiten mit den MNIST-Rohdaten ist ziemlich schwierig, da sie in einem proprietären, binären Format gespeichert sind. Ich habe ein Hilfsprogramm geschrieben, um die ersten 1.000 Elemente aus den 60.000 Trainingselementen zu extrahieren. Ich habe die Daten als „mnist_pytorch_1000.txt“ in einem Unterverzeichnis „Data“ gespeichert.

Die sich ergebenden Daten sehen folgendermaßen aus:

7 = 0 255 67 . . 123
2 = 113 28 0 . . 206
...
9 = 0 21 110 . . 254

Jede Zeile stellt eine Ziffer dar. Der erste Wert in jeder Zeile ist die Ziffer. Der zweite Wert ist ein beliebiges Gleichheitszeichen und dient nur der besseren Lesbarkeit. Die nächsten 28x28 = 784 Werte sind Graustufen-Pixelwerte zwischen 0 und 255. Alle Werte werden durch ein einzelnes Leerzeichen getrennt. Abbildung 3 zeigt das Datenelement am Index [30] in der Datendatei, das eine typische Ziffer „3“ ist.

Eine typische MNIST-Ziffer
Abbildung 3: Eine typische MNIST-Ziffer

Das Dataset wird mit den folgenden Anweisungen in den Speicher geladen:

data_file = ".\\Data\\mnist_pytorch_1000.txt"
data_x = np.loadtxt(data_file, delimiter=" ",
  usecols=range(2,786), dtype=np.float32)
labels = np.loadtxt(data_file, delimiter=" ",
  usecols=[0], dtype=np.float32)
norm_x = data_x / 255

Beachten Sie, dass sich die Ziffer/Bezeichnung in der Spalte 0 befindet und sich die 784 Pixelwerte in den Spalten 2 bis 785 befinden. Nachdem alle 1.000 Bilder in den Speicher geladen wurden, wird eine normalisierte Version der Daten erstellt, indem jeder Pixelwert durch 255 dividiert wird, sodass die skalierten Pixelwerte alle zwischen 0,0 und 1,0 liegen.

Definieren des Autoencodermodells

Das Demoprogramm definiert einen 784-100-50-100-784-Autoencoder. Die Anzahl der Knoten in den Ein- und Ausgabeschichten (784) wird durch die Daten bestimmt, aber die Anzahl der verborgenen Schichten und die Anzahl der Knoten in jeder Schicht sind Hyperparameter, die durch Versuch und Irrtum ermittelt werden müssen.

Das Demoprogramm verwendet eine vom Programm definierte Klasse („Net“), um die Schichtarchitektur und den Eingabe-/Ausgabemechanismus des Autoencoders zu definieren. Eine Alternative besteht darin, den Autoencoder direkt zu erstellen, z.B. über die Sequence-Funktion:

net = T.nn.Sequential(
  T.nn.Linear(784,100), T.nn.Tanh(),
  T.nn.Linear(100,50), T.nn.Tanh(),
  T.nn.Linear(50,100), T.nn.Tanh(),
  T.nn.Linear(100,784), T.nn.Tanh())

Der Gewichtungsinitialisierungsalgorithmus (Glorot uniform), die Aktivierungsfunktion für verborgene Schichten (tanh) und die Aktivierungsfunktion für die Ausgabeschicht (tanh) sind Hyperparameter. Da alle Eingabe- und Ausgabewerte für dieses Problem zwischen 0,0 und 1,0 liegen, ist die logistische Sigmoidfunktion eine gute Alternative zur Untersuchung der Ausgabeaktivierung.

Trainieren und Auswerten des Autoencodermodells

Das Demoprogramm bereitet das Training mit diesen Anweisungen vor:

net = net.train()  # explicitly set
bat_size = 40
loss_func = T.nn.MSELoss()
optimizer = T.optim.Adam(net.parameters(), lr=0.01)
batcher = Batcher(num_items=len(norm_x),
  batch_size=bat_size, seed=1)
max_epochs = 100

Da der Demoautoencoder keine Dropout- oder Batchnormalisierung verwendet, ist es nicht erforderlich, das Netzwerk explizit in den Trainingsmodus zu versetzen, aber meiner Meinung nach ist es dennoch sinnvoll. Die Batchgröße (40), der Trainingsoptimierungsalgorithmus (Adam), die anfängliche Lernrate (0,01) und die maximale Anzahl der Epochen (100) sind Hyperparameter. Wenn Sie neu im Bereich des neuronalen Machine Learning sind, denken Sie vielleicht: „Neuronale Netze weisen eine Vielzahl von Hyperparametern auf“, und Sie hätten Recht damit.

Das vom Programm definierte Batcher-Objekt dient dazu, die Indizes von 40 zufälligen Datenelementen gleichzeitig aufzurufen, bis alle 1.000 Elemente verarbeitet wurden (eine Epoche). Ein alternativer Ansatz ist die Verwendung der integrierten Dataset- und DataLoader-Objekte im torch.utils.data-Modul.

Die Struktur des Trainingsprozesses ist wie folgt:

for epoch in range(0, max_epochs):
  # print loss every 10 epochs
  for curr_bat in batcher:
    X = T.Tensor(norm_x[curr_bat])
    optimizer.zero_grad()
    oupt = net(X)
    loss_obj = loss_func(oupt, X)
    loss_obj.backward()
    optimizer.step()

Jeder Batch von Elementen wird mit dem Tensor-Konstruktor erstellt, der torch.float32 als Standarddatentyp verwendet. Beachten Sie, dass die loss_func-Funktion berechnete Ausgaben mit den Eingaben vergleicht, was dazu führt, dass das Netzwerk trainiert wird, um seine Eingabewerte vorherzusagen.

Nach dem Training werden Sie normalerweise das Modell speichern wollen, aber das liegt etwas außerhalb des Rahmens dieses Artikels. Die PyTorch-Dokumentation enthält gute Beispiele, die zeigen, wie ein trainiertes Modell auf verschiedene Weise gespeichert werden kann.

Bei der Arbeit mit Autoencodern gibt es unter den meisten Umständen (und auch in diesem Beispiel) keine inhärente Definition der Modellgenauigkeit. Sie müssen festlegen, wie nah berechnete Ausgabewerte an den zugehörigen Eingabewerten liegen müssen, um als richtige Vorhersage gezählt zu werden, und dann eine vom Programm definierte Funktion schreiben, um Ihre Genauigkeitsmetrik zu berechnen.

Verwenden des Autoencodermodells zum Ermitteln anormaler Daten

Nach dem Training des Autoencodermodells geht es darum, Datenelemente zu ermitteln, die schwer richtig vorherzusagen sind, oder auch schwer zu rekonstruierende Datenelemente. Der Democode durchsucht alle 1.000 Datenelemente und berechnet so die quadrierte Differenz zwischen den normalisierten Eingabewerten und den berechneten Ausgabewerten:

net = net.eval()  # not needed - no dropout
X = T.Tensor(norm_x)  # all input item as Tensors
Y = net(X)            # all outputs as Tensors
N = len(data_x)
max_se = 0.0; max_ix = 0
for i in range(N):
  curr_se = T.sum((X[i]-Y[i])*(X[i]-Y[i]))
  if curr_se.item() > max_se:
    max_se = curr_se.item()
    max_ix = i

Der maximale quadratische Fehler (max_se) wird berechnet und der Index des zugeordneten Bilds (max_ix) gespeichert. Eine Alternative zum Bestimmen des einzelnen Elements mit dem größten Rekonstruktionsfehler besteht darin, alle quadratischen Fehler zu speichern, sie zu sortieren und die Top N Elemente zurückzugeben, wobei der Wert von n von dem jeweiligen Problem abhängt, das Sie untersuchen.

Nachdem das anormalste einzelne Datenelement gefunden wurde, wird es über die vom Programm definierte Anzeigefunktion angezeigt:

raw_data_x = data_x.astype(np.int)
raw_data_y = labels.astype(np.int)
print("Highest reconstruction error is index ", max_ix)
display(raw_data_x, raw_data_y, max_ix)

Die Pixel- und Bezeichnungswerte werden zumeist nur aus Prinzip aus dem Typ float32 in den Typ int konvertiert, da die imshow-Funktion von Matplotlib innerhalb der vom Programm definierten Anzeigefunktion beide Datentypen akzeptieren kann.

Zusammenfassung

Die Anomalieerkennung mit einem Deep Neural Autoencoder, wie in diesem Artikel vorgestellt, ist keine gut untersuchte Technik. Ein großer Vorteil der Verwendung eines neuronalen Autoencoders im Vergleich zu den meisten Standardclusteringtechniken besteht darin, dass neuronale Techniken nicht-numerische Daten durch Codierung dieser Daten verarbeiten können. Die meisten Clusteringtechniken hängen von einem numerischen Maß ab, z.B. vom euklidischen Abstand, was bedeutet, dass die Quelldaten streng numerisch sein müssen.

Eine verwandte, aber auch wenig erforschte Technik zur Erkennung von Anomalien ist die Erstellung eines Autoencoders für das zu untersuchende Dataset. Anstatt dann einen Rekonstruktionsfehler zu verwenden, um anormale Daten zu ermitteln, können Sie die Daten mit einem Standardalgorithmus wie k-means bündeln, da die Knoten der innersten verborgenen Schicht eine streng numerische Darstellung jedes Datenelements enthalten. Nach dem Clustering können Sie nach Clustern mit sehr wenigen Datenelementen suchen oder nach Datenelementen innerhalb von Clustern, die am weitesten von ihrem Clusterschwerpunkt entfernt sind. Dieser Ansatz weist Merkmale auf, die der neuronalen Worteinbettung ähneln, bei der Wörter in numerische Vektoren konvertiert werden, aus denen dann ein Abstandsmaß zwischen Wörtern berechnet werden kann.


Dr. James McCaffreyist in Redmond (Washington, USA) für Microsoft Research tätig. Er hat an verschiedenen wichtigen Microsoft-Produkten mitgearbeitet, unter anderem an Azure und Bing. Dr. McCaffrey erreichen Sie unter jamccaff@microsoft.com.

Unser Dank gilt den folgenden technischen Experten von Microsoft für die Durchsicht dieses Artikels: Chris Lee, Ricky Loynd