一个旁听生做机器学习实验一(多项式拟合正弦曲线)有感,想做一个方便调参的工具来熟悉一下GUI和多线程的基本用法
运行结果如下
代码是两个py文件,放在同一目录下,运行main.py即可
main.py如下
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.pyplot as plt
import numpy as np
import numpy as np
from threading import Thread
from lab1 import *
plt.rcParams["font.sans-serif"] = ["SimHei"] # 用来正常显示中文标签SimHei
plt.rcParams["axes.unicode_minus"] = False # 用来正常显示负号
# 默认参数
params = {
"train_num": 10,
"validation_num": 100,
"test_num": 1000,
"order": 7,
"x_left": 0,
"x_right": 1,
"noise_scale": 0.25,
"learning_rate": 0.01,
"delta": 1e-6,
"lambd": np.power(np.e, -7),
}
class UpdatePlotThread(Thread):
def __init__(self, button):
super(UpdatePlotThread, self).__init__()
self.button = button
def run(self):
update_plot()
self.button.config(state="normal")
class ComputeThread(Thread):
def __init__(self, func, *args, **kwargs):
super(ComputeThread, self).__init__()
self.func = func
self.args = args
self.kwargs = kwargs
self.result = None
def run(self):
self.result = self.func(*self.args, **self.kwargs)
print(self.func, self.args, self.kwargs)
def get_result(self):
return self.result
def update_plot():
button.config(state="disabled")
# 读入数据
train_num = int(train_num_var.get())
validation_num = int(validation_num_var.get())
test_num = int(test_num_var.get())
order = int(order_var.get())
x_left = float(x_left_var.get())
x_right = float(x_right_var.get())
noise_scale = float(noise_scale_var.get())
learning_rate = float(learning_rate_var.get())
delta = float(delta_var.get())
lambd = float(lambd_var.get())
# 初始化数据
text.delete("1.0", tk.END) # 清空文本框
# 训练集
text.insert(tk.END, "初始化数据...\n\n")
train_data = get_data((x_left, x_right), train_num, base_func, noise_scale)
x_train = train_data["X"]
y_train = train_data["Y"]
# 验证集
x_validation = np.linspace(x_left, x_right, validation_num)
y_validation = base_func(x_validation)
# 测试集
x_test = np.linspace(x_left, x_right, test_num)
y_test = base_func(x_test)
text.insert(tk.END, "数据初始化完毕\n\n")
# 启动四个计算线程
thread1 = ComputeThread(
predict_analytical_without_penalty,
x_train,
y_train,
x_test,
x_validation,
y_validation,
order,
)
thread2 = ComputeThread(
predict_analytical_with_penalty,
x_train,
y_train,
x_test,
x_validation,
y_validation,
order,
lambd_penalty=lambd,
)
thread3 = ComputeThread(
predict_gradient_descent,
x_train,
y_train,
x_test,
lambd,
learning_rate,
order,
delta,
)
thread4 = ComputeThread(
predict_conjugate_gradient,
x_train,
y_train,
x_test,
lambd,
order,
delta,
)
text.insert(tk.END, "计算不带惩罚项的解析解...\n\n")
text.insert(tk.END, "计算带惩罚项的解析解...\n\n")
text.insert(tk.END, "利用验证集来计算lambd...\n\n")
text.insert(tk.END, "计算梯度下降法...\n\n")
text.insert(tk.END, "计算共轭梯度法...\n\n")
# 启动线程
threads = [thread1, thread2, thread3, thread4]
# threads = [thread4]
for thread in threads:
thread.start()
# 等待所有线程完成
for thread in threads:
thread.join()
button.config(state="normal")
# 获取计算结果
result1 = thread1.get_result()
result2 = thread2.get_result()
result3 = thread3.get_result()
result4 = thread4.get_result()
text.insert(tk.END, "不带惩罚项的解析解计算完毕\n\n")
text.insert(tk.END, f"lambd={result2[2]} , MSE={result2[3]}\n\n")
text.insert(tk.END, "带惩罚项的解析解计算完毕\n\n")
text.insert(tk.END, "梯度下降法计算完毕\n\n")
text.insert(tk.END, f"梯度下降法迭代次数为{result3[0]}\n\n")
text.insert(tk.END, "共轭梯度法计算完毕\n\n")
text.insert(tk.END, f"共轭梯度法迭代次数为{result4[0]}\n\n")
# 更新 UI 和绘制图形
update_ui(
x_train, y_train, x_test, y_test, result1[0], "不带惩罚项的解析解", ax1, canvas1
)
update_ui(
x_train, y_train, x_test, y_test, result2[0], "带惩罚项的解析解", ax2, canvas2
)
update_ui(x_train, y_train, x_test, y_test, result3[1], "梯度下降法", ax3, canvas3)
update_ui(x_train, y_train, x_test, y_test, result4[1], "共轭梯度法", ax4, canvas4)
def update_ui(x_train, y_train, x_test, y_test, y_pred, title, ax, canvas):
ax.clear()
ax.scatter(x_train, y_train, label="训练集", color="white", edgecolors="darkblue")
ax.plot(x_test, y_test, linewidth=2, label="测试集", color="gray", linestyle="-.")
ax.plot(x_test, y_pred, linewidth=2, label="预测值", color="green", linestyle="-")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_title(title)
ax.legend()
canvas.draw()
if __name__ == "__main__":
root = tk.Tk()
root.geometry("1800x800")
root.title("多项式拟合正弦曲线")
for i in range(30): # 有30列
root.grid_columnconfigure(i, weight=1)
for i in range(30): # 有30行
root.grid_rowconfigure(i, weight=1)
# 创建参数输入框
labels = [
"train_num",
"validation_num",
"test_num",
"order",
"x_left",
"x_right",
"noise_scale",
"learning_rate",
"delta",
"lambd",
]
vars = {}
for i, label in enumerate(labels):
tk.Label(root, text=label).grid(row=i, column=15, sticky="e", padx=5, pady=5)
vars[label] = tk.StringVar(value=str(params[label]))
tk.Entry(root, textvariable=vars[label]).grid(
row=i, column=16, sticky="w", padx=5, pady=5
)
train_num_var = vars["train_num"]
validation_num_var = vars["validation_num"]
test_num_var = vars["test_num"]
order_var = vars["order"]
x_left_var = vars["x_left"]
x_right_var = vars["x_right"]
noise_scale_var = vars["noise_scale"]
learning_rate_var = vars["learning_rate"]
delta_var = vars["delta"]
lambd_var = vars["lambd"]
# 文本框
text = tk.Text(root, height=10, width=40)
text.grid(row=3, column=22, columnspan=2, rowspan=2, sticky="nsew")
# 绘制图形的初始设置
fig1, ax1 = plt.subplots(figsize=(4, 3))
canvas1 = FigureCanvasTkAgg(fig1, master=root) # 创建画布对象
canvas_widget1 = canvas1.get_tk_widget()
canvas_widget1.grid(row=2, column=4, columnspan=2, rowspan=4, sticky="nsew")
fig2, ax2 = plt.subplots(figsize=(4, 3))
canvas2 = FigureCanvasTkAgg(fig2, master=root) # 创建画布对象
canvas_widget2 = canvas2.get_tk_widget()
canvas_widget2.grid(row=2, column=6, columnspan=2, rowspan=4, sticky="nsew")
fig3, ax3 = plt.subplots(figsize=(4, 3))
canvas3 = FigureCanvasTkAgg(fig3, master=root) # 创建画布对象
canvas_widget3 = canvas3.get_tk_widget()
canvas_widget3.grid(row=6, column=4, columnspan=2, rowspan=4, sticky="nsew")
fig4, ax4 = plt.subplots(figsize=(4, 3))
canvas4 = FigureCanvasTkAgg(fig4, master=root) # 创建画布对象
canvas_widget4 = canvas4.get_tk_widget()
canvas_widget4.grid(row=6, column=6, columnspan=2, rowspan=4, sticky="nsew")
# 创建更新按钮
button = tk.Button(
root, text="Update Plot", command=lambda: UpdatePlotThread(button).start()
)
button.grid(row=len(labels), column=15, columnspan=2, sticky="ew", padx=5, pady=5)
root.mainloop()
lab1.py如下
import numpy as np
import pandas as pd
# 基础函数*****************************************************************************
def base_func(x):
"""
基函数 y = sin(2 * pi * x)
"""
return np.sin(2 * np.pi * x)
def get_data(
x_range: tuple[float, float] = (0, 1),
sample_num: int = 10,
base_func=base_func,
noise_scale=0.25,
) -> pd.DataFrame:
"""
生成数据 加入噪声
"""
X = np.linspace(
x_range[0], x_range[1], num=sample_num
) # np.linspace用于生成一个等间距的数值序列
Y = base_func(X) + np.random.normal(loc=0, scale=noise_scale, size=X.shape)
data = pd.DataFrame(data=np.dstack((X, Y))[0], columns=["X", "Y"])
"""np.dstack用于沿着数组深度来堆叠多个二维数组
补充:
X = np.array([1,2,3]).shape-------(3,)
X = np.array([[1, 2, 3]]).shape-------(1,3)
X = np.array([[1],[2],[3]]).shape-------(3,1)
举例:
二维数组
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
result = np.dstack((a, b))
print(result)
print(result.shape)
得到
[ [ [1 5][2 6] ] [ [3 7][4 8] ] ]
(2, 2, 2)
一维数组
X = np.array([1, 2, 3])
Y = np.array([4, 5, 6])
result = np.dstack((X, Y))
print(result)
print(result.shape)
得到
[ [ [1 4][2 5][3 6] ] ]
(1, 3, 2)
"""
return data
def get_params(X, T, lambda_penalty=0):
"""
解析法
"""
return np.linalg.pinv(X.T @ X + lambda_penalty * np.identity(X.shape[1])) @ X.T @ T
# np.linalg.pinv用于计算矩阵的伪逆
# np.identity用于创建单位矩阵
def calc_RMSE(y_true, y_pred):
return np.sqrt(np.mean(np.square(y_true - y_pred)))
def calc_loss(X, T, lambda_penalty, W):
"""
计算损失函数
"""
return 0.5 * np.mean((X @ W - T).T @ (X @ W - T) + lambda_penalty * W.T * W)
def calc_derivative(X, T, lambda_penalty, W):
"""
计算损失函数对参数的导数
"""
return X.T @ X @ W - X.T @ T + lambda_penalty * W
def gradient_descent(X, T, lambda_penalty, W, learning_rate=0.1, delta=1e-6):
"""
梯度下降法
"""
loss_init = calc_loss(X, T, lambda_penalty, W)
k = 0
while True:
W = W - learning_rate * calc_derivative(X, T, lambda_penalty, W)
loss = calc_loss(X, T, lambda_penalty, W)
if np.abs(loss - loss_init) < delta:
break
else:
k += 1
if loss > loss_init: # 说明学习率可能太大,导致步长过大
learning_rate *= 0.5
loss_init = loss
return k, W
def get_x_series(x, order) -> list[float]:
"""return 1, x^1, x^2,..., x^n, n = order"""
series = [1.0]
for _ in range(order):
series.append(series[-1] * x)
return series
def get_x_matrix(x_vec, order: int = 1) -> list[list[float]]:
x_matrix = []
for i in range(len(x_vec)):
x_matrix.append(get_x_series(x_vec[i], order))
return np.asarray(x_matrix)
def predict_analytical_without_penalty(
x_train, y_train, x, x_validation, y_validation, order, lambd_penalty=0
):
W = get_params(get_x_matrix(x_train, order=order), y_train, lambd_penalty)
return np.dot(get_x_matrix(x, order=order), W), W
def predict_analytical_with_penalty(
x_train, y_train, x, x_validation, y_validation, order, lambd_penalty
):
# text.insert(tk.END, "利用验证集来计算lambd...\n\n")
min_lambda, min_mse = calc_lambd(
x_train, y_train, x_validation, y_validation, order
)
# text.insert(tk.END, f"lambd={min_lambda} , MSE={min_mse}\n\n")
W = get_params(get_x_matrix(x_train, order=order), y_train, min_lambda)
return np.dot(get_x_matrix(x, order=order), W), W, min_lambda, min_mse
def calc_lambd(x_train, y_train, x_validation, y_validation, order):
error_ln_lambda = []
for i in range(-50, 0):
y, w = predict_analytical_without_penalty(
x_train,
y_train,
x_validation,
x_validation,
y_validation,
order,
lambd_penalty=np.exp(i),
)
error_ln_lambda.append([i, calc_RMSE(y_validation, y), "validation"])
# 创建数据框
data = pd.DataFrame(error_ln_lambda, columns=["lambda", "MSE", "type"])
# 找到MSE最小的点
min_mse_row = data.loc[data["MSE"].idxmin()]
# 获取最小的MSE对应的参数和MSE值
min_lambda = np.exp(min_mse_row["lambda"])
min_mse = min_mse_row["MSE"]
# 返回最小的MSE对应的参数和MSE值
return min_lambda, min_mse
def predict_gradient_descent(
x_train, y_train, x, lambd_penalty, learning_rate, order, delta
):
# text.insert(tk.END, "计算梯度下降法...\n\n")
k, w = gradient_descent(
get_x_matrix(x_train, order),
y_train,
lambd_penalty,
np.zeros(order + 1),
learning_rate,
delta,
)
# text.insert(tk.END, f"迭代次数为{k}\n\n")
# text.insert(tk.END, "梯度下降法计算完毕\n\n")
return k, np.dot(get_x_matrix(x, order), w), w
def predict_conjugate_gradient(x_train, y_train, x, lambda_penalty, order, delta):
A, x_0, b = switch_deri_func_for_conjugate_gradient(
get_x_matrix(x_train, order),
y_train,
lambda_penalty,
w=np.zeros(order + 1),
)
k, w = conjugate_gradient(A, x_0, b, delta)
return k, np.dot(get_x_matrix(x, order), w), w
def switch_deri_func_for_conjugate_gradient(x_matrix, t, lambda_penalty, w):
A = x_matrix.T @ x_matrix + lambda_penalty * np.identity(len(x_matrix.T))
b = x_matrix.T @ t
return A, w, b
def conjugate_gradient(A, x, b, delta=1e-6):
"""
求解Ax=b
"""
r_0 = b - A @ x
p = r_0
k = 0
while True:
alpha = (r_0.T @ r_0) / (p.T @ A @ p)
x = x + alpha * p
r = r_0 - alpha * A @ p
if r_0.T @ r_0 < delta:
break
beta = (r.T @ r) / (r_0.T @ r_0)
p = r + beta * p
r_0 = r
k += 1
return k, x