Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >学习SVM(一) SVM模型训练与分类的OpenCV实现

学习SVM(一) SVM模型训练与分类的OpenCV实现

作者头像
chaibubble
发布于 2022-05-07 01:32:06
发布于 2022-05-07 01:32:06
1.7K0
举报

简介

学习SVM(一) SVM模型训练与分类的OpenCV实现

学习SVM(二) 如何理解支持向量机的最大分类间隔

学习SVM(三)理解SVM中的对偶问题

学习SVM(四) 理解SVM中的支持向量(Support Vector)

学习SVM(五)理解线性SVM的松弛因子

Andrew Ng 在斯坦福大学的机器学习公开课上这样评价支持向量机:

support vector machines is the supervised learning algorithm that many people consider the most effective off-the-shelf supervised learning algorithm.That point of view is debatable,but there are many people that hold that point of view.

可见,在监督学习算法中支持向量机有着非常广泛的应用,而且在解决图像分类问题时有着优异的效果。

OpenCV集成了这种学习算法,它被包含在ml模块下的CvSVM类中,下面我们用OpenCV实现SVM的数据准备模型训练加载模型实现分类,为了理解起来更加直观,我们用三个工程来实现。

数据准备

在OpenCV的安装路径下,搜索digits,可以得到一张图片,图片大小为1000_2000,有0-9的10个数字,每5行为一个数字,总共50行,共有5000个手写数字,每个数字块大小为20_20。 下面将把这些数字中的0和1作为二分类的准备数据。其中0有500张,1有500张。

用下面的代码将图片准备好,在写入路径提前建立好文件夹:

代码语言:javascript
AI代码解释
复制
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
	char ad[128]={0};
	int  filename = 0,filenum=0; 
	Mat img = imread("digits.png");
	Mat gray;
	cvtColor(img, gray, CV_BGR2GRAY);
	int b = 20;
	int m = gray.rows / b;   //原图为1000*2000
	int n = gray.cols / b;   //裁剪为5000个20*20的小图块

	for (int i = 0; i < m; i++)
	{
		int offsetRow = i*b;  //行上的偏移量
		if(i%5==0&&i!=0)
		{
			filename++;
			filenum=0;
		}
		for (int j = 0; j < n; j++)
		{
			int offsetCol = j*b; //列上的偏移量
			sprintf_s(ad, "D:\\data\\%d\\%d.jpg",filename,filenum++);
			//截取20*20的小块
			Mat tmp;
			gray(Range(offsetRow, offsetRow + b), Range(offsetCol, offsetCol + b)).copyTo(tmp);
			imwrite(ad,tmp);
		}
	}
	return 0;
}

最后可以得到这样的结果:

组织的二分类数据形式为:

代码语言:javascript
AI代码解释
复制
--D--data
    --train_image
      --0400张)
      --1400张)
    --test_image
      --0100张)
      --1100张)

训练数据800张,0,1各400张;测试数据200张,0,1各100张

模型训练

数据准备完成之后,就可以用下面的代码训练了:

代码语言:javascript
AI代码解释
复制
#include <stdio.h>  
#include <time.h>  
#include <opencv2/opencv.hpp>  
#include <opencv/cv.h>  
#include <iostream> 
#include <opencv2/core/core.hpp>  
#include <opencv2/highgui/highgui.hpp>  
#include <opencv2/ml/ml.hpp>  
#include <io.h>

using namespace std;
using namespace cv;

void getFiles( string path, vector<string>& files);
void get_1(Mat& trainingImages, vector<int>& trainingLabels);
void get_0(Mat& trainingImages, vector<int>& trainingLabels);

int main()
{
	//获取训练数据
	Mat classes;
	Mat trainingData;
	Mat trainingImages;
	vector<int> trainingLabels;
	get_1(trainingImages, trainingLabels);
	get_0(trainingImages, trainingLabels);
	Mat(trainingImages).copyTo(trainingData);
	trainingData.convertTo(trainingData, CV_32FC1);
	Mat(trainingLabels).copyTo(classes);
	//配置SVM训练器参数
	CvSVMParams SVM_params;
	SVM_params.svm_type = CvSVM::C_SVC;
	SVM_params.kernel_type = CvSVM::LINEAR; 
	SVM_params.degree = 0;
	SVM_params.gamma = 1;
	SVM_params.coef0 = 0;
	SVM_params.C = 1;
	SVM_params.nu = 0;
	SVM_params.p = 0;
	SVM_params.term_crit = cvTermCriteria(CV_TERMCRIT_ITER, 1000, 0.01);
	//训练
	CvSVM svm;
	svm.train(trainingData, classes, Mat(), Mat(), SVM_params);
	//保存模型
	svm.save("svm.xml");
	cout<<"训练好了!!!"<<endl;
	getchar();
	return 0;
}
void getFiles( string path, vector<string>& files )  
{  
	long   hFile   =   0;  
	struct _finddata_t fileinfo;  
	string p;  
	if((hFile = _findfirst(p.assign(path).append("\\*").c_str(),&fileinfo)) !=  -1)  
	{  
		do  
		{  
			if((fileinfo.attrib &  _A_SUBDIR))  
			{  
				if(strcmp(fileinfo.name,".") != 0  &&  strcmp(fileinfo.name,"..") != 0)  
					getFiles( p.assign(path).append("\\").append(fileinfo.name), files );  
			}  
			else  
			{  
				files.push_back(p.assign(path).append("\\").append(fileinfo.name) );  
			}  
		}while(_findnext(hFile, &fileinfo)  == 0);  

		_findclose(hFile);  
	}  
} 
void get_1(Mat& trainingImages, vector<int>& trainingLabels)
{
	char * filePath = "D:\\data\\train_image\\1"; 
	vector<string> files;  
	getFiles(filePath, files );  
	int number = files.size();  
	for (int i = 0;i < number;i++)  
	{  
		Mat  SrcImage=imread(files[i].c_str());
		SrcImage= SrcImage.reshape(1, 1);
		trainingImages.push_back(SrcImage);
		trainingLabels.push_back(1);
	}  
}
void get_0(Mat& trainingImages, vector<int>& trainingLabels)
{
	char * filePath = "D:\\data\\train_image\\0"; 
	vector<string> files;  
	getFiles(filePath, files );  
	int number = files.size();  
	for (int i = 0;i < number;i++)  
	{  
		Mat  SrcImage=imread(files[i].c_str());
		SrcImage= SrcImage.reshape(1, 1);
		trainingImages.push_back(SrcImage);
		trainingLabels.push_back(0);
	}  
}

整个训练过程可以分为一下几个部分:

数据准备:

该例程中一个定义了三个子程序用来实现数据准备工作:

getFiles()用来遍历文件夹下所有文件,可以参考:

http://blog.csdn.net/chaipp0607/article/details/53914954

getBubble()用来获取有气泡的图片和与其对应的Labels,该例程将Labels定为1。

getNoBubble()用来获取没有气泡的图片与其对应的Labels,该例程将Labels定为0。

getBubble()与getNoBubble()将获取一张图片后会将图片(特征)写入到容器中,紧接着会将标签写入另一个容器中,这样就保证了特征和标签是一一对应的关系push_back(0)或者push_back(1)其实就是我们贴标签的过程。

代码语言:javascript
AI代码解释
复制
trainingImages.push_back(SrcImage);
trainingLabels.push_back(0);

在主函数中,将getBubble()与getNoBubble()写好的包含特征的矩阵拷贝给trainingData,将包含标签的vector容器进行类型转换后拷贝到trainingLabels里,至此,数据准备工作完成,trainingData与trainingLabels就是我们要训练的数据。

代码语言:javascript
AI代码解释
复制
	Mat classes;
	Mat trainingData;
	Mat trainingImages;
	vector<int> trainingLabels;
	getBubble(trainingImages, trainingLabels);
	getNoBubble(trainingImages, trainingLabels);
	Mat(trainingImages).copyTo(trainingData);
	trainingData.convertTo(trainingData, CV_32FC1);
	Mat(trainingLabels).copyTo(classes);

特征选取

其实特征提取和数据的准备是同步完成的,我们最后要训练的也是正负样本的特征。本例程中同样在getBubble()与getNoBubble()函数中完成特征提取工作,只是我们简单粗暴将整个图的所有像素作为了特征,因为我们关注更多的是整个的训练过程,所以选择了最简单的方式完成特征提取工作,除此中外,特征提取的方式有很多,比如LBP,HOG等等。

代码语言:javascript
AI代码解释
复制
  SrcImage= SrcImage.reshape(1, 1);

我们利用reshape()函数完成特征提取,原型如下:

代码语言:javascript
AI代码解释
复制
 Mat reshape(int cn, int rows=0) const;

可以看到该函数的参数非常简单,cn为新的通道数,如果cn = 0,表示通道数不会改变。参数rows为新的行数,如果rows = 0,表示行数不会改变。我们将参数定义为reshape(1, 1)的结果就是原图像对应的矩阵将被拉伸成一个一行的向量,作为特征向量。

参数配置

参数配置是SVM的核心部分,在Opencv中它被定义成一个结构体类型,如下:

代码语言:javascript
AI代码解释
复制
struct CV_EXPORTS_W_MAP CvSVMParams
{
    CvSVMParams();
    CvSVMParams(  
    int svm_type, 
    int kernel_type,
    double degree, 
    double coef0,
    double Cvalue, 
    double p,
    CvMat* class_weights, 
    CvTermCriteria term_crit );
    CV_PROP_RW int         svm_type;
    CV_PROP_RW int         kernel_type;
    CV_PROP_RW double      degree; // for poly
    CV_PROP_RW double      gamma;  // for poly/rbf/sigmoid
    CV_PROP_RW double      coef0;  // for poly/sigmoid
    CV_PROP_RW double      C;  // for CV_SVM_C_SVC,       CV_SVM_EPS_SVR and CV_SVM_NU_SVR
    CV_PROP_RW double      nu; // for CV_SVM_NU_SVC, CV_SVM_ONE_CLASS, and CV_SVM_NU_SVR
    CV_PROP_RW double      p; // for CV_SVM_EPS_SVR
    CvMat*      class_weights; // for CV_SVM_C_SVC
    CV_PROP_RW CvTermCriteria term_crit; // termination criteria
};

所以在例程中我们定义了一个结构体变量用来配置这些参数,而这个变量也就是CVSVM类中train函数的第五个参数,下面对参数进行说明。

SVM_params.svm_type :SVM的类型:

C_SVC表示SVM分类器,C_SVR表示SVM回归

SVM_params.kernel_type:核函数类型

线性核LINEAR:

d(x,y)=(x,y)

多项式核POLY:

d(x,y)=(gamma*(x’y)+coef0)degree

径向基核RBF:

d(x,y)=exp(-gamma*|x-y|^2)

sigmoid核SIGMOID:

d(x,y)= tanh(gamma*(x’y)+ coef0)

SVM_params.degree:核函数中的参数degree,针对多项式核函数;

SVM_params.gama:核函数中的参数gamma,针对多项式/RBF/SIGMOID核函数;

SVM_params.coef0:核函数中的参数,针对多项式/SIGMOID核函数;

SVM_params.c:SVM最优问题参数,设置C-SVCEPS_SVRNU_SVR的参数;

SVM_params.nu:SVM最优问题参数,设置NU_SVCONE_CLASSNU_SVR的参数;

SVM_params.p:SVM最优问题参数,设置EPS_SVR 中损失函数p的值.

训练模型

代码语言:javascript
AI代码解释
复制
CvSVM svm;
svm.train(trainingData, classes, Mat(), Mat(), SVM_params);

通过上面的过程,我们准备好了待训练的数据和训练需要的参数,**其实可以理解为这个准备工作就是在为svm.train()函数准备实参的过程。**来看一下svm.train()函数,Opencv将SVM封装成CvSVM库,这个库是基于台湾大学林智仁(Lin Chih-Jen)教授等人开发的LIBSVM封装的,由于篇幅限制,不再全部粘贴库的定义,所以一下代码只是CvSVM库中的一部分数据和函数:

代码语言:javascript
AI代码解释
复制
class CV_EXPORTS_W CvSVM : public CvStatModel
{
public:
virtual bool train( 
  const CvMat* trainData, 
  const CvMat* responses,
  const CvMat* varIdx=0, 
  const CvMat* sampleIdx=0,
  CvSVMParams params=CvSVMParams() );
virtual float predict( 
  const CvMat* sample, 
  bool returnDFVal=false ) const;

我们就是应用类中定义的train函数完成模型训练工作。

保存模型

代码语言:javascript
AI代码解释
复制
svm.save("svm.xml");

保存模型只有一行代码,利用save()函数,我们看下它的定义:

代码语言:javascript
AI代码解释
复制
    CV_WRAP virtual void save( const char* filename, const char* name=0 ) const;

该函数被定义在CvStatModel类中,CvStatModel是ML库中的统计模型基类,其他 ML 类都是从这个类中继承。

**总结:**到这里我们就完成了模型训练工作,可以看到真正用于训练的代码其实很少,OpenCV最支持向量机的封装极大地降低了我们的编程工作。

加载模型实现分类

代码语言:javascript
AI代码解释
复制
#include <stdio.h>  
#include <time.h>  
#include <opencv2/opencv.hpp>  
#include <opencv/cv.h>  
#include <iostream> 
#include <opencv2/core/core.hpp>  
#include <opencv2/highgui/highgui.hpp>  
#include <opencv2/ml/ml.hpp>  
#include <io.h>

using namespace std;
using namespace cv;

void getFiles( string path, vector<string>& files );

int main()
{
	int result = 0;
	char * filePath = "D:\\data\\test_image\\0"; 
	vector<string> files;  
	getFiles(filePath, files );  
	int number = files.size();  
	cout<<number<<endl;
	CvSVM svm;
	svm.clear();
	string modelpath = "svm.xml";
	FileStorage svm_fs(modelpath,FileStorage::READ);
	if(svm_fs.isOpened())
	{
		svm.load(modelpath.c_str());
	}
	for (int i = 0;i < number;i++)  
	{  
		Mat inMat = imread(files[i].c_str());
		Mat p = inMat.reshape(1, 1);
		p.convertTo(p, CV_32FC1);
		int response = (int)svm.predict(p);
		if (response == 0)
		{
			result++;
		}
	}
	cout<<result<<endl;
	getchar();
	return  0;
}
void getFiles( string path, vector<string>& files )  
{  
	long   hFile   =   0;  
	struct _finddata_t fileinfo;  
	string p;  
	if((hFile = _findfirst(p.assign(path).append("\\*").c_str(),&fileinfo)) !=  -1)  
	{  
		do  
		{  
			if((fileinfo.attrib &  _A_SUBDIR))  
			{  
				if(strcmp(fileinfo.name,".") != 0  &&  strcmp(fileinfo.name,"..") != 0)  
					getFiles( p.assign(path).append("\\").append(fileinfo.name), files );  
			}  
			else  
			{  		files.push_back(p.assign(path).append("\\").append(fileinfo.name) );  
			}  
		}while(_findnext(hFile, &fileinfo)  == 0);  
		_findclose(hFile);  
	}  
} 

在上面我们把该介绍的都说的差不多了,这个例程中只是用到了load()函数用于模型加载,加载的就是上面例子中生成的模型,load()被定义在CvStatModel这个基类中:

代码语言:javascript
AI代码解释
复制
	svm.load(modelpath.c_str());

load的路径是string modelpath = "svm.xml",这意味着svm.mxl文件应该在测试工程的根目录下面,但是因为训练和预测是两个独立的工程,所以必须要拷贝一下这个文件。最后用到predict()函数用来预测分类结果,predict()被定义在CVSVM类中。

注意:

1.为什么要建立三个独立的工程呢?

主要是考虑写在一起话,代码量会比较大,逻辑没有分开清晰,当跑通上面的代码之后,就可以随意的改了。

2.为什么加上数据准备?

之前有评论说道数据的问题,提供数据后实验能更顺利一些,因为本身代码没有什么含金量,这样可以更顺利的运行起来工程,并修改它。

3.一些容易引起异常的情况:

(1):注意生成的.xml记得拷贝到预测工程下;

(2):注意准备好数据路径和代码是不是一致;

(3):注意训练的特征要和测试的特征一致;

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2017-03-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
opencv 视觉项目学习笔记(二): 基于 svm 和 knn 车牌识别
    训练数据: 所有训练数据存储再一个 N x M 的矩阵中, 其中 N 为样本数, M 为特征数(每个样本是该训练矩阵中的一行)。这些数据  所有数据存在  xml 文件中, 
用户2434869
2018/10/11
3.3K0
OpenCV 2.4.9 支持向量机(SVM)说明
本文翻译自OpenCV 2.4.9官方文档《opencv2refman.pdf》。 前言 Originally, support vector machines (SVM) was a techni
剑影啸清寒
2018/01/02
2K0
OpenCV 2.4.9 支持向量机(SVM)说明
OpenCV编程:OpenCV3.X训练自己的分类器
QT版本: 5.12.6 (我的程序里主要是QT+OpenCV实现图像处理显示的)
DS小龙哥
2022/01/17
2.1K0
OpenCV编程:OpenCV3.X训练自己的分类器
机器学习_分类_adaboost
Boosting, 也称为增强学习或提升法,是一种重要的集成学习技术, 能够将预测精度仅比随机猜度略高的弱学习器增强为预测精度高的强学习器。
AomanHao
2022/01/13
2850
OpenCV 3.1 imwrite()函数写入异常问题解决方法
本文介绍了OpenCV 3.1中imwrite()函数用于写入图像时可能遇到的异常情况,并给出了两种解决方法。第一种方法是使用try-catch语句捕获异常,并修改代码以处理该异常。第二种方法是使用imwrite()函数的可选参数来控制图像的压缩级别,从而避免异常。这两种方法都可以解决异常,但需要根据具体情况进行选择。
chaibubble
2018/01/02
4.6K0
OpenCV 3.1 imwrite()函数写入异常问题解决方法
使用 OpenCV + 微信二维码引擎实现二维码识别
点击上方蓝字关注我们 微信公众号:OpenCV学堂 关注获取更多计算机视觉与深度学习知识 作者:Tony沈哲 链接:https://juejin.cn/post/7079313321446506532 来源:稀土掘金 背景 今年自疫情以来,我都没有写过文章。一方面是疫情导致居家办公比较烦躁,另一方面最近有点懒了。但是工作还是要继续,趁这几天优化了一下最近的项目,我整理了一下如何使用 OpenCV 和微信二维码引擎来实现二维码的识别。 微信开源了其二维码的解码功能,并贡献给 OpenCV 社区。其开源的
OpenCV学堂
2022/03/28
5.6K0
OpenCV 删除轮廓的方法(一)
一种比较方便的删除轮廓的处理方式,是我刚刚学习到的一个方法,在这之前,如果我想删除一个不需要的轮廓,用的方法是将该轮廓填充为背景色,之前的博客提到过,在countours容器中,如果把轮廓填充为背景色,那么只是视觉上看不到该轮廓,但是实际上还存在在容器中。所以之前总是要填充之后从新copyto一下,然后重新找一遍轮廓,达到删除轮廓的效果。这种方式实在是low。 [见之前的博客http://blog.csdn.net/chaipp0607/article/details/52858661 代码如下:
chaibubble
2022/05/07
5920
OpenCV 删除轮廓的方法(一)
终于可以摆脱OpenCV中Hough圆调参的烦恼了
OpenCV图像项目中,圆的检测很常见。 例如:检测烂苹果的个数,寻找目标靶心,人眼,嘴巴识别等。 其中用到的关键技术是OpenCV中集成的霍夫圆检测函数。 HoughCircles( InputArray image, // 输入图像 ,必须是8位的单通道灰度图像 OutputArray circles, // 输出结果,发现的圆信息 Int method, // 方法 - HOUGH_GRADIENT Double dp,
用户9831583
2022/06/16
3.3K0
终于可以摆脱OpenCV中Hough圆调参的烦恼了
学习KNN(二)KNN算法手写数字识别的OpenCV实现
本文介绍了KNN算法在图像分类和手写数字识别中的应用,并通过具体代码示例讲解了如何实现。
chaibubble
2018/01/08
2.2K0
学习KNN(二)KNN算法手写数字识别的OpenCV实现
C++ OpenCV SVM实战Kindle检测(一)----训练数据
最近也是在接触机器学习,通过做了几个MLNET的例子对机器学习有了一点了解,OpenCV中也有机器学习这块,所以我们就直接来用Kindle做一个实战。
Vaccae
2019/10/12
2K1
C++ OpenCV SVM实战Kindle检测(一)----训练数据
OpenCV findContours函数边缘近似方法
在使用OpenCV的过程中,findcontours是相对使用比较多的,在之前的博客中,介绍了vector<vector<Point> > contours容器: http://blog.csdn.net/chaipp0607/article/details/52858661 查找轮廓时内轮廓与外轮廓: http://blog.csdn.net/chaipp0607/article/details/53765440
chaibubble
2022/05/07
6880
OpenCV findContours函数边缘近似方法
【走进OpenCV】霍夫变换检测直线和圆
学习计算机视觉最重要的能力应该就是编程了,为了帮助小伙伴尽快入门计算机视觉,小白准备了【走进OpenCV】系列,主要帮助小伙伴了解如何调用OpenCV库,涉及到的知识点会做简单讲解。
小白学视觉
2019/10/24
2K0
【走进OpenCV】霍夫变换检测直线和圆
OpenCV 应用读取文件路径与文件名批量处理图片
在应用OpenCV大量测试图片时,需要对图片批量的读入并进行处理。之前处理这个问题时是使用这种方法:把待处理的图片放到一个文件夹内,全选它们然后重命名1,这样系统会自动给他们全部重命名为1(1),1(2),1(3)等等等 然后用下面的代码把图片读进来:
chaibubble
2022/05/07
9470
OpenCV 应用读取文件路径与文件名批量处理图片
基于opencv人脸识别
5.开始、读训练数据、计算平均脸、计算协方差矩阵、计算特征值、特征矩阵、 PAC降维、子空间模型、检测
全栈程序员站长
2022/09/05
7640
基于opencv人脸识别
【OpenCV入门之十九】Harris与Shi-Tomasi角点检测
学习计算机视觉最重要的能力应该就是编程了,为了帮助小伙伴尽快入门计算机视觉,小白准备了【OpenCV入门】系列。新的一年文章的内容进行了很大的完善,主要是借鉴了更多大神的文章,希望让小伙伴更加容易理解。如果小伙伴觉得有帮助,请点击一下文末的“在看”鼓励一下小白。
小白学视觉
2019/05/30
1.2K0
OpenCV中如何获取Mat类型中的步长stride及分析 C++实现
先从本地读取一张图片,单步进去看到srcImage的属性如下图,有stride,也有width,height,channels等
OpenCV学堂
2019/07/05
3.7K0
OpenCV特征点检测------Surf(特征点篇)
Surf算法的原理                                                                          
流川疯
2022/05/06
2.3K1
OpenCV特征点检测------Surf(特征点篇)
深度学习:驾驶行为分析
程序功能简介: 使用yolo训练,OpenCV调用、实现打哈欠、手机、抽烟、系安全带,口罩检测。
DS小龙哥
2022/01/17
8550
深度学习:驾驶行为分析
【OpenCV入门之十三】如何在ROI中添加Logo
它的英文全称是Region Of Interest,对应的中文解释就是感兴趣区域。
小白学视觉
2019/05/30
1.8K0
Histogram of Oriented Gridients(HOG) 方向梯度直方图
作者 张旭 编辑 徐松 1. HOG简介 2. 数字图像梯度定义 3. HOG基本步骤 4. OpenCV实现HOG 5. 用KNN与HOG实现一个手写数字输入识别 1. HOG简介 方向梯度直方图(Histogram of Oriented Gradient, HOG)于2005年提出,是一种常用的特征提取方法,且HOG+SVM的方式在行人检测中有着优异的效果。经典的论文为《Histograms of oriented gradients for human detection》,这篇文章中,HOG就是
机器学习算法工程师
2018/03/06
1.6K0
Histogram of Oriented Gridients(HOG) 方向梯度直方图
推荐阅读
相关推荐
opencv 视觉项目学习笔记(二): 基于 svm 和 knn 车牌识别
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档