在人间漂流

厚德 求真 励学 笃行


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 公益404

树的最小支配集,最小点覆盖集,最大点独立集

发表于 2019-06-20 | 分类于 Algorithm | 0 comments
阅读次数:
  |   字数统计: 3.8k(字)   |   阅读时长: 15(分)

首先看一下三者的定义:

定义1对于图G=(V,E)来说, 最小支配集 指的是从V中取尽量少的点组成一个集合,使得对于V中剩余的点都与取出来的点有边相连。也就是说,设V‘是图G的一个支配集,则对于图中的任意一个顶点u,要么属于集合V’,要么与V‘中的顶点相邻。在V’中出去任何元素后V‘不再是支配集,则支配集是极小支配集。称G的所有支配集中顶点个数最少的支配集为最小支配集,最小支配集中顶点的个数称为支配数。

定义2对于图G=(V,E)来说, 最小点覆盖 指的是从V中取尽量少的点组成一个集合,使得E中所有的边都与取出来的点相连。也就是说,设V‘是图G的一个顶点覆盖,则对于图中的任意一条边(u,v),要么u属于集合V’,要么v属于集合V‘。在V‘中除去任何元素后V’不在是顶点覆盖,则V‘是极小顶点覆盖。称G的所有顶点覆盖中顶点个数最少的覆盖为最小点覆盖。

定义3对于图G=(V,E)来说, 最大独立集 指的是从V中取尽量多的点组成一个集合,使得这些点之间没有边相连。也就是说,设V’是图G的一个独立集,则对于图中任意一条边(u,v),u和v不能同时属于集合V’,甚至可以u和v都不属于集合V‘。在V’中添加任何不属于V‘元素后V’不再是独立集,则V‘是极大独立集。称G的所有顶点独立集中顶点个数最多的独立集为最大独立集。

对于任意图G来说,这三个问题不存在多项式时间的解法。 不过对于树来说,却很容易。目前有两种解法,一种基于贪心思想,另一种基于树形动态规划思想。贪心法能得到具体的点集,而动态规划只能得到集合大小。

一、贪心法

基本算法:

以最小支配集为例,对于树上的最小支配集问题,贪心策略是首先选择一点为根,按照深度优先遍历得到遍历序列,按照所得序列的反向序列的顺序进行贪心,对于一个既不属于支配集也不与支配集中的点相连的点来说,如果他的父节点不属于支配集,将其父节点加入支配集。

这里注意到贪心的策略中贪心的顺序非常重要,按照深度优先遍历得到遍历序列的反向进行贪心,可以保证对于每个点来说,当期子树都被处理过后才轮到该节点的处理,保证了贪心的正确性。

伪代码:

①以1号点深度优先搜索整棵树,求出每个点在深度优先遍历序列中的编号和每个点的父节点编号。

②按照深度优先序列的反向序列检查,如果当前点既不属于支配集也不与支配集中的点相连,且他的父节点不属于支配集,将其父节点加入支配集,支配集中点的个数加1.标记当前节点、当前节点的父节点和当前节点的父节点的父节点,因为这些节点要么属于支配集,要么与支配集中的点相连。

最小点覆盖于最大独立集与上面的做法相似。对于最小点覆盖来说,贪心的策略是,如果当前点和当前点的父节点都不属于顶点覆盖集合,则将父节点加入到顶点覆盖集合,并标记当前节点和其父节点都被覆盖。对于最大独立集来说,贪心策略是如果当前点没有被覆盖,则将当前节点加入到独立集,并标记当前节点和其父节点都被覆盖。

需要注意的是由于默认程序中根节点和其他节点的区别在于根节点的父节点是其自己,所以三种问题对根节点的处理不同。对于最小支配集和最大独立集,需要检查根节点是否满足贪心条件,但是对于最小点覆盖不可以检查根节点。

具体实现:

采用链式前向星存储整棵树。对于DFS(),newpos[i]表示深度优先遍历序列的第i个点是哪个点,now表示当前深度优先遍历序列已经有多少个点了。select[]用于深度优先遍历序列的判重,p[i]表示点i的父节点的编号。对于greedy(),s[i]如果为true,表示第i个点被覆盖。set[i]表示点i属于要求的点集。

最小支配集贪心实现

树上最小支配集贪心完整实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#pragma GCC optimize("O2")
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5; //点数目
const int maxm = 1e6; //边数目

struct Edge
{
int to;
int next;
}edge[maxm];

int p[maxn]; //父节点
int head[maxn];
bool domi_set[maxn]; //最小支配集包含的点
bool select[maxn];
int newpos[maxn]; //dfs顺序访问元素
int now, cnt;
int n, m; //输入图的点、边数目

//无向边
void add_edge(int u, int v)
{
edge[cnt].to = v, edge[cnt].next = head[u], head[u] = cnt++;
edge[cnt].to = u, edge[cnt].next = head[v], head[v] = cnt++;
}

//获取dfs序
void dfs(int x)
{
newpos[now++]=x;
int k;
for(k=head[x];k!=-1;k=edge[k].next)
{
if(!select[edge[k].to])
{
select[edge[k].to]=true;
p[edge[k].to]=x;
dfs(edge[k].to); }
}
}

//dfs逆序贪心,这样遍历到的当前节点其子树节点已全部计算完毕
int greedy()
{
bool s[maxn];
int ans=0;
int i;
for(i=n-1;i>=0;i--)
{
int t=newpos[i];
if(!s[t])
{
if(!domi_set[p[t]])
{
domi_set[p[t]]=true;
ans++;
}
s[t]=true;
s[p[t]]=true;
s[p[p[t]]]=true;
}
}
return ans;
}

void init() {
now = 0;
memset(head, -1, sizeof(head));
memset(select, 0, sizeof(select));
memset(domi_set, 0, sizeof(domi_set));
}

void read() {
init();
scanf("%d%d", &n, &m);
int a, b;
for (int i = 0; i < m; i++) {
scanf("%d%d", &a, &b);
add_edge(a, b);
}
}

int main() {
//读入图信息
read();
//从0开始dfs
select[0] = 1;
p[0] = 0;
dfs(0);
printf("最小点支配集大小: %d\n", greedy());
//输出最小支配集
for (int i = 0; i < n; i++) {
if (domi_set[i]) {
printf("%d ", i);
}
}
}

最小点覆盖集贪心实现

对于最小点覆盖集,将greedy函数改为如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int greedy()
{
bool s[maxn]={0};
int ans=0;
int i;
for(i=n-1;i>=1;i--)
{
int t=newpos[i];
if(!s[t]&&!s[p[t]])
{
domi_set[p[t]]=true;
ans++;
s[t]=true;
s[p[t]]=true;
}
}
return ans;
}

最大点独立集贪心实现

对于最大点独立集,将greedy函数改为如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int greedy()
{
bool s[maxn]={0};
int ans=0;
int i;
for(i=n-1;i>=0;i--)
{
int t=newpos[i];
if(!s[t])
{
domi_set[t]=true;
ans++;
s[t]=true;
s[p[t]]=true;
}
}
return ans;
}

复杂度分析:该方法经过一次深度优先遍历和一次贪心得到最终解,第一步的时间复杂度为O(m),由于这是一棵树,m=n-1.第二步是O(n),总的是O(n)。

二、树形动态规划法

最小支配集树形dp实现

基本算法:

由于这是在树上求最值的问题,显然可以用树形动态规划,只是状态的设计比较复杂。为了保证动态规划的正确性,对于每个点设计了三种状态,这三种状态的意义如下:

  • dp[i][0]:表示点i属于支配集,并且以点i为根的子树都被覆盖了的情况下支配集中所包含的的最少点的个数。

  • dp[i][1]:i不属于支配集,且以i为根的子树都被覆盖,且i被其中不少于1个子节点覆盖的情况下支配集中所包含最少点的个数。

  • dp[i][2]:i不属于支配集,且以i为根的子树都被覆盖,且i没被子节点覆盖的情况下支配集中所包含最少点的个数。

对于第一种状态,dp[i][0]等于每个儿子节点的3种状态(其儿子是否被覆盖没有关系)的最小值之和加1,即只要每个以i的儿子为根的子树都被覆盖,再加上当前点i,所需要的最少点的个数,方程如下:

对于第二种状态,如果点i没有子节点,那么dp[i][1]=INF;否则,需要保证它的每个以i的儿子为根的子树都被覆盖,那么要取每个儿子节点的前两种状态的最小值之和,因为此时i点不属于支配集,不能支配其子节点,所以子节点必须已经被支配,与子节点的第三种状态无关。如果当前所选的状态中,每个儿子都没有被选择进入支配集,即在每个儿子的前两种状态中,第一种状态都不是所需点最少的,那么为了满足第二种状态的定义,需要重新选择点i的一个儿子的状态为第一种状态,这时取花费最少的一个点,即取min(dp[u][0]-dp[u][1])的儿子节点u,强制取其第一种状态,其他儿子节点都取第二种状态,转移方程为:

1
2
if(i没有子节点)  dp[i][1]=INF
else dp[i][1]=Σmin(dp[u][0],dp[u][1])+inc

其中对于inc有:

1
2
if(上面式子中的Σmin(dp[u][0],dp[u][1])中包含某个dp[u][0]) inc=0;
else inc=min(dp[u][0]-dp[u][1])。

对于第三种状态,i不属于支配集,且以i为根的子树都被覆盖,又i没被子节点覆盖,那么说明点i和点i的儿子节点都不属于支配集,则点i的第三种状态只与其儿子的第二种状态有关,方程为

树上最小支配集树形dp完整实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#pragma GCC optimize("O2")
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5; //点数目
const int maxm = 1e6; //边数目
const int INF = 0x7FFFFFFF;

struct Edge
{
int to;
int next;
}edge[maxm];

//dp[i][0]:表示点i属于支配集,并且以点i为根的子树都被覆盖了的情况下支配集中所包含的的最少点的个数
//dp[i][1]:i不属于支配集,且以i为根的子树都被覆盖,且i被其中不少于1个子节点覆盖的情况下支配集中所包含最少点的个数
//dp[i][2]:i不属于支配集,且以i为根的子树都被覆盖,且i没被子节点覆盖的情况下支配集中所包含最少点的个数
int dp[maxn][3];

int n, m; //输入图的点、边数目
int cnt;
int head[maxn];

void add_edge(int u, int v)
{
edge[cnt].to = v, edge[cnt].next = head[u], head[u] = cnt++;
edge[cnt].to = u, edge[cnt].next = head[v], head[v] = cnt++;
}


void DP(int u,int p) {
dp[u][2] = 0;
dp[u][0] = 1;
bool s = false;
int sum = 0, inc = INF;
int k;
for(k = head[u]; k != -1; k = edge[k].next) {
int to = edge[k].to;
if(to == p) continue;
DP(to, u);
dp[u][0] += min(dp[to][0], min(dp[to][1], dp[to][2]));
if(dp[to][0] <= dp[to][1]) {
sum += dp[to][0];
s = true;
} else {
sum += dp[to][1];
inc = min(inc, dp[to][0] - dp[to][1]);
}
if(dp[to][1] != INF && dp[u][2] != INF) dp[u][2] += dp[to][1];
else dp[u][2] = INF;
}
if(inc == INF && !s) dp[u][1] = INF;
else {
dp[u][1]=sum;
if(!s) dp[u][1] += inc;
}
}

void init() {
memset(head, -1, sizeof(head));
}

void read() {
init();
scanf("%d%d", &n, &m);
int a, b;
for (int i = 0; i < m; i++) {
scanf("%d%d", &a, &b);
add_edge(a, b);
}
}

int main() {
read();
//假设0为root
DP(0, 0);
printf("最小支配集大小: %d\n", min(dp[0][0], dp[0][1]));
}

最终的最小支配集大小为:min(dp[0][0], dp[0][1])

最小点覆盖集树形dp实现

对于最小的覆盖问题,为每个点设计了两种状态,这两种状态的意义如下:

  • dp[i][0]:表示点i属于点覆盖,并且以点i为根的子树中所连接的边都被覆盖的情况下点覆盖集中所包含最少点的个数。

  • dp[i][1]:表示点i不属于点覆盖,并且以点i为根的子树中所连接的边都被覆盖的情况下点覆盖集中所包含最少点的个数。

对于第一种状态dp[i][0],等于每个儿子节点的两种状态的最小值之和加1,方程如下:

对于第二种状态dp[i][1],要求所有与i连接的边都被覆盖,但是i点不属于点覆盖,那么i点所有的子节点都必须属于点覆盖,即对于点i的第二种状态与所有子节点的第二种状态无关,在数值上等于所有子节点的第一种状态之和。方程如下:

对于最小点覆盖集实现,将DP函数改为如下:

1
2
3
4
5
6
7
8
9
10
11
12
void DP(int u,int p) {
dp[u][0] = 1;
dp[u][1] = 0;
int k, to;
for(k = head[u]; k != -1; k = edge[k].next) {
to = edge[k].to;
if(to == p) continue;
DP(to, u);
dp[u][0] += min(dp[to][0], dp[to][1]);
dp[u][1] += dp[to][0];
}
}

最终的最小点覆盖集大小为:min(dp[0][0], dp[0][1])

最大点独立集树形dp实现

对于最大独立集问题,为每个节点设立两种状态,这两种状态的意义如下:

  • dp[i][0]:表示点i属于独立集的情况下,最大独立集中点的个数。

  • dp[i][1]:表示点i不属于独立集的情况下,最大独立集中点的个数。

对于第一种状态dp[i][0],由于点i属于独立集,他的子节点都不能属于独立集,所以只与第二种状态有关。方程如下:

对于第二种状态,点i的子节点可以属于独立集,也可以不属于独立集,方程如下:

对于最大点独立集实现 ,将DP函数改为如下:

1
2
3
4
5
6
7
8
9
10
11
12
void DP(int u,int p) {
dp[u][0] = 1;
dp[u][1] = 0;
int k, to;
for(k = head[u]; k != -1; k = edge[k].next) {
to = edge[k].to;
if(to == p) continue;
DP(to, u);
dp[u][0] += dp[to][1];
dp[u][1] += max(dp[to][0], dp[to][1]);
}
}

最终的最大点独立集大小为:max(dp[0][0], dp[0][1])

---------------- The End ----------------

作者: brooksjay
联系邮箱: jaypark@smail.nju.edu.cn
本文地址:
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布
转载请注明出处, 谢谢!

单调栈

发表于 2019-05-28 | 分类于 Algorithm , ACM Solution | 0 comments
阅读次数:
  |   字数统计: 1.2k(字)   |   阅读时长: 6(分)

单调栈理解起来很容易,但是实际运用却没那么简单。一句话解释单调栈,就是一个栈,里面的元素的大小按照他们所在栈内的位置,满足一定的单调性。

下面结合题目,在实战中学会使用单调栈解决问题。

Google面试题

(google面试)题目: 给一个数组,返回一个大小相同的数组。返回的数组的第i个位置的值应当是,对于原数组中的第i个元素,至少往右走多少步,才能遇到一个比自己大的元素(如果之后没有比自己大的元素,或者已经是最后一个元素,则在返回数组的对应位置放上-1)。

简单的例子:

input: 5,3,1,2,4

return: -1 3 1 1 -1

解决方案:递减栈

implementation:

1
2
3
4
5
6
7
8
9
10
11
12
vector<int> nextExceed(vector<int> &input) {
vector<int> result (input.size(), -1);
stack<int> monoStack;
for(int i = 0; i < input.size(); ++i) {
while(!monoStack.empty() && input[monoStack.top()] < input[i]) {
result[monoStack.top()] = i - monoStack.top();
monoStack.pop();
}
monoStack.push(i);
}
return result;
}

LeetCode-42

题目: LeetCode-42. Trapping Rain Water

解决方案:递减栈

implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int trap(vector<int>& height) {
vector<int> st(height.size());
int head = -1;
int res = 0;
for (int i = 0; i < height.size(); i++) {
while (head >= 0 && height[st[head]] <= height[i]) {
if (st[head] + 1 != i) {
res += (height[st[head]] - height[st[head + 1]]) * (i - st[head] - 1);
}
head--;
}
if (head >= 0) res += (height[i] - height[st[head + 1]]) * (i - st[head] - 1);
st[++head] = i;
}
return res;
}
};

LeetCode-84

题目:LeetCode-84. Largest Rectangle in Histogram

Given n non-negative integers representing the histogram’s bar height where the width of each bar is 1, find the area of largest rectangle in the histogram.

Above is a histogram where width of each bar is 1, given height = [2,1,5,6,2,3].

The largest rectangle is shown in the shaded area, which has area = 10 unit.

For example,
Given heights = [2,1,5,6,2,3],
return 10.

解决方案:递增栈

implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int largestRectangleArea(vector<int> &height) {
int ret = 0;
height.push_back(0);
stack<int> index;
for(int i = 0; i < height.size(); i++) {
while(index.size() > 0 && height[index.top()] > height[i]) {
// 计算以index.top()为轴的矩形大小,左右两边是已从栈中弹出的比index.top()高的小矩形
int h = height[index.top()];
index.pop();
int idx = index.empty() ? -1 : index.top();
ret = max(ret, (i - idx - 1) * h);
}
index.push(i);
}
return ret;
}

LeetCode-85

题目: LeetCode-85. Maximal Rectangle

Given a 2D binary matrix filled with 0’s and 1’s, find the largest rectangle containing only 1’s and return its area.

Example:

1
2
3
4
5
6
7
8
Input:
[
["1","0","1","0","0"],
["1","0","1","1","1"],
["1","1","1","1","1"],
["1","0","0","1","0"]
]
Output: 6

implementation:

方法一: 单调递增栈(20ms)
实质上与LeetCode-84求最大矩形面积等价,通过迭代行来更新高度数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
public:
int maximalRectangle(vector<vector<char>>& matrix) {
if (matrix.size() == 0) return 0;
int m = matrix.size(), n = matrix[0].size();
vector<int> height(n, 0);
int ret = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == '0') height[j] = 0;
else height[j]++;
}
ret = max(ret, largestRectangleArea(height));
}
return ret;
}
private:
int largestRectangleArea(vector<int> &height) {
int ret = 0;
height.push_back(0);
stack<int> index;
for(int i = 0; i < height.size(); i++) {
while(index.size() > 0 && height[index.top()] > height[i]) {
int h = height[index.top()];
index.pop();
int idx = index.empty() ? -1 : index.top();
ret = max(ret, (i - idx - 1) * h);
}
index.push(i);
}
height.pop_back();
return ret;
}
};

方法二:动态规划dp(16ms)
lefts[j]和rights[j]分别存储以height[j]为高度的矩阵的左边界和右边界

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
public:
int maximalRectangle(vector<vector<char>>& matrix) {
if (matrix.size() == 0) return 0;
int m = matrix.size(), n = matrix[0].size();
vector<int> heights(n, 0), lefts(n, 0), rights(n, n);
int ret = 0;
for (int i = 0; i < m; i++) {
int cur_left = 0, cur_right = n;
for (int j = 0; j < n; j++) {
if (matrix[i][j] == '0') heights[j] = 0;
else heights[j]++;
if (heights[j] > 0) {
lefts[j] = max(cur_left, lefts[j]);
} else {
lefts[j] = 0;
cur_left = j + 1;
}
}
for (int j = n - 1; j >= 0; j--) {
if (heights[j] > 0) {
rights[j] = min(cur_right, rights[j]);
} else {
rights[j] = n;
cur_right = j;
}
}
for (int j = 0; j < n; j++) {
ret = max(ret, (rights[j] - lefts[j]) * heights[j]);
}
}
return ret;
}
};

方法三:暴力枚举(60ms)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
int maximalRectangle(vector<vector<char>>& matrix) {
if (matrix.size() == 0) return 0;
int m = matrix.size(), n = matrix[0].size();
vector<int> heights(n, 0);
int ret = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == '0'){
heights[j] = 0;
continue;
} else heights[j]++;
int min_height = heights[j];
for (int k = j; k >= 0; k--) {
min_height = min(heights[k], min_height);
ret = max(ret, (j - k + 1) * min_height);
}
}
}
return ret;
}
};
---------------- The End ----------------

作者: brooksjay
联系邮箱: jaypark@smail.nju.edu.cn
本文地址:
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布
转载请注明出处, 谢谢!

基于sklearn的分类器实战

发表于 2019-05-23 | 分类于 Data Mining | 0 comments
阅读次数:
  |   字数统计: 3.2k(字)   |   阅读时长: 12(分)

完整代码实现见github:click me

一、实验说明

1.1 任务描述

1.2 数据说明

一共有十个数据集,数据集中的数据属性有全部是离散型的,有全部是连续型的,也有离散与连续混合型的。通过对各个数据集的浏览,总结出各个数据集的一些基本信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
连续型数据集: 
1. diabets(4:8d-2c)
2. mozilla4(6:5d-2c)
3. pc1(7:21d-2c)
4. pc5(8:38d-2c)
5. waveform-5000(9:40d-3c)
离散型数据集:
1. breast-w(0:9d-2c-?)
离散-连续混合型数据集:
1. colic(1:22d-2c-?)
2. credit-a(2:15d-2c-?)
3. credit-g(3:20d-2c)
4. hepatitis(少量离散属性)(5:19d-2c-?)

举一个例子说明,colic(1:22d-2c-?)对应colic这个数据集,冒号前面的1表示人工标注的数据集序号(在代码实现时我是用序号来映射数据集的),22d表示数据集中包含22个属性,2c表示数据集共有3种类别,’?’表示该数据集中含有缺失值,在对数据处理前需要注意。

二、数据预处理

由于提供的数据集文件格式是weka的.arff文件,可以直接导入到weka中选择各类算法模型进行分析,非常简便。但是我没有借助weka而是使用sklearn来对数据集进行分析的,这样灵活性更大一点。所以首先需要了解.arff的数据组织形式与结构,然后使用numpy读取到二维数组中。

具体做法是过滤掉.arff中’%’开头的注释,对于’@’开头的标签,只关心’@attribute’后面跟着的属性名与属性类型,如果属性类型是以’{}’围起来的离散型属性,就将这些离散型属性映射到0,1,2……,后面读取到这一列属性的数据时直接用建好的映射将字符串映射到数字。除此之外就是数据内容了,读完一个数据集的内容之后还需要检测该数据集中是否包含缺失值,这个使用numpy的布尔型索引很容易做到。如果包含缺失值,则统计缺失值这一行所属类别中所有非缺失数据在缺失属性上各个值的频次,然后用出现频次最高的值来替换缺失值,这就完成对缺失值的填补。具体实现可以参见preprocess.py模块中fill_miss函数。

三、代码设计与实现

实验环境:

python 3.6.7

configparser 3.7.4

scikit-learn 0.20.2

numpy 1.15.4

matplotlib 3.0.3

各个分类器都要用到的几个模块在这里做一个简要说明。

  • 交叉验证: 使用sklearn.model_selection.StratifiedKFold对数据作分层的交叉切分,分类器在多组切分的数据上进行训练和预测
  • AUC性能指标: 使用sklearn.metrics.roc_auc_score计算AUC值,AUC计算对多类(二类以上)数据属性还需提前转换成one hot编码,使用了sklearn,preprocessing.label_binarize来实现,对于多分类问题选择micro-average
  • 数据标准化: 使用sklearn.preprocessing.StandardScaler来对数据进行归一标准化,实际上就是z分数

3.1 朴素贝叶斯Naive Bayes

由于大部分数据集中都包含连续型属性,所以选择sklearn.naive_bayes.GaussianNB来对各个数据集进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
clf = GaussianNB()
skf = StratifiedKFold(n_splits=10)
skf_accuracy1 = []
skf_accuracy2 = []
n_classes = np.arange(np.unique(y).size)
for train, test in skf.split(X, y):
clf.fit(X[train], y[train])
skf_accuracy1.append(clf.score(X[test], y[test]))
if n_classes.size < 3:
skf_accuracy2.append(roc_auc_score(y[test], clf.predict_proba(X[test])[:, 1], average='micro'))
else:
ytest_one_hot = label_binarize(y[test], n_classes)
skf_accuracy2.append(roc_auc_score(ytest_one_hot, clf.predict_proba(X[test]), average='micro'))
accuracy1 = np.mean(skf_accuracy1)
accuracy2 = np.mean(skf_accuracy2)

在各个数据集上进行交叉验证后的accuracy和AUC性能指标如下

可以看到大部分数据集上的auc指标都比acc高,说明控制好概率阈值(这里默认0.5)acc可能还有提升空间,因为样本分布跟总体分布还有一定的差距,样本数布可能很不平衡,并且权衡一个合适的阈值点还需要结合分类问题的背景和关注重点。由于auc指标考虑到了所有可能阈值划分情况,auc越高能说明模型越理想,总体上能表现得更好。

3.2 决策树decision tree

使用sklearn.tree.DecisionTreeClassifier作决策树分析,并且采用gini系数选择效益最大的属性进行划分,下面给出接口调用方式,交叉验证方式与前面的naive bayes一样

1
clf = DecisionTreeClassifier(random_state=0, criterion='gini')

在各个数据集上进行交叉验证后的accuracy和AUC性能指标如下

3.3 K近邻KNN

使用sklearn.neighbors.KNeighborsClassifier实现KNN,邻居数设置为3即根据最近的3个邻居来投票抉择样本所属分类,因为数据集最多不超过3类,邻居数选择3~6较为合适,设得更高效果增益不明显并且时间开销大,下面给出接口调用方式

1
clf = KNeighborsClassifier(n_neighbors=3)

在各个数据集上进行交叉验证后的accuracy和AUC性能指标如下

3.4 神经网络之多层感知机MLP

使用sklearn.neural_network.MLPClassifier实现MLP,设置一层隐藏层100个节点,激活函数relu,优化器Adam,学习率1e-3并且自适应降低,l2正则项惩罚系数,下面给出具体的接口调用方式以及参数配置

1
2
3
4
5
6
7
8
9
clf = MLPClassifier(hidden_layer_sizes=(100), 
activation='relu',
solver='adam',
batch_size=128,
alpha=1e-4,
learning_rate_init=1e-3,
learning_rate='adaptive',
tol=1e-4,
max_iter=200)

在各个数据集上进行交叉验证后的accuracy和AUC性能指标如下

3.5 支持向量机SVM

使用sklean.svm.LinearSVC实现线性SVM分类器,接口调用方式如下

1
clf = LinearSVC(penalty='l2', random_state=0, tol=1e-4)

在各个数据集上进行交叉验证后的accuracy和AUC性能指标如下

四、实验结果与分析

4.1 不同数据集上的模型对比

4.1.1 breast-w dataset

breast-w数据集上,各分类模型的效果都很好,其中linear svm的准确率最高,mlp的auc值最高

4.1.2 colic dataset

colic数据集上,knn效果不佳,其它分类模型的效果都很好,其中decision tree的准确率最高,mlp的auc值最高

4.1.3 credit-a dataset

credit-a数据集上,各分类模型的效果都不是很好,其中decision tree的准确率最高,naive bayes的auc值最高

4.1.4 credit-g dataset

credit-a数据集上,各分类模型的效果都不是很好,其中naive bayes的准确率和auc值都是最高的

4.1.5 diabetes dataset

diabetes数据集上,各分类模型的效果都不是很好,其中naive bayes的准确率和auc值都是最高的

4.1.6 hepatitis dataset

hepatitis数据集上,各分类模型的准确率都没达到90%,decision tree的准确率最高,mlp的auc值最高,但是各分类模型的auc值基本都比acc高除了decision tree,说明hepatitis数据集的数据分布可能不太平衡

通过weka对hepatitis数据集上的正负类进行统计得到下面的直方图

从上面的直方图可以验证之前的猜测是对的,hepatitis数据集正负类1:4,数据分布不平衡,正类远少于负类样本数

4.1.7 mozilla4 dataset

mozilla4数据集上,各分类模型的表现差异很大,其中knn的acc和auc都是最高的,naivie bayes的acc和auc相差甚大

4.1.8 pc1 dataset

pc1数据集上,各分类模型的准确率基本都挺高的,但是auc值普遍都很低,使用weka对数据进行统计分析后发现pc1数据集的正负类比达到13:1,根据auc计算原理可知正类太多可能会导致TPR相比FPR会低很多,从而压低了auc值

4.1.9 pc5 dataset

pc5数据集上,各分类模型的准确率都达到了90%以上,但是auc都比acc要低,其中mlp和linear svm的acc与auc相差甚大,原因估计和pc1差不多,正类样本太多拉低了AUC,使用weka分析后发现pc5正负类样本比值达到了32:1,并且数据中夹杂着些许异常的噪声点

4.1.10 waveform-5000 dataset

waveform-5000数据集上,各分类模型的准确率基本都是在80%左右,各分类模型的auc基本都有90%除了decision tree以外。waveform-5000是一个三类别的数据集,相比前面的2分类数据集预测难度也会更大,概率阈值的选择尤为关键,一个好的阈值划分会带来更高的准确率。

4.2 模型的bagging和single性能对比

4.2.1 breast-w dataset

准确率对比

AUC对比

4.2.1 colic dataset

准确率对比

AUC对比

4.2.3 credit-a dataset

准确率对比

AUC对比

4.2.4 credit-g dataset

准确率对比

AUC对比

4.2.5 diabetes dataset

准确率对比

AUC对比

4.2.6 hepatitis dataset

准确率对比

AUC对比

4.2.7 mozilla4 dataset

准确率对比

AUC对比

4.2.8 pc1 dataset

准确率对比

AUC对比

4.2.9 pc5 dataset

准确率对比

AUC对比

4.2.10 waveform-5000 dataset

准确率对比

AUC对比

五、优化

5.1 降维

pc1,pc5,waveform-5000,colic,credit-g这几个数据集的属性维度都在20维及以上,而对分辨样本类别有关键作用的就只有几个属性,所以想到通过降维来摈除干扰属性以使分类模型更好得学习到数据的特征。用主成分分析(PCA)降维之后的数据分类效果不佳,考虑到都是带标签的数据,就尝试使用线性判别分析(LDA)利用数据类别信息来降维。但是LDA最多降维到类别数-1,对于二类数据集效果不好。waveform-5000包含三种类别,于是就尝试用LDA对waveform-5000降维之后再使用各分类模型对其进行学习。

使用sklearn.discriminant_analysis.LinearDiscriminantAnalysis对waveform-5000降维之后的数据样本分布散点图如下,可以明显看到数据被聚为三类,降维之后的数据特征信息更为明显,干扰信息更少,对分类更有利

各分类模型在原数据集和LDA降维数据集上准确率对比如下图

各分类模型在原数据集和LDA降维数据集上AUC值对比如下图

可以看到降维之后的分类效果很理想,无论是acc还是auc,各个分类模型都得到了不同程度的性能提升

5.2 标准化提升bagging with KNN性能

由于KNN是基于样本空间的欧氏距离来计算的,而多属性样本的各属性通常有不同的量纲和量纲单位,这无疑会给计算样本之间的真实距离带来干扰,影响KNN分类效果。为了消除各属性之间的量纲差异,需要进行数据标准化处理,计算属性的z分数来替换原属性值。在具体的程序设计时使用sklearn.preprocessing.StandardScaler来对数据进行标准化。

bagging with KNN在原数据集和标准化数据集上准确率对比如下图

bagging with KNN在原数据集和标准化数据集上AUC对比如下图

可以看到标准化之后效果还是不错的,无论是acc还是auc,基本在各数据集上都得到了性能提升

六、实验总结

此次实验让我对各个分类器的具体用法有了初步的了解,不同的数据集上应用不同分类模型的效果截然不同。数据挖掘任务70%时间消耗在数据清洗上,无论什么分类模型,其在高质量数据上的表现肯定是要优于其在低质量数据上的表现。所以拿到一个数据集后不能贸然往上面套分类模型,而是应该先对数据集进行观察分析,然后利用工具对数据进行清洗、约简、集成以及转换,提升数据质量,让分类更好得学习到数据的特征,从而有更好的分类效果。本次实验我也对各数据集进行了预处理,包括数据缺失值填补、数据类型转换、数据降维、数据标准化等等,这些工作都在各分类模型的分类效果上得到了一定的反馈。实验过程中也遇到了一些问题,比如使用sklearn.metrics.roc_auc_score计算多类别数据的AUC时需要提前将数据标签转换为one hot编码,LDA最多只能将数据维度降维到类别数-1等等,这些都为我以后进行数据挖掘工作积累了宝贵经验。

---------------- The End ----------------

作者: brooksjay
联系邮箱: jaypark@smail.nju.edu.cn
本文地址:
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布
转载请注明出处, 谢谢!

parsing:NLP之chart parser句法分析器

发表于 2019-04-24 | 分类于 Machine Learning , NLP | 0 comments
阅读次数:
  |   字数统计: 1.5k(字)   |   阅读时长: 5(分)

完整代码实现放在我的github上:click me

一、任务要求

  • 实现一个基于简单英语语法的chart句法分析器。

二、技术路线

        采用自底向上的句法分析方法,简单的自底向上句法分析效率不高,常常会重复尝试相同的匹配操作(回溯之前已匹配过)。一种基于图的句法分析技术(Chart Parsing)被提出,它把已经匹配过的结果保存起来,今后需要时可直接使用它们,不必重新匹配。(动态规划)

  • chart parsing的数据表示:
    • p图(chart)的结点表示句子中词之间的位置数字
    • p非活动边集(chart的核心,常直接就被称为chart
      • n记录分析中规约成功所得到的所有词法/句法符号
    • 活动边集
      • 未完全匹配的产生式,用加小圆圈标记(º)的产生式来表示,如:
        • NP -> ART ºADJ N
        • NP -> ART ºN
    • 待处理表(agenda)
      • 实际上是一个队列模型,记录等待加入chart的已匹配成功的词法/句法符号
    • 上面的活动边、非活动边以及词法/句法符号都带有“始/终结点”位置信息
  • chart parsing对“~1~ The ~2~ cat ~3~ caught ~4~ a ~5~ mouse ~6~”进行分析的数据示例:

1541994559204

  • chart parsing的句法分析算法步骤描述如下:

    • 若agenda为空,则把句子中下一个词的各种词法符号(词)和它们的位置加入进来
    • 从agenda中取一个元素(设为C,位置为:p1-p2)
    • 对下面形式的每个规则增加活动边:
      • X->CX~1~…X~n~,增加一条活动边:X->C º X~1~…X~n~,位置为:p1-p2;
      • X->C,把X加入agenda,位置为:p1-p2
    • 将C作为非活动边加入到chart的位置p1-p2
    • 对已有活动边进行边扩展
      • 对每个形式为:X->X~1~… º C…X~n~的活动边,若它在p0-p1之间,则增加一条活动边:X->X~1~… C º…X~n~,位置:p0-p2
      • 对每个形式为: X->X~1~… X~n~ º C的活动边,若它在p0-p1之间,则把X加入agenda ,位置为:p0-p2
  • 程序实现的大致流程:输入英文语句,对在词典dic_ec.txt中不存在的英文单词进行形态还原,对还原后的语句执行chart parsing算法并将分析出的所有非活动边输出。由于一个英文单词可能存在多种词性,这种情况下会对每种可能的词性进行递归,对于不符合句法规则的词性会进行回溯尝试以其它的词性进行句法规则的匹配与分析。直到找到符合句法规则的词性组合则结束递归,尝试完所有的词性组合还是没能找到则句法分析失败,输入的句子不符合当前的句法规则。

三、数据说明

  • 由于这个实验中引用了token实验模块,所以需要用到token实验中的三个数据字典dic_ec.txt,irregualr nouns.txt,irregular verbs.txt,关于这三个数据字典的说明在token实验中已给出,此处不再赘述。除此之外,chart parsing算法还需要用到dic_ec.txt词典中英文单词的词性。

四、遇到的问题及解决方案

  • 程序实现过程中受到文件编码和分隔符的困扰,最后用vim把用到的3个数据词典统一设置成gbk编码,以\t进行分隔,方便程序统一读入处理。
  • dic_ec.txt这个数据字典中的数据质量不太好,很多英文单词都被标注成none.词性,由于无法获取词的正确词性从而无法完成句子的句法分析。

五、性能分析

  • 对句法分析部分作一个性能的度量,单句句法分析的结果基本都在毫秒级别,下面给出基于规则S->NP VP,NP->ART N,NP->ART ADJ N,VP->V,VP->V NP对the cat catch a mouse进行句法分析得到的运行结果及耗时截图:

1542023773939

六、运行环境

  • 将执行文件parsing.exe与数据字典dic_ec.txt,irregular nouns.txt,irregualr verbs.txt放在同一个文件夹下,然后点击parsing.exe即可正常运行程序。

七、用户手册

  • 在运行环境下正常运行程序后会出现下图这样的主菜单文字界面:

1542024516293

  • 根据主菜单进行操作,首先选择1来写入规则,可一次写入多个规则,输入q!结束规则写入,如果后期需要增加规则,可以在主菜单界面再次选择1来写入增添的规则,这样就实现了规则的灵活扩展。下面是写入规则模块的截图:

1542025005658

  • 写入规则结束后又回退到主菜单界面,这时候可以选择2来输入句子进行句法分析,程序会输出分析过程中得到的所有非活动边对应的短语及位置范围,下面是在上面所写入规则的基础上对the cat caught a mouse进行句法分析的结果截图,程序会对dic_ec.txt中不存在的单词尝试调用词形还原模块进行还原再分析:

1542026424475

  • 句法分析回退到主菜单界面,可以继续选择1进行规则扩展,也可以选择2进行句法分析,选择q退出程序运行。
  • 基于下面的句法规则给出一个句法分析示例:
1
2
3
4
5
6
7
NP->ART N
NP->ART
NP->PRON
NP->N
NP->ART ADJ N
VP->V
VP->V NP

对I like her进行句法分析的结果截图如下:

1542031147205

---------------- The End ----------------

作者: brooksjay
联系邮箱: jaypark@smail.nju.edu.cn
本文地址:
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布
转载请注明出处, 谢谢!

token:NLP之词形还原

发表于 2019-04-24 | 分类于 Machine Learning , NLP | 0 comments
阅读次数:
  |   字数统计: 939(字)   |   阅读时长: 3(分)

完整代码实现放在我的github上:click me

一、任务描述

  • 形态还原算法:
    1. 输入一个单词
    2. 如果词典里有该词,输出该词及其属性,转4,否则,转3
    3. 如果有该词的还原规则,并且,词典里有还原后的词,则输出还原后的词及其属性,转4,否则,调用<未登录词模块>
    4. 如果输入中还有单词,转(1),否则,结束。

二、技术路线

  1. 加载dic_ec.txt词典,词典存储着英到汉的映射,对于输入的单词,如果dic_ec.txt词典中包含这个单词的映射则直接输出。下面给出dic_ec.txt内容的基本形式:
1
2
3
4
5
//gbk编码,以\t分隔
homokaryosis none. 同核性, 同核现象
homokaryotic adj. 同核体的
homokurtic none. 等峰态性
homolanthionine none. 高羊毛氨酸
  1. 考虑到有些单词本身就是原形,也是其它单词的形态变换,所以在设计时决定把所有可能的结果都输出。在完成词典映射后再检查该单词是否能通过变换规则转换得到。我们知道英文单词的形态变换存在有规律的和无规律的变换,首先看有规律的变换,动词的规律变换形式有下面4条规则:
1
2
3
4
规则1.  *ves --> *f/*fe
规则2. *ies --> *y
规则3. *es --> *
规则4. *s --> *

        名次的规律变换形式有下面9条规则:

1
2
3
4
5
6
7
8
9
10
11
12
//第三人称单数
规则5. *ies --> *y
规则6. *es --> *
规则7. *s --> *
//现在进行时
规则8. *??ing --> *?
规则9. *ying --> *ie
规则10. *ing --> */*e
//过去时、过去分词
规则11. *??ed --> *?
规则12. *ied --> *y
规则13. *ed --> */*e

        通过在程序中写入这些规则来对单词形态进行还原,而无规则的形态变换只能通过预先建立好的词库来完成词形形态映射。在程序中通过加载irregualr nouns.txt对名词进行还原,加载irregualr verbs.txt对动词进行还原。下面分别给出这两文件中的内容形式:

  irregular nouns.txt的内容形式:

1
2
3
4
5
//gbk编码,每行的第一个词是原形,后面的是变换形态,以\t分隔
grief griefs
roof roofs
gulf gulfs
grief griefs

         irregualr verbs.txt的内容形式:

1
2
3
4
5
//gbk编码,每行的第一个词是原形,后面的是变换形态,以\t分隔
bear bore borne born
alight alighted alit alighted alit
arise arose arisen
awake awoke awaked awoken awoke awaked

        如果找到了还原映射,则在dic_ec.txt词典中查找还原后的单词并输出结果。

  1. 若最终该单词没有检索到结果则把他登记到单词缺失词典missing words.txt中。

三、数据说明

  • 英汉词典dic_ec.txt,名词的不规律变换词典irregualr nouns.txt,动词的不规律变换词典irregualr verbs.txt,这几个数据词典的编码以及内容形式都已在技术路线中给出,此处不再赘述。

四、遇到的问题及解决方案

  • 程序实现过程中唯一遇到的问题就是文件编码和分隔符的问题,最后用vim把用到的3个数据词典统一设置成gbk编码,以\t进行分隔,方便程序统一读入处理。

五、性能分析

  • 下面是性能单词查询的耗时截图,平均不超过0.001s:

1541928466093

六、运行环境

  • 将token.exe与dic_ec.txt,irregualr nouns.txt,irregualr verbs.txt,missing words.txt放在同一个目录下,然后点击token.exe即可正确运行程序。
---------------- The End ----------------

作者: brooksjay
联系邮箱: jaypark@smail.nju.edu.cn
本文地址:
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布
转载请注明出处, 谢谢!

seg:NLP之正向向最大匹配分词

发表于 2019-04-24 | 分类于 Machine Learning , NLP | 0 comments
阅读次数:
  |   字数统计: 546(字)   |   阅读时长: 2(分)

完整代码实现放在我的github上:click me

一、任务要求

  • 实现一个基于词典与规则的汉语自动分词系统。

二、技术路线

  • 采用正向最大匹配(FMM)方法对输入的中文语句进行分词,具体的实现可以分为下面几个步骤:

    1. 对输入的一个中文语句,首先在程序中判断并确保语句中不包含数字或者字母
    2. 在句子中的当前位置开始取与词典dic_ce.txt中最大匹配长度的词作为一个分词段,如果没有在词典中成功匹配到就将句子在当前匹配位置的这个字作为一个分词段并将匹配位置向前挪一个位置
    3. 重复第2步直到匹配位置移到句末
  • 下面是用FMM方法分词的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//param@seg:保存分词段结果的vector
//param@st:带分词的中文语句
void segment(vector<string> &seg, string st) {
int pos = 0;
int sz = st.length();
string t;
int cnt = 0, spos;
while (pos < sz) {
cnt = pos;
spos = pos;
t = "";
while (st[cnt]) {
t += st.substr(cnt, 2);
if (wordmap.find(t) != wordmap.end())
pos = cnt + 2;
cnt += 2;
}
if (pos == spos) {
seg.push_back(st.substr(spos, 2));
pos += 2;
}else {
seg.push_back(st.substr(spos, pos - spos));
}
}
return;
}

三、数据说明

  • 汉英词典dic_ce.txt,读取其中的汉词用于与句中词进行匹配,词典采用GBK编码,下面是给出文件内容示例:
1
2
3
4
5
//gbk编码,每行第一个词是汉词,后面是它对应的英译单词,以','分隔
阿弥陀佛,Amitabha
阿米巴,amoeba,amoebae
阿姆斯特丹,Amsterdam
阿斯匹林,aspirin

四、性能分析

  • 假设输入中文语句长度为n,程序时间复杂度最坏情况下是O(n^2),最好情况是O(n),下面是程序分析结果及分词耗时评测的截图:

1541992901499

五、运行环境

  • 将执行文件seg.exe与数据字典dic_ce.txt放在同一个目录下,然后点击seg.exe即可正常运行,进入运行窗口后根据提示进行输入即可得到分词结果。
---------------- The End ----------------

作者: brooksjay
联系邮箱: jaypark@smail.nju.edu.cn
本文地址:
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布
转载请注明出处, 谢谢!

多机同步管理hexo博客

发表于 2019-04-24 | 分类于 Hexo | 0 comments
阅读次数:
  |   字数统计: 1.4k(字)   |   阅读时长: 6(分)

一、关于搭建的流程

  1. 创建仓库,\< your github username >.github.io;可参考我的tracy-talent.github.io

  2. 创建两个分支:master 与 hexo;

  3. 设置hexo为默认分支(因为我们只需要手动管理这个分支上的Hexo网站文件,git clone会download默认分支);

  4. 使用git clone git@github.com:username/username.github.io.git拷贝仓库;

  5. 在本地username.github.io文件夹下通过Git bash依次执行npm install hexo、hexo init、npm install 和 npm install hexo-deployer-git(此时当前分支应显示为hexo);

  6. 修改_config.yml中的deploy参数,分支应为master;

  7. 下载hexo主题,推荐使用next,先fork一份hexo-theme-next到自己账号上(主要为了方便自己后期对主题的定制),然后进入到themes目录下执行git submodule add git@github.com:username/hexo-theme-next.git将fork的next主题仓库download下来作为当前博客主仓库的子模块,这时username.github.io目录下会多出一个.gitmodules的文件(里面存储着所有子模块的文件路径和url),然后进入到hexo-theme-next主题目录下,使用git branch -a查看当前子模块所出的分支,确保在主分支master上,然后git pull origin master使得子模块与远程保持一致,接着在进入到博客根目录username.github.io下,依次git add、git commit、git push提交到github hexo分支;

  8. 网页端进入到username.github.io仓库下可以看到themes目录下hexo-theme-next存的是指向你fork的hexo-theme-next仓库最新commit id的指针,而不是实实在在的文件内容,点击之后会跳转到子仓库,github这样做也是为了节省存储开销

  9. 后期如果对主题自定义,每次修改完之后确保将主题模块切换到master分支,然后提交修改。再回到博客主目录下提交修改,更新主线(hexo分支)指向子模块的指针,这样才能将第三方库即子模块同步到主线上,使得主线上指向子模块的指针对应的永远是子模块最新的commit id

  10. 这时候如果想在另一台机器上同步部署博客,那么需要先安装npm,然后用npm安装hexo(npm install -g hexo-cli, npm install -g hexo)。接着拉取自己的github pages仓库,执行以下命令

    1
    2
    3
    4
    5
    git clone git@github.com:username/username.github.io.git
    git submodule init && git submodule update

    #下面这一句的效果和上面三条命令的效果是一样的,多加了个参数 `--recursive`
    git clone git@github.com:username/username.github.io.git --recursive

    拉取后创建hexo目录,进入到hexo目录下执行hexo init命令(必须为空目录才能执行成功),初始化站点并安装依赖包。接着将github pages仓库中的所有内容copy到hexo目录下,如果github pages仓库中包含package.json和package-lock.json则可以跳过不执行上一步的hexo init(package.json会指定hexo所依赖插件的版本,依赖插件的版本可能会与hexo版本不兼容而报错,所以最好在仓库中保存依赖包版本配置文件,以便随处可复用),在hexo目录下依次执行npm install和npm install hexo-deployer-git,最后进入到hexo\themes\hexo-theme-next目录下,执行git checkout master切换到子模块的master分支上,执行git pull origin master同步子模块与远程子仓库。至此就实现了在另一台机器上与远程完全同步的工作了,可以在这台机器上写博客发表文章了,记得每次在不同机器作业时先在博客主目录下git pull origin master同步commit之后再开始写博客发表。

  11. 执行hexo g -d生成网站并部署到GitHub上。

这样一来,在GitHub上的username.github.io仓库就有两个分支,一个hexo分支用来存放网站的原始文件,一个master分支用来存放生成的静态网页。完美( •̀ ω •́ )y!

二、关于日常的改动流程

在本地对博客进行修改(添加新博文、修改样式等等)后,通过下面的流程进行管理。

  1. 先git pull origin hexo同步远程仓库
  2. 依次执行git add .、git commit -m “…”、git push origin hexo指令将改动推送到GitHub(此时当前分支应为hexo);
  3. 然后才执行hexo g -d发布网站到master分支上。

三、Issues

  1. 公式渲染失败

    hexo 默认的渲染引擎是 marked,但是 marked 不支持 mathjax,卸载marked并安装kramed

    1
    2
    npm uninstall hexo-renderer-marked --save
    npm install hexo-renderer-kramed --save

    然后,更改node_modules/hexo-renderer-kramed/lib/renderer.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // Change inline math rule
    function formatText(text) {
    // Fit kramed's rule: $$ + \1 + $$
    return text.replace(/`\$(.*?)\$`/g, '$$$$$1$$$$');
    }
    修改为
    // Change inline math rule
    function formatText(text) {
    return text;
    }

    然后停止使用hexo-math(hexo-math依赖hexo-inject一并删除),安装mathjax

    1
    2
    3
    npm uninstall hexo-math --save
    npm uninstall hexo-inject --save
    npm install hexo-renderer-mathjax --save

    更改默认转义规则,更改node_modules/kramed/lib/rules/inline.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 替换11行
    escape: /^\\([\\`*{}\[\]()#$+\-.!_>])/,
    //修改为
    escape: /^\\([`*\[\]()# +\-.!_>])/,

    //替换20行
    em: /^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,
    //修改为
    em: /^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,

    开启mathjax,配置themes/hexo-theme-next/_config.yml

    1
    2
    3
    4
    mathjax:
    enable: true
    perpage: false
    cdn: //cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML

    并在博客中使用mathjax

    1
    2
    3
    4
    5
    6
    ---
    title: Testing Mathjax with Hexo
    category: Uncategorized
    date: 2017/05/03
    mathjax: true
    ---
  2. 博客顶部菜单栏点击后网址末尾多了’20%’导致网页无法找到

    修改themes/hexo-theme-next/_config.yaml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    menu:
    home: / || home
    about: /about/ || user
    tags: /tags/ || tags

    将空格链接与||之间的空格去掉
    menu:
    home: /|| home
    about: /about/|| user
    tags: /tags/|| tags
  3. 站点概览中点击日志标签找不到网页

    在 themes\hexo-theme-next\layout\_macro 找到sidebar.swig 文件找到如下代码

    1
    2
    3
    <a href="{{ url_for(theme.menu.archives).split('||')[0] | trim }}">
    修改为
    <a href="{{ url_for(theme.menu.archives.split('||')[0]) | trim }}">
---------------- The End ----------------

作者: brooksjay
联系邮箱: jaypark@smail.nju.edu.cn
本文地址:
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布
转载请注明出处, 谢谢!

apriori && fpgrowth:频繁模式与关联规则挖掘

发表于 2019-04-24 | 分类于 Data Mining | 0 comments
阅读次数:
  |   字数统计: 4.4k(字)   |   阅读时长: 20(分)

详细代码实现放在我的github上:click me

一、实验说明

1.1 任务描述

1.2 数据集说明

  • GroceryStore数据集

    This data set contains transaction records of a grocery store in a month. Each line is a transaction, where the purchased items line in {}, separated by “,” (the space is replaced by “/”). In total, there are 9835 transactions of 169 items.

  • UNIX_usage数据集

    The data set contains 9 sets of sanitized user data drawn from the
    command histories of 8 UNIX computer users at Purdue over the course
    of up to 2 years (USER0 and USER1 were generated by the same person,
    working on different platforms and different projects). The data is
    drawn from tcsh(1) history files and has been parsed and sanitized to
    remove filenames, user names, directory structures, web addresses,
    host names, and other possibly identifying items. Command names,
    flags, and shell metacharacters have been preserved. Additionally,
    SOF and EOF tokens have been inserted at the start and end of
    shell sessions, respectively. Sessions are concatenated by date order
    and tokens appear in the order issued within the shell session, but no
    timestamps are included in this data.

    For example, the two sessions:

    # Start session 1
    cd ~/private/docs
    ls -laF | more
    cat foo.txt bar.txt zorch.txt > somewhere
    exit
    # End session 1
    
    # Start session 2
    cd ~/games/
    xquake &
    fg
    vi scores.txt
    mailx john_doe@somewhere.com
    exit
    # End session 2
    

    would be represented by the token stream
    ​
    ​ SOF
    ​ cd
    ​ <1> # one “file name” argument
    ​ ls
    ​ -laF
    ​ |
    ​ more
    ​ cat
    ​ <3> # three “file” arguments
    ​ >
    ​ <1>
    ​ exit
    ​ EOF
    ​ SOF
    ​ cd
    ​ <1>
    ​ xquake
    ​ &
    ​ fg
    ​ vi
    ​ <1>
    ​ mailx
    ​ <1>
    ​ exit
    ​ EOF

二、代码设计与实现

对apriori算法手动实现了一个dummy版本和一个advanced版本,dummy版本没有使用剪枝的trick,使用暴力的方法生成候选项级,対事务表不做任何处理。而advanced版本则在dummy版本的基础上加入了一剪枝的trick,性能更胜一筹。对fpgrowth算法使用了已有的python包,其中的实现细节没有深入去review了。下面对这几份代码的详细设计与实现做一个说明。

2.1 dummy apriori

从命令行接收算法的控制参数

1
2
3
4
5
6
if len(sys.argv) != 4:
print('usage: python apriori_dummy.py dataset-path support-ratio K')
else:
dataset_path_name = sys.argv[1] # 要处理的数据集GroceryStore or UNIX_usage
min_sup = float(sys.argv[2]) # 支持率
itemset_size = int(sys.argv[3]) # 要挖掘的最大频繁项集大小

读取数据集之后每一事务以set形式存储在一个列表data中,然后匹配、统计生成频繁一项集:

1
2
3
4
5
6
7
8
9
10
11
oneitem_freq = {}
for itemset in data:
for item in itemset:
oneitem_freq[item] = 0
for itemset in data:
for item in itemset:
oneitem_freq[item] += 1
oneitem_freqset = []
for oneitem in oneitem_freq.keys():
if oneitem_freq[oneitem] >= frequency_threshold:
oneitem_freqset.append([oneitem])

接下来就进入循环过程:由k项频繁项集生成k+1项候选项集,然后匹配事务表统计频次,筛选出高于支持率对应频次的作为k+1项频繁项集,以此往复循环直到生成的频繁项集为空或者达到要挖掘的频繁项集大小的上限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pre_freqset = oneitem_freqset
for k in range(2, itemset_size + 1):
start_time = time.time()
candidates_k = generate_candidates(pre_freqset)
k_item_freq = count_freqset(data, candidates_k)
pre_freqset = []
new_k_item_freq = {}
for key in k_item_freq.keys():
if k_item_freq[key] >= frequency_threshold:
pre_freqset.append(sorted(key.split(',')))
new_k_item_freq[','.join(pre_freqset[-1])] = k_item_freq[key]
k_item_freq = new_k_item_freq
if len(pre_freqset) == 0:
break
pre_freqset.sort()
output(pre_freqset, k_item_freq, frequency_threshold, k, dataset_path_name)

这段主控制程序中调用了2个甚为关键的函数:generate_candidates用于生成候选项集,count_freqset用于匹配事务表进行统计。

generate_candidates: 使用纯暴力拼接的方式,由上一轮的k频繁项集构成的单项元素集合中每一个元素与任一k频繁项连接(前提是该单项元素不出现在这一频繁项中)构成一个k+1候选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def generate_candidates(pre_freqset):
"""
由k-1的频繁项集生成k候选项集
:param pre_freqset: 前一次生成的k-1频繁项集
:param k: 要生成的k频繁项集对应的k
:return: 包含k候选项集的列表
"""
items = []
for itemset in pre_freqset:
for item in itemset:
items.append(item)
items = list(set(items))
candidates = []
for itemset in pre_freqset:
for item in items:
if item not in itemset:
itemset_backup = copy.deepcopy(itemset)
itemset_backup.append(item)
candidates.append(','.join(sorted(itemset_backup)))
candidates_unique = []
candidates = set(candidates)
for can in candidates:
candidates_unique.append(can.split(','))
return candidates_unique

count_freqset: 对由generate_candidates生成的每一候选项在事务表中扫描匹配,统计出现频次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def count_freqset(data, candidates_k):
"""
对照事务表统计候选项集频次
:param data: 事务集
:param candidates_k: k候选项集
:return: 候选项映射到频次的dict
"""
counter = {}
for can in candidates_k:
canset = set(can)
canstr = ','.join(can)
counter[canstr] = 0
for itemset in data:
if canset <= itemset:
counter[canstr] += 1
return counter

2.2 advanced apriori

advanced apriori在dummy apriori的基础上加入了一些剪枝的技巧,包括减小生成候选项集的规模、不断减少事务表规模、减少事务表中元组中的项。

  • 减小生成候选项集规模

    如果一个k+1候选项是频繁的,那么生成它的k频繁项集中必包含其k+1个子集。例如,如果{t1, t2, t3, t5}是频繁的,那么它将由{t1, t2, t3}和{t1, t2,t5 }生成,否则它就不会被生成。这么做要求生成候选项集的频繁项集的项内是有序的,各项之间也是有序的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    def generate_candidates(pre_freqset, k):
    """
    由k-1的频繁项集生成k候选项集
    :param pre_freqset: 前一次生成的k-1频繁项集(项内与项之间都为有序状态)
    :param k: 要生成的k频繁项集对应的k
    :return: 包含k候选项集的列表
    """
    candidates = []
    if k == 2:
    for i in range(len(pre_freqset)):
    for j in range(i + 1, len(pre_freqset)):
    candidates.append([pre_freqset[i][0], pre_freqset[j][0]])
    else:
    i = 0
    while i < len(pre_freqset) - 1:
    tails = []
    while i < len(pre_freqset) - 1 and pre_freqset[i][:-1] == pre_freqset[i + 1][:-1]:
    tails.append(pre_freqset[i][-1])
    i += 1
    if tails:
    tails.append(pre_freqset[i][-1])
    prefix = copy.deepcopy(pre_freqset[i][0:-1])
    for a in range(len(tails)):
    for b in range(a + 1, len(tails)):
    items = copy.deepcopy(prefix)
    items.append(tails[a])
    items.append(tails[b])
    candidates.append(items)
    i += 1

    return candidates
  • 减小事务表规模

    如果一个k+1项候选项能与事务表中一条记录匹配,那么其k+1个子集也必能与事务表中该条记录匹配。因此在事务表中匹配k候选项集的时候,统计每条记录被匹配到的次数,如果少于k+1次,那么将该条记录从事务表中移除,因为它绝不可能与下一轮的任一k+1项候选项匹配。这样每次迭代都减小了事务表的规模,从而减小扫描事务表的时间消耗

  • 减少事务表中元组的项

    如果事务表中元组的某一项能包含在一个k+1频繁项中,那么该项比出现在这个k+1频繁项的k个k项子集中。所以在扫描事务表统计k项候选集的出现频次时,如果事务表中任一元组的某一项未被匹配中k次,那么该项将在筛选出k频繁项集后从元组中除去,从而减少统计k+1项候选集频次时与事务表中元组的匹配次数。具体实现在减少事务表规模实现的基础上稍作修改即可,下面是结合减少事务表规模以及表中元组项数的实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    def count_freqset(data, candidates_k):
    """
    对照事务表统计候选项集频次
    :param data: 事务集
    :param candidates_k: k候选项集
    :return: 候选项映射到频次的dict
    """
    prune_basis = []
    for itemset in data:
    prune_basis.append([0] * (len(itemset) + 1))
    counter = {}
    for can in candidates_k:
    canset = set(can)
    canstr = ','.join(can)
    counter[canstr] = 0
    for i in range(len(data)):
    if canset <= data[i]:
    counter[canstr] += 1
    prune_basis[i][-1] += 1
    j = 0
    for item in data[i]:
    if item in canset:
    prune_basis[i][j] += 1
    j += 1
    return counter, prune_basis

    def prune(data, prune_basis, k):
    """
    由k-1的频繁项集生成k候选项集
    :param data: 存储事务的列表
    :param prune_basis: 由k候选项集匹配事务表时生成的剪枝依据
    :param k: 为生成k+1频繁项集剪枝
    :return: None
    """
    # 删除列表中某一项之后列表元素会自动往前补位,即被删除元素后的元素对应索引-1
    if len(prune_basis) != 0:
    h = 0
    for i in range(len(prune_basis)):
    if prune_basis[i][-1] < k + 1:
    del data[h]
    else:
    j = 0
    del_items = []
    for item in data[h]:
    if prune_basis[i][j] < k:
    del_items.append(item)
    j += 1
    for item in del_items:
    data[h].remove(item)
    h += 1

2.3 基于apriori的频繁项集挖掘关联规则

基于每论迭代得到的k-频繁项集,对其中每一k-频繁项利用二进制法构造其所有势大于0的子集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def get_all_subsets(itemset):
"""
计算itemset的所有势大于0的子集
:param itemset: 包含一个项集的列表
:return: 项集itemset的所有势大于0的子集(type: list(set...))
"""
n = len(itemset)
all_subsets = []
for i in range(1, 2**(n - 1)):
subset = set()
for j in range(n):
if (i >> j) % 2 == 1:
subset.add(itemset[j])
all_subsets.append(subset)
return all_subsets

任一子集与该子集和生成它的父集k-频繁项之间的差集构造k项的关联规则,筛选出大于置信度且提升度大于1.0的关联规则作为要挖掘的强关联规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def generate_association_rules(k_freqset, itemset_freq, min_con, dataset_size):
"""
生成关联规则
:param k_freqset: k频繁项集
:param itemset_freq: 项集与频次之间的字典映射
:param min_con: 置信度
:param dataset_size: 数据集记录总数
:return: 关联规则列表(list(list(previous item, post item, frequency)...))
"""
association_rules = []
for i in range(len(k_freqset)):
all_subsets = get_all_subsets(k_freqset[i])
par_set = set(k_freqset[i])
par_cnt = itemset_freq[','.join(k_freqset[i])]
for subset in all_subsets:
subset_str = ','.join(sorted(list(subset)))
diffset_str = ','.join(sorted(list(par_set.difference(subset))))
subset_cnt = itemset_freq[subset_str]
diffset_cnt = itemset_freq[diffset_str]
subset_prob = 1.0 * subset_cnt / dataset_size
diffset_prob = 1.0 * diffset_cnt / dataset_size
if 1.0 * par_cnt / subset_cnt >= min_con and 1.0 * par_cnt / (subset_cnt * diffset_prob) >= 1.0:
association_rules.append([subset_str, diffset_str, 1.0 * par_cnt / subset_cnt])
elif 1.0 * par_cnt / diffset_cnt >= min_con and 1.0 * par_cnt / (diffset_cnt * subset_prob) >= 1.0:
association_rules.append([diffset_str, subset_str, 1.0 * par_cnt / diffset_cnt])

return association_rules

2.4 fpgrowth

这里没有多少要讲的,直接调用接口

1
2
3
4
5
6
7
import pyfpgrowth

data = []
read(data, dataset_path_name)

freq_patterns = pyfpgrowth.find_frequent_patterns(data, frequency_threshold)
association_rules = pyfpgrowth.generate_association_rules(freq_patterns, min_con)

三、实验结果及分析对比

3.1 实验结果展示

  • apriori算法在UNIX_usage数据集上挖掘的频繁5-项集的一部分(支持度0.01)
1
2
3
4
5
6
7
8
9
10
11
12
13
&,-l,<1>,<2>,cd : 131
&,<1>,<2>,>,cd : 142
&,<1>,<2>,>,rm : 111
&,<1>,<2>,cd,cp : 139
&,<1>,<2>,cd,ll : 193
&,<1>,<2>,cd,ls : 198
&,<1>,<2>,cd,mv : 155
&,<1>,<2>,cd,rm : 217
&,<1>,<2>,cd,vi : 209
&,<1>,<2>,ll,rm : 127
&,<1>,<2>,ll,vi : 132
&,<1>,<2>,ls,rm : 127
&,<1>,<2>,mv,rm : 114
  • apriori算法在GroceryStore数据集上挖掘的频繁3-项集的一部分(支持度0.01)
1
2
3
4
5
6
7
8
9
10
bottled water,other vegetables,whole milk : 106
butter,other vegetables,whole milk : 113
citrus fruit,other vegetables,root vegetables : 102
citrus fruit,other vegetables,whole milk : 128
citrus fruit,whole milk,yogurt : 101
curd,whole milk,yogurt : 99
domestic eggs,other vegetables,whole milk : 121
fruit/vegetable juice,other vegetables,whole milk : 103
other vegetables,pastry,whole milk : 104
other vegetables,pip fruit,whole milk : 133
  • apriori算法在GroceryStore数据集上挖掘的一些关联规则(支持度0.01,置信度:0.5)
1
2
3
4
5
6
7
butter,other vegetables -> whole milk	confidence:0.5736040609137056
citrus fruit,root vegetables -> other vegetables confidence:0.5862068965517241
curd,yogurt -> whole milk confidence:0.5823529411764706
domestic eggs,other vegetables -> whole milk confidence:0.5525114155251142
other vegetables,pip fruit -> whole milk confidence:0.5175097276264592
rolls/buns,root vegetables -> other vegetables confidence:0.502092050209205
root vegetables,tropical fruit -> other vegetables confidence:0.5845410628019324
  • fpgrowth算法在GroceryStore数据集上挖掘的一些关联规则(置信度0.5)
1
2
3
4
5
curd,yogurt -> whole milk	confidence: 0.5823529411764706
butter,other vegetables -> whole milk confidence: 0.5736040609137056
domestic eggs,other vegetables -> whole milk confidence: 0.5525114155251142
other vegetables,whipped/sour cream -> whole milk confidence: 0.5070422535211268
other vegetables,pip fruit -> whole milk confidence: 0.5175097276264592
  • apriori算法在UNIX_usage数据集上挖掘的一些关联规则a -> b(支持度0.01, 置信度:0.8)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    & -> <1>	confidence:0.8453738910012675
    netscape -> & confidence:0.8895705521472392
    &,-l -> <1> confidence:0.9875776397515528
    &,-l -> <2> confidence:0.8509316770186336
    &,-l -> <1>,<2> confidence:0.84472049689441
    &,-l,<2> -> <1> confidence:0.9927007299270073
    &,>,rm -> <1>,<2> confidence:0.9024390243902439
    &,<2>,cd,cp -> <1> confidence:1.0
    -,<1>,k,ll -> <2>,cd confidence:0.959349593495935
    -,k,ll -> <1>,<2>,cd confidence:0.959349593495935
    <1>,<2>,cp,ls,mv,rm -> cd confidence:0.9888888888888889
    <2>,cp,ls,mv,rm -> <1>,cd confidence:0.9888888888888889
    <n2>,cd,cp,ls,mv,rm,vi -> <1> confidence:1.0
    <1>,cd,cp,ls,mv,rm,vi -> <2> confidence:1.0

从上面挖掘出的关联规则可以发现,GroceryStore数据集的相关度很低,将置信度调到0.5的才挖掘到少量的关联规则,但是置信度不高并不一定代表它就没什么意义,上面列出的几条规则人眼观察还是很合理的,从如此多的数据记录中只挖掘出少量的几条关联规则使其更直观更具有参考价值,挖出太多关联规则反而更容易让人眼花缭乱,从而失去判断。反观在UNIX_usage上挖掘出的关联规则,置信度高而且数量又多,但很多都看起来让人难以理解,意义不大,但还是可以大致看出来哪些命令经常一起共用。fpgrowth算法在UNIX_usage数据集上运行时递归深度太深而没能跑出结果来。

3.2 时间性能对比

3.2.1 频繁项集挖掘

3.2.1.1 UNIX_usage Dataset

对比dummy apriori,仅剪枝候选项集规模的advanced_1 apriori,剪枝候选项集规模和事务表规模的advanced_2 apriori,剪枝候选项集规模和事务表规模以及表中元组项数的advanced_3 apriori在相同数据集、实验环境和实验参数下的时间损耗(单位:秒/s)

实验参数与数据集:

支持度:0.01

最大频繁项大小:10

数据集:UNIX_usage

下面表格中以da表示dummy apriori,a1a表示advance_1 apriori,a2a表示advanced_2 apriori,a3a表示advanced_3 apriori

表 1 apriori在UNIX_usage数据集上的频繁项集挖掘时耗(秒/s)
freq-1 freq-2 freq-3 freq-4 freq-5 freq-6 freq-7 freq-8
da 0.021 2.156 17.066 33.359 35.156 17.190 2.522 0.183
a1a 0.016 2.960 5.234 5.808 2.812 0.675 0.128 0.007
a2a 0.019 2.962 3.208 2.758 1.200 0.246 0.027 0.001
a3a 0.017 3.311 3.997 3.658 1.663 0.413 0.051 0.002
3.2.1.2 GroceryStore Dataset

实验参数与数据集:

支持度:0.01

最大频繁项大小:10

数据集:GroceryStore

表 2 apriori在GroceryStore数据集上的频繁项集挖掘时耗(秒/s)
freq-1 freq-2 freq-3
da 0.019 1.255 2.547
a1a 0.014 1.978 0.402
a2a 0.012 1.982 0.263
a3a 0.014 2.254 0.322

3.2.2 关联规则挖掘

由于关联规则挖掘依赖频繁项集挖掘,选择上述时间性能最好的apriori算法来实现关联呢规则挖掘

3.2.2.1 UNIX_usage Dataset

实验参数与数据集:

支持度:0.01

最大频繁项大小:10

置信度:0.8

数据集:UNIX_usage

表 3 UNIX_usage数据集上的关联规则挖掘时耗(秒/s)
2-items 3-items 4-items 5-items 6-items 7-items 8-items
apiori 2.840 3.092 2.619 1.779 0.268 0.035 0.003
3.2.2.2 GroceryStore Dataset

实验参数与数据集:

支持度:0.01

最大频繁项大小:10

置信度:0.5

数据集:GroceryStore

表 4 GroceryStore数据集上的关联规则挖掘时耗(秒/s)
3-items
apriori 0.286

3.2.3 简要分析

从两个数据集的表现来看,advanced_2 apriori性能是最好的,advanced_1 apriori性能与advanced_2 apriori性能相近,在较小的数据集GroceryStore上差距在毫秒级别,在较大数据集UNIX_usage上差距在几秒以内。advanced_3 apriori理想情况下应该是性能最好的,因其使用了最多的剪枝技巧,但是实际情况却并非如此,用在删减事务表中元组项的时间比其能优化的时间还要多,得不偿失,如果能删除较多的元组项就能带来不错的性能提升,所以这一剪枝的trick能否有效还是要视数据集而定,不同的数据集上优化效果也不一样。而advanced apriori的三个算法相比dummy apriori在时间性能上提升了好几倍。由此可知,时间性能的主要提升点在减小候选项集规模,减小事务表规模的时间性能提升效果不明显,但是当数据规模较大时也能带来秒级的性能提升。在aporiori挖掘出的频繁项集基础上挖掘强关联规则的时间消耗基本与只作频繁项集挖掘的时间消一致,这是因为k-频繁项的k还比较小,生成的子集不多,相对于候选集生成与数据记录扫描匹配的时间复杂度可忽略不计,这才使得两者时间消耗基本一致。当频繁项足够长时,子集的数目是呈指数增长的,那是复杂度将变得让人难以接受,因此使用apriori挖掘关联规则时最好限制要挖掘的最大频繁项的大小。

3.3 内存占用对比

3.3.1 UNIX_usage Dataset

实验参数与数据集:

最小支持度:0.01

最大频繁项大小:10

数据集:UNIX_usage

  • dummy apriori频繁项集挖掘

  • advanced_1 apriori频繁项集挖掘

  • advanced_2 apriori频繁项集挖掘

  • advanced_3 apriori频繁项集挖掘

  • advanced_2 apriori关联规则挖掘

3.3.2 GroceryStore Dataset

实验参数与数据集:

支持度:0.01

最大频繁项大小:10

置信度:0.5

数据集:GroceryStore

  • dummy apriori频繁项集挖掘

  • advanced_1 apriori频繁项集挖掘

  • advanced_2 apriori频繁项集挖掘

  • advanced_3 apriori频繁项集挖掘

  • advanced_2 apriori关联规则挖掘

  • fpgrowth关联规则挖掘

3.3.3 简要分析对比

advanced_1 apriori和advanced_2 apriori的内存消耗基本一致,在程序运行期间比较平稳,而dummy apriori相比advanced apriori占用内存较大,峰值达到了advanced apriori的2倍以上,主要消耗在暴力穷举候选项集,导致候选项集规模太大,占用内存过多。

---------------- The End ----------------

作者: brooksjay
联系邮箱: jaypark@smail.nju.edu.cn
本文地址:
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布
转载请注明出处, 谢谢!

降维与度量学习之LDA && NCA

发表于 2019-04-19 | 分类于 Data Mining | 0 comments
阅读次数:
  |   字数统计: 1.4k(字)   |   阅读时长: 5(分)

代码实现放在我的github上:click me

一、Linear Discriminant Analysis(LDA)

1.1 Rationale

        线性判别分析(LDA)是一种监督学习的分类和降维的方法,但更多是被用来降维。LDA的原理是让投影后同一类中数据的投影点之间尽可能地靠近,而类不同类别中数据的类别中心之间的距离尽可能远,用一句话概括就是“投影后类内方差最小,类间方差最大”。

        假设我们的数据集D={(x1,y1),(x2,y2),…,((xm,ym))},其中任意样本xixi为n维向量,yi{C1,C2,…,Ck}。我们定义Nj(j=1,2…k)为第j类样本的个数,Xj(j=1,2…k)为第j类样本的集合,而μj(j=1,2…k)为第j类样本的均值向量,定义Σj(j=1,2…k)为第j类样本的协方差矩阵。由于我们是多类向低维投影,则此时投影到的低维空间就不是一条直线,而是一个超平面了。假设我们投影到的低维空间的维度为d,对应的基向量为(w1,w2,…wd),基向量组成的矩阵为W, 它是一个n×d的矩阵。此时优化的目标变成

其中,为所有样本均值向量。。但是有一个问题,就是和都是矩阵,不是标量,无法作为一个标量函数来优化。可以用如下的替代优化目标

其中为A的主对角线元素的乘积。利用广义瑞利商可知,最大值是的最大特征值,而最大的d个值的乘积就是最大的d个特征值的乘积,而W即为最大的d个特征值对应的d个特征向量张成的矩阵,这样就得到用于降维的转换矩阵W。这里有一点需要注意的是W降维的大小不能超过k-1即数据类别数-1。因为矩阵的秩小于等于各个相加得到它的矩阵的秩的和,而累加得到的的秩为1,所以的秩不超过k,又因为第k个可由前k-1个线性表示,因此秩最大为k-1,则不为0的特征值最多有k-1个。

1.2 LDA vs PCA

  • Commonalities:
    • 两者均可以对数据进行降维。
    • 两者在降维时均使用了矩阵特征分解的思想。
    • 两者都假设数据符合高斯分布。
  • Differences:
    • LDA是有监督的降维方法,而PCA是无监督的降维方法
    • LDA降维最多降到类别数==k-1==的维数,而PCA没有这个限制。
    • LDA除了可以用于降维,还可以用于分类。
    • LDA选择分类性能最好的投影方向,而PCA选择样本点投影具有最大方差的方向。

二、Neighborhood Component Analysis(NCA)

2.1 Rationale

        近邻成分分析(NCA)是由Jacob Goldberger和Geoff Hinton等大佬们在2005年发表于NIPS上的一项工作1,属于度量学习(Metric Learning)和降维(Dimension Reduction)领域。NCA的原理是以马氏距离为距离度量的KNN为基础,通过不断优化KNN分类的准确率来学习转换矩阵,最终得到对原数据进行降维的转换矩阵。

1. http://www.cs.toronto.edu/~fritz/absps/nca.pdf ↩

        接下来对NCA学习转换矩阵的数学推导,设表示原数据的列向量表示,A是d*D的转换矩阵,其中D为原数据的维度,而d为降维后的维度,定义为映射空间中欧氏距离(相当于原空间中的马氏距离)的softmax概率值

设为i能够被正确分类的概率,表示与i属于同一类的样本的集合,那么

优化目标就是最大化能被正确分类的点的数目

f(A)对A求偏导,定义

有了目标函数对A梯度之后就可以设定迭代次数和A的初始值,利用梯度下降法不断优化目标函数上限(当然也可以使用其它的优化方法比如拟牛顿法),设学习率为,通过下面公式迭代

2.2 NCA vs PCA

  • commonalities:
    • 都可以用来降维
  • differences:
    • NCA除了降维还是一种度量学习的方法
    • NCA对数据分布没有假设,而PCA要求数据服从高斯分布
    • NCA基于KNN选择分类性能最好的投影方向,而PCA选择样本点投影具有最大方差的方向
---------------- The End ----------------

作者: brooksjay
联系邮箱: jaypark@smail.nju.edu.cn
本文地址:
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布
转载请注明出处, 谢谢!

hadoop tips

发表于 2019-04-19 | 分类于 Distributed System , Bigdata Technology | 0 comments
阅读次数:
  |   字数统计: 720(字)   |   阅读时长: 3(分)

hadoop tips

  • yarn查看application log的方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
1.在${HADOOP_HOME}/etc/hadoop/yarn-site.xml中配置:
<property>
<name>yarn.log-aggregation-enable</name>
<value>true</value>
</property>
<property >
<name>yarn.log-aggregation.retain-seconds</name>
<value>604800</value>
</property>
<property>
<name>yarn.nodemanager.remote-app-log-dir</name>
<value>/app-logs</value>
</property>
<property>
<name>yarn.nodemanager.remote-app-log-dir-suffix</name>
<value>logs</value>
</property>
<!-- 是否将对容器强制实施物理内存限制 -->
<property>
<name>yarn.nodemanager.pmem-check-enabled</name>
<value>false</value>
</property>
<!-- 是否将对容器强制实施虚拟内存限制 -->
<property>
<name>yarn.nodemanager.vmem-check-enabled</name>
<value>false</value>
</property>

2.在hdfs上创建/app-logs
hadoop fs -mkdir /app-logs

3.在/app-logs下查看运行过的application的applicationId,然后使用如下命令查看日志:
yarn logs -applicationId application_12**345
  • hadoop关于jobhistory server的配置,以便在作业web界面查看日志信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1.mapred-site.xml的配置
<property>
<name>mapreduce.jobhistory.address</name>
<value>localhost:10020</value>
</property>
<property>
<name>mapreduce.jobhistory.webapp.address</name>
<value>localhost:19888</value>
</property>
2.yarn-site.xml的配置
</property>
<property>
<name>yarn.log-aggregation-enable</name>
<value>true</value>
</property>
<property>
<name>yarn.log.server.url</name>
<value>http://localhost:19888/jobhistory/logs/</value>
  • hadoop启动方式:
1
2
3
4
5
6
7
8
9
10
11
12
1.启动hdfs
./sbin/start-dfs.sh
或者分开启动
./sbin/hadoop-daemon.sh start namenode
./sbin/hadoop-daemon.sh start datanode
2.启动yarn
./sbin/start-yarn.sh
或者分开启动
./sbin/yarn-daemon.sh start resourcemanager
./sbin/yarn-daemon.sh start nodemanager
3.启动jobhistory server
./sbin/mr-jobhistory-daemon.sh start historyserver
  • hadoop jar提交作业时提示Exception in thread “main” java.lang.SecurityException: Invalid signature file digest for Manifest main attributes,跟jar包签名有关,可以在.pom下配置plugin:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>

如果还无法解决问题,就在每次生成jar包后,命令行下输入以下命令删除签名文件:

1
zip -d wordcount.jar 'META-INF/.SF' 'META-INF/.RSA' 'META-INF/*SF'
  • hdfs的目录默认是放在/user/linux用户名下的
  • 关闭namenode的safemode:hdfs dfsadmin -safemode leave
---------------- The End ----------------

作者: brooksjay
联系邮箱: jaypark@smail.nju.edu.cn
本文地址:
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布
转载请注明出处, 谢谢!

<i class="fa fa-angle-left"></i>1234<i class="fa fa-angle-right"></i>
brooksj

brooksj

我听说虫洞可以穿梭时空

31 日志
15 分类
40 标签
GitHub E-Mail
Links
  • 博客旧址
  • 苏剑林's blog
  • Karpathy's Blog
  • Ruder's Blog
  • Edward Ma's Blog
  • Miskcoo's Blog
© 2023 brooksj
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4