实验一  BP算法实现分类预测

一、实验学时: 2学时

二、实验目的

  1. 掌握BP算法在前馈神经网络中的实现与应用;
  2. 理解数据预处理(独热编码与归一化)在模型训练中的作用;
  3. 熟悉神经网络在二分类问题中的应用方法;

 三、实验内容

利用BP算法实现西瓜数据集的分类预测。

四、主要实验步骤及结果

使用BP算法训练一个单隐藏层网络,编程实现在西瓜数据集(melon.csv)上预测“是否为好瓜”的应用,需要给出源代码以及运行过程和结果的截图。

1. 数据预处理,将西瓜特征转换为数值,将好瓜标记为“1”,坏瓜标记为“0”,对“密度”和“含糖量”进行归一化,关键代码如下:

def preprocess_data(file_path):
    # 读取数据
    data = pd.read_csv(file_path)
    print("melon.csv的数据:")
    print(data)
    # 初始化类别特征编码器字典
    label_encoders = {}
    # 需要编码的类别特征列
    categorical_cols = ['色泽', '根蒂', '敲声', '纹理', '脐部', '触感']
    # 对每个类别特征进行标签编码
    for col in categorical_cols:
        le = LabelEncoder()
        data[col] = le.fit_transform(data[col])  # 拟合并转换数据
        label_encoders[col] = le  # 保存编码器用于后续预测
    # 将标签列("好瓜")转换为数值 (是->1, 否->0)
    data['好瓜'] = data['好瓜'].map({'是': 1, '否': 0})
    # 提取特征矩阵 (X) 和标签向量 (y)
    x = data[['色泽', '根蒂', '敲声', '纹理', '脐部', '触感', '密度', '含糖率']].values
    y = data['好瓜'].values.reshape(-1, 1)  # 转换为列向量
    # 对数值特征"密度"和"含糖率"进行归一化
    scaler = MinMaxScaler()
    # 只对最后两列(数值特征)进行归一化
    x[:, -2:] = scaler.fit_transform(x[:, -2:])
    return x, y, label_encoders, scaler

读取melon.csv中的数据,并输出,检查是否成功读取,运行结果如图4-1所示:

图4-1 读取数据 

2. 构建模型,设置输入层节点数为8(节点数等于特征数),隐藏层节点数1,输出层节点数为1,调用BP网络类,初始化权重,使用小随机值(np.random.randn() * 0.1)防止梯度爆炸,初始化偏置为0,关键代码如下:

# 初始化
def __init__(self, input_size, hidden_size, output_size):
    # 初始化输入层到隐藏层的权重矩阵 (小型随机值)
    self.W1 = np.random.randn(input_size, hidden_size) * 0.1
    # 初始化隐藏层偏置 (初始为0)
    self.b1 = np.zeros((1, hidden_size))
    # 初始化隐藏层到输出层的权重矩阵 (小型随机值)
    self.W2 = np.random.randn(hidden_size, output_size) * 0.1
    # 初始化输出层偏置 (初始为0)
    self.b2 = np.zeros((1, output_size))

def sigmoid(self, x):  # Sigmoid激活函数
    return 1 / (1 + np.exp(-x))

def sigmoid_derivative(self, x):  # Sigmoid函数的导数
    return x * (1 - x)

3. 模型训练,使用Sigmoid激活函数计算更新权重和偏置,设置训练的学习率为0.1,训练10000次,前向传播计算输出值,反向传播更新权重和偏置,每1000次输出一次损失值,关键代码如下所示:

def forward(self, x):
    # 计算隐藏层输入 (线性组合)
    self.z1 = np.dot(x, self.W1) + self.b1
    # 计算隐藏层输出 (应用激活函数)
    self.a1 = self.sigmoid(self.z1)
    # 计算输出层输入 (线性组合)
    self.z2 = np.dot(self.a1, self.W2) + self.b2
    # 计算输出层输出 (应用激活函数)
    self.a2 = self.sigmoid(self.z2)
    return self.a2

def backward(self, x, y, output, learning_rate):
    m = x.shape[0]  # 样本数量
    # 计算输出层误差 (实际输出与预测输出的差)
    output_error = output - y
    # 计算输出层delta (误差乘以激活函数的导数)
    output_delta = output_error * self.sigmoid_derivative(output)
    # 计算隐藏层误差 (反向传播输出层delta)
    hidden_error = np.dot(output_delta, self.W2.T)
    # 计算隐藏层delta (误差乘以激活函数的导数)
    hidden_delta = hidden_error * self.sigmoid_derivative(self.a1)
    # 更新隐藏层到输出层的权重和偏置
    self.W2 -= learning_rate * np.dot(self.a1.T, output_delta) / m
    self.b2 -= learning_rate * np.sum(output_delta, axis=0, keepdims=True) / m
    # 更新输入层到隐藏层的权重和偏置
    self.W1 -= learning_rate * np.dot(x.T, hidden_delta) / m
    self.b1 -= learning_rate * np.sum(hidden_delta, axis=0) / m

def train(self, x, y, epochs=10000, learning_rate=0.1, verbose=True):
    for i in range(epochs):
        # 前向传播
        output = self.forward(x)
        # 反向传播
        self.backward(x, y, output, learning_rate)
        # 每1000轮打印一次损失
        if verbose and i % 1000 == 0:
            loss = np.mean(np.square(y - output))  # 均方误差
            print(f"Epoch {i}, Loss: {loss:.4f}")

def predict(self, x):
    # 预测结果 (0或1)
    output = self.forward(x)
    return (output > 0.5).astype(int)  # 大于0.5为1,否则为0

训练过程如图4-2所示:

图4-2 训练结果

4.预测结果,计算测试集预测准确率及混淆矩阵,关键代码如下:

def predict_single_melon(model, melon_features, label_encoders, scaler):
    # 将特征字典转换为DataFrame以便处理
    melon_df = pd.DataFrame([melon_features])
    categorical_cols = ['色泽', '根蒂', '敲声', '纹理', '脐部', '触感']
    for col in categorical_cols:
        melon_df[col] = label_encoders[col].transform(melon_df[col])
    # 提取特征并归一化数值特征
    x = melon_df[['色泽', '根蒂', '敲声', '纹理', '脐部', '触感', '密度', '含糖率']].values
    x[:, -2:] = scaler.transform(x[:, -2:])  # 只归一化最后两列(数值特征)
    prediction = model.predict(x)  # 获取预测类别 (0或1)
    probability = model.forward(x)[0][0]  # 获取预测概率
    return prediction[0][0], probability

运行结果如图4-3所示:

图4-3 预测结果

五、实验小结(包括问题和解决办法、心得体会、意见与建议等)

 1. 问题和解决办法:

问题1:数据集中的数据较少,训练的误差较大,测试集准确率较低

解决方法:添加更多的数据进行训练,隐藏层节点数设置为1,模型拟合能力不足,测试集准确率较低。

问题2:模型训练过程中损失值下降缓慢,收敛速度较慢。

解决方法:在训练过程中,发现损失值下降速度较慢,模型收敛需要较长时间,这可能是由于学习率设置不当、模型初始化不佳或优化算法选择不合适等原因造成的。

2. 心得体会:通过本次实验,我深刻理解了BP算法的基本原理和实现方法,掌握了神经网络模型在分类预测中的应用技巧。实验过程中,我认识到数据预处理对模型训练的重要性,以及模型结构和参数设置对预测性能的影响。同时,我也意识到在实际应用中,需要根据具体问题选择合适的模型结构和参数设置,以达到最佳的预测效果。本次实验不仅加深了我对深度学习的理解,也锻炼了我的实践能力和问题解决能力。我相信,这些经验和技能将对我未来的学习和研究产生积极的影响。

 源代码:

import numpy as np
import pandas as pd
from sklearn.preprocessing import LabelEncoder, MinMaxScaler
from sklearn.model_selection import train_test_split


class BPNeuralNetwork:
    # 初始化
    def __init__(self, input_size, hidden_size, output_size):
        self.z2 = None
        self.a1 = None
        self.z1 = None
        self.a2 = None
        # 初始化输入层到隐藏层的权重矩阵 (小型随机值)
        self.W1 = np.random.randn(input_size, hidden_size) * 0.1
        # 初始化隐藏层偏置 (初始为0)
        self.b1 = np.zeros((1, hidden_size))
        # 初始化隐藏层到输出层的权重矩阵 (小型随机值)
        self.W2 = np.random.randn(hidden_size, output_size) * 0.1
        # 初始化输出层偏置 (初始为0)
        self.b2 = np.zeros((1, output_size))

    def sigmoid(self, x):  # Sigmoid激活函数
        return 1 / (1 + np.exp(-x))

    def sigmoid_derivative(self, x):  # Sigmoid函数的导数
        return x * (1 - x)

    def forward(self, x):
        # 计算隐藏层输入 (线性组合)
        self.z1 = np.dot(x, self.W1) + self.b1
        # 计算隐藏层输出 (应用激活函数)
        self.a1 = self.sigmoid(self.z1)
        # 计算输出层输入 (线性组合)
        self.z2 = np.dot(self.a1, self.W2) + self.b2
        # 计算输出层输出 (应用激活函数)
        self.a2 = self.sigmoid(self.z2)
        return self.a2

    def backward(self, x, y, output, learning_rate):
        m = x.shape[0]  # 样本数量
        # 计算输出层误差 (实际输出与预测输出的差)
        output_error = output - y
        # 计算输出层delta (误差乘以激活函数的导数)
        output_delta = output_error * self.sigmoid_derivative(output)
        # 计算隐藏层误差 (反向传播输出层delta)
        hidden_error = np.dot(output_delta, self.W2.T)
        # 计算隐藏层delta (误差乘以激活函数的导数)
        hidden_delta = hidden_error * self.sigmoid_derivative(self.a1)
        # 更新隐藏层到输出层的权重和偏置
        self.W2 -= learning_rate * np.dot(self.a1.T, output_delta) / m
        self.b2 -= learning_rate * np.sum(output_delta, axis=0, keepdims=True) / m
        # 更新输入层到隐藏层的权重和偏置
        self.W1 -= learning_rate * np.dot(x.T, hidden_delta) / m
        self.b1 -= learning_rate * np.sum(hidden_delta, axis=0) / m

    def train(self, x, y, epochs=10000, learning_rate=0.1, verbose=True):
        for i in range(epochs):
            # 前向传播
            output = self.forward(x)
            # 反向传播
            self.backward(x, y, output, learning_rate)
            # 每1000轮打印一次损失
            if verbose and i % 1000 == 0:
                loss = np.mean(np.square(y - output))  # 均方误差
                print(f"Epoch {i}, Loss: {loss:.4f}")

    def predict(self, x):
        # 预测结果 (0或1)
        output = self.forward(x)
        return (output > 0.5).astype(int)  # 大于0.5为1,否则为0


def preprocess_data(file_path):
    # 读取数据
    data = pd.read_csv(file_path)
    print("melon.csv的数据:")
    print(data)
    # 初始化类别特征编码器字典
    label_encoders = {}
    # 需要编码的类别特征列
    categorical_cols = ['色泽', '根蒂', '敲声', '纹理', '脐部', '触感']
    # 对每个类别特征进行标签编码
    for col in categorical_cols:
        le = LabelEncoder()
        data[col] = le.fit_transform(data[col])  # 拟合并转换数据
        label_encoders[col] = le  # 保存编码器用于后续预测
    # 将标签列("好瓜")转换为数值 (是->1, 否->0)
    data['好瓜'] = data['好瓜'].map({'是': 1, '否': 0})
    # 提取特征矩阵 (X) 和标签向量 (y)
    x = data[['色泽', '根蒂', '敲声', '纹理', '脐部', '触感', '密度', '含糖率']].values
    y = data['好瓜'].values.reshape(-1, 1)  # 转换为列向量
    # 对数值特征"密度"和"含糖率"进行归一化
    scaler = MinMaxScaler()
    # 只对最后两列(数值特征)进行归一化
    x[:, -2:] = scaler.fit_transform(x[:, -2:])
    return x, y, label_encoders, scaler


def predict_single_melon(model, melon_features, label_encoders, scaler):
    # 将特征字典转换为DataFrame以便处理
    melon_df = pd.DataFrame([melon_features])
    # 对类别特征进行编码 (使用之前保存的编码器)
    categorical_cols = ['色泽', '根蒂', '敲声', '纹理', '脐部', '触感']
    for col in categorical_cols:
        melon_df[col] = label_encoders[col].transform(melon_df[col])
    # 提取特征并归一化数值特征
    x = melon_df[['色泽', '根蒂', '敲声', '纹理', '脐部', '触感', '密度', '含糖率']].values
    x[:, -2:] = scaler.transform(x[:, -2:])  # 只归一化最后两列(数值特征)
    # 进行预测
    prediction = model.predict(x)  # 获取预测类别 (0或1)
    probability = model.forward(x)[0][0]  # 获取预测概率
    return prediction[0][0], probability


def main():
    # 加载和预处理数据集
    data_file = 'melon.csv'
    print(f"正在加载并预处理数据: ", data_file)
    x, y, label_encoders, scaler = preprocess_data(data_file)
    # 划分训练集和测试集 (80%训练, 20%测试)
    x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=42)
    # 创建神经网络
    input_size = x_train.shape[1]  # 输入层节点数 = 特征数
    print("输入层:", input_size)
    hidden_size = 1  # 隐藏层节点数
    output_size = 1  # 输出层节点数
    nn = BPNeuralNetwork(input_size, hidden_size, output_size)
    # 训练神经网络
    print("\n开始训练神经网络...")
    nn.train(x_train, y_train, epochs=10000, learning_rate=0.1)
    # 在测试集上评估模型
    test_predictions = nn.predict(x_test)
    test_accuracy = np.mean(test_predictions == y_test)
    print(f"\n测试集准确率: {test_accuracy * 100:.2f}%")
    # 需要预测的西瓜特征
    test1 = {
        '色泽': '青绿',
        '根蒂': '蜷缩',
        '敲声': '浊响',
        '纹理': '清晰',
        '脐部': '凹陷',
        '触感': '硬滑',
        '密度': 0.697,
        '含糖率': 0.460
    }
    # 青绿 稍蜷 浊响 稍糊 凹陷 硬滑 0.639 0.161 0
    test2 = {
        '色泽': '青绿',
        '根蒂': '稍蜷',
        '敲声': '浊响',
        '纹理': '稍糊',
        '脐部': '凹陷',
        '触感': '硬滑',
        '密度': 0.639,
        '含糖率': 0.161
    }
    # 进行预测并输出结果
    print("\n预测单个西瓜:")
    print("西瓜特征:", test2)
    prediction, probability = predict_single_melon(nn, test2, label_encoders, scaler)
    result = "好瓜" if prediction == 1 else "坏瓜"
    print(f"预测结果: {result} (概率: {probability:.4f})")


if __name__ == "__main__":
    # 运行主函数
    main()

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐