【opencv3】 svm實現手寫體與人臉識別
主要 內容 opencv3 除錯2分類svm 和多分類svm。
引數除錯參考地址
官方文件說明地址
實現內容:1 opencv +svm實現首先體二分類
2 opencv+svm實現人臉分類
3 opencv+pca+svm實現人臉識別
1.opencv3除錯二分類SVM
第5小點有完整程式參考。
<1>資料準備 區分手寫字型0,1
首先在專案資料夾下面建立兩個子資料夾,分別為train,test。train,test資料夾下面分別再建立子資料夾0,1。具體如圖所示:
<2> 給訓練集資料加上標籤
Mat& trainingImages, vector& trainingLabels:這裡採用了引用&,所以不清空前會一直累加變化
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 = "train\\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 = "train\\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);
}
}
<2> SVM的建立與引數設定以及訓練
cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
svm->setType(cv::ml::SVM::Types::C_SVC);
svm->setKernel(cv::ml::SVM::KernelTypes::LINEAR);
svm->setTermCriteria(cv::TermCriteria(cv::TermCriteria::MAX_ITER, 1000,0.01));
// train operation
svm->train(trainingData,SampleTypes::ROW_SAMPLE, trainingLabels);
//儲存模型
svm->save("svm.xml");
<3>測試 載入訓練模型 測試0
string modelpath = "svm.xml";
Ptr<ml::SVM> svm = Algorithm::load<ml::SVM>(modelpath);
char * filePath = "test\\0";
vector<string> files;
getFiles(filePath, files);
int number = files.size();
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)//因為現在測試為0所以標籤為0 預測值出來為零
{
std::cout<<"預測為0 預測正確"<<endl;
}
}
<5>程式
digits檔案如下,在opencv庫中有!路徑為==/opencv/sources/samples/data/==
總共三部分:從digits中分割出所需訓練集與測試集;訓練;測試。
/
部分1 將一張巨大的圖片中的元素切割出來 digits.png
///
//#include <opencv2/opencv.hpp>
//#include <iostream>
//#include <ml.hpp>
//
//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, "E:\\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;
//}
//
//
/
部分2 訓練
///
//#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>
//#include "opencv2/ml.hpp"
//using namespace std;
//using namespace cv;
//using namespace cv::ml;
//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訓練器引數
// // initial SVM
// cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
// svm->setType(cv::ml::SVM::Types::C_SVC);
// svm->setKernel(cv::ml::SVM::KernelTypes::LINEAR);
// svm->setTermCriteria(cv::TermCriteria(cv::TermCriteria::MAX_ITER, 1000,0.01));
//
//
// // train operation
// svm->train(trainingData,SampleTypes::ROW_SAMPLE, trainingLabels);
//
//
//
// //儲存模型
// 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 = "testdata\\train\\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 = "testdata\\train\\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);
// }
//}
/
部分3 測試
///
#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;
using namespace ml;
void getFiles(string path, vector<string>& files);
int main()
{
int result = 0;
char * filePath = "testdata\\test\\0";
vector<string> files;
getFiles(filePath, files);
int number = files.size();
cout << number << endl;
//cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
//svm->clear();
string modelpath = "svm.xml";
Ptr<ml::SVM> svm = Algorithm::load<ml::SVM>(modelpath);
//FileStorage svm_fs(modelpath, FileStorage::READ);
//if (svm_fs.isOpened())
//{
svm->load(modelpath.c_str());
// Ptr<ml::SVM> svm = Algorithm::load<ml::SVM>("svm.xml");
//}
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);
}
}
2 多分類svm 人臉識別
因為一個svm模型只能分為兩類,當有N個人作區分時,產生N個模型即可。
opencv參考地址https://docs.opencv.org/3.3.0/d1/d2d/classcv_1_1ml_1_1SVM.html#aad7f1aaccced3c33bb256640910a0e56 我發現svm中(C_SVM)支援多分類 所以只需要類別標籤分為貼為1~15,predict出來就是1-15。
修改的程式如下,只是在標籤貼的時候不同!!!!
檢測標準, cout << response;輸出為1,就是第一個人;輸出為2,就是第二個人!
資料格式:
訓練集:
測試集:
程式直接可用如下:
#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> //查詢檔案相關函式
#include <vector>
#include <string>
#include <fstream>
using namespace std;
using namespace cv;
using namespace cv::ml;
//宣告
void getFiles(string path, vector<string>& files);
void getBubble(Mat& trainingImages, vector<int>& trainingLabels, int num, int count);//,int num
void train2modle(int classesNum);
void Test_result(int classesNum);
void getFiles(string path, vector<string>& files)
{
intptr_t 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 getBubble(Mat& trainingImages, vector<int>& trainingLabels, int num, int count)//count第幾個訓練器 num第幾個資料夾
{
string tem;
std::stringstream StrStm;
StrStm.clear(); //std::stringstream變數在使用(轉化)前都應先呼叫.cleat()函式
tem.clear();
StrStm << num;
StrStm >> tem;
string filePath = "faces\\train\\" + tem; //正樣本路徑
//char * filePath = "faces\\train\\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);
// if (count == num)
trainingLabels.push_back(num);//該樣本為數字
// else
// trainingLabels.push_back(0);//該樣本為數字
}
}
int main()
{
train2modle(15);
Test_result(15);
return 0;
}
void train2modle(int classesNum)
{
int count = 1;
//獲取訓練資料
Mat classes;
Mat trainingData;
Mat trainingImages;
vector<int> trainingLabels;
// for (int count = 1; count <= classesNum; count++)//測試資料夾 有15個人臉資料夾
{
trainingLabels.clear();
trainingImages.release();
for (int num = 1; num <= classesNum; num++)
getBubble(trainingImages, trainingLabels, num, count);
//getNoBubble(trainingImages, trainingLabels);
//在主函式中,將getBubble()與getNoBubble()寫好的包含特徵的矩陣拷貝給trainingData,將包含標籤的vector容器進行類
//型轉換後拷貝到trainingLabels裡,至此,資料準備工作完成,trainingData與trainingLabels就是我們要訓練的資料。
Mat(trainingImages).copyTo(trainingData);
trainingData.convertTo(trainingData, CV_32FC1);
Mat(trainingLabels).copyTo(classes);
classes.convertTo(classes, CV_32SC1);
// 建立分類器並設定引數
Ptr<SVM> SVM_params = SVM::create();
SVM_params->clear();
SVM_params->setType(SVM::C_SVC);
SVM_params->setKernel(SVM::LINEAR); //核函式
SVM_params->setDegree(0);
SVM_params->setGamma(1);
SVM_params->setCoef0(0);
SVM_params->setC(1);
SVM_params->setNu(0);
SVM_params->setP(0);
SVM_params->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER + TermCriteria::EPS, 1000, 0.01));
Ptr<TrainData> tData = TrainData::create(trainingData, ROW_SAMPLE, classes);
// 訓練分類器
SVM_params->train(tData);
string tem;
std::stringstream StrStm;
StrStm.clear(); //std::stringstream變數在使用(轉化)前都應先呼叫.cleat()函式
tem.clear();
StrStm << count;
StrStm >> tem;
string savexml = "svm.xml";
//儲存模型
SVM_params->save(savexml);
std::cout << "訓練好了!!!" << endl;
}
}
void Test_result(int classesNum)
{
std::stringstream StrStm;
int result = 0; int count = 1; string tem; int sum = 0;
vector <int> responseSave;
for (int j = 1; j <= classesNum; j++) //資料集測試圖片載入
{
StrStm.clear();
tem.clear();
StrStm << j;
StrStm >> tem;
string filePath = "faces//test//" + tem;
vector<string> files;
getFiles(filePath, files);
int number = files.size();
for (int i = 0; i < number; i++)//測試資料夾下子資料夾內的圖片
{
// for (int count = 1; count <= classesNum; count++)//生成的模型載入
{
StrStm.clear();
tem.clear();
StrStm << count;
StrStm >> tem;
string modelpath = "svm.xml";
sum = number + sum;
Ptr<ml::SVM> svm = Algorithm::load<ml::SVM>(modelpath);
Mat inMat = imread(files[i].c_str());
Mat p = inMat.reshape(1, 1);
p.convertTo(p, CV_32FC1);
int response = (int)svm->predict(p);
cout << response;
responseSave.push_back(response);
if (response== j )
{
result = result + 1;
std::cout << "識別為S" << count << "實際為S" << j << endl;
// //break;
}
}
cout << endl;
}
}
std::cout << result << endl;
std::cout << "識別率" << endl;
std::cout << result / sum * 100 << "%" << endl;
getchar();
}
以下是一開始沒有好好閱讀文件,以為svm是二分類,因此15類就傻瓜式的生成15個svm模型來作為多分類
<1>資料準備
同樣在專案工程下建立兩個資料夾 train、test但是測試的有十五個人所以 train 、test資料夾下各15個資料夾具體如下:
<2>新增類別標籤
對於第一個分類器而言,只有第一個人為正樣本 剩餘全為負樣本
對於第二個分類器而言,只有第二個人為正樣本 剩餘全為負樣本
.
.
.
void getFiles(string path, vector<string>& files)
{
intptr_t 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 getBubble(Mat& trainingImages, vector<int>& trainingLabels, int num, int count)//count第幾個訓練器 num第幾個資料夾
{
string tem;
std::stringstream StrStm;
StrStm.clear(); //std::stringstream變數在使用(轉化)前都應先呼叫.cleat()函式
tem.clear();
StrStm << num;
StrStm >> tem;
string filePath = "faces\\train\\" + tem; //正樣本路徑
//char * filePath = "faces\\train\\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);
if (count == num)
trainingLabels.push_back(1);//該樣本為數字
else
trainingLabels.push_back(0);//該樣本為數字
}
}
<2>訓練
每次訓練新的模型前,都需要把trainingLabels,trainingImages釋放!!不然==引用&會一直補充在後!導致維度一直在增會出錯。
== trainingLabels.clear();
trainingImages.release();
void train2modle(int classesNum)
{
int count = 1;
//獲取訓練資料
Mat classes;
Mat trainingData;
Mat trainingImages;
vector<int> trainingLabels;
for (int count = 1; count <= classesNum; count++)//測試資料夾 有15個人臉資料夾
{
trainingLabels.clear();
trainingImages.release();
for (int num = 1; num <= classesNum; num++)
getBubble(trainingImages, trainingLabels, num,count);
//getNoBubble(trainingImages, trainingLabels);
//在主函式中,將getBubble()與getNoBubble()寫好的包含特徵的矩陣拷貝給trainingData,將包含標籤的vector容器進行類
//型轉換後拷貝到trainingLabels裡,至此,資料準備工作完成,trainingData與trainingLabels就是我們要訓練的資料。
Mat(trainingImages).copyTo(trainingData);
trainingData.convertTo(trainingData, CV_32FC1);
Mat(trainingLabels).copyTo(classes);
classes.convertTo(classes, CV_32SC1);
// 建立分類器並設定引數
Ptr<SVM> SVM_params = SVM::create();
SVM_params->clear();
SVM_params->setType(SVM::C_SVC);
SVM_params->setKernel(SVM::LINEAR); //核函式
SVM_params->setDegree(0);
SVM_params->setGamma(1);
SVM_params->setCoef0(0);
SVM_params->setC(1);
SVM_params->setNu(0);
SVM_params->setP(0);
SVM_params->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER + TermCriteria::EPS, 1000, 0.01));
Ptr<TrainData> tData = TrainData::create(trainingData, ROW_SAMPLE, classes);
// 訓練分類器
SVM_params->train(tData);
string tem;
std::stringstream StrStm;
StrStm.clear(); //std::stringstream變數在使用(轉化)前都應先呼叫.cleat()函式
tem.clear();
StrStm << count;
StrStm >> tem;
string savexml = "svm" + tem + ".xml";
//儲存模型
SVM_params->save(savexml);
std::cout<< "訓練好了!!!" << endl;
}
}
<3>測試
輸出的是標籤。cout << response ;對於每一張測試樣本來說會輸出15個數(每一個模型進去就會出來一個預測標籤)其中第幾個1就判為第幾個人;比如100000000000000 =該樣本預測為第一個人
010000000000000 =該樣本預測為第二個人
void Test_result(int classesNum)
{
std::stringstream StrStm;
int result = 0; int count = 1; string tem; int sum = 0;
vector <int> responseSave;
for (int j = 1; j <= classesNum; j++) //資料集測試圖片載入
{
StrStm.clear();
tem.clear();
StrStm << j;
StrStm >> tem;
string filePath = "faces//test//"+tem;
vector<string> files;
getFiles(filePath, files);
int number = files.size();
for (int i = 0; i < number; i++)//測試資料夾下子資料夾內的圖片
{
for (int count = 1; count <= classesNum; count++)//生成的模型載入
{
StrStm.clear();
tem.clear();
StrStm << count;
StrStm >> tem;
string modelpath = "svm" + tem + ".xml";
sum = number + sum;
Ptr<ml::SVM> svm = Algorithm::load<ml::SVM>(modelpath);
Mat inMat = imread(files[i].c_str());
Mat p = inMat.reshape(1, 1);
p.convertTo(p, CV_32FC1);
int response = (int)svm->predict(p);
cout << response ;
responseSave.push_back(response);
if (response == 1 &&(j ==count))
{
result = result + 1;
std::cout << "識別為S" << count << "實際為S" << j << endl;
// //break;
}
}
cout << endl;
}
}
std::cout << result << endl;
std::cout << "識別率" << endl;
std::cout << result / sum * 100 << "%" << endl;
getchar();
}
<5>主函式
int main()
{
train2modle(15);
Test_result(15);
return 0;
}
相關文章
- 機器學習實戰-SVM模型實現人臉識別機器學習模型
- opencv python 基於SVM的手寫體識別OpenCVPython
- 手把手教你實現人臉識別,有手就行
- 純前端實現人臉識別-提取-合成前端
- 人臉識別與人體動作識別技術及應用pdf
- python ubuntu人臉識別 -1 SVM binary 分類器PythonUbuntu
- 人臉活體檢測人臉識別:眨眼+張口
- SVM多分類器的實現(Opencv3,C++)OpenCVC++
- 基於Android平臺實現人臉識別Android
- 如何用Excel 9步實現CNN人臉識別ExcelCNN
- 人臉識別技術,讓科幻成為現實
- 如何快速實現人臉識別通道?一文了解具體技巧
- opencv 人臉識別OpenCV
- OpenCV — 人臉識別OpenCV
- 計算機視覺—人臉識別(Hog特徵+SVM分類器)(8)計算機視覺HOG特徵
- 四款支援3D人臉識別手機推薦 3D人臉識別手機有哪些3D
- 如何理解並實現一個簡單的人臉識別演算法(下):人臉識別演算法
- 人臉識別:技術應用與商業實踐
- 人臉識別和手勢識別應用(face++)開發
- matlab實現人臉識別(數學基礎原理)Matlab
- 64行程式碼實現簡單人臉識別行程
- 人臉識別技術,將電影變成現實
- 人臉識別 -- 活體檢測(張嘴搖頭識別)
- 人臉識別 — 活體檢測(張嘴搖頭識別)
- 人臉檢測識別,人臉檢測,人臉識別,離線檢測,C#原始碼C#原始碼
- 使用TensorFlow實現手寫識別(Softmax)
- 虹軟人臉識別ArcSoft3.0NodeJs 版本實現NodeJS
- 【ROS】OpenCV+ROS 實現人臉識別(Ubantu16.04)ROSOpenCV
- Mars演算法實踐——人臉識別演算法
- 人臉識別檢測專案實戰
- 從零玩轉人臉識別之RGB人臉活體檢測
- 在PaddlePaddle上實現MNIST手寫體數字識別
- C#人臉識別入門篇-STEP BY STEP人臉識別--入門篇C#
- 智慧人臉識別門禁系統開發,人臉識別開鎖流程
- 人臉識別損失函式疏理與分析函式
- 人臉識別之特徵臉方法(Eigenface)特徵
- 前端人臉識別--兩張臉相似度前端
- 程式碼實現(機器學習識別手寫數字)機器學習