C/C++實現(xiàn)手寫數(shù)字識別的示例詳解
絕大多數(shù)人都是用python寫,俺就喜歡用C/C++一句一句寫。代碼還未認真整理,寫的有點亂,應該還有優(yōu)化的余地。
程序主要目的:對測試數(shù)據(jù)集的樣本進行預測,計算預測準確率。
數(shù)據(jù)集:分別使用32*32數(shù)據(jù)集和28*28數(shù)據(jù)集(mnist數(shù)據(jù)集)。前者是txt文本,后者是png圖片。
算法:使用最簡單的KNN算法。
加快程序執(zhí)行的主要思路:
1.使用多線程(10個線程)讀取0到9數(shù)字對應的樣本文件;
2.使用多線程(10個線程)對0到9數(shù)字對應的樣本進行預測;
3.求距離時,使用歐式距離,但是不做開平方運算(開平方運算是多余的,浪費時間);
4.求K個最近距離鄰居(類似于求TOP K),采用C++的push_heap和pop_heap(小根堆)。
對于32*32的數(shù)據(jù)集,由于樣本是txt文件,直接讀取txt文件中的0和1,很容易求出樣本之間的距離。
對于mnist數(shù)據(jù)集,因為原始數(shù)據(jù)是png圖片,所以需要做一些預處理:可以使用opencv讀取像素數(shù)據(jù),轉成類似于32*32的txt數(shù)據(jù)集,方便求距離。另外,方便程序處理,重命名了文件名,文件名分別是0_0.txt,0_1.txt……,轉換成txt文件的mnist數(shù)據(jù)集如下:

簡便起見,對樣本個數(shù)做了一些簡化,32*32數(shù)據(jù)集的訓練數(shù)據(jù)是180*10=1800個,測試數(shù)據(jù)是80*10=800個,mnist數(shù)據(jù)集的訓練數(shù)據(jù)是5000*10=5萬個,測試數(shù)據(jù)是800*10=8千個(原始測試樣本是1萬個,每個數(shù)字的測試樣本個數(shù)不同)。從數(shù)據(jù)量對比來看,后者是前者的280倍(訓練數(shù)據(jù)集是28倍,測試數(shù)據(jù)集是10倍),對應的程序運行時間(計算測試樣本的預測準確率)后者也是前者的200多倍(排序算法很重要,如果不使用小根堆求TOP K,而是使用傳統(tǒng)的整體數(shù)據(jù)排序,估計運行速度會差很多)。
部分代碼如下(代碼還未整理,比較亂,暫時只貼主要代碼,表明整體思路),具體代碼回頭整理好了再貼。
#define TrainingDataNum 50000
#define TestDataNum 8000
#define FeatureNum 784 //28*28
#define ClassNum 10
#define FileMaxCol 40 //大于28
#define K 3
//……………………
struct DisAndLabel
{
int distance;
int classLabel;
};
bool Cmp(const DisAndLabel& a, const DisAndLabel& b)
{
return a.distance < b.distance;
}
int FileToTrainingDataArray(int** pTrainingDataFeture, int* pTrainingDataClass, int tid);
int FileToTestDataArray(int** pTestDataFeture, int* pTestDataClass, int tid);
void ClassifyTest(int** pTestDataFeture, int** pTrainingDataFeture, int* pTestDataClass, int* pTrainingDataClass, int tid);
//……………………
int main()
{
clock_t t1, t2, t3, t4, t5;
t1 = clock();
int** pTrainingDataFeture = new int* [TrainingDataNum];
if (pTrainingDataFeture == NULL)
{
exit(-1);
}
for (int i = 0; i < TrainingDataNum; i++)
{
pTrainingDataFeture[i] = new int[FeatureNum + 1];
if (pTrainingDataFeture[i] == NULL)
{
exit(-1);
}
}
//……………………
t2 = clock();
double duration = (double(t2) - double(t1)) / CLOCKS_PER_SEC;
cout << "分配內存:" << duration << "秒" << endl;
//……………………
thread td[10];
for (int tid = 0; tid < 10; tid++)
{
td[tid] = thread(&FileToArrayThread, pTrainingDataFeture, pTrainingDataClass, pTestDataFeture, pTestDataClass, tid);
}
for (int tid = 0; tid < 10; tid++)
{
td[tid].join();
}
//……………………
int errorCountTotal = 0;
for (int i = 0; i < 10; i++)
{
errorCountTotal += errorCount[i];
}
cout << "*********************************************************" << endl;
cout << "測試集測試結果:" << endl;
cout << "k=" << K << endl;
cout << "測試集樣本個數(shù):" << TestDataNum << endl;
cout << "預測錯誤個數(shù):" << errorCountTotal << endl;
cout << "錯誤率:" << double(errorCountTotal * 100.0 / TestDataNum) << "%" << endl;
cout << "正確率:" << 100.0 - double(errorCountTotal * 100.0 / TestDataNum) << "%" << endl;
//cout << "*********************************************************" << endl;
t4 = clock();
duration = (double(t4) - double(t3)) / CLOCKS_PER_SEC;
cout << "*********************************************************" << endl;
cout << "8000個樣本預測準確率分析:" << duration << "秒" << endl;
duration = (double(t4) - double(t1)) / CLOCKS_PER_SEC;
cout << "*********************************************************" << endl;
cout << "總共用時:" << duration << "秒" << endl;
//……………………
}運行結果如下:

K=3時,8000個測試樣本,預測錯誤了78個(其實算法沒有問題,很多情況是因為測試樣本里的數(shù)字寫的太奇葩了,數(shù)字X實在不像數(shù)字X),下面把K改為5,準確率提高了一些。

從運行時間看,8000個樣本的預測用時是5秒多,如果只預測一個樣本,用時大概是5.4秒/8000=0.7毫秒,主要是訓練數(shù)據(jù)集數(shù)據(jù)有點多(5萬個樣本)。
下面是32*32數(shù)據(jù)集的運行結果,由于訓練樣本和測試樣本的數(shù)據(jù)量比mnist數(shù)據(jù)集少很多,所以程序運行速度快了200倍,平均每個樣本的預測時間大概是0.03秒/800=0.04毫秒。
程序總共用時不到50毫秒(用python寫,貌似要10秒左右),比python快了200倍。C語言直接操作內存+高速緩存+宇宙第一IDE的代碼優(yōu)化+多線程,運行速度比python快2個數(shù)據(jù)級很正常。但是代碼量也比python多很多,一般2到3倍,如果python是直接調算法庫,比python多10倍以上 -_-。

樣本預測錯誤的具體原因是什么,可以分析K個最近樣本的距離值,以數(shù)字8(預測錯誤的重災區(qū))為例:

到此這篇關于C/C++實現(xiàn)手寫數(shù)字識別的示例詳解的文章就介紹到這了,更多相關C++數(shù)字識別內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
C++實現(xiàn)選擇排序(selectionSort)
這篇文章主要為大家詳細介紹了C++實現(xiàn)選擇排序,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-04-04
C++ 標準庫中的 <algorithm> 頭文件算法操作總結
C++ 標準庫中的 <algorithm> 頭文件提供了大量有用的算法,主要用于操作容器(如 vector, list, array 等),這些算法通常通過迭代器來操作容器元素,本文給大家介紹C++ 標準庫中的 <algorithm> 頭文件算法總結,感興趣的朋友一起看看吧2025-04-04

