Notizen zu Machine Learning, Teil 1

Es folgen ein paar Notizen zu Machine Learning mit Python, scikit-learn, Keras und Pandas, wie sie mir untergekommen sind. Vielleicht ist das auch für andere nützlich.

CSV lesen mit Pandas

Mit Pandas ein CSV-File lesen:

import pandas as pd
dataset = pd.read_csv("dataset.csv")

Unter der Annahme, dass das Klassenattribut ganz rechts steht, trennt man die Attribute x und das Klassenattribut y wie folgt:

class_attr = dataset.columns.size - 1
y = dataset.iloc[:, class_attr]
x = dataset.iloc[:, 0:class_attr]

Wenn im CSV (außer in der Titelzeile) nur Zahlen stehen und keine weitere Vorverarbeitung stattfinden soll war es das schon.

Die erste Zeile im CSV wird automatisch als Titelzeile für die Attributnamen genommen.

Klassifikation mit scikit-learn

Einen Klassifikator mit scikit-learn kriegt man dann sehr einfach. Im Beispiel sind ein Multinomial Naive Bayes, ein Entscheidungsbaum, eine SVM und k-Nearest-Neighbour:

from sklearn.naive_bayes import MultinomialNB
from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import LinearSVC
from sklearn.neighbors import KNeighborsClassifier

classifier = MultinomialNB()
classif_tree = DecisionTreeClassifier()
classif_svm = LinearSVC()
classif_knn = KNeighborsClassifier(n_neighbors=1)

Dann kann man entweder trainieren mit fit und die Vorhersagen holen mit predict:

classifier.fit(x, y)
predictions = classifier..predict(x)

Dann sollte man aber vorher einen Split in Trainings- und Testdaten gemacht haben, wenn man nicht auf dem Trainingsset testen will.

Cross-validation mit scikit-learn

Oder man macht gleich eine standardmäßige stratified 10-fold-cross-validation. Davon gibt es verschiedene Varianten in scikit-learn, entweder unter Vorgabe einer Metrik oder, wie hier gezeigt, einfach indem man die Predictions mit cross-validation holt und anschließend auswertet:

from sklearn.model_selection import cross_val_predict
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import confusion_matrix
from sklearn.metrics import classification_report

predictions = cross_val_predict(classifier, x, y, cv=StratifiedKFold(n_splits=10), n_jobs=-1)

print(confusion_matrix(y, predictions))
print(classification_report(y, predictions))

Die -1 bei n_jobs bewirkt, dass so viele parallele Threads gestartet werden wie die CPU (logische) Kerne hat.

Textdaten

Textdaten müssen tokenisiert werden. Das ist mit scikit-learn möglich. Textdatensätze kommen auch nicht immer als CSV, sondern z.B in zwei getrennten Dateien X.txt und Y.txt. In der ersten Datei steht ein Text pro Zeile, in der zweiten Datei steht jeweils in der gleichen Zeile die zugehörige Klasse.

Man kann die Vorverarbeitung und Klassifikation in scikit-learn in eine Pipeline packen, dann klappt es auch ganz wunderbar mit einer ordentlichen cross-validation.

Eine Maximalgröße für das Vokabular kann man vorgeben, muss man aber nicht. Wenn man z.B. für die Einbindung eines neuronalen Netzes mit Keras (s. weiter unten) vorher wissen muss, wie viele Attribute es gibt, kann man das machen.

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.pipeline import Pipeline

vocab_size = 20000

with open(filename_x) as file_x:
    X = file_x.readlines()
    file_x.close()

with open(filename_y) as file_y:
    Y = file_y.readlines()
    file_y.close()

if len(X) != len(Y):
    print("Fehler: X und Y unterschiedlich lang")
    exit()

print(f"{len(X)} Instanzen geladen.")

classifier = Pipeline([('tfc', TfidfVectorizer(max_features=vocab_size)), ('clf', MultinomialNB())])
predictions = cross_val_predict(classifier, X, Y, cv=StratifiedKFold(n_splits=10), n_jobs=-1)

print(classification_report(Y, predictions))

Keras-Wrapper

Es gibt einen Keras-Wrapper für scikit-learn, um ein neronales Netz aus Keras wie einen scikit-learn classifier benutzen zu können.

PlaidML

Wenn man keine Nvidia-Grafikkarte (mit CUDA) hat und trotzdem auf der GPU rechnen möchte, kann man sich vom TensorFlow-Backend von Keras verabschieden und stattdessen PlaidML nehmen, dann klappt’s auch mit AMD (und wahrscheinlich auch Intel, hab‘ ich nicht getestet, nehme ich aber an, da PlaidML inzwischen von Intel kommt). Dazu nimmt man, bevor man Keras einbindet:

import plaidml.keras
plaidml.keras.install_backend()

Exkurs: TensorFlow

Manchmal ist bei mir PlaidML langsam, obwohl es die GPU nutzt. Das Phänomen tritt speziell bei rekurrenten Netzen auf, dann wird es extrem langsam. Bei eher einfachen Netzen mit wenig Neuronen wie das aus dem Beispiel unten kann es aber auch passieren. Vielleicht liegt das am Overhead der Interaktion mit der GPU? Weiß jemand mehr?

Falls man also doch TensorFlow haben will und sich daran stört, dass die standardmäßigen Binaries so überhaupt keine Features halbwegs moderner CPUs nutzen (AVX, SSE, usw.) kann man sich Binaries, bei denen die Extensions aktiviert sind, auf einer github-Seite holen. (Warum ist das eigentlich nicht drin? Gibt’s ernsthaft noch CPUs ohne AVX und SSE4?)

Um ein einzelnes Wheel zu installieren, das ein bereits installiertes ersetzen soll, ohne dass die ganzen Abhängigkeiten geprüft werden, nimmt man:

pip install --force-reinstall --no-deps Downloads/tensorflow-1.13.1-cp36-cp36m-macosx_10_13_x86_64.whl 

Ich habe damit ein über conda installiertes TensorFlow ersetzt und das Vorgehen in der Tat noch nicht auf Spätfolgen für das conda env getestet, aber bis jetzt läuft es problemlos und vor allem spürbar schneller als ohne AVX und SSE. Aus Gründen habe ich mir aber trotzdem vorher das conda env dupliziert, um ein Backup zu haben.

Weiter mit Keras

Für die Keras-scikit-learn-Integration lagert man den Netzaufbau in eine build-Funktion aus, hier mit einem beispielhaften Netz mit drei Dense Layers. Man muss vorher die Anzahl der Attribute kennen (wenn man Textdaten hat und die Anzahl der Attribute nach der Tokenisierung erst mal nicht kennt, kann man das über eine Begrenzung der Größe des Vokabulars machen, s.o.) und die Anzahl der Klassen kennen. Eine quick’n’dirty-Variante, wenn man die Anzahl der Klassen nicht vorher kennt die Performance eh mit einer Baseline vergleich will ist, sie sich aus der confusion matrix zu ziehen.

nb_text = Pipeline([('tfc', TfidfVectorizer(max_features=vocab_size)), ('clf', MultinomialNB())])
p_nb = cross_val_predict(nb_text, X, Y, cv=StratifiedKFold(n_splits=10), n_jobs=-1)

print("Naive Bayes:")
cm_nb = confusion_matrix(Y, p_nb)
nclasses = len(cm_nb) # Anzahl der Klassen :-)
print(cm_nb)
print(classification_report(Y, p_nb))

Ebenfalls sehr quick’n’dirty ist, die Anzahl der Attribute und Klassen in die Build-Funktion mittels globaler Variablen rein zu holen, damit man sich die Parameterübergabe über die Pipeline spart.

from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
from keras.wrappers.scikit_learn import KerasClassifier

def build_model():
    model = Sequential()
    model.add(Dense(32, input_dim=vocab_size, activation='relu'))
    model.add(Dropout(0.2))
    model.add(Dense(32))
    model.add(Dropout(0.2))
    model.add(Dense(nclasses, activation='softmax'))
    model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
    return model

keras_txt = Pipeline([
    ('tfc', TfidfVectorizer(max_features=vocab_size)),
    ('keras', KerasClassifier(build_fn=build_model, epochs=20, batch_size=64, verbose=2))
])

p_keras = cross_val_predict(keras_txt, X, Y, cv=StratifiedKFold(n_splits=10), n_jobs=1)
print("Neuronales Netz:")
print(confusion_matrix(Y, p_keras))
print(classification_report(Y, p_keras))

Sollte jemand diese Notizen für nützlich befinden: Viel Spaß beim Experimentieren! 😀👍

Quellen

(Alles Stand 13.06.19)