代码链接:人工智能实验
文章目录
- 人工智能实验三:分类算法实验
- 一、实验目的
- 二、实验的硬件、软件平台
- 三、实验内容及步骤
- 四、思考题:
- 五、实验报告要求
- 六、实验步骤
- 数据集读取
- 朴素贝叶斯
- 基础理论
- 拉普拉斯平滑
- 实验代码
- 训练
- 预测
- 结果
- 决策树
- 基础理论
- ID3
- C4.5
- CART
- 实验代码
- ID3
- C4.5
- CART
- 划分依据
- 建树
- 预测
- 结果
- 支持向量机
- 理论基础
- 代码实现
- 结果
- 神经网络
- 理论基础
- 代码实现
- 结果
- 实验结果
- 思考题
人工智能实验三:分类算法实验
一、实验目的
- 巩固4种基本的分类算法的算法思想:朴素贝叶斯算法,决策树算法,人工神经网络,支持向量机;
- 能够使用现有的分类器算法代码进行分类操作
- 学习如何调节算法的参数以提高分类性能;
二、实验的硬件、软件平台
硬件:计算机
软件:操作系统:WINDOWS/Linux
应用软件:C, Java或者Matlab或python
三、实验内容及步骤
利用现有的分类器算法对文本数据集进行分类
实验步骤:
- 了解文本数据集的情况并阅读算法代码说明文档;
- 编写代码设计朴素贝叶斯算法,决策树算法,人工神经网络,支持向量机等分类算法,利用文本数据集中的训练数据对算法进行参数学习;
- 利用学习的分类器对测试数据集进行测试;
- 统计测试结果;
四、思考题:
- 如何在参数学习或者其他方面提高算法的分类性能?
五、实验报告要求
- 对各种算法的原理进行说明;
- 对实验过程进行描述;
- 附上完整的实验代码,代码的详细说明,统计实验结果,对分类性能进行比较说明;
- 对算法的时间空间复杂度进行比较分析。
六、实验步骤
数据集读取
总共给出了三个数据集:data集,predict集,test集,从名字可以看出,data集用于训练,test集用于测试,predict集用于验证算法分类的性能。
所以通过pandas库对数据集进行读取:
data = pd.read_csv("C:/Users/a2783/Desktop/AI/Exp/Exp3/dataset.txt")
pred = pd.read_csv("C:/Users/a2783/Desktop/AI/Exp/Exp3/predict.txt")
test = pd.read_csv("C:/Users/a2783/Desktop/AI/Exp/Exp3/test.txt")
由于数据以文本形式存储,需要转换成对应的数值类型。这里采用的方法是,对于每一个特征,遍历其所有的类别,将每一个类别赋予一个值。每个特征值得范围从1开始,最大即特征对应的类别得种类数:
for i in data.columns[:-1]:
cnt = 1
feature = data[i]
feature = np.array(feature)
for j in np.unique(feature):
data.loc[data[i] == j, i] = cnt
pred.loc[pred[i] == j, i] = cnt
test.loc[test[i] == j, i] = cnt
cnt += 1
由于之后做分类时,要求标签从0开始,所以对于Class_Values
的值从0开始:
cnt = 0
for j in np.unique(data[data.columns[-1]]):
data.loc[data[data.columns[-1]] == j, data.columns[-1]] = cnt
pred.loc[pred[data.columns[-1]] == j, pred.columns[-1]] = cnt
test.loc[test[data.columns[-1]] == j, test.columns[-1]] = cnt
cnt += 1
至此,数据处理完毕。
朴素贝叶斯
基础理论
贝叶斯公式:
其中,为归一化因子,可以忽略,是先验概率,是后验概率,即似然。
但是根据贝叶斯公式,若要估计后验概率,由于类条件概率为数据的所有属性上的联合概率,所有在样本集中较难直接估计得等到。所以朴素贝叶斯算法采用了属性条件独立性假设,假设所有属性相互独立,因此可得公式:
所以,对于分类任务,可以由公式:
来选择最大的值作为当前样本的分类。
对于朴素贝叶斯算法,可以在数据集中直接估计得到类先验概率以及条件概率
如果属性是连续属性,那么可以根据概率密度函数来计算得到条件概率,假设其服从高斯分布,其和是第类样本在第个属性上的均值和方差。
拉普拉斯平滑
考虑到样本集中的样本并不是完全的,可能存在某个类的某个属性没有出现,所以采用拉普拉斯平滑,避免未出现的属性携带的信息被抹去。主要就是在求条件概率和先验概率时,将分子加上,相应的分母加上。本次实验中采用的是最简单的采用
实验代码
训练
计算先验概率:
for i in np.unique(np.array(y)):
self.prior[i] = (y.count(i) + self.L) / (len(y) + len(np.unique(np.array(y))))
计算条件概率:
for c in np.unique(np.array(y)):
D_c = Data.loc[Data[Data.columns[-1]] == c]
for x in feature:
for i in np.unique(np.array(Data[x])):
D_x = D_c.loc[D_c[x] == i]
before = str(x) + "," + str(i)
after = str(c)
key = before + "|" + after
self.P[key] = (len(D_x) + self.L) / (len(D_c) + len(np.unique(np.array(Data[x]))))
这里存储的方式都采用字典(Dict)的方式,对于条件概率,key使用字符串形式表示,将其各个特征的值转换为字符串并拼接来作为key。
整体代码:
def fit(self, Data: pd.DataFrame):
y = Data.iloc[:, -1]
y = list(y)
feature = Data.columns[:-1]
# priority
for i in np.unique(np.array(y)):
# 拉普拉斯平滑
self.prior[i] = (y.count(i) + self.L) / (len(y) + len(np.unique(np.array(y))))
# given
for c in np.unique(np.array(y)):
D_c = Data.loc[Data[Data.columns[-1]] == c]
for x in feature:
for i in np.unique(np.array(Data[x])):
D_x = D_c.loc[D_c[x] == i]
# 将属性转换为字符串形式 作为key
before = str(x) + "," + str(i)
after = str(c)
key = before + "|" + after
# 拉普拉斯平滑
self.P[key] = (len(D_x) + self.L) / (len(D_c) +
len(np.unique(np.array(Data[x]))))
传入的Data即训练集
预测
具体的方式就是取出每一个待预测的样本,根据朴素贝叶斯的公式,取出每个属性的值进行连乘,最后选取概率最大的作为当前的预测标签,最终返回
def pred(self, Data: pd.DataFrame) -> Tuple[List[str], List[str]]:
ans = []
acc = []
for _, val in Data.iterrows():
ret = None
mle = 0
for c in self.prior.keys():
pred = self.prior[c]
for idx in range(len(Data.columns[:-1])):
feature = Data.columns[idx]
cls = val[idx]
key = str(feature) + "," + str(cls) + "|" + str(c)
pred *= self.P[key]
if pred > mle:
mle = pred
ret = c
ans.append(ret)
if ret == val[-1]:
acc.append(1)
else:
acc.append(0)
return ans, acc
结果
在perdict集上正确率为:0.8095238095238095
决策树
基础理论
决策树学习本质是从训练数据集中归纳出一组分类规则,在每个结点进行分裂,将原本的数据集进行划分,最终得到叶子结点,即当前数据的标签,也就是之后预测的结果。基于树结构的算法都有一个比较好的特点:不需要进行特征的归一化,而且训练速度较快。
不同的决策树算法采用不同的分裂依据。
ID3
ID3决策树使用信息增益来作为分裂依据,通过计算当前数据集中,信息增益最大的特征作为划分依据,将数据集划分为多个子数据集,作为当前节点的分支。
信息熵:用来表示当前的样本集合纯度
为第类样本所占的比例
信息增益:
其中,D为当前结点的数据集,v为其特征
从而,对于每一个特征计算信息增益,选择最大的特征作为分类的依据,从而得到该节点的子结点,一直递归的执行,直到到达叶子节点:均属于同一类样本或没有特征可供选择,对于后者,选取当前数据集中,包含最多样本的类别作为当前叶子结点的值。
C4.5
C4.5算法与ID3算法相似,不过其采用信息增益率而非信息增益来选择最优化分属性。除此之外,还支持预剪枝以及后剪枝的方法。
信息增益率
表示属性的固有值。
信息增益率对可取值数目较少的属性有所偏好,所以对C4.5算法进行改进,采用一个启发式来进行选择:从候选划属性中找到信息增益高于平均水平的属性,再从中选择增益率最高的。
CART
CART则采用基尼指数来进行划分属性的选择
基尼值:
基尼指数:
与前两者不同,其选取的是基尼指数最小的属性作为最优化分属性,而且CART不仅可以用于分类,而且还可以用于回归。
除了上述所说的,决策树多用于集成学习算法,如随机森林,GBDT等算法,都有比较好的效果。
实验代码
本次决策树的实验代码支持了上述三种构造的方法
ID3
def entropy(self, x: pd.DataFrame) -> float:
'''信息熵'''
result = 0
size = x.size
for i in np.unique(np.array(x)):
p = x[x == i].size / size
result += p * np.log2(p)
return -1 * result
def cond_entropy(self, x: pd.DataFrame) -> float:
'''条件熵'''
result = 0
size = len(x)
feature = x.columns[0]
for i in np.unique(np.array(x.iloc[:, 0])):
D_i = x.loc[x[feature] == i]
result += len(D_i) / size * self.entropy(D_i.iloc[:, -1])
return result
def gain(self, x: pd.DataFrame) -> float:
'''信息增益 ID3
x: 仅包含对应的特征列与标签列
'''
return self.entropy(np.array(x.iloc[:, -1])) - self.cond_entropy(x)
C4.5
def gain_ratio(self, x: pd.DataFrame) -> float:
'''增益率 C4.5
x: 仅包含对应的特征列与标签列
'''
return self.gain(x) / self.entropy(x.iloc[:, 0])
CART
def gini(self, x: pd.DataFrame) -> float:
'''基尼系数'''
result = 1.0
size = x.size
for i in np.unique(np.array(x)):
p = x[x == i].size / size
result -= p ** 2
return result
def gain_gini(self, x: pd.DataFrame) -> Tuple[float, str]:
'''基尼指数 CART
x: 仅包含对应的特征列与标签列
'''
min_gini = float("+inf")
kind = ""
feature = x.columns[0]
size = len(x)
for i in np.unique(np.array(x[feature])):
D_1 = x.loc[x[feature] == i].iloc[:, -1]
D_2 = x.loc[x[feature] != i].iloc[:, -1]
gini = D_1.size / size * self.gini(D_1) + D_2.size / size * self.gini(D_2)
if gini < min_gini:
min_gini = gini
kind = i
return min_gini, kind
以上,ID3和C4.5都返回的是计算得到的该特征的信息增益或增益率,而CART则是返回基尼指数以及最小的属性值
在此之后,就可以实现决策树的构造:
划分依据
def get_max(self, x: pd.DataFrame) -> Tuple[str, List[pd.DataFrame]]:
'''计算划分依据
x: 样本集D
return: max_feature本次依据的特征 dataset: 根据该特征划分出的数据集
'''
max_val = 0
max_feature = str
if self.method == "CART": # CART还要有当前的属性
max_kind = str
max_val = float("+inf") # 由于选择是最小的基尼指数,所以应当为极大值
for i in x.columns[:-1]: # 遍历每一个特征,选取最优划分依据
A = pd.concat([x[i], x[x.columns[-1]]], axis=1)
val = float
if self.method == "ID3": # 根据不同的方法选择得到不同的值
val = self.gain(A)
elif self.method == "C4.5":
val = self.gain_ratio(A)
elif self.method == "CART":
val, kind = self.gain_gini(A)
if val > max_val and self.method != "CART":
max_val = val
max_feature = i
elif val < max_val and self.method == "CART":
max_val = val
max_feature = i
max_kind = kind
dataset = [] # 划分
if self.method != "CART": # 多叉树
for i in np.unique(np.array(x[max_feature])):
D_i = x.loc[x[max_feature] == i]
dataset.append(D_i)
elif self.method == "CART": # 二叉树
D_1 = x.loc[x[max_feature] == max_kind]
D_2 = x.loc[x[max_feature] != max_kind]
dataset.append(D_1)
dataset.append(D_2)
return max_feature, dataset
由于CART为二叉树,并且划分依据与其他的不同,所以在这为了适配三种方法,采用了特殊的判断。最终,这个函数返回的是最优划分属性以及划分后的数据集,使用List[pd.DataFrame]表示
建树
def build(self, x: pd.DataFrame) -> Dict:
deep = copy.deepcopy(self.depth)
self.depth += 1
y = x.iloc[:, -1]
if len(np.unique(np.array(y))) == 1: # 叶子节点
self.depth -= 1
return y.iloc[0]
if len(x.columns) == 1: # 没有属性可供划分
result = list(x.iloc[:, -1])
val, label = 0, str
for i in np.unique(np.array(result)): # 投票
if result.count(i) > val:
val = result.count(i)
label = i
return label
if deep > self.max_depth: # 到达最大深度 剪枝
result = list(x.iloc[:, -1])
val, label = 0, str
for i in np.unique(result):
if result.count(i) > val:
val = result.count(i)
label = i
return label
feature, dataset = self.get_max(x)
tree = {feature: {}}
if self.method != "CART": # 多叉树构造,遍历上述方法求得的dataset集
for i in range(len(dataset)):
data = dataset[i].copy()
kind = data[feature].iloc[0]
data.drop(feature, inplace=True, axis=1)
feature_ch = self.build(data)
tree[feature][kind] = feature_ch
elif self.method == "CART": # 二叉树构造,只有该种属性和其他
data_1 = dataset[0].copy()
data_2 = dataset[1].copy()
kind = data_1[feature].iloc[0]
data_1.drop(feature, inplace=True, axis=1)
data_2.drop(feature, inplace=True, axis=1)
feature_ch = self.build(data_1)
tree[feature][kind] = feature_ch
feature_ch = self.build(data_2)
tree[feature]["!" + kind] = feature_ch # 其他采用 "!"+属性值 表示
self.depth -= 1
return tree
预测
def predict(self, pred: pd.DataFrame) -> List[str]:
ans = [] # 最终预测的结果
class_list = list(pred.iloc[:, -1]) # 叶子结点的属性值
for _, data in pred.iterrows(): # 遍历每一个测试样例
key = [elem for elem in self.tree.keys()][0] # 取出当前树的键值(划分的属性依据)
feature = data[key] # 得到预测的数据中该属性的值
if self.method != "CART": # 不是CART决策时
class_val = self.tree[key][feature] # 得到该属性的该值对应的子树
elif self.method == "CART": # 是CART
try:
class_val = self.tree[key][feature] # 决策树的属性值与测试数据该属性的值相匹配
except KeyError:
feature_notin = [elem for elem in self.tree[key].keys()][1] # 不匹配 得到另一枝
# 得到该属性的另一值对应的子树(CART为二叉树)
class_val = self.tree[key][feature_notin]
while class_val not in class_list: # 当子树的属性不是叶子节点的属性(当不是叶子节点时)
key = [elem for elem in class_val.keys()][0] # 重复上面的操作
feature = data[key]
if self.method != "CART":
class_val = class_val[key][feature]
elif self.method == "CART":
try:
class_val = class_val[key][feature]
except KeyError:
feature_notin = [elem for elem in class_val[key].keys()][1]
class_val = class_val[key][feature_notin]
ans.append(class_val) # 将本次预测的结果加入到ans中
return ans # 返回所有数据预测的结果
预测部分与朴素贝叶斯相似,都是返回预测的预测集结果。在预测的过程中,同样的遍历每一个样例,取出当前树的划分依据,根据样例的属性来选择分支,一直继续直到到达叶子节点,此时,得到的就是该样例的预测结果。
结果
在predict集上能够得到完全的预测。
支持向量机
这一部分由于课本上并没有深入的介绍,所以我也没有具体去手动实现,因为SMO算法的实现确实有难度。
理论基础
对于样本的分类,最基本的想法就是基于数据集D,在样本空间找到一个划分超平面,把不同类别的进行分开。而支持向量机,则是在该基础上,找到两个分类到达超平面的最大间隔最大,那么这一个超平面则是要学习到的超平面。
根据超平面的方程:可见超平面可用来表示,那么样本空间中任意点到超平面的距离即
考虑到目标是找到超平面,使得距离该超平面最近的样本点最远,所以有
求解目标即:
如果可用完全正确分类,那么有
在边界上的样本点即支持向量,两个异类支持向量到超平面的距离和为,所以,目标即使其最大化,等价于
得到了上述的式子,显然是一个最优化问题,可以通过梯度下降等优化算法来对其进行求解。
此处可以选择损失函数为,对其使用梯度下降进行优化,从而得到最终的解。
另一种效率更高的方式即SMO算法:引入拉格朗日乘子得到其对偶问题,并且根据上式的约束条件满足KKT条件,可以使用SMO算法来求解拉格朗日乘子,进而得到所求的和。
除此之外,还可以使用来将样本点映射到高维空间,以解决线性不可分的问题。通过引入软间隔,使得其允许接受一定程度得错误。
代码实现
这里采用的是sklearn库的SVC方法
svc = SVC()
svc.fit(x, y)
采用了默认的参数,即使用高斯核,软间隔超参数C=1.0
结果
能够做到 0.9206349206349206 的正确率
神经网络
理论基础
简单来说,每个神经网络必有一层输入层,一层输出层,中间的隐层可以自行设置。输入层得大小即样本的特征数量,输出层的大小为分类的类别数目。对于每一个样本的输入,首先经过前向传播:简单的线性函数累加,再经过激活函数之后继续传递到下一层,到达输出层后计算误差,将误差进行反向传播,并对参数进行优化。经过多次训练,直到损失函数收敛,训练结束。
预测部分则将数据输入,得到每个输出神经元的值,从中选取最大的值最为当前样本的预测结果。
考虑上图的神经网络,输入为,经过前向传播到达隐层,得到隐层结点的值:,经过一次激活函数得到:,再继续传播得到输出的值:(假设偏置放入到输入作为,对应的输入)。
之后再将误差反向传播,首先得到输出层的误差:,反向传播得到隐层的误差,同时求得梯度为,继续对参数进行优化:
代码实现
采用了四层的神经网络,包含两个隐层,激活函数选择ReLU激活函数:
class NN(nn.Module):
def __init__(self, in_dim, hidden_1, hidden_2, out_dim):
super(NN, self).__init__()
self.hidden1 = nn.Linear(in_dim, hidden_1)
self.hidden2 = nn.Linear(hidden_1, hidden_2)
self.out = nn.Linear(hidden_2, out_dim)
def forward(self, x):
x = self.hidden1(x)
x = F.relu(x)
x = self.hidden2(x)
x = F.relu(x)
x = self.out(x)
x = F.relu(x)
return x
在训练时,选择的优化器为随机梯度下降优化器,学习率默认为0.1,使用交叉熵作为损失函数,训练轮数默认5000轮:
def __init__(self, in_dim, hidden_1, hidden_2, out_dim, lr=0.1, epochs=5000):
self.model = NN(in_dim, hidden_1, hidden_2, out_dim)
self.optimizer = optim.SGD(self.model.parameters(), lr=lr) # 随机梯度下降优化器
self.criterion = nn.CrossEntropyLoss() # 交叉熵
self.epochs = epochs
if torch.cuda.is_available():
self.device = torch.device('cuda:0')
self.model.to(self.device)
self.criterion = self.criterion.cuda()
训练:
def fit(self, data: pd.DataFrame, test: pd.DataFrame):
X_train = data.iloc[:, :-1]
y_train = data.iloc[:, -1]
X_test = test.iloc[:, :-1]
y_test = test.iloc[:, -1]
X_train = np.array(X_train, dtype=np.float32)
y_train = np.array(y_train, dtype=np.float32)
X_test = np.array(X_test, dtype=np.float32)
y_test = np.array(y_test, dtype=np.float32)
X_test = torch.from_numpy(X_test)
y_test = torch.from_numpy(y_test)
if torch.cuda.is_available():
X_test = X_test.to(self.device)
for epoch in range(self.epochs): # 每一轮训练
x = torch.from_numpy(X_train)
y = torch.from_numpy(y_train)
if torch.cuda.is_available():
x = x.to(self.device)
y = y.to(self.device)
pred = self.model(x)
loss = self.criterion(pred, y.long()) # 计算loss
self.optimizer.zero_grad() # 优化
loss.backward() # 反向传播
self.optimizer.step()
预测则是输入数据,选择最大的类别作为当前的分类:
def predict(self, pred: pd.DataFrame) -> float:
X_pred = pred.iloc[:, :-1]
y_pred = pred.iloc[:, -1]
X_pred = np.array(X_pred, dtype=np.float32)
y_pred = np.array(y_pred, dtype=np.float32)
x = torch.from_numpy(X_pred)
y = torch.from_numpy(y_pred)
if torch.cuda.is_available():
x = x.to(self.device)
predict = self.model(x)
predict = torch.max(predict, 1)[1] # 选择最大的类别
if torch.cuda.is_available():
predict = predict.cpu()
pred_y = predict.data.numpy()
target_y = y.data.numpy()
acc = float((pred_y == target_y).astype(int).sum()) / float(target_y.size)
return acc
结果
每500次训练进行loss的输出以及准确率的查看
最终预测结果为0.9682539682539683的准确率。若将轮数增大,准确率能够逼近1.0
实验结果
均为在perdict数据集上做预测的结果。
显而易见的是,神经网络的分类方法能够适用于任何数据集,在模型搭建好之后,只需要进行训练,就可以得到不错的结果,训练的轮数越多,网络的层数越深,就能够得到约准确的分类,但是带来的时间开销也是巨大的。
对于其他三种算法,支持向量机的性能是很不错的,泛化性能较强,而且是用于解决高维的问题;朴素贝叶斯较简单,由于是有坚实的数学基础所以其分类效率较稳定,但是需要直到其先验概率才可以进行学习;决策树的优点正如上述所讲,不需要进行特征归一化,但是其容易忽略样本特征之间的相关性,而且容易过拟合。
思考题
对于参数的调整,支持向量机可以尝试不同的核,或者减少其软间隔系数,以使得分类效果更好,但是会降低其泛化性能;决策树则可以尝试不同的剪枝策略来提高其泛化能力;朴素贝叶斯则可以尝试进行不同的平滑策略,除了拉普拉斯平滑还可以选择lidstone平滑;而神经网络,则可以尝试增加训练轮数或者增大网络的深度来得到更好的分类结果,但是带来的代价则是时间开销的增大,此外还可以调整学习率来得到更好的训练结果。