机器学习小白实战【逻辑回归——成绩预测(从“绿色锯齿”到“完美决策边界”)】

目录

前言

再学完线性回归预测房价之后,接着便是吴恩达老师的课程中的“分类问题”。今天的目标是用逻辑回归(Logistic Regression)来预测“学生能不能被录取”。
在刚开始写的时候,我踩了一个天坑——损失曲线变成了“绿色的锯齿”,怎么调学习率都没用。后来我找到了根源:没有做特征归一化。加了三行归一化的代码后,模型瞬间丝滑收敛。
这篇博客记录了我的完整代码和debug全过程,希望给大家带来帮助。

一、工具准备与环境配置

核心库还是老两样:
NumPy:用来做矩阵运算和梯度计算。
Matplotlib:用来画图和可视化决策边界。
以及相应的汉化操作,可参考上篇内容。

二、生成模拟数据

我模拟了 100 个学生的成绩,并假设录取的评判规则是:
0.3 * 考试成绩1 + 0.5 * 考试成绩2 - 40 > 0(加上一点随机噪音)。
然后生成对应的标签 y(1表示录取,0表示未录取)。

np.random.seed(42)
m = 100  # 100个学生
x1 = np.random.uniform(30, 100, m)
x2 = np.random.uniform(30, 100, m)
# 真实规律
z_real = 0.3 * x1 + 0.5 * x2 - 40 + np.random.normal(0, 5, m)
y = (z_real > 0).astype(int)
X = np.column_stack((x1, x2))

三、定义核心算法

逻辑回归的核心三步:
Sigmoid 函数:把输出值压缩到 [0, 1] 之间,变成概率。
对数损失函数(Log Loss):预测错的越离谱,损失指数级增长。
梯度下降:计算梯度,不断修正 w 和 b,并加了提前退出机制(损失连续10次不变就停止,节约算力)。

def sigmoid(z):
    return 1 / (1 + np.exp(-z))
def compute_log_loss(X, y, w, b):
    m = len(y)
    z = np.dot(X, w) + b
    p = sigmoid(z)
    p = np.clip(p, 1e-10, 1 - 1e-10)
    loss = - (1 / m) * np.sum(y * np.log(p) + (1 - y) * np.log(1 - p))
    return loss
def gradient_descent(X, y, w_init, b_init, alpha, iterations):
    w, b = w_init, b_init
    m = len(y)
    cost_history = []
    for i in range(iterations):
        z = np.dot(X, w) + b
        p = sigmoid(z)
        grad_w = (1 / m) * np.dot(X.T, (p - y))
        grad_b = (1 / m) * np.sum(p - y)
        w = w - alpha * grad_w
        b = b - alpha * grad_b
        cost = compute_log_loss(X, y, w, b)
        cost_history.append(cost)
        # 自动停止机制
        if i > 50 and abs(cost_history[-1] - cost_history[-10]) < 1e-6:
            print(f"已收敛!在第 {i} 次迭代停止。")
            break
    return w, b, cost_history

四、训练模型

设定初始化数据:学习率为0.05,迭代次数为200(实际运行时会自动停止)。

w_init = np.zeros(X_norm.shape[1])
b_init = 0
alpha = 0.05
iterations = 200
print("开始训练...")
final_w, final_b, cost_history = gradient_descent(X_norm, y, w_init, b_init, alpha, iterations)
print("\n最终结果:")
print(f"权重 w (归一化后): {final_w}")
print(f"截距 b: {final_b:.4f}")
print(f"最终损失: {cost_history[-1]:.4f}")

五、结果可视化

我主要画了两张图:

plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.plot(range(len(cost_history)), cost_history, color='green', linewidth=2)
plt.xlabel('迭代次数')
plt.ylabel('对数损失')
plt.title('损失下降过程 (平滑收敛)')
plt.grid(True)

plt.subplot(1, 2, 2)
plt.scatter(X[y == 0, 0], X[y == 0, 1], color='red', label='未录取 (0)')
plt.scatter(X[y == 1, 0], X[y == 1, 1], color='blue', label='录取 (1)')

# 画决策边界(反归一化到原始坐标系)
x1_range = np.array([30, 100])
x1_norm_range = (x1_range - X_mean[0]) / X_std[0]
x2_norm_range = -(final_w[0] * x1_norm_range + final_b) / final_w[1]
x2_range = x2_norm_range * X_std[1] + X_mean[1]
plt.plot(x1_range, x2_range, color='black', linewidth=3, label='决策边界')
plt.xlabel('考试成绩 1')
plt.ylabel('考试成绩 2')
plt.title('完美切分的决策边界')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()

第一张损失下降图:可以看到其震荡十分剧烈,究其原因是没有做归一化;我们通过归一化公式:(x - mean) / std,将特征值调到-1到1之间。
在这里插入图片描述
经过归一化,和学习率与迭代次数的调试我们最终得到如下图所示的平滑曲线:

#归一化代码
X_mean = np.mean(X, axis=0)
X_std = np.std(X, axis=0)
X_norm = (X - X_mean) / X_std

在这里插入图片描述

第二张图:完美切分的决策边界:红点(未录取)和蓝点(录取)被黑线精准分开了。黑线是公式:w1x1 + w2x2 + b = 0 反推出来的。
在这里插入图片描述

六、预测是否录取

模型训练好了,我们来预测一个真正的新学生(没有参与过训练):考试1拿 70分,考试2拿 50分,他能不能录取?
注意:预测时必须用训练集算出来的 X_mean 和 X_std 进行相同的归一化,否则预测结果会全错!

new_student = np.array([[70, 50]])
new_student_norm = (new_student - X_mean) / X_std
z_new = np.dot(new_student_norm, final_w) + final_b
prob_new = sigmoid(z_new)
print("\n" + "="*30)
print("🎯 新学生预测结果:")
print(f"考试成绩1: {new_student[0][0]}, 考试成绩2: {new_student[0][1]}")
print(f"该学生被录取的概率: {prob_new[0]:.2%}")
if prob_new[0] >= 0.5:
    print("👉 最终判定:✅ 录取!")
else:
    print("👉 最终判定:❌ 未录取")
print("="*30)

这是预测结果:
在这里插入图片描述

总结

逻辑回归的核心:本质上还是画一条分界线,只是加了个 Sigmoid 把输出变成概率。
特征归一化是必须的:这也是我今天踩得最大的坑。一旦数据量级差异大,梯度下降就会震荡,加上归一化立马“丝滑”。
预测时要用训练集的均值和标准差:这点非常重要,不然预测值会被完全扭曲。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐