Der etwas andere Einstellungstest

Jeder Informatiker kennt es und die meisten haben es selbst implementiert – FizzBuzz. Der Klassiker der Einstellungstests, der eigentlich als Mathematik Übung für Grundschüler dient. Doch wie verhält sich FizzBuzz, wenn man Deep Learning darauf ansetzt? Für diejenigen die noch nicht das Glück hatten mit Fizzbuzz in Berührung zu kommen, hier nochmal die Aufgabenstellung:

Schreiben Sie ein Programm, dass als Input ganze Zahlen erhält. Für Vielfache von drei soll das Programm „Fizz“ ausgeben und für Vielfache von fünf „Buzz“. Bei Zahlen, die sowohl ein Vielfaches von drei als auch von fünf sind, soll „FizzBuzz“ ausgegeben werden. Für alle anderen Zahlen soll „other“ ausgegeben werden.

Eine typische Lösung für die Aufgabe sieht in Python etwa so aus:

def fizz_buzz(n):
    if n % 3 == 0 and n % 5 == 0:
        print('FizzBuzz')
    elif n % 5 == 0:
        print('Buzz')
    elif n % 3 == 0:
        print('Fizz')
    else:
        print('other')


Datensatz erzeugen

Um diese Aufgabe mit Deep Learning zu lösen brauchen wir als erstes einen passenden Datensatz. Mit einer leicht abgeänderten Version der obigen FizzBuzz-Implementierung lässt sich problemlos das zugehörige Label generieren.

def label_of_int(n):
    if n % 3 == 0 and n % 5 == 0:
        return [0, 0, 0, 1]  # fizzbuzz
    elif n % 5 == 0:
        return [0, 0, 1, 0]  # buzz
    elif n % 3 == 0:
        return [0, 1, 0, 0]  # fizz
    else:
        return [1, 0, 0, 0]  # other number

Als Input für das neuronales Netz wird die Binärdarstellung der Zahlen verwendet. Dazu lassen wir uns alle Zahlen von 0 bis 1024 in Binärdarstellung mit folgender Funktion erzeugen:

def binary_of_int(n):
    binary = []
    for i in range(10): # 1024 needs 10 bits
        binary.append(n >> i & 1)
    return binary

Klassengewichte anpassen

Damit haben wir unsere benötigten Features (Binärdarstellung der Zahlen) und die zugehörigen Labels (Einteilung in „Fizz“, „Buzz“, „FizzBuzz“, „other number“). Jetzt könnten wir mit dem Training beginnen. Um jedoch das Ungleichgewicht der Daten zu kompensieren – die Verteilung der Labels im Bereich 0 bis 1024 ist nicht gleichmäßig – passen wir noch die class_weights an. Mit class_weight können wir beim Fitten des Models in Keras angeben, dass die unterrepräsentierten Klassen beim Training stärker gewichtet werden sollen.

total_count = len(all_numbers)
class_weight = {
    0: total_count / count_other_number(all_numbers),
    1: total_count / count_fizz(all_numbers),
    2: total_count / count_buzz(all_numbers),
    3: total_count / count_fizzbuzz(all_numbers),
}

Netzarchitektur

Als nächstes zur eingesetzten Netzarchitektur. Unser Datenset besteht aus Binärzahlen zwischen 0 und 1024. Um die größte Zahl, 1024, binär darzustellen, werden 10 Bit benötigt. Daraus ergibt sich der Aufbau der Inputschicht unseres neuronalen Netzes aus 10 Neuronen. Darauf folgen zwei versteckte Layerpaare. Das erste Paar besteht aus einer Hiddenlayer mit jeweils 128 Neuronen, relu als Aktivierungfunktion und einer Dropoutlayer mit einer Verlustrate von 0,3. Das zweite Paar unterscheidet sich lediglich in der Verlustrate von 0,2 anstatt 0,3. Die Outputschicht hat 4 Neuronen mit Aktivierungsfunktion softmax, um die Kategorisierung in 4 Labelklassen zu ermöglichen. Als Optimizer verwenden wir sgd (stochastischen Gradientenabstieg) und als Fehlerfunktion categorial crossentropy. Zum Finden der besten Hyperparameter eignet sich unser Blog Post wie optimiere ich ein neuronales Netz.

# 10 input neurons for the 10 bits we need to 
# represent numbers up to 1024
input_layer = l.Input(shape=(config.NUM_DIGITS_INPUT,))

hidden_layer = l.Dense(units=128, activation='relu')(input_layer)
hidden_layer = l.Dropout(0.3)(hidden_layer)

hidden_layer = l.Dense(units=128, activation='relu')(hidden_layer)
hidden_layer = l.Dropout(0.2)(hidden_layer)

# 4 output neurons for: fizzbuzz | fizz | buzz | other_number
output_layer = l.Dense(units=config.NUM_CLASSES_OUTPUT,
    activation='softmax')(hidden_layer)

model = Model(inputs=[input_layer], outputs=[output_layer])   

model.compile(optimizer='sgd', loss='categorical_crossentropy',
    metrics=['accuracy'])

Training

Das Ganze trainieren wir dann für 20.000 Epochen mit unserem berechneten class_weight und einem validation split von 0,2. Anschließend speichern wir unser Model.

model.fit(features, 
    labels, 
    batch_size=config.BATCH_SIZE, #256
    epochs=20000, 
    validation_split=0.2, 
    shuffle=True, 
    class_weight=class_weight,
    callbacks=[tb_callback])

model.save('weights/deepbuzz.h5')

Die Ergebnisse in Tensorboard zeigen eine validation accuracy von 97% und ein loss von 0,44.

Einsatz des Models

Um jetzt unser Model an anderer Stelle zu benutzen, laden wir mit load_model sowohl die Netzarchitektur, als auch die trainierten Gewichte. Wir fordern den User auf, einen Integer-Input einzugeben und wandeln diesen in Binärdarstellung um. Mit predict lassen wir das Netz unseren Input klassifizieren und geben dann das entsprechende Label aus.

# returns a compiled model identical to the trained one
model = load_model('weights/deepbuzz.h5', compile=True)

user_input = int(input("Input an integer "))

# binary of input
binary_number = data.binary_of_int(user_input)

# prediction
prediction = model.predict(np.array([binary_number]))

# get class label of argmax
prediction = data.class_of_label(prediction.argmax(axis=-1))
print(f'The number {user_input} is category: {prediction}')

Den gesamten Code des Projekts gibt es auf unserem Github.