這個學期要學DM&ML,用的是《數據挖掘算法原理與實現》王振武 本着造福同學的思想,開一個DM&ML的筆記系列,打算給書上的源代碼添加一點註釋,方便閱讀和理解。
前置知識要求:
C++,STL,離散數學
這份算法的實現,大量使用了STL的容器和迭代器,對於不熟悉C++的同學,請順便了解什麼是命名空間(using namespace std;Apriori:: 之類的),稍微看一下用到的vector,map,pair容器大概是怎麼樣,瞭解下模版(vector
具體實現:
//書中原註釋直接給出,我加的註釋應該都加上了hiro:的字樣,僅供參考。
#include<iostream>
#include<string>
#include <vector>
#include <map>
#include <algorithm>
using namespace std;
class Apriori
{
public:
Apriori(size_t is =0,unsigned int mv=0)
{
item_size = is;
min_value = mv;
}
//~Apriori() {};
void getItem();
map< vector<string>,unsigned int> find_freitem();//求事務的頻繁項
//連接連個k-1級頻繁項,得到第k級頻繁項
map< vector<string>,unsigned int > apri_gen(unsigned int K , map< vector<string>,unsigned int > K_item);
//展示頻繁項集
void showAprioriItem(unsigned int K,map< vector<string>,unsigned int > showmap);
private:
map< int , vector<string> > item;//存儲所有最開始的事務及其項
map< vector<string>,unsigned int > K_item;//存儲頻繁項集
size_t item_size;//事務數目
unsigned int min_value;//最小閾值
};
void Apriori::getItem()//用戶輸入最初的事務集
{
int ci = item_size;
for (int i=0;i<ci;i++)
{
string str;
vector<string> temp;
cout<<"請輸入第 "<<i+1<<"個事務的項集(123 end):";
while (cin>>str && str !="123")
{
temp.push_back(str);
}
sort(temp.begin(),temp.end());
//hiro:map 的鍵值對 用pair來存放,插入後返回迭代器和bool組成的pair
pair< map<int ,vector<string> >::iterator , bool> ret = item.insert(make_pair(i+1 ,temp));
if (!ret.second)
{
--i;
cout<<"你輸入的元素已存在!請重新輸入!"<<endl;
}
}
cout<<"-------------運行結果如下:--------------"<<endl;
}
map< vector<string>,unsigned int> Apriori::find_freitem()
{
/*hiro:1.檢查初始的事務集非空*/
unsigned int i = 1;
bool isEmpty = false;
map< int , vector<string> >::iterator mit ;
for (mit=item.begin();mit != item.end();mit++)
{
vector<string> vec = mit->second;
if (vec.size() != 0)
break;
}
if (mit == item.end())//hiro:事務集爲空
{
isEmpty = true;
cout<<"事務集爲空!程序無法進行..."<<endl;
map< vector<string>,unsigned int> empty;
return empty;//hiro:局部變量不會被吞掉嗎,,,,不是等價於NULL嗎
}
/*hiro:2.*/
while(1)
{
map< vector<string>,unsigned int > K_itemTemp = K_item;
K_item = apri_gen(i++,K_item);//hiro:將i傳進去函數以後,i變量本身才+1s,
if (K_itemTemp == K_item)//hiro:篩選到最後,結束
{
i = UINT_MAX;
break;
}
//判斷是否需要進行下一次的尋找
map< vector<string>,unsigned int > pre_K_item = K_item;
size_t Kitemsize = K_item.size();
//存儲應該刪除的第K級頻繁項集,不能和其他K級頻繁項集構成第K+1級項集的集合
/*hiro:看了一些資料,都沒有提及都下面的這個要刪除的項到底是什麼,但是根據上面的註釋以及代碼閱讀得知
這裏是打算刪除K級項中,兩兩間交集的所有集合?*/
if (Kitemsize != 1 && i != 1)
{
vector< map< vector<string>,unsigned int >::iterator > eraseVecMit;//hiro:保存要刪除的項的迭代器
map< vector<string>,unsigned int >::iterator pre_K_item_it1 = pre_K_item.begin() , pre_K_item_it2;
//hiro:這個謎之pre_K_item_it2,上下文好像沒有再次出現過。。。
while (pre_K_item_it1 != pre_K_item.end() )
{
map< vector<string>,unsigned int >::iterator mit = pre_K_item_it1;
bool isExist = true;
vector<string> vec1;
vec1 = pre_K_item_it1->first;
vector<string> vec11(vec1.begin(), vec1.end() - 1);
/*hiro:end()本身指向vector末尾,相當於 定義了 int a[5],然後a[5]就是這裏的end()
/ 而這個vector的構造器構造的範圍是[begin,end),所以vec11複製了vec1的最後一個元素以外所有元素,即
/int a[5]={0,1,2,3,4},b={0,1,2,3}*/
while (mit != pre_K_item.end())
{
vector<string> vec2;
vec2 = mit->first;
vector<string> vec22(vec2.begin(),vec2.end()-1);
if (vec11 == vec22)
break;
++mit;
}
if (mit == pre_K_item.end())
isExist = false;
if (!isExist && pre_K_item_it1 != pre_K_item.end())
eraseVecMit.push_back(pre_K_item_it1);//該第K級頻繁項應該刪除
++pre_K_item_it1;
}
size_t eraseSetSize = eraseVecMit.size();
if (eraseSetSize == Kitemsize)
break;
else
{
vector< map< vector<string>,unsigned int >::iterator >::iterator currentErs = eraseVecMit.begin();
while (currentErs != eraseVecMit.end())//刪除所有應該刪除的第K級頻繁項
{
map< vector<string>,unsigned int >::iterator eraseMit = *currentErs;
K_item.erase(eraseMit);
++currentErs;
}
}
}
else
if(Kitemsize == 1 )
break;
}
cout<<endl;
showAprioriItem(i,K_item);
return K_item;
}
/*hiro:完成了連接和剪枝,返回符合支持度的
1.求候選集C_K:把兩個K級頻繁項集在滿足連接條件的情況下連接起來
2.統計K+1級候選頻繁項集的支持度並進行剪枝
*/
map< vector<string>,unsigned int > Apriori::apri_gen(unsigned int K , map< vector<string>,unsigned int > K_item)
{
if (1 == K)//求候選集C1
{
size_t c1 = item_size;
map< int , vector<string> >::iterator mapit = item.begin();
vector<string> vec;
map<string,unsigned int> c1_itemtemp;
while (mapit != item.end() )//將原事務中所有的單項統計出來
{
vector<string> temp = mapit->second;
vector<string>::iterator vecit = temp.begin();
while (vecit != temp.end() )
{
pair< map<string,unsigned int>::iterator , bool > ret = c1_itemtemp.insert(make_pair(*vecit++ , 1));
if (!ret.second)
{
++ ret.first->second;
}
}
++mapit;
}
map<string,unsigned int>::iterator item_it = c1_itemtemp.begin();
map< vector<string>,unsigned int > c1_item;
while (item_it != c1_itemtemp.end() )//構造第一級頻繁項集
{
vector<string> temp;
if ( item_it->second >= min_value)
{
temp.push_back(item_it->first);
c1_item.insert(make_pair(temp , item_it->second) );
}
++item_it;
}
return c1_item;
}
else
{
/* hiro:1.求候選集C_K:把兩個K級頻繁項集在滿足連接條件的情況下連接起來*/
cout<<endl;
showAprioriItem(K-1,K_item);
map< vector<string>,unsigned int >::iterator ck_item_it1 = K_item.begin(),ck_item_it2;
map< vector<string>,unsigned int > ck_item;
while (ck_item_it1 != K_item.end() )
{
ck_item_it2 = ck_item_it1;
++ck_item_it2;
map< vector<string>,unsigned int >::iterator mit = ck_item_it2;
//取當前第K級頻繁項與其後面的第K級頻繁項集聯合,但要注意聯合條件
//聯合條件:連個頻繁項的前K-1項完全相同,只是第K項不同,然後兩個聯合生成第K+1級候選頻繁項
while(mit != K_item.end() )
{
vector<string> vec,vec1,vec2;
vec1 = ck_item_it1->first;
vec2 = mit->first;
vector<string>::iterator vit1,vit2;
vit1 = vec1.begin();
vit2 = vec2.begin();
while (vit1 < vec1.end() && vit2 < vec2.end() )
{
string str1 = *vit1;
string str2 = *vit2;
++vit1;
++vit2;
if ( K ==2 || str1 == str2 )
{
if (vit1 != vec1.end() && vit2 != vec2.end() )
{
vec.push_back(str1);
}
}
else
break;
}
if (vit1 == vec1.end() && vit2 == vec2.end() )
{
--vit1;
--vit2;
string str1 = *vit1;
string str2 = *vit2;
/*hiro:新的候選項按字典序存放 所以>*/
if (str1>str2)
{
vec.push_back(str2);
vec.push_back(str1);
}
else
{
vec.push_back(str1);
vec.push_back(str2);
}
/*hiro:至此爲止生成了一個新的候選項*/
/*2.統計K + 1級候選頻繁項集的支持度並進行剪枝*/
map< int , vector<string> >::iterator base_item = item.begin();
unsigned int Acount = 0 ;
/*hiro:子集的判斷*/
while (base_item != item.end() )//統計該K+1級候選項在原事務集出現次數
{
unsigned int count = 0 ,mincount = UINT_MAX;
vector<string> vv = base_item->second;
vector<string>::iterator vecit , bvit ;
for (vecit = vec.begin();vecit < vec.end();vecit++)
{
string t = *vecit;
count = 0;
for (bvit=vv.begin();bvit < vv.end();bvit++)
{
if (t == *bvit)
count++;
}
/*hiro:這裏的數據設計很巧妙,一旦count曾經爲0,即有一個不相符,min就永遠爲0*/
/*然而是不是應該有更優雅的方式,,,?*/
mincount = (count < mincount ? count : mincount );
}
if (mincount >=1 && mincount != UINT_MAX)
Acount += mincount;
++base_item;
}
/*hiro:這裏的Acount直接解決了樣例數據中 {I1,I2,I3,I5},子集{I2,I3,I5}不是頻繁的問題,
貌似可以證明這裏用Acount就足夠判斷{I1,I2,I3,I5}的子集{I2,I3,I5}不是頻繁集
因爲K+1級是由兩個K級的集合組成的,這兩個K級的頻繁項集不能保證鏈接後的結果也是頻繁,
而造成連接後的結果不頻繁,也正是因爲有不頻繁的子集存在;
對不起水平有限,或許再理一理清思路應該可以證明的,但是【攤手】,只能表達個大概意思了。
其實就是書上37頁關於剪枝步的說明。
*/
if (Acount >= min_value && Acount != 0)
{
sort(vec.begin(),vec.end());
//該第K+1級候選項爲頻繁項,插入頻繁項集
pair< map< vector<string>,unsigned int >::iterator , bool> ret = ck_item.insert(make_pair(vec,Acount));
if (! ret.second)
{
ret.first->second += Acount;
}
}
}
++mit;
}
++ck_item_it1;
}
if (ck_item.empty())//該第K+1級頻繁項集爲空,說明調用結束,把上一級頻繁項集返回
return K_item;
else
return ck_item;
}
}
void Apriori::showAprioriItem(unsigned int K,map< vector<string>,unsigned int > showmap)
{
map< vector<string>,unsigned int >::iterator showit = showmap.begin();
if (K != UINT_MAX)
cout<<endl<<"第 "<<K<<" 級頻繁項集爲:"<<endl;
else
cout<<"最終的頻繁項集爲:"<<endl;
cout<<"項 集"<<" \t "<<"頻率"<<endl;
while (showit != showmap.end() )
{
vector<string> vec = showit->first;
vector<string>::iterator vecit = vec.begin();
cout<<"{ ";
while (vecit != vec.end())
{
cout<<*vecit<<" ";
++vecit;
}
cout<<"}"<<" \t ";
cout<<showit->second<<endl;
++showit;
}
}
unsigned int parseNumber(const char * str)//對用戶輸入的數字進行判斷和轉換
{
if (str == NULL)
return 0;
else
{
unsigned int num = 0;
//hiro:size_t類型 用於跨平臺,取該硬件平臺下最大的數值,比如32位取4B,64位取8B
size_t len = strlen(str);
for (size_t i=0;i<len;i++)
{
num *= 10;
if (str[i]>= '0' && str[i] <= '9')
num += str[i] - '0';
else
return 0;
}
return num;
}
}
void main()
{
//Apriori a;
unsigned int itemsize = 0;
unsigned int min;
/*hiro:爲何不直接cin>>??爲了無限長輸入整數?不明所以*/
do
{
cout<<"請輸入事務數:";
char * str = new char;
cin>>str;
itemsize = parseNumber(str);
if (itemsize == 0)
{
cout<<"請輸入大於0正整數!"<<endl;
}
} while (itemsize == 0);
do
{
cout<<"請輸入最小閾值:";
char * str = new char;
cin>>str;
min = parseNumber(str);
if (min == 0)
{
cout<<"請輸入大於0正整數!"<<endl;
}
} while (min == 0);
Apriori a(itemsize,min);
a.getItem();
map< vector<string>,unsigned int> AprioriMap = a.find_freitem();
//a.showAprioriItem(UINT_MAX,AprioriMap);
system("pause");
}
感想
首先這份代碼目前還有一段不是很明白他的目的,爲何要刪掉兩兩間沒有交集的頻繁項?這個留着給老師發郵件解決,我會更新這個問題【如果還記得】
正式接觸DM&ML,之前大致瞭解過,知道是做一些數據的統計和分析的。但是這個書上的實現版本,說實話不敢恭維。如果設計一下數據結構的話,應該不至於把遍歷寫得如此的彆扭,源碼中使用了很大量的局部變量進行副本存檔,效率也是各種堪憂。。。
當然我自己編碼經驗不豐富,也可能是我不習慣這種編碼的風格吧。
算法本身的效率也是很成問題的,空間開銷巨大,時間開銷也是N^2以上的級別,應該是會有優化的方案的,我暫時還沒有去了解。
今天就先到這裏吧。
附樣例數據:
9
2
I1 I2 I5 123
I2 I4 123
I2 I3 123
I1 I2 I4 123
I1 I3 123
I2 I3 123
I1 I3 123
I1 I2 I3 I5 123
I1 I2 I3 123