机器学习中核函数(Kernel)的理解与Kernel-SVM原理解析

http://dawnote.net/2018/01/13/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%E4%B8%AD%E6%A0%B8%E5%87%BD%E6%95%B0-Kernel-%E7%9A%84%E7%90%86%E8%A7%A3%E4%B8%8EKernel-SVM%E5%8E%9F%E7%90%86%E8%A7%A3%E6%9E%90/

第一节 由线性分类器说起

线性分类器是一种采用超平面(hyper-plane)分割数据的分类方法,对于分布是凸性的两类分类问题,线性分类器基本可以达到最优的分类效果。

典型的线性分类的场景

注意文章中的“线性分类器”和“分类平面”是同一个概念。

线性分类器的局限性

对于非凸分布的数据类,线性分类器难以对数据进行有效的分类。如下图,设任意点\bold P = [x_1,x_2]^TP=[x1,x2]T属于C_1C1C_2C2类,其分布满足方程:(散点图如下)

\begin{cases} x_1^2+2x_1x_2+3x_2^2=4 & \text{if } \bold P \in C_1\\[2ex] 2x_1^2+x_1x_2+2x_2^2=1 & \text{if } \bold P \in C_2 \end{cases} \qquad\qquad(1)x12+2x1x2+3x22=42x12+x1x2+2x22=1if PC1if PC2(1)

显然,没有任何一种线性分类器可以将这两类分开。

non-convex data

如何绕过局限性

目前已经知道,线性分类器无法作用于类似上图一类“包起来”另一类的数据。现在我们有两种解决方法:

  1. 更换分类器
  2. 对数据进行处理

为了引申Kernel的意义,这里不再讨论第一种方法,只讨论第二种方法:如何对数据进行处理,使得线性分类器可以有效分开两组数据?

观察式(1)(1) ,若设点\bold Q=[y_1,y_2,y_3]^TQ=[y1,y2,y3]T,并手动定义非线性映射:

\bold y = \phi(\bold x): \begin{cases} y_1 = x_1^2 \\ y_2 = x_1x_2 \\ y_3 = x_2^2 \end{cases} \qquad\qquad(2)y=ϕ(x):y1=x12y2=x1x2y3=x22(2)

将上式代入(1)(1)中,得:

\begin{cases} y_1+2y_2+3y_3=4 & \text{if } \bold P \in C_1 \\[2ex] 2y_1+y_2+2y_3=1 & \text{if } \bold P \in C_2 \end{cases} \qquad\qquad(3)y1+2y2+3y3=42y1+y2+2y3=1if PC1if PC2(3)

绘制散点图如下。可以发现,原本式(1)(1)中定义的二维的”包起来"的两类数据,被高维映射到三维空间中的两个平面(毕竟(3)(3)式为平面一般方程)上!如下图左所示。而且很明显,这两组数据在三维空间中是线性可分的!

高维映射后的数据点某一个可以分开两类的平面
高维映射后的数据点分类结果

现在对该三维空间中的数据进行线性分类。这里手动选择一个线性分类器(分类平面),如上图右半部分品红色平面所示。其分类器平面的表达式为:

\frac{3}{2}y_1+\frac{3}{2}y_2+\frac{5}{2}y_3=\frac{5}{2} \qquad\qquad(4)23y1+23y2+25y3=25(4)

可以看到,该线性分类器可以很好地区分两类数据。

最后在将这个分类器映射回原始二维空间。由非线性变换(2)(2),可以将(4)(4)降为二维表达形式为:

\frac{3}{2}x_1^2+\frac{3}{2}x_1^2x_2^2+\frac{5}{2}x_2^2=\frac{5}{2} \qquad\qquad(5)23x12+23x12x22+25x22=25(5)

(5)(5)图像如下图品红色曲线所示,可以看到,三维空间中的线性分类器在二维空间中是非线性的,该二维非线性分类器很好地区分了两类数据点。

Classification_result

至此我们可以得出结论,非线性可分的数据映射到高维空间后有可能变为线性可分的数据,高维映射为数据分类提供了一种新思路。

第二节 分类平面计算算法

由上一节可以看到,若对非线性可分地数据进行分类,要经过以下步骤:

  1. 手动定义高维映射,使得该数据集在映射后线性可分
  2. 在映射后的高维空间手动选取分类面,该分类面可以区分该线性可分数据集
  3. 将该分类面逆映射回原始空间,即可对原始空间中非线性可分的数据进行分类

目前这里的高维映射和分类面都是手动选取的。当然,如果它们可以通过一定算法自动选取,这将会带来很大的便利性。在这一节,先考虑如何自动选取一个分类面,使其可以区分线性可分数据。

有些读者可能已经反应过来了,SVM!SVM可以说是最好的线性分类器。下面给出SVM的具体解释,以供没反应过来的读者食用。

SVM算法简介

Overview

SVM这里仅简要说明,并列出之后所需要的公式。具体的细节还请读者查阅相关书籍和博客

对于两类分类问题,SVM的主要任务是,找到一个“最佳”分类面(Separating Hyperplane),将两类点分离。

这里“最佳”的含义是,这个分类面在保证对当前数据集正确分类(这里不讨论容许一定错误率的软间隔SVM)的前提下,同时应该满足该分类面与两类数据点中与该分类面最近的数据点的距离是最大的(The distance between the separating hyperplane and the two-class points whose distance from the hyperplane is the minimum is maximal)。这个距离被称为margin。以上“最佳”分类面的判定原则被称为最小最大化原则。

SVM的应用过程是,首先采用分类已知的数据点对SVM进行训练(即找到margin最大的分类平面),输出一个分类平面,然后对于待分类的数据(不知道其类别),通过这个分类平面进行分类。所以SVM是一个有监督的学习算法。

SVM-Planes

注:图引自:http://dni-institute.in/blogs/wp-content/uploads/2015/09/SVM-Planes.png

如上图所示,黑白两种颜色的数据点代表两类数据。这里采用SVM找到了使得margin最大化的分类平面(分类平面一定在margin中间,而不是偏向某一侧,这是因为它与两类最小距离数据点的距离都要最大化)。该平面可以很好地区分两类数据。

当然,取其他分类平面也可以很好地区分两类数据。想象一下,把上图中的分类平面稍稍倾斜一点,其实也可以将两类数据很好地分离,只不过此时的margin不是最大而已。但为什么非要取margin最大的分类平面呢?这是因为margin最大的分类平面具有最好的鲁棒性。

想象一下,假如分类平面在正确分类的前提下非常靠近上图Target=No数据点。此时输入一个待分类数据点(此时SVM已经训练完毕),这个数据点实际属于Target=No。但是因为分类平面与Targei=No数据集距离很近,所以如果这个数据点在Target=No面向分类平面的边界上,并且扰动较大,它很容易超出分类平面,被误分类到Target=Yes类。所以margin最大化使得误分类率最小化,提高了SVM分类的泛化能力。这是margin最大化的直观解释,理论上的解释需要采用VC维度,这里不再叙述。

理论推导

理论推导固然难,但是下面的式子只要求看懂它的功能,不要求搞懂推导式怎么做的。

1. 分类平面的表达式与分类方法

对于线性SVM,其分类平面的表达式为:

\bold w^T\bold x+b=0\qquad\qquad(6)wTx+b=0(6)

其中xx是数据点坐标,ww是”斜率“(slope),bb是偏移(offset)。对于二维空间,wwxx2\times 12×1的列向量,无论在多少维的空间下,bb都是常数。

分类平面的分类方法是:

\begin{cases} \bold x \in C_1 & \text{if } \bold w^T\bold x+b \geq 1\\[2ex] \bold x \in C_2 & \text{if } \bold w^T\bold x+b \leq -1 \end{cases} \qquad\qquad(7)xC1xC2if wTx+b1if wTx+b1(7)

2. 如何求解分类平面

现在我们已经知道了分类平面的表达式以及采用分类平面进行分类的方法。那么对于给定的已知分类的数据集,如何找到它的分类平面呢?根据SVM理论,确定分类平面的两个必要参数\bold wwbb只需要求解以下优化问题

\begin{aligned} & \min_{\bold w,b} \frac{1}{2}||\bold w||^2 \\[2ex] & s.t. l_i(\bold w^T\bold x_i + b) \geq 1, \\[2ex] & i=1,2,...,n. \end{aligned} \qquad\qquad(8)w,bmin21w2s.t.li(wTxi+b)1,i=1,2,...,n.(8)

其中nn为数据点的个数,l_ili\bold x_ixi数据点的类标(label),取值如下:

\begin{cases} l_i=1 & \text{if } \bold x_i \in C_1 \\[2ex] l_i=-1 & \text{if } \bold x_i \in C_2 \end{cases} \qquad\qquad(9)li=1li=1if xiC1if xiC2(9)

此时求解出来的\bold w​, b​即为margin最大的分类平面的参数。

3. 转化对偶问题求解

(8)(8)的优化问题可以转化为其对偶问题

\begin{aligned} \max_{\bold \alpha} \sum_{i=1}^{n}\alpha_i&-\frac{1}{2}\sum_{i=1}^{n}\sum_{j=1}^{n}\alpha_i\alpha_jl_il_j\bold x_i^T\bold x_j \\[2ex] & s.t. \sum_{i=1}^{n}\alpha_il_i=0,\\[2ex] & \alpha_i \geq 0, \\[2ex] & i=1,2,...,n. \end{aligned} \qquad\qquad(10)αmaxi=1nαi21i=1nj=1nαiαjliljxiTxjs.t.i=1nαili=0,αi0,i=1,2,...,n.(10)

注意上述过程需满足KKT条件:

\begin{cases} \alpha_i \geq 0 \\[2ex] l_if(\bold x_i) - 1\geq 0 \\[2ex] \alpha_i(l_if(\bold x_i)-1) =0 \end{cases} \qquad\qquad(11)αi0lif(xi)10αi(lif(xi)1)=0(11)

其中\bold \alpha = [\alpha_1,\alpha_2,...,\alpha_n]^Tα=[α1,α2,...,αn]T,为拉格朗日乘子项;另外f(\bold x_i) = \bold w^T\bold x_i + bf(xi)=wTxi+b

通过这个优化问题解出\bold \alphaα之后,分类平面斜率参数\bold ww通过下式确定:

\bold w = \sum_{i=1}^{n}\alpha_il_i\bold x_i \qquad\qquad(12)w=i=1nαilixi(12)

bb的确定要稍麻烦一点。观察式(11)(11)可以发现,\alpha_iαil_if(\bold x_i)-1lif(xi)1必须至少有一个为零。此时观察式(12)(12)可以发现,\alpha_iαi为零的数据点\bold x_ixi\bold ww没有贡献。也就是说,只有\alpha_iαi不为零的数据点\bold x_ixi才对分类平面有贡献,这些数据点被称为支持向量。

bb的求解,只需要在解完对偶问题(10)(10)之后,随便取一个支持向量x_{support}xsupport,联立以下方程即可得出。

\begin{cases} \alpha_{support}(l_{support}f(\bold x_{support})-1)=0 \\[2ex] \bold w = \sum_{i=1}^{n}\alpha_il_i\bold x_i \\[2ex] f(\bold x) = \bold w^T\bold x + b \end{cases} \qquad\qquad(13)αsupport(lsupportf(xsupport)1)=0w=i=1nαilixif(x)=wTx+b(13)

目前公式太多可能的确有点绕,但只需要记住一点就可以了:求解对偶问题(10)​,即可求解出分类平面。这是SVM的核心优化问题,也是Kernel方法的关键。

SVM算法的应用效果

我们按照该节最开始所述的三个步骤,采用SVM求解第一节中的非线性分类问题。

1. 手动定义高维映射

这一部在第一节中已经做过了,直接引用高维映射后的数据集(3)(3),这里重新贴数据图。

高维映射后的数据点

2. 采用SVM选取分类面

在上一节中我们手动选取分类面如式(4)(4)所示,这一节采用SVM选取分类面,计算得到的分类平面表达式为:

0.8174y_1+1.5955y_2+2.4111y_3=2.2096 \qquad\qquad(14)0.8174y1+1.5955y2+2.4111y3=2.2096(14)

分类平面的图像如下所示:

分类结果

可以看到该分类平面完美区分了两组数据点。

3. 将分类面逆映射回原始空间

根据高维映射方程(2)(2),将分类平面(12)(12)逆映射回原始二维空间。此时的分类器的表达式为:

0.8174x_1^2+1.5955x_1^2x_2^2+2.4111x_2^2=2.2096 \qquad\qquad(15)0.8174x12+1.5955x12x22+2.4111x22=2.2096(15)

分类器图像如下图所示:

Classification_result_using_SVM

可以看到该分类器是非线性的,并且很完美地区分两类数据点。

第三节 Kernel到底是什么

由于笔者的慢热性(追妹子也慢热),铺垫了好久才步入正题,这里是整部文章的关键(要表白了哈哈哈哈)。

目前我们已经实现自动选取分类平面的问题,并且也看到,非线性可分的数据映射到高维空间后有可能变为线性可分的数据。但是手动选取高维映射的问题还没有得到解决。

但是对于Kernel方法而言,因为kernel是人工选择的,而kernel间接定义了高维映射,所以实际上高维映射也是人工指定的。所以在这个方法里实现不了高维映射的自动选取。但是因这个间接,Kernel方法反倒带来了一些好处。

上面这一段中加粗的文字其实就是Kernel的本质。不过看不懂没关系,之后会慢慢重新引出来。

高维空间分类数据的一般过程

由第一节第二节可以看出,若寻找到一个高维映射

\bold y = \phi(\bold x) \qquad\qquad(16)y=ϕ(x)(16)

使得原本线性不可分的数据\bold xx在映射后的高维空间中变得线性可分(即\bold yy线性可分),那么在原始空间构建有效分类器的方法如下图所示:

高维映射线性分类逆映射

这个过程分三步进行,比较繁琐,所以可否寻找一种方法,将这三步合为一步呢?

Kernel Trick

由2.1.2我们已经知道,线性SVM的训练过程实际上就是求解优化问题(10)(10)。高维映射(15)(15)将线性不可分的\bold xx映射为线性可分的\bold yy。这里对\bold yy做SVM分类,将其代入优化问题(10)(10)中,得:

\begin{aligned} \max_{\bold \alpha} \sum_{i=1}^{n}\alpha_i&-\frac{1}{2}\sum_{i=1}^{n}\sum_{j=1}^{n}\alpha_i\alpha_jl_il_j\bold y_i^T\bold y_j \\[2ex] & s.t. \sum_{i=1}^{n}\alpha_il_i=0,\\[2ex] & \alpha_i \geq 0, \\[2ex] & i=1,2,...,n. \end{aligned} \qquad\qquad(16)αmaxi=1nαi21i=1nj=1nαiαjliljyiTyjs.t.i=1nαili=0,αi0,i=1,2,...,n.(16)

可以发现所有数据点\bold yy仅通过内积\bold y_i^T\bold y_jyiTyj与该优化式耦合。此时定义:(重点来了啊,敲黑板)

K_{ij} = \bold y_i^T\bold y_j \qquad\qquad(17)Kij=yiTyj(17)

将式(17)(17)代入式(16)(16),得:

\begin{aligned} \max_{\bold \alpha} \sum_{i=1}^{n}\alpha_i&-\frac{1}{2}\sum_{i=1}^{n}\sum_{j=1}^{n}\alpha_i\alpha_jl_il_jK_{ij} \\[2ex] & s.t. \sum_{i=1}^{n}\alpha_il_i=0,\\[2ex] & \alpha_i \geq 0, \\[2ex] & i=1,2,...,n. \end{aligned} \qquad\qquad(18)αmaxi=1nαi21i=1nj=1nαiαjliljKijs.t.i=1nαili=0,αi0,i=1,2,...,n.(18)

优化式(18)(18)等价于优化式(16)(16),它们的优化结果相同。这样做貌似并没有什么卵用;但是,如果采用式(18)(18)做分类,可以发现,原本我们对数据点做1. 高维映射,然后2. 应用式(16)(16)进行分类的这两步,只需要通过定义一个K_{ij}Kij,就可以直接采用式(18)(18)进行优化了!两步被合为一步。

K_{ij}Kij,或者K(\bold x_i,\bold x_j)K(xi,xj),就是核函数,而上述替代内积的应用方法,就是Kernel Trick

对于分类器的确定如下式:

\begin{aligned} g(\bold x_j) &= f(\phi(\bold x_j)) \\[2ex] &= \bold w^T\phi(\bold x_j) + b \\[2ex] &= \sum_{i=1}^n\alpha_il_i\bold y_i^T\phi(\bold x_j) +b \\[2ex] &= \sum_{i=1}^n\alpha_il_i\phi(\bold x_i) ^T\phi(\bold x_j) +b \\[2ex] &= \sum_{i=1}^n\alpha_il_iK_{ij} +b \end{aligned} \qquad\qquad(19)g(xj)=f(ϕ(xj))=wTϕ(xj)+b=i=1nαiliyiTϕ(xj)+b=i=1nαiliϕ(xi)Tϕ(xj)+b=i=1nαiliKij+b(19)

目前bb还是待定的,根据式(11)(11),联立以下方程即可求解:

\begin{cases} \alpha_{support}(l_{support}f(\bold x_{support})-1)=0 \\[2ex] f(\bold x) = \sum_{i=1}^n\alpha_il_iK_{i,support} +b \end{cases} \qquad\qquad(20)αsupport(lsupportf(xsupport)1)=0f(x)=i=1nαiliKi,support+b(20)

从式(19)(19)(20)(20)可以看出,通过核函数也可以直接求解原始空间中的分类器,毋须对高维空间中的分类器逆映射至原始空间。

故核函数的定义将本节开始提到的三个步骤合为一步(即通过求解式(18)(19)(20)(18)(19)(20),直接对原始数据分类)。它使得我们不需要对原始数据做手动的高维映射然后再线性分类;而是隐式地将高维映射嵌入到SVM直接做分类:这个过程一般称作隐式映射。而采用了Kernel的SVM叫Kernel-SVM。

至此我们知道,Kernel是一个内积函数,该内积隐式(implicitly)包含了高维映射。Kernel Trick是一种用Kernel函数替换算法中内积项的方法。

一个实际的例子

Kernel间接定义了一个高维映射\phiϕ,这里我们重现2.2中手动映射数据点后做分类的场景,只不过这里采用Kernel-SVM,而Kernel通过已有的高维映射(2)(2)定义,以比对比同一种高维映射函数在手动映射和Kernel隐式映射下,产生的分类器的异同

根据式(2)(2),定义Kernel如下:

\begin{aligned} K_{ij} &= \bold y_i^T\bold y_j = \phi(\bold x_i)^T\phi(\bold x_j) \\[2ex] &= [(x_{1}^{(i)})^2,x_{1}^{(i)}x_{2}^{(i)},(x_{2}^{(i)})^2]\begin{bmatrix}(x_{1}^{(j)})^2\\x_{1}^{(j)}x_{2}^{(j)}\\(x_{2}^{(j)})^2\end{bmatrix} \\[2ex] &= (x_{1}^{(i)})^2(x_{1}^{(j)})^2+x_{1}^{(i)}x_{2}^{(i)}x_{1}^{(j)}x_{2}^{(j)}+(x_{2}^{(i)})^2(x_{2}^{(j)})^2 \end{aligned} \qquad\qquad(21)Kij=yiTyj=ϕ(xi)Tϕ(xj)=[(x1(i))2,x1(i)x2(i),(x2(i))2](x1(j))2x1(j)x2(j)(x2(j))2=(x1(i))2(x1(j))2+x1(i)x2(i)x1(j)x2(j)+(x2(i))2(x2(j))2(21)

注:其中x_{1}^{(i)}x1(i)表示第ii个数据点的第一个坐标值(横坐标),x_{2}^{(j)}x2(j)表示第jj个数据点的第二个坐标值(纵坐标)。这个式子看起来蛮吓人的,但其实很简单,只不过涉及的角标多一点而已。

直接采用Kernel-SVM(将式(12)(12)代入式(18)(18) ,然后根据最优的\alphaα计算式(19)(20)(19)(20)构建分类器)对数据集(1)(1)进行分类,下图为与2.2节分类器结果对比图:

difference

可以看到在同种高维映射下,2.2节手动映射计算出的分类器和Kernel-SVM计算出来的分类器是一致的。

第四节 关于常用Kernel

可能到这里有读者会有疑问,在3.3节的例子中,我们是先知道了某种高维映射可以使得数据点线性可分,然后再定义相应的Kernel做Kernel-SVM。绕了这么一圈,除了减小计算量,貌似还是摆脱不了显式定义高维映射啊。而且,对于大部分数据集,定义一个线性可分的高维映射也是一件非常困难的事情。所以能不能只定义Kernel,不再显式定义高维映射吗?

这里先看一下Cover’s theorem:

A complex pattern-classification problem, cast in a high-dimensional space nonlinearly, is more likely to be linearly separable than in a low-dimensional space.

Cover’s theorem表明,投影到高维空间中的数据更有可能线性可分。如果我们投影更高维,甚至到无穷维的高维空间,数据集线性可分的概率应该是很大的。所以此时不需要显示定义高维映射,只需要定义一个隐含高维映射的核函数,数据集就有可能线性可分(就是差不多定义一个能把数据映射到更高空间的核函数就行了(当然核函数本身也要满足一定的条件))。

目前常用的核函数有多项式核函数(映射至高维空间),高斯核函数(映射至无穷维空间)。

多项式核函数

多项式核函数的表达式为:

K(\bold x_1,\bold x_2)=(\bold x_1^T \bold x_2+c)^d \qquad\qquad(22)K(x1,x2)=(x1Tx2+c)d(22)

其中dd为多项式核函数的阶数,cc为非负常数,如果c=0c=0,此时多项式核函数变成齐次多项式。

若采用2阶多项式核函数对第一节中的分类问题做分类,可以得到分类器如下所示:

polynomial

可以发现,它的结果跟手动映射的SVM分类器非常相似,其实也非常相似于3.3节中Kernel-SVM的结果。这是因为对于二维空间,2阶多项式Kernel的表达式为:

\begin{aligned} K(\bold x_i,\bold x_j) &= (\sum_{k=1}^{n}x_k^{(i)}x_k^{(j)}+c)^d|_{d=2,n=2} \\[2ex] &= \sum_{k=1}^2(x_k^{(i)})^2(x_k^{(j)})^2+\sum_{p=2}^2\sum_{q=1}^{p-1}(\sqrt{2}x_p^{(i)}x_q^{(i)})(\sqrt{2}x_p^{(j)}x_q^{(j)})+\sum_{k=1}^2(\sqrt{2c}x_k^{(i)})(\sqrt{2c}x_k^{(j)}) + c^2 \\[2ex] &= (x_1^{(i)})^2(x_1^{(j)})^2 + 2x_1^{(i)}x_2^{(i)}x_1^{(j)}x_2^{(j)} + (x_2^{(i)})^2(x_2^{(j)})^2 + 2cx_1^{(i)}x_1^{(j)} + 2cx_2^{(i)}x_2^{(j)} + c^2 \end{aligned} \qquad\qquad(23)K(xi,xj)=(k=1nxk(i)xk(j)+c)dd=2,n=2=k=12(xk(i))2(xk(j))2+p=22q=1p1(2xp(i)xq(i))(2xp(j)xq(j))+k=12(2cxk(i))(2cxk(j))+c2=(x1(i))2(x1(j))2+2x1(i)x2(i)x1(j)x2(j)+(x2(i))2(x2(j))2+2cx1(i)x1(j)+2cx2(i)x2(j)+c2(23)

该Kernel包含了式(21)(21)Kernel所含的所有因子,所以它们的结果是非常类似的。

高斯核函数

高斯核函数又名径向基核函数(RBF kernel),其表达式为:

K(\bold x_i,\bold x_j) = exp(-\frac{||\bold x_i-\bold x_j||^2}{2\sigma^2})\qquad\qquad(24)K(xi,xj)=exp(2σ2xixj2)(24)

高斯核函数隐含了无穷维映射,采用该核函数对第一节中的问题做分类可得:

Gaussian

可以看到高斯核函数很好地区分了两类数据点。

第五节 Kernel方法的扩展

前几节通过SVM引入了Kernel方法,但是Kernel方法绝不会只能应用于SVM。只要是只利用原始数据内积,或者可以转化为只利用原始数据内积的算法,都可以采用Kernel Trick将其转化为Kernel算法。

如Kernel-PCA,Kernel-LDA就是两种典型的嵌入核函数的算法。因介绍这两种算法的资料颇丰,不再赘述其嵌入方式和应用场景。


已经结束了

休息一会喝杯茶吧~

别忘了歇歇眼睛

参考资料

[1]周志华. 机器学习[M]. 清华大学出版社

[2]https://en.wikipedia.org/wiki/Polynomial_kernel

[3]https://en.wikipedia.org/wiki/Gaussian_function


  • 3
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
提供的源码资源涵盖了小程序应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值