Ein Sattelpunkt ist ein Punkt, an dem die Steigung 0 ist und das Minimum aus einer bestimmten Richtung und das Maximum aus einer bestimmten Richtung genommen wird. Es ist ein wichtiges Thema in verschiedenen Bereichen, wie es GAN beim maschinellen Lernen und bei Übergangszuständen bei der Suche nach chemischen Reaktionswegen darstellt. GAN besteht aus einem Generator, der Daten generiert, die genau wie die realen Daten aussehen, und einem Diskriminator, der zwischen den realen und den generierten Daten unterscheidet. Lernen Sie, mit dem Generator das Maximum und mit dem Diskriminator das Minimum zu nehmen. Bei der chemischen Reaktion passieren verschiedene stabile Rohstoffe die Sattelpunkte auf der potenziell energiegekrümmten Oberfläche und wandeln sich in stabilere Verbindungen um. In diesem Artikel werden wir mit der Gradientenmethode nach Sattelpunkten suchen. Diese Methode ist für einfache Modelle effektiv, verhält sich jedoch für komplexe Modelle (z. B. Quantenchemie usw.) etwas instabil. Daher werde ich sie hinzufügen, sobald sie verbessert werden kann. Wenn Sie an diesem Artikel interessiert sind, LGTM! Bitte.
Beschreiben Sie den Sattelpunkt wie folgt. Die Zielfunktion ist $ y = f (\ boldsymbol {x}) $, $ \ boldsymbol {x} = (x_1, x_2, \ cdots, x_n) ^ \ rm {T} $, und der dem Minimalwert entsprechende Einheitsvektor ist $ \ boldsymbol {b} = (b_1, b_2, \ cdots, b_n) ^ \ rm {T} $, der dem Maximalwert entsprechende Einheitsvektor ist $ \ boldsymbol {c} = (c_1, c_2, \ cdots, c_n) ^ Definiert als \ rm {T} $. Zu diesem Zeitpunkt wird der Gradient von $ f $ zu $ 0 $, und $ y = f (\ boldsymbol {x} + \ boldsymbol {b} t) $ nimmt den Mindestwert bei $ t = 0 $ in Bezug auf $ t $ und $ y an Der Sattelpunkt ist, dass = f (\ boldsymbol {x} + \ boldsymbol {c} t) $ den Maximalwert bei $ t = 0 $ in Bezug auf $ t $ annimmt. Die Formel lautet wie folgt.
Am Sattelpunkt $ \ boldsymbol {x} = \ boldsymbol {x} _0 $
\left.\frac{\partial f(\boldsymbol{x})}{\partial \boldsymbol{x}}\right |_{\boldsymbol{x}=\boldsymbol{x}_0}= 0\\
\left.\frac{\partial^2 f(\boldsymbol{x}+\boldsymbol{b}t)}{\partial t^2}\right |_{\boldsymbol{x}=\boldsymbol{x}_0, t=0} > 0\\
\left.\frac{\partial^2 f(\boldsymbol{x}+\boldsymbol{c}t)}{\partial t^2}\right |_{\boldsymbol{x}=\boldsymbol{x}_0, t=0} < 0
Ist festgelegt.
Betrachten Sie als Beispiel $ y = x_1 ^ 2-x_2 ^ 2 $. Bei $ x_1 = 0 $, $ x_2 = 0 $ wird der Gradient zu $ 0 $ und bei $ \ boldsymbol {b} = (1, 0) ^ \ rm {T} $ ist $ y = (0 + 1 t) ^ 2- (0 + 0 t) ^ 2 = t ^ 2 $ nimmt einen Mindestwert bei $ t = 0 $ und $ y = bei $ \ boldsymbol {c} = (0, 1) ^ \ rm {T} $ an (0 + 0 t) ^ 2- (0 + 1t) ^ 2 = -t ^ 2 $ nimmt den Maximalwert bei $ t = 0 $ an. Daher kann dieser Punkt als Sattelpunkt geschätzt werden.
Diese Methode besteht aus zwei Schritten: Initialisierung und Exploration. Bei der Initialisierung werden die entsprechenden Anfangswerte für $ \ boldsymbol {b} $ und $ \ boldsymbol {c} $ gefunden. Bei der Suche werden die Sattelpunkte nach dem Gradienten durchsucht.
Wie oben erwähnt, entspricht $ \ boldsymbol {b} $ dem Maximalwert und $ \ boldsymbol {c} $ dem Minimalwert. Bestimmen Sie zunächst $ \ boldsymbol {b} $, das die Differenzierung zweiter Ordnung von $ y = f (\ boldsymbol {x} + \ boldsymbol {b} t) $ in Bezug auf $ t $ maximiert. Der Einfachheit halber werden wir die Wiederholungsmethode unter Verwendung der Re-Deep-Methode verwenden. Die Formel lautet wie folgt.
\mathrm{grad}\ \boldsymbol{b}_1 \leftarrow \frac{1}{\delta} \left( \nabla f \left(\boldsymbol{x}+\delta \boldsymbol{b} \right) - \nabla f \left(\boldsymbol{x}-\delta \boldsymbol{b} \right) \right)\\
\mathrm{grad}\ \boldsymbol{b}_2 \leftarrow \mathrm{grad}\ \boldsymbol{b}_1 - \left( \boldsymbol{b} \cdot \mathrm{grad}\ \boldsymbol{b}_1 \right)\boldsymbol{b}\\
\boldsymbol{b} \leftarrow \boldsymbol{b} + \epsilon_1 \ \mathrm{grad}\ \boldsymbol{b}_2\\
\boldsymbol{b} \leftarrow \frac{\boldsymbol{b}}{\mathrm{norm} \left(\boldsymbol{b}\right)}\\
Der Minutenbetrag ist $ \ delta $ und die Lernrate ist $ \ epsilon_1 $. Die erste Gleichung ist die Differenzierung des euklidischen Raums der $ n $ -Dimension in Bezug auf die Basis. Als Gerät wurde die Formel so transformiert, dass der Gradient verwendet werden konnte. Wenn dies so wie es ist als Aktualisierungsbetrag verwendet wird, ist es für den Einheitsvektor $ \ boldsymbol {b} $ nicht geeignet, daher ist es notwendig, ihn in den Aktualisierungsbetrag von $ \ boldsymbol {b} $ entlang der Einheitskugel in der zweiten Gleichung umzuwandeln. es gibt. Die dritte Formel wird auf den Maximalwert aktualisiert, und die vierte Formel wird standardisiert, um sie zu einem Einheitsvektor zu machen. In ähnlicher Weise bestimmen Sie $ \ boldsymbol {c} $ so, dass das Differential zweiter Ordnung von $ y = f (\ boldsymbol {x} + \ boldsymbol {c} t) $ in Bezug auf $ t $ maximiert wird. Die Formel lautet wie folgt.
\mathrm{grad}\ \boldsymbol{c}_1 \leftarrow \frac{1}{\delta} \left( \nabla f \left(\boldsymbol{x}+\delta \boldsymbol{c} \right) - \nabla f \left(\boldsymbol{x}-\delta \boldsymbol{c} \right) \right)\\
\mathrm{grad}\ \boldsymbol{c}_2 \leftarrow \mathrm{grad}\ \boldsymbol{c}_1 - \left( \boldsymbol{c} \cdot \mathrm{grad}\ \boldsymbol{c}_1 \right)\boldsymbol{c}\\
\boldsymbol{c} \leftarrow \boldsymbol{c} - \epsilon_1 \ \mathrm{grad}\ \boldsymbol{c}_2\\
\boldsymbol{c} \leftarrow \frac{\boldsymbol{c}}{\mathrm{norm} \left(\boldsymbol{c}\right)}
Der Unterschied zu $ \ boldsymbol {b} $ besteht darin, dass es in Richtung des Minimalwerts in der dritten Gleichung aktualisiert wird. Sie können $ \ mathrm {grad} \ \ boldsymbol {b} \ _2 $ und $ \ mathrm {grad} \ \ boldsymbol {c} \ _2 $ als Konvergenzurteile verwenden.
Die Suche verwendet $ \ mathrm {grad} \ \ boldsymbol {b} \ _2 $ und $ \ mathrm {grad} \ \ boldsymbol {c} \ _2 $. Dies liegt daran, dass es einen Minimalwert in Richtung $ - \ mathrm {grad} \ \ boldsymbol {b} \ _2 $ und einen Maximalwert in Richtung $ \ mathrm {grad} \ \ boldsymbol {c} \ _2 $ gibt. Sie können den Sattelpunkt erreichen, indem Sie entsprechend aktualisieren. Die Formel lautet wie folgt.
\boldsymbol{x} \leftarrow \boldsymbol{x} + \epsilon_2 \ \left( -\mathrm{grad}\ \boldsymbol{b}_2 +\mathrm{grad}\ \boldsymbol{c}_2 \right)
Außerdem haben sich $ \ mathrm {grad} \ \ boldsymbol {b} _2 $ und $ \ mathrm {grad} \ \ boldsymbol {c} _2 $ gemäß der Aktualisierung von $ \ boldsymbol {x} $ geändert, also Initialisierung Sie müssen auch $ \ boldsymbol {b} $ und $ \ boldsymbol {c} $ gleichzeitig ausführen.
Die Sattelpunkte verschiedener Funktionen wurden gemäß dem obigen Algorithmus abgeleitet. Die Update-Methode verwendete die Re-Dive-Methode und wurde in Python ausgeführt. Die rote Linie ist der Minimalwertvektor und die grüne Linie ist der Maximalwertvektor.
・ Funktion $ y = x_1 ^ 2-x_2 ^ 2 $
Anfangswert $ x_1 = -2 $, $ x_2 = -1 $
Kleiner Betrag $ \ delta = 0,001 $
Lernrate $ \ epsilon_1 = 0,1 $, $ \ epsilon_1 = 0,1 $
Ergebnis
Sattelpunkt $ x_1 = -0.0003 $, $ x_2 = -0.001 $
Initialisieren
Sattelpunktsuche
・ Funktion $ y = x_1 ^ 2 + x_2 ^ 3-x_2 $
Anfangswert $ x_1 = -2 $, $ x_2 = -0,5 $
Kleiner Betrag $ \ delta = 0,05 $
Lernrate $ \ epsilon_1 = 0,1 $, $ \ epsilon_1 = 0,1 $
Ergebnis
Sattelpunkt $ x_1 = -0.0011 $, $ x_2 = -0.5774 $
Initialisieren
Sattelpunktsuche
Als Verbesserung können, da es sich um eine Gradientenmethode handelt, verschiedene Optimierer (z. B. Momentum, RMSProp, Adam, RAdam) und Wegstain-Methoden in der konjugierten Gradientenmethode und beim maschinellen Lernen verwendet werden. Das Problem ist, dass es nicht stabil ist. Ich fragte mich, ob es für die Sattelpunktsuche verwendet werden könnte, die für die Vorhersage chemischer Reaktionen verwendet werden kann, aber es konvergierte mit einer seltsamen Struktur. Abhängig vom Anfangswert kann es auch sein, dass es nicht zum Sattelpunkt konvergiert. Ein Beispiel ist unten gezeigt. Dies unterscheidet sich vom Anfangswert im vorherigen Beispiel.
・ Funktion $ y = x_1 ^ 2 + x_2 ^ 3-x_2 $ Anfangswert $ x_1 = -2 $, $ x_2 = -0,5 $ Kleiner Betrag $ \ delta = 0,05 $ Lernrate $ \ epsilon_1 = 0,1 $, $ \ epsilon_1 = 0,1 $
Initialisieren
Sattelpunktsuche
In diesem Beispiel wurden $ \ boldsymbol {b} $ und $ \ boldsymbol {c} $ aktualisiert, sodass die Richtung von $ x_2 $ den Minimalwert und die Richtung von $ x_1 $ den Maximalwert hat. Während wir auf den Minimalwert der kubischen Funktion aktualisieren, steigen wir daher stetig die Steigung der quadratischen Funktion an.
In diesem Artikel haben wir mit der Gradientenmethode nach Sattelpunkten gesucht. Zur Veranschaulichung wird ein Beispiel für eine Funktion mit zwei Variablen gezeigt. Sie können jedoch auch eine beliebige Variablenfunktion verwenden. Wenn Sie Fragen haben, werden wir in den Kommentaren antworten. Fühlen Sie sich frei zu kommentieren, wenn Sie irgendwelche Anfragen nach Formeltransformationen oder Quellcode haben.
In "Berechnung 1 Initialisierung" gibt es Einstellungen wie Funktionen, Anfangswerte und Lernrate. Sie können den Inhalt der Klasse SDG in verschiedene Optimierer ändern.
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from datetime import datetime
np.set_printoptions(precision=4, floatmode='maxprec')
class SDG:
def __init__(self, learning_rate):
self.learning_rate = learning_rate
def solve(self, coord, gradient):
return coord - self.learning_rate*gradient
class SaddlePoint:
def __init__(self, function, gradient, coordinate, delta=1.0e-3,
learning_rate=0.5, max_iteration=1000,
judgment1=1.0e-8, judgment2=1.0e-8, log_span=100):
"""
Initialisierungskonstruktor
function --Zielfunktion
differential --Funktionsdifferenzierung erster Ordnung
coordinates --Anfangskoordinaten
delta --Kleiner Wert
learning_rate --Lernrate
judgment --Konvergenzurteil 1
judgment --Konvergenzurteil 2
log_span --Protokollanzeigeintervall
"""
self.function = function
self.gradient = gradient
self.coordinate = coordinate.copy()
self.dim = len(coordinate)
self.delta = delta
self.learning_rate = learning_rate
self.max_iteration = max_iteration
self.judgment1 = judgment1
self.judgment2 = judgment2
self.log_span = log_span
#Basisvektor
self.b = np.random.rand(self.dim)
self.b /= np.linalg.norm(self.b)
self.c = np.random.rand(self.dim)
self.c /= np.linalg.norm(self.c)
# SDG
self.sdg_b_init = SDG(learning_rate)
self.sdg_c_init = SDG(learning_rate)
self.sdg_b_solv = SDG(learning_rate)
self.sdg_c_solv = SDG(learning_rate)
self.sdg_a_solv = SDG(learning_rate)
def initialize(self):
"""
Funktion zum Initialisieren. Geeignet b,Bestimmen Sie c
Rückgabewert--Minimalwert Richtungsvektor b,Maximalwert Richtungsvektor c
"""
#Steigung
gradient = self.gradient
#Koordinate b
coordinate = self.coordinate
#Basisvektor
b = self.b.copy()
c = self.c.copy()
#Betrag aktualisieren
diff_b = np.zeros_like(b)
diff_c = np.zeros_like(c)
#Lernrate
learning_rate = self.learning_rate
#Kleiner Wert
delta = self.delta
#Standardisierung
norm = np.linalg.norm
#Beurteilung
judgement1 = self.judgment1
#Protokollintervall
log_span = self.log_span
# SDG
sdg_b = self.sdg_b_init
sdg_c = self.sdg_c_init
z, _ = gradient(coordinate)
print("-----Initialization of b has started.-----")
for i in range(self.max_iteration):
#Differential erster Ordnung
z_b1, grad_b1 = gradient(coordinate + delta*b)
z_b2, grad_b2 = gradient(coordinate - delta*b)
#Betragsberechnung ändern
nabla_b = (grad_b1 - grad_b2)/delta
grad_b = nabla_b - (np.dot(b, nabla_b))*b
#aktualisieren
b = sdg_b.solve(b, -grad_b)
#Standardisierung
b /= norm(b)
#Konvergenzurteil
error = np.linalg.norm(grad_b)
if i%log_span == 0:
print("Iteration = {}, Error = {}".format(i, error))
if error < judgement1:
print("Converged! Iteration = {}, Error = {}".format(i, error))
break
self.b = b.copy()
print()
print("-----Initialization of c has started.-----")
for i in range(self.max_iteration):
#Gradientenberechnung
z_c1, grad_c1 = gradient(coordinate + delta*c)
z_c2, grad_c2 = gradient(coordinate - delta*c)
#Betragsberechnung ändern
nabla_c = (grad_c1 - grad_c2)/delta
grad_c = nabla_c - (np.dot(c, nabla_c))*c
#aktualisieren
c = sdg_c.solve(c, grad_c)
#Standardisierung
c /= norm(c)
#Konvergenzurteil
error = np.linalg.norm(grad_c)
if i%log_span == 0:
print("Iteration = {}, Error = {}".format(i, error))
if error < judgement1:
print("Converged! Iteration = {}, Error = {}".format(i, error))
break
self.c = c.copy()
print()
print("Result")
print("b = {}".format(self.b))
print("c = {}".format(self.c))
print()
return self.b, self.c
def solve(self):
"""
Suche nach Sattelpunkten
Rückgabewert--Sattelpunktkoordinaten koordinieren,Minimalwert Richtungsvektor b,Maximalwert Richtungsvektor c
"""
#Steigung
gradient = self.gradient
#Koordinaten, eine Sammlung von Koordinaten
coordinate = self.coordinate.copy()
coordinate_Array = coordinate.copy()
#Basisvektor
b = self.b.copy()
c = self.c.copy()
#Betrag aktualisieren
diff_b = np.zeros_like(b)
diff_c = np.zeros_like(c)
#Lernrate
learning_rate = self.learning_rate
#Kleiner Wert
delta = self.delta
#Standardisierung
norm = np.linalg.norm
#Beurteilung
judgement1 = self.judgment1
judgement2 = self.judgment2
#Protokollintervall
log_span = self.log_span
# SDG
sdg_a = self.sdg_a_solv
sdg_b = self.sdg_b_solv
sdg_c = self.sdg_c_solv
print("-----Saddle-point solver has started.-----")
for i in range(self.max_iteration):
#Differential erster Ordnung
z_b1, grad_b1 = gradient(coordinate + delta*b)
z_b2, grad_b2 = gradient(coordinate - delta*b)
z_c1, grad_c1 = gradient(coordinate + delta*c)
z_c2, grad_c2 = gradient(coordinate - delta*c)
grad_through_b = (z_b1-z_b2) / (2.0*delta)
grad_through_c = (z_c1-z_c2) / (2.0*delta)
#Differential zweiter Ordnung
z, _ = gradient(coordinate)
grad2_through_b = (z_b1-2.0*z+z_b2) / delta**2.0
grad2_through_c = (z_c1-2.0*z+z_c2) / delta**2.0
#aktualisieren
# coordinate = sdg_a.solve(coordinate,
# grad_through_b*b/(np.linalg.norm(grad_through_b)+np.linalg.norm(grad2_through_b))
# -grad_through_c*c/(np.linalg.norm(grad_through_c)+np.linalg.norm(grad2_through_c)))
coordinate = sdg_a.solve(coordinate, grad_through_b*b - grad_through_c*c)
coordinate_Array = np.vstack([coordinate_Array, coordinate])
#Konvergenzurteil
error_coordinate = np.linalg.norm(grad_through_b**2 + grad_through_c**2)
# b,Aktualisierung von c
nabla_b = -(grad_b1 - grad_b2)/delta
grad_b = nabla_b - (np.dot(b, nabla_b))*b
#aktualisieren
b = sdg_b.solve(b, grad_b)
#Standardisierung
b /= norm(b)
#Konvergenzurteil
error_b = np.linalg.norm(grad_b)
nabla_c = (grad_c1 - grad_c2)/delta
grad_c = nabla_c - (np.dot(c, nabla_c))*c
#aktualisieren
c = sdg_c.solve(c, grad_c)
#Standardisierung
c /= norm(c)
#Konvergenzurteil
error_c = np.linalg.norm(grad_c)
if i%log_span == 0:
print("B converged! Iteration = {}, Error = {}".format(i, error_b))
print("C converged! Iteration = {}, Error = {}".format(i, error_c))
print("Iteration = {}, Error = {}".format(i, error_coordinate))
print()
if error_coordinate < judgement2:
print("Converged! Iteration = {}, Error = {}".format(i, error_coordinate))
break
self.coordinate = coordinate.copy()
self.b = b.copy()
self.c = c.copy()
print()
print("Result")
print("coordinate = {}".format(self.coordinate))
print("b = {}".format(self.b))
print("c = {}".format(self.c))
print()
return self.coordinate, coordinate_Array, self.b, self.c
# =============================================================================
#Berechnung 1 Initialisierung
# =============================================================================
def f(x):
#Funktion
return x[0]**2 - x[1]**2
def gradient_f(x):
#Funktionsdifferenzierung erster Ordnung
return f(x), np.array([2*x[0], -2*x[1]])
x_init = np.array([-2.0, -1.0], dtype="float")
saddlePoint = SaddlePoint(f, gradient_f, x_init, delta=1e-3,
learning_rate=0.1, max_iteration=100,
judgment1=1.0e-5, judgment2=1.0e-5, log_span=1)
b, c = saddlePoint.initialize() #Initialisieren
# =============================================================================
#Diagrammzeichnung(2D)
# =============================================================================
t = np.linspace(-1.0, 1.0, 100)
tb = np.linspace(x_init-1.0*b, x_init+1.0*b, 100)
tc = np.linspace(x_init-1.0*c, x_init+1.0*c, 100)
fb = f(tb.T)
fc = f(tc.T)
plt.xlabel("t")
plt.ylabel("z")
plt.plot(t, fb, c="red")
plt.plot(t, fc, c="green")
plt.savefig("file/" + str(datetime.now().strftime("%Y_%m_%d_%H_%M_%S")) + "_2d1.png ", dpi=300)
plt.show()
# =============================================================================
#Diagrammzeichnung(3D)
# =============================================================================
#Drahtrahmen
fig = plt.figure(figsize=(6, 6))
ax = fig.add_subplot(111, projection='3d')
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_zlabel("z")
X = np.linspace(-2.5, 2.5, 50)
Y = np.linspace(-2.5, 2.5, 50)
X, Y = np.meshgrid(X, Y)
Z = f([X,Y])
#Basisvektor
width = 1.0
bt = np.linspace(x_init-width*b,x_init+width*b,10)
bz = f(bt.T)
ct = np.linspace(x_init-width*c,x_init+width*c,10)
cz = f(ct.T)
#Flugbahn
zArray = f(x_init)
#Anzeige
ax.plot_wireframe(X,Y,Z,color="gray",linewidth=0.2) #Drahtrahmen
ax.plot(bt[:,0],bt[:,1],bz,color="red") #Minimal
ax.plot(ct[:,0],ct[:,1],cz,color="green") #maximal
ax.scatter(x_init[0],x_init[1],f(x_init),color="blue") #Flugbahn
plt.savefig("file/" + str(datetime.now().strftime("%Y_%m_%d_%H_%M_%S")) + "_3d1.png ", dpi=300)
plt.show()
# =============================================================================
#Berechnung 2 Sattelpunktsuche
# =============================================================================
x, xArray, b, c = saddlePoint.solve() #Sattelpunktberechnung
# =============================================================================
#Diagrammzeichnung(2D)
# =============================================================================
t = np.linspace(-1.0, 1.0, 100)
tb = np.linspace(x-1.0*b, x+1.0*b, 100)
tc = np.linspace(x-1.0*c, x+1.0*c, 100)
fb = f(tb.T)
fc = f(tc.T)
plt.xlabel("t")
plt.ylabel("z")
plt.plot(t, fb, c="red")
plt.plot(t, fc, c="green")
plt.savefig("file/" + str(datetime.now().strftime("%Y_%m_%d_%H_%M_%S")) + "_2d2.png ", dpi=300)
plt.show()
# =============================================================================
#Diagrammzeichnung(2D)
# =============================================================================
#Drahtrahmen
fig = plt.figure(figsize=(6, 6))
ax = fig.add_subplot(111, projection='3d')
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_zlabel("z")
X = np.linspace(-2.5, 2.5, 50)
Y = np.linspace(-2.5, 2.5, 50)
X, Y = np.meshgrid(X, Y)
Z = f([X,Y])
#Basisvektor
width = 1.0
bt = np.linspace(x-width*b,x+width*b,10)
bz = f(bt.T)
ct = np.linspace(x-width*c,x+width*c,10)
cz = f(ct.T)
#Flugbahn
zArray = f(xArray.T)
#Anzeige
ax.plot_wireframe(X,Y,Z,color="gray",linewidth=0.2) #Drahtrahmen
ax.plot(bt[:,0],bt[:,1],bz,color="red") #Minimal
ax.plot(ct[:,0],ct[:,1],cz,color="green") #maximal
ax.scatter(xArray[:,0],xArray[:,1],zArray,color="blue") #Flugbahn
ax.text(xArray[0,0],xArray[0,1],zArray[0], "start")
ax.text(xArray[-1,0],xArray[-1,1],zArray[-1], "goal")
plt.savefig("file/" + str(datetime.now().strftime("%Y_%m_%d_%H_%M_%S")) + "_3d2.png ", dpi=300)
plt.show()
Recommended Posts