本篇讲解机器学习理论。涵盖线性回归与逻辑回归的理论与实践,重点讲解数据预处理、模型构建与训练、性能评估及可视化,结合PaddlePaddle框架,系统学习深度学习开发流程并积累实战经验
☞☞☞AI 智能聊天, 问答助手, AI 智能搜索, 免费无限量使用 DeepSeek R1 模型☜☜☜

前言
上一节我们从机器学习的基础出发,逐步实现了线性回归和分类任务。现在我们将通过加州房价预测的线性回归问题,理解了如何使用简单的线性模型进行数值预测。
接着,我们使用Logistic 回归解决了Moon100数据集的分类问题,并通过优化算法提升了模型的准确性。
逐步让大家熟练学习机器学习的理论和实践
一、利用Paddle实现基于线性回归的加州房价预测
1.1 数据集介绍
我们将使用 加州房价数据集(California Housing Dataset)作为本案例的研究对象。该数据集包含 1990年加州普查区 的房价与其他特征信息,用于研究地区特征与房价之间的关系。数据集共有 20640条样本 和 8个特征,目标变量是该地区的房价中位数。
1.1.1 数据集特征说明
以下是数据集中的特征描述:
MedInc区域收入中位数万美元HouseAge区域房屋年龄中位数年AveRooms每户平均房间数无单位AveBedrms每户平均卧室数无单位Population区域人口数量人AveOccup每户平均入住人数人Latitude区域纬度度Longitude区域经度度Target(目标变量)房价中位数(待预测)万美元
1.1.2 目标分析
本案例的目标是通过 线性回归模型 预测区域的房价中位数(Target),并评估模型的性能。同时,尝试通过特征工程和正则化方法改进模型效果。
In [1]
# 导入必要库import paddlefrom sklearn.datasets import fetch_california_housingimport pandas as pd# 加载加州房价数据集housing = fetch_california_housing(as_frame=True)# 转换为DataFramedata = housing.frame# 显示数据集基本信息print("数据集基本信息:")print(data.info())# 显示前几行样本print("数据集示例:")print(data.head())# 查看目标变量分布print("目标变量分布(房价中位数):")print(data['MedHouseVal'].describe())
/opt/conda/envs/python35-paddle120-env/lib/python3.10/site-packages/paddle/utils/cpp_extension/extension_utils.py:686: UserWarning: No ccache found. Please be aware that recompiling all source files may be required. You can download and install ccache from: https://github.com/ccache/ccache/blob/master/doc/INSTALL.md warnings.warn(warning_message)
数据集基本信息:RangeIndex: 20640 entries, 0 to 20639Data columns (total 9 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 MedInc 20640 non-null float64 1 HouseAge 20640 non-null float64 2 AveRooms 20640 non-null float64 3 AveBedrms 20640 non-null float64 4 Population 20640 non-null float64 5 AveOccup 20640 non-null float64 6 Latitude 20640 non-null float64 7 Longitude 20640 non-null float64 8 MedHouseVal 20640 non-null float64dtypes: float64(9)memory usage: 1.4 MBNone数据集示例: MedInc HouseAge AveRooms AveBedrms Population AveOccup Latitude 8.3252 41.0 6.984127 1.023810 322.0 2.555556 37.88 1 8.3014 21.0 6.238137 0.971880 2401.0 2.109842 37.86 2 7.2574 52.0 8.288136 1.073446 496.0 2.802260 37.85 3 5.6431 52.0 5.817352 1.073059 558.0 2.547945 37.85 4 3.8462 52.0 6.281853 1.081081 565.0 2.181467 37.85 Longitude MedHouseVal 0 -122.23 4.526 1 -122.22 3.585 2 -122.24 3.521 3 -122.25 3.413 4 -122.25 3.422 目标变量分布(房价中位数):count 20640.000000mean 2.068558std 1.153956min 0.14999025% 1.19600050% 1.79700075% 2.647250max 5.000010Name: MedHouseVal, dtype: float64
1.2 数据清洗与可视化分析
在机器学习任务中,数据清洗是模型构建的基础环节,目的是处理缺失值、异常值等可能影响模型性能的问题,同时通过可视化分析数据的分布和特征之间的关系。
1.2.1 数据清洗
(1) 缺失值分析
缺失值可能会导致模型无法正常训练。我们需要检查数据集是否存在缺失值,并采取适当措施进行填补或移除。
In [2]
# 检查缺失值missing_values = data.isnull().sum()print("各列缺失值情况:n", missing_values)# 若存在缺失值,可以选择删除或填充# 删除缺失值样本cleaned_data = data.dropna()# 或者使用均值填充filled_data = data.fillna(data.mean())print("清洗后的数据集基本信息:")print(cleaned_data.info())
各列缺失值情况: MedInc 0HouseAge 0AveRooms 0AveBedrms 0Population 0AveOccup 0Latitude 0Longitude 0MedHouseVal 0dtype: int64清洗后的数据集基本信息:RangeIndex: 20640 entries, 0 to 20639Data columns (total 9 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 MedInc 20640 non-null float64 1 HouseAge 20640 non-null float64 2 AveRooms 20640 non-null float64 3 AveBedrms 20640 non-null float64 4 Population 20640 non-null float64 5 AveOccup 20640 non-null float64 6 Latitude 20640 non-null float64 7 Longitude 20640 non-null float64 8 MedHouseVal 20640 non-null float64dtypes: float64(9)memory usage: 1.4 MBNone
(2) 异常值处理
异常值是指数据中明显偏离常规范围的值。我们通常通过统计方法或可视化工具(如箱线图)来检测异常值。
IQR方法:利用四分位数间距(IQR)检测异常值。
异常值范围:小于 (Q1−1.5×IQR)(Q1−1.5×IQR) 或大于 (Q3+1.5×IQR)(Q3+1.5×IQR) 的值被视为异常值。
In [3]
# 统计每列的上下四分位数和异常值范围for col in data.columns[:-1]: # 遍历所有特征列 Q1 = data[col].quantile(0.25) Q3 = data[col].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR # 过滤异常值 outliers = data[(data[col] upper_bound)] print(f"{col} 列异常值数量:{len(outliers)}") # 去除异常值(可选) data = data[(data[col] >= lower_bound) & (data[col] <= upper_bound)]
MedInc 列异常值数量:681HouseAge 列异常值数量:0AveRooms 列异常值数量:439AveBedrms 列异常值数量:1116Population 列异常值数量:1063AveOccup 列异常值数量:528Latitude 列异常值数量:0Longitude 列异常值数量:0
1.3 数据集特征的箱线图可视化
箱线图是一种直观的统计图表,用于展示数据分布及检测异常值。通过绘制箱线图,我们可以快速发现数据的分布情况和离群点
通过箱线图,我们可以观察: 1. 各特征值的分布范围及中心位置。 2. 是否存在显著的离群点。 3. 不同特征的量级差异。
In [4]
!pip install seaborn
Looking in indexes: https://mirror.baidu.com/pypi/simple/, https://mirrors.aliyun.com/pypi/simple/Collecting seaborn Downloading https://mirrors.aliyun.com/pypi/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl (294 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 294.9/294.9 kB 7.5 MB/s eta 0:00:00a 0:00:01Requirement already satisfied: numpy!=1.24.0,>=1.20 in /opt/conda/envs/python35-paddle120-env/lib/python3.10/site-packages (from seaborn) (1.26.4)Requirement already satisfied: pandas>=1.2 in /opt/conda/envs/python35-paddle120-env/lib/python3.10/site-packages (from seaborn) (2.2.3)Requirement already satisfied: matplotlib!=3.6.1,>=3.4 in /opt/conda/envs/python35-paddle120-env/lib/python3.10/site-packages (from seaborn) (3.9.2)Requirement already satisfied: contourpy>=1.0.1 in /opt/conda/envs/python35-paddle120-env/lib/python3.10/site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (1.3.0)Requirement already satisfied: cycler>=0.10 in /opt/conda/envs/python35-paddle120-env/lib/python3.10/site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (0.12.1)Requirement already satisfied: fonttools>=4.22.0 in /opt/conda/envs/python35-paddle120-env/lib/python3.10/site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (4.54.1)Requirement already satisfied: kiwisolver>=1.3.1 in /opt/conda/envs/python35-paddle120-env/lib/python3.10/site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (1.4.7)Requirement already satisfied: packaging>=20.0 in /opt/conda/envs/python35-paddle120-env/lib/python3.10/site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (24.1)Requirement already satisfied: pillow>=8 in /opt/conda/envs/python35-paddle120-env/lib/python3.10/site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (10.4.0)Requirement already satisfied: pyparsing>=2.3.1 in /opt/conda/envs/python35-paddle120-env/lib/python3.10/site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (3.2.0)Requirement already satisfied: python-dateutil>=2.7 in /opt/conda/envs/python35-paddle120-env/lib/python3.10/site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (2.9.0.post0)Requirement already satisfied: pytz>=2020.1 in /opt/conda/envs/python35-paddle120-env/lib/python3.10/site-packages (from pandas>=1.2->seaborn) (2024.2)Requirement already satisfied: tzdata>=2022.7 in /opt/conda/envs/python35-paddle120-env/lib/python3.10/site-packages (from pandas>=1.2->seaborn) (2024.2)Requirement already satisfied: six>=1.5 in /opt/conda/envs/python35-paddle120-env/lib/python3.10/site-packages (from python-dateutil>=2.7->matplotlib!=3.6.1,>=3.4->seaborn) (1.16.0)Installing collected packages: seabornSuccessfully installed seaborn-0.13.2
In [5]
import matplotlib.pyplot as pltimport seaborn as sns# 设置画图风格sns.set(style="whitegrid")# 选择部分特征绘制箱线图selected_features = ['MedInc', 'HouseAge', 'AveRooms', 'AveBedrms', 'Population']# 绘制箱线图plt.figure(figsize=(12, 6))sns.boxplot(data=data[selected_features])plt.title("Boxplot of dataset feature.", fontsize=16)plt.xticks(fontsize=12)plt.ylabel("value", fontsize=12)plt.xlabel("feature", fontsize=12)plt.show()
1.4 数据特征归一化
在机器学习任务中,特征归一化 是特征工程的重要步骤。其目的是将不同量纲的特征值映射到相同的尺度范围,从而提高模型的训练效率和性能,尤其是在梯度下降算法和距离相关的模型(如线性回归、逻辑回归和支持向量机)中。
1.4.1 为什么需要特征归一化?
现实数据中,不同特征的取值范围可能存在巨大差异。例如:
收入中位数(MedInc):范围可能为 [0, 15]。人口数量(Population):范围可能为 [500, 40,000]。经纬度(Latitude 和 Longitude):范围为 [30, 50] 和 [-125, -115]。
这种差异会导致以下问题:
梯度更新不均衡:梯度下降时,不同特征的梯度大小会不一致,可能导致模型难以收敛或收敛速度过慢。模型偏向问题:未归一化的特征可能对模型决策产生不均衡的影响,例如范围较大的特征可能主导模型输出。优化器敏感性:大多数优化算法(如SGD)对特征的尺度变化较为敏感。
1.4.2 常见的特征归一化方法
(1) 最小-最大归一化(Min-Max Scaling)
将特征值线性缩放到指定范围(通常为 [0, 1] 或 [-1, 1])。
公式:
[x′=x−min(x)max(x)−min(x)][x′=max(x)−min(x)x−min(x)]
优点:
保持数据分布形状不变。对特征值范围的约束性强。
适用场景:
特征范围已知,且分布无显著异常值。
(2) 标准化(Standardization)
将特征值转化为标准正态分布(均值为0,标准差为1)。
公式: [x′=x−μσ][x′=σx−μ] 其中,(μ)(μ) 为均值,(σ(σ) 为标准差。
优点:
适合处理有偏分布数据。对异常值较为鲁棒。
适用场景:
特征范围未知,且数据分布可能存在不同量级。
(3) 对数缩放(Log Scaling)
对数变换将特征值压缩到较小范围。
公式: [x′=log(x+1)][x′=log(x+1)]
适用场景:
特征值存在长尾分布,且非负。
(4) 归一化的对比
Min-Max归一化不改变分布易受异常值影响特征范围已知标准化转换为标准分布鲁棒特征范围未知,分布不均对数缩放减少长尾效应易受负值影响存在长尾分布In [10]
# # 使用最大最小归一化import numpy as npfrom sklearn.model_selection import train_test_split# 提取特征与目标变量features = data.iloc[:, :-1].valuestarget = data.iloc[:, -1].values# # 最小-最大归一化实现# def min_max_scaling(features):# min_vals = np.min(features, axis=0)# max_vals = np.max(features, axis=0)# return (features - min_vals) / (max_vals - min_vals)# # # 应用归一化# # normalized_features = min_max_scaling(features)# # 数据集划分# X_train, X_test, y_train, y_test = train_test_split(normalized_features, target, test_size=0.2, random_state=42)
In [11]
# 标准化from sklearn.preprocessing import StandardScaler# 标准化实现scaler = StandardScaler()standardized_features = scaler.fit_transform(features)# 数据集划分X_train, X_test, y_train, y_test = train_test_split(standardized_features, target, test_size=0.2, random_state=42)
In [12]
import paddleimport paddle.nn.functional as F# 将数据转换为Paddle张量X_train_tensor = paddle.to_tensor(X_train, dtype='float32')X_test_tensor = paddle.to_tensor(X_test, dtype='float32')# 使用Paddle实现标准化mean = paddle.mean(X_train_tensor, axis=0)std = paddle.std(X_train_tensor, axis=0)X_train_normalized = (X_train_tensor - mean) / stdX_test_normalized = (X_test_tensor - mean) / stdprint("标准化后的训练数据:", X_train_normalized.numpy()[:5])
标准化后的训练数据: [[-0.5087326 1.4234632 0.12109426 0.05857366 -0.6497711 0.15639436 1.0031793 -1.2958153 ] [ 1.0481998 -0.8750934 0.9622561 2.2335987 -0.49749768 0.10866082 0.655153 -1.1851951 ] [ 0.66350424 -0.8750934 -0.05120656 0.2758133 -0.42929187 -0.79060733 0.7539172 -1.1751386 ] [ 2.9235477 0.76673275 2.0087693 -0.01296391 0.00373571 -0.74548185 -0.72284335 0.6651809 ] [-1.147807 0.5204589 -0.953951 0.690019 -0.93687 -0.79818255 -0.4500659 0.75066024]]
W0108 10:22:09.703733 294 dygraph_functions.cc:83253] got different data type, run type promotion automatically, this may cause data type been changed.
(5)可视化归一化效果
In [13]
import matplotlib.pyplot as pltplt.figure(figsize=(12, 6))plt.boxplot(features, labels=data.columns[:-1])plt.title("Feature distribution before normalization") # 归一化前特征分布plt.xticks(rotation=45)plt.show()
/tmp/ipykernel_294/2547550394.py:4: MatplotlibDeprecationWarning: The 'labels' parameter of boxplot() has been renamed 'tick_labels' since Matplotlib 3.9; support for the old name will be dropped in 3.11. plt.boxplot(features, labels=data.columns[:-1])
In [15]
plt.figure(figsize=(12, 6))plt.boxplot(standardized_features)plt.title("Feature distribution after normalization") # 归一化后特征分布(Min-Max)plt.show()
1.5 模型构建
在完成数据处理后,我们开始进入模型构建部分。本文将使用 PaddlePaddle 框架来自定义一个线性回归模型,并通过最小二乘法的解析解直接求解模型参数,从而避免复杂的迭代训练过程。
1.5.1 自定义 Linear 算子
PaddlePaddle 提供了灵活的算子设计接口,可以通过继承 paddle.nn.Layer 来定义自定义算子。我们首先构建一个简单的线性算子。
In [16]
import paddleimport paddle.nn as nnimport paddle.nn.functional as F# 自定义Linear算子class Linear(nn.Layer): def __init__(self, in_features, out_features): super(Linear, self).__init__() # 初始化权重和偏置 self.weight = self.create_parameter( shape=[in_features, out_features], default_initializer=nn.initializer.XavierUniform()) self.bias = self.create_parameter( shape=[out_features], default_initializer=nn.initializer.Constant(0.0)) def forward(self, x): # 线性变换公式:y = Wx + b return paddle.matmul(x, self.weight) + self.bias
1.5.2 定义 Runner 类
Runner 类是模型训练的核心封装,我们将其用于配置模型、损失函数、优化器,以及完成模型的训练、评价和预测任务。
In [17]
# Runner类定义class Runner: def __init__(self, input_dim, regularization_lambda=0.01): """ 初始化线性回归模型以及其他必需的参数 """ self.model = Linear(in_features=input_dim, out_features=1) self.loss_fn = F.mse_loss # 均方误差损失 self.optimizer = paddle.optimizer.SGD(parameters=self.model.parameters(), learning_rate=0.01) self.regularization_lambda = regularization_lambda # L2正则化项 def prepare_model(self, X_train, y_train): """ 使用正规方程(带L2正则化)来求解模型的权重和偏置 """ # 转换为Paddle张量 X_train_tensor = paddle.to_tensor(X_train, dtype='float32') y_train_tensor = paddle.to_tensor(y_train, dtype='float32') # 添加偏置项到 X_train X_train_with_bias = paddle.concat([X_train_tensor, paddle.ones([X_train_tensor.shape[0], 1], dtype='float32')], axis=1) # 正规方程解析解:W = (X^T X + λI)^-1 X^T y X_transpose = paddle.transpose(X_train_with_bias, perm=[1, 0]) # 加上正则化项 (λI) identity_matrix = paddle.eye(X_train_with_bias.shape[1]) regularization_matrix = self.regularization_lambda * identity_matrix # 计算正规方程解 weights = paddle.matmul( paddle.inverse(paddle.matmul(X_transpose, X_train_with_bias) + regularization_matrix), paddle.matmul(X_transpose, y_train_tensor) ) # 拆分权重和偏置 self.model.weight.set_value(weights[:-1].reshape([X_train.shape[1], 1])) # 权重调整为二维张量 self.model.bias.set_value(weights[-1:].reshape([1])) # 偏置调整为一维张量 print("解析解求得的模型参数:") print("权重:", self.model.weight.numpy()) print("偏置:", self.model.bias.numpy()) def evaluate_model(self, X_test, y_test): """ 使用测试集进行模型评价 """ X_test_tensor = paddle.to_tensor(X_test, dtype='float32') y_test_tensor = paddle.to_tensor(y_test, dtype='float32') # 进行预测 predictions = self.model(X_test_tensor) loss = self.loss_fn(predictions, y_test_tensor) print("模型评价 - 测试集均方误差:", loss.numpy()) return predictions.numpy() def predict(self, X_new): """ 对新数据进行预测 """ X_new_tensor = paddle.to_tensor(X_new, dtype='float32') predictions = self.model(X_new_tensor) return predictions.numpy()
1.5.3 使用 Runner 类来构建线性回归模型并完成训练、评价和预测。
In [18]
# 加载数据X_train, X_test, y_train, y_test = X_train_normalized, X_test_normalized, y_train, y_test# 创建 Runner 实例runner = Runner(input_dim=X_train.shape[1])# 使用最小二乘法解析解求解模型参数runner.prepare_model(X_train, y_train)# 评价模型predictions = runner.evaluate_model(X_test, y_test)# 对新数据进行预测X_new = X_test[:5] # 假设我们使用测试集的前5条数据进行预测predicted_prices = runner.predict(X_new)print("预测结果(前5条):", predicted_prices)
解析解求得的模型参数:权重: [[ 0.7456653 ] [ 0.16260225] [-0.16771364] [ 0.1250522 ] [ 0.04790141] [-0.27607393] [-0.8478774 ] [-0.7796167 ]]偏置: [2.025095]模型评价 - 测试集均方误差: 1.9318573预测结果(前5条): [[1.5605581] [1.4985472] [1.6648219] [1.4926147] [1.280458 ]]
二、线性分类
在前边的学习过程中,通过对线性回归的学习,为我们提供了一种直观且基础的预测模型。通过将输入特征与目标变量之间的关系表示为一个线性方程,线性回归帮助我们理解了如何在多维空间中找到最优的拟合直线。然而,除了回归问题,分类问题也是机器学习中的重要领域。在许多应用场景中,我们需要根据输入数据的特征对样本进行分类。此时,线性分类模型成为了一种非常有效的选择。
与线性回归模型相似,线性分类模型通过线性方程来决定样本所属的类别。然而,线性分类的目标是将数据点分割成不同的类别,而不是预测一个连续的数值。接下来,我们将介绍线性分类的定义及其核心思想,并展示如何使用机器学习方法进行分类。
线性分类是一种基于线性模型的分类方法,旨在通过一个线性决策边界将样本分为不同的类别。其基本思想是利用线性函数来对数据进行分割,从而使得数据能够被映射到不同的类别标签。
具体来说,给定一个输入数据点 (x∈Rn)(x∈Rn),线性分类模型试图通过一个线性方程进行分类:
[y=sign(w1x1+w2x2+⋯+wnxn+b)][y=sign(w1x1+w2x2+⋯+wnxn+b)]
其中:
(w1,w2,…,wn)(w1,w2,…,wn) 是模型的权重,表示每个特征的重要性。(x1,x2,…,xn)(x1,x2,…,xn) 是输入特征。(b)(b) 是偏置项,用于调整决策边界的位置。(y)(y) 是模型的输出,它表示预测的类别,通常取值为 (±1)(±1) 或 0 和 1。
在训练过程中,线性分类器的目标是找到最佳的权重 (w)(w) 和偏置 (b)(b),使得决策边界能够最有效地将不同类别的样本分开。
与回归问题不同,线性分类模型的输出通常是一个类别标签,而不是一个连续的数值。根据训练数据的不同,线性分类器会调整其参数,使得分类边界最大程度地正确地划分样本。
常见的线性分类模型
常见的线性分类模型包括:
感知机(Perceptron):一种最简单的线性分类模型,基于梯度下降方法,通过不断调整权重来找到分割不同类别的超平面。支持向量机(SVM):通过最大化类别之间的间隔来寻找最优超平面,从而提高分类精度。逻辑回归(Logistic Regression):尽管名字中包含“回归”,它是一种广泛使用的线性分类模型,输出的是概率值,经过Sigmoid激活函数后用于二分类问题。Softmax回归: 解决了二分类问题中逻辑回归的局限,能够处理多个类别的分类问题。在Softmax回归中,我们通过一个线性模型计算每个类别的得分,然后通过Softmax函数将这些得分转换为概率,最终输出每个类别的预测概率
2.1 介绍 Logistic 回归
Logistic 回归(Logistic Regression)是一种广泛使用的统计模型,尽管其名称中带有“回归”二字,但它实际上是一种分类模型,主要用于解决二分类问题。Logistic 回归的核心思想是通过一个线性模型预测数据属于某一类别的概率。
2.1 Logistic 回归模型
在二分类问题中,我们通过线性模型计算一个实数值,然后使用Sigmoid函数(也称为Logistic函数)将其转换为一个0到1之间的概率值,表示数据属于某一类别的可能性。
假设输入特征为 (x∈Rn)(x∈Rn),模型的输出为类别 (y∈{0,1})(y∈{0,1}),则 Logistic 回归模型的预测概率为:
[P(y=1∣x)=σ(wTx+b)][P(y=1∣x)=σ(wTx+b)]
其中:
$( w $) 是特征的权重,$( x $) 是输入特征向量,$( b $) 是偏置项,$( sigma $) 是 Sigmoid 函数,它将线性函数的输出转换为概率。
2.2 相关的激活函数
Logistic 回归中使用的核心激活函数是 Sigmoid 函数,它的数学表达式为:
[σ(z)=11+e−z][σ(z)=1+e−z1]
Sigmoid 函数的作用是将输入值 ( z ) 映射到区间 ( (0, 1) ),这使得它非常适合用来表示概率。对于输入值 ( z ) 较大时,Sigmoid 输出接近 1,而当输入值较小时,输出接近 0。
Sigmoid 函数的特性
当 (z→+∞)(z→+∞),(σ(z)→1)(σ(z)→1)。当 (z→−∞)(z→−∞),(σ(z)→0)(σ(z)→0)。Sigmoid 函数是一个S型曲线,具有平滑的导数,适合用于梯度下降优化。
2.3. Logistic 函数实现代码
使用PaddlePaddle框架,实现一个简单的 Logistic 回归模型的实现代码
Logistic 函数实现了标准的 Sigmoid 激活函数 (σ(z)=11+e−z)(σ(z)=1+e−z1)
使用 paddle.linspace 生成从 -10 到 10 的等间距数值,并将这些值输入到 Logistic 函数中,计算其对应的输出值。
In [19]
import paddle# 定义 Logistic 函数def Logistic(x): return 1 / (1 + paddle.exp(-x))# 创建一组从 -10 到 10 的数值x = paddle.linspace(-10, 10, 10000)# 计算 Logistic 函数值y = Logistic(x)# 打印结果的前几项print(y[:10]) # 输出前10项# 可视化 Logistic 函数import matplotlib.pyplot as pltplt.plot(x.tolist(), y.tolist(),color = 'red')plt.title('Sigmoid Function (Logistic)')plt.xlabel('z')plt.ylabel('σ(z)')plt.grid(True)plt.show()
Tensor(shape=[10], dtype=float32, place=Place(cpu), stop_gradient=True, [0.00004540, 0.00004549, 0.00004558, 0.00004567, 0.00004576, 0.00004585, 0.00004595, 0.00004604, 0.00004613, 0.00004622])
函数解析
X轴:表示线性模型的输出 (z=wTx+b)(z=wTx+b)Y轴:表示通过 Sigmoid 激活函数得到的概率值 (σ(z))(σ(z))。当 ( z ) 较大时,Sigmoid 输出接近 1,表示类别1的概率很高。当 ( z ) 较小时,Sigmoid 输出接近 0,表示类别0的概率很高。在 ( z = 0 ) 时,Sigmoid 输出为 0.5,表示类别1和类别0的概率相等。
2.4 基于Logistic实现Moon1000数据集回归测试
我们将构建一个简单的二分类数据集,使用的是 Moon1000 数据集,这是一个由两个半月形状的数据集,常用于测试二分类模型的性能。
make_moons:我们生成了 1000 条带有噪声(noise=0.1)的二分类数据,形成了两个半月形状的数据集。
StandardScaler:对数据进行标准化处理,使其均值为 0,标准差为 1,这有助于提高模型训练的效果。
plt.scatter:通过不同的颜色和标记(Class 0 用蓝色,Class 1 用红色)绘制两类数据点。
In [20]
# 使用 `sklearn.datasets.make_moons` 来生成一个二分类的数据集。随机抽取其中的 1000 个样本,import numpy as npimport matplotlib.pyplot as pltfrom sklearn.datasets import make_moonsfrom sklearn.preprocessing import StandardScaler# 生成 Moon 数据集X, y = make_moons(n_samples=1000, noise=0.1, random_state=42)# 数据标准化scaler = StandardScaler()X_scaled = scaler.fit_transform(X)# 可视化数据分布plt.figure(figsize=(8, 6))plt.scatter(X_scaled[y == 0][:, 0], X_scaled[y == 0][:, 1], color='b', label='Class 0', s=10)plt.scatter(X_scaled[y == 1][:, 0], X_scaled[y == 1][:, 1], color='r', label='Class 1', s=10)plt.title('Moon1000 Dataset Visualization')plt.xlabel('Feature 1')plt.ylabel('Feature 2')plt.legend()plt.grid(True)plt.show()
In [21]
# 拆分数据集(训练集、验证集、测试集)from sklearn.model_selection import train_test_split# 第一次拆分:将数据集分为训练集(60%)和临时集(40%)X_train, X_temp, y_train, y_temp = train_test_split(X_scaled, y, test_size=0.4, random_state=42)# 第二次拆分:将临时集分为验证集(20%)和测试集(20%)X_val, X_test, y_val, y_test = train_test_split(X_temp, y_temp, test_size=0.5, random_state=42)# 打印数据集形状print(f"训练集特征 X_train 形状: {X_train.shape}")print(f"验证集特征 X_val 形状: {X_val.shape}")print(f"测试集特征 X_test 形状: {X_test.shape}")print(f"训练集标签 y_train 形状: {y_train.shape}")print(f"验证集标签 y_val 形状: {y_val.shape}")print(f"测试集标签 y_test 形状: {y_test.shape}")
训练集特征 X_train 形状: (600, 2)验证集特征 X_val 形状: (200, 2)测试集特征 X_test 形状: (200, 2)训练集标签 y_train 形状: (600,)验证集标签 y_val 形状: (200,)测试集标签 y_test 形状: (200,)
2.5 构建 Logistic 回归模型
In [22]
import paddleimport paddle.nn as nnimport paddle.optimizer as optimimport paddle.nn.functional as F# 自定义 Logistic 回归模型(仅用 paddle.matmul)class LogisticRegression(nn.Layer): def __init__(self, input_dim): super(LogisticRegression, self).__init__() # 定义线性层 self.params = {} self.params['w'] = paddle.zeros([input_dim, 1]) self.params['b'] = paddle.zeros(shape = [1]) self.fc = nn.Linear(input_dim, 1) def forward(self, x): # 使用 paddle.matmul 计算加权和 logits = paddle.matmul(x, self.params['w']) + self.params['b'] outputs = Logistic(logits) return outputs # 这里没有使用激活函数 sigmoid
In [23]
paddle.seed(0)input = paddle.randn([3, 4])print(input)model = LogisticRegression(4)output = model(input)print(output)
Tensor(shape=[3, 4], dtype=float32, place=Place(cpu), stop_gradient=True, [[-0.75711036, -0.38059190, 0.10946669, 1.34467661], [-0.84002435, -1.27341712, 2.47224617, 0.14070207], [ 0.60608417, 0.23396523, 1.35604191, 0.10350471]])Tensor(shape=[3, 1], dtype=float32, place=Place(cpu), stop_gradient=True, [[0.50000000], [0.50000000], [0.50000000]])
2.6 损失函数介绍
损失函数(Loss Function),也叫代价函数(Cost Function),在机器学习中是评估模型预测结果与真实结果之间差异的一个函数。损失函数的主要作用是通过量化模型的预测误差来引导优化过程,最终帮助模型调整参数,使得预测结果更加精确。
1. 损失函数的定义与作用
损失函数是一个数学函数,用来衡量模型的预测输出与真实标签之间的差异。其核心作用是:
量化预测误差用于优化算法的目标函数引导模型参数的调整
通常,损失函数的值越小,表示模型的预测结果与真实值的差距越小,因此,优化算法的目标就是通过不断最小化损失函数来训练模型。
2. 损失函数的公式
在不同的任务中,损失函数有所不同。常见的损失函数包括:
均方误差(Mean Squared Error,MSE):用于回归问题,公式如下:
L(y,y^)=1N∑i=1N(yi−y^i)2L(y,y^)=N1i=1∑N(yi−y^i)2
其中,yiyi 为真实值,y^iy^i 为预测值,NN 为样本数量。
交叉熵(Cross-Entropy):用于分类问题,特别是二分类和多分类任务,公式如下:
对于二分类,交叉熵损失函数可以写为:
L(y,y^)=−1N∑i=1N[yilog(y^i)+(1−yi)log(1−y^i)]L(y,y^)=−N1i=1∑N[yilog(y^i)+(1−yi)log(1−y^i)]
对于多分类,交叉熵损失函数可以写为:
L(y,y^)=−∑i=1Cyilog(y^i)L(y,y^)=−i=1∑Cyilog(y^i)
其中,yiyi 是实际类别标签(通常是一个 one-hot 向量),y^iy^i 是预测的类别概率,CC 是类别数。
3. 交叉熵损失函数
交叉熵损失函数主要用于分类问题,尤其是在处理二分类和多分类任务时广泛应用。交叉熵的基本思想是量化两个概率分布之间的差异,通常用于衡量模型输出的概率分布与实际分布之间的差距。
二分类交叉熵:当标签是二分类时,交叉熵损失函数用于度量二进制分类模型输出的概率和真实标签之间的差异。
多分类交叉熵:对于多分类问题,交叉熵损失会计算所有类别的概率差异,并选择具有最大概率的类别作为模型的输出。
4. 构建交叉熵损失函数类代码
以下是一个简单的交叉熵损失函数实现,代码使用了 PaddlePaddle 框架:
In [24]
import paddleimport paddle.nn as nnclass CrossEntropyLoss(nn.Layer): def __init__(self): super(CrossEntropyLoss, self).__init__() def forward(self, logits, labels): """ 计算交叉熵损失 :param logits: 预测值(通常是模型的输出,没有经过 softmax 的原始值) :param labels: 真实标签,通常是 one-hot 编码 :return: 交叉熵损失 """ # 使用 PaddlePaddle 的函数计算交叉熵损失 loss = nn.functional.cross_entropy(logits, labels) return loss# 示例:使用自定义交叉熵损失函数logits = paddle.to_tensor([[1.2, 0.3, -0.5], [0.7, 1.4, -1.0]], dtype='float32') # 模拟模型输出labels = paddle.to_tensor([0, 1], dtype='int64') # 假设有两个样本,标签分别为类别 0 和类别 1loss_fn = CrossEntropyLoss()loss = loss_fn(logits, labels)print("交叉熵损失:", loss.numpy())
交叉熵损失: 0.46265036
2.7 回归模型优化 – 梯度优化
梯度优化(Gradient Optimization)是机器学习中最常用的优化方法之一,它基于梯度下降算法(Gradient Descent)来通过调整模型的参数,最小化损失函数,从而提高模型的预测性能。在训练过程中,模型的参数通过计算梯度并朝着损失函数的最小值方向进行更新。
1. 梯度计算
梯度计算是优化算法的核心步骤,它用于计算损失函数相对于模型参数的偏导数。梯度描述了损失函数在某个点上的变化率,并指示了如何调整参数以减少损失。
损失函数:在逻辑回归中,常用的损失函数是交叉熵损失函数,计算的是模型预测与实际标签之间的差异。偏导数:偏导数是衡量一个参数对损失函数变化的敏感程度,即参数变化时,损失函数的变化量。计算每个参数的偏导数可以告诉我们如何调整该参数以减少损失。
2. 偏导数计算
对于逻辑回归的交叉熵损失函数,损失函数可以表示为:
L(y,y^)=−(ylog(y^)+(1−y)log(1−y^))L(y,y^)=−(ylog(y^)+(1−y)log(1−y^))
其中,y^=σ(Wx+b)y^=σ(Wx+b) 为模型的预测输出,yy 为实际标签,WW 为权重,bb 为偏置,σσ 为Sigmoid激活函数。
为了优化权重和偏置,我们需要计算损失函数相对于模型参数(权重 WW 和偏置 bb)的偏导数。通过链式法则,我们可以得到每个参数的梯度:
对权重 WW 的梯度:
∂L∂W=1N∑i=1N(y^i−yi)xi∂W∂L=N1i=1∑N(y^i−yi)xi
对偏置 bb 的梯度:
∂L∂b=1N∑i=1N(y^i−yi)∂b∂L=N1i=1∑N(y^i−yi)
3. Backward函数的实现
为了在逻辑回归模型中实现梯度计算,我们需要增加一个 backward 函数来计算损失函数的梯度。该函数将根据模型的输入数据和实际标签计算出每个参数的梯度,并将其存储在模型的 grads 属性中。
In [25]
import paddleimport paddle.nn as nn# 自定义 Logistic 回归模型(仅用 paddle.matmul)class LogisticRegression(nn.Layer): def __init__(self, input_dim): super(LogisticRegression, self).__init__() # 定义线性层 self.params = {} self.params['w'] = paddle.zeros([input_dim, 1]) # 初始化权重 self.params['b'] = paddle.zeros(shape=[1]) # 初始化偏置 self.fc = nn.Linear(input_dim, 1) def forward(self, x): # 使用 paddle.matmul 计算加权和 logits = paddle.matmul(x, self.params['w']) + self.params['b'] outputs = self.sigmoid(logits) return outputs def sigmoid(self, x): # Sigmoid 激活函数 return 1 / (1 + paddle.exp(-x)) def backward(self, x, y): """ 计算损失函数对模型参数的梯度,并将其存放在 grads 属性中。 :param x: 输入特征 :param y: 真实标签 :return: None """ # 计算预测输出 logits = paddle.matmul(x, self.params['w']) + self.params['b'] y_pred = self.sigmoid(logits) # 计算损失函数对权重的偏导数 dw = paddle.matmul(paddle.transpose(x, [1, 0]), (y_pred - y)) / x.shape[0] db = paddle.sum(y_pred - y) / x.shape[0] # 将梯度存放在 grads 属性中 self.grads = {} self.grads['w'] = dw self.grads['b'] = db # 输出计算的梯度 print("梯度 w:", dw.numpy()) print("梯度 b:", db.numpy())
In [26]
# 使用 backward 函数进行梯度计算# 模型创建与输入数据生成input_dim = 2model = LogisticRegression(input_dim)# 随机生成输入数据和标签x_sample = paddle.to_tensor([[0.5, 1.0], [1.5, -1.0], [1.0, 2.0]], dtype='float32')y_sample = paddle.to_tensor([[1], [0], [1]], dtype='float32')# 前向计算outputs = model(x_sample)print("模型输出:", outputs.numpy())# 计算梯度model.backward(x_sample, y_sample)# 查看存储的梯度print("存储在 grads 中的权重梯度:", model.grads['w'].numpy())print("存储在 grads 中的偏置梯度:", model.grads['b'].numpy())
模型输出: [[0.5] [0.5] [0.5]]梯度 w: [[ 0. ] [-0.6666667]]梯度 b: -0.16666667存储在 grads 中的权重梯度: [[ 0. ] [-0.6666667]]存储在 grads 中的偏置梯度: -0.16666667
2.8 模型优化 – 设计优化器
在模型训练过程中,优化器的作用是根据计算得到的梯度来调整模型的参数,逐步最小化损失函数,从而提高模型的预测能力。常见的优化算法有梯度下降(Gradient Descent)及其变种,如随机梯度下降(SGD)、Adam等。
1. 梯度下降法(Gradient Descent)
梯度下降法是最基本的优化算法,主要思想是通过计算损失函数的梯度,并按梯度的反方向更新参数,以便使损失函数逐步减小,直至收敛。
梯度下降的更新公式
对于一个模型的参数 θθ,在每一次迭代中,梯度下降法通过以下公式更新参数:
θ:=θ−η∂L∂θθ:=θ−η∂θ∂L
其中:
θθ 是模型的参数(例如,权重 WW 和偏置 bb)。ηη 是学习率(learning rate),控制着每次更新的步长。∂L∂θ∂θ∂L 是损失函数 LL 关于参数 θθ 的梯度。
计算梯度并进行更新
假设我们有一个模型 f(x,θ)f(x,θ),其损失函数为 L(y,f(x,θ))L(y,f(x,θ)),我们首先通过反向传播计算出模型的梯度。然后使用梯度下降算法,通过以下步骤更新参数:
计算损失函数 LL 对于模型参数 θθ 的梯度 ∂L∂θ∂θ∂L。
根据梯度更新模型参数:
θnew=θ−η⋅∂L∂θθnew=θ−η⋅∂θ∂L
在每次迭代中,参数会沿着梯度的反方向进行调整。
2. 优化器设计 – 自定义梯度下降
在设计优化器时,我们需要使用梯度下降法来更新模型参数。假设我们有一个简单的逻辑回归模型,已经通过 backward 函数计算了梯度,接下来,我们使用优化器来更新模型的权重和偏置。
In [27]
import paddleimport paddle.nn as nn# 自定义 Logistic 回归模型(仅用 paddle.matmul)class LogisticRegression(nn.Layer): def __init__(self, input_dim): super(LogisticRegression, self).__init__() # 定义线性层 self.params = {} self.params['w'] = paddle.zeros([input_dim, 1]) # 初始化权重 self.params['b'] = paddle.zeros(shape=[1]) # 初始化偏置 self.fc = nn.Linear(input_dim, 1) def forward(self, x): # 使用 paddle.matmul 计算加权和 logits = paddle.matmul(x, self.params['w']) + self.params['b'] outputs = self.sigmoid(logits) return outputs def sigmoid(self, x): # Sigmoid 激活函数 return 1 / (1 + paddle.exp(-x)) def backward(self, x, y): """ 计算损失函数对模型参数的梯度,并将其存放在 grads 属性中。 :param x: 输入特征 :param y: 真实标签 :return: None """ # 计算预测输出 logits = paddle.matmul(x, self.params['w']) + self.params['b'] y_pred = self.sigmoid(logits) # 计算损失函数对权重的偏导数 dw = paddle.matmul(paddle.transpose(x, [1, 0]), (y_pred - y)) / x.shape[0] db = paddle.sum(y_pred - y) / x.shape[0] # 将梯度存放在 grads 属性中 self.grads = {} self.grads['w'] = dw self.grads['b'] = db # 输出计算的梯度 # print("梯度 w:", dw.numpy()) # print("梯度 b:", db.numpy()) def update_params(self, lr=0.01): """ 使用梯度下降法更新模型的权重和偏置 :param lr: 学习率 :return: None """ # 更新权重和偏置 self.params['w'] = self.params['w'] - lr * self.grads['w'] self.params['b'] = self.params['b'] - lr * self.grads['b'] # print("更新后的权重 w:", self.params['w'].numpy()) # print("更新后的偏置 b:", self.params['b'].numpy())
2.9 模型训练与优化
通过以下步骤进行模型训练,并应用自定义优化器来更新参数:
1.定义模型并初始化。
2.计算每个小批次的梯度。
3.使用梯度下降法更新模型参数。
4.重复以上步骤,直到损失函数收敛。
In [71]
# 将数据集转换为 Paddle 张量X_train_tensor = paddle.to_tensor(X_train, dtype='float32')y_train_tensor = paddle.to_tensor(y_train.reshape(-1, 1), dtype='float32') # 确保标签形状匹配# 构建模型input_dim = X_train.shape[1] # 输入特征的维度model = LogisticRegression(input_dim)# 训练过程epochs = 10000learning_rate = 0.01for epoch in range(epochs): # 前向计算 outputs = model(X_train_tensor) # 计算梯度 model.backward(X_train_tensor, y_train_tensor) # 更新参数 model.update_params(lr=learning_rate) # 每200回合打印一次损失值 if (epoch + 1) % 200 == 0: # 计算预测值与实际标签的损失 logits = paddle.matmul(X_train_tensor, model.params['w']) + model.params['b'] predictions = model.sigmoid(logits) loss = paddle.mean(-y_train_tensor * paddle.log(predictions) - (1 - y_train_tensor) * paddle.log(1 - predictions)) print(f"Epoch {epoch + 1}/{epochs}, Loss: {loss.numpy()}")
Epoch 200/10000, Loss: 0.434282124042511Epoch 400/10000, Loss: 0.35399749875068665Epoch 600/10000, Loss: 0.31881678104400635Epoch 800/10000, Loss: 0.2997851073741913Epoch 1000/10000, Loss: 0.2880668342113495Epoch 1200/10000, Loss: 0.2802077829837799Epoch 1400/10000, Loss: 0.2746124267578125Epoch 1600/10000, Loss: 0.2704521119594574Epoch 1800/10000, Loss: 0.26725637912750244Epoch 2000/10000, Loss: 0.2647395730018616Epoch 2200/10000, Loss: 0.26271817088127136Epoch 2400/10000, Loss: 0.2610691785812378Epoch 2600/10000, Loss: 0.25970688462257385Epoch 2800/10000, Loss: 0.25856953859329224Epoch 3000/10000, Loss: 0.25761178135871887Epoch 3200/10000, Loss: 0.25679925084114075Epoch 3400/10000, Loss: 0.25610560178756714Epoch 3600/10000, Loss: 0.2555100917816162Epoch 3800/10000, Loss: 0.25499647855758667Epoch 4000/10000, Loss: 0.25455155968666077Epoch 4200/10000, Loss: 0.2541647255420685Epoch 4400/10000, Loss: 0.2538272738456726Epoch 4600/10000, Loss: 0.25353190302848816Epoch 4800/10000, Loss: 0.2532727122306824Epoch 5000/10000, Loss: 0.2530447244644165Epoch 5200/10000, Loss: 0.25284361839294434Epoch 5400/10000, Loss: 0.2526659667491913Epoch 5600/10000, Loss: 0.25250861048698425Epoch 5800/10000, Loss: 0.2523690164089203Epoch 6000/10000, Loss: 0.2522450387477875Epoch 6200/10000, Loss: 0.25213468074798584Epoch 6400/10000, Loss: 0.25203630328178406Epoch 6600/10000, Loss: 0.2519485056400299Epoch 6800/10000, Loss: 0.2518700659275055Epoch 7000/10000, Loss: 0.25179991126060486Epoch 7200/10000, Loss: 0.25173699855804443Epoch 7400/10000, Loss: 0.2516807019710541Epoch 7600/10000, Loss: 0.25163009762763977Epoch 7800/10000, Loss: 0.25158464908599854Epoch 8000/10000, Loss: 0.2515437602996826Epoch 8200/10000, Loss: 0.2515070140361786Epoch 8400/10000, Loss: 0.25147390365600586Epoch 8600/10000, Loss: 0.25144410133361816Epoch 8800/10000, Loss: 0.2514171898365021Epoch 9000/10000, Loss: 0.2513929307460785Epoch 9200/10000, Loss: 0.25137099623680115Epoch 9400/10000, Loss: 0.2513512074947357Epoch 9600/10000, Loss: 0.2513332962989807Epoch 9800/10000, Loss: 0.2513171434402466Epoch 10000/10000, Loss: 0.25130248069763184
3.0 模型评估与预测
在训练完模型之后,我们需要对模型进行评估并在新的数据上进行预测。在这一部分,我们将介绍如何使用训练好的 Logistic 回归模型来进行评估和预测。
1. 模型评估
模型评估的核心是通过计算损失函数(如交叉熵损失)来度量模型在测试集上的表现。我们可以使用测试集的真实标签和模型预测结果计算损失,以了解模型在训练后是否有较好的泛化能力。
评估公式:
L(y,y^)=−1N∑i=1N[yilog(y^i)+(1−yi)log(1−y^i)]L(y,y^)=−N1i=1∑N[yilog(y^i)+(1−yi)log(1−y^i)]
其中,yy 为真实标签,y^y^ 为预测输出,NN 为样本数。
In [72]
# 模型评估def evaluate_model(model, X_test, y_test): # 前向计算 logits = paddle.matmul(X_test, model.params['w']) + model.params['b'] predictions = model.sigmoid(logits) # 计算交叉熵损失 loss = paddle.mean(-y_test * paddle.log(predictions) - (1 - y_test) * paddle.log(1 - predictions)) print(f"测试集损失: {loss.numpy()}") # 预测标签 predicted_labels = (predictions > 0.5).astype('float32') # 将 predicted_labels 转为 float32 # 计算准确率 accuracy = paddle.mean(paddle.cast(predicted_labels == y_test, dtype='float32')) print(f"测试集准确率: {accuracy.numpy()}") return predicted_labels.numpy()# 将数据转换为 Paddle 张量X_test_tensor = paddle.to_tensor(X_test, dtype='float32')y_test_tensor = paddle.to_tensor(y_test, dtype='float32')predicted_labels = evaluate_model(model, X_test_tensor, y_test_tensor)# print("预测标签:", predicted_labels)
测试集损失: 1.567063570022583测试集准确率: 0.5015000104904175
In [ ]
In [73]
# 将数据转换为 Paddle 张量X_test_tensor = paddle.to_tensor(X_test, dtype='float32')y_test_tensor = paddle.to_tensor(y_test, dtype='float32')# 测试阶段的预测predictions = model(X_test_tensor) # 概率值predicted_labels = (predictions > 0.5).astype('int32') # 转为二分类标签# # 检查预测结果分布# print("预测概率分布:", predictions.numpy().flatten())# print("预测标签分布:", predicted_labels.numpy().flatten())# 可视化对比plt.figure(figsize=(10, 6))# 实际标签可视化plt.subplot(1, 2, 1)plt.scatter(X_test[:, 0], X_test[:, 1], c=y_test.flatten(), cmap='bwr', alpha=0.6)plt.title('Actual Labels')plt.xlabel('Feature 1')plt.ylabel('Feature 2')# 预测标签可视化plt.subplot(1, 2, 2)plt.scatter(X_test[:, 0], X_test[:, 1], c=predicted_labels.numpy().flatten(), cmap='bwr', alpha=0.6)plt.title('Predicted Labels')plt.xlabel('Feature 1')plt.ylabel('Feature 2')plt.tight_layout()plt.show()
以上就是【PaddlePaddle】基础理论教程 – 机器学习理论实践的详细内容,更多请关注创想鸟其它相关文章!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/72274.html
微信扫一扫
支付宝扫一扫