深圳幻海软件技术有限公司 欢迎您!

Python实现逻辑回归(Logistic Regression)

2023-05-23

💥项目专栏:【Python实现经典机器学习算法】附代码+原理介绍文章目录前言一、基于原生Python实现逻辑回归算法二、逻辑回归模型的算法原理三、算法实现3.1导包3.2定义随机数种子3.3定义逻辑回归模型3.3.1模型训练3.3.1.1初始化参数3.3.1.2正向传播3.3.1.3损失函数3.3

💥 项目专栏:【Python实现经典机器学习算法】附代码+原理介绍

文章目录

  • 前言
  • 一、基于原生Python实现逻辑回归算法
  • 二、逻辑回归模型的算法原理
  • 三、算法实现
    • 3.1 导包
    • 3.2 定义随机数种子
    • 3.3 定义逻辑回归模型
      • 3.3.1 模型训练
        • 3.3.1.1 初始化参数
        • 3.3.1.2 正向传播
        • 3.3.1.3 损失函数
        • 3.3.1.4 反向传播
      • 3.3.2 模型预测
      • 3.3.3 模型分数
      • 3.3.4 Logistic Regression模型
    • 3.4 导入数据
    • 3.5 划分训练集、测试集
    • 3.6 模型训练
    • 3.7 打印结果
    • 3.8 可视化决策边界
  • 完整源码


前言

  • 👑 最近粉丝群中很多朋友私信咨询一些决策树、逻辑回归等机器学习相关的编程问题,为了能更清晰的说明,所以建立了本专栏 专门记录基于原生Python实现一些入门必学的机器学习算法,帮助广大零基础用户达到轻松入门,为了更深刻算法的基本原理,本专栏没有采用第三方库来实现(sklearn),而是采用原生Python自己复现相关算法,从而帮助新手理解算法的内部细节。

  • 👑 本专栏适用人群:🚨🚨🚨 机器学习初学者刚刚接触sklearn的用户群体,专栏将具体讲解如何基于原生Python来实现一些经典机器学习算法,快速让新手小白能够对机器学习算法有更深刻的理解

  • 👑 本专栏内包含基于原生Python从零实现经典机器学习算法,通过自复现帮助新手小白对算法有更深刻的认识,理论与实践相结合,每一篇文章都附带有 完整的代码+原理讲解

🚨 我的项目环境:

  • 平台:Windows11
  • 语言环境:Python 3.7
  • 编译器:Jupyter Lab
  • Pandas:1.3.5
  • Numpy:1.19.3
  • Scipy:1.7.3
  • Matplotlib:3.1.3

💥 项目专栏:【Python实现经典机器学习算法】附代码+原理介绍


一、基于原生Python实现逻辑回归算法

逻辑回归是一种经典机器学习分类算法,它被广泛应用于二元分类问题中,该算法的目的是预测二元输出变量(比如0和1),逻辑回归算法有很多应用,比如预测股票市场、客户购买行为、疾病诊断等等。它被广泛应用于医学、金融、社交网络、搜索引擎等各个领域。

本篇文章我们采用Python语言实现经典的机器学习算法 Logistic Regression ,作为该专栏的第一篇文章,本篇将💎 详细介绍项目的每个实现部分以及细节处理,帮助新手小白快速建立起机器学习算法训练的框架

二、逻辑回归模型的算法原理

逻辑回归(Logistic Regression)是一种广泛使用的分类算法,它的主要思想是将输入变量的线性组合映射到0到1之间的概率,用于预测二元输出变量的概率。

以下是逻辑回归模型的算法原理:

  1. 假设我们有一个二元分类问题,需要预测一个样本属于两个类别中的哪一个。
  2. 逻辑回归模型使用一个参数化函数来计算给定输入变量的输出概率。该函数称为 sigmoid 函数,它将输入变量的线性组合映射到0到1之间的值,表示预测样本属于正例的概率。
  3. sigmoid 函数的数学形式为: g ( z ) = 1 1 + e − z g(z) = \frac{1}{1 + e^{-z}} g(z)=1+ez1其中,z 是输入变量的线性组合,可以表示为: z = b + w 1 x 1 + w 2 x 2 + . . . + w n x n z = b + w_1x_1 + w_2x_2 + ... + w_nx_n z=b+w1x1+w2x2+...+wnxn其中, w i w_i wi是模型的权重(即系数), x i x_i xi是输入变量的值。
  4. 训练模型的过程就是通过最大化似然函数来估计模型的权重。似然函数是一个关于模型参数的函数,表示给定模型下,样本的概率。在逻辑回归中,似然函数可以表示为: L ( w ) = ∏ i = 1 n g ( z i ) y i ( 1 − g ( z i ) ) 1 − y i L(w) = \prod_{i=1}^n g(z_i)^{y_i}(1-g(z_i))^{1-y_i} L(w)=i=1ng(zi)yi(1g(zi))1yi其中, z i z_i zi是第i个样本的线性组合, y i y_i yi是对应的类别标签(0或1)。
  5. 为了最大化似然函数,我们可以使用梯度下降算法来更新模型的权重。梯度下降算法通过反复迭代来最小化损失函数,直到找到最优解。
  6. 损失函数通常使用对数损失函数(log loss)来衡量模型的性能。对数损失函数可以表示为: J ( w ) = − 1 n ∑ i = 1 n y i log ⁡ g ( z i ) + ( 1 − y i ) log ⁡ ( 1 − g ( z i ) ) J(w) = -\frac{1}{n}\sum_{i=1}^n y_i \log g(z_i) + (1-y_i) \log (1-g(z_i)) J(w)=n1i=1nyilogg(zi)+(1yi)log(1g(zi))其中, z i z_i zi是第i个样本的线性组合, y i y_i yi是对应的类别标签(0或1)。
  7. 训练模型后,我们可以使用模型来预测新的样本的类别标签。预测类别标签的方法是,将新样本的特征向量代入 sigmoid 函数,计算输出概率,如果概率大于0.5,则预测为正例,否则预测为负例。

逻辑回归模型简单而直观,易于理解和实现,常用于二元分类问题的建模

三、算法实现

本部分将讲解如何使用原生Python来实现逻辑回归算法,本文并没有使用 sklearn 直接调用定义模型,而是采用自己复现,因为这样才能够帮新手小白理解算法内部的具体流程。

注:本文复现的算法为逻辑回归算法的阉割版(为了刚入门的同学更容易理解),相对于 sklearn 框架实现的算法相对简略,但保留了算法的核心部分,这是因为在初学期间应更注重算法的主干到底做了什么事,对于一些小细节以及各种变体以及算法优化策略这些应在熟悉该算法之后再考虑。

3.1 导包

对于本项目主要使用到的第三方库有以下几种,都是比较常见的

  • numpy:常见的科学计算库
  • matplotlib:进行绘图使用
  • sklearn.datasets:导入训练需要的数据集
  • train_test_split:划分训练集和测试集
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
  • 1
  • 2
  • 3
  • 4

3.2 定义随机数种子

在机器学习任务中,随机数种子的作用是确保实验结果的可重复性。具体来说,机器学习算法中通常会涉及到一些随机性,比如 随机初始化参数、随机选择训练样本 等,这些随机性会导致同一份代码运行多次得到的结果不一定完全相同。

通过 设定随机数种子,可以使得同一份代码在不同的时间运行得到的随机结果是相同的,从而确保实验结果的可重复性。这对于算法的评估和比较非常重要,因为它能够消除随机性带来的误差和不确定性,使得实验结果更加准确和可信。

需要注意的是,在使用随机数种子的同时,应该确保数据集、算法和参数设置等都是相同的,才能保证结果的可重复性。

# 设置随机种子
seed_value = 2023
np.random.seed(seed_value)
  • 1
  • 2
  • 3

3.3 定义逻辑回归模型

为了更容易理解,本文定义逻辑回归模型的方式和 sklearn 封装的算法一致,都是定义 fitpredictscore 接口。

逻辑回归模型的训练过程可以分为以下几个步骤:

  1. 数据预处理:首先需要对数据进行预处理,包括数据清洗、特征选择、特征缩放等。确保数据的质量和可用性。
  2. 模型初始化:根据数据集的特征数量初始化模型参数,即权重和偏置。
  3. 前向传播:将训练数据集中的每个样本的特征向量乘以模型的权重,再加上偏置,得到线性组合。将线性组合输入到sigmoid函数中,得到该样本属于正例的概率。
  4. 计算损失函数:使用对数损失函数来衡量模型的性能,该损失函数的定义在前面已经介绍过了。需要对所有训练样本的损失函数进行求和,并除以样本数量,得到平均损失函数。
  5. 反向传播:使用梯度下降算法来最小化损失函数。首先需要计算损失函数对模型参数的偏导数,即梯度。然后,使用梯度下降算法来更新模型的权重和偏置。
  6. 重复步骤3-5,直到达到预定的迭代次数或损失函数达到某个阈值。
  7. 模型评估:使用测试数据集来评估训练得到的模型的性能。可以使用多个指标来评估模型的性能,如准确率、精确率、召回率、F1得分等。
  8. 模型调优:根据评估结果来调整模型的参数,如学习率、迭代次数等,以提高模型的性能。

在实际训练过程中,可以使用各种优化算法来加速收敛速度,如随机梯度下降(SGD)、Adam等。此外,还可以使用正则化方法来防止模型过拟合,如L1正则化、L2正则化等。

上面是逻辑回归比较详细的训练过程,但是对于刚入门的小伙伴可以选择性的理解,可以先理解算法的基本骨架,在此基础上再去添枝加叶,所以本文在实现方面也是保留了核心部分,一些不必要的部分没有实现。

3.3.1 模型训练

该部分主要定义模型的执行逻辑,也就是给定数据模型到底是如何训练的,对于逻辑回归模型主要包括三个部分:初始化参数、正向传播、计算损失、反向传播

  1. 模型初始化:根据数据集的特征数量初始化模型参数,即权重和偏置。
  2. 前向传播:将训练数据集中的每个样本的特征向量乘以模型的权重,再加上偏置,得到线性组合。将线性组合输入到sigmoid函数中,得到该样本属于正例的概率。
  3. 计算损失函数:使用对数损失函数来衡量模型的性能,该损失函数的定义在前面已经介绍过了。需要对所有训练样本的损失函数进行求和,并除以样本数量,得到平均损失函数。
  4. 反向传播:使用梯度下降算法来最小化损失函数。首先需要计算损失函数对模型参数的偏导数,即梯度。然后,使用梯度下降算法来更新模型的权重和偏置。
  5. 重复步骤2-4,直到达到预定的迭代次数或损失函数达到某个阈值。

3.3.1.1 初始化参数

对于逻辑回归使用到的公式为 y = b + w 1 x 1 + w 2 x 2 + . . . + w n x n y=b+w_1x_1+w_2x_2+...+w_nx_n y=b+w1x1+w2x2+...+wnxn,对于该算法的核心就是学习到权重系数 w w w,为此我们需要定义两个参数,分别为权重 w 和偏置 b

可以使用如下代码来初始化模型的可训练参数:

# 初始化参数
self.weights = np.random.randn(X.shape[1])
self.bias = 0
  • 1
  • 2
  • 3

3.3.1.2 正向传播

由于逻辑回归是进行二分类,所以我们希望得到对应的概率,那么我们需要将计算的结果送入到 Sigmoid 中得到对应的概率值,对应公式为 y ^ = s i g m o i d ( y ) = s i g m o i d ( w ∗ x + b ) = 1 ( 1 + e x p w ∗ x + b ) \hat y=sigmoid(y)=sigmoid(w * x + b)=\frac{1}{(1+exp^{w*x+b})} y^=sigmoid(y)=sigmoid(wx+b)=(1+expwx+b)1

# 计算sigmoid函数的预测值, y_hat = sigmoid(w * x + b)
y_hat = sigmoid(np.dot(X, self.weights) + self.bias)
  • 1
  • 2

3.3.1.3 损失函数

训练模型的目的就是希望通过模型预测的值更加接近真实的值,所以这里为了衡量预测值和真实值之间的差异,需要定义一个 损失函数 有些文章里也叫做代价函数,这里使用的是交叉熵函数(Cross Entropy)
l o s s = − 1 n ∑ y l n ( y ^ ) + ( 1 − y ) l n ( 1 − y ^ ) loss=-\frac{1}{n}\sum yln(\hat y)+(1-y)ln(1- \hat y) loss=n1yln(y^)+(1y)ln(1y^)

对于该损失通过如下代码实现:

# 计算损失函数
loss = (-1 / len(X)) * np.sum(y * np.log(y_hat) + (1 - y) * np.log(1 - y_hat))
  • 1
  • 2

3.3.1.4 反向传播

逻辑回归中使用的参数更新方法是梯度下降法,不断将当前参数减去损失对应的梯度,让模型向着损失减小的方向发展。

在逻辑回归中,我们使用 sigmoid 函数将线性输出映射到 [0,1] 之间的概率值。如果我们定义代价函数为交叉熵,那么我们可以使用梯度下降算法更新权重参数。具体地,我们首先通过前向传播计算模型的预测值和代价函数的值。然后,我们使用反向传播算法计算代价函数相对于每个权重参数的导数,以便我们可以知道哪些权重需要更新。最后,我们使用梯度下降算法更新权重参数,使 代价函数最小化

具体来说,在反向传播中,我们需要计算代价函数相对于每个权重参数的偏导数。对于逻辑回归模型,这可以通过链式法则来计算。

# 计算梯度
dw = (1 / len(X)) * np.dot(X.T, (y_hat - y))
db = (1 / len(X)) * np.sum(y_hat - y)

# 更新参数
self.weights -= self.learning_rate * dw
self.bias -= self.learning_rate * db
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

3.3.2 模型预测

对于逻辑回归的正向传播是计算出对应的概率,为了得到该样本对应的类别,我们需要将其二值化,即大于0.5将其转化为类别1,小于0.5将其转化为类别0。

使用如下代码实现预测过程:

# 预测
def predict(self, X):
    y_hat = sigmoid(np.dot(X, self.weights) + self.bias)
    y_hat[y_hat >= 0.5] = 1
    y_hat[y_hat < 0.5] = 0
    return y_hat
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3.3.3 模型分数

由于逻辑回归是二分类算法,所以最终的衡量指标通常为准确率,所以我们需要计算出样本中分类正确的样本,然后将其除以总样本数得到准确率,如果感兴趣也可以使用其他指标,如 F1-score、AUC 等。

# 精度
def score(self, y_pred, y):
    accuracy = (y_pred == y).sum() / len(y)
    return accuracy
  • 1
  • 2
  • 3
  • 4

3.3.4 Logistic Regression模型

以下是逻辑回归模型的完整定义:

# 定义逻辑回归算法
class LogisticRegression:
    def __init__(self, learning_rate=0.003, iterations=100):
        self.learning_rate = learning_rate # 学习率
        self.iterations = iterations # 迭代次数

    def fit(self, X, y):
        # 初始化参数
        self.weights = np.random.randn(X.shape[1])
        self.bias = 0

        # 梯度下降
        for i in range(self.iterations):
            # 计算sigmoid函数的预测值, y_hat = w * x + b
            y_hat = sigmoid(np.dot(X, self.weights) + self.bias)

            # 计算损失函数
            loss = (-1 / len(X)) * np.sum(y * np.log(y_hat) + (1 - y) * np.log(1 - y_hat))

            # 计算梯度
            dw = (1 / len(X)) * np.dot(X.T, (y_hat - y))
            db = (1 / len(X)) * np.sum(y_hat - y)

            # 更新参数
            self.weights -= self.learning_rate * dw
            self.bias -= self.learning_rate * db

            # 打印损失函数值
            if i % 10 == 0:
                print(f"Loss after iteration {i}: {loss}")

    # 预测
    def predict(self, X):
        y_hat = sigmoid(np.dot(X, self.weights) + self.bias)
        y_hat[y_hat >= 0.5] = 1
        y_hat[y_hat < 0.5] = 0
        return y_hat
    
    # 精度
    def score(self, y_pred, y):
        accuracy = (y_pred == y).sum() / len(y)
        return accuracy
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

3.4 导入数据

为了测试模型,这里使用到的数据为入门级数据——鸢尾花数据集。

鸢尾花数据集(Iris Dataset)是一个常用的分类问题数据集,由英国统计学家和生物学家Ronald Fisher在1936年收集整理而来。该数据集包含了三个不同种类的鸢尾花,每个种类 50 个样本,共计 150 个样本。

每个样本包含了鸢尾花的 萼片(Sepal)花瓣(Petal)长度宽度 四个特征(即:萼片长度、萼片宽度、花瓣长度、花瓣宽度),并且每个特征都以厘米为单位。

三个鸢尾花的品种为 山鸢尾(Iris Setosa)变色鸢尾(Iris Versicolor)维吉尼亚鸢尾(Iris Virginica),它们具有不同的萼片和花瓣的大小和形状,因此它们是可以通过这些特征进行区分的。

由于逻辑回归是二分类,鸢尾花数据集中有三个类别,所以需要对该数据集进行处理,将类别1和类别2归为1类,这样就会得到两个类别的数据(类别0和合并的类别)。

由于后文我们需要对数据进行可视化,所以这里特征只选择了其中的两列进行建模,感兴趣的小伙伴可以使用4个特征,之后再绘图时利用PCA进行降维处理。

# 导入数据
iris = load_iris()
X = iris.data[:, :2]
y = (iris.target != 0) * 1
  • 1
  • 2
  • 3
  • 4

3.5 划分训练集、测试集

在机器学习中,将数据集划分为训练集和测试集是非常重要的一步。训练集用于训练模型,而测试集用于评估模型的性能。

这里使用了 sklearn 中的 train_test_split 方法来实现数据集划分:

# 划分训练集、测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.15, random_state=seed_value)
  • 1
  • 2

3.6 模型训练

接下来我们就可以定义一个模型对象,指定模型训练的超参数 学习率迭代次数,然后将训练数据送给模型调用模型的 fit 方法完成训练过程:

# 训练模型
model = LogisticRegression(learning_rate=0.03, iterations=1000)
model.fit(X_train, y_train)
  • 1
  • 2
  • 3

3.7 打印结果

模型训练完成之后可以使用如下代码查看训练集和测试集的准确率,如果有能力小伙伴可以绘制AUC曲线等查看模型效果。

# 结果
y_train_pred = model.predict(X_train)
y_test_pred = model.predict(X_test)

score_train = model.score(y_train_pred, y_train)
score_test = model.score(y_test_pred, y_test)

print('训练集Accuracy: ', score_train)
print('测试集Accuracy: ', score_test)

>>>训练集Accuracy:  1.0
>>>测试集Accuracy:  0.9565217391304348
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

3.8 可视化决策边界

为了可视化模型的分类效果,该部分实现了如下代码来绘制决策边界查看模型的分类效果如何,决策边界图 如下:

# 可视化决策边界
x1_min, x1_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
x2_min, x2_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
xx1, xx2 = np.meshgrid(np.linspace(x1_min, x1_max, 100), np.linspace(x2_min, x2_max, 100))
Z = model.predict(np.c_[xx1.ravel(), xx2.ravel()])
Z = Z.reshape(xx1.shape)
plt.contourf(xx1, xx2, Z, cmap=plt.cm.Spectral)
plt.scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.Spectral)
plt.xlabel("Sepal length")
plt.ylabel("Sepal width")
plt.savefig('a.png', dpi=720)
plt.show()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

完整源码

注意🚨🚨🚨:由于是针对于新手小白入门的系列专栏,所以代码并没有采用开发大型项目的方式,而是python单文件实现,这样能够帮助新人一键复制调试运行,不需要理解复杂的项目构造,另外一点就是由于是帮助新人理解机器学习算法基本训练预测过程,所以源码仅包含了算法的基本框架结构,有些地方实现略有简陋,有能力的小伙伴可以根据自己的能力在此基础上进行修改,尝试更多的参数,以及进行分文件编写(模型训练、模型测试、定义模型、绘制图像)达到项目开发流程。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

# 设置随机种子
seed_value = 2023
np.random.seed(seed_value)

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

# 定义逻辑回归算法
class LogisticRegression:
    def __init__(self, learning_rate=0.003, iterations=100):
        self.learning_rate = learning_rate # 学习率
        self.iterations = iterations # 迭代次数

    def fit(self, X, y):
        # 初始化参数
        self.weights = np.random.randn(X.shape[1])
        self.bias = 0

        # 梯度下降
        for i in range(self.iterations):
            # 计算sigmoid函数的预测值, y_hat = w * x + b
            y_hat = sigmoid(np.dot(X, self.weights) + self.bias)

            # 计算损失函数
            loss = (-1 / len(X)) * np.sum(y * np.log(y_hat) + (1 - y) * np.log(1 - y_hat))

            # 计算梯度
            dw = (1 / len(X)) * np.dot(X.T, (y_hat - y))
            db = (1 / len(X)) * np.sum(y_hat - y)

            # 更新参数
            self.weights -= self.learning_rate * dw
            self.bias -= self.learning_rate * db

            # 打印损失函数值
            if i % 10 == 0:
                print(f"Loss after iteration {i}: {loss}")

    # 预测
    def predict(self, X):
        y_hat = sigmoid(np.dot(X, self.weights) + self.bias)
        y_hat[y_hat >= 0.5] = 1
        y_hat[y_hat < 0.5] = 0
        return y_hat
    
    # 精度
    def score(self, y_pred, y):
        accuracy = (y_pred == y).sum() / len(y)
        return accuracy

# 导入数据
iris = load_iris()
X = iris.data[:, :2]
y = (iris.target != 0) * 1

# 划分训练集、测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.15, random_state=seed_value)

# 训练模型
model = LogisticRegression(learning_rate=0.03, iterations=1000)
model.fit(X_train, y_train)

# 结果
y_train_pred = model.predict(X_train)
y_test_pred = model.predict(X_test)

score_train = model.score(y_train_pred, y_train)
score_test = model.score(y_test_pred, y_test)

print('训练集Accuracy: ', score_train)
print('测试集Accuracy: ', score_test)

# 可视化决策边界
x1_min, x1_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
x2_min, x2_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
xx1, xx2 = np.meshgrid(np.linspace(x1_min, x1_max, 100), np.linspace(x2_min, x2_max, 100))
Z = model.predict(np.c_[xx1.ravel(), xx2.ravel()])
Z = Z.reshape(xx1.shape)
plt.contourf(xx1, xx2, Z, cmap=plt.cm.Spectral)
plt.scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.Spectral)
plt.xlabel("Sepal length")
plt.ylabel("Sepal width")
plt.show()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
文章知识点与官方知识档案匹配,可进一步学习相关知识
算法技能树首页概览46871 人正在系统学习中
业务咨询 | 商务合作 | 学习交流
微信名片