神经网络基本原理简明教程之线性分类之线性二分类

一. 摘要

分类问题在很多资料中都称之为逻辑回归,Logistic Regression,其原因是使用了线性回归中的线性模型,加上一个Logistic二分类函数,共同构造了一个分类器。我们在本书中统称之为分类。

神经网络的一个重要功能就是分类,现实世界中的分类任务复杂多样,但万变不离其宗,我们都可以用同一种模式的神经网络来处理。

我们从最简单的线性二分类开始学习,包括其原理,实现,训练过程,推理过程等等,并且以可视化的方式来帮助大家更好地理解这些过程。

做二分类时,我们一般用Sigmoid函数做分类函数,那么和Sigmoid函数长得特别像的双曲正切函数能不能做分类函数呢?我们将会探索这件事情,从而对分类函数、损失函数、样本标签有更深的理解。

二. 多入单出的单层神经网路

1.提出问题

我们经常看到中国象棋棋盘中,用楚河汉界分割开了两个阵营的棋子。回忆历史,公元前206年前后,楚汉相争,当时刘邦和项羽麾下的城池,在中原地区的地理位置示意图如下:

在这里插入图片描述

红色圆点,楚,项羽的城池
绿色叉子,汉,刘邦的城池

在这里插入图片描述

在本例中,中原地区的经纬度坐标其实应该是一个两位数以上的实数,比如(35.234, -122.455)。为了简化问题,我们已经把它们归一化到[0,1]之间了。

问题:

1 经纬度相对坐标值为(0.58,0.92)时,属于楚还是汉?
2 经纬度相对坐标值为(0.62,0.55)时,属于楚还是汉?
3 经纬度相对坐标值为(0.39,0.29)时,属于楚还是汉?

读者可能会觉得这个太简单了,这不是有图吗?定位坐标值后在图上一比划,一下子就能找到对应的区域了。但是我们要求用机器学习的方法来解决这个看似简单的问题,以便将来的预测行为是快速准确的,而不是拿个尺子在图上去比划。

另外,本着用简单的例子说明复杂的原理的原则,我们用这个看似简单的例子,是想让读者对问题和解决方法都有一个视觉上的清晰认识,而这类可以可视化的问题,在实际生产环境中并不多见。

2 逻辑回归模型

回归问题可以分为两类:线性回归和逻辑回归。

逻辑回归的英文是Logistic Regression,逻辑回归是用来计算“事件=Success”和“事件=Failure”的概率。当因变量的类型属于二元(1 / 0,真/假,是/否)变量时,我们就应该使用逻辑回归。

回忆线性回归,使用一条直线拟合样本数据,而逻辑回归是“拟合”0或1两个数值,而不是具体的连续数值,所以它叫广义线性模型。逻辑回归又称logistic回归分析,常用于数据挖掘,疾病自动诊断,经济预测等领域。

例如,探讨引发疾病的危险因素,并根据危险因素预测疾病发生的概率等。以胃癌病情分析为例,选择两组人群,一组是胃癌组,一组是非胃癌组,两组人群必定具有不同的体征与生活方式等。因此因变量就为是否胃癌,值为“是”或“否”;自变量就可以包括很多了,如年龄、性别、饮食习惯、幽门螺杆菌感染等。

自变量既可以是连续的,也可以是分类的。然后通过logistic回归分析,可以得到自变量的权重,从而可以大致了解到底哪些因素是胃癌的危险因素。同时根据该权值可以根据危险因素预测一个人患癌症的可能性。

逻辑回归的另外一个名字叫做分类器,分为线性分类器和非线性分类器,本章中我们学习线性分类器。而无论是线性还是非线性分类器,又分为两种:二分类问题和多分类问题,在本章中我们学习二分类问题。线性多分类问题将会在下一章讲述,非线性分类问题在后续的步骤中讲述。

综上所述,我们本章要学习的路径是:回归问题->逻辑回归问题->线性逻辑回归即分类问题->线性二分类问题。

下图示意说明了线性二分类和非线性二分类的区别:
在这里插入图片描述

三. 二分类函数

1.二分类函数

对率函数Logistic Function,即可以做为激活函数使用,又可以当作二分类函数使用。而在很多不太正规的文字材料中,把这两个概念混用了,比如下面这个说法:“我们在最后使用Sigmoid激活函数来做二分类”,这是不恰当的。在本书中,我们会根据不同的任务区分激活函数和分类函数这两个概念,在二分类任务中,叫做Logistic函数,而在作为激活函数时,叫做Sigmoid函数。

公式
在这里插入图片描述
函数图像:
在这里插入图片描述
使用方式

此函数实际上是一个概率计算,它把(−∞,∞)之间的任何数字都压缩到(0,1)之间,返回一个概率值,这个概率值接近1时,认为是正例,否则认为是负例。

训练时,一个样本x在经过神经网络的最后一层的矩阵运算结果作为输入z,经过Logistic计算后,输出一个(0,1)之间的预测值。我们假设这个样本的标签值为0属于负类,如果其预测值越接近0,就越接近标签值,那么误差越小,反向传播的力度就越小。

推理时,我们预先设定一个阈值比如0.5,则当推理结果大于0.5时,认为是正类;小于0.5时认为是负类;等于0.5时,根据情况自己定义。阈值也不一定就是0.5,也可以是0.65等等,阈值越大,准确率越高,召回率越低;阈值越小则相反,准确度越低,召回率越高。

比如:

input=2时,output=0.88,而0.88>0.5,算作正例
input=-1时,output=0.27,而0.27<0.5,算作负例

2. 正向传播
在这里插入图片描述
在这里插入图片描述

3. 反向传播
在这里插入图片描述
在这里插入图片描述
我们惊奇地发现,使用交叉熵函数求导得到的分母,与Logistic分类函数求导后的结果,正好可以抵消,最后只剩下了a−y这一项。真的有这么巧合的事吗?实际上这是依靠科学家们的聪明才智寻找出了这种匹配关系,以满足以下条件:

1.损失函数满足二分类的要求,无论是正例还是反例,都是单调的;
2.损失函数可导,以便于使用反向传播算法;
3.让计算过程非常简单,一个减法就可以搞定。

多样本情况

我们用三个样本做实例化推导:
在这里插入图片描述
所以,用矩阵运算时可以简化为矩阵相减的形式:A−Y。

四. 对数几率的来历
经过数学推导后可以知道,神经网络实际也是在做这样一件事:经过调整w和b的值,把所有正例的样本都归纳到大于0.5的范围内,所有负例都小于0.5。但是如果只说大于或者小于,无法做准确的量化计算,所以用一个对率函数来模拟。

说到对率函数,还有一个问题,它为啥叫做“对数几率”函数呢?从哪里看出是“对数”了?“几率”是什么意思呢?

我们举例说明:假设有一个硬币,抛出落地后,得到正面的概率是0.5,得到反面的概率是0.5,这两个概率叫做probability。如果用正面的概率除以反面的概率,0.5/0.5=1,这个数值叫做odds,几率。

泛化一下,如果正面的概率是a,则反面的概率就是1-a,则几率等于:

在这里插入图片描述
上式中,如果a是把样本x的预测为正例的可能性,那么1-a就是其负例的可能性,a/(1-a)就是正负例的比值,称为几率(odds),反映了x作为正例的相对可能性,而对几率取对数就叫做对数几率(log odds, logit)。

如果假设概率如下表:

在这里插入图片描述
可以看到0dds的值不是线性的,不利于分析问题,所以在表中第4行对odds取对数,可以得到一组成线性关系的值,即:

在这里插入图片描述
对公式10两边取自然指数:

在这里插入图片描述
对公式11取倒数:

在这里插入图片描述
变形:
在这里插入图片描述

在这里插入图片描述
公式12就是公式2!对数几率的函数形式可以认为是这样得到的。

以上推导过程,实际上就是用线性回归模型的预测结果来逼近样本分类的对数几率。这就是为什么它叫做逻辑回归(logistic regression),但其实是分类学习的方法。这种方法的优点如下:

  1. 直接对分类可能性建模,无需事先假设数据分布,避免了假设分布不准确所带来的问题
  2. 不仅预测出类别,而是得到了近似的概率,这对许多需要利用概率辅助决策的任务很有用
  3. 对率函数是任意阶可导的凸函数,有很好的数学性,许多数值优化算法都可以直接用于求取最优解

四. 线性二分类的神经网络实现

我们先看看如何用神经网络在两组不同标签的样本之间画一条明显的分界线。这条分界线可以是直线,也可以是曲线。这就是二分类问题。如果只画一条分界线的话,无论是直线还是曲线,我们可以用一支假想的笔(即一个神经元),就可以达到目的,也就是说笔的走向,完全依赖于这一个神经元根据输入信号的判断。

再看楚汉城池示意图,在两个颜色区域之间似乎存在一条分割的直线,即线性可分的。

1.从视觉上判断是线性可分的,所以我们使用单层神经网络即可;
2.输入特征是经度和纬度,所以我们在输入层设置两个输入X1=经度,X2=维度;
3.最后输出的是一个二分类,分别是楚汉地盘,可以看成非0即1的二分类问题,所以我们只用一个输出单元就可以了。

1.定义神经网络结构

根据前面的猜测,看来我们只需要一个二入一出的神经元就可以搞定。这个网络只有输入层和输出层,由于输入层不算在内,所以是一层网络。

在这里插入图片描述
与上一章的网络结构图的区别是,这次我们在神经元输出时使用了分类函数,所以有个A的输出,而不是以往的Z的直接输出。

在这里插入图片描述
在这里插入图片描述

2. 反向传播

接下来,我们求loss对w的导数。本例中,w的形式是一个2行1列的向量,所以求w的偏导时,要对向量求导:

在这里插入图片描述
最终结果:

在这里插入图片描述
3 代码实现

这是封装好地一些包的提取二维码:
在这里插入图片描述
获取数据集的二维码:
在这里插入图片描述

由于以前我们的神经网络只会做线性回归,现在多了一个做分类的技能,所以我们加一个枚举类型,可以让调用者通过指定参数来控制神经网络的功能。

class NetType(Enum):
    Fitting = 1,
    BinaryClassifier = 2,
    MultipleClassifier = 3

然后在超参类里把这个新参数加在初始化函数里:

class HyperParameters(object):
    def __init__(self, eta=0.1, max_epoch=1000, batch_size=5, eps=0.1, net_type=NetType.Fitting):
        self.eta = eta
        self.max_epoch = max_epoch
        self.batch_size = batch_size
        self.eps = eps
        self.net_type = net_type

再增加一个Logistic分类函数:

class Logistic(object):
    def forward(self, z):
        a = 1.0 / (1.0 + np.exp(-z))
        return a

以前只有均方差函数,现在我们增加了交叉熵函数,所以新建一个类便于管理:

class LossFunction(object):
    def __init__(self, net_type):
        self.net_type = net_type
    # end def

    def MSE(self, A, Y, count):
        ...

    # for binary classifier
    def CE2(self, A, Y, count):
        ...

上面的类是通过初始化时的网络类型来决定何时调用均方差函数(MSE),何时调用交叉熵函数(CE2)的。

下面修改一下NeuralNet类的前向计算函数,通过判断当前的网络类型,来决定是否要在线性变换后再调用sigmoid分类函数:

class NeuralNet(object):
    def __init__(self, params, input_size, output_size):
        self.params = params
        self.W = np.zeros((input_size, output_size))
        self.B = np.zeros((1, output_size))

    def __forwardBatch(self, batch_x):
        Z = np.dot(batch_x, self.W) + self.B
        if self.params.net_type == NetType.BinaryClassifier:
            A = Sigmoid().forward(Z)
            return A
        else:
            return Z

最后是主过程:

if __name__ == '__main__':
    # data
    reader = SimpleDataReader()
    reader.ReadData()
    # net
    params = HyperParameters(eta=0.1, max_epoch=100, batch_size=10, eps=1e-3, net_type=NetType.BinaryClassifier)
    input = 2
    output = 1
    net = NeuralNet(params, input, output)
    net.train(reader, checkpoint=1)
    # inference
    x_predicate = np.array([0.58,0.92,0.62,0.55,0.39,0.29]).reshape(3,2)
    a = net.inference(x_predicate)
    print("A=", a)    

与以往不同的是,我们设定了超参中的网络类型是BinaryClassifier。

4 运行结果

损失函数值记录很平稳地下降,说明网络收敛了:

在这里插入图片描述

最后几行的打印输出:

epoch=95
95 19 0.2107366003875905
epoch=96
96 19 0.20989241623899846
epoch=97
97 19 0.20905641456530918
epoch=98
98 19 0.20823454980498363
epoch=99
99 19 0.20742586902509108
W= [[-7.66469954]
 [ 3.15772116]]
B= [[2.19442993]]
A= [[0.65791301]
 [0.30556477]
 [0.53019727]]

打印出来的W,B的值对我们来说是几个很神秘的数字,下一节再解释。A值是返回的预测结果:

1.经纬度相对值为(0.58,0.92)时,概率为0.65,属于汉
2.经纬度相对值为(0.62,0.55)时,概率为0.30,属于楚
3.经纬度相对值为(0.39,0.29)时,概率为0.53,属于汉

分类的方式是,可以指定当A > 0.5时是正例,A <= 0.5时就是反例。有时候正例反例的比例不一样或者有特殊要求时,也可以用不是0.5的数来当阈值。

贴上这个的完整代码:
import numpy as np

from HelperClass.NeuralNet_1_2 import *

file_name = “ch06.npz”

主程序

if __name__ == '__main__':
    # data
    reader = DataReader_1_1(file_name)
    reader.ReadData()
    # net
    num_input = 2
    num_output = 1
    hp = HyperParameters_1_1(num_input, num_output, eta=0.1, max_epoch=100, batch_size=10, eps=1e-3,
                             net_type=NetType.BinaryClassifier)
    net = NeuralNet_1_2(hp)
    net.train(reader, checkpoint=1)

    # inference
    x_predicate = np.array([0.58, 0.92, 0.62, 0.55, 0.39, 0.29]).reshape(3, 2)
    a = net.inference(x_predicate)
    print("A=", a)

五. 线性二分类原理

1 线性分类和线性回归的异同

此原理对线性和非线性二分类都适用。

回忆一下前面学习过的线性回归,通过用均方差函数的误差反向传播的方法,不断矫正拟合直线的角度(Weights)和偏移(Bias),因为均方差函数能够准确地反映出当前的拟合程度。那么在线性分类中,我们能不能采取类似的方法呢?

线性分类,试图在含有两种样本的空间中划出一条分界线,让双方截然分开,就好像是中国象棋的棋盘中的楚河汉界一样。与线性回归相似的地方是,两者都需要划出那条“直线”来,但是不同的地方也很明显:

在这里插入图片描述
可以看到线性回归中的目标–“距离最短”,还是很容易理解的,但是线性分类的目标–“分布在两侧”,用数学方式如何描述呢?我们可以有代数和几何两种方式来描述:

代数方式:通过一个分类函数计算所有样本点在经过线性变换后的概率值,使得正例样本的概率大于0.5,而负例样本的概率小于0.5

几何方式:下图中,让所有正例样本处于直线的上方,所有负例样本处于直线的下方

在这里插入图片描述
2 二分类过程

下面我们以单样本双特征值为例来说明神经网络的二分类过程,这是用代数方式来解释其工作原理。

在这里插入图片描述
3 数值计算举例

用下图举例来说明计算过程:

在这里插入图片描述
假设:
绿色点为正例,标签值y=1,坐标值为(1, 3)

红色点为负例,标签值y=0,坐标值分别为(1.5, 3)和(3, 1.5)

假设公式1的初始值w1=−1,w2=1,b=0,所以有z=−1⋅x1+1⋅x2+0,即z=−x1+x2。如果令z=0,即Logistic(z)=0.5,是两个区域的分割线(面),则有平面上的直线x2=x1

在上面三个点中,绿色点和右下角的红色点,是分类正确的点,而中间的红色点是分类错误的点。下面我们把公式1,2,3代入具体的值,来看一下神经网络如何做分类的。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
对比总结如下表:
在这里插入图片描述
在正例情况y=1时,a如果越靠近1,表明分类越正确,此时损失值会越小。点1就是这种情况:a=0.88,距离1不远;loss值0.127,不算很大

在负例情况y=0时,a如果越靠近0,表明分类越正确,此时损失值会越小。点3就是这种情况:a=0.182,距离0不远;loos值0.2,不算很大

点2是分类错误的情况,a=0.817,应该是小于0.5的,却距离0远,距离1反而近,所以它的loss=1.7,从与其它两个点比较的相对值来看,是非常大的,这样误差就大,反向传播的力度也大

4 二分类的几何原理

我们再观察一下下面这张分类正确的图:

在这里插入图片描述

假设绿色方块为正类:标签值y=1,红色三角形为负类:标签值y=0。

从几何关系上理解,如果我们有一条直线,其公式为:z=w⋅x1+b,如图中的虚线所示,则所有正类的样本的x2都大于z,而所有的负类样本的x2都小于z,那么这条直线就是我们需要的分割线。用正例的样本来表示:

在这里插入图片描述
那么神经网络用矩阵运算+分类函数+损失函数这么复杂的流程,其工作原理是什么呢?

经典机器学习中的SVM确实就是用这种思路来解决这个问题的,即一个类别的所有样本在分割线的一侧,而负类样本都在线的另一侧。神经网络的正向公式如公式1,2所示,当a>0.5时,判为正类。当a<0.5时,判为负类。z=0即a=0.5时为分割线:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

六. 二分类结果可视化

1 可视化的重要性

我们虽然得到了结果,但都是一些神秘的数字,我们如何知道它们是正确还是错误的呢?

后面我们会讲到,在实际的工程实践中,一般我们会把样本分成训练集、验证集、测试集,用测试集来测试训练结果的正确性。在本例中我们没有这样做,原因有二:

1.样本数据量比较少,一共只有200个样本,如果再分成两部分,会造成数据集覆盖不全面,存在很大的差异,对训练、验证、测试都没有帮助
2.由于本例的数据特征比较少,所以我们有更好的手段:可视化。在神经网络学习初期,可视化的训练过程与结果会对读者有巨大的帮助。

神经网络的可视化,说简单也很简单,说难也很难,关键是对框架系统的理解,对运行机制和工作原理的理解,掌握了这些,可视化就会使一件轻而易举且令人愉快的事情。

2 权重值的含义

关于W,B数字如何解读呢?

W= [[-7.66469954]
 [ 3.15772116]]
B= [[2.19442993]]
A= [[0.65791301]
 [0.30556477]
 [0.53019727]]

学习了线性二分类的原理,其中提到了如果我们能够根据训练结果,在图上画出一条直线来分割正例和负例两个区域,是不是就很直观了呢?

在这里插入图片描述
对公式2来说,当a大于0.5时,属于正例(属于汉),当a小于0.5时,属于负例(属于楚)。那么a=0.5时,就是楚汉边界啦!

在这里插入图片描述
把x2留在等式左侧,其它的挪到右侧去,就可以得到一条直线的方程了:
在这里插入图片描述
好了,这就是标准的直线方程y=ax+b的形式了。这个公式等同于二分类原理中的公式7,8。

3 代码实现

用Python代码实现公式3如下:

def draw_split_line(net,):
    b12 = -net.B[0,0]/net.W[1,0]
    w12 = -net.W[0,0]/net.W[1,0]
    print(w12,b12)
    x = np.linspace(0,1,10)
    y = w12 * x + b12
    plt.plot(x,y)
    plt.axis([-0.1,1.1,-0.1,1.1])
    plt.show()

上面代码中的计算w12,b12的code就是根据公式3来的,只不过我们的W的定义是(w1, w2),而python是zero-based,所以: w1=W[0,0],w2=W[0,1],b=B[0,0]。

同时需要展示样本数据,以便判断分割线和样本数据的吻合程度:

def draw_source_data(net, dataReader):
    fig = plt.figure(figsize=(6.5,6.5))
    X,Y = dataReader.GetWholeTrainSamples()
    for i in range(200):
        if Y[i,0] == 1:
            plt.scatter(X[i,0], X[i,1], marker='x', c='g')
        else:
            plt.scatter(X[i,0], X[i,1], marker='o', c='r')
        #end if
    #end for

最后还可以显示一下三个预测点的位置,看看是否正确:

def draw_predicate_data(net):
    x = np.array([0.58,0.92,0.62,0.55,0.39,0.29]).reshape(3,2)
    a = net.inference(x)
    print("A=", a)
    for i in range(3):
        if a[i,0] > 0.5:
            plt.scatter(x[i,0], x[i,1], marker='^', c='g', s=100)
        else:
            plt.scatter(x[i,0], x[i,1], marker='^', c='r', s=100)
        #end if
    #end for

主程序:

主程序

if __name__ == '__main__':
    # data
    reader = SimpleDataReader()
    reader.ReadData()
    # net
    params = HyperParameters(eta=0.1, max_epoch=100, batch_size=10, eps=1e-3, net_type=NetType.BinaryClassifier)
    input = 2
    output = 1
    net = NeuralNet(params, input, output)
    net.train(reader, checkpoint=1)

    # show result
    draw_source_data(net, reader)
    draw_predicate_data(net)
    draw_split_line(net)
    plt.show()

4 运行结果

下图为结果:
在这里插入图片描述
虽然蓝色的分割线大体分开了楚汉两国,但是细心的读者会发现在上下两端,还是绿点在分割线右侧,而红点在分割线左侧的情况。这说明我们的神经网络的训练精度不够。所以,稍微改一下超参,再训练一遍:

params = HyperParameters(eta=0.1, max_epoch=10000, batch_size=10, eps=1e-3, net_type=NetType.BinaryClassifier)

把max_epoch从100改成了10000,再跑一次:

在这里插入图片描述

从上图的loss曲线看,损失函数值一直在下降,说明网络还在继续收敛。

再看下图的直线位置,已经比较完美地分开了红色和绿色区域

在这里插入图片描述

三个三角点是求解问题的三个坐标,其中第三个三角点处于分割线附近,用肉眼不是很容易分得出来,看打印输出:

W= [[-42.62417571]
 [ 21.36558218]]
B= [[10.5773054]]
A= [[0.99597669]
 [0.01632475]
 [0.53740392]]
w12= 1.994992477013739
b12= -0.49506282174794675

前两个点的概率分别是0.995和0.016,可以明确地区分正例负例,第三个点是0.537,大于0.5,可以算作正例。

在matplot的绘图控件中,我们也可以放大局部观察,可以看到如下细节:

在这里插入图片描述

第三个点位于左侧正例区域。

好了,我们已经自信满满地找到了解释神经网络工作的原理,有数值计算验证,有公式推导,有图形显示,至少可以自圆其说了。但实际情况是不是这样呢?有没有更高深的原理还没有接触到呢?暂且留下这个问题,留在以后的章节去继续学习。

HelperClass包扫上面二维码获得:

完整代码:

import numpy as np
import matplotlib.pyplot as plt
from HelperClass.NeuralNet_1_2 import *
from HelperClass.HyperParameters_1_1 import *
from HelperClass.Visualizer_1_0 import *

file_name = "ch06.npz"

def draw_split_line(net):
    b12 = -net.B[0,0]/net.W[1, 0]
    w12 = -net.W[0,0]/net.W[1, 0]
    print("w12=", w12)
    print("b12=", b12)
    x = np.linspace(0, 1, 10)
    y = w12 * x + b12
    plt.plot(x, y)
    plt.axis([-0.1, 1.1, -0.1, 1.1])
    plt.show()

def draw_source_data(dataReader, show=False):
    fig = plt.figure(figsize=(6,6))
    X,Y = dataReader.GetWholeTrainSamples()
    DrawTwoCategoryPoints(X[:, 0], X[:, 1], Y[:, 0], show=show)

def draw_predicate_data(net, threshold=0.5):
    x = np.array([0.58,0.92,0.62,0.55,0.39,0.29]).reshape(3,2)
    a = net.inference(x)
    print("A=", a)
    DrawTwoCategoryPoints(x[:,0], x[:,1], a[:,0], show=False, isPredicate=True)
    """
    for i in range(3):
        if a[i,0] > threshold:
            plt.scatter(x[i,0], x[i,1], marker='^', c='r', s=200)
        else:
            plt.scatter(x[i,0], x[i,1], marker='^', c='b', s=200)
    """

# 主程序
if __name__ == '__main__':
    # data
    reader = DataReader_1_1(file_name)
    reader.ReadData()
    draw_source_data(reader, show=True)
    # net
    num_input = 2
    num_output = 1
    hp = HyperParameters_1_1(num_input, num_output, eta=0.1, max_epoch=1000, batch_size=10, eps=1e-3, net_type=NetType.BinaryClassifier)
    net = NeuralNet_1_2(hp)
    net.train(reader, checkpoint=10)

    # show result
    draw_source_data(reader, show=False)
    draw_predicate_data(net)
    draw_split_line(net)
    plt.show()
  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值