Lehrgang Internal Auditing, Universität St.Gallen (HSG), 2023
Die Analysen des Seminars Audit Data Analytics basieren auf Jupyter Notebook (https://jupyter.org). Anhand solcher Notebooks ist es möglich eine Vielzahl von Datenanalysen und statistischen Validierungen durchzuführen.
Im letzten Lab haben Sie die verschiedenen Elemente eines Supervised Deep Learning Workflow kennengelernt z.B. Datenaufbereitung, Modell Training und Modell Validierung. In diesem fünften Lab werden wir Jupyter Notebook verwenden, um ein erstes Deep Learning basiertes Audit-Analyseverfahren zu implementieren und anzuwenden.
Hierzu werden wir die im Seminar vorgestellten Deep Autoencoder Neural Networks (AENNs) anwenden um Anomalien im Buchungsstoff einer Finanzbuchhaltung zu detektieren. Im Gegensatz zu klassischen Feedforward-Netzen lernen AENNs, die Eingabedaten in eine niedrig-dimensionale Repräsentation zu enkodieren. Gleichzeitig lernt das AENN, die ursprünglichen Daten wieder aus der enkodierten Repräsentation zu dekodieren.
Die dekodierten Daten, die in der Regel als Rekonstruktion bezeichnet werden, sollten eine grosse Ähnlichkeit zu den ursprünglichen Eingabedaten aufweisen. Die Buchungssätze, für welche eine erfolgreiche Rekonstruktion nur fehlerhaft gelingt müssen deshalb eine oder mehrere ungewöhnliche Eigenschaften aufweisen. Die nachstehende Abbildung zeigt einen Überblick über den Deep Learning Prozess bzw. die AENN Netzarchitektur, welche wir in diesem Lab implementieren.
Im Rahmen des Lab werden wir wieder einige Funktionen der PyTorch
Bibliothek nutzen, um das AENN zu implementieren und zu trainieren. Im Laufe des Trainingsprozess soll das AENN die charakteristische Eigenschaften historischer Buchungen bzw. Journal Entries lernen. Nach erfolgreichen Modelltraining, werden wir das Modell anwenden, um anhand des Rekonstruktionsfehlers ungewöhnliche Buchungen innerhalb des Datensatzes zu detektieren. Abschliessend werden wir die gelernten Repräsentationen der einzelnen Journaleinträge dazu verwenden, um die erhaltenen Ergebnisse noch aussagekräftiger zu interpretieren.
Bei etwaigen Fragen wenden Sie sich, wie immer gerne an uns via marco (dot) schreyer (at) unisg (dot) ch. Wir wünschen Ihnen Viel Freude mit unseren Notebooks und Ihren revisorischen Analysen!
Nach der heutigen Übung sollten Sie in der Lage sein:
- Die Grundkonzepte, Funktionsweise und Bestandteile von Autoencoder Neuronalen Netzen zu verstehen.
- Eine Vorverarbeitung von kategorischen Finanzdaten (d.h. One-Hot Encoding und Min-Max Normalisierung) durchzuführen.
- Autoencoder Neuronalen Netze anzuwenden, um Anomalien in umfangreichen Finanzdaten aufzuspüren.
- Die Ergebnisse bzw. den Rekonstruktionsfehler von Autoencoder Neuronalen Netzen zu interpretieren.
Ähnlich wie in den vorangegangenen Übungen werden wir zunächst eine Reihe von Python-Bibliotheken importieren, welche die Datenanalyse und -visualisierung ermöglichen. In dieser Übung werden wir die Bibliotheken PyTorch
, Pandas
, Numpy
, Scikit-Learn
, Matplotlib
und Seaborn
verwenden. Nachfolgend importieren wir die benötigten Bibliotheken durch die Ausführung der folgenden Anweisungen:
# import python data science and utility libraries
import os, sys, itertools, urllib, io, warnings
import datetime as dt
import pandas as pd
import pandas_datareader as dr
import numpy as np
Import der PyTorch
Deep Learning Bibliotheken:
# pytorch libraries
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils import data
from torch.utils.data import dataloader
Import der Matplotlib
und Seaborn
Visualisierungs Bibliotheken und setzen der Visualisierungsparameter:
import matplotlib.pyplot as plt
import seaborn as sns
plt.style.use('seaborn')
plt.rcParams['figure.figsize'] = [10, 5]
plt.rcParams['figure.dpi']= 150
Ausschalten möglicher Warnmeldungen z.B. aufgrund von zukünftigen Änderungen der Bibliotheken:
# set the warning filter flag to ignore warnings
warnings.filterwarnings('ignore')
Aktivieren der sog. Inline-Darstellung von Visualisierungen in Jupyter-Notebook:
%matplotlib inline
Erstellen von Unterverzeichnissen innerhalb des aktuellen Arbeitsverzeichnisses für (1) das Speichern der Originaldaten, (2) der Analyseergebnisse und (3) der trainierten Modelle:
# create the data sub-directory
data_directory = './01_data'
if not os.path.exists(data_directory): os.makedirs(data_directory)
# create the results sub-directory
results_directory = './02_results'
if not os.path.exists(results_directory): os.makedirs(results_directory)
# create the models sub-directory
models_directory = './03_models'
if not os.path.exists(models_directory): os.makedirs(models_directory)
Festlegen eines zufälligen Seeds zur Gewährleistung der Reproduzierbarkeit:
# init deterministic seed
seed_value = 1234
np.random.seed(seed_value) # set numpy seed
torch.manual_seed(seed_value); # set pytorch seed cpu
Aktivieren des GPU Computing, durch setzen des device
flag und setzen eines zufälligen CUDA
Seeds:
# set cpu or gpu enabled device
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu').type
# set pytorch gpu seed
torch.cuda.manual_seed(seed_value)
# log type of device enabled
now = dt.datetime.utcnow().strftime("%Y.%m.%d-%H:%M:%S")
print('[LOG {}] notebook with {} computation enabled'.format(str(now), str(device)))
Anzeige der Hardware Informationen zu den ggf. verfügbaren GPU(s):
!nvidia-smi
Anzeige der Software Informationen über die verfügbaren Python
bzw. PyTorch
Versionen:
# print current Python version
now = dt.datetime.utcnow().strftime("%Y.%m.%d-%H:%M:%S")
print('[LOG {}] The Python version: {}'.format(now, sys.version))
# print current PyTorch version
now = dt.datetime.utcnow().strftime("%Y.%m.%d-%H:%M:%S")
print('[LOG {}] The PyTorch version: {}'.format(now, torch.__version__))
Heutzutage beschleunigen Unternehmen die Digitalisierung von Geschäftsprozessen, wovon auch Enterprise Resource Planning (ERP)-Systeme betroffen sind. Diese Systeme sammeln grosse Mengen Daten auf granularer Ebene. Dies gilt insbesondere für die Journalbuchungen einer Organisation, die innerhalb des Hauptbuch und den jeweiligen Nebenbüchern erfasst werden.
Die Darstellung in Abbildung 1 zeigt eine hierarchische Ansicht eines ERP-Systems, das Journalbuchungen in Datenbanktabellen erfasst. Im Kontext revisorischer Prüfungen können die in solchen Systemen erfassten Daten Spuren bzw. wertvolle Hinweise auf mögliche dolose Handlungen enthalten.
Abbildung 1: Hierarchische Ansicht eines Enterprise Resource Planning (ERP)-Systems, das Geschäftsvorfälle auf verschiedene Abstraktionsebenen in Datenbanktabellen erfasst, d.h. auf Ebene (1) des Geschäftsprozesses, (2) der Buchhaltung sowie (3) der Datenbank.
Zunächst werden wir den im Rahmen des Labs verwendeten Datensatzes deskriptiv analysieren. Anschliessend werden wir die Daten vorverarbeiten um eine Ausgangslage für das Training eines Neuronalen Netzes zu schaffen. Der Lab Datensatz basiert auf einer angepassten Teilmenge des "Synthetic Financial Dataset For Fraud Detection " Datensatz von Lopez-Rojas. Der Originaldatensatz wurde ursprünglich über die Kaggle-Plattform für Data Science Wettbewerbe veröffentlicht und kann über den nachfolgenden Link abgerufen werden kann: https://www.kaggle.com/ntnu-testimon/paysim1.
In einem ersten Schritt laden wir den Datensatz in unsere Analyseumgebung:
# load the dataset into the notebook
url = 'https://raw.githubusercontent.com/GitiHubi/courseACA/main/lab05/data/fraud_dataset.csv'
ori_dataset = pd.read_csv(url)
Anschliessend prüfen wir die Dimensionalität des Datensatzes:
# inspect the datasets dimensionalities
now = dt.datetime.utcnow().strftime("%Y.%m.%d-%H:%M:%S")
print('[LOG {}] transactional dataset of {} rows and {} columns retreived.'.format(now, ori_dataset.shape[0], ori_dataset.shape[1]))
Darüber hinaus speichern wir eine Sicherheitskopie des geladenen Datensatzes mit aktuellem Zeitstempel:
# determine current timestamp
timestamp = dt.datetime.utcnow().strftime("%Y-%m-%d_%H-%M-%S")
# define dataset filename
filename = timestamp + " - original_fraud_dataset.xlsx"
# save dataset extract to the data directory
ori_dataset.head(1000).to_excel(os.path.join(data_directory, filename))
Der Datensatz enthält insgesamt sieben kategorische und zwei numerische Attribute, welche den innerhalb eines SAP FICO Moduls enthaltenen Tabellen BKPF (Buchungsbelegköpfe) und BSEG (Buchungsbelegsegmente) entsprechen. Die nachstehende Liste enthält einen Überblick über die einzelnen Attribute sowie eine kurze Beschreibung ihrer jeweiligen Semantik:
BELNR
: die Nummer des Buchhaltungsbelegs,BUKRS
: der BuchungskreisBSCHL
: der Buchungsschlüssel,HKONT
: das gebuchte Hauptbuchkonto,PRCTR
: das gebuchte Profit Center,WAERS
: der Währungsschlüssel,KTOSL
: der Schlüssel des Hauptbuchkontos,DMBTR
: der Betrag in der Hauswährung,WRBTR
: der Betrag in der Belegwährung.
Sehen wir uns auch einmal die ersten 10 Zeilen des Datensatzes im Detail an:
# inspect top rows of dataset
ori_dataset.head(10)
Vielleicht ist Ihnen bei der Durchsicht der Attribute auch das Attribut mit der Bezeichung Label
in den Daten aufgefallen. Dieses Attribut enthält die Ground-Truth Informationen zu den jeweils einzelnen Buchungen. Das Attribut beschreibt die 'wahre Natur' jeder Transaktion, d.h. ob es sich um eine reguläre Transaktion (gekennzeichnet durch regulär
) oder eine Anomalie (gekennzeichnet durch global
und lokal
) handelt.
Innerhalb unseres Vorgehens werden wir die Label Information nur dazu verwenden, um die Ergebnisse unserer trainierten Modelle zu validieren. Bitte beachten Sie jedoch, dass uns eine solches Feld in der Realität oftmals nicht zur Verfügung steht. Schauen wir uns nun einmal die Verteilung der regulären gegenüber den anomalen Buchungen im Datensatz an:
# number of anomalies vs. regular transactions
ori_dataset.label.value_counts()
Die Analyse zeigt, dass wir es, ähnlich wie in der realen Welt, mit einem unbalanzierten Datensatz konfrontiert sind. D.h. insgesamg enthält der Datensatz nur einen sehr kleinen Anteil von 100 (0,109 %) anomalen Transaktionen. Unter den 100 Anomalien befinden sich 70 (0,076 %) globale Anomalien und 30 (0,003 %) lokale Anomalien.
In einem nächsten Schritt entfernen wir das label
Attribut aus dem Trainingsdatensatz und speichern es in einer gesonderten Variable:
# remove the "ground-truth" label information for the following steps of the class
label = ori_dataset.pop('label')
Aus der Sichtung der Daten geht hervor, dass die Mehrzahl der Attribute kategorische (diskrete) Attributwerte aufweisen, z.B. das Buchungsdatum, das Hauptbuchkonto, die Buchungsart und die Währung. Schauen wir uns nun die Verteilung der kategorischen Attribute Buchungsschlüssel BSCHL
sowie Hauptbuchkonto HKONT
einmal im Detail an:
# prepare to plot posting key and general ledger account side by side
fig, ax = plt.subplots(1, 2)
fig.set_figwidth(18)
# plot the distribution of the posting key attribute
plot = sns.countplot(x=ori_dataset['BSCHL'], ax=ax[0])
# set axis labels
plot.set_xticklabels(plot.get_xticklabels(), rotation=90)
plot.set_xlabel('Attribute Value', fontsize=16)
plot.set_ylabel('Attribute Value Count', fontsize=16)
# set plot title
plot.set_title('Buchungsschlüssel Attribute Value Distribution', fontsize=16)
# plot the distribution of the general ledger attribute
plot = sns.countplot(x=ori_dataset['HKONT'], ax=ax[1])
# set axis labels
plot.set_xticklabels(plot.get_xticklabels(), rotation=90)
plot.set_xlabel('Attribute Value', fontsize=16)
plot.set_ylabel('Attribute Value Count', fontsize=16)
# set plot title
plot.set_title('Hauptbuchkonto Attribute Value Distribution', fontsize=16);
Im Allgemeinen sind Neuronale Netze dafür konzipiert numerische Daten zu verarbeiten. Eine Möglichkeit, diese Anforderung zu erfüllen, ist die Anwendung eines Verfahrens, das als sog. One-Hot Kodierung bezeichnet wird. Hierdurch kann eine numerische Darstellung kategorischer Attributwerte abgeleitet werden. Bei der One-Hot Kodierung wird für jeden kategorischen Attributwert eine zusätzliche binäre Spalte in den Daten erstellt.
Schauen wir uns hierzu das Beispiel in Abbildung 2 unten an. Das kategorische Attribut Receiver in den Orginaldaten enthält die Namen 'Sally', 'John' und 'Emma'. Wir kodieren das Attribut als 'one-hot' Attribut, indem wir eine zusätzliche binäre Spalte für jeden kategorischen Wert in der Spalte 'Receiver' erstellen. Anschliessend kodieren wir z.B. jede Transaktion, die den Wert 'Sally' in der Spalte 'Receiver' aufweist mit dem Wert 1.0 innerhalb der 'Sally' Spalte der Transaktion. Sollte eine Transaktion einen anderen Wert in der Spalte 'Receiver' aufweisen, kodieren wir die 'Sally' Spalte mit dem Wert 0.0.
Abbildung 2: Beispielhafte 'One-Hot' Kodierung der verschiedenen Receiver Attributwerte in spezifische binäre 'One-Hot' Spalten. Dabei resultiert jeder im Datensatz beobachtbare Attributwert in einer eigene Spalte. Der Spaltenwert 1.0 kodiert das Vorkommen des Attributwertes in der entsprechenden Buchung. Der Spaltenwert 0.0 hingegen zeigt, dass der Attributwert nicht innerhalb der entsprechenden Buchung vorkommt.
Anhand dieses Verfahrens können die insgesamt sechs kategorischen Attribute des Datensatzes in numerische Attribute überführt werden. Die Pandas
Bibliothek stellt hierzu die entsprechende Funktionalität zur Verfügung, welche wir im Nachfolgenden anwenden:
# select categorical attributes to be "one-hot" encoded
categorical_attr_names = ['BUKRS', 'KTOSL', 'PRCTR', 'BSCHL', 'HKONT', 'WAERS']
# encode categorical attributes into a binary one-hot encoded representation
ori_dataset_cat_processed = pd.get_dummies(ori_dataset[categorical_attr_names])
Nachfolgend überprüfen wie die vorgenommene One-Hot Kodierung anhand der 10 ersten Buchungen des Datensatzes:
# inspect encoded sample transactions
ori_dataset_cat_processed.head(10)
Anschliessend Analysieren wir nun die Verteilungen der beiden numerischen Attribute des Datensatzes. Hierbei handelt es sich um die Attribute (1) Betrag in Hauswährung DMBTR
und (2) Betrag in Dokumentwährung WRBTR
deren jeweilge Verteilungen wir nachfolgend visualisieren:
# plot the log-scaled 'DMBTR' as well as the 'WRBTR' attribute value distribution
fig, ax = plt.subplots(1,2)
fig.set_figwidth(18)
# plot distribution of the local amount attribute
plot = sns.distplot(ori_dataset['DMBTR'].tolist(), ax=ax[0])
# set axis labels
plot.set_xlabel('Attribute Value', fontsize=16)
plot.set_ylabel('Attribute Value Count', fontsize=16)
# set plot title
plot.set_title('Betrag in Hauswährung - Attribute Value Distribution', fontsize=16)
# plot distribution of the document amount attribute
plot = sns.distplot(ori_dataset['WRBTR'].tolist(), ax=ax[1])
# set axis labels
plot.set_xlabel('Attribute Value', fontsize=16)
plot.set_ylabel('Attribute Value Count', fontsize=16)
# set plot title
plot.set_title('Betrag in Dokumentwährung - Attribute Value Distribution', fontsize=16);
Die Werte beider Betragsattribute weisen eine jeweils schiefe und steile Verteilung auf. Wir skalieren deshalb die Werte zunächst logarithmisch. Anschliessend min-max normalisieren wir die Skalierten Werte:
# select the 'DMBTR' and 'WRBTR' attribute
numeric_attr_names = ['DMBTR', 'WRBTR']
# add a small epsilon to eliminate zero values from data for log scaling
numeric_attr = ori_dataset[numeric_attr_names] + 1e-7
# log scale the 'DMBTR' and 'WRBTR' attribute values
numeric_attr = numeric_attr.apply(np.log)
# normalize all numeric attributes to the range [0,1]
ori_dataset_num_processed = (numeric_attr - numeric_attr.min()) / (numeric_attr.max() - numeric_attr.min())
In einem nächsten Schritt visualisieren wir die Verteilungen der skalierten bzw. normierten Werte beider Betragsattribute:
# plot the log-scaled 'DMBTR' as well as the 'WRBTR' attribute value distribution
fig, ax = plt.subplots(1,2)
fig.set_figwidth(18)
# plot distribution of the local amount attribute
plot = sns.distplot(ori_dataset_num_processed['DMBTR'].tolist(), ax=ax[0])
# set axis labels
plot.set_xlabel('Attribute Value', fontsize=16)
plot.set_ylabel('Attribute Value Count', fontsize=16)
# set plot title
plot.set_title('Betrag in Hauswährung - Attribute Value Distribution', fontsize=16)
# plot distribution of the document amount attribute
plot = sns.distplot(ori_dataset_num_processed['WRBTR'].tolist(), ax=ax[1])
# set axis labels
plot.set_xlabel('Attribute Value', fontsize=16)
plot.set_ylabel('Attribute Value Count', fontsize=16)
# set plot title
plot.set_title('Betrag in Dokumentwährung - Attribute Value Distribution', fontsize=16);
Abschliessend fügen wir die beiden vorverarbeiteten numerischen und kategorischen Attribute zu einem einzigen Datensatz zusammen. Der zusammengeführte Datensatz bildet die Grundlage für das nachfolgende Training des Deep Autoencoder Neural Networks (AENNs):
# merge categorical and numeric subsets
ori_subset_transformed = pd.concat([ori_dataset_cat_processed, ori_dataset_num_processed], axis = 1)
Werfen wir nun abschliessend noch einen finalen einen Blick auf die Dimensionalität des zusammengefügten Datensatzes:
# inspect final dimensions of pre-processed transactional data
ori_subset_transformed.shape
Nach Abschluss der Vorverarbeitungsschritte verfügen wir über einen Datensatz, der aus einer Gesamtzahl von 91.147 Datensätzen (Zeilen) und 618 Attributen (Spalten) besteht. Wir behalten die Anzahl der Spalten im Hinterkopf, da sie die Dimensionalität der Eingabe- und Ausgabeschicht unseres AENNs bestimmen wird.
In diesem Abschnitt möchten wir uns mit der zugrundeliegenden Idee und dem Aufbau eines tiefen Autoencoder Neural Networks (AENN) vertraut zu machen. Hierzu werden wir die einzelne Bausteine und die spezifische Netzwerkstruktur von AENNs anhand der PyTorch
Open-Source-Bibliothek implementieren.
Autoencoder Neural Networks oder auch Replicator Neural Networks sind eine Unsupervised Learning Variante der klassischen Feed-Forward Netze. Diese besondere Architektur wurde ursprünglich von Goeffery Hinton und Ruslan Salakhutdinov entwickelt. AENNs bestehen in der Regel aus einer symmetrischen Netzarchitektur sowie einer zentralen verborgenen Schicht, die als latente oder engpass Schicht bezeichnet wird. Diese Schicht weist eine geringere Dimensionalität als die Eingabe- und Ausgabeschicht des Netzwerks auf. Das Lernziel des AENN besteht darin, die ursprünglichen Eingabedaten an der Ausgabeschicht des Netzes möglichst fehlerfrei zu rekonstruieren. Abbildung 3 zeigt eine schematische Darstellung eines Autoencoder Neural Network's:
Abbildung 3: Schematische Darstellung eines Autoencodernetzes, das aus zwei nicht-linearen Abbildungen bzw. Feed-Forward-Netzen besteht. Die beiden miteinander verknüpften Netze werden als Encoder $f_\theta: \mathbb{R}^{dx} \mapsto \mathbb{R}^{dz}$ und Decoder $g_\theta: \mathbb{R}^{dz} \mapsto \mathbb{R}^{dx}$ bezeichnet.
Grundsäzlich können AENNs als 'verlustbehaftete' Komprimierungsalgorithmen interpretiert werden. Sie sind 'verlustbehaftet' in dem Sinne, dass die rekonstruierten Ausgaben im Vergleich zu den ursprünglichen Eingaben Fehler aufweisen können. Die Differenz zwischen der ursprünglichen Eingabe $x^i$ und ihrer Rekonstruktion $\hat{x}^i$ wird auch als Rekonstruktionsfehler bezeichnet. Im Allgemeinen bestehen AENNs aus drei Hauptbestandteilen:
- einem Encoder $f_\theta$,
- einer Decoder $g_\theta$,
- einer Fehlerfunktion $\mathcal{L_{\theta}}$.
Der Encoder und Decoder bestehen jeweils aus einem klassischen Feedforward Netz mit zu lernenden Parametern $\theta$. Das Encodernetz $f_\theta(\cdot)$ bildet einen Eingabevektor (z.B. eine Buchung der Finanzbuchhaltung) $x^i$ auf eine komprimierte (d.h. niedrig dimensionale) Repräsentation $z^i$ ab im sog. latenten Raum $Z$ ab. Die niedrig-dimensionale Repräsentation $z^i$ wird anschliessend durch das Decodernetz $g_\theta(\cdot)$ auf einen Ausgabevektor $\hat{x}^i$ des ursprünglichen Eingaberaums (z.B. die rekonstruierte Buchung der Finanzbuchhaltung) abgebildet. Formal können die beiden Netze jeweils auch als nicht-lineare Abbildungen bzw. Funktion interpretiert werden:
wobei die beiden Funktionen die zu lernenden Modellparameter $\theta = \{W, b, W', d\}$ aufweisen. Die Parameter $W \in \mathbb{R}^{d_x \times d_z}, W' \in \mathbb{R}^{d_z \times d_y}$ bezeichnen die Gewichtsmatrizen, die Parameter $b \in \mathbb{R}^{dx}$, $d \in \mathbb{R}^{dz}$ die Bias-Vektoren der Netze, und $s$ bzw. $s′$ die jeweils nicht-linearen Aktivierungsfunktionen.
In einem nächsten Schritt möchten wir nun das Encoder Netz in PyTorch
implementieren. Der Encoder soll aus insgesamt neun Schichten von fully-connected Neuronen bestehen. Darüber hinaus solle der Encoder die nachfolgende Anzahl von Neuronen pro Schicht enthalten: 618-256-128-64-32-16-8-4-3. Die vorhergehende Notation bedeutet, dass die erste Schicht 618 Neuronen umfasst (bestimmt durch die Dimensionalität der Eingabedaten), die zweite Schicht 256 Neuronen und die weiteren Schichten 128, 64, 32, 16, 8, 4 bzw. 3 Neuronen.
Den nachfolgenden drei Elementen der Implementierung des Encoder Netzes möchten wir eine besonderes Augenmerk schenken:
self.encoder_Lx
: definiert die lineare Transformation der jeweiligen Schicht, welche auf die Eingabe angewandt wird: $Wx + b$.nn.init.xavier_uniform
: initialisiert Gewichtsparameter anhand einer gleichmäßigen Xavier Verteilung.self.encoder_Rx
: definiert die nicht-lineare Transformation der jeweiligen Schicht, welche auf die Eingabe angewandt wird: $\sigma(\cdot)$.
Wir verwenden sog. Leaky ReLUs, um saturierende bzw. 'sterbende' Neuronen zu vermeiden und die Trainingskonvergenz zu beschleunigen. Die Anwendung von Leaky ReLUs ermöglicht die Berechnung von Gradienten auch innerhalb des negativen Bereichs einer Aktivierungsfunktion (siehe Schaubild oben).
# implementation of the encoder network
class encoder(nn.Module):
# define class constructor
def __init__(self):
# call super class constructor
super(encoder, self).__init__()
# specify layer 1 - in 618, out 512
self.encoder_L1 = nn.Linear(in_features=ori_subset_transformed.shape[1], out_features=512, bias=True) # add linearity
nn.init.xavier_uniform_(self.encoder_L1.weight) # init weights according to [9]
self.encoder_R1 = nn.LeakyReLU(negative_slope=0.4, inplace=True) # add non-linearity according to [10]
# specify layer 2 - in 512, out 256
self.encoder_L2 = nn.Linear(512, 256, bias=True)
nn.init.xavier_uniform_(self.encoder_L2.weight)
self.encoder_R2 = nn.LeakyReLU(negative_slope=0.4, inplace=True)
# specify layer 3 - in 256, out 128
self.encoder_L3 = nn.Linear(256, 128, bias=True)
nn.init.xavier_uniform_(self.encoder_L3.weight)
self.encoder_R3 = nn.LeakyReLU(negative_slope=0.4, inplace=True)
# specify layer 4 - in 128, out 64
self.encoder_L4 = nn.Linear(128, 64, bias=True)
nn.init.xavier_uniform_(self.encoder_L4.weight)
self.encoder_R4 = nn.LeakyReLU(negative_slope=0.4, inplace=True)
# specify layer 5 - in 64, out 32
self.encoder_L5 = nn.Linear(64, 32, bias=True)
nn.init.xavier_uniform_(self.encoder_L5.weight)
self.encoder_R5 = nn.LeakyReLU(negative_slope=0.4, inplace=True)
# specify layer 6 - in 32, out 16
self.encoder_L6 = nn.Linear(32, 16, bias=True)
nn.init.xavier_uniform_(self.encoder_L6.weight)
self.encoder_R6 = nn.LeakyReLU(negative_slope=0.4, inplace=True)
# specify layer 7 - in 16, out 8
self.encoder_L7 = nn.Linear(16, 8, bias=True)
nn.init.xavier_uniform_(self.encoder_L7.weight)
self.encoder_R7 = nn.LeakyReLU(negative_slope=0.4, inplace=True)
# specify layer 8 - in 8, out 4
self.encoder_L8 = nn.Linear(8, 4, bias=True)
nn.init.xavier_uniform_(self.encoder_L8.weight)
self.encoder_R8 = nn.LeakyReLU(negative_slope=0.4, inplace=True)
# specify layer 9 - in 4, out 3
self.encoder_L9 = nn.Linear(4, 3, bias=True)
nn.init.xavier_uniform_(self.encoder_L9.weight)
self.encoder_R9 = nn.LeakyReLU(negative_slope=0.4, inplace=True)
# define forward pass
def forward(self, x):
# define forward pass through the network
x = self.encoder_R1(self.encoder_L1(x))
x = self.encoder_R2(self.encoder_L2(x))
x = self.encoder_R3(self.encoder_L3(x))
x = self.encoder_R4(self.encoder_L4(x))
x = self.encoder_R5(self.encoder_L5(x))
x = self.encoder_R6(self.encoder_L6(x))
x = self.encoder_R7(self.encoder_L7(x))
x = self.encoder_R8(self.encoder_L8(x))
x = self.encoder_R9(self.encoder_L9(x))
return x
In einem nächsten Schritt instanzieren wir ein Modell des Encoder Netzes:
# intstantiate the encoder network model
encoder_train = encoder()
Anschliessend transferieren wir das Encoder Modell auf die CPU
oder eine ggf. verfügbare GPU
:
# push model to compute device
encoder_train = encoder_train.to(device)
Sofern verfügbar, prüfen wir ob das Modell erfolgreich auf die GPU
übertragen wurde:
!nvidia-smi
Nun können wir die Modellstruktur visualisieren und die Netzarchitektur nochmals durch das Ausführen der folgenden Zelle überprüfen:
# print the initialized architectures
now = dt.datetime.utcnow().strftime("%Y.%m.%d-%H:%M:%S")
print('[LOG {}] encoder architecture:\n\n{}\n'.format(now, encoder_train))
In einem nächsten Schritt vervollständigen wir nun die Autoencoder Architektur durch die Implementierung des entsprechenden Decoder Netzes. Der Decoder soll ebenfall aus insgesamt neun Schichten von fully-connected Neuronen bestehen. Zudem soll der Decoder die Architektur des Encoders symmetrisch spiegeln. Wir invertieren hierzu die Ausgestaltung der Schichten des Encoders schichtweise, gemäss der folgenden Struktur 3-4-8-16-32-64-128-256, im Rahmen der Implementierung des Decoders:
# implementation of the decoder network
class decoder(nn.Module):
# define class constructor
def __init__(self):
# call super class constructor
super(decoder, self).__init__()
# specify layer 1 - in 3, out 4
self.decoder_L1 = nn.Linear(in_features=3, out_features=4, bias=True) # add linearity
nn.init.xavier_uniform_(self.decoder_L1.weight) # init weights according to [9]
self.decoder_R1 = nn.LeakyReLU(negative_slope=0.4, inplace=True) # add non-linearity according to [10]
# specify layer 2 - in 4, out 8
self.decoder_L2 = nn.Linear(4, 8, bias=True)
nn.init.xavier_uniform_(self.decoder_L2.weight)
self.decoder_R2 = nn.LeakyReLU(negative_slope=0.4, inplace=True)
# specify layer 3 - in 8, out 16
self.decoder_L3 = nn.Linear(8, 16, bias=True)
nn.init.xavier_uniform_(self.decoder_L3.weight)
self.decoder_R3 = nn.LeakyReLU(negative_slope=0.4, inplace=True)
# specify layer 4 - in 16, out 32
self.decoder_L4 = nn.Linear(16, 32, bias=True)
nn.init.xavier_uniform_(self.decoder_L4.weight)
self.decoder_R4 = nn.LeakyReLU(negative_slope=0.4, inplace=True)
# specify layer 5 - in 32, out 64
self.decoder_L5 = nn.Linear(32, 64, bias=True)
nn.init.xavier_uniform_(self.decoder_L5.weight)
self.decoder_R5 = nn.LeakyReLU(negative_slope=0.4, inplace=True)
# specify layer 6 - in 64, out 128
self.decoder_L6 = nn.Linear(64, 128, bias=True)
nn.init.xavier_uniform_(self.decoder_L6.weight)
self.decoder_R6 = nn.LeakyReLU(negative_slope=0.4, inplace=True)
# specify layer 7 - in 128, out 256
self.decoder_L7 = nn.Linear(128, 256, bias=True)
nn.init.xavier_uniform_(self.decoder_L7.weight)
self.decoder_R7 = nn.LeakyReLU(negative_slope=0.4, inplace=True)
# specify layer 8 - in 256, out 512
self.decoder_L8 = nn.Linear(256, 512, bias=True)
nn.init.xavier_uniform_(self.decoder_L8.weight)
self.decoder_R8 = nn.LeakyReLU(negative_slope=0.4, inplace=True)
# specify layer 9 - in 512, out 618
self.decoder_L9 = nn.Linear(in_features=512, out_features=ori_subset_transformed.shape[1], bias=True)
nn.init.xavier_uniform_(self.decoder_L9.weight)
self.decoder_R9 = nn.LeakyReLU(negative_slope=0.4, inplace=True)
# define forward pass
def forward(self, x):
# define forward pass through the network
x = self.decoder_R1(self.decoder_L1(x))
x = self.decoder_R2(self.decoder_L2(x))
x = self.decoder_R3(self.decoder_L3(x))
x = self.decoder_R4(self.decoder_L4(x))
x = self.decoder_R5(self.decoder_L5(x))
x = self.decoder_R6(self.decoder_L6(x))
x = self.decoder_R7(self.decoder_L7(x))
x = self.decoder_R8(self.decoder_L8(x))
x = self.decoder_R9(self.decoder_L9(x)) # don't apply dropout to the AE output
return x
Wir instanzieren nun auch das Decoder Modell für das CPU
bzw. GPU
Training und überzeugen uns davon, dass das Modell erfolgreich initialisiert wurde. Hierzu visualisieren wir wieder die Netzarchitektur durch das Ausführen der nachfolgenden Zelle:
# intstantiate the decoder network model
decoder_train = decoder()
# push model to compute device
decoder_train = decoder_train.to(device)
# print the initialized architectures
now = dt.datetime.utcnow().strftime("%Y.%m.%d-%H:%M:%S")
print('[LOG {}] decoder architecture:\n\n{}\n'.format(now, decoder_train))
Abschliessend werfen wir noch einen Blick auf die Anzahl der Modellparameter, die wir im Folgenden beabsichtigen zu trainieren:
# init the number of encoder model parameters
encoder_num_params = 0
# iterate over the distinct encoder parameters
for param in encoder_train.parameters():
# collect number of parameters
encoder_num_params += param.numel()
# init the number of decoder model parameters
decoder_num_params = 0
# iterate over the distinct decoder parameters
for param in decoder_train.parameters():
# collect number of parameters
decoder_num_params += param.numel()
# print the number of model paramters
now = dt.datetime.utcnow().strftime("%Y.%m.%d-%H:%M:%S")
print('[LOG {}] number of to be trained AENN model parameters: {}.'.format(now, encoder_num_params + decoder_num_params))
Ok, unser AENN Modell umfasst eine beachtliche Gesamtzahl von 985.021 zu trainierenden Modellparametern.
Nach erfolgreicher Instanziierung des AENN Modells möchten wir das Modell nun trainieren. Bevor wir jedoch mit dem Training beginnen, ist es notwendig eine geeignete Fehlerfunktion zu definieren. Zur Erinnerung: Wir wollen unser Modell so trainieren, dass es ein Set von Encoder bzw. Decoder Parametern $\theta$ lernt, welche die Ähnlichkeit einer gegebenen Buchung $x^{i}$ und ihrer Rekonstruktion $\hat{x}^{i} = g_\theta(f_\theta(x^{i}))$ maximiert.
Formal ausgedrückt besteht unser Trainingsziel darin, Parameter $\theta^*$ zu lernen, für welche gilt $\arg\min_{\theta} \|X - g_\theta(f_\theta(X))\|$. Um dieses Optimierungsziel zu erreichen, möchten wir mit zunehmenden Training die Fehlerfunktion bzw. einen sog. Rekonstruktionsfehler $\mathcal{L_{\theta}}$ kontinuierlich minimieren. Eine hierfür geeignete Fehlerfunktion findet sich im sog. Binary-Cross-Entropy (BCE) Rekonstruktionsfehler, der formal wie nachfolgend definiert ist:
wobei $x^{i}$, $i=1,...,n$ die Menge an Buchungen bezeichnet, $\hat{x}^{i}$ die jeweiligen Rekonstruktionen und $j=1,...,k$ die verschiedenen Buchungsattribute indexiert. Nachfolgend instanzieren wir die entsprechende BCE Fehlerfunktion der PyTorch
Bibliothek:
# define the optimization criterion / loss function
loss_function = nn.BCEWithLogitsLoss()
Anschliessend transferieren wir die Berechnung der Fehlerfunktion auf die CPU
oder eine ggf. verfügbare GPU
:
# push the optimization criterion / loss function to compute device
loss_function = loss_function.to(device)
Auf der Grundlage der Fehlerhöhe eines Mini-Batches an Buchungen berechnet die PyTorch
Bibliothek automatisiert die Gradienten. Anschliessend werden AENN-Parameter $\theta$ auf Grundlage der ermittelten Gradienten optimiert. Hierzu ist es lediglich notwendig das gewünschte Optimierungsverfahren in PyTorch zu definieren. In der nachfolgenden Notebook Zelle verwenden wir das sog. Adam Optimierungsverfahren für die Optimierung der Modellparameter $\theta$. Darüber hinaus definieren eine Lernrate $l = 0.0001$:
# set the learning rate
learning_rate = 1e-4
#set the paramete optimization strategy of both networks
encoder_optimizer = torch.optim.Adam(encoder_train.parameters(), lr=learning_rate)
decoder_optimizer = torch.optim.Adam(decoder_train.parameters(), lr=learning_rate)
Nachdem wir die drei Bausteine des AENN-Modells erfolgreich implementiert und instanziiert haben. Nehmen wir uns Zeit, die Definition der Modelle Encoder und Decoder sowie des BCE-Rekonstruktionsfehlers nochmals zu überprüfen und etwaige Fragen zu besprechen.
In diesem Abschnitt möchten wir nun ein AENN-Modell anhand der kodierten Transaktionsdaten trainieren. Darüber hinaus werfen wir einen detaillierten Blick auf die einzelnen Trainingshyperparameter und Trainingsschritte sowie den Trainingsfortschritt im Zeitverlauf.
Beginnen wir nun damit, ein AENN Modell für 5 Trainingsepochen und 128 Buchungen pro Mini-Batch zu trainieren. Diese Konfiguration der Hyperparameter bedeutet, dass der Datensatz dem AENN ingesamt fünfmal in Mini-Batches von 128 jeweils Buchungen zugeführt wird. Diese Hyperparameter Konfiguration hat zu Folge, dass pro Trainingsepoche 713 Updates (91.247 Buchungen modulo 128 Buchungen pro Mini-Batch) der AENN Modellparameter erfolgen.
# specify training parameters
num_epochs = 5 # number of training epochs
mini_batch_size = 128 # size of the mini-batches
Während der Trainingsphase sollen dem AENN Modell kontinuierlich Mini-Batches der gesamten Population von Buchungen zugeführt werden. Hierzu verwenden wir die DataLoader
Funktionalität der PyTorch
Bibliothek. Dabei handelt es sich im sog. Iteratoren, welche die Buchungen kontinuierlich in Form von Mini-Batches zur Verfügung stellen. In der nachfolgenden Zelle instanzieren wir einen PyTorch Dataloader der Buchungsdaten:
# convert pre-processed transactional data to PyTorch tensor
torch_dataset = torch.from_numpy(ori_subset_transformed.values).float()
# push pre-processed transactional data to compute device
torch_dataset = torch_dataset.to(device)
# init training dataloader
train_dataloader = dataloader.DataLoader(torch_dataset, batch_size=mini_batch_size, shuffle=True)
(Hinweis: Durch das Setzen des Parameter shuffle
werden Mini-Batches in unterschiedlicher Reihenfolge pro Epoche zur Verfügung gestellt.)
Nach Definition der Hyperparameter können wir mit dem Training des Modells beginnen. Für jeden zugeführten Mini-Batch werden im Rahmen des Trainingsprozesses die nachfolgenden Schritte ausgeführt:
- Durchführung des Forwardpass durch das Encoder- und Decoder-Net.
- Berechnen des BCE-Rekonstruktionsfehlers $\mathcal{L^{BCE}_{\theta}}(x^{i};\hat{x}^{i})$.
- Durchführung des Backwardpass durch das Decoder- und Encoder-Netz.
- Update der Encoder $f_\theta(\cdot)$- und Decoder $g_\theta(\cdot)$ Parameter.
Um das Lernen während des Trainings zu gewährleisten, beobachten wir den BCE Rekonstruktionsfehler des AENN-Modells mit fortschreitendem Training. Durch diese Beobachtung ist es möglich auf den Lernfortschritt des Modells zu schliessen. Darüber hinaus kann festgestellt werden, ob bzw. wann der Rekonstruktionsfehler konvergiert.
Im Rahmen der Modelloptimierung möchten wir den nachfolgenden PyTorch
Anweisungen eine besondere Beachtung schenken:
reconstruction_loss.backward()
Berechnung die Gradienten auf der Grundlage des Rekonstruktionsfehlers.encoder_optimizer.step()
unddecoder_optimizer.step()
Aktualisierung der Parameter auf Grundlage der Gradienten.
Nach jeder abgeschlossenen Trainingsepoche möchten wir zudem einen sog. Modell Checkpoint speichern. Die Checkpoints enthalten eine Bestandsaufnahme bzw. 'Schnappschuss' der Modellparameter. Im Allgemeinen ist es eine gute Praxis, während des Trainings solche Checkpoints in regelmässigen Abständen zu speichen. Sollte das Training einmal unterbrochen werden, kann es beginnend auf dem letzten Checkpoint wieder fortgesetzt werden. Für das Speichern eines Modell Checkpoints verwenden wir die nachfolgende PyTorch
Anweisung:
torch.save()
: speichert den Checkpoint der aktuellen Modellparameterwerte auf dem lokalen Dateisystem.
# init collection of training epoch losses
train_epoch_losses = []
# set the model in training mode (apply dropout when needed)
encoder_train.train()
decoder_train.train()
# init the best loss by setting it to infinity
best_loss = np.inf
# train autoencoder model
for epoch in range(num_epochs):
# init collection of epoch losses
train_mini_batch_losses = []
# init mini batch counter
mini_batch_count = 0
# iterate over all mini-batches
for mini_batch_data in train_dataloader:
# increase mini batch counter
mini_batch_count += 1
# push mini batch data to compute device
mini_batch_data = mini_batch_data.to(device)
# =================== (1) forward pass ===================================
# run forward pass
z_representation = encoder_train(mini_batch_data) # encode mini-batch data
mini_batch_reconstruction = decoder_train(z_representation) # decode mini-batch data
# =================== (2) compute reconstruction loss ====================
# determine reconstruction loss
reconstruction_loss = loss_function(mini_batch_reconstruction, mini_batch_data)
# =================== (3) backward pass ==================================
# reset graph gradients
decoder_optimizer.zero_grad()
encoder_optimizer.zero_grad()
# run backward pass
reconstruction_loss.backward()
# =================== (4) update model parameters ========================
# update network parameters
decoder_optimizer.step()
encoder_optimizer.step()
# =================== monitor training progress ==========================
# print training progress each 1.000 mini-batches
if mini_batch_count % 1000 == 0:
# print mini batch reconstuction results
now = dt.datetime.utcnow().strftime("%Y.%m.%d-%H:%M:%S")
print('[LOG {}] epoch: [{}/{}], batch: {}, batch-train-loss: {}'.format(str(now), str(epoch+1), str(num_epochs), str(mini_batch_count), str(np.round(reconstruction_loss.item(), 8))))
# collect mini-batch loss
train_mini_batch_losses.extend([reconstruction_loss.item()])
# =================== evaluate model performance =============================
# determine mean min-batch loss of epoch
train_epoch_loss = np.mean(train_mini_batch_losses)
# print training epoch results
now = dt.datetime.utcnow().strftime("%Y.%m.%d-%H:%M:%S")
print('[LOG {}] epoch: [{}/{}], epoch-train-loss: {}'.format(str(now), str(epoch+1), str(num_epochs), str(np.round(train_epoch_loss, 8))))
# determine mean min-batch loss of epoch
train_epoch_losses.append(train_epoch_loss)
# =================== save model snapshot to disk ============================
# case: new best model trained
if train_epoch_loss < best_loss:
# save trained encoder model file to disk
encoder_model_name = "ep_{}_encoder_model.pth".format((epoch+1))
torch.save(encoder_train.state_dict(), os.path.join(models_directory, encoder_model_name))
# save trained decoder model file to disk
decoder_model_name = "ep_{}_decoder_model.pth".format((epoch+1))
torch.save(decoder_train.state_dict(), os.path.join(models_directory, decoder_model_name))
# update best loss
best_loss = train_epoch_loss
# print epoch loss
now = dt.datetime.utcnow().strftime("%Y.%m.%d-%H:%M:%S")
print('[LOG {}] epoch: [{}/{}], new best epoch-train-loss: {} found'.format(str(now), str(epoch+1), str(num_epochs), str(np.round(train_epoch_loss, 8))))
In einem nächsten Schritt visualisieren wir den jeweiligen Rekonstruktionsfehler für jede Trainingsepoche:
# prepare plot
fig = plt.figure()
ax = fig.add_subplot(111)
fig.set_figwidth(18)
# add grid
ax.grid(linestyle='dotted')
# plot the training epochs vs. the epochs' prediction error
ax.plot(np.array(range(1, len(train_epoch_losses)+1)), train_epoch_losses, label='epoch loss (blue)')
# add axis legends
ax.set_xlabel("[Training Epoch $e_i$]", fontsize=14)
ax.set_ylabel("[Reconstruction Error $\mathcal{L}^{BCE}$]", fontsize=14)
# set plot legend
plt.legend(loc="upper right", numpoints=1, fancybox=True)
# add plot title
plt.title('Training Epochs $e_i$ vs. Reconstruction Error $L^{BCE}$', fontsize=16);
Es ist zu beobachten, dass der Rekonstruktionsfehler des AENN Models nach fünf Epochen kontinuierlich zu sinken beginnt. Diese Beobachtung impliziert, dass es dem Modell sukzessive gelingt die innerhalb des Datensatzes enthaltenen Buchungen zu rekonstruieren. Anhand der Visualisierung wird jedoch auch deutlich, dass das Modell noch einige Epochen weiter trainiert werden könnte bis der Rekonstruktionsfehler nicht mehr sinkt bzw. konvergiert.
In diesem Abschnitt möchten wir die Fähigkeit des erlernten AENN Modells zur Erkennung von Anomalien in Buchhaltungsdaten evaluieren. Hierzu werden wir auf vortrainierte AENN Modelle zurück greifen. Die Evaluation umfasst die lokalen als auch die globalen Anomalien des Datensatzes.
Für die Evaluation laden wir üblicherweise das AAENN-Modell mit geringstem Rekonstruktions- bzw. Diskriminationsfehler. Pro Trainingsepoche wurde im Rahmen des Modelltrainings jeweils ein Checkpoint der Modellparameter innerhalb des lokalen Modellverzeichnis gespeichert. Wir werden nun die bereits für 30 Trainingsepochen trainierten Modell Checkpoint laden:
# restore pretrained model checkpoint
encoder_model_name = 'https://raw.githubusercontent.com/GitiHubi/courseACA/main/lab05/models/ep_30_encoder_model_small.pth'
decoder_model_name = 'https://raw.githubusercontent.com/GitiHubi/courseACA/main/lab05/models/ep_30_decoder_model_small.pth'
# read stored model from the remote location
encoder_bytes = urllib.request.urlopen(encoder_model_name)
decoder_bytes = urllib.request.urlopen(decoder_model_name)
# load tensor from io.BytesIO object
encoder_buffer = io.BytesIO(encoder_bytes.read())
decoder_buffer = io.BytesIO(decoder_bytes.read())
# init evaluation encoder and decoder model
encoder_eval = encoder()
decoder_eval = decoder()
# push encoder and decoder model to compute device
encoder_eval = encoder_train.to(device)
decoder_eval = decoder_train.to(device)
# load trained models
encoder_eval.load_state_dict(torch.load(encoder_buffer, map_location=lambda storage, loc: storage))
decoder_eval.load_state_dict(torch.load(decoder_buffer, map_location=lambda storage, loc: storage))
Nach erfolgreichem Laden des Modell Checkpoints transferieren wir das Modell für Evaluierungszwecke auf die CPU
(Hinweis: Dies ermöglicht uns die Rekonstruktionsfehler aller Buchungen ohne etwaige Limitierungen durch den Arbeitsspeicher der GPU
zu berechnen):
# set networks in evaluation mode (don't apply dropout)
encoder_eval.eval()
decoder_eval.eval()
# push encoder and decoder model to compute device
encoder_eval = encoder_eval.to('cpu')
decoder_eval = decoder_eval.to('cpu')
# push the dataset to the CPU
torch_dataset = torch_dataset.to('cpu')
# push the loss function to the CPU
loss_function = loss_function.to('cpu')
In einem nächsten Schritt berechnen wir die individuellen BCE Rekonstruktionsfehler für jede Buchung $x_{i}$ innerhalb des Datensatzes. Zu diesem Zweck wird zunächst die Rekonstruktion $\hat{x}_{i}$ jeder Buchung ermittelt. Im Anschluss wird der BCE Rekonstruktionsfehler der rekonstruierten Buchung $\hat{x}_{i}$ durch Vergleich mit der originalen Buchungen $x_{i}$ des Datensatzes berechnet:
# reconstruct encoded transactional data
reconstruction = decoder_eval(encoder_eval(torch_dataset))
# init binary cross entropy errors
reconstruction_loss_transaction = np.zeros(reconstruction.size()[0])
# iterate over all detailed reconstructions
for i in range(0, reconstruction.size()[0]):
# determine reconstruction loss - individual transactions
reconstruction_loss_transaction[i] = loss_function(reconstruction[i], torch_dataset[i]).item()
if(i % 10000 == 0):
### print conversion summary
now = dt.datetime.utcnow().strftime("%Y.%m.%d-%H:%M:%S")
print('[LOG {}] collected individual reconstruction loss of: {:06}/{:06} transactions'.format(now, i, reconstruction.size()[0]))
Nach Berechnung der individuellen Rekonstruktionsfeheler visualiseren wir die Höhe des jeweils berechneten Fehlers:
# prepare plot
fig = plt.figure()
ax = fig.add_subplot(111)
# set plot size
fig.set_figwidth(16)
fig.set_figheight(8)
# assign unique id to transactions
plot_data = np.column_stack((np.arange(len(reconstruction_loss_transaction)), reconstruction_loss_transaction))
# obtain regular transactions as well as global and local anomalies
regular_data = plot_data[label == 'regular']
global_outliers = plot_data[label == 'global']
local_outliers = plot_data[label == 'local']
# plot reconstruction error scatter plot
ax.scatter(regular_data[:, 0], regular_data[:, 1], c='C0', alpha=0.4, marker="o", s=30, label='regular') # plot regular transactions
ax.scatter(global_outliers[:, 0], global_outliers[:, 1], c='C1', marker="^", s=80, label='global') # plot global outliers
ax.scatter(local_outliers[:, 0], local_outliers[:, 1], c='C2', marker="*", s=80, label='local') # plot local outliers
# add plot legend of transaction classes
ax.legend(loc='best')
# add axis legends
ax.set_xlabel("[Journal Entry ID $x_i$]", fontsize=14)
ax.set_ylabel("[Reconstruction Error $\mathcal{L}^{BCE}$]", fontsize=14)
# add plot title
plt.title('Journal Entry ID $x_i$ vs. Reconstruction Error $L^{BCE}$', fontsize=16);
Die Visualisierung zeigt, dass das unser AENN Modell die meisten regulären Buchungen mit geringem Fehler rekonstruieren kann. Prallel weisen die globalen Anomalien (grün) als auch lokalen Anomalien (rot) einen Vergleichsweise hohen Rekonstruktionsfehler auf. Auf Grundlage dieses Analyseergebnis lässt sich konstatieren, dass es Anhand der Rekonstruktionsfehler möglich ist, Anomalien (grün und rot) von den regulären Buchungen (blau) des Buchungsstoffes zu unterscheiden.
Um diese Feststellung weiter zu untersuchen wollen wir nun die Buchungen, die einen Rekonstruktionsfehler >= 0.12 aufweisen aus dem Datensatz filtern. Darüber hinaus nehmen wir (wie oben dargestellt) an, dass diese Buchungen den globalen Anomalien des Buchungsstoffes entsprechen:
# append labels to original dataset
ori_dataset['label'] = label
# extract transactions exhibiting a reconstruction error >= 0.12
autoencoder_global_anomalies = ori_dataset[reconstruction_loss_transaction >= 0.12]
# inspect transactions exhibiting a reconstruction error >= 0.12
autoencoder_global_anomalies
Lassen Sie uns die nun die Buchungen in eine Excel-Tabelle extrahieren, um diese dem Prüfungsteam zur Verfügung zu stellen. Hierzu werden wir in einem ersten Schritt einen Zeitstempel des Datenextrakts für den Audit-Trail der Prüfung generieren:
timestamp = dt.datetime.utcnow().strftime("%Y-%m-%d_%H-%M-%S")
Anschliessend extrahieren wir die gefilterten globalen Anomalien als Excel-Datei zur weiteren substantiellen Prüfung:
# specify the filename of the excel spreadsheet
filename = str(timestamp) + " - ACA_001_autoencoder_global_anomalies.xlsx"
# specify the target data directory of the excel spreadsheet
data_directory = os.path.join(results_directory, filename)
# extract the filtered transactions to excel
autoencoder_global_anomalies.to_excel(data_directory, header=True, index=False, sheet_name="Global_Anomalies", encoding="utf-8")
Schauen wir uns nun auch die Buchungen genauer an, die einen Rekonstruktionsfehler >= 0.04 jedoch =< 0.12 aufweisen. Hierbei nehmen wir (wie oben dargestellt) an, dass diese Buchungen lokalen Anomalien des Buchungsstoffes entsprechen:
# extract transactions exhibiting a reconstruction error < 0.12 and >= 0.04
autoencoder_local_anomalies = ori_dataset[(reconstruction_loss_transaction >= 0.04) & (reconstruction_loss_transaction < 0.12)]
# inspect transactions exhibiting a reconstruction error < 0.12 and >= 0.04
autoencoder_local_anomalies
Lassen Sie uns die gefilterten Transaktionen erneut in eine Excel-Tabelle extrahieren, um sie dem Prüfungsteam zur Verfügung zu stellen. Hierzu werden wir zunächst wieder einen Zeitstempel des Datenextrakts für den Audit-Trail der Prüfung generieren:
timestamp = dt.datetime.utcnow().strftime("%Y-%m-%d_%H-%M-%S")
Anschliessend extrahieren wir die gefilterten lokalen Anomalien als Excel-Datei zur weiteren substantiellen Prüfung:
# specify the filename of the excel spreadsheet
filename = str(timestamp) + " - ACA_002_autoencoder_local_anomalies.xlsx"
# specify the target data directory of the excel spreadsheet
data_directory = os.path.join(results_directory, filename)
# extract the filtered transactions to excel
autoencoder_local_anomalies.to_excel(data_directory, header=True, index=False, sheet_name="Local_Anomalies", encoding="utf-8")
Im realen Prüfungskontext ist es in der Regel von Vorteil, zusätzlich zur Höhe des Rekonstruktionsfehlers die durch das AENN-Modell erlernten Repräsentationen der Buchungen zu untersuchen. Eine solche Untersuchung ermöglicht es Rückschlüsse über die strukturelle Semantik des Buchungsstoffes und der Buchungsattribute zu ziehen. Darüber hinaus bietet die Analyse die Möglichkeit evtl. ermittelte Anomalien in den Gesamtkontext des Buchungstoffes einzordnen
Um die Repräsentation der Buchungen zu erhalten führen wir für jede Buchung einen Forwardpass durch den Encoder des AENN Modells durch. Hierzu laden wir einen bereits für 80 Epochen trainierten Model Checkpoint des Encoder Netzes:
# restore pretrained model checkpoint
encoder_model_name = 'https://raw.githubusercontent.com/GitiHubi/courseACA/main/lab05/models/ep_80_encoder_model_small.pth'
# read stored model from the remote location
encoder_bytes = urllib.request.urlopen(encoder_model_name)
# load tensor from io.BytesIO object
encoder_buffer = io.BytesIO(encoder_bytes.read())
# init evaluation encoder and decoder model
encoder_eval = encoder()
# push encoder and decoder model to compute device
encoder_eval = encoder_eval.to('cpu')
# load trained models
encoder_eval.load_state_dict(torch.load(encoder_buffer, map_location=lambda storage, loc: storage))
In einem nächsten Schritt führen wir für jede Buchung einen Forwardpass durch das AENN Modell durch. Um Ergebnis erhalten wir somit die drei-dimensionale Repräsentation jeder Buchung:
# push the dataset to the CPU
torch_dataset = torch_dataset.to('cpu')
# run forward path through encoder to obtain journal entry representations
entry_representations = encoder_eval(torch_dataset)
# convert the representations to a pandas dataframe
entry_representation = pd.DataFrame(entry_representations.data.cpu().numpy(), columns=['z1', 'z2', 'z3'])
Anschliessend versehen wir, für Validierungs- und Visualierungszwecke, die Repräsentationen mit den ursprünglichen Labeln:
entry_representation['label'] = label
Vor der Visualisierung werfen wir noch einen prüfenden Blick auf die erhaltenen Koordinaten:
entry_representation.head(10)
Aufgrund des drei-dimensionalen Latenten Raums können wir den Raum und die Repräsentationen mit Hilfe der Matplotlib
3D-Funktionalität zu visualisieren. Innerhalb der nachfolgenden Zelle erstellen wir eine Visualisierung dieses Raums samt Repräsentationen:
# enforce inline plotting
%matplotlib inline
# import 3d plotting and animation libraries
from IPython.display import HTML
from matplotlib import animation
from mpl_toolkits.mplot3d import axes3d
# init the plot
fig = plt.figure(figsize=(5, 5))
ax = fig.add_subplot(111, projection='3d')
# change plot perspective
ax.view_init(elev=30, azim=240)
# set axis paramaters of subplot
ax.grid(linestyle='dotted')
# plot regular transactions, just the first 1000 to gain an intuition
regular = entry_representation[entry_representation['label'] == 'regular']
ax.scatter(regular['z1'][0:2000], regular['z2'][0:2000], regular['z3'][0:2000], c='C0', alpha=0.4, marker="o", label='regular')
# plot first order anomalous transactions
global_anomalies = entry_representation[entry_representation['label'] == 'global']
ax.scatter(global_anomalies['z1'], global_anomalies['z2'], global_anomalies['z3'], c='C1', s=100, marker="^", label='global')
# plot second order anomalous transactions
local_anomalies = entry_representation[entry_representation['label'] == 'local']
ax.scatter(local_anomalies['z1'], local_anomalies['z2'], local_anomalies['z3'], c='C2', s=100, marker="*", label='local')
# set axis labels
ax.set_xlabel('activation [$z_1$]', weight='normal', fontsize=12)
ax.set_ylabel('activation [$z_2$]', weight='normal', fontsize=12)
ax.set_zlabel('activation [$z_3$]', weight='normal', fontsize=12)
# add plot legend of transaction classes
ax.legend(loc='best')
# set plot title
plt.title('AENN Model Latent Space', fontsize=10);
Um Ihr wissen zu vertiefen empfehlen wir, die nachfolgenden Übungen zu bearbeiten:
1. Trainieren und evaluieren Sie ein Autoencoder Neural Network model mit reduziertem Bottleneck.
Die innerhalb des Notebooks vorgestellte Architektur führte zu einem guten Modell für die Erkennung von Anomalien. Prüfen Sie wie sich die Performance verändert, wenn die Dimensionalität der Bottleneck Schicht reduziert wird. Reduzieren Sie hierzu die Anzahl der Neuronen innerhalb des Encoder bzw. Decoder Bottlenecks auf zwei Neuronen. Wie ändert sich die Fähigkeit des Modells Anomalien im Buchungsstoff zu erkennen? Lassen sich unterschiedliche Aussagen für globale bzw. lokale Anomalien treffen?
# ***************************************************
# Sie können Ihre Lösung an dieser Stelle einfügen
# ***************************************************
2. Trainieren und evaluieren Sie ein shallow
Autoencoder Neural Network model.
Die innerhalb des Notebooks vorgestellte Architektur führte zu einem guten Modell für die Erkennung von Anomalien. Prüfen Sie wie sich die Performance verändert, wenn mehrere der versteckten Schichten entfernt werden. Passen Sie hierzu die Implementierungen des Encoders und Decoders entsprechend an. Wie ändert sich die Fähigkeit des Modells Anomalien im Buchungsstoff zu erkennen? Lassen sich unterschiedliche Aussagen für globale bzw. lokale Anomalien treffen?
# ***************************************************
# Sie können Ihre Lösung an dieser Stelle einfügen
# ***************************************************
Diese Notebook umfasste eine schrittweise Einführung in Entwurf, Implementierung, Training und Bewertung eines auf einem neuronalen Netz basierenden Ansatzes zur Erkennung von Anomalien in Buchhaltungsdaten. Die vorgestellten Code Beispiele und die Übungen können als Ausgangspunkt für für die Entwicklung und das Testen komplexerer Strategien zur Erkennung von Anomalien dienen.