Plötzlich begann ich in Kapitel 4 von "Deep Learning von Grund auf - Theorie und Implementierung von Deep Learning, das mit Python gelernt wurde" zu studieren. Es ist ein Memo der Reise.
Die Ausführungsumgebung ist macOS Mojave + Anaconda 2019.10 und die Python-Version ist 3.7.4. Weitere Informationen finden Sie in Kapitel 1 dieses Memos.
(Zu anderen Kapiteln dieses Memos: Kapitel 1 / Kapitel 2 / Kapitel 3 / Kapitel 4 / Kapitel 5 / [Kapitel 6](https: / /qiita.com/segavvy/items/ca4ac4c9ee1a126bff41) / Kapitel 7 / Kapitel 8 / Zusammenfassung)
Dieses Kapitel beschreibt das Lernen neuronaler Netze.
Normalerweise leitet eine Person Regelmäßigkeit ab, denkt an einen Algorithmus, schreibt ihn in ein Programm und lässt ihn von einem Computer ausführen. Maschinelles Lernen, neuronale Netze und tiefes Lernen lassen den Computer über diesen Algorithmus nachdenken.
In diesem Buch sind für die Daten, die Sie verarbeiten möchten, diejenigen, die die Extraktion von Merkmalsmengen (Vektorisierung usw.) erfordern, die die Leute im Voraus gedacht haben, "maschinelles Lernen", und dass "maschinelles Lernen" der Extraktion von Merkmalsmengen überlassen bleibt. Diejenige, die die Weitergabe von Rohdaten ermöglicht, wird als "neuronales Netzwerk (Deep Learning)" definiert. Diese Definition mag etwas grob erscheinen, aber ich bin nicht sehr an der richtigen Verwendung von Wörtern interessiert, also werde ich weitermachen, ohne mir darüber Sorgen zu machen.
Es erklärt Trainingsdaten, Testdaten, Übertraining usw., aber es gab keinen besonderen Stolperstein.
Dies ist eine Erklärung der Summe von Quadratfehlern und Kreuzentropiefehlern, die häufig als Verlustfunktionen verwendet werden, und eine Erklärung des Mini-Batch-Lernens, bei dem ein Teil der Trainingsdaten verwendet wird. Auch hier gab es keinen besonderen Stolperstein. Es scheint in Ordnung zu sein, alle Trainingsdaten zu verwenden, aber es braucht Zeit und ist ineffizient. Ich denke, es ist wie eine sogenannte Stichprobenerhebung.
Es wird auch erklärt, dass der Grund, warum die Erkennungsgenauigkeit nicht anstelle der Verlustfunktion verwendet werden kann, darin besteht, dass die Erkennungsgenauigkeit nicht auf winzige Änderungen im Ergebnis reagiert und sich diskontinuierlich ändert, sodass sie nicht gut gelernt werden kann. Es mag zunächst nicht zu Ihnen kommen, aber ich denke, Sie werden nach der Erklärung der nächsten Differenzierung wütend.
Es ist eine Erklärung der Differenzierung. Die Erklärung des Rundungsfehlers zum Zeitpunkt der Montage ist praktisch. Es scheint schwierig zu sein, die Wörter "Differential" und "partielles Differential" zu hören, aber wie ändert sich das Ergebnis, wenn Sie den Wert ein wenig ändern? Deshalb kann ich weitermachen, ohne die Mathematik der High School überprüfen zu müssen.
Übrigens wird das Symbol $ \ partielle $, das durch Differenzierung erscheint, als Wikipedia Del, Dee, Partial Dee, Round Dee usw. gelesen. Korrekt.
Trotzdem kann Python leicht eine Funktion als Argument übergeben. Als Programmierer war ich hauptsächlich C / C ++, aber ich hasste die Notation von Funktionszeigern, weil sie wirklich verwirrend war: Schweiß:
Der Gradient ist das partielle Differential aller Variablen als Vektor. Das an sich ist nicht schwierig.
Es ist schön zu sehen, dass der Wert gerundet und angezeigt wird, wenn ein Bruch mit einem NumPy-Array ausgegeben wird.
python
>>> import numpy as np
>>> a = np.array([1.00000000123, 2.99999999987])
>>> a
array([1., 3.])
Es kann jedoch ein Problem sein, wenn es ohne Erlaubnis aufgerollt wird, und als ich nachschaute, um welche Art von Spezifikationen es sich handelte, gab es eine Funktion zum Festlegen der Anzeigemethode. numpy.set_printoptions
, wie Brüche und viele Elemente angezeigt werden Sie können die Abkürzungsmethode des Falls ändern. Wenn Sie beispielsweise eine große Anzahl von Stellen nach dem Dezimalpunkt mit "Genauigkeit" angeben, wird diese angezeigt, ohne dass sie richtig gerundet sind.
python
>>> np.set_printoptions(precision=12)
>>> a
array([1.00000000123, 2.99999999987])
Das ist praktisch!
Das Wort "Gradientenabstiegsmethode" erscheint im Text, der in den Unterrichtsmaterialien, als ich zuvor studiert habe, als "steilste Abstiegsmethode" übersetzt wurde.
Auch das Symbol $ \ eta $, das die Lernrate anzeigt, wird angezeigt, aber dies wird in griechischen Buchstaben als eta gelesen (ich erinnerte mich, wie ich es gelesen hatte, als ich vorher studiert hatte, aber ich habe es völlig vergessen und gegoogelt : Schweiß :).
Ich benutze numerischer_gradient (f, x)
, um den Gradienten zu finden, aber die Funktion, die ich an dieses f
übergebe, ist
python
def f(W):
return net.loss(x, t)
Ist das? Verwendet diese Funktion das Argument "W"? Ich war ein wenig verwirrt, aber weil ich versuche, die Form der in "4.4 Gradient" implementierten Funktion "numerischer Gradient (f, x)" zu verwenden, ist das Argument "W" ein Dummy. Sicher, die simpleNet
-Klasse hat ihr eigenes Gewicht W
, sodass Sie das Gewicht W
nicht an die Verlustfunktion simpleNet.loss
übergeben müssen. Es ist schwer zu verstehen, ob es einen Dummy gibt, deshalb habe ich beschlossen, ihn ohne Argumente zu implementieren.
Auch hier müssen wir "numerischer Gradient" so ändern, dass er in einem mehrdimensionalen Array verwendet werden kann.
Von nun an werden wir die probabilistische Gradientenabstiegsmethode (SGD) unter Verwendung der bisher erlernten Methoden implementieren.
Das erste ist functions.py
, eine Sammlung notwendiger Funktionen.
functions.py
# coding: utf-8
import numpy as np
def sigmoid(x):
"""Sigmaid-Funktion
Da es bei der Implementierung des Buches überläuft, wird es unter Bezugnahme auf die folgende Site korrigiert.
http://www.kamishima.net/mlmpyja/lr/sigmoid.html
Args:
x (numpy.ndarray):Eingang
Returns:
numpy.ndarray:Ausgabe
"""
#Korrigieren Sie x in einem Bereich, der nicht überläuft
sigmoid_range = 34.538776394910684
x2 = np.maximum(np.minimum(x, sigmoid_range), -sigmoid_range)
#Sigmaid-Funktion
return 1 / (1 + np.exp(-x2))
def softmax(x):
"""Softmax-Funktion
Args:
x (numpy.ndarray):Eingang
Returns:
numpy.ndarray:Ausgabe
"""
#Für die Stapelverarbeitung ist x(Anzahl der Chargen, 10)Es wird eine zweidimensionale Anordnung von.
#In diesem Fall ist es erforderlich, für jedes Bild, das gesendet wird, eine gute Berechnung durchzuführen.
#Hier np, damit es in 1 und 2 Dimensionen geteilt werden kann..max()Und np.sum()Ist Achse=-Berechnet mit 1
#Keepdims, damit es so gesendet werden kann, wie es ist=True, um die Dimension beizubehalten.
c = np.max(x, axis=-1, keepdims=True)
exp_a = np.exp(x - c) #Überlaufmaßnahmen
sum_exp_a = np.sum(exp_a, axis=-1, keepdims=True)
y = exp_a / sum_exp_a
return y
def numerical_gradient(f, x):
"""Gradientenberechnung
Args:
f (function):Verlustfunktion
x (numpy.ndarray):Ein Array von Gewichtungsparametern, für die Sie den Verlauf überprüfen möchten
Returns:
numpy.ndarray:Steigung
"""
h = 1e-4
grad = np.zeros_like(x)
# np.Zählen Sie die Elemente eines mehrdimensionalen Arrays mit nditer auf
it = np.nditer(x, flags=['multi_index'])
while not it.finished:
idx = it.multi_index # it.multi_Index ist die Elementnummer in der Liste
tmp_val = x[idx] #Originalwert speichern
# f(x + h)Berechnung von
x[idx] = tmp_val + h
fxh1 = f()
# f(x - h)Berechnung von
x[idx] = tmp_val - h
fxh2 = f()
#Berechnen Sie den Gradienten
grad[idx] = (fxh1 - fxh2) / (2 * h)
x[idx] = tmp_val #Rückgabewert
it.iternext()
return grad
def cross_entropy_error(y, t):
"""Berechnung des Kreuzentropiefehlers
Args:
y (numpy.ndarray):Neuronale Netzwerkausgabe
t (numpy.ndarray):Richtiges Antwortetikett
Returns:
float:Kreuzentropiefehler
"""
#Formen Sie die Form, wenn nur ein Datenelement vorhanden ist
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size)
#Berechnen Sie den Fehler und normalisieren Sie ihn anhand der Anzahl der Chargen
batch_size = y.shape[0]
return -np.sum(t * np.log(y + 1e-7)) / batch_size
def sigmoid_grad(x):
"""In Kapitel 5 erlernte Funktionen. Erforderlich bei Verwendung der Fehlerrückverteilungsmethode.
"""
return (1.0 - sigmoid(x)) * sigmoid(x)
softmax
ist [Memo, dass ein Amateur in Deep Learning von Grund auf neu gestolpert ist: Kapitel 3](https://qiita.com/segavvy/items/6d79d0c3b4367869f4ea#35-%E5%87%BA%E5%8A% 9B% E5% B1% A4% E3% 81% AE% E8% A8% AD% E8% A8% 88) Ich habe versucht, es noch erfrischender zu machen. Ich verweise in der Ausgabe des GitHub-Repositorys dieses Buches auf Plan zur Verbesserung des Softmax-Funktionscodes Nr. 45. ..
numerischer_gradient
hat das im Argument f
übergebene Funktionsargument beseitigt, wie oben erwähnt. Es wird auch eine Schleife bei numpy.nditer
erstellt, um mehrdimensionale Arrays aufzunehmen. Im Code des Buches wird "op_flags = ['readwrite']" angegeben, wenn "numpy.nditer" verwendet wird, aber der Index für den Zugriff auf "x" wird nur von "multi_index" abgerufen. Ich habe op_flags
(op_flags = ['readonly']
) weggelassen, weil ich die vom Iterator aufgezählten Objekte nicht aktualisiere. Weitere Informationen finden Sie unter Iterieren über Arrays # Ändern von Array-Werten.
Die letzte Funktion sigmoid_grad
wird in Kapitel 5 gelernt, aber es ist notwendig, die Verarbeitungszeit zu verkürzen (später beschrieben), damit sie wie im Buch implementiert wird.
Als nächstes folgt two_layer_net.py
, das ein zweischichtiges neuronales Netzwerk implementiert.
two_layer_net.py
# coding: utf-8
from functions import sigmoid, softmax, numerical_gradient, \
cross_entropy_error, sigmoid_grad
import numpy as np
class TwoLayerNet:
def __init__(self, input_size, hidden_size, output_size,
weight_init_std=0.01):
"""Zweischichtiges neuronales Netzwerk
Args:
input_size (int):Anzahl der Neuronen in der Eingabeschicht
hidden_size (int):Anzahl der Neuronen mit versteckter Schicht
output_size (int):Anzahl der Neuronen in der Ausgabeschicht
weight_init_std (float, optional):Einstellparameter des Anfangswertes des Gewichts. Der Standardwert ist 0.01。
"""
#Gewichtsinitialisierung
self.params = {}
self.params['W1'] = weight_init_std * \
np.random.randn(input_size, hidden_size)
self.params['b1'] = np.zeros(hidden_size)
self.params['W2'] = weight_init_std * \
np.random.randn(hidden_size, output_size)
self.params['b2'] = np.zeros(output_size)
def predict(self, x):
"""Inferenz durch neuronales Netzwerk
Args:
x (numpy.ndarray):Eingabe in das neuronale Netzwerk
Returns:
numpy.ndarray:Neuronale Netzwerkausgabe
"""
#Parameterabruf
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['b1'], self.params['b2']
#Berechnung des neuronalen Netzes (vorwärts)
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
y = softmax(a2)
return y
def loss(self, x, t):
"""Berechnung des Verlustfunktionswerts
Args:
x (numpy.ndarray):Eingabe in das neuronale Netzwerk
t (numpy.ndarray):Richtiges Antwortetikett
Returns:
float:Wert der Verlustfunktion
"""
#Inferenz
y = self.predict(x)
#Berechnung des Kreuzentropiefehlers
loss = cross_entropy_error(y, t)
return loss
def accuracy(self, x, t):
"""Berechnung der Erkennungsgenauigkeit
Args:
x (numpy.ndarray):Eingabe in das neuronale Netzwerk
t (numpy.ndarray):Richtiges Antwortetikett
Returns:
float:Erkennungsgenauigkeit
"""
y = self.predict(x)
y = np.argmax(y, axis=1)
t = np.argmax(t, axis=1)
accuracy = np.sum(y == t) / x.shape[0]
return accuracy
def numerical_gradient(self, x, t):
"""Berechnung des Gradienten für Gewichtsparameter
Args:
x (numpy.ndarray):Eingabe in das neuronale Netzwerk
t (numpy.ndarray):Richtiges Antwortetikett
Returns:
dictionary:Ein Wörterbuch, in dem Farbverläufe gespeichert werden
"""
grads = {}
grads['W1'] = \
numerical_gradient(lambda: self.loss(x, t), self.params['W1'])
grads['b1'] = \
numerical_gradient(lambda: self.loss(x, t), self.params['b1'])
grads['W2'] = \
numerical_gradient(lambda: self.loss(x, t), self.params['W2'])
grads['b2'] = \
numerical_gradient(lambda: self.loss(x, t), self.params['b2'])
return grads
def gradient(self, x, t):
"""In Kapitel 5 erlernte Funktionen. Implementierung der Fehlerrückverbreitungsmethode
"""
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['b1'], self.params['b2']
grads = {}
batch_num = x.shape[0]
# forward
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
y = softmax(a2)
# backward
dy = (y - t) / batch_num
grads['W2'] = np.dot(z1.T, dy)
grads['b2'] = np.sum(dy, axis=0)
dz1 = np.dot(dy, W2.T)
da1 = sigmoid_grad(a1) * dz1
grads['W1'] = np.dot(x.T, da1)
grads['b1'] = np.sum(da1, axis=0)
return grads
Es ist fast das gleiche wie der Code im Buch. Der letzte "Gradient" wird in Kapitel 5 gelernt, aber es ist notwendig, die Verarbeitungszeit (später beschrieben) zu verkürzen, damit er wie im Buch implementiert wird.
Schließlich die Implementierung des Mini-Batch-Lernens.
mnist.py
# coding: utf-8
import numpy as np
import matplotlib.pylab as plt
import os
import sys
from two_layer_net import TwoLayerNet
sys.path.append(os.pardir) #Fügen Sie dem Pfad das übergeordnete Verzeichnis hinzu
from dataset.mnist import load_mnist
#Lesen Sie die MNIST-Trainingsdaten und Testdaten
(x_train, t_train), (x_test, t_test) = \
load_mnist(normalize=True, one_hot_label=True)
#Hyper-Parametereinstellungen
iters_num = 10000 #Anzahl der Updates
batch_size = 100 #Chargengröße
learning_rate = 0.1 #Lernrate
#Liste der Ergebnisse aufzeichnen
train_loss_list = [] #Wertänderungen der Verlustfunktion
train_acc_list = [] #Erkennungsgenauigkeit für Trainingsdaten
test_acc_list = [] #Erkennungsgenauigkeit für Testdaten
train_size = x_train.shape[0] #Größe der Trainingsdaten
iter_per_epoch = max(train_size / batch_size, 1) #Anzahl der Iterationen pro Epoche
#Zweischichtige Erzeugung neuronaler Arbeit
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
#Fang an zu lernen
for i in range(iters_num):
#Mini-Batch-Generierung
batch_mask = np.random.choice(train_size, batch_size, replace=False)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
#Gradientenberechnung
# grad = network.numerical_gradient(x_batch, t_batch)Verwenden Sie die Methode zur Fehlerrückübertragung, da sie langsam ist ...
grad = network.gradient(x_batch, t_batch)
#Aktualisierung der Gewichtsparameter
for key in ('W1', 'b1', 'W2', 'b2'):
network.params[key] -= learning_rate * grad[key]
#Berechnung des Verlustfunktionswerts
loss = network.loss(x_batch, t_batch)
train_loss_list.append(loss)
#Berechnung der Erkennungsgenauigkeit für jede Epoche
if i % iter_per_epoch == 0:
train_acc = network.accuracy(x_train, t_train)
test_acc = network.accuracy(x_test, t_test)
train_acc_list.append(train_acc)
test_acc_list.append(test_acc)
#Fortschrittsanzeige
print(f"[Anzahl der Updates]{i: >4} [Wert der Verlustfunktion]{loss:.4f} "
f"[Erkennungsgenauigkeit von Trainingsdaten]{train_acc:.4f} [Erkennungsgenauigkeit von Testdaten]{test_acc:.4f}")
#Zeichnen Sie den Übergang des Wertes der Verlustfunktion
x = np.arange(len(train_loss_list))
plt.plot(x, train_loss_list, label='loss')
plt.xlabel("iteration")
plt.ylabel("loss")
plt.xlim(left=0)
plt.ylim(bottom=0)
plt.show()
#Zeichnen Sie den Übergang der Erkennungsgenauigkeit von Trainingsdaten und Testdaten
x2 = np.arange(len(train_acc_list))
plt.plot(x2, train_acc_list, label='train acc')
plt.plot(x2, test_acc_list, label='test acc', linestyle='--')
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.xlim(left=0)
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()
Im Code des Buches wird [numpy.random.choice
](https://docs.scipy.org/doc/numpy-1.15.0/reference/generated/numpy.random.choice" für die Mini-Batch-Generierung verwendet Es gibt keine Angabe von replace = False
im Argument von .html), aber ich habe versucht, sie anzugeben, da es den Anschein hat, dass dasselbe Element mehr als einmal extrahiert werden kann.
Ursprünglich wird der Gradient durch numerische Differenzierung mit "TwoLayerNet.numerical_gradient" berechnet, aber die Verarbeitungsgeschwindigkeit ist langsam und in der jeweiligen Umgebung ~~ Es scheint, dass 10.000 Aktualisierungen nicht abgeschlossen werden, selbst wenn es einen Tag dauert ~~ Es kann nur ungefähr 600 Mal an einem halben Tag aktualisiert werden, und es scheint, dass es ungefähr 8 Tage dauern wird, um 10.000 Mal zu aktualisieren. Daher habe ich gemäß den Ratschlägen in diesem Buch "TwoLayerNet.gradient" verwendet, das die in Kapitel 5 erlernte Fehlerausbreitungsmethode implementiert.
Schließlich werden der Übergang des Wertes der Verlustfunktion und der Übergang der Erkennungsgenauigkeit von Trainingsdaten und Testdaten in einem Diagramm angezeigt.
Unten sind die Ausführungsergebnisse.
[Anzahl der Updates] 0 [Wert der Verlustfunktion]2.2882 [Erkennungsgenauigkeit von Trainingsdaten]0.1044 [Erkennungsgenauigkeit von Testdaten]0.1028
[Anzahl der Updates] 600 [Wert der Verlustfunktion]0.8353 [Erkennungsgenauigkeit von Trainingsdaten]0.7753 [Erkennungsgenauigkeit von Testdaten]0.7818
[Anzahl der Updates]1200 [Wert der Verlustfunktion]0.4573 [Erkennungsgenauigkeit von Trainingsdaten]0.8744 [Erkennungsgenauigkeit von Testdaten]0.8778
[Anzahl der Updates]1800 [Wert der Verlustfunktion]0.4273 [Erkennungsgenauigkeit von Trainingsdaten]0.8972 [Erkennungsgenauigkeit von Testdaten]0.9010
[Anzahl der Updates]2400 [Wert der Verlustfunktion]0.3654 [Erkennungsgenauigkeit von Trainingsdaten]0.9076 [Erkennungsgenauigkeit von Testdaten]0.9098
[Anzahl der Updates]3000 [Wert der Verlustfunktion]0.2816 [Erkennungsgenauigkeit von Trainingsdaten]0.9142 [Erkennungsgenauigkeit von Testdaten]0.9146
[Anzahl der Updates]3600 [Wert der Verlustfunktion]0.3238 [Erkennungsgenauigkeit von Trainingsdaten]0.9195 [Erkennungsgenauigkeit von Testdaten]0.9218
[Anzahl der Updates]4200 [Wert der Verlustfunktion]0.2017 [Erkennungsgenauigkeit von Trainingsdaten]0.9231 [Erkennungsgenauigkeit von Testdaten]0.9253
[Anzahl der Updates]4800 [Wert der Verlustfunktion]0.1910 [Erkennungsgenauigkeit von Trainingsdaten]0.9266 [Erkennungsgenauigkeit von Testdaten]0.9289
[Anzahl der Updates]5400 [Wert der Verlustfunktion]0.1528 [Erkennungsgenauigkeit von Trainingsdaten]0.9306 [Erkennungsgenauigkeit von Testdaten]0.9320
[Anzahl der Updates]6000 [Wert der Verlustfunktion]0.1827 [Erkennungsgenauigkeit von Trainingsdaten]0.9338 [Erkennungsgenauigkeit von Testdaten]0.9347
[Anzahl der Updates]6600 [Wert der Verlustfunktion]0.1208 [Erkennungsgenauigkeit von Trainingsdaten]0.9362 [Erkennungsgenauigkeit von Testdaten]0.9375
[Anzahl der Updates]7200 [Wert der Verlustfunktion]0.1665 [Erkennungsgenauigkeit von Trainingsdaten]0.9391 [Erkennungsgenauigkeit von Testdaten]0.9377
[Anzahl der Updates]7800 [Wert der Verlustfunktion]0.1787 [Erkennungsgenauigkeit von Trainingsdaten]0.9409 [Erkennungsgenauigkeit von Testdaten]0.9413
[Anzahl der Updates]8400 [Wert der Verlustfunktion]0.1564 [Erkennungsgenauigkeit von Trainingsdaten]0.9431 [Erkennungsgenauigkeit von Testdaten]0.9429
[Anzahl der Updates]9000 [Wert der Verlustfunktion]0.2361 [Erkennungsgenauigkeit von Trainingsdaten]0.9449 [Erkennungsgenauigkeit von Testdaten]0.9437
[Anzahl der Updates]9600 [Wert der Verlustfunktion]0.2183 [Erkennungsgenauigkeit von Trainingsdaten]0.9456 [Erkennungsgenauigkeit von Testdaten]0.9448
Bei Betrachtung der Ergebnisse lag die Erkennungsgenauigkeit bereits bei 94,5%, was die Erkennungsgenauigkeit der in Kapitel 3 erstellten gelernten Parameter übertraf.
Es mag gut sein, Kapitel 4 als Buch zu lesen, aber es war ziemlich schwierig, bei der Implementierung fortzufahren. (Ich wollte eine Erklärung des Teils, in dem die Softmax-Funktion und die numerische Differenzierungsfunktion einem mehrdimensionalen Array entsprechen ...)
Das ist alles für dieses Kapitel. Wenn Sie Fehler haben, wäre ich Ihnen dankbar, wenn Sie darauf hinweisen könnten. (Zu anderen Kapiteln dieses Memos: Kapitel 1 / Kapitel 2 / Kapitel 3 / Kapitel 4 / Kapitel 5 / [Kapitel 6](https: / /qiita.com/segavvy/items/ca4ac4c9ee1a126bff41) / Kapitel 7 / Kapitel 8 / Zusammenfassung)
Recommended Posts