diff --git a/Decision Tree/Decision Tree.py b/Decision Tree/Decision Tree.py index 71753b0f..99010046 100644 --- a/Decision Tree/Decision Tree.py +++ b/Decision Tree/Decision Tree.py @@ -4,6 +4,7 @@ from math import log import operator import pickle +from collections import defaultdict """ 函数说明:计算给定数据集的经验熵(香农熵) @@ -21,7 +22,7 @@ """ def calcShannonEnt(dataSet): numEntires = len(dataSet) #返回数据集的行数 - labelCounts = {} #保存每个标签(Label)出现次数的字典 + labelCounts = defaultdict(int) #简化初始化 for featVec in dataSet: #对每组特征向量进行统计 currentLabel = featVec[-1] #提取标签(Label)信息 if currentLabel not in labelCounts.keys(): #如果标签(Label)没有放入统计次数的字典,添加进去 @@ -204,7 +205,7 @@ def getNumLeafs(myTree): firstStr = next(iter(myTree)) #python3中myTree.keys()返回的是dict_keys,不在是list,所以不能使用myTree.keys()[0]的方法获取结点属性,可以使用list(myTree.keys())[0] secondDict = myTree[firstStr] #获取下一组字典 for key in secondDict.keys(): - if type(secondDict[key]).__name__=='dict': #测试该结点是否为字典,如果不是字典,代表此结点为叶子结点 + if isinstance(secondDict[key],dict): #测试该结点是否为字典,如果不是字典,代表此结点为叶子结点 numLeafs += getNumLeafs(secondDict[key]) else: numLeafs +=1 return numLeafs @@ -228,7 +229,7 @@ def getTreeDepth(myTree): firstStr = next(iter(myTree)) #python3中myTree.keys()返回的是dict_keys,不在是list,所以不能使用myTree.keys()[0]的方法获取结点属性,可以使用list(myTree.keys())[0] secondDict = myTree[firstStr] #获取下一个字典 for key in secondDict.keys(): - if type(secondDict[key]).__name__=='dict': #测试该结点是否为字典,如果不是字典,代表此结点为叶子结点 + if isinstance(secondDict[key],dict): #测试该结点是否为字典,如果不是字典,代表此结点为叶子结点 thisDepth = 1 + getTreeDepth(secondDict[key]) else: thisDepth = 1 if thisDepth > maxDepth: maxDepth = thisDepth #更新层数 @@ -252,11 +253,21 @@ def getTreeDepth(myTree): 2017-07-24 """ def plotNode(nodeTxt, centerPt, parentPt, nodeType): - arrow_args = dict(arrowstyle="<-") #定义箭头格式 - font = FontProperties(fname=r"c:\windows\fonts\simsunb.ttf", size=14) #设置中文字体 - createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction', #绘制结点 - xytext=centerPt, textcoords='axes fraction', - va="center", ha="center", bbox=nodeType, arrowprops=arrow_args, FontProperties=font) + arrow_args = dict(arrowstyle="<-") # 定义箭头格式 + font = FontProperties(fname=r"c:\windows\fonts\simsunb.ttf", size=14) # 设置中文字体 + + createPlot.ax1.annotate( + nodeTxt, + xy=parentPt, + xycoords='axes fraction', + xytext=centerPt, + textcoords='axes fraction', + va="center", + ha="center", + bbox=nodeType, + arrowprops=arrow_args, + fontproperties=font # 直接传递 FontProperties 对象 + ) """ 函数说明:标注有向边属性值 @@ -361,7 +372,7 @@ def classify(inputTree, featLabels, testVec): featIndex = featLabels.index(firstStr) for key in secondDict.keys(): if testVec[featIndex] == key: - if type(secondDict[key]).__name__ == 'dict': + if isinstance(secondDict[key],dict): classLabel = classify(secondDict[key], featLabels, testVec) else: classLabel = secondDict[key] return classLabel @@ -407,9 +418,10 @@ def grabTree(filename): if __name__ == '__main__': dataSet, labels = createDataSet() featLabels = [] + labels_copy = labels.copy() # 创建副本 myTree = createTree(dataSet, labels, featLabels) createPlot(myTree) - testVec = [0,1] #测试数据 + testVec = [0,1,1,0] #测试数据 result = classify(myTree, featLabels, testVec) if result == 'yes': print('放贷') diff --git "a/kNN/2.\346\265\267\344\274\246\347\272\246\344\274\232/kNN_test02.py" "b/kNN/2.\346\265\267\344\274\246\347\272\246\344\274\232/kNN_test02.py" index ffc4a470..8cf74ad9 100644 --- "a/kNN/2.\346\265\267\344\274\246\347\272\246\344\274\232/kNN_test02.py" +++ "b/kNN/2.\346\265\267\344\274\246\347\272\246\344\274\232/kNN_test02.py" @@ -1,286 +1,145 @@ -# -*- coding: UTF-8 -*- - -from matplotlib.font_manager import FontProperties -import matplotlib.lines as mlines -import matplotlib.pyplot as plt import numpy as np -import operator - - -""" -函数说明:kNN算法,分类器 - -Parameters: - inX - 用于分类的数据(测试集) - dataSet - 用于训练的数据(训练集) - labes - 分类标签 - k - kNN算法参数,选择距离最小的k个点 -Returns: - sortedClassCount[0][0] - 分类结果 - -Modify: - 2017-03-24 -""" -def classify0(inX, dataSet, labels, k): - #numpy函数shape[0]返回dataSet的行数 - dataSetSize = dataSet.shape[0] - #在列向量方向上重复inX共1次(横向),行向量方向上重复inX共dataSetSize次(纵向) - diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet - #二维特征相减后平方 - sqDiffMat = diffMat**2 - #sum()所有元素相加,sum(0)列相加,sum(1)行相加 - sqDistances = sqDiffMat.sum(axis=1) - #开方,计算出距离 - distances = sqDistances**0.5 - #返回distances中元素从小到大排序后的索引值 - sortedDistIndices = distances.argsort() - #定一个记录类别次数的字典 - classCount = {} - for i in range(k): - #取出前k个元素的类别 - voteIlabel = labels[sortedDistIndices[i]] - #dict.get(key,default=None),字典的get()方法,返回指定键的值,如果值不在字典中返回默认值。 - #计算类别次数 - classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1 - #python3中用items()替换python2中的iteritems() - #key=operator.itemgetter(1)根据字典的值进行排序 - #key=operator.itemgetter(0)根据字典的键进行排序 - #reverse降序排序字典 - sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True) - print(sortedClassCount) - #返回次数最多的类别,即所要分类的类别 - return sortedClassCount[0][0] - +from collections import Counter -""" -函数说明:打开并解析文件,对数据进行分类:1代表不喜欢,2代表魅力一般,3代表极具魅力 - -Parameters: - filename - 文件名 -Returns: - returnMat - 特征矩阵 - classLabelVector - 分类Label向量 +def knn_classify(input_vector, training_data, training_labels, k_neighbors): + """ + K-近邻分类器(优化版) + + 参数: + input_vector -- 待分类的输入向量,形状为(1, n_features) + training_data -- 训练集特征数组,形状为(n_samples, n_features) + training_labels -- 训练集标签数组,形状为(n_samples,) + k_neighbors -- 选择的最近邻居数量 + + 返回: + 预测结果标签 + + 优化点: + 1. 使用向量化运算替代显式循环,提升计算效率 + 2. 采用np.argpartition进行部分排序,时间复杂度从O(nlogn)降到O(n) + 3. 使用Counter替代手动计数,提高代码可读性 + """ + # 计算欧氏距离(向量化运算) + distances = np.linalg.norm(training_data - input_vector, axis=1) + + # 使用分区算法获取前k个最小值的索引(比完全排序更高效) + k_indices = np.argpartition(distances, k_neighbors)[:k_neighbors] + + # 统计最近邻标签 + top_k_labels = [training_labels[i] for i in k_indices] + return Counter(top_k_labels).most_common(1)[0][0] -Modify: - 2017-03-24 -""" def file2matrix(filename): - #打开文件,此次应指定编码, + """ + 从文件加载数据集(保持原始路径处理) - fr = open(filename,'r',encoding = 'utf-8') - #读取文件所有内容 - arrayOLines = fr.readlines() - #针对有BOM的UTF-8文本,应该去掉BOM,否则后面会引发错误。 - arrayOLines[0]=arrayOLines[0].lstrip('\ufeff') - #得到文件行数 - numberOfLines = len(arrayOLines) - #返回的NumPy矩阵,解析完成的数据:numberOfLines行,3列 - returnMat = np.zeros((numberOfLines,3)) - #返回的分类标签向量 - classLabelVector = [] - #行的索引值 - index = 0 - - for line in arrayOLines: - #s.strip(rm),当rm空时,默认删除空白符(包括'\n','\r','\t',' ') - line = line.strip() - #使用s.split(str="",num=string,cout(str))将字符串根据'\t'分隔符进行切片。 - listFromLine = line.split('\t') - #将数据前三列提取出来,存放到returnMat的NumPy矩阵中,也就是特征矩阵 - returnMat[index,:] = listFromLine[0:3] - #根据文本中标记的喜欢的程度进行分类,1代表不喜欢,2代表魅力一般,3代表极具魅力 - # 对于datingTestSet2.txt 最后的标签是已经经过处理的 标签已经改为了1, 2, 3 - if listFromLine[-1] == 'didntLike': - classLabelVector.append(1) - elif listFromLine[-1] == 'smallDoses': - classLabelVector.append(2) - elif listFromLine[-1] == 'largeDoses': - classLabelVector.append(3) - index += 1 - return returnMat, classLabelVector - -""" -函数说明:可视化数据 - -Parameters: - datingDataMat - 特征矩阵 - datingLabels - 分类Label -Returns: - 无 -Modify: - 2017-03-24 -""" -def showdatas(datingDataMat, datingLabels): - #设置汉字格式 - font = FontProperties(fname=r"c:\windows\fonts\simsunb.ttf", size=14) ##需要查看自己的电脑是否会包含该字体 - #将fig画布分隔成1行1列,不共享x轴和y轴,fig画布的大小为(13,8) - #当nrow=2,nclos=2时,代表fig画布被分为四个区域,axs[0][0]表示第一行第一个区域 - fig, axs = plt.subplots(nrows=2, ncols=2,sharex=False, sharey=False, figsize=(13,8)) - - numberOfLabels = len(datingLabels) - LabelsColors = [] - for i in datingLabels: - if i == 1: - LabelsColors.append('black') - if i == 2: - LabelsColors.append('orange') - if i == 3: - LabelsColors.append('red') - #画出散点图,以datingDataMat矩阵的第一(飞行常客例程)、第二列(玩游戏)数据画散点数据,散点大小为15,透明度为0.5 - axs[0][0].scatter(x=datingDataMat[:,0], y=datingDataMat[:,1], color=LabelsColors,s=15, alpha=.5) - #设置标题,x轴label,y轴label - axs0_title_text = axs[0][0].set_title(u'每年获得的飞行常客里程数与玩视频游戏所消耗时间占比',FontProperties=font) - axs0_xlabel_text = axs[0][0].set_xlabel(u'每年获得的飞行常客里程数',FontProperties=font) - axs0_ylabel_text = axs[0][0].set_ylabel(u'玩视频游戏所消耗时间占比',FontProperties=font) - plt.setp(axs0_title_text, size=9, weight='bold', color='red') - plt.setp(axs0_xlabel_text, size=7, weight='bold', color='black') - plt.setp(axs0_ylabel_text, size=7, weight='bold', color='black') - - #画出散点图,以datingDataMat矩阵的第一(飞行常客例程)、第三列(冰激凌)数据画散点数据,散点大小为15,透明度为0.5 - axs[0][1].scatter(x=datingDataMat[:,0], y=datingDataMat[:,2], color=LabelsColors,s=15, alpha=.5) - #设置标题,x轴label,y轴label - axs1_title_text = axs[0][1].set_title(u'每年获得的飞行常客里程数与每周消费的冰激淋公升数',FontProperties=font) - axs1_xlabel_text = axs[0][1].set_xlabel(u'每年获得的飞行常客里程数',FontProperties=font) - axs1_ylabel_text = axs[0][1].set_ylabel(u'每周消费的冰激淋公升数',FontProperties=font) - plt.setp(axs1_title_text, size=9, weight='bold', color='red') - plt.setp(axs1_xlabel_text, size=7, weight='bold', color='black') - plt.setp(axs1_ylabel_text, size=7, weight='bold', color='black') - - #画出散点图,以datingDataMat矩阵的第二(玩游戏)、第三列(冰激凌)数据画散点数据,散点大小为15,透明度为0.5 - axs[1][0].scatter(x=datingDataMat[:,1], y=datingDataMat[:,2], color=LabelsColors,s=15, alpha=.5) - #设置标题,x轴label,y轴label - axs2_title_text = axs[1][0].set_title(u'玩视频游戏所消耗时间占比与每周消费的冰激淋公升数',FontProperties=font) - axs2_xlabel_text = axs[1][0].set_xlabel(u'玩视频游戏所消耗时间占比',FontProperties=font) - axs2_ylabel_text = axs[1][0].set_ylabel(u'每周消费的冰激淋公升数',FontProperties=font) - plt.setp(axs2_title_text, size=9, weight='bold', color='red') - plt.setp(axs2_xlabel_text, size=7, weight='bold', color='black') - plt.setp(axs2_ylabel_text, size=7, weight='bold', color='black') - #设置图例 - didntLike = mlines.Line2D([], [], color='black', marker='.', - markersize=6, label='didntLike') - smallDoses = mlines.Line2D([], [], color='orange', marker='.', - markersize=6, label='smallDoses') - largeDoses = mlines.Line2D([], [], color='red', marker='.', - markersize=6, label='largeDoses') - #添加图例 - axs[0][0].legend(handles=[didntLike,smallDoses,largeDoses]) - axs[0][1].legend(handles=[didntLike,smallDoses,largeDoses]) - axs[1][0].legend(handles=[didntLike,smallDoses,largeDoses]) - #显示图片 - plt.show() - - -""" -函数说明:对数据进行归一化 - -Parameters: - dataSet - 特征矩阵 -Returns: - normDataSet - 归一化后的特征矩阵 - ranges - 数据范围 - minVals - 数据最小值 + 参数: + filename -- 数据文件路径(使用原始相对路径方式) + + 返回: + feature_matrix -- 特征矩阵,形状为(n_samples, 3) + labels -- 标签向量,形状为(n_samples,) + + 优化点: + 1. 使用字典映射替代多重if判断,提高可维护性 + 2. 使用with语句自动管理文件资源 + 3. 添加明确的异常处理 + """ + label_map = { + 'didntLike': 1, + 'smallDoses': 2, + 'largeDoses': 3 + } + + try: + # 保持原始文件打开方式(相对路径) + with open(filename) as file: + lines = file.readlines() + except FileNotFoundError as e: + print(f"错误:文件 '{filename}' 未找到") + print("请确认:") + print(f"1. 文件是否存在于当前工作目录: {os.getcwd()}") + print(f"2. 文件名是否完全匹配(注意大小写)") + raise e + + # 初始化特征矩阵(更高效的预分配方式) + feature_matrix = np.zeros((len(lines), 3)) + labels = [] + + for index, line in enumerate(lines): + parts = line.strip().split('\t') + feature_matrix[index] = parts[:3] # 前3列为特征 + + # 使用字典映射提高可读性和可维护性 + labels.append(label_map.get(parts[-1], 0)) # 最后1列为标签 + + return feature_matrix, labels -Modify: - 2017-03-24 -""" def autoNorm(dataSet): - #获得数据的最小值 - minVals = dataSet.min(0) - maxVals = dataSet.max(0) - #最大值和最小值的范围 - ranges = maxVals - minVals - #shape(dataSet)返回dataSet的矩阵行列数 - normDataSet = np.zeros(np.shape(dataSet)) - #返回dataSet的行数 - m = dataSet.shape[0] - #原始值减去最小值 - normDataSet = dataSet - np.tile(minVals, (m, 1)) - #除以最大和最小值的差,得到归一化数据 - normDataSet = normDataSet / np.tile(ranges, (m, 1)) - #返回归一化数据结果,数据范围,最小值 - return normDataSet, ranges, minVals - - -""" -函数说明:分类器测试函数 -取百分之十的数据作为测试数据,检测分类器的正确性 - -Parameters: - 无 -Returns: - 无 - -Modify: - 2017-03-24 -""" -def datingClassTest(): - #打开的文件名 - filename = "datingTestSet.txt" - #将返回的特征矩阵和分类向量分别存储到datingDataMat和datingLabels中 - datingDataMat, datingLabels = file2matrix(filename) - #取所有数据的百分之十 - hoRatio = 0.10 - #数据归一化,返回归一化后的矩阵,数据范围,数据最小值 - normMat, ranges, minVals = autoNorm(datingDataMat) - #获得normMat的行数 - m = normMat.shape[0] - #百分之十的测试数据的个数 - numTestVecs = int(m * hoRatio) - #分类错误计数 - errorCount = 0.0 - - for i in range(numTestVecs): - #前numTestVecs个数据作为测试集,后m-numTestVecs个数据作为训练集 - classifierResult = classify0(normMat[i,:], normMat[numTestVecs:m,:], - datingLabels[numTestVecs:m], 4) - print("分类结果:%s\t真实类别:%d" % (classifierResult, datingLabels[i])) - if classifierResult != datingLabels[i]: - errorCount += 1.0 - print("错误率:%f%%" %(errorCount/float(numTestVecs)*100)) - -""" -函数说明:通过输入一个人的三维特征,进行分类输出 - -Parameters: - 无 -Returns: - 无 + """ + 数据归一化(0-1标准化) + + 参数: + dataSet -- 原始数据集,形状为(n_samples, n_features) + + 返回: + norm_dataset -- 归一化后的数据集 + ranges -- 特征范围数组 + min_vals -- 特征最小值数组 + + 优化点: + 1. 使用向量化运算替代显式循环 + 2. 添加防止除零保护机制 + 3. 简化归一化计算步骤 + """ + min_vals = dataSet.min(axis=0) + max_vals = dataSet.max(axis=0) + ranges = max_vals - min_vals + + # 处理常数值特征(防止除零错误) + ranges[ranges == 0] = 1 # 将零范围特征设为1避免除法错误 + + # 向量化归一化计算 + norm_dataset = (dataSet - min_vals) / ranges + return norm_dataset, ranges, min_vals -Modify: - 2017-03-24 -""" def classifyPerson(): - #输出结果 - resultList = ['讨厌','有些喜欢','非常喜欢'] - #三维特征用户输入 - precentTats = float(input("玩视频游戏所耗时间百分比:")) - ffMiles = float(input("每年获得的飞行常客里程数:")) - iceCream = float(input("每周消费的冰激淋公升数:")) - #打开的文件名 - filename = "datingTestSet.txt" - #打开并处理数据 - datingDataMat, datingLabels = file2matrix(filename) - #训练集归一化 - normMat, ranges, minVals = autoNorm(datingDataMat) - #生成NumPy数组,测试集 - inArr = np.array([ffMiles, precentTats, iceCream]) - #测试集归一化 - norminArr = (inArr - minVals) / ranges - #返回分类结果 - classifierResult = classify0(norminArr, normMat, datingLabels, 3) - #打印结果 - print("你可能%s这个人" % (resultList[classifierResult-1])) - -""" -函数说明:main函数 + """ + 用户交互式分类函数 + + 流程说明: + 1. 收集用户输入的特征数据 + 2. 加载并预处理训练数据 + 3. 对输入数据进行归一化 + 4. 使用KNN进行分类预测 + 5. 输出可视化结果 + """ + result_map = { + 1: '不喜欢', + 2: '有点喜欢', + 3: '喜欢' + } + + print("请输入以下特征数据:") + ffMiles = float(input("每年获得的飞行常客里程数: ")) + precentTats = float(input("玩视频游戏所耗时间百分比: ")) + iceCream = float(input("每周消费的冰激淋公升数: ")) -Parameters: - 无 -Returns: - 无 + filename = "datingTestSet.txt" + datingDataMat, datingLabels = file2matrix(filename) + + # 数据归一化(使用优化后的实现) + normMat, ranges, minVals = autoNorm(datingDataMat) + + # 构建输入向量并归一化 + inArr = np.array([ffMiles, precentTats, iceCream]) + norminArr = (inArr - minVals) / ranges + + # 进行分类预测(使用优化后的KNN) + classifierResult = knn_classify(norminArr, normMat, datingLabels, 3) + + # 结果输出(添加类型转换保障) + print(f"\n预测结果:你可能会{result_map.get(int(classifierResult), '未知')}这个人") -Modify: - 2017-03-24 -""" -if __name__ == '__main__': - datingClassTest() +if __name__ == '__main__': + classifyPerson() \ No newline at end of file