Erzeugung synthetischer Daten

Synthetische Daten mit Python

Roboter erzeugen synthetische Daten

Ein Problem des maschinellen Lernens, - insbesondere wenn man sich am Einarbeiten ist mehr über die Algorithmen erfahren möchte, - besteht darin, dass es oft schwierig ist, geeignete Testdaten zu erhalten. Einige kosten viel Geld, andere sind nicht frei verfügbar, weil sie urheberrechtlich geschützt sind. Deshalb können künstlich erzeugte Testdaten in einigen Fällen eine Lösung sein.

Aus diesem Grund befasst sich dieses Kapitel unseres Tutorials mit der künstlichen Datenerzeugung. In diesem Kapitel geht es darum, künstliche Daten zu erstellen. In den vorherigen Kapiteln unseres Tutorials haben wir gelernt, dass Scikit-Learn (sklearn) verschiedene Datensätze enthält. Einerseits gibt es kleine Spielzeug-Datensätze, andererseits bietet dieses Modul auch größere Datensätze, die häufig verwendet werden, um Algorithmen zu testen oder auch als Benchmark zu dienen. sklearn bietet uns Daten, die aus der "realen Welt" stammen.

All dies ist großartig, aber in vielen Fällen reicht dies immer noch nicht aus. Vielleicht findet man die richtigen Art von Daten, aber man benötigt mehr Daten dieser Art, oder die Daten entsprechen nicht vollständig dem, was man sucht. So benötigt man beispielsweise komplexere oder weniger komplexere Daten. Dies ist der Punkt, an dem man in Betracht ziehen sollte, die Daten selbst zu erstellen. Hier bietet sklearn Hilfe an. Es enthält verschiedene random-Beispielgeneratoren, mit denen benutzerdefinierte künstliche Datasets erstellt werden können. Datasets, die den eigenen Ideen von Größe und Komplexität entsprechen.

Der folgende Python-Code ist ein einfaches Beispiel, eines welches noch nicht sklearn benutzt. Wir erzeugen künstliche Wetterdaten für einige deutsche Städte. Dazu verwenden wir Pandas und Numpy, um die Daten zu erstellen:

import numpy as np
import pandas as pd


cities = ['Berlin', 'Frankfurt', 'Hamburg', 
          'Nuremberg', 'Munich', 'Stuttgart',
          'Hanover', 'Saarbruecken', 'Cologne',
          'Constance', 'Freiburg', 'Karlsruhe'
         ]

n= len(cities)
data = {'Temperature': np.random.normal(24, 3, n),
        'Humidity': np.random.normal(78, 2.5, n),
        'Wind': np.random.normal(15, 4, n)
       }
df = pd.DataFrame(data=data, index=cities)
df
Ausgabe:
Temperature Humidity Wind
Berlin 22.320235 80.220367 23.039794
Frankfurt 25.580538 78.025427 13.852446
Hamburg 29.184371 80.720426 5.178373
Nuremberg 24.162373 77.096790 21.191182
Munich 20.491273 79.605320 18.713445
Stuttgart 18.519613 82.019420 15.877972
Hanover 21.291383 80.000568 10.231694
Saarbruecken 27.821346 79.696954 10.818042
Cologne 22.364220 74.496690 14.037689
Constance 26.309676 81.463110 14.709574
Freiburg 18.229215 76.792430 13.387500
Karlsruhe 26.264173 75.027791 12.973147

Ein weiteres Beispiel

Wir erzeugen nun synthetische Daten für vier nicht existierende Blumensorten:

  • Flos Pythonem
  • Flos Java
  • Flos Margarita
  • Flos artificialis

Die durchschnittlichen RGB-Farbwerte entsprechen folgenden Werten:

  • (255, 0, 0)
  • (245, 107, 0)
  • (206, 99, 1)
  • (255, 254, 101)

Der durchschnittliche Durchmesser des Calyx ist:

  • 3.8
  • 3.3
  • 4.1
  • 2.9
Flos pythonem
(254, 0, 0)
Flos Java
(245, 107, 0)
Flos margarita
(206, 99, 1)
Flos artificialis
(255, 254, 101)
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

from scipy.stats import truncnorm

def truncated_normal(mean=0, sd=1, low=0, upp=10, type=int):
    return truncnorm(
        (low - mean) / sd, (upp - mean) / sd, loc=mean, scale=sd)

def truncated_normal_floats(mean=0, sd=1, low=0, upp=10, num=100):
    res = truncated_normal(mean=mean, sd=sd, low=low, upp=upp)
    return res.rvs(num)

def truncated_normal_ints(mean=0, sd=1, low=0, upp=10, num=100):
    res = truncated_normal(mean=mean, sd=sd, low=low, upp=upp)
    return res.rvs(num).astype(np.uint8)

# number of items for each flower class:
number_of_items_per_class = [190, 205, 230, 170]
flowers = {}
# flos Pythonem:
number_of_items = number_of_items_per_class[0]
reds = truncated_normal_ints(mean=254, sd=18, low=235, upp=256,
                             num=number_of_items)
greens = truncated_normal_ints(mean=107, sd=11, low=88, upp=127,
                             num=number_of_items)
blues = truncated_normal_ints(mean=0, sd=15, low=0, upp=20,
                             num=number_of_items)
calyx_dia = truncated_normal_floats(3.8, 0.3, 3.4, 4.2,
                             num=number_of_items)
data = np.column_stack((reds, greens, blues, calyx_dia))
flowers["flos_pythonem"] = data

# flos Java:
number_of_items = number_of_items_per_class[1]
reds = truncated_normal_ints(mean=245, sd=17, low=226, upp=256,
                             num=number_of_items)
greens = truncated_normal_ints(mean=107, sd=11, low=88, upp=127,
                             num=number_of_items)
blues = truncated_normal_ints(mean=0, sd=10, low=0, upp=20,
                             num=number_of_items)
calyx_dia = truncated_normal_floats(3.3, 0.3, 3.0, 3.5,
                             num=number_of_items)
data = np.column_stack((reds, greens, blues, calyx_dia))
flowers["flos_java"] = data

# flos Margarita:
number_of_items = number_of_items_per_class[2]
reds = truncated_normal_ints(mean=206, sd=17, low=175, upp=238,
                             num=number_of_items)
greens = truncated_normal_ints(mean=99, sd=14, low=80, upp=120,
                             num=number_of_items)
blues = truncated_normal_ints(mean=1, sd=5, low=0, upp=12,
                             num=number_of_items)
calyx_dia = truncated_normal_floats(4.1, 0.3, 3.8, 4.4,
                             num=number_of_items)
data = np.column_stack((reds, greens, blues, calyx_dia))
flowers["flos_margarita"] = data

# flos artificialis:
number_of_items = number_of_items_per_class[3]
reds = truncated_normal_ints(mean=255, sd=8, low=245, upp=256,
                             num=number_of_items)
greens = truncated_normal_ints(mean=254, sd=10, low=240, upp=255,
                             num=number_of_items)
blues = truncated_normal_ints(mean=101, sd=5, low=90, upp=112,
                             num=number_of_items)
calyx_dia = truncated_normal_floats(2.9, 0.4, 2.4, 3.5,
                             num=number_of_items)
data = np.column_stack((reds, greens, blues, calyx_dia))
flowers["flos_artificialis"] = data


data = np.concatenate((flowers["flos_pythonem"], 
                      flowers["flos_java"],
                      flowers["flos_margarita"],
                      flowers["flos_artificialis"]
                     ), axis=0)

# assigning the labels
target = np.zeros(sum(number_of_items_per_class)) # 4 flowers
previous_end = 0
for i in range(1, 5):
    num = number_of_items_per_class[i-1]
    beg = previous_end
    target[beg: beg + num] += i
    previous_end = beg + num
    
conc_data = np.concatenate((data, target.reshape(target.shape[0], 1)),
                           axis=1)

from pathlib import Path
Path("data").mkdir(exist_ok=True)
np.savetxt("data/strange_flowers.txt", conc_data, fmt="%2.2f")
import matplotlib.pyplot as plt

target_names = list(flowers.keys())
feature_names = ['red', 'green', 'blue', 'calyx']
n = 4
fig, ax = plt.subplots(n, n, figsize=(16, 16))

colors = ['blue', 'red', 'green', 'yellow']

for x in range(n):
    for y in range(n):
        xname = feature_names[x]
        yname = feature_names[y]
        for color_ind in range(1, len(target_names)+1):
            ax[x, y].scatter(data[target==color_ind, x], 
                             data[target==color_ind, y],
                             label=target_names[color_ind-1],
                             c=colors[color_ind-1])

        ax[x, y].set_xlabel(xname)
        ax[x, y].set_ylabel(yname)
        ax[x, y].legend(loc='upper left')


plt.show()
No description has been provided for this image

Synthetische Daten mit sklearn erzeugen

Deutlich einfacher gestaltet sich die Erzeugung von synthetischen Daten mit dem Modul sklearn.

Die in sklearn verfügbaren Funktionalitäten können wir folgt gruppiert werden:

  1. Generatoren zur Klassifikation und Clustering
  2. Generatoren zur Erzeugung von Daten zur Regression
  3. Generatoren für "Manifold Learning"
  4. Generatoren für Zerlegungs-Probleme (Decomposition)

Generatoren zur Klassifikation und Clustering

Wir starten mit der Funktion make_blobs von sklearn.datasets um Klecks-ähnliche (englisch: blob) Daten-Strukturen zu erzeugen. Indem wir den Wert von centers auf n_classes setzen, bestimmen wir die Anzahl der Blobs, d. h. der Cluster. n_samples entspricht der Anzahl der Datenpunkte, gleichverteilt über alle Klassen. Falls random_state nicht gesetzt ist, werden wir jedesmal, wenn wir die Funktion aufrufen, andere Zufallwerte erhalten. Wir weisen diesem Parameter eine ganze Zahl zu, um reproduzierbare Werte zu erzeugen.

from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
import numpy as np

data, labels = make_blobs(
    n_samples=1000,
    centers=np.array([[2, 3], [4, 5], [7, 9]]),
    random_state=1,
)

# Für das Speichern hängen wir die Labels nur temporär als Spalte an.
# `labels` selbst bleibt eindimensional, wie es scikit-learn erwartet.
labels_column = labels[:, np.newaxis]
all_data = np.concatenate((data, labels_column), axis=1)
np.savetxt("squirrels.txt", all_data)
all_data[:10]
Ausgabe:
array([[ 1.72415394,  4.22895559,  0.        ],
       [ 4.16466507,  5.77817418,  1.        ],
       [ 4.51441156,  4.98274913,  1.        ],
       [ 1.49102772,  2.83351405,  0.        ],
       [ 6.0386362 ,  7.57298437,  2.        ],
       [ 5.61044976,  9.83428321,  2.        ],
       [ 5.69202866, 10.47239631,  2.        ],
       [ 6.14017298,  8.56209179,  2.        ],
       [ 2.97620068,  5.56776474,  1.        ],
       [ 8.27980017,  8.54824406,  2.        ]])

reshape und concatenate im kleinen Beispiel

Im vorherigen Beispiel werden die von make_blobs erzeugten Klassenlabels zunächst zu einer Spalte umgeformt und anschließend mit den Merkmalsdaten verbunden. Das folgende Minimalbeispiel zeigt die beiden Schritte ohne weiteren Kontext:

import numpy as np
a = np.array( [[1, 2], [3, 4]])
b = np.array( [5, 6])
b = b.reshape((b.shape[0], 1))
print(b)
x = np.concatenate( (a, b), axis=1)
x
[[5]
 [6]]
Ausgabe:
array([[1, 2, 5],
       [3, 4, 6]])

Daten wieder einlesen und Merkmale und Labels trennen

Die zuvor gespeicherten Daten können mit numpy.loadtxt wieder eingelesen werden. Anschließend trennen wir die Merkmalsmatrix data und den Zielvektor labels wieder voneinander:

file_data = np.loadtxt("squirrels.txt")
data = file_data[:,:-1]
labels = file_data[:,2:]
labels = labels.reshape((labels.shape[0]))
import matplotlib.pyplot as plt
colours = ('green', 'red', 'blue', 'magenta', 'yellow', 'cyan')
n_classes = 3
fig, ax = plt.subplots()
for n_class in range(0, n_classes):
    ax.scatter(data[labels==n_class, 0], data[labels==n_class, 1], 
               c=colours[n_class], s=10, label=str(n_class))
ax.set(xlabel='Night Vision',
       ylabel='Fur color from sandish to black, 0 to 10 ',
       title='Sahara Virtual Squirrel')
ax.legend(loc='upper right')
Ausgabe:
<matplotlib.legend.Legend at 0x7fc1037cfb10>
No description has been provided for this image

Trainings- und Testdaten bilden

Für ein Klassifikationsbeispiel teilen wir die synthetischen Daten mit train_test_split reproduzierbar in Trainings- und Testdaten. Danach trainieren wir einen KNeighborsClassifier und bestimmen seine Trefferquote auf den Testdaten:

from sklearn.model_selection import train_test_split
train_data, test_data, train_labels, test_labels = train_test_split(
    data, labels, test_size=0.2, random_state=42, stratify=labels
)
# import model
from sklearn.neighbors import KNeighborsClassifier
# create classifier
knn = KNeighborsClassifier(n_neighbors=8)
# train
knn.fit(train_data, train_labels)
# test on test data:
calculated_labels = knn.predict(test_data)
calculated_labels
Ausgabe:
array([1., 1., 0., 1., 1., 0., 2., 0., 1., 1., 0., 2., 0., 0., 2., 0., 0.,
       0., 2., 2., 0., 0., 1., 1., 1., 2., 1., 0., 2., 1., 2., 2., 0., 1.,
       0., 1., 1., 1., 0., 2., 0., 2., 1., 2., 0., 0., 1., 1., 0., 1., 1.,
       1., 2., 0., 1., 0., 2., 2., 0., 1., 2., 1., 1., 0., 2., 0., 1., 1.,
       2., 2., 2., 1., 2., 2., 0., 2., 2., 0., 0., 2., 2., 2., 2., 1., 0.,
       2., 1., 2., 1., 2., 2., 1., 0., 2., 2., 1., 2., 0., 2., 0., 1., 1.,
       1., 2., 1., 2., 0., 0., 2., 0., 0., 2., 2., 2., 0., 2., 2., 2., 2.,
       2., 1., 1., 2., 1., 0., 0., 0., 0., 0., 0., 0., 2., 0., 1., 1., 1.,
       1., 1., 2., 1., 2., 0., 0., 0., 0., 2., 1., 0., 0., 0., 0., 1., 1.,
       0., 0., 1., 2., 1., 1., 0., 2., 0., 1., 1., 0., 0., 2., 2., 1., 0.,
       0., 1., 2., 2., 2., 0., 2., 0., 1., 1., 0., 0., 2., 0., 1., 1., 0.,
       1., 2., 0., 2., 1., 2., 0., 2., 1., 0., 1., 1., 1.])
from sklearn import metrics
print("Accuracy:", metrics.accuracy_score(test_labels, calculated_labels))
Accuracy: 0.935

Weitere interessante synthetische Verteilungen

Neben Gaußschen Clustern bietet sklearn.datasets Generatoren für nichtlineare Strukturen. make_moons erzeugt zwei ineinandergreifende Halbmonde, make_circles zwei konzentrische Kreise. Solche Datensätze sind nützlich, um zu demonstrieren, wie Klassifikationsverfahren mit nichtlinearen Entscheidungsgrenzen umgehen.

import numpy as np
import sklearn.datasets as ds

data, labels = ds.make_moons(
    n_samples=150,
    shuffle=True,
    noise=0.19,
    random_state=42
)

# Verschiebe beide Koordinaten so, dass die kleinsten Werte bei 0 liegen.
data -= data.min(axis=0)
data.min(axis=0)
Ausgabe:
array([0., 0.])
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.scatter(data[labels==0, 0], data[labels==0, 1], 
               c='orange', s=40, label='oranges')
ax.scatter(data[labels==1, 0], data[labels==1, 1], 
               c='blue', s=40, label='blues')
ax.set(xlabel='X',
       ylabel='Y',
       title='Moons')
#ax.legend(loc='upper right');
Ausgabe:
[Text(0.5, 0, 'X'), Text(0, 0.5, 'Y'), Text(0.5, 1.0, 'Moons')]
No description has been provided for this image

Wertebereiche skalieren

Sollen Werte aus einem Intervall [min, max] linear auf ein neues Intervall [a, b] abgebildet werden, verwenden wir

$$f(x) = \frac{(b-a)(x-min)}{max-min} + a.$$

Das folgende Beispiel wendet diese Transformation auf beide Koordinaten eines make_moons-Datensatzes an:

min_x_new, max_x_new = 33, 88
min_y_new, max_y_new = 12, 20
data, labels = ds.make_moons(n_samples=100, 
                             shuffle=True, 
                             noise=0.05, 
                             random_state=None)
min_x, min_y = np.ndarray.min(data[:,0]), np.ndarray.min(data[:,1])
max_x, max_y = np.ndarray.max(data[:,0]), np.ndarray.max(data[:,1])
#data -= np.array([min_x, 0]) 
#data *= np.array([(max_x_new - min_x_new) / (max_x - min_x), 1])
#data += np.array([min_x_new, 0]) 
#data -= np.array([0, min_y]) 
#data *= np.array([1, (max_y_new - min_y_new) / (max_y - min_y)])
#data += np.array([0, min_y_new]) 
data -= np.array([min_x, min_y]) 
data *= np.array([(max_x_new - min_x_new) / (max_x - min_x), (max_y_new - min_y_new) / (max_y - min_y)])
data += np.array([min_x_new, min_y_new]) 
#np.ndarray.min(data[:,0]), np.ndarray.max(data[:,0])
data[:6]
Ausgabe:
array([[84.75580656, 15.53468745],
       [34.09401859, 15.67229095],
       [37.75927425, 18.01338358],
       [76.37107584, 13.2117179 ],
       [50.37364723, 17.25049973],
       [63.70538119, 18.5262856 ]])
def scale_data(data, new_limits, inplace=False ):
    if not inplace:
        data = data.copy()
    min_x, min_y = np.ndarray.min(data[:,0]), np.ndarray.min(data[:,1])
    max_x, max_y = np.ndarray.max(data[:,0]), np.ndarray.max(data[:,1])
    min_x_new, max_x_new = new_limits[0]
    min_y_new, max_y_new = new_limits[1]
    data -= np.array([min_x, min_y]) 
    data *= np.array([(max_x_new - min_x_new) / (max_x - min_x), (max_y_new - min_y_new) / (max_y - min_y)])
    data += np.array([min_x_new, min_y_new]) 
    if inplace:
        return None
    else:
        return data
    
    
data, labels = ds.make_moons(n_samples=100, 
                             shuffle=True, 
                             noise=0.05, 
                             random_state=None)
scale_data(data, [(1, 4), (3, 8)], inplace=True)
data[:10]
Ausgabe:
array([[1.62803808, 7.79775805],
       [2.50808739, 3.86021853],
       [2.95293319, 5.55644351],
       [2.31749794, 7.37316116],
       [3.92818055, 5.75614403],
       [2.00418923, 6.1702033 ],
       [3.00777824, 4.82584303],
       [4.        , 6.09192639],
       [1.11976711, 5.72520204],
       [1.65727635, 7.64797219]])
fig, ax = plt.subplots()
ax.scatter(data[labels==0, 0], data[labels==0, 1], 
               c='orange', s=40, label='oranges')
ax.scatter(data[labels==1, 0], data[labels==1, 1], 
               c='blue', s=40, label='blues')
ax.set(xlabel='X',
       ylabel='Y',
       title='moons')
 
ax.legend(loc='upper right');
No description has been provided for this image
import sklearn.datasets as ds
data, labels = ds.make_circles(n_samples=100, 
                             shuffle=True, 
                             noise=0.05, 
                             random_state=None)
fig, ax = plt.subplots()
ax.scatter(data[labels==0, 0], data[labels==0, 1], 
               c='orange', s=40, label='oranges')
ax.scatter(data[labels==1, 0], data[labels==1, 1], 
               c='blue', s=40, label='blues')
ax.set(xlabel='X',
       ylabel='Y',
       title='circles')
ax.legend(loc='upper right')
Ausgabe:
<matplotlib.legend.Legend at 0x7fc102940cd0>
No description has been provided for this image

make_classification

Mit make_classification lassen sich kontrollierte Klassifikationsprobleme mit einer wählbaren Zahl informativer und redundanter Merkmale erzeugen:

from sklearn.datasets import make_classification
import matplotlib.pyplot as plt

X, y = make_classification(
    n_samples=400,
    n_features=2,
    n_redundant=0,
    n_informative=2,
    n_clusters_per_class=1,
    random_state=42
)

fig, ax = plt.subplots()
ax.scatter(X[:, 0], X[:, 1], c=y, s=20)
ax.set(title="Synthetischer Klassifikationsdatensatz", xlabel="Merkmal 1", ylabel="Merkmal 2")
Ausgabe:
[Text(0.5, 1.0, 'Synthetischer Klassifikationsdatensatz'),
 Text(0.5, 0, 'Merkmal 1'),
 Text(0, 0.5, 'Merkmal 2')]
No description has been provided for this image