深度学习 Pytorch 单层神经网络

神经网络是模仿人类大脑结构所构建的算法,在人脑里,我们有轴突连接神经元,在算法中,我们用圆表示神经元,用线表示神经元之间的连接,数据从神经网络的左侧输入,让神经元处理之后,从右侧输出结果。

下图是一个最简单的神经元的结构。从这里开始,我们正式开始认识神经网络。

28 单层回归网络:线性回归

28.1 单层回归网络的理论基础

深度学习中的计算是"简单大量",而不是"复杂的单一问题"。神经网络的原理很多时候都比经典机器学习算法简单。了解神经网络,可以从 线性回归 算法开始。

线性回归算法是机器学习中最简单的回归类算法,多元线性回归指的就是一个样本对应多个特征的线性回归问题。假设我们的数据现在就是二维表,对于一个 有 n n n个特征的样本而言,它的预测结果可以写作一个几乎人人熟悉的方程:
z ^ i = b + w 1 x i 1 + w 2 x i 2 + ... + w n x i n \hat{z}i = b + w_1 x{i1} + w_2 x_{i2} + \ldots + w_n x_{in} z^i=b+w1xi1+w2xi2+...+wnxin
w w w和 b b b被统称为模型的权重,其中 b b b被称为截距(intercept),也叫做偏差(bias), w 1 w_1 w1~ w n w_n wn被称为回归系数(regression coefficient),也叫作权重(weights), x i 1 x_{i1} xi1~ x i n x_{in} xin是样本 i i i上的不同特征。这个表达式,其实就和我们小学时就无比熟悉的 y = a x + b y = ax + b y=ax+b 是同样的性质。其中 y y y被我们称为因变量,在线性回归中表示为 z z z,在机器学习中也就表现为我们的标签。如果写作 z z z,则代表真实标签。如果写作 z ^ \hat{z} z^(读作z帽或者zhat),则代表预测出的标签。模型得出的结果,一定是预测的标签

符号规范
在我们学习autograd的时候,我们说线性回归的方程是 y ^ i = b + w 1 x i 1 + w 2 x i 2 + ... + w n x i n \hat{y}i = b + w_1 x{i1} + w_2 x_{i2} + \ldots + w_n x_{in} y^i=b+w1xi1+w2xi2+...+wnxin。但在这里,为什么写做 z z z呢?首先,无论是回归问题还是分类问题,y永远表示标签(labels) 。在回归问题中,y是连续型数字,在分类问题中,y是离散型的整数。对于线性回归来说,线性方程的输出结果就是最终的标签。但对于整个深度学习体系而言,复杂神经网络的输出才是最后的标签。在我们单独对线性回归进行说明的时候,行业惯例就是使用 z z z来表示线性回归的结果。

如果考虑我们有m个样本,则回归结果可以被写作:
z ^ i = b + w 1 x i 1 + w 2 x i 2 + ... + w n x i n \hat{z}i = b + w_1 x{i1} + w_2 x_{i2} + \ldots + w_n x_{in} z^i=b+w1xi1+w2xi2+...+wnxin

其中 z ^ i \hat{z}_i z^i是包含了m个全部的样本的预测结果的列向量。注意,我们通常使用粗体的小写字母来表示列向量,粗体的大写字母表示矩阵或者行列式。 并且在机器学习中,我们默认所有的一维向量都是列向量。

我们可以使用矩阵来表示上面多个样本的回归结果的方程,其中 w w w可以被看做是一个结构为(n+1,1)的列矩阵(这里的n加上的1是我们的截距b), 是一个结构为(m,n+1)的特征矩阵(这里的n加上的1是为了与截距b相乘而留下的一列1,这列1有时也被称作 x 0 x_0 x0,则有:

z \^ 1 z \^ 2 z \^ 3 ... z \^ m \] = \[ 1 x 11 x 12 x 13 ... x 1 n 1 x 21 x 22 x 23 ... x 2 n 1 x 31 x 32 x 33 ... x 3 n ... ... ... ... ... 1 x m 1 x m 2 x m 3 ... x m n \] ∗ \[ b w 1 w 2 ... w n \] \\begin{bmatrix}\\hat{z}_1 \\\\\\hat{z}_2 \\\\\\hat{z}_3 \\\\\\ldots \\\\\\hat{z}_m\\end{bmatrix}= \\begin{bmatrix} 1 \& x_{11} \& x_{12} \& x_{13} \& \\ldots \& x_{1n} \\\\ 1 \& x_{21} \& x_{22} \& x_{23} \& \\ldots \& x_{2n} \\\\ 1 \& x_{31} \& x_{32} \& x_{33} \& \\ldots \& x_{3n} \\\\ \\ldots \& \\ldots \& \\ldots \& \\ldots \& \& \\ldots \\\\ 1 \& x_{m1} \& x_{m2} \& x_{m3} \& \\ldots \& x_{mn} \\end{bmatrix} \* \\begin{bmatrix} b \\\\ w_1 \\\\ w_2 \\\\ \\ldots \\\\ w_n \\end{bmatrix} z\^1z\^2z\^3...z\^m = 111...1x11x21x31...xm1x12x22x32...xm2x13x23x33...xm3............x1nx2nx3n...xmn ∗ bw1w2...wn z \^ = X w \\hat{z} = Xw z\^=Xw 如果在我们的方程里没有常量`b`,我们则可以不写**X** 中的第一列以及**w**中的第一行。 线性回归的任务,就是构造一个预测函数来映射输入的特征矩阵 和标签值 的**线性关系**。这个预测函数的图像是一条直线,所以线性回归的求解就是对直线的拟合过程。 **预测函数的本质就是我们需要构建的模型,而构造预测函数的核心就是找出模型的权重向量** ,也就是求解线性方程组的参数(相当于求解 y = a x + b y=ax+b y=ax+b里的 a a a与 b b b)。 现在假设,我们的数据只有`2`个特征,则线性回归方程可以写作如下结构: z \^ = b + x 1 w 1 + x 2 w 2 \\hat{z}=b+x_1w_1+x_2w_2 z\^=b+x1w1+x2w2 此时,我们只要对模型输入特征 x 1 x_1 x1, x 2 x_2 x2的取值,就可以得出对应的预测值 z \^ \\hat{z} z\^。神经网络的预测过程是从神经元左侧输入特征,让神经元处理数据,并从右侧输出预测结果。这个过程和我们刚才说到的线性回归输出预测值的过程是一致的。如果我们使用一个神经网络来表达线性回归上的过程,则可以有: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/a274e860a2d24b9e86fc6b4c636f96a6.png) 这就是一个最简单的**单层回归神经网络**的表示图。 在神经网络中,竖着排列在一起的一组神经元叫做"一层网络",所以线性回归的网络直观看起来有两层,两层神经网络通过写有参数的线条相连。我们从左侧输入常数1和特征取值 x 1 x_1 x1, x 2 x_2 x2,再让它们与相对应的参数**相乘** ,就可以得到 b b b, x 1 w 1 x_1w_1 x1w1, x 2 w 2 x_2w_2 x2w2三个结果。这三个结果通过连接到下一层神经元的直线,被输入下一层神经元。我们在第二层的神经元中将三个乘积进行**加和** (使用符号 ∑ \\sum ∑表示),就可以得到加和结果 z \^ \\hat{z} z\^,即 b + x 1 w 1 + x 2 w 2 b+x_1w_1+x_2w_2 b+x1w1+x2w2,这个值正是我们的预测值。**可见,线性回归方程与上面的神经网络图达到的效果是一模一样的**。 在上述过程中,左侧的是神经网络的**输入层** (`input layer`)。输入层由众多承载数据用的神经元组成,数据从这里输入,并流入处理数据的神经元中。在所有神经网络中,输入层永远只有一层,且每个神经元上只能承载一个特征(一个 x x x)或一个常量(通常都是`1`)。现在的二元线性回归只有两个特征,所以输入层上只需要三个神经元,包括两个特征和一个常量,其中这里的常量仅仅是被用来乘以偏差 b b b用的。对于没有偏差的线性回归来说,我们可以不设置常量`1`。 右侧的是**输出层** (`output layer`)。输出层由大于等于一个神经元组成,我们总是从这一层来获取预测结果。输出层的每个神经元上都承载着单个或多个功能,可以处理被输入神经元的数据。在线性回归中,这个功能就是"加和",当我们把加和替换成其他的功能,就能够形成各种不同的神经网络。 在神经元之间相互连接的线表示了数据流动的方向,就像人脑神经细胞之间相互联系的"轴突"。在人脑神经细胞中,轴突控制电子信号流过的强度,在人工神经网络中,神经元之间的连接线上的权重也代表了信息可通过的强度。最简单的例子是,当 w w w为`0.5`时,在特征 x 1 x_1 x1上的信息就只有`0.5`倍能够传递到下一层神经元中,因为被输入到下层神经元中去进行计算的实际值是 0.5 x 1 0.5x_1 0.5x1。相对的,如果 w 1 w_1 w1是`2.5`,则会传递`2.5`倍的 上的信息。因此,有的深度学习课程会将权重 w w w比喻成是电路中的"电压",电压越大,则电信号越强烈,电压越小,信号也越弱,这都是在描述权重 w w w会如何影响传入下一层神经元的信息/数据量的大小。 到此,我们已经了解了线性回归的网络是怎么一回事,它是最简单的回归神经网络,同时也是最简单的神经网络。类似于线性回归这样的神经网络,被称为**单层神经网络**。 | 单层神经网络 | |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 从直观来看,线性回归的网络结构明明有两层,为什么线性回归被叫做"单层神经网络"呢?业内通识是,在描述神经网络的层数的时候,**我们不考虑输入层**。输入层是每个神经网络都必须存在的一层,当使用相同的输入数据时,任意两个神经网络之间的不同之处就在输入层之后的所有层。所以,我们把输入层之后只有一层的神经网络称为单层神经网络。在非常非常少见的情况下,有的深度学习课程或教材中也会直接将所有层都算入其中,将上述网络称为"两层神经网络",这种做法虽然不太规范,但也不能称之为"错误的"。因此,当出现"N层神经网络"的描述时,一定要注意原作者是否将输入层考虑进去了。 | ### 28.2 tensor实现单层神经网络的正向传播 让我们使用一组非常简单的代码来实现一下回归神经网络求解 z \^ \\hat{z} z\^的过程,在神经网络中,这个过程是从左向右进行的,被称为神经网络的正向传播(`forward spread`)。来看下面这组数据: | x~0~ | x~1~ | x~2~ | z | |:----:|:----:|:----:|:-----:| | 1 | 0 | 0 | -0.2 | | 1 | 1 | 0 | -0.05 | | 1 | 0 | 1 | -0.05 | | 1 | 1 | 1 | 0.1 | 我们将构造能够**拟合**出以上数据的单层回归神经网络 ```python import torch X = torch.tensor([[1,0,0],[1,1,0],[1,0,1],[1,1,1]], dtype = torch.float32) z = torch.tensor([[-0.2],[-0.05], [-0.05],[0.1]]) w = torch.tensor([-0.2,0.15,0.15]) def LinearR(X,w): zhat = torch.mv(X,w) return zhat zhat = LinearR(X,w) ``` ### 28.3 tensor计算中的新手陷阱 接下来,我们对这段代码进行详细的说明 ```python # 导入库 import torch # 首先生成特征张量 X = torch.tensor([[1, 0, 0], [1, 1, 0], [1, 0, 1], [1, 1, 1]]) # 我们输入的是整数,默认生成的是int64的类型 # 生成标签 z = torch.tensor([-0.2, -0.05, -0.05, 0.1]) # 我们输入的是浮点数,默认生成的是float32的类型 # 定义常量b和权重w w = torch.tensor([-0.2, 0.15, 0.15]) # 注意,常量b所在的位置必须与特征张量中X中全为1的那一列所在位置相对应 ``` **tensor计算中的第一大坑:PyTorch的静态性** > 在前几节有提到过:静态性指的是对输入的张量类型有明确的要求,例如部分函数只能输入浮点型张量,而不能输入整型张量。 ```python # 定义线性回归计算的函数 def LinearR(X, w): # 矩阵与向量相乘时,向量必须作为mv的第二个参数 zhat = torch.mv(X, w) return zhat ``` ```python LnearR(X,w) # output : RuntimeError : expected scalar type Long but found Float ``` > ps:Long = int64 `PyTorch`中的许多函数都不接受浮点型的分类标签,但也有许多函数要求真实标签的类型必须与预测值的类型一致,因此标签的类型定义总是一个容易踩坑的地方。通常来说,我们还是回将标签定义为`float32`,如果在函数运行时报错,要求整形,我们再使用`.long()`方法将其转换为整型。 另一个非常容易踩坑的地方是,`PyTorch`中许多函数不接受一维张量但同时也有许多函数不接受二维标签`( ̄_ ̄|||)`。因此我们在生成标签时,可以默认生成二维标签,若函数报错说不能接受二维标签,我们再使用`view()`函数将其调整为一维。 ```python # 因此之后需要改成: def LinearR(X, w): zhat = torch.mv(X.float(), w) return zhat # 还可以使用大写的Tensor来解决这个问题,但这个方法并不推荐 X = torch.Tensor([[1, 0, 0], [1, 1, 0], [1, 0, 1], [1, 1, 1]]) # 或者直接养成好习惯 X = torch.Tensor([[1, 0, 0], [1, 1, 0], [1, 0, 1], [1, 1, 1]], dtype = torch.float32) ``` ```python LinearR(X,w) # output : tensor([-0.2000, -0.0500, -0.0500, 0.1000]) ``` > torch.tensor------判断你的输入类型是什么类型,然后根据你输入的数据类型来确定结果的数据类型 > > torch.Tensor------无论你输入什么数据,都无脑使用float32 **tensor计算中的第二大坑:精度问题** ```python # 预测值 zhat = LinearR(X, w) # 真实值 z = torch.tensor([[-0.2],[-0.05], [-0.05],[0.1]], dtype = torch.float32) ``` ```python zhat == z # output : tensor([ True, False, False, False]) ``` `False`一定不是因为数据类型发生错误得出的,因为我们已经把`z`的数据类型改为了浮点型。 在多元线性回归中,我们使用`SSE`(误差平方和)来衡量回归的结果优劣: S S E = ∑ i = 1 m ( z i − z \^ i ) 2 SSE = \\sum_{i=1}\^{m}(z_i - \\hat{z}_i)\^2 SSE=i=1∑m(zi−z\^i)2 如果预测值和真实值完全相等,那`SSE`的结果应该为`0`。在这里,`SSE`虽然非常接近`0`,但的确是不为`0`的。 ```python SSE = sum((zhat - z) ** 2) SSE # output : tensor(8.3267e-17) ``` ```python #设置显示精度,再来看yhat与y_reg torch.set_printoptions(precision=30) #看小数点后面30位的情况 ``` ```python zhat # output : tensor([-0.200000002980232238769531250000, -0.049999997019767761230468750000, -0.049999997019767761230468750000, 0.100000008940696716308593750000]) ``` ```python z # output : tensor([-0.200000002980232238769531250000, -0.050000000745058059692382812500, -0.050000000745058059692382812500, 0.100000001490116119384765625000]) ``` `zhat`和`z`的差异有两个原因: * `float32`由于只保留`32`位,所以精确性会有一些问题。 * `torch.mv`这个函数在进行计算时,内部计算时会出现一些很微小的精度问题。 精度问题在`tensor`维度很高,数字很大时,也会变得更大 ```python preds = torch.ones(300,68,64,64) * 0.1 preds.sum() * 10 # output : tensor(83558352.) ``` ```python preds = torch.ones(300,68,64,64) preds.sum() # output : tensor(83558400.) ``` *怎么解决这个问题呢?* 与`python`中存在`decimal`库不同,`pytorch`设置了`64`位浮点数来 **尽量** 减轻精度问题 ```python preds = torch.ones(300,68,64,64,dtype = torch.float64) *0.1 preds.sum() * 10 # output : tensor(83558400.000000059604644775390625000000, dtype=torch.float64) ``` 但即便如此,也不能完全消除精度问题带来的区别 如果你希望能够无视掉非常小的区别,而让两个张量的比较结果展示为True,可以使用下面的代码 ```python torch.allclose(zhat, z) ``` ### 28.4 torch.nn.Linear实现单层回归神经网络的正向传播 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/acbe2839f00847d8bd325cecb7e6c8b7.png) 上面为`pytorch`的架构图,从图中我们可以看到,`torch.nn`是包含了构筑神经网络结构基本元素的包,在这个包中可以找到任意的神经网络层。这些神经网络层都是`nn.Module`这个大类的子类。 我们的`torch.nn.Linear`就是神经网络中的"线性层",它可以实现形如 z \^ = X w \\hat{z}=Xw z\^=Xw的加和功能。 在单层回归神经网络结构图中,`torch.nn.Linear`类表示了我们的输出层。现在我们就来看看它是如何使用的。 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/b176b0f36d394fc9b0bff1beebaa00a8.png) 回顾一下我们的数据: | x~0~ | x~1~ | x~2~ | z | |:----:|:----:|:----:|:-----:| | 1 | 0 | 0 | -0.2 | | 1 | 1 | 0 | -0.05 | | 1 | 0 | 1 | -0.05 | | 1 | 1 | 1 | 0.1 | 接下来,使用`nn.Linear`来实现单层回归神经网络: ```python import torch X = torch.tensor([[0,0],[1,0],[0,1],[1,1]],dtype = torch.float32) output = torch.nn.Linear(2, 1) zhat = output(X) ``` * `nn.Linear`是一个类,在这里代表了输出层,所以使用了`output`作为变量名,`output = `这一行相当于是类的实例化过程。 * 实例化的时候,`nn.Linear`需要输入两个参数,分别是(上一层的神经元个数,这一层的神经元个数)。上一层是输出层,因此神经元个数由特征的个数决定(`2`个)。这一层是输出层,作为回归神经网络,输出层只有一个神经元。因此`nn.Linear`中输入的是(`2,1`)。 * 上面只定义了`X`,没有定义`w`和`b`。所有`nn.Module`的子类,形如`nn.XXX`的层,都会在实例化的同时随机生成`w`和`b`的初始值。所以实例化之后,我们就可以调用以下属性来查看生成的 w w w和 b b b: ```python # 查看生成的w output.weight # output : Parameter containing:tensor([[ 0.683788955211639404296875000000, -0.588803172111511230468750000000]],requires_grad=True) # 查看生成的b output.bias # output : Parameter containing:tensor([0.426940977573394775390625000000], requires_grad=True) ``` * 其中,`w`是必然会生成的,`b`是我们可以控制是否要生成的。 ```python output = torch.nn.Linear(2, 1, bias = False) ``` * 由于`w`和`b`是随机生成的,所以同样的代码运行多次后的结果是不一致的。如果我们希望控制随机性,则可以使用`torch`中的`random`类。如下所示: ```python torch.random.manual_seed(420) # 人为设置随机数种子 ``` * 由于不需要定义常量`b`,因此在特征张量中,也不需要留出与常数项相乘的x~0~那一列,在输入数据时,我们只输入了两个特征x~1~和x~2~。 * 输入层只有一层,且输入层的结构(神经元的个数)由输入的特征张量 **X** 决定,因此在`pytorch`中构筑神经网络时,不需要定义输入层。 * 实例化之后,将特征张量输入到实例化后的类中,即可得到输出层的输出结果。 由于我们没有自己定义`w`和`b`,所以无法让`nn.Linear`输出的`zhat`与我们真实的`z`接近------让真实值与预测值差异更小的部分,我们会在之后进行讲解。 ## 29 二分类神经网络:逻辑回归 ### 29.1 二分类神经网络的理论基础 线性回归是统计学经典算法,它能够拟合出一条直线来描述变量之间的 **线性关系** 。但 **在实际中,变量之间的关系通常都不是一条直线,而是呈现出某种曲线关系** 。在统计学的历史中,为了让统计学模型能够更好地拟合曲线,统计学家们在线性回归的方程两边引入了联系函数(`link function`),对线性回归的方程做出了各种各样的变化,并将这些变化后的方程称为"广义线性回归"。其中比较著名的有等式两边同时取对数的对数函数回归、同时取指数的`S`形函数回归等。 y = a x + b → ln ⁡ y = ln ⁡ ( a x + b ) y = a x + b → e y = e a x + b \\begin{align\*} y \&= ax + b \\quad \\rightarrow \\quad \\ln y = \\ln(ax + b) \\\\ y \&= ax + b \\quad \\rightarrow \\quad e\^y = e\^{ax + b} \\end{align\*} yy=ax+b→lny=ln(ax+b)=ax+b→ey=eax+b 在探索的过程中,一种奇特的变化吸引了统计学家们的注意,这个变化就是`sigmoid`函数带来的变化。 `Sigmoid`函数的公式如下: σ = S i g m o i d ( z ) = 1 1 + e − z \\sigma = Sigmoid(z) = \\frac{1}{1 + e\^{-z}} σ=Sigmoid(z)=1+e−z1 其中 e e e为自然常数(约为`2.71828`),其中 z z z是它的自变量, σ \\sigma σ是因变量, z z z的值常常是线性模型的取值(比如,线性回归的结果 z z z)。`Sigmoid`函数是一个`S`型的函数,它的图像如下: ![](https://i-blog.csdnimg.cn/20230724024159.png) 从图像上就可以看出,这个函数的性质相当特别。当自变量 z z z趋近正无穷时,因变量 σ \\sigma σ趋近于`1`,而当 z z z趋近负无穷时, σ \\sigma σ趋近于`0`,这使得`sigmoid`函数能够将任何实数映射到`(0,1)`区间。同时,`Sigmoid`的导数在$ z=0 点时最大(这一点的斜率最大),所以它可以快速将数据从 点时最大(这一点的斜率最大),所以它可以快速将数据从 点时最大(这一点的斜率最大),所以它可以快速将数据从z=0$的附近排开,让数据点到远离自变量取`0`的地方去。这样的性质,让`sigmoid`函数拥有**将连续性变量 转化为离散型变量 的力量,这也就是化回归算法为分类算法的力量**。 具体怎么操作呢?只要将线性回归方程的结果作为自变量带入`sigmoid`函数,得出的数据就一定是`(0,1)`之间的值。此时,只要我们设定一个阈值(比如`0.5`),规定 `大于0.5`时,预测结果为`1`类, `小于0.5`时,预测结果为`0`类,则可以顺利将回归算法转化为分类算法。此时,我们的标签就是类别`0和1`了。这个阈值可以自己调整,在没有调整之前,一般默认`0.5`。 σ = 1 1 + e − z = 1 1 + e − X w \\sigma = \\frac{1}{1 + e\^{-z}} = \\frac{1}{1 + e\^{-Xw}} σ=1+e−z1=1+e−Xw1 更神奇的是,当我们对线性回归的结果取`sigmoid`函数之后,只要再进行以下操作: 1)将结果 σ \\sigma σ以几率 ( σ 1 − σ ) \\left(\\frac{\\sigma}{1-\\sigma}\\right) (1−σσ)的形式展现 2)在几率上求以`e`为底的对数 就很容易得到: ln ⁡ σ 1 − σ = ln ⁡ ( 1 1 + e − X w 1 − 1 1 + e − X w ) = ln ⁡ ( 1 1 + e − X w e − X w 1 + e − X w ) = ln ⁡ ( 1 e − X w ) = ln ⁡ ( e X w ) = X w \\begin{align\*} \\ln \\frac{\\sigma}{1-\\sigma} \&= \\ln \\left( \\frac{\\frac{1}{1+e\^{-Xw}}}{1 - \\frac{1}{1+e\^{-Xw}}} \\right) \\\\ \&= \\ln \\left( \\frac{\\frac{1}{1+e\^{-Xw}}}{\\frac{e\^{-Xw}}{1+e\^{-Xw}}} \\right) \\\\ \&= \\ln \\left( \\frac{1}{e\^{-Xw}} \\right) \\\\ \&= \\ln (e\^{Xw}) \\\\ \&= Xw \\end{align\*} ln1−σσ=ln(1−1+e−Xw11+e−Xw1)=ln(1+e−Xwe−Xw1+e−Xw1)=ln(e−Xw1)=ln(eXw)=Xw 不难发现,让 σ \\sigma σ取对数几率后所得到的值就是我们线性回归的 z z z!因为这个性质,在等号两边加`sigmoid`的算法被称为"对数几率回归",在英文中就是`Logistic Regression`,就是**逻辑回归** 。逻辑回归可能是广义线性回归中最广为人知的算法,它是一个叫做"回归"实际上却总是被用来做分类的算法,对机器学习和深度学习都有重大的意义。在面试中,如果我们希望了解一个人对机器学习的理解程度,第一个问题可能就会从`sigmoid`函数以及逻辑回归是如何来的开始。 | σ \\sigma σ值代表了样本为某一类标签的概率 | |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ln ⁡ σ 1 − σ \\ln \\frac{\\sigma}{1 - \\sigma} ln1−σσ是形似对数几率的一种变化。而几率`odds`的本质其实是 p 1 − p \\frac{p}{1-p} 1−pp,其中p是事件A发生的概率,而`1-p`是事件`A`不会发生的概率,并且`p+(1-p)=1`。因此,很多人在理解逻辑回归时,都对 σ \\sigma σ做出如下的解释:我们让线性回归结果逼近`0`和`1`,此时 σ \\sigma σ和 1 − σ 1-\\sigma 1−σ之和为1,因此它们可以被我们看作是一对**正反例发生的概率** ,即 σ \\sigma σ是某样本`i`的标签被预测为`1`的概率,而 1 − σ 1-\\sigma 1−σ是`i`的标签被预测为`0`的概率, σ 1 − σ \\frac{\\sigma}{1-\\sigma} 1−σσ就是样本i的标签被预测为1的相对概率。基于这种理解,逻辑回归、即单层二分类神经网络返回的结果被当成是概率来看待和使用(如果直接说它就是概率,或许不太严谨)。每当我们希望求解"样本`i`的标签是`1`或是`0`的概率"时,我们就使用逻辑回归。因此,当一个样本对应的 σ i \\sigma_i σi越接近`1`或`0`,我们就认为逻辑回归对这个样本的预测结果越肯定,样本被分类正确的可能性也越高。如果 σ i \\sigma_i σi非常接近阈值(比如0.5),就说明逻辑回归其实对这个样本究竟应该是哪一类别,不是非常肯定。 | ### 29.2 tensor实现二分类神经网络的正向传播 我们可以在`PyTorch`中非常简单地实现逻辑回归的预测过程,让我们来看下面这一组数据。很容易注意到,这组数据和上面的回归数据的特征( x 1 , x 2 x_1,x_2 x1,x2)是完全一致的,只不过标签`y`由连续型结果转变为了分类型的`0`和`1`。这一组分类的规律是这样的:当两个特征都为`1`的时候标签就为`1`,否则标签就为`0`。这一组特殊的数据被我们称之为 **"与门"(AND GATE)** ,这里的"与"正是表示"特征一与特征二都是`1`"的含义。 | x~0~ | x~1~ | x~2~ | andgate | |:----:|:----:|:----:|:-------:| | 1 | 0 | 0 | 0 | | 1 | 1 | 0 | 0 | | 1 | 0 | 1 | 0 | | 1 | 1 | 1 | 1 | 要**拟合** 这组数据,只需要在刚才我们写好的代码后面加上`sigmoid`函数以及阈值处理后的变化。 ```python import torch X = torch.tensor([[1, 0, 0], [1, 1, 0], [1, 0, 1], [1, 1, 1]], dtype = torch.float32) andgate = torch.tensor([-0.2, 0.15, 0.15], dtype = torch.float32) # 保险起见,生成二维的、float32类型的标签 w = torch.tensor([-0.2,0.15,0.15], dtype = torch.float32) def LogisticR(X,w): zhat = torch.mv(X,w) sigma = 1/(1+torch.exp(-zhat)) #sigma = torch.sigmoid(zhat) andhat = torch.tensor([int(x) for x in sigma >= 0.5], dtype = torch.float32) return sigma, andhat ``` 接下来,我们对这段代码进行详细的说明: ```python # 导入torch库 import torch # 特征张量,养成良好习惯,上来就定义数据类型 X = torch.tensor([[1,0,0],[1,1,0],[1,0,1],[1,1,1]], dtype = torch.float32) #标签,分类问题的标签是整型 andgate = torch.tensor([0,0,0,1], dtype = torch.float32) #定义w,注意这一组w与之前在回归中使用的完全一样 w = torch.tensor([-0.2,0.15,0.15], dtype = torch.float32) def LogisticR(X,w): #首先执行线性回归的过程,依然是mv函数,让矩阵与向量相乘得到z zhat = torch.mv(X,w) #执行sigmoid函数,你可以调用torch中的sigmoid函数,也可以自己用torch.exp来写 sigma = torch.sigmoid(zhat) #sigma = 1/(1+torch.exp(-zhat)) #设置阈值为0.5, 使用列表推导式将值转化为0和1 andhat = torch.tensor([int(x) for x in sigma >= 0.5], dtype = torch.float32) return sigma, andhat ``` ```python sigma, andhat = LogisticR(X,w) ``` ```python sigma # output : tensor([0.450166016817092895507812500000, 0.487502634525299072265625000000,0.487502634525299072265625000000, 0.524979174137115478515625000000]) ``` ```python andhat # output : tensor([0., 0., 0., 1.]) ``` ```python andgate == andhat #最后得到的都是0和1,虽然andhat数据格式是float32,但本质上数还是整数,不存在精度问题 ``` 可见,这里得到了与我们期待的结果一致的结果,这就将回归算法转变为了二分类。这个过程在神经网络中的表示图如下: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/0f3c77cdcc724b2fa843168507160c34.png) 可以看出,这个结构与线性回归的神经网络唯一不同的就是**输出层中多出了一个Sigmoid(z)** 。当有了`Sigmoid`函数的结果 σ \\sigma σ之后,只要了解阈值是`0.5`(或者任意我们自己设定的数值),就可以轻松地判断任意样本的预测标签 y \^ \\hat{y} y\^。在二分类神经网络中,`Sigmoid`实现了将连续型数值转换为分类型数值的作用,在现代神经网络架构中,除了`Sigmoid`函数之外,还有许多其他的函数可以被用来将连续型数据分割为离散型数据,接下来,我们就介绍一下这些函数。 ### 29.3 符号函数sign,ReLU,Tanh **符号函数sign** ![](https://i-blog.csdnimg.cn/blog_migrate/095f9da149b392061c40d387a7c5207a.png) 我们可以使用以下表达式来表示它: y = { 1 if z \> 0 0 if z = 0 − 1 if z \< 0 y = \\begin{cases} 1 \& \\text{if } z \> 0 \\\\ 0 \& \\text{if } z = 0 \\\\ -1 \& \\text{if } z \< 0 \\end{cases} y=⎩ ⎨ ⎧10−1if z\>0if z=0if z\<0 由于函数的取值是间断的,**符号函数也被称为"阶跃函数"** ,表示在`0`的两端,函数的结果`y`是从`-1`直接阶跃到了`1`。在这里,我们使用`y`而不是 σ \\sigma σ来表示输出的结果,是因为输出结果直接是`0`、`1`、`-1`这样的类别,就相当于标签了。对于`sigmoid`函数而言, 返回的是`0~1`之间的概率值,如果我们希望获取最终预测出的类别,还需要将概率转变成`0`或`1`这样的数字才可以。但符号函数可以直接返回类别,因此我们可以认为符号函数输出的结果就是最终的预测结果`y`。在二分类中,符号函数也可以忽略中间的时候,直接分为`0`和`1`两类,用如下式子表示: y = { 1 if z \> 0 0 if z ≤ 0 y = \\begin{cases} 1 \& \\text{if } z \> 0 \\\\ 0 \& \\text{if } z \\leq 0 \\end{cases} y={10if z\>0if z≤0 等号被并在上方或下方都可以。这个式子可以很容易被转化为下面的式子: ∵ z = w 1 x 1 + w 2 x 2 + b ∴ y = { 1 if w 1 x 1 + w 2 x 2 + b \> 0 0 if w 1 x 1 + w 2 x 2 + b ≤ 0 ∴ y = { 1 if w 1 x 1 + w 2 x 2 \> − b 0 if w 1 x 1 + w 2 x 2 ≤ − b \\because z = w_1 x_1 + w_2 x_2 + b \\\\ \\therefore y = \\begin{cases} 1 \& \\text{if } w_1 x_1 + w_2 x_2 + b \> 0 \\\\ 0 \& \\text{if } w_1 x_1 + w_2 x_2 + b \\leq 0 \\end{cases} \\\\ \\therefore y = \\begin{cases} 1 \& \\text{if } w_1 x_1 + w_2 x_2 \> -b \\\\ 0 \& \\text{if } w_1 x_1 + w_2 x_2 \\leq -b \\end{cases} ∵z=w1x1+w2x2+b∴y={10if w1x1+w2x2+b\>0if w1x1+w2x2+b≤0∴y={10if w1x1+w2x2\>−bif w1x1+w2x2≤−b 此时, − b -b −b就是一个阈值,我们可以使用任意字母来替代它,比较常见的是字母 θ \\theta θ 。当然,不把它当做阈值,依然保留 w 1 x 1 + w 2 x 2 + b w_1x_1+w_2x_2+b w1x1+w2x2+b与`0`进行比较的关系也没有任何问题。和`sigmoid`一样,我们也可以使用阶跃函数来处理"与门"的数据: ```python import torch X = torch.tensor([[0,0],[1,0],[0,1],[1,1]],dtype=torch.float32) andgate = torch.tensor([[0],[0],[0],[1]], dtype = torch.float32) w = torch.tensor([-0.2,0.15, 0.15], dtype = torch.float32) def LinearRwithsign(X,w): zhat = torch.mv(X,w) andhat = torch.tensor([int(x) for x in zhat >= 0], dtype = torch.float32) return zhat, andhat ``` 阶跃函数和`sigmoid`都可以完成二分类的任务。在神经网络的二分类中, 的默认取值一般都是`sigmoid`函数,少用阶跃函数,这是由神经网络的解法决定的。 **ReLU** `ReLU(Rectified Linear Unit)`函数又名整流线型单元函数,应用甚至比`sigmoid`更广泛。`ReLU`提供了一个很简单的非线性变换:当输入的自变量大于`0`时,直接输出该值,当输入的自变量小于等于`0`时,输出`0`。这个过程可以用以下公式表示出来: R e L U : σ = { z ( z \> 0 ) 0 ( z ≤ 0 ) ReLU: \\sigma = \\begin{cases} z \& (z \> 0) \\\\ 0 \& (z \\leq 0) \\end{cases} ReLU:σ={z0(z\>0)(z≤0) `ReLU`函数是一个非常简单的函数,本质就是`max(0,z)`。`max`函数会从输入的数值中选择较大的那个值进行输出,以达到保留正数元素,将负元素清零的作用。`ReLU`的图像如下所示: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/d3b82e033597403ea5e329bc74b56f9d.png) 相对的,`ReLU`函数导数的图像如下: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/de6d1c5d97ad40c4b51c6d29b5819986.png) 当输入 z z z为正数时,`ReLU`函数的导数为`1`,当 z z z为负数时,`ReLU`函数的导数为`0`,当输入为`0`时,`ReLU`函数不可导。因此,`ReLU`函数的导数图像看起来就是阶跃函数,这是一个美好的巧合。 **tanh** `tanh(hyperbolic tangent)`是双曲正切函数,双曲正切函数的性质与`sigmoid`相似,它能够将数值压缩到`(-1,1)`区间内。 t a n h : σ = e 2 z − 1 e 2 z + 1 tanh: \\sigma = \\frac{e\^{2z} - 1}{e\^{2z} + 1} tanh:σ=e2z+1e2z−1 而双曲正切函数的图像如下: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/ee5af1e80d4048d4bdb609d1f5ced4ac.png) 可以看出,`tanh`的图像和`sigmoid`函数很像,不过`sigmoid`函数的范围是在`(0,1)`之间,`tanh`却是在坐标系的原点`(0,0)`点上中心对称。 对`tanh`求导后可以得到如下公式和导数图像: tanh ⁡ ′ ( z ) = 1 − tanh ⁡ 2 ( z ) \\tanh'(z) = 1 - \\tanh\^2(z) tanh′(z)=1−tanh2(z) ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/30f041c4234e45df852e2c757a230816.png) 可以看出,当输入的 约接近于0,tanh函数导数也越接近最大值1,当输入越偏离0时,tanh函数的导数越接近于0。\*\*这些函数是最常见的二分类转化函数,他们在神经网络的结构中有着不可替代的作用。\*\*在单层神经网络中,这种作用是无法被体现的,因此关于这一点,我们可以之后再进行说明。到这里,我们只需要知道这些函数都可以将连续型数据转化为二分类就足够了。 ### 29.4 torch.functional实现二分类神经网络的正向传播 之前我们使用torch.nn.Linear类实现了单层回归神经网络,现在我们试着来实现单层二分类神经网络,也就是逻辑回归。逻辑回归与线性回归的唯一区别,就是在线性回归的结果之后套上了sigmoid函数。 不难想象,只要让nn.Linear的输出结果再经过sigmoid函数,就可以实现逻辑回归的正向传播了。 在`PyTorch`中,我们几乎总是从`nn.functional`中调用相关函数。 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/6857705a253b4a208286ecc377c0d5cf.png) 回顾一下我们的数据和网络架构: | x~0~ | x~1~ | x~2~ | andgate | |:----:|:----:|:----:|:-------:| | 1 | 0 | 0 | 0 | | 1 | 1 | 0 | 0 | | 1 | 0 | 1 | 0 | | 1 | 1 | 1 | 1 | 接下来,我们在之前线性回归代码的基础上,加上`nn.functional`来实现单层二分类神经网络: ```python import torch from torch.nn import functional as F X = torch.tensor([[0,0],[1,0],[0,1],[1,1]], dtype = torch.float32) torch.random.manual_seed(420) #人为设置随机数种子 dense = torch.nn.Linear(2,1) zhat = dense(X) sigma = F.sigmoid(zhat) y = [int(x) for x in sigma > 0.5] ``` 在这里,`nn.Linear`虽然依然是输出层,但却没有担任最终输出值的角色,因此这里我们使用`dense`作为变量名。`dense`表示紧密链接的层,即上一层的大部分神经元都与这一层的大部分神经元相连,在许多神经网络中我们都会用到密集链接的层,因此`dense`是我们经常会用到的一个变量名。我们将数据从`nn.Linear`传入,得到`zhat`,然后再将`zhat`的结果传入`sigmoid`函数,得到`sigma`,之后再设置阈值为`0.5`,得到最后的`y`。 在`PyTorch`中,我们可以从`functional`模块里找出大部分之前我们提到的函数 ```python #符号函数sign torch.sign(zhat) #ReLU F.relu(zhat) #tanh torch.tanh(zhat) ``` 在`PyTorch`的安排中,符号函数`sign`与双曲正切函数`tanh`更多时候只是被用作数学计算工具,而`ReLU`和`Sigmoid`却作为神经网络的组成部分被放在库`functiona`l中,这其实反映出实际使用时大部分人的选择。 `ReLU`与`Sigmoid`还是主流的、位于`nn.Linear`后的函数。 ## 30 多分类神经网络:Softmax回归 ### 30.1 认识softmax函数 之前介绍分类神经网络时,我们只说明了二分类问题,即标签只有两种类别的问题(0和1,猫和狗)。虽然在实际应用中,许多分类问题都可以用二分类的思维解决,但依然存在很多多分类的情况,最典型的就是手写数字的识别问题。计算机在识别手写数字时,需要对每一位数字进行判断,而个位数字总共有`10`个(0\~9),所以手写数字的分类是十分类问题,一般分别用0\~9表示。 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/b586ea485642457a99e901c92ee80e33.png) `Softmax`函数是深度学习基础中的基础,它是神经网络进行多分类时,默认放在输出层中处理数据的函数。假设现在神经网络是用于三分类数据,且三个分类分别是苹果,柠檬和百香果,序号则分别是分类`1`、分类`2`和分类`3`。则使用`softmax`函数的神经网络的模型会如下所示: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/a9455e0abfc94d4ba72b93b02ad7725c.png) 与二分类一样,我们从网络左侧输入特征,从右侧输出概率,且概率是通过线性回归的结果 z z z外嵌套`softmax`函数来进行计算。在二分类时,输出层只有一个神经元,只输出样本对于正类别的概率(通常是标签为`1`的概率),而`softmax`的输出层有三个神经元,分别输出该样本的真实标签是苹果、柠檬或百香果的概率 σ 1 , σ 2 , σ 3 \\sigma_1,\\sigma_2,\\sigma_3 σ1,σ2,σ3。**在多分类中,神经元的个数与标签类别的个数是一致的** ,如果是十分类,在输出层上就会存在十个神经元,分别输出十个不同的概率。此时,**样本的预测标签就是所有输出的概率 σ 1 , σ 2 , σ 3 \\sigma_1,\\sigma_2,\\sigma_3 σ1,σ2,σ3中最大的概率对应的标签类别**。 那每个概率是如何计算出来的呢?来看Softmax函数的公式: σ k = Softmax ( z k ) = e z k ∑ K e z \\sigma_k = \\text{Softmax}(z_k) = \\frac{e\^{z_k}}{\\sum\^{K} e\^z} σk=Softmax(zk)=∑Kezezk 其中 e e e为自然常数(约为`2.71828`), 与`sigmoid`函数中的 z z z一样,表示回归类算法(如线性回归)的结果。 表示该数据的标签中总共有 K K K个标签类别,如三分类时 K = 3 K=3 K=3,四分类时 K = 4 K=4 K=4。 k k k表示标签类别 k k k类。很容易可以看出,`Softmax`函数的分子是多分类状况下某**一个** 标签类别的回归结果的指数函数,分母是多分类状况下**所有** 标签类别的回归结果的指数函数之和,因此**Softmax\*\*\*\*函数的结果代表了样本的** 结果为类别 k k k的概率\*\*。 ### 30.2 Pytorch中的softmax函数 我们曾经提到过,神经网络是模型效果很好,但运算速度非常缓慢的算法。`softmax`函数也存在相同的问题------它可以将多分类的结果转变为概率(这是一个极大的优势),但它需要的计算量非常巨大。由于`softmax`的分子和分母中都带有 e e e为底的指数函数,所以在计算中非常容易出现极大的数值。 ![在这里插入图片描述](https://i-blog.csdnimg.cn/img_convert/abd0cd60d2421ba9ed4dcc12c1ed0364.png) 如上图所示, e^10^就已经等于`20000`了,而回归结果 z z z完全可能是成千上万的数字。事实上e^100^会变成一个后面有40多个0的超大值,e^1000^则会直接返回无限大`inf`,这意味着这些数字已经超出了计算机处理数时要求的有限数据宽度,超大数值无法被计算机运算和表示。这种现象叫做"溢出",当计算机返回"内存不足"或`Python`服务器直接断开连接的时候,可能就是发生了这个问题。来看看这个问题实际发生时的状况: ```python #对于单一样本,假定一组巨大的z z = torch.tensor([1010,1000,990], dtype=torch.float32) torch.exp(z) / torch.sum(torch.exp(z)) # softmax函数的运算 # output : tensor([nan, nan, nan]) ``` 因此,我们一般不会亲自使用`tensor`来手写`softmax`函数。在`PyTorch`中,我们往往使用内置好的`softmax`函数来计算`softmax`的结果,我们可以使用`torch.softmax`来轻松的调用它,具体代码如下: ```python z = torch.tensor([1010,1000,990], dtype=torch.float32) torch.softmax(z,0) #你也可以使用F.softmax, 它返回的结果与torch.softmax是完全一致的 ``` ```python #假设三个输出层神经元得到的z分别是10,9,5 z = torch.tensor([10,9,5], dtype=torch.float32) torch.exp(z) / torch.sum(torch.exp(z)) # softmax函数的运算 z = torch.tensor([10,9,5], dtype=torch.float32) torch.softmax(z,0) # 第二个参数表示计算的维度索引 # output : tensor([0.7275, 0.2676, 0.0049]) ``` 从上面的结果可以看出,`softmax`函数输出的是从`0`到`1.0`之间的实数,而且多个输出值的总和是`1`。因为有了这个性质,我们可以把`softmax`函数的输出解释为"概率",这和我们使用`sigmoid`函数之后认为函数返回的结果是概率异曲同工。从结果来看,我们可以认为返回了我们设定的 `([10,9,5])`的这个样本的结果应该是第一个类别(也就是`z=10`的类别),因为类别1的概率是最大的 需要注意的是,使用了`softmax`函数之后,各个 之间的大小关系并不会随之改变,这是因为指数函数e^z^是单调递增函数,也就是说,使用`softmax`之前的 如果比较大,那使用`softmax`之后返回的概率也依然比较大。这是说,**无论是否使用softmax,我们都可以判断出样本被预测为哪一类,我们只需要看最大的那一类就可以了** 。所以,在神经网络进行分类的时候,如果不需要了解具体分类问题每一类的概率是多少,而只需要知道最终的分类结果,我们可以省略输出层上的`softmax`函数。 ### 30.3 使用nn.Linear与functional实现多分类神经网络的正向传播 ```python import torch from torch.nn import functional as F X = torch.tensor([[0,0],[1,0],[0,1],[1,1]], dtype = torch.float32) torch.random.manual_seed(420) dense = torch.nn.Linear(2,3) #此时,输出层上的神经元个数是3个,因此应该是(2,3) zhat = dense(X) sigma = F.softmax(zhat,dim=1) #此时需要进行加和的维度是1 ``` ## 31 回归vs二分类vs多分类 到这里,我们已经见过了三个不同的神经网络: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/6683f7eb3f4945b7a65cbc3ad71eccce.png) 注意到有什么相似和不同了吗? 首先可能会注意到的是,这三个神经网络都是单层神经网络,除了输入层,他们都有且只有一层网络。实际上,现实中使用的神经网络几乎`99%`都是多层的,但我们的网络也能够顺利进行预测,这说明单层神经网络其实已经能够实现基本的预测功能。同时,这也说明了一个问题,无论处理的是回归还是分类,神经网络的处理原理是一致的。实际上,就连算法的限制、优化方法和求解方法也都是一致的。回归和分类神经网络唯一的不同只有输出层上的 σ \\sigma σ。 虽然线性回归看起来并没有 σ \\sigma σ的存在,但实际上我们可以认为线性回归中的 σ \\sigma σ是一个恒等函数(`identityfunction`),即是说 σ ( z ) = z \\sigma(z)=z σ(z)=z(相当于 y = x y=x y=x,或 f ( x ) = x f(x)=x f(x)=x)。而多分类的时候也可以不采用任何函数,只观察 z z z的大小,所以多分类也可以被认为是利用了恒等函数作为 σ \\sigma σ。总结来说,回归和分类对应的 σ \\sigma σ分别如下: | 输出类型 | σ \\sigma σ | |:----:|:-------------------------------------:| | 回归 | 恒等函数 | | 二分类 | `sigmoid`或任意可以实现二分类的函数(通常都是`sigmoid`) | | 多分类 | `softmax`或恒等函数 | 第二个很容易发现的现象是,只有多分类的情况在输出层出现了超过一个神经元。实际上,当处理单标签问题时(即只有一个`y`的问题),回归神经网络和二分类神经网络的输出层永远只有一个神经元,而只有多分类的情况才会让输出层上超过一个神经元。

相关推荐
卧式纯绿3 分钟前
每日文献(八)——Part one
人工智能·yolo·目标检测·计算机视觉·目标跟踪·cnn
巷95510 分钟前
OpenCV图像形态学:原理、操作与应用详解
人工智能·opencv·计算机视觉
深蓝易网39 分钟前
为什么制造企业需要用MES管理系统升级改造车间
大数据·运维·人工智能·制造·devops
带娃的IT创业者41 分钟前
《Python实战进阶》No39:模型部署——TensorFlow Serving 与 ONNX
pytorch·python·tensorflow·持续部署
xiangzhihong81 小时前
Amodal3R ,南洋理工推出的 3D 生成模型
人工智能·深度学习·计算机视觉
狂奔solar1 小时前
diffusion-vas 提升遮挡区域的分割精度
人工智能·深度学习
资源大全免费分享1 小时前
MacOS 的 AI Agent 新星,本地沙盒驱动,解锁 macOS 操作新体验!
人工智能·macos·策略模式
跳跳糖炒酸奶2 小时前
第四章、Isaacsim在GUI中构建机器人(2):组装一个简单的机器人
人工智能·python·算法·ubuntu·机器人
AI.NET 极客圈2 小时前
AI与.NET技术实操系列(四):使用 Semantic Kernel 和 DeepSeek 构建AI应用
人工智能·.net
Debroon2 小时前
应华为 AI 医疗军团之战,各方动态和反应
人工智能·华为