IIIa. Einführung in TensorFlow: Realisierung eines Perzeptrons mit TensorFlow

1. Einleitung

1.1. Was haben wir vor?

Im zweiten Artikel dieser Serie sind wir darauf eingegangen, wie man TensorFlow prinzipiell nutzt. Wir wollen das Gelernte an einem einfachen Modell anwenden. Bevor wir dies jedoch tun, müssen wir die Theorie hinter dem Modell verstehen um TensorFlow richtig anwenden zu können.

Dafür bietet sich ein Adaline-Perzeptron sehr gut an. Es ist ein einfaches Modell mit nur einer Schicht, wo die Theorie verständlich ist.

1.2. Aufgabenstellung

Abb.1 Trainingsdaten: Grün \rightarrow Label 0, Rot
\rightarrow Label 1

In Abb.1 sehen wir unsere Trainingsdaten, die
zufällig generiert wurden. Alle grün markierten Datenpunkte haben das Label 0 und die rot markierten Punkte erhalten das Label 1. 

Wir möchten einen Adaline-Perzeptron entwickeln, der unsere Daten  je nach Position in die richtige Klasse zuordnet. Somit haben wir eine Aufgabe mit binärer Klassifikation

2. Grundlagen

2.1. Funktionsweise eines Perzeptrons

Ein Perzeptron ist ein mathematisches Modell, welches eine Nervenzelle beschreiben soll.

Abb.2 Schematische Darstellung einer Nervenzelle und ihren Bestandteilen

Vereinfacht funktioniert eine Nervenzelle, auch Neuron genannt, folgendermaßen: Eine Vielzahl von Reizen bzw. Eingabesignalen wird von den Dendriten aufgenommen, die dann im Kern verarbeitet werden. Wenn die verschiedenen Eingabesignale die ’richtige’ Dosis an Reizen erreichen und einen Schwellwert erreichen, dann feuert das Neuron ab und leitet ein Signal weiter. 

Für eine detaillierte Beschreibung, wie ein Perzeptron mathematisch beschrieben wird, möchte ich auf diesen Artikel hinweisen.

Wir wollen uns in diesem Artikel auf den Adaline-Algorithmus (ADAptive LINear Element) konzentrieren. Dieser ist eine Weiterentwicklung des Perzeptron. Die Besonderheit an diesem Algorithmus liegt darin, dass das Konzept der Fehlerminimierung durch Minimierung der Straffunktion der berechneten und der tatsächlichen Ergebnisse enthält. Ein weiter wesentlicher Unterschied zu einem einfachen Perzeptron ist vor allem, dass wir bei Adaline keine einfache Sprungfunktion als Aktivierungsfunktion haben, sondern eine stetige Funktion nutzen und somit eine Differenzierung/Ableitung der Aktivierungsfunktion durchführen können. Dieser Punkt ist für die Optimierung der Gewichte und des Lernens unseres Modells ein entscheidender Vorteil.

Das Schema in Abb.3 zeigt uns die Funktionsweise, wie unser Adaline-Algorithmus funktionieren soll.

Abb.3 Schematische Darstellung des Adaline-Perzeptrons

  1. Eingang: In dieser Schicht werden unsere Daten ein gepfangen und weitergeleitet
  2. Die Gewichte geben an, welchen Einfluss unsere Eingangssignale haben. Sie sind auch unsere Größe, die in unserem Algorithmus optimiert werden.
  3. Die Nettoeingabefunktion wird durch die Zusammenführung von Eingangssignalen und Gewichten erzeugt. Je nachdem wie die Eingänge und Gewichte verbunden sind,  müssen diese mathematisch korrekt multipliziert werden.
  4. Die Nettoeingabe wird dann, in die Aktivierungsfunktion eingebunden. Je nachdem welche Aktivierungsfunktion man nutzt, ändert sich die Ausgabe nach der Aktivierungsfunktion. 
  5.  In der Fehlerrückgabe werden die vorhergesagten Ausgaben mit den tatsächlichen Werten/Labels verglichen. Auch hier gibt es verschiedene Verfahren, um eine Fehlerfunktion zu bilden. 
  6. In der Optimierung werden dann auf Basis der Fehlerfunktion die Gewichte so optimiert, dass der Fehler zwischen unseren Label und den vorhergesagten Werten minimiert wird.
  7. Der Quantisierer ist ein optionales Element. Bei einer kategorischen Problemstellung bekommen wir nach der Aktivierungsfunktion eine Wahrscheinlichkeit zu der die Daten zu welchem Label zugeteilt werden. Der Quantisierer wandelt diese Wahrscheinlichkeiten zu Labeln um. Zum Beispiel haben wir einen Datensatz und unser Modell sagt voraus, dass dieser Datensatz zu 88 % das Label 1 hat. Je nachdem welche Grenze dem Quantisierer gegeben wird, teilt dieser dann den Datensatz in die entsprechende Klasse ein. Wenn wir sagen die Grenze soll 50% sein, dann sagt der Quantisierer, dass unser Datensatz Label 1 ist.

2.2. Aktivierungsfunktionen

Die Aktivierungsfunktion ist ein sehr wichtiger Bestandteil bei neuronalen Netzen. Diese bestimmen, wie sich das Ausgangssignal verhält. Es gibt eine Vielzahl von Aktivierungsfunktionen, die ihre Vor- und Nachteile haben. Wir wollen uns erstmal auf die Sigmoidfunktion konzentrieren.

Eigentlich haben wir bei der Sprungfunktion alles was wir brauchen. Wenn wir einen Schwellenwert erreichen z \geq 0, dann feuert die Sprungfunktion und das sehr abrupt. Die Sigmoidfunktion hingegen hat einen sanfteren und natürlicheren Verlauf als die Sprungfunktion. Außerdem ist sie eine stetig und differenzierbare Funktion, was sehr vorteilhaft für das Gradientenverfahren (Optimierung) ist. Daher wollen wir die Sigmoidfunktion für unsere Problemstellung nutzen.

    \begin{align*} \text{sig}(z) = \frac{1}{1 + e^{-z}}\end{align*}

Abb.4 Sigmoid-Funktion mit ihrer Ableitung und deren Sättigungsbereichen

2.3. Optimierungsverfahren

2.3.1. Fehlerfunktion

Die wohl am häufigsten genutzten Fehlerfunktionen (oder auch Ziel-, Kosten-, Verlust-, Straffunktion) sind wohl der mittlere quadratische Fehler bei Regressionen und die Kreuzentropie bei kategorischen Daten.

In unserem Beispiel haben wir Daten kategorischer Natur und eine binäre Thematik, weshalb wir uns auf die Kreuzentropie in Kombination mit der Sigmoidfunktion konzentrieren wollen.

Aus der Matrizenrechnung t (z =\boldsymbol{xw}^T) erhalten wir ein Skalar (eindimensional). Geben wir diese in die Sigmoidfunktion ein, kommen wir auf folgende Gleichung.



    \begin{align*} \text{sig}(z=\boldsymbol{xw}^T) = \frac{1}{1 + e^{-\boldsymbol{xw}^T}} \end{align*}


Hinweis: Wie in Abb.4 kann die Sigmoidfunktion nur Werte zwischen 0 und 1 erreichen, ohne diese jemals zu erreichen. Außerdem ändert sich die Funktion bei sehr großen Beträgen nur noch minimal, man spricht auch von Sättigung. Dieser Fakt ist sehr wichtig, wenn um die Optimierung der Gewichte geht. Wenn wir unsere Nettoeingabe nicht skalieren, dann kann es passieren, dass unser Modell sehr langsam lernt, da der Gradient der Sigmoidfunktion bei großen Beträgen sehr klein ist.

Bei Aufgaben mit binärer Klassifizierung hat sich die Kreuzentropie als Fehlerfunktion etabliert. Sie ist ein Maß für die Qualität eines Modells, welche eine Wahrscheinlichkeitsverteilung angibt. Je kleiner diese Größe ist, desto besser unser Modell. Es gilt also unsere Fehlerfunktion zu minimieren!

Wir wollen in einem separaten Artikel genauer auf die Kreuzentropie eingehen. Für den jetzigen Zeitpunkt soll es reichen, wenn wir die Formel vor Augen haben und was sie grob bedeutet.

P = \{p_1,p_2,\dots,p_N\} sei die ‘wahre’ Wahrscheinlichkeitsverteilung aus der Menge X = \{x_1,x_2,\dots,x_N\}, in unserem Fall, die Wahrscheinlichkeitsverteilung, ob ein Datenpunkt dem Label 0 oder 1 zugehört. Wenn wir nun unser Eingangssignal durch die Aktivierungsfunktion fließen lassen, dann erhalten wir ebenfalls eine ‘berechnete’ Wahrscheinlichkeitsverteilung die Q = \{q_1,q_2,\dots,q_N\} genannt werden soll. Um die Wahrscheinlichkeitsverteilungen p und q zu vergleichen, nutzen wir die Kreuzentropie, welche wie folgt für diskrete Daten definiert ist:

    \begin{align*}\log_2{x}&= \operatorname{ld}(x) \\H(P;Q) &= - \sum{P \cdot \operatorname{ld}(Q)}\\H(P;Q) &= -p_1 \operatorname{ld}(q_1) - p_2  \operatorname{ld}(q_2)\end{align*}

Beispiel einer binären Problemstellung. Wir haben unsere Label 0 und 1. p1 ist die Wahrscheinlichkeit, inwiefern unser Datenpunkt das Label 0 hat. Da wir die Trainingsdaten kennen, wissen wir auch das dieser Punkt zu 100 %, welches Label hat. Unser Modell hat zum Beispiel im ersten Durchgang eine Wahrscheinlichkeit von 0.8 und später 0.9 berechnet.

Fall I : P = Q Die Wahrscheinlichkeitsverteilungen P und Q sind identisch:

    \begin{align*}P &= \{p_1 = 1.0, p_2 = 0.0 \} \\Q_0 &= \{q_1 = 1.0, q_2 = 0.0 \} \\ \\H_{0}(P;Q_I) &= -1.0 \operatorname{ld}(1) -0.0 \operatorname{ld}(0.0) = 0.0\\\end{align*}

Fall II: P \neq Q Die Wahrscheinlichkeitsverteilungen P und Q sind nicht identisch:

    \begin{align*}P &= \{p_1 = 1.0, p_2 = 0.0 \} \\Q_{1} &= \{q_1 = 0.8, q_2 = 0.2 \} \\ Q_{2} &= \{q_1 = 0.9, q_2 = 0.1 \} \\ Q_{3} &= \{q_1 = 0.99, q_2 = 0.01 \} \\ \\H_{1}(P;Q_{1}) &= -1.0 \operatorname{ld}(0.8) -0.0 \operatorname{ld}(0.2) = 0.3219 \\H_{2}(P;Q_{2}) &= -1.0 \operatorname{ld}(0.9) -0.0 \operatorname{ld}(0.1) = 0.1520 \\ H_{3}(P;Q_{3}) &= -1.0 \operatorname{ld}(0.99) -0.0 \operatorname{ld}(0.01) = 0.0144\\\end{align*}

In der oberen Berechnung haben wir zum einfachen Verständnis der Kreuzentropie ein einfaches Beispiel. p_1 ist eine 100 % ige  Wahrscheinlichkeit, dass zum Beispiel unser Datensatz das Label 0 hat. Unser perfektes Modell mit Q_0 hat eine Kreuzentropie-Wert von 0. Unser zweites Modell  H_1(P;Q1) hat eine gewisse Unbestimmtheit, die sich durch eine größere Kreuzentropie H_1 = 0.1520 bemerkbar macht. Je mehr sich also unser Modell von den wirklichen Daten abweicht, desto größer ist die Kreuzentropie.

2.3.2. Optimierung nach dem Gradientenverfahren

Wenn wir es also schaffen die Kreuzentropie zu minimieren, dann erhalten wir auch ein besseres Modell! Bei der Optimierung nach dem Gradientenverfahren versuchen wir uns schrittweise an das Minimum zu bewegen.

    \begin{align*}H(P;Q) &= H(y; \varPhi(z)) \\            &= H(y; \text{sig}(z))\\             &= H(y; \text{sig}(xw))\\H' &= \frac{\partial H}{\partial w} \rightarrow Min.\end{align*}

Ziel der Optimierung ist es, dass unsere Gewichte so angepasst werden, dass sich der Fehler in unserer Fehlerfunktion minimiert. Wir leiten also die Fehlerfunktion nach w ab. 

Diese Aufgabe wird zum Glück von TensorFlow übernommen und wir müssen die Randbedingungen nur dem System geben.

Neben dem Gradientenverfahren, gibt es auch noch eine Menge anderer Optimierer, auf die wir später nochmal eingehen werden.

3. Zusammenfassung

Bevor wir TensorFlow nutzen, ist es wichtig, dass wir unser Modell verstehen. TensorFlow ist wie vieles nur ein Werkzeug, wenn man die Grundlagen nicht verstanden hat. Daher haben wir uns in diesem Artikel erstmal auf die Theorie konzentriert und ich habe dabei versucht mich auf das Wesentliche zu beschränken. 

Im nächsten Artikel werden wir dann unser Modell in TensorFlow realisieren.

PS: In einem separaten Artikel wollen später nochmal detaillierter auf Aktivierungsfunktion, Kreuzentropie und das Gradientenverfahren eingehen.

Language Detecting with sklearn by determining Letter Frequencies

Of course, there are better and more efficient methods to detect the language of a given text than counting its lettes. On the other hand this is a interesting little example to show the impressing ability of todays machine learning algorithms to detect hidden patterns in a given set of data.

For example take the sentence:

“Ceci est une phrase française.”

It’s not to hard to figure out that this sentence is french. But the (lowercase) letters of the same sentence in a random order look like this:

“eeasrsçneticuaicfhenrpaes”

Still sure it’s french? Regarding the fact that this string contains the letter “ç” some people could have remembered long passed french lessons back in school and though might have guessed right. But beside the fact that the french letter “ç” is also present for example in portuguese, turkish, catalan and a few other languages, this is still a easy example just to explain the problem. Just try to guess which language might have generated this:

“ogldviisnntmeyoiiesettpetorotrcitglloeleiengehorntsnraviedeenltseaecithooheinsnstiofwtoienaoaeefiitaeeauobmeeetdmsflteightnttxipecnlgtetgteyhatncdisaceahrfomseehmsindrlttdthoaranthahdgasaebeaturoehtrnnanftxndaeeiposttmnhgttagtsheitistrrcudf”

While this looks simply confusing to the human eye and it seems practically impossible to determine the language it was generated from, this string still contains as set of hidden but well defined patterns from which the language could be predictet with almost complete (ca. 98-99%) certainty.

First of all, we need a set of texts in the languages our model should be able to recognise. Luckily with the package NLTK there comes a big set of example texts which actually are protocolls of the european parliament and therefor are publicly availible in 11 differen languages:

  •  Danish
  •  Dutch
  •  English
  •  Finnish
  •  French
  •  German
  •  Greek
  •  Italian
  •  Portuguese
  •  Spanish
  •  Swedish

Because the greek version is not written with the latin alphabet, the detection of the language greek would just be too simple, so we stay with the other 10 languages availible. To give you a idea of the used texts, here is a little sample:

“Resumption of the session I declare resumed the session of the European Parliament adjourned on Friday 17 December 1999, and I would like once again to wish you a happy new year in the hope that you enjoyed a pleasant festive period.
Although, as you will have seen, the dreaded ‘millennium bug’ failed to materialise, still the people in a number of countries suffered a series of natural disasters that truly were dreadful.”

Train and Test

The following code imports the nessesary modules and reads the sample texts from a set of text files into a pandas.Dataframe object and prints some statistics about the read texts:

Above you see a sample set of random rows of the created Dataframe. After removing very short text snipplets (less than 200 chars) we are left with 56481 snipplets. The function clean_eutextdf() then creates a lower case representation of the texts in the coloum ‘ltext’ to facilitate counting the chars in the next step.
The following code snipplet now extracs the features – in this case the relative frequency of each letter in every text snipplet – that are used for prediction:

Now that we have calculated the features for every text snipplet in our dataset, we can split our data set in a train and test set:

After doing that, we can train a k-nearest-neigbours classifier and test it to get the percentage of correctly predicted languages in the test data set. Because we do not know what value for k may be the best choice, we just run the training and testing with different values for k in a for loop:

As you can see in the output the reliability of the language classifier is generally very high: It starts at about 97.5% for k = 1, increases for with increasing values of k until it reaches a maximum level of about 98.5% at k ≈ 10.

Using the Classifier to predict languages of texts

Now that we have trained and tested the classifier we want to use it to predict the language of example texts. To do that we need two more functions, shown in the following piece of code. The first one extracts the nessesary features from the sample text and predict_lang() predicts the language of a the texts:

With this classifier it is now also possible to predict the language of the randomized example snipplet from the introduction (which is acutally created from the first paragraph of this article):

The KNN classifier of sklearn also offers the possibility to predict the propability with which a given classification is made. While the probability distribution for a specific language is relativly clear for long sample texts it decreases noticeably the shorter the texts are.

Background and Insights

Why does a relative simple model like counting letters acutally work? Every language has a specific pattern of letter frequencies which can be used as a kind of fingerprint: While there are almost no y‘s in the german language this letter is quite common in english. In french the letter k is not very common because it is replaced with q in most cases.

For a better understanding look at the output of the following code snipplet where only three letters already lead to a noticable form of clustering:

 

Even though every single letter frequency by itself is not a very reliable indicator, the set of frequencies of all present letters in a text is a quite good evidence because it will more or less represent the letter frequency fingerprint of the given language. Since it is quite hard to imagine or visualize the above plot in more than three dimensions, I used a little trick which shows that every language has its own typical fingerprint of letter frequencies:

What more?

Beside the fact, that letter frequencies alone, allow us to predict the language of every example text (at least in the 10 languages with latin alphabet we trained for) with almost complete certancy there is even more information hidden in the set of sample texts.

As you might know, most languages in europe belong to either the romanian or the indogermanic language family (which is actually because the romans conquered only half of europe). The border between them could be located in belgium, between france and germany and in swiss. West of this border the romanian languages, which originate from latin, are still spoken, like spanish, portouguese and french. In the middle and northern part of europe the indogermanic languages are very common like german, dutch, swedish ect. If we plot the analysed languages with a different colour sheme this border gets quite clear and allows us to take a look back in history that tells us where our languages originate from:

As you can see the more common letters, especially the vocals like a, e, i, o and u have almost the same frequency in all of this languages. Far more interesting are letters like q, k, c and w: While k is quite common in all of the indogermanic languages it is quite rare in romanic languages because the same sound is written with the letters q or c.
As a result it could be said, that even “boring” sets of data (just give it a try and read all the texts of the protocolls of the EU parliament…) could contain quite interesting patterns which – in this case – allows us to predict quite precisely which language a given text sample is written in, without the need of any translation program or to speak the languages. And as an interesting side effect, where certain things in history happend (or not happend): After two thousand years have passed, modern machine learning techniques could easily uncover this history because even though all these different languages developed, they still have a set of hidden but common patterns that since than stayed the same.

Analyse der Netzwerktopologie des Internets auf Basis des IPv4-Protokolls

Wie kommen Daten die man via Internet quer durch die Welt sendet eigentlich an ihr Ziel? Welchen Weg nehmen beispielsweise die Datenpakete, wenn ich von mir zu Hause eine Datei an meinen Nachbarn ein Haus weiter sende? Wie groß ist der “Umweg”, den die Daten nehmen? Und macht es eigentlich einen Unterschied, ob ich www.google.de, www.google.com oder www.google.nl aufrufe, oder gehen alle Suchanfragen sowieso an dasselbe Ziel?

Fragen wie diese lassen sich durch eine Kombination von Tools wie traceroute oder tracepath und geoiplookup beantworten und unter Verwendung des Python-Paketes geoplotlib sogar graphisch auf einer Weltkarte darstellen. Die so gewonnenen Ergebnisse zeigen Teile der Netzwerktopologie des Internets auf und führen zu interessanten, teils unerwarteten Erkenntnissen.

Ziel dieses Artikels soll sein, ein möglichst einfaches Tutorial zum selber mitbasteln bereit zu stellen. Die einzelnen Schritte die hierfür notwendig sind, werden möglichst einfach verständlich dargestellt und erklärt, trotzdem sind zum vollständigen Verständnis grundlegende Kenntnisse in Python sowie der Kommandozeile hilfreich. Er richtet sich aber auch an alle, die sich einfach einmal etwas in ihrer virtuellen Umgebung „umschauen“ möchten oder einfach nur an den Ergebnissen interessiert sind, ohne sich mit den Details und wie diese umgesetzt werden, auseinander setzen zu wollen.  Am Ende des Artikels werden die einzelnen Skripte des Projekts als zip-Datei bereitgestellt.

Hinweis: Diese Anleitung bezieht sich auf ein Linux-System und wurde unter Ubuntu getestet. Windows-User können beispielsweise mit dem Befehl tracert (als Ersatz für traceroute) ähnliche Ergebnisse erziehlen, jedoch muss dann das Parsing der IP-Adressen abgeändert werden.

1. Grundsätzliches Erkunden der Route, die ein Datenpaket nimmt

Hierfür wird ein Programm wie traceroute, tracepath oder nmap benötigt, welches durch Versenden von „abgelaufenen Datenpaketen“ die Hosts „auf dem Weg“ zum Ziel dazu bringt, ihre IPv4-Adresse zurück zu geben. In diesem Artikel wird beispielhaft traceroute verwendet, da dieses unter den meisten Linux-Versionen bereits zur „Grundausstattung“ gehört und somit für diesen Schritt keine weitere Software installiert werden muss. Die Verwendung von traceroute folgt der Syntax:

Als Ziel muss hier die IP-Adresse bzw. der Domainname des Zielrechners angegeben werden. Ein Beispiel soll dies vereinfachen:

Im Beispiel wird die Route zum Hostrechner mit der Domain www.google.de ermittelt. In der ersten Spalte der Ausgabe ist die Nummer des jeweiligen „Hops“ zu sehen. Wichtig ist insbesondere die zweite Spalte, welche die IPv4-Adresse des jeweiligen Rechners auf dem Weg zum Ziel darstellt. Die folgenden Spalten enthalten weitere Informationen wie Antwortzeiten der jeweiligen Server und die IP-Adressen der Folge-Server.

Um die Ausgabe in eine Form umzuwandeln, welche später einfacher von Python gelesen werden kann, muss diese noch ausgelesen werden (Parsing). zuerst soll die erste Zeile der Ausgabe herausgeschnitten werden, da diese zwar informativ, jedoch kein Teil der eigentlichen Route ist. Dies kann sehr einfach durchgeführt werden, indem die Ausgabe des traceroute-Befehls an einen Befehl wie beispielsweise sed „gepiped“ (also weitergeleitet) wird. Die dabei entstehende Pipe sieht dann wie folgt aus:

Um bei unserem Beispiel mit der Route zu www.google.de zu bleiben, sieht der Befehl und die Entsprechende Ausgabe wie folgt aus:

Anschließend soll die zweite Spalte der Ausgabe herausgeschnitten werden. Dies ist am einfachsten mit dem Befehl awk zu bewerkstelligen. Das Prinzip dahinter ist das gleiche wie im obigen Schritt: die Ausgabe des vorherigen Befehls wird dem Befehl awk als Eingabe weitergeleitet, womit der gesamte Befehl nun wie folgt aussieht:

Bezogen auf das google-Beispiel sehen Ein- und Ausgabe nun so aus:

Im letzten Schritt sollen die einzelnen IP-Adressen durch Leerzeichen getrennt in eine einzelne Zeile geschrieben werden. Sinn dieses Schrittes ist, dass später viele Zielrechner nacheinander aus einer Datei eingelesen werden können und jede Route zu einem Zielrechner als eine einzelne Zeile in eine Zieldatei geschrieben wird.
Auch dieser Schritt funktioniert ähnlich wie die obigen Schritte, indem die Ausgabe des letzten Schrittes an einen weiteren Befehl weitergeleitet wird, der diese Funktion erfüllt. Dieser Schritt könnte wieder mit dem Befehl sed durchgeführt werden, da aber nur ein einzelnes Zeichen (nämlich das Zeilenumbruch-Zeichen bzw. Newline) durch ein Leerzeichen ersetzt werden soll, wird hier aufgrund der einfacheren Syntax der Befehl tr verwendet.
Der fertige Befehl sieht nun wie folgt aus:

Oder im fertigen Beispiel mit www.google.de:

Hiermit ist das Parsen abgeschlossen und die fertige Ausgabe kann nun in eine Ergebnisdatei geschrieben werden. Um automatisch viele Zielrechner aus einer Datei einzulesen und alle gefundenen Routen in eine Zieldatei zu schreiben, wird der obige Befehl in eine Schleife „verpackt“ welche die Zielrechner Zeile für Zeile aus der Datei zieladressen.txt ausliest und die gefundenen Routen ebenso Zeile für Zeile in die Datei routen.csv schreibt. Die Datei routen.csv kann später zur Ermittlung verschiedener Informationen zu den gefunden IP-Adressen einfach mit einem Python-Skript eingelesen und geparst werden.

In diesem Artikel wird das fertige Skript ohne weitere Erklärung in der beiliegenden zip-Datei bereitgestellt. Wen die genaue Funktionsweise der Schleife interessiert, sei angehalten sich generell über die Funktionsweise von Shellskripten einzulesen, da dies den Rahmen des Artikels sprengen würde.

Dieses Skript benötigt die Datei zieladressen.txt welche wie folgt aussehen muss (anstatt Domainnamen können auch direkt IPv4-Adressen verwendet werden):

2. Sammeln von (Geo-)Informationen zu bestimmten IPv4-Adressen

Die gefundenen IPv4-Adressen können anschließend mit dem Befehl geoiplookup oder über die Internetseite http://geoiplookup.net/ relativ genau (meißtens auf Städteniveau) lokalisiert werden. Dies funktioniert, da einzelne Subnets in der Regel bestimmten Regionen und Internetprovidern zugeordnet sind.

Der Befehl geoiplookup greift hierbei auf eine vorher installierte und lokal gespeicherte Datenbank zu, welche je nach installierter Version als Country- oder City-Edition vorliegt. Da geoiplookup nicht zu den Standartbordmitteln unter Linux gehört und um die weiteren Schritte auch Benutzern anderer Betriebssysteme zu ermöglichen, wird hier nur ein kurzes Beispiel der Benutzung dieses Befehls und dessen Ausgabe gegeben und im weiteren die Online-Abfrage mittels eines Python-Skriptes beschrieben.

Die Internetseite http://geoiplookup.net bietet einen Onlineservice welcher Geo- und weitere Informationen zu gegebenen IPv4-Adressen bereitstellt. Öffnet man die Seite ohne Angabe einer IP-Adresse in einem Browser, so erhält man die entsprechenden Informationen über die eigene IP-Adresse. (Achtung: die Verwendung eines Proxies oder gar Tor führt zwangsläufig zu falschen Ergebnissen.)

Da die Seite auch über eine API (also eine automatisierte Abfrageschnittstelle) unter der Adresse “http://api.geoiplookup.net/?query=${IPADRESSE}” verfügt, kann man die entsprechenden Informationen zu den IP-Adressen mittels eines Pythonskriptes abfragen und auswerten. Als Antwort erhält man eine XML‑Datei welche beispielsweise folgendermaßen aussieht:

Diese kann im Browser z. B. unter der Adresse http://api.geoiplookup.net/?query=77.20.253.87 aufgerufen werden (oder unter: http://api.geoiplookup.net/ für die eigene Adresse).

Um die hierin enthaltenen Informationen mit Hilfe von Python auszulesen lässt sich ElementTree aus aus dem Modul xml.etree, das in der Python-Standartbibliothek vorhanden ist, verwenden. Dies wird im beiliegenden Skript mit der Funktion get_hostinfo() bewerkstelligt:

Diese parst die XML-Datei automatisch zu einem Python-DefaultDict das dann die entsprechenden Informationen enthält (das DefaultDict wird verwendet da normale Python Dictionaries zu Fehlern führen, wenn nicht gesetzte Werte abgefragt werden). Die Ausgabe der Funktion sieht dann wie folgt aus:

3. Plotten der gefundenen Routen mit geoplotlib auf einer Weltkarte

Wichtig für das anschließende Plotten ist hierbei die Geolocation also ‘latitude’ und ‘longitude’. Mit den Werten kann man anschließend die mit traceroute gefundenen Pfade als Basemap plotten. Dies funktioniert mit der Funktion drawroutes2map():

Der Plot einer Verbindungsanfrage an www.google.de aus Berlin sieht beispielsweise folgendermaßen aus:

Hier wird deutlich, dass Datenpakete durchaus nicht immer den kürzesten Weg nehmen, sondern teilweise rund um die Welt gesendet werden (Deutschland – USA – Sydney(!) – USA), bevor sie an ihrem Ziel ankommen und dass das Ziel einer Verbindung zu einer Domain mit der Endung „de“ nicht unbedingt in Deutschland liegen muss.

Mit Default-Einstellungen werden von der Funktion drawroutes2map() alle Routen in zufälligen Farben geplottet, welche in der Datei routen.csv gefunden werden.

Lässt man viele Routen plotten wird hierbei die Netzwerkstruktur deutlich, über die die Daten im Internet verteilt werden. Auf dem obigen Plot kann man recht gut erkennen, dass die meisten Internetseiten in Europa oder den USA gehostet werden, einige noch in China und Japan, dagegen beispielsweise Afrika praktisch unbedeutend ist.

Auf dem nächsten Plot wiederum ist zu erkennen, dass es tatsächlich eine Art “Hotspots” gibt über die fast alle Daten laufen, wie z. B. Frankfurt am Main, Zürich und Madrid.

4. Schematische Darstellung der Routen als directed Graph mit graphviz

Mit graphviz lassen sich schematische Graphen darstellen. Mit dem Paket pygraphviz existiert hiefür auch eine Python-Anbindung. Die schematische Darstellung als Graph ist in vielen Fällen deutlich übersichtlicher als die Darstellung auf einer Weltkarte und die Topologie des Netzwerkes wird besser sichtbar.

Die entsprechende Python-Funktion, die alle Routen aus der Datei routes.csv als geplotteten Graph ausgibt ist drawroutes2graph():

Die Funktion schreibt den erstellten Graph in der Dot-Language in die Datei routes.dot und erstellt zwei verschiedene visuelle Darstellungen als png-Dateien.

Da mit der Funktion get_hostinfo() auch weitere Informationen zu den jeweiligen IP-Adressen verfügbar sind  können diese auch visuell im Graph dargestellt werden. So sind in der folgenden Darstellung Hosts in verschiedenen Ländern in unterschiedlichen Farben dargestellt. (Deutschland in grün, USA in rot, Spanien in gelb, Schweiz in blau, China in magenta und alle übrigen Länder und Hosts ohne Länderinformation in schwarz).

Diese Art der Darstellung vereint damit die Vorteile der schematischen Darstellung mit der Geoinformation zu den jeweiligen Hosts. Aus der Grafik lässt sich beispielsweise sehr gut erkennen, dass, trotz oft vieler Zwischenstationen innerhalb eines Landes, Landesgrenzen überschreitende Verbindungen relativ selten sind.

Auch interessant ist, dass das Netzwerk durchaus Maschen aufweist – mit anderen Worten: Dass ein und dieselbe Station bei verschiedenen Verbindungsanfragen über verschiedene Zwischenstationen angesprochen wird und Daten, die von Punkt A nach Punkt B gesendet werden, nicht immer denselben Weg nehmen.

5. Schlussfolgerung

Was kann man hieraus denn nun letztendlich an Erkenntnissen ziehen? Zum einen natürlich, wie Daten via Internet über viele Zwischenstationen rund um die Welt gesendet und hierbei mit jeder Station neu sortiert werden. Vor allem aber auch, dass mit dem entsprechenden Know-How und etwas Kreativität mit bemerkenswert wenig Code bereits Unmengen an Daten gesammelt, geordnet und ausgewertet werden können. Alle möglichen Daten werden in unserer heutigen Welt gespeichert und sind zu einem nicht unbeträchtlichen Teil auch für jeden, der weiß, wer diese Daten hat oder wie man sie selber ermitteln kann, verfügbar und oft lassen sich hier interessante Einblicke in die Funktionsweise unserer Welt gewinnen.

Statistical Relational Learning – Part 2

In the first part of this series onAn Introduction to Statistical Relational Learning”, I touched upon the basic Machine Learning paradigms, some background and intuition of the concepts and concluded with how the MLN template looks like. In this blog, we will dive in to get an in depth knowledge on the MLN template; again with the help of sample examples. I would then conclude by highlighting the various toolkit available and some of its differentiating features.

MLN Template – explained

A Markov logic network can be thought of as a group of formulas incorporating first-order logic and also tied with a weight. But what exactly does this weight signify?

Weight Learning

According to the definition, it is the log odds between a world where F is true and a world where F is false,

and captures the marginal distribution of the corresponding predicate.

Each formula can be associated with some weight value, that is a positive or negative real number. The higher the value of weight, the stronger the constraint represented by the formula. In contrast to classical logic, all worlds (i.e., Herbrand Interpretations) are possible with a certain probability [1]. The main idea behind this is that the probability of a world increases as the number of formulas it violates decreases.

Markov logic networks with its probabilistic approach combined to logic posit that a world is less likely if it violates formulas unlike in pure logic where a world is false if it violates even a single formula. Consider the case when a formula with high weight i.e. more significance is violated implying that it is less likely in occurrence.

Another important concept during the first phase of Weight Learning while applying an MLN template is “Grounding”. Grounding means to replace each variable/function in predicate with constants from the domain.

Weight Learning – An Example

Note: All examples are highlighted in the Alchemy MLN format

Let us consider an example where we want to identify the relationship between 2 different types of verb-noun pairs i.e noun subject and direct object.

The input predicateFormula.mln file contains

  1. The predicates nsubj(verb, subject) and dobj(verb, object) and
  2. Formula of nsubj(+ver, +s) and dobj(+ver, +o)

These predicates or rules are to learn all possible SVO combinations i.e. what is the probability of a Subject-Verb-Object combination. The + sign ensures a cross product between the domains and learns all combinations. The training database consists of the nsubj and dobj tuples i.e. relations is the evidence used to learn the weights.

When we run the above command for this set of rules against the training evidence, we learn the weights as here:

Note that the formula is now grounded by all occurrences of nsubj and dobj tuples from the training database or evidence and the weights are attached to it at the start of each such combination.

But it should be noted that there is no network yet and this is just a set of weighted first-order logic formulas. The MLN template we created so far will generate Markov networks from all of our ground formulas. Internally, it is represented as a factor graph.where each ground formula is a factor and all the ground predicates found in the ground formula are linked to the factor.

Inference

The definition goes as follows:

Estimate probability distribution encoded by a graphical model, for a given data (or observation).

Out of the many Inference algorithms, the two major ones are MAP & Marginal Inference. For example, in a MAP Inference we find the most likely state of world given evidence, where y is the query and x is the evidence.

which is in turn equivalent to this formula.

Another is the Marginal Inference which computes the conditional probability of query predicates, given some evidence. Some advanced inference algorithms are Loopy Belief Propagation, Walk-SAT, MC-SAT, etc.

The probability of a world is given by the weighted sum of all true groundings of a formula i under an exponential function, divided by the partition function Z i.e. equivalent to the sum of the values of all possible assignments. The partition function acts a normalization constant to get the probability values between 0 and 1.

Inference – An Example

Let us draw inference on the the same example as earlier.

After learning the weights we run inference (with or without partial evidence) and query the relations of interest (nsubj here), to get inferred values.

Tool-kits

Let’s look at some of the MLN tool-kits at disposal to do learning and large scale inference. I have tried to make an assorted list of all tools here and tried to highlight some of its main features & problems.

For example, BUGS i.e. Bayesian Logic uses a Swift Compiler but is Not relational! ProbLog has a Python wrapper and is based on Horn clauses but has No Learning feature. These tools were invented in the initial days, much before the present day MLN looks like.

ProbCog developed at Technical University of Munich (TUM) & the AI Lab at Bremen covers not just MLN but also Bayesian Logic Networks (BLNs), Bayesian Networks & ProLog. In fact, it is now GUI based. Thebeast gives a shell to analyze & inspect model feature weights & missing features.

Alchemy from University of Washington (UoW) was the 1st First Order (FO) probabilistic logic toolkit. RockIt from University of Mannheim has an online & rest based interface and uses only Conjunctive Normal Forms (CNF) i.e. And-Or format in its formulas.

Tuffy scales this up by using a Relational Database Management System (RDBMS) whereas Felix allows Large Scale inference! Elementary makes use of secondary storage and Deep Dive is the current state of the art. All of these tools are part of the HAZY project group at Stanford University.

Lastly, LoMRF i.e. Logical Markov Random Field (MRF) is Scala based and has a feature to analyse different hypothesis by comparing the difference in .mln files!

 

Hope you enjoyed the read. The content starts from basic concepts and ends up highlighting key tools. In the final part of this 3 part blog series I would explain an application scenario and highlight the active research and industry players. Any feedback as a comment below or through a message is more than welcome!

Back to Part I – Statistical Relational Learning

Additional Links:

[1] Knowledge base files in Logical Markov Random Fields (LoMRF)

[2] (still) nothing clever Posts categorized “Machine Learning” – Markov Logic Networks

[3] A gentle introduction to statistical relational learning: maths, code, and examples

Auswertung von CSV- und Log-Dateien auf der Command Line mit awk

Die Programmiersprache awk ist klein und unscheinbar, unter Data Science at the Command Line-Verfechtern allerdings ein häufiges Tool zur schnellen Analyse von CSV-Datein und vergleichbar strukturierten Daten (z. B. Logfiles) mit über Trennzeichen differenzierten Spalten. Auch in Shell-Skripten kommt awk meistens dann zum Einsatz, wenn es um den Zugriff, aber auch um die Manipulation von solchen Dateien geht.

Data Science at the Command Line: Facing the Future with Time-Tested Tools

awk wird als Skriptsprache mit nahezu jeder Linux-Distribution ausgeliefert und ist recht einfach eingehalten, kann jedoch auch schnell kryptisch werden. awk wird meistens ad-hoc auf der Kommandozeile ausgeführt, es können jedoch auch Skripte in awk-Dateien erstellt werden. Häufiger Grund für den Einsatz von awk ist die Anwendung von regulären Ausdrücken (Textmustersuche) auf Logdateien.

Nachfolgend ein kleines Tutorial für den Schnelleinstieg in diese interessante Analysetool auf Kommandozeile. Die CSV-Datei einfach hier downloaden: (einen Überblick über den Inhalt bietet auch eine Einführung in Python, die ebenfalls auf dieser CSV-Datei basiert)

CSV-Datei gedownloaded? Dann kann es losgehen im Terminal jeder beliebiger Linux-Distribution:

Anweisungen, so auch die obige, beginnen stets mit “awk”. Da diese CSV-Datei nicht mit dem Standardchar (Komma), sondern einem vertikalen Strich (Pipe) getrennt ist, muss dies via “-F’|'” angegeben werden. Wäre das Trennzeichen ein Semikolon, wäre der Parameter “-F’;'” korrekt. Der Befehl gibt jede Zeile des CSV in der Kommandozeile aus, so dass wie nachfolgend den gesamten Dateiinhalt sehen:

Viele CSV- und Logdateien haben keinen Header, diese hier hat jedoch die erste Zeile als Header, die daher bei der Analyse nicht als Werte-Zeile fehlinterpretiert werden darf, daher wird nachfolgend von nun an die Anweisung “NR>1” mitgegeben:

Spalten werden in awk über das Dollarzeichen angesprochen, folgende Anweisung zeigt uns alle Zeilen der zweiten Spalte:

Diese Skriptsprache beherrscht assoziative Arrays. Es können demnach auch nicht-numerische Schlüssel für den Zugriff auf Datenfelder verwendet werden. Dies machen wir uns für das Anzeigen aller Standorte mit Angabe der jeweiligen Mitarbeiterzahl an dem Standort zu nutze. Die Variable a speichert alle Mitarbeiterzahlen in Spalte 4 über den Schlüssel des Standortnamens in Spalte 2, dann endet der Anweisungsblock und es folgt eine For-Schleife, die alle Schlüsselwerte ausgibt und den dazugehörigen Speicherwert (Mitarbeiterzahl) ausgibt.

Auch If-Anweisungen sind einfach machbar. Folgendes Beispiel unterscheidet die Zeilennummern (Spalte1) nach geraden und ungeraden Zahlen und gibt den dazugehörigen Standortnamen (Spalte 2) aus.

Folgendes Beispiel klassifiziert alle Standorte mit weniger als 10 Mitarbeitern, allerdings nicht über “if…else…”, sondern über die Kurzabfrage nach dem Schema a>b?”True”:”False”.

Folgendes Code-Beispiel zeigt die Zählung der Vorkommnisse (Entsprechung: GROUP BY Spalte3, Count(*)).

Etwas umformuliert, können wir auch die Werte pro Gruppe aufsummieren, nachfolgend beispielhaft der Gewinn (Einnahmen aus Spalte 5 – Kosten aus Spalte 6) und die Mitarbeiterzahl über die jeweilige Gruppe.

Das Zusammenführen von Zeichenketten erfolgt simpel durch Aneinandereihung:

Ein letztes Beispiel möchte keine einzelnen Zeilen des Datensatzes auflisten und auch keine Gruppierung unterscheiden, sondern die Zusammenfassung über die Angabe der gesamten Mitarbeiteranzahl und der Gewinn-Summe über alle Standorte angeben.

Fazit

Als Programmiersprache ist awk sicherlich nur ein nice-to-have, aber wenn man das Prinzip dieser Sprache erstmal verstanden hat, kann sie ein interessantes Tool darstellen, um schon auf Kommandozeilenebene sich schnell einen Überblick über Datenbestände zu beschaffen und auch um Datenqualitätstests durchzuführen.