1. 逻辑回归:二分类问题的数学本质

第一次接触逻辑回归时,很多人会被"回归"二字误导——这明明是个分类算法啊!其实逻辑回归的核心思想,是用线性回归的变体来解决二分类问题。想象你正在教小朋友区分猫和狗:给每张图片打分,分数越高越可能是猫,最后设定一个阈值(比如0.5)来判断。

Sigmoid函数是这个过程的魔法转换器。当线性输出z=w^Tx+b穿过这个S形曲线时,任意实数都被压缩到(0,1)区间,正好对应概率值。我常把它比作温度计:z值极大时概率接近1(确定是猫),极小时接近0(确定不是猫),中间过渡平滑。

关键细节:Sigmoid导数σ'=σ(1-σ)这个特性在反向传播时特别有用,就像自带加速器的刹车系统,能快速调整参数方向。

损失函数的设计更是精妙。为什么不用简单的平方误差?因为那会导致优化地形坑坑洼洼(非凸),梯度下降容易卡在局部低谷。交叉熵损失就像智能导航仪,总能找到通往全局最优的平滑路径:

def loss(y_hat, y):
    return - (y * np.log(y_hat) + (1-y) * np.log(1-y_hat))

当y=1时,-log(y_hat)惩罚低估;y=0时,-log(1-y_hat)惩罚高估。这种不对称设计让模型对错误预测非常敏感,学习效率大幅提升。

2. 梯度下降:参数优化的登山指南

理解梯度下降最形象的比喻是蒙眼下山:每次用脚试探周围坡度最陡的方向(梯度),然后迈出一小步(学习率α)。在猫狗分类任务中,参数w和b的更新公式看似简单:

w = w - α * dw  # dw是损失对w的偏导
b = b - α * db  # db是损失对b的偏导

但这里有三个实战经验值得分享:

  1. 学习率选择:α=0.01是常见起点,但我在猫狗数据集测试发现,0.0002收敛更快。太大(如0.1)会导致损失值震荡,太小(如0.00001)训练像蜗牛爬。
  2. 向量初始化:虽然w初始化为零在逻辑回归可行,但在深层网络中会引发对称性问题。后续课程会讲到Xavier初始化等高级技巧。
  3. 特征缩放:如果输入特征量纲差异大(如像素值0-255),建议归一化到[0,1],否则梯度下降路线会扭曲。

计算图是理解反向传播的利器。以J=3(a+bc)为例,从右向左逐层求导的链式法则,就像多米诺骨牌的反向连锁反应。在逻辑回归中,关键导数关系dz=a-y如此简洁,堪称数学之美。

3. 向量化编程:告别for循环的慢时代

在实现逻辑回归时,新手最容易犯的错误就是用for循环逐个处理样本。我曾在数据集上对比测试:当m=10,000时,向量化实现仅需1.9毫秒,而for循环版本要531毫秒——相差近300倍!

NumPy的广播机制是提速秘诀。比如计算Z=w^TX+b时:

Z = np.dot(W.T, X) + b  # W.T是转置,X是(n_x,m)矩阵

这里b作为标量,会自动扩展成(1,m)向量。同样,求dw时用矩阵乘法代替循环:

dw = np.dot(X, dZ.T)/m  # X是(n_x,m), dZ是(1,m)

广播规则的核心是维度对齐:

  • (m,n) ±/* (1,n) → 复制m次
  • (m,n) ±/* (m,1) → 复制n次
  • (m,1) ±/* (1) → 双向复制

踩坑提醒:np.random.randn(5)生成的是秩为1的数组(既非行向量也非列向量),应用reshape(5,1)明确形状,否则可能引发难以调试的广播错误。

4. 实战:从零构建猫狗分类器

让我们用Python实现完整流程。首先加载数据:

def load_data():
    cat_imgs = [cv2.resize(cv2.imread(f)/255, (64,64)) 
               for f in glob('cats/*.jpg')[:100]]
    dog_imgs = [cv2.resize(cv2.imread(f)/255, (64,64)) 
               for f in glob('dogs/*.jpg')[:100]]
    X = np.array([img.flatten() for img in cat_imgs + dog_imgs]).T
    Y = np.array([[1]*100 + [0]*100])
    return X, Y

接着实现核心算法:

def logistic_regression(X, Y, iterations=1000, lr=0.01):
    n_x, m = X.shape
    w, b = np.zeros((n_x,1)), 0
    
    for i in range(iterations):
        Z = np.dot(w.T, X) + b
        A = 1/(1+np.exp(-Z))
        dZ = A - Y
        dw = np.dot(X, dZ.T)/m
        db = np.sum(dZ)/m
        w -= lr * dw
        b -= lr * db
        
        if i%100 == 0:
            print(f"Iter {i}, cost: {np.mean(-Y*np.log(A)-(1-Y)*np.log(1-A))}")
    
    return w, b

测试时发现准确率约57%,看似不高,但比随机猜测的50%已有提升。这是因为:

  1. 原始像素特征区分度低
  2. 逻辑回归是线性分类器
  3. 未进行超参数精细调优

这正为后续学习埋下伏笔:如何用更复杂的网络结构和优化技巧提升性能?答案将在接下来的课程中揭晓。

更多推荐