机器学习算法原理实现——使用交叉熵、梯度下降求解逻辑回归

发布时间 2023-09-07 19:43:21作者: bonelee

交叉熵的定义以及和熵的区别?

 

 

 

交叉熵是衡量两个概率分布之间的差异的一个度量。在机器学习和深度学习中,尤其是分类问题,交叉熵常被用作损失函数。交叉熵度量的是实际分布(标签)与模型预测之间的不一致程度。

 

这个值越小,模型的预测与真实分布越接近。完美的预测会有交叉熵为0,这是因为模型的预测概率分布与真实概率分布完全匹配。

在实际应用中,当我们使用交叉熵作为损失函数并利用优化算法(如梯度下降)来最小化这个损失时,模型的预测结果会逐渐地接近真实的标签

 

交叉熵为什么可以衡量两个概率分布之间的差异?内在机理通俗解释。
  

交叉熵背后的直觉和内在机制其实来源于信息论。在信息论中,熵是用来衡量一个随机变量不确定性的度量。交叉熵则可以被视为当你使用错误的概率分布去编码信息时,你平均需要的位数。

为了通俗解释这个概念,让我们用一个类比:

想象你和你的朋友在玩一个游戏。在这个游戏中,你们每次抽取一张带有数字的卡片。每张卡片上的数字都有一个与之关联的概率。你的任务是猜测每次抽取的数字,并且你已经知道每个数字的真实概率。

如果你知道真实的概率分布并根据这个概率分布做出决策,那么平均上你将达到最优的猜测效果。这个“最优效果”的平均不确定性(或你的平均惊讶程度)就是熵。

但假设你误解了这些概率,而是按照一个错误的概率分布来猜测(即你的模型的预测)。在这种情况下,你的平均惊讶程度会增加,因为你经常会被实际结果惊讶到。这个“增加的”惊讶度实际上是交叉熵。

更直接地说,交叉熵衡量的是,当我们使用模型的预测概率分布去解释真实的事件发生时,我们的平均惊讶程度是多少。如果模型的预测非常准确,那么交叉熵接近熵。如果模型的预测经常是错误的,那么交叉熵会远大于熵。

这就是为什么交叉熵可以作为两个概率分布之间差异的度量:它衡量了使用一个概率分布(模型的预测)去解释另一个概率分布(真实的标签)时的平均“惊讶度”。

 

应用:

Logistic Regression公式推导及其交叉熵损失函数

 

 

 再补上1/m(m为样本数):

 

好了,有了上面的知识就可以开始编码了!

import numpy as np


### 定义sigmoid函数
def sigmoid(x):
    '''
    输入:
    x:数组
    输出:
    z:经过sigmoid函数计算后的数组
    '''
    z = 1 / (1 + np.exp(-x))
    return z


### 定义参数初始化函数
def initialize_params(dims):
    '''
    输入:
    dims:参数维度
    输出:
    z:初始化后的参数向量W和参数值b
    '''
    # 将权重向量初始化为零向量
    W = np.zeros((dims, 1))
    # 将偏置初始化为零
    b = 0
    return W, b


### 定义对数几率回归模型主体
def logistic(X, y, W, b):
    '''
    输入:
    X: 输入特征矩阵
    y: 输出标签向量
    W: 权重系数
    b: 偏置参数
    输出:
    a: 对数几率回归模型输出
    cost: 损失
    dW: 权重梯度
    db: 偏置梯度
    '''
    # 训练样本量
    num_train = X.shape[0]
    # 训练特征数
    num_feature = X.shape[1]
    # 对数几率回归模型输出
    a = sigmoid(np.dot(X, W) + b)
    # 交叉熵损失
    cost = -1/num_train * np.sum(y*np.log(a) + (1-y)*np.log(1-a))
    # 权重梯度
    dW = np.dot(X.T, (a-y))/num_train
    # 偏置梯度
    db = np.sum(a-y)/num_train
    # 压缩损失数组维度
    cost = np.squeeze(cost)
    return a, cost, dW, db


### 定义对数几率回归模型训练过程
def logistic_train(X, y, learning_rate, epochs):
    '''
    输入:
    X: 输入特征矩阵
    y: 输出标签向量
    learning_rate: 学习率
    epochs: 训练轮数
    输出:
    cost_list: 损失列表
    params: 模型参数
    grads: 参数梯度
    '''
    # 初始化模型参数
    W, b = initialize_params(X.shape[1])
    # 初始化损失列表
    cost_list = []
    # 迭代训练
    for i in range(epochs):
        # 计算当前迭代的模型输出、损失和参数梯度
        a, cost, dW, db = logistic(X, y, W, b)
        # 参数更新
        W = W - learning_rate * dW
        b = b - learning_rate * db
        # 记录损失
        if i % 100 == 0:
            cost_list.append(cost)
        # 打印训练过程中的损失
        if i % 100 == 0:
            print('epoch %d cost %f' % (i, cost))
    # 保存参数
    params = {
        'W': W,
        'b': b
    }
    # 保存梯度
    grads = {
        'dW': dW,
        'db': db
    }
    return cost_list, params, grads


### 定义预测函数
def predict(X, params):
    '''
    输入:
    X: 输入特征矩阵
    params: 训练好的模型参数
    输出:
    y_pred: 转换后的模型预测值
    '''
    # 模型预测值
    y_pred = sigmoid(np.dot(X, params['W']) + params['b'])
    # 基于分类阈值对概率预测值进行类别转换
    for i in range(len(y_pred)):
        if y_pred[i] > 0.5:
            y_pred[i] = 1
        else:
            y_pred[i] = 0
    return y_pred


### 绘制对数几率回归分类决策边界
def plot_decision_boundary(X_train, y_train, params):
    '''
    输入:
    X_train: 训练集输入
    y_train: 训练集标签
    params:训练好的模型参数
    输出:
    分类决策边界图
    '''
    # 训练样本量
    n = X_train.shape[0]
    # 初始化类别坐标点列表
    xcord1 = []
    ycord1 = []
    xcord2 = []
    ycord2 = []
    # 获取两类坐标点并存入列表
    for i in range(n):
        if y_train[i] == 1:
            xcord1.append(X_train[i][0])
            ycord1.append(X_train[i][1])
        else:
            xcord2.append(X_train[i][0])
            ycord2.append(X_train[i][1])
    # 创建绘图
    fig = plt.figure()
    ax = fig.add_subplot(111)
    # 绘制两类散点,以不同颜色表示
    ax.scatter(xcord1, ycord1,s=32, c='red')
    ax.scatter(xcord2, ycord2, s=32, c='green')
    # 取值范围
    x = np.arange(-1.5, 3, 0.1)
    # 分类决策边界公式
    y = (-params['b'] - params['W'][0] * x) / params['W'][1]
    # 绘图
    ax.plot(x, y)
    plt.xlabel('X1')
    plt.ylabel('X2')
    plt.show()


# 导入matplotlib绘图库
import matplotlib.pyplot as plt
# 导入生成分类数据函数
from sklearn.datasets import make_classification
# 生成100×2的模拟二分类数据集
X, labels = make_classification(
    n_samples=100,
    n_features=2,
    n_redundant=0,
    n_informative=2,
    random_state=1,
    n_clusters_per_class=2)
# 设置随机数种子
rng = np.random.RandomState(2)
# 对生成的特征数据添加一组均匀分布噪声
X += 2 * rng.uniform(size=X.shape)
# 标签类别数
unique_labels = set(labels)
# 根据标签类别数设置颜色
colors = plt.cm.Spectral(np.linspace(0, 1, len(unique_labels)))
# 绘制模拟数据的散点图
for k,col in zip(unique_labels, colors):
    x_k = X[labels==k]
    plt.plot(x_k[:,0], x_k[:,1],'o',
             markerfacecolor=col,
             markeredgecolor='k',
             markersize=14)
plt.title('Simulated binary data set')
plt.show()

# 按9∶1简单划分训练集与测试集
offset = int(X.shape[0] * 0.9)
X_train, y_train = X[:offset], labels[:offset]
X_test, y_test = X[offset:], labels[offset:]
y_train = y_train.reshape((-1,1))
y_test = y_test.reshape((-1,1))
print('X_train =', X_train.shape)
print('X_test =', X_test.shape)
print('y_train =', y_train.shape)
print('y_test =', y_test.shape)



# 执行对数几率回归模型训练
cost_list, params, grads = logistic_train(X_train, y_train, 0.01, 1000)
# 打印训练好的模型参数
print(params)
# 基于训练参数对测试集进行预测
y_pred = predict(X_test, params)
print(y_pred)

# 导入classification_report模块
from sklearn.metrics import classification_report
# 打印测试集分类预测评估报告
print(classification_report(y_test, y_pred))
plot_decision_boundary(X_train, y_train, params)


# 导入对数几率回归模块
from sklearn.linear_model import LogisticRegression
# 拟合训练集
clf = LogisticRegression(random_state=0).fit(X_train, y_train)
# 预测测试集
y_pred = clf.predict(X_test)
# 打印预测结果
print(y_pred)

  

运行截图:

 

 

X_train = (90, 2)
X_test = (10, 2)
y_train = (90, 1)
y_test = (10, 1)
epoch 0 cost 0.693147
epoch 100 cost 0.554066
epoch 200 cost 0.480953
epoch 300 cost 0.434738
epoch 400 cost 0.402395
epoch 500 cost 0.378275
epoch 600 cost 0.359468
epoch 700 cost 0.344313
epoch 800 cost 0.331783
epoch 900 cost 0.321216
{'W': array([[ 1.55740577],
       [-0.46456883]]), 'b': -0.594451885315136}
[[0.]
 [1.]
 [1.]
 [0.]
 [1.]
 [1.]
 [0.]
 [0.]
 [1.]
 [0.]]
              precision    recall  f1-score   support

           0       1.00      1.00      1.00         5
           1       1.00      1.00      1.00         5

    accuracy                           1.00        10
   macro avg       1.00      1.00      1.00        10
weighted avg       1.00      1.00      1.00        10

 
[0 1 1 0 1 1 0 0 1 0]

  

可以看到预测结果和sklearn的完全一样。

 

补充:predict还可以这样写得更简洁:

def predict(X, params):
    ans = []
    y = sigmoid(np.dot(X, params['W']) + params['b'])
    return (y > 0.5).astype(int)
"""
    for i in range(X.shape[0]):
        if y[i][0] > 0.5:
            ans.append(1)
        else:
            ans.append(0)
    return ans
"""

### 定义预测函数
def predict2(X, params):
    '''
    输入:
    X: 输入特征矩阵
    params: 训练好的模型参数
    输出:
    y_pred: 转换后的模型预测值
    '''
    # 模型预测值
    y_pred = sigmoid(np.dot(X, params['W']) + params['b'])
    # 基于分类阈值对概率预测值进行类别转换
    for i in range(len(y_pred)):
        if y_pred[i] > 0.5: # y_pred[i][0] is more precise!!!
            y_pred[i] = 1
        else:
            y_pred[i] = 0
    return y_pred