Der vorherige Artikel war hier
DNN (Deep Neural Network) ist zum letzten Mal abgeschlossen.
(Ich habe vor, in einem anderen Artikel mit DNN zu spielen, einschließlich der Verwendung des Ebenenmanagers.)
Hier erstellen wir ein CNN (Convolutional Neural Network) zur Bilderkennung.
Die hier verwendeten Funktionen im2col
und col2im
sind hier und hier. ) Ist vorgestellt.
Der nächste Artikel ist hier
Ein Prozess namens ** Faltung ** bietet einen großen Vorteil für die Bilderkennung. Als Einführung gilt für Daten wie Bilder, bei denen die Positionsbeziehung wichtig zu sein scheint, das einfache Glätten in ein neuronales Netzwerk in einer Dimension und das Fließen wie das Wegwerfen der wichtigen Informationen der Positionsbeziehung, was eine Verschwendung ist. Es ist wie es ist. Die Rolle der Faltungsschicht besteht darin, Daten durch das neuronale Netzwerk zu fließen, während die Eingabedimension beibehalten wird, dh wichtige Informationen wie Positionsbeziehungen beibehalten werden. Für die Faltungsschicht entspricht dieser Filter dem Gewicht in einer normalen Schicht. Danach können Sie den Code schreiben, der gemäß diesem GIF funktioniert. Wenn Sie ihn jedoch so implementieren, wie er ist, handelt es sich um einen schweren Code, der nicht praktikabel ist. Denn wenn Sie diesen GIF-Teil vereinfachen und implementieren
Image = I_h×I_Array von w
Filter = F_h×F_Array von w
Output = O_h×O_Array von w
for h in range(O_h):
h_lim = h + F_h
for w in range(O_w):
w_lim = w + F_w
Output[h, w] = Image[h:h_lim, w:w_lim] * Filter
Es wird so, als würde man in einer Doppelschleife auf das numpy-Array zugreifen, das Elementprodukt auf den entsprechenden Teil der Eingabe anwenden und das Ergebnis in der Ausgabe speichern.
Darüber hinaus ist diese Schleife, hier eine Doppelschleife, eine Vierfachschleife, da die tatsächliche Eingabe vierdimensional ist. Es ist leicht vorstellbar, dass die Anzahl der Schleifen schnell zunimmt.
Da numpy die Spezifikation hat, dass es langsam ist, wenn Sie mit der Anweisung for
darauf zugreifen, möchten Sie den Zugriff in einer Schleife so weit wie möglich vermeiden. Hier kommt die im2col
Funktion ins Spiel.
Das vorherige GIF ist
a = 1W + 2X + 5Y + 6Z \\
b = 2W + 3X + 6Y + 7Z \\
c = 3W + 4X + 7Y + 8Z \\
d = 5W + 6X + 9Y + 10Z \\
e = 6W + 7X + 10Y + 11Z \\
f = 7W + 8X + 11Y + 12Z \\
g = 9W + 10X + 13Y + 14Z \\
h = 10W + 11X + 14Y + 15Z \\
i = 11W + 12X + 15Y + 16Z
Es ist so, aber wenn Sie dies als Matrixprodukt ausdrücken
\left(
\begin{array}{c}
a \\
b \\
c \\
d \\
e \\
f \\
g \\
h \\
i
\end{array}
\right)^{\top}
=
\left(
\begin{array}{cccc}
W & X & Y & Z
\end{array}
\right)
\left(
\begin{array}{ccccccccc}
1 & 2 & 3 & 5 & 6 & 7 & 9 & 10 & 11 \\
2 & 3 & 4 & 6 & 7 & 8 & 10 & 11 & 12 \\
5 & 6 & 7 & 9 & 10 & 11 & 13 & 14 & 15 \\
6 & 7 & 8 & 10 & 11 & 12 & 14 & 15 & 16
\end{array}
\right)
Es wird sein. Die im2col
Funktion ist eine Funktion zum Konvertieren eines Eingabebildes oder Filters in eine Matrix wie diese. Weitere Informationen finden Sie unter hier.
Übrigens kann mit dieser im2col
-Funktion das obige Problem erheblich gelöst werden. Wenn jedoch die Funktion "im2col" verwendet wird, ist die Form der ursprünglichen Eingabe natürlich unterschiedlich, sodass das Lernen mit der Fehlerrückvermehrungsmethode nicht so fortgesetzt werden kann, wie sie ist. Daher ist es notwendig, die "col2im" -Funktion zu beißen, die zum Zeitpunkt der Rückausbreitung die entgegengesetzte Operation ausführt. Weitere Informationen finden Sie unter hier.
Bisher habe ich den Umriss der Faltschicht kurz erklärt, daher zeige ich Ihnen die Konstruktionszeichnung.
Beginnen wir mit der Vorwärtsausbreitung. Der relevante Teil ist der Farbteil in der folgenden Abbildung. Operativ
Die Grundoperation ist dieselbe wie die Vorwärtsausbreitung eines normalen neuronalen Netzwerks. Der einzige Unterschied besteht darin, dass Sie die Funktion "im2col" davor stellen. Lass uns genauer hinschauen. Erstens ist die Faltungsoperation wie in der folgenden Abbildung gezeigt. Vorspannung wird weggelassen. Die Eingabe ist ein Tensor mit der Stapelgröße $ B $, der Anzahl der Kanäle $ C $ und der Bildgröße $ (I_h, I_w) $. Es gibt $ M $ Filter für jeden Kanal, der die gleiche Anzahl von Kanälen wie der Eingang hat und ein Tensor mit einer Filtergröße von $ (F_h, F_w) $ ist. Der jedem Eingangskanal entsprechende Kanalfilter wird nach allen Chargendaten gefiltert, was zu einem Tensor mit der Form $ (B, M, O_h, O_w) $ führt. Mal sehen, wie man diesen Prozess konkret macht. Verarbeiten Sie die Eingänge und Filter wie in der folgenden Abbildung gezeigt. Infolgedessen kann der 4-dimensionale Tensor in 2 Dimensionen fallen gelassen werden, und Matrixprodukte können durchgeführt werden. Fügen Sie dieser Ausgabe eine Vorspannung hinzu (Form ist eine zweidimensionale Matrix von $ (M, 1) $). Verwenden Sie zu diesem Zeitpunkt die Broadcast-Funktion von "numpy", um allen Spalten den gleichen Wert hinzuzufügen. Danach wird dieser Ausgang transformiert und die Dimensionen werden ausgetauscht, um den Ausgangstensor zu erhalten. Wirf diesen Ausgangstensor in die Aktivierungsfunktion, um die Vorwärtsausbreitung der Faltungsschicht zu vervollständigen.
Als nächstes kommt die Rückausbreitung. Der zugehörige Teil ist der Farbteil in der folgenden Abbildung. Als Operation
col2im
verwendet.Es ist wie es ist. Lass uns genauer hinschauen. Der propagierte Gradient ist ein Tensor von $ (B, M, O_h, O_w) $. Transformieren Sie zunächst diesen Gradienten in umgekehrter Reihenfolge der Vorwärtsausbreitung. Die Steigung zum Filter wird als Produkt aus Steigung und Eingangsmatrix berechnet. Da das resultierende Ergebnis eine zweidimensionale Matrix ist, kann sie in einen vierdimensionalen Tensor mit der gleichen Form wie der Filter umgewandelt werden. Der Schlüssel zum zu verzerrenden Gradienten besteht darin, allen Spalten während der Vorwärtsausbreitung den gleichen Wert hinzuzufügen. Das Hinzufügen des gleichen Werts zu mehreren Elementen zeigt, dass dies einem Netzwerk entspricht, das wie in der folgenden Abbildung dargestellt geformt ist. (Die Nummer ist angemessen) Daher ist $ axis = 1 $, dh die Rückausbreitung wird von jeder Spaltenrichtung in Richtung einer Vorspannung durchgeführt, und die Summe davon ist der Gradient zur Vorspannung. Der Gradient zur Eingabe wird durch das Matrixprodukt des Filters und den Gradienten berechnet. Wie Sie anhand des Tensors des Berechnungsergebnisses sehen können, entspricht dies dem Ergebnis des Werfens des Eingangstensors auf die Funktion "im2col", wenn sich die Form vorwärts ausbreitet. Wenn Sie dies in die Funktion "col2im" werfen, die das Gegenteil bewirkt, entsteht ein Gradiententensor für die Eingabe. Dies vervollständigt die Rückausbreitung der Faltungsschicht.
Nun, Sie müssen den Filter nicht jedes Mal umwandeln. Sie müssen es am Anfang nur einmal tun. Der Grund ist, dass "der Filter sich jedes Mal gleich verformt, so dass es nicht erforderlich ist, ihn zu wiederholen." Der Filter bleibt wie nach der ersten Transformation, was bedeutet, dass der durch Backpropagation berechnete Gradient zum Filter ebenfalls nicht transformiert werden muss. Als solches hat das Lernen der Faltungsschicht die gleiche Form wie die normale Schicht.
Also werde ich es umsetzen. Ein wenig Einfallsreichtum ist jedoch erforderlich, um den "BaseLayer" zu erben.
conv.py
import numpy as np
class ConvLayer(BaseLayer):
def __init__(self, *, I_shape=None, F_shape=None,
stride=1, pad="same",
name="", wb_width=5e-2,
act="ReLU", opt="Adam",
act_dic={}, opt_dic={}, **kwds):
self.name = name
if I_shape is None:
raise KeyError("Input shape is None.")
if F_shape is None:
raise KeyError("Filter shape is None.")
if len(I_shape) == 2:
C, I_h, I_w = 1, *I_shape
else:
C, I_h, I_w = I_shape
self.I_shape = (C, I_h, I_w)
if len(F_shape) == 2:
M, F_h, F_w = 1, *F_shape
else:
M, F_h, F_w = F_shape
self.F_shape = (M, C, F_h, F_w)
if isinstance(stride, tuple):
stride_ud, stride_lr = stride
else:
stride_ud = stride
stride_lr = stride
self.stride = (stride_ud, stride_lr)
if isinstance(pad, tuple):
pad_ud, pad_lr = pad
elif isinstance(pad, int):
pad_ud = pad
pad_lr = pad
elif pad == "same":
pad_ud = 0.5*((I_h - 1)*stride_ud - I_h + F_h)
pad_lr = 0.5*((I_w - 1)*stride_lr - I_w + F_w)
self.pad = (pad_ud, pad_lr)
O_h = get_O_shape(I_h, F_h, stride_ud, pad_ud)
O_w = get_O_shape(I_w, F_w, stride_lr, pad_lr)
self.O_shape = (M, O_h, O_w)
self.n = np.prod(self.O_shape)
#Stellen Sie Filter und Vorspannung ein
self.w = wb_width*np.random.randn(*self.F_shape).reshape(M, -1).T
self.b = wb_width*np.random.randn(M)
#Aktivierungsfunktion(Klasse)Erhalten
self.act = get_act(act, **act_dic)
#Optimierer(Klasse)Erhalten
self.opt = get_opt(opt, **opt_dic)
def forward(self, x):
B = x.shape[0]
M, O_h, O_w = self.O_shape
x, _, self.pad_state = im2col(x, self.F_shape,
stride=self.stride,
pad=self.pad)
super().forward(x.T)
return self.y.reshape(B, O_h, O_w, M).transpose(0, 3, 1, 2)
def backward(self, grad):
B = grad.shape[0]
I_shape = B, *self.I_shape
M, O_h, O_w = self.O_shape
grad = grad.transpose(0, 2, 3, 1).reshape(-1, M)
super().backward(grad)
self.grad_x = col2im(self.grad_x.T, I_shape, self.O_shape,
stride=self.stride, pad=self.pad_state)
return self.grad_x
Ich werde erklären, welchen Bereich Sie planen. Wenn Sie es wie oben beschrieben ohne Einfallsreichtum implementieren, wird es wie folgt aussehen.
conv.py
import numpy as np
class ConvLayer(BaseLayer):
def __init__(self, *, I_shape=None, F_shape=None,
stride=1, pad="same",
name="", wb_width=5e-2,
act="ReLU", opt="Adam",
act_dic={}, opt_dic={}, **kwds):
self.name = name
if I_shape is None:
raise KeyError("Input shape is None.")
if F_shape is None:
raise KeyError("Filter shape is None.")
if len(I_shape) == 2:
C, I_h, I_w = 1, *I_shape
else:
C, I_h, I_w = I_shape
self.I_shape = (C, I_h, I_w)
if len(F_shape) == 2:
M, F_h, F_w = 1, *F_shape
else:
M, F_h, F_w = F_shape
self.F_shape = (M, C, F_h, F_w)
_, O_shape, self.pad_state = im2col(np.zeros((1, *self.I_shape)), self.F_shape,
stride=stride, pad=pad)
self.O_shape = (M, *O_shape)
self.stride = stride
self.n = np.prod(self.O_shape)
#Stellen Sie Filter und Vorspannung ein
self.w = wb_width*np.random.randn(*self.F_shape).reshape(M, -1)
self.b = wb_width*np.random.randn(M, 1)
#Aktivierungsfunktion(Klasse)Erhalten
self.act = get_act(act, **act_dic)
#Optimierer(Klasse)Erhalten
self.opt = get_opt(opt, **opt_dic)
def forward(self, x):
B = x.shape[0]
M, O_h, O_w = self.O_shape
self.x, _, self.pad_state = im2col(x, self.F_shape,
stride=self.stride,
pad=self.pad)
self.u = [email protected] + self.b
self.u = self.u.reshape(M, B, O_h, O_w).transpose(1, 0, 2, 3)
self.y = self.act.forward(self.u)
return self.y
def backward(self, grad):
B = grad.shape[0]
I_shape = B, *self.I_shape
_, O_h, O_w = self.O_shape
dact = grad*self.act.backward(self.u, self.y)
dact = dact.transpose(1, 0, 2, 3).reshape(M, -1)
self.grad_w = [email protected]
self.grad_b = np.sum(dact, axis=1).reshape(M, 1)
self.grad_x = self.w.T@dact
self.grad_x = col2im(self.grad_x, I_shape, self.O_shape,
stride=self.stride, pad=self.pad_state)
return self.grad_x
Schauen wir uns die Unterschiede zu "BaseLayer" genauer an und lassen den Code weg.
Achtungsteil | BaseLayer |
gestalten | ConvLayer |
gestalten | |
---|---|---|---|---|---|
w | randn(prev, n) | randn(*F_shape).reshape(M, -1) | |||
b | randn(n) | randn(M, 1) | |||
x | - | im2col(x) | |||
u | x@w + b | w@x + b | |||
u | - | - | u.reshape(M, B, O_h, O_w).transpose(1, 0, 2, 3) | ||
y | act.forward(u) | act.forward(u) | |||
grad | - | - | |||
dact | grad*act.backward(u, y) | grad*act.backward(u, y) | |||
dact | - | - | dact.transpose(1, 0, 2, 3).reshape(M, -1) | ||
grad_w | x.T@dact | [email protected] | |||
grad_b | sum(dact, axis=0) | sum(dact, axis=1).reshape(M, 1) | |||
grad_x | [email protected] | w.T@dact | |||
grad_x | - | - | col2im(grad_x) |
Lassen Sie uns zunächst die Vorwärtsausbreitung ausrichten. Der größte Unterschied in der Vorwärtsausbreitung ist die Berechnung von "u".
\boldsymbol{x}@\boldsymbol{w} + \boldsymbol{b} \quad \Leftrightarrow \quad \boldsymbol{w}@\boldsymbol{x} + \boldsymbol{b}
Die Reihenfolge des Matrixprodukts kann durch Setzen von $ \ boldsymbol {w} @ \ boldsymbol {x} = \ boldsymbol {x} ^ {\ top} @ \ boldsymbol {w} ^ {\ top} $ umgekehrt werden. Zum,
\begin{align}
\boldsymbol{x} &\leftarrow \textrm{im2col}(\boldsymbol{x})^{\top} = (BO_hO_w, CF_hF_w) \\
\boldsymbol{w} &\leftarrow \boldsymbol{w}^{\top} = (CF_hF_w, M) \\
\boldsymbol{b} & \leftarrow (M, )
\end{align}
Es ist möglich, sich an der Vorwärtsausbreitungsformel auszurichten, indem Sie als einstellen. In Bezug auf die Vorspannung ist es auch möglich, ein eindimensionales Array anstelle einer zweidimensionalen Matrix mit $ (M, 1) $ zu erstellen, um die Rundfunkfunktion von "numpy" zu aktivieren. Wenn Sie die Vorwärtsausbreitung so ändern
\boldsymbol{x}@\boldsymbol{w} + \boldsymbol{b} = (BO_hO_w, CF_hF_w)@(CF_hF_w, M) + (M) = (BO_hO_w, M)
Nach dem Berechnen mit "vorwärts" von "BaseLayer" ist die Ausbreitung zur nächsten Schicht "self.y.reshape" (B, O_h, O_w, M) .transpose (0, 3, 1, 2) Es kann durch Setzen von) `in $ (B, M, O_h, O_w) $ umgewandelt werden. Wenn Sie sich den Code der Person ansehen, die ihn entwickelt hat, transformiert die Anweisung "return" ihn wie oben beschrieben und fließt ihn, wobei jedoch die Formen von "u" und "y" als $ (BO_hO_w, M) $ verbleiben. Das ist in Ordnung so wie es ist.
Als nächstes kommt die Rückausbreitung. Der Gradient "grad" ist $ (B, M, O_h, O_w) $, und das Elementprodukt von "grad * act.backward (u, y)" kann nicht so berechnet werden, wie es ist.
\boldsymbol{grad} \otimes \textrm{act.backward}(\boldsymbol{u}, \boldsymbol{y}) = (B, M, O_h, O_w) \otimes (BO_hO_w, M)
Also transformieren wir grad
und richten es aus.
Sie können dies mit grad.transpose (0, 2, 3, 1) .reshape (-1, M)
tun.
Danach, wenn Sie es auf den "Rückwärts" des "BaseLayer" werfen
\begin{array}[cccc]
d\boldsymbol{dact} &= \boldsymbol{grad} \otimes \textrm{act.backward}(\boldsymbol{u}, \boldsymbol{y}) &= (BO_hO_w, M) & \\
\boldsymbol{grad_w} &= \boldsymbol{x}^{\top}@\boldsymbol{dact} &= (CF_hF_w, BO_hO_w)@(BO_hO_w, M) &= (CF_hF_w, M)\\
\boldsymbol{grad_b} &= \textrm{sum}(\boldsymbol{dact}, \textrm{axis}=0) &= (M, ) & \\
\boldsymbol{grad_x} &= \boldsymbol{dact}@\boldsymbol{w}^{\top} &= (BO_hO_w, M)@(M, CF_hF_w) &= (BO_hO_w, CF_hF_w)
\end{array}
Weil es wird
\boldsymbol{grad_x} \leftarrow \textrm{col2im}(\boldsymbol{grad_x}^{\top}) = (B, C, I_h, I_w)
Wenn ja, ist es OK. Es ist nicht erforderlich, die "Update" -Funktion von "BaseLayer" wie oben beschrieben zu ändern. Damit ist die Faltschicht fertig.
Als nächstes kommt die Pooling-Schicht. Erstens ist die Pooling-Ebene eine Ebene, die die Datengröße reduziert, indem nur die Informationen extrahiert werden, die aus dem Eingabebild wichtig zu sein scheinen. Die wichtige Information in diesem Fall ist normalerweise das Maximum oder der Durchschnitt. Wenn dies implementiert wird, ist es außerdem schneller und effizienter, wenn die Funktion "im2col" und die Funktion "col2im" sowie die Faltungsschicht verwendet werden. Die Konstruktionszeichnung der Poolebene sieht wie folgt aus.
Schauen wir uns die Vorwärtsausbreitung an. Es ist der Farbteil, der relevant ist. Als Operation
Es ist wie es ist. Es gibt einige Dinge, die für die Rückausbreitung aufbewahrt werden müssen. Lass uns genauer hinschauen. Die Zieloperation ist wie in der folgenden Abbildung dargestellt. Werfen Sie zuerst den Eingangstensor in die Funktion "im2col", um ihn in eine zweidimensionale Matrix umzuwandeln. Weiterhin wird diese zweidimensionale Matrix transformiert. Fügen Sie nach der Transformation in eine solche vertikal lange Matrix die Summe in Spaltenrichtung hinzu und transformieren und vertauschen Sie schließlich die Dimensionen, um die Ausgabe abzuschließen. Außerdem müssen Sie den Index des Maximalwerts abrufen, bevor Sie die Spaltensumme verwenden.
Als nächstes kommt die Rückausbreitung. Es ist der Farbteil der Nummer, der verwandt ist. Als Operation
im2col
werfencol2im
Es ist wie es ist. Es ist schwer, die Operation mit nur wenigen Worten zu verstehen ... Es sieht wie folgt aus.
Wie Sie der Konstruktionszeichnung entnehmen können, sind in der Poolebene keine Parameter zu lernen. Also lerne ich nicht einmal.
Die Erklärung der Poolschicht war viel einfacher als die der Faltschicht. Die Implementierung ist auch nicht so kompliziert.
pool.py
import numpy as np
class PoolingLayer(BaseLayer):
def __init__(self, *, I_shape=None,
pool=1, pad=0,
name="", **kwds):
self.name = name
if I_shape is None:
raise KeyError("Input shape is None.")
if len(I_shape) == 2:
C, I_h, I_w = 1, *I_shape
else:
C, I_h, I_w = I_shape
self.I_shape = (C, I_h, I_w)
_, O_shape, self.pad_state = im2col(np.zeros((1, *self.I_shape)), (pool, pool),
stride=pool, pad=pad)
self.O_shape = (C, *O_shape)
self.n = np.prod(self.O_shape)
self.pool = pool
self.F_shape = (pool, pool)
def forward(self, x):
B = x.shape[0]
C, O_h, O_w = self.O_shape
self.x, _, self.pad_state = im2col(x, self.F_shape,
stride=self.pool,
pad=self.pad_state)
self.x = self.x.T.reshape(B*O_h*O_w*C, -1)
self.max_index = np.argmax(self.x, axis=1)
self.y = np.max(self.x, axis=1).reshape(B, O_h, O_w, C).transpose(0, 3, 1, 2)
return self.y
def backward(self, grad):
B = grad.shape[0]
I_shape = B, *self.I_shape
C, O_h, O_w = self.O_shape
grad = grad.transpose(0, 2, 3, 1).reshape(-1, 1)
self.grad_x = np.zeros((grad.size, self.pool*self.pool))
self.grad_x[:, self.max_index] = grad
self.grad_x = self.grad_x.reshape(B*O_h*O_w, C*self.pool*self.pool).T
self.grad_x = col2im(self.grad_x, I_shape, self.O_shape,
stride=self.pool, pad=self.pad_state)
return self.grad_x
def update(self, **kwds):
pass
Als ich den experimentellen Code von CNN zusammenstellte, funktionierte er nicht gut und ich untersuchte ihn die ganze Zeit ... Aus der Schlussfolgerung heraus gab es kein Problem mit der Faltungsschicht und der Pooling-Schicht, und die Aktivierungsfunktion war das Problem. Die Implementierung von Liste der Aktivierungsfunktionen wurde ebenfalls geändert. Ich werde den experimentellen Code im nächsten Artikel veröffentlichen. Ich habe auch die "LayerManager" -Klasse geändert und so weiter.