本文为《机器学习实战》学习笔记
SciPy 基于Python生态系统提供了数学运算、科学和工程的开源软件,主要包括基本N维数组包NumPy,科学计算基本库SciPy library, 用于2D绘图的Matplotlib,交互式控制台IPython,用于符号数学Sympy,用于数据结构和分析的pandas。
NumPy是python科学计算的基础包。包括强大的N维数组对象;复杂的函数;集成C/C++和Fortran代码的工具;线性代数、傅里叶变换和随机数功能。
标准python库中的array是一维的,功能更少。numpy的数组是ndarray,可以是多维的,拥有更多功能和属性。ndarray的主要属性如下:
ndim – 数组维度
shape – 数组形状,使用tuple表示数组在各个维度的大小,对于一个n行m列的数组,返回值为(n, m),该元组的长度为数组维度的数量,即ndim。
size – 数组中元素的总数
dtype –数组中元素的类型
ndarray的基本运算如下:
A * B表示两个数组对应位置的元素相乘
A.dot(B) / dot(A, B) 表示矩阵积
A.sum(axis = ) 对数组元素求和。A.sum()表示对数组中的所有元素求和,axis = 0表示对每一列求和,axis = 1表示对每一行求和。
不同数据类型的数据运算时,结果向上转型(upcasting)
Matplotlib是Python 2D绘图库。matplotlib.pyplot集合了命令行风格的函数,使得Matplotlib能够像MATLAB一样工作。每个pyplot函数都会导致图片的一些改变。
operator模块包含了Python的标准操作函数,也定义了用于通用属性和item查找的工具。
io模块主要提供处理多种类型I/O的工具。
os模块与依赖操作系统的功能有关。其中,通过open()函数读写文件,os.path模块提供了操作路径的方法,fileinput模块能够在命令行中读取所有文件的所有行,tempfile模块提供创建临时文件和目录的操作,shutil模块提供高级的文件和目录处理。
#构造数据集
from numpy import *
def createDataSet():
#group为样本,labels为样本对应标签
group = array([[1.0, 1.1], [1.0, 1.0], [0, 0], [0, 0.1]])
labels = ['A', 'A', 'B', 'B']
return group, labels
numpy.array(object, dtype=None, copy=True, order=’K’, subok=False, ndmin=0)建立数组。object为数组,dtype指定数据类型,ndmin指定最小维度
#kNN算法
#inX为待分类向量,dataSet为训练样本集, labels为标签,k
def kNN(inX, dataSet, labels, k):
#计算待分类向量与所有训练样本的欧氏距离
dataSetSize = dataSet.shape[0] #获得训练样本的个数
difMat = tile(inX, (dataSetSize, 1)) - dataSet #待分类向量与所有训练样本的差值矩阵
sqDifMat = difMat ** 2 #差值平方
sqDistances = sqDifMat.sum(axis = 1) # 对每行求和
distances = sqDistances ** 0.5 #开根号,得到待分类向量与所有训练样本的距离
sortedDistIndicies = distances.argsort() #得到使距离有序的索引数组
#统计k个最近邻样本的标签数
classCounts = {} #使用字典记录不同类别的样本数
for i in range(k):
voteIlabel = labels[sortedDistIndicies[i]]
classCounts[voteIlabel] = classCounts.get(voteIlabel, 0) + 1
#按照标签的数量对标签逆序排序
sortedClassCount = sorted(classCounts.items(), key = operator.itemgetter(1), reverse = True)
return sortedClassCount[0][0]
numpy.tile(A, reps)函数通过将A重复reps次,构造数组。A为输入的数组,reps为A在每个维度上的重复次数。
numpy.argsort(a, axis=-1, kind=’quicksort’, order=None)返回能够使数组有序的索引数组index_array,a[index_array]使数组a有序。a为待排序数组;axis为排序维度,默认为-1,表示最后一个维度;kind为排序算法,可为’quicksort’, ‘mergesort’,’heapsort’;order为指定的顺序,当a具有字段定义时有效。
sorted(iterable[, key][, reverse])返回可迭代对象iterable的有序列表,是稳定排序。key为函数,指定用于比较的自变量,默认为None,表示直接进行排序;reverse为bool变量,指定是否逆序。
operator.itemgetter(item)返回可调用的对象,实现operator._getitem_(a, b)函数的功能,返回a在索引b处的值,item即为b。如果指定多个对象,返回元组。operator.itemgetter函数获取的不是值,而是定义了一个函数,通过该函数作用到对象上才能获取值。
如,f = operator.itemgetter(2, 3)定义函数f,获取索引(2, 3)处的值, 调用f(r)返回(r[2], r[3])
文本文件中的数据如下所示:
需要将待处理数据改为分类器可接受的格式,即每个样本的特征为一行,存入数组,所有样本的标签存入列表。
#将文本转换为特征矩阵和标签列表
def file2matrix(filename):
fr = open(filename, 'r')
try:
#得到文件的行数
arrayOfLines = fr.readlines() #按行读入文件,存入list
numberOfLines = len(arrayOfLines) #得到文件的行数
#创建返回的Numpy特征矩阵
returnMat = zeros((numberOfLines, 3)) #全0填充指定大小的矩阵
classLabelVector = []
#解析文本
index = 0
for line in arrayOfLines:
line = line.strip() #去掉文本前后的空白符
listFromLine = line.split('\t') #用'\t'划分整行数据,得到元素列表
returnMat[index, :] = listFromLine[0 : 3]
classLabelVector.append(listFromLine[-1])
index += 1
finally:
fr.close()
return returnMat, classLabelVector
numpy.zeros(shape, dtype=float, order=’C’)返回给定形状和数据类型,用0填充的数组
io.readline(size=-1)从流中读并返回1行,如果指定size,最多读入size字节。
io.readlines(hint=-1)从流中读入并返回行组成的列表,可以指定hint控制读入行的数量。
str.strip([chars])拷贝并移除str首尾的chars的任意组合。chars缺失,移除首尾空格。例如,
>>> strA = ' www.helloworld.com '
>>> strB = strA.strip()
>>> strC = strB.strip('.wcdo') #移除首尾'.wcdo的任意组合'
>>> strA
' www.helloworld.com '
>>> strB
'www.helloworld.com'
>>> strC
'helloworld.com'
图形化展示数据更直观,有助于了解数据的真实含义。可使用Matplotlib制作原始数据的散点图。
#分析数据--画散点图
import matplotlib.pyplot as plt
from pylab import *
mpl.rcParams['font.sans-serif'] = ['SimHei'] #动态设置添加中文黑体
mpl.rcParams['axes.unicode_minus'] = False #更改字体导致显示不出负号,所以设置为true,保证负号的显示
fig = plt.figure() #创建图
ax1 = fig.add_subplot(211) #创建子图,ax1位于两行一列的第一个
#在子图区域绘制散点图,后两项为不同的标签设置不同的节点大小和颜色
#将list转换为array,才能给每项*15,否则为将list重复15次
ax1.scatter(datingDataMat[:, 1], datingDataMat[:, 2], 15.0 * array(datinglabels), 15.0 * array(datinglabels))
plt.xlabel('玩游戏所耗时间百分比')
plt.ylabel('每周消费的冰淇淋公升数')
#添加图例,将不同类型的点分开添加到散点图中
ids_1 = []
ids_2 = []
ids_3 = []
for i in range(len(datinglabels)):
if datinglabels[i] == 1:
ids_1.append(i)
elif datinglabels[i] == 2:
ids_2.append(i)
else:
ids_3.append(i)
ax2 = fig.add_subplot(212) #ax2位于两行一列的第二个
ax2.scatter(datingDataMat[ids_1, 0], datingDataMat[ids_1, 1], marker = 'x', c = 'g', label = '极具魅力')
ax2.scatter(datingDataMat[ids_2, 0], datingDataMat[ids_2, 1], marker = '+', c = 'y', label = '魅力一般')
ax2.scatter(datingDataMat[ids_3, 0], datingDataMat[ids_3, 1], marker = 'o', c = 'b', label = '不喜欢')
plt.ylabel('玩游戏所耗时间百分比')
plt.xlabel('每年获取的飞行常客里程数')
ax2.legend(loc = 4) #4等价于'lower right'
plt.show()
figure(num=None, figsize=None, dpi=None, facecolor=None, edgecolor=None, frameon=True, **kwargs)创建新的图片,所有参数均可选,不指定则设为默认值。num为图片的id,可以为整数或字符串,如果不指定num,默认递增,如果指定的num已存在,将会激活对应图片,否则创建新图片;figsize指定图片宽度和长度(以英寸为单位),整数元组;dpi指定图片分辨率,整数;facecolor指定背景颜色;edgecolor指定边界颜色。
add_subplot(*args, **kwargs)添加子图。包含若干参数,数字’ABC’或’A, B, C’表示将figure划分为A行,B列,对划分后的区域按行从1编码到A*B,当前plot为编码为C的区域。
subplot()函数创建子图,但会把之前存在的重叠子图覆盖。而add_subplot()激活之前存在的子图。
scatter(x, y, s=None, c=None, marker=None, cmap=None, norm=None, vmin=None, vmax=None, alpha=None, linewidths=None, verts=None, edgecolors=None, hold=None, data=None, **kwargs)绘制x, y的散点图,x, y的形状均为(n,);s为点的大小;c为点的颜色,当使用RGB时,c应为2维数组,行为RGB。
legend(*args, **kwargs)为axes添加图例,loc表示图例的位置,可用数字0-10和对应字符串表示:
Location String | Location Code |
---|---|
‘best’ | 0 |
‘upper right’ | 1 |
‘upper left’ | 2 |
‘lower left’ | 3 |
‘lower right’ | 4 |
‘right’ | 5 |
‘center left’ | 6 |
‘center right’ | 7 |
‘lower center’ | 8 |
‘upper center’ | 9 |
‘center’ | 10 |
还有许多参数可用来设置列数,背景色等。当图中包含不同类型的数据,添加图例时,需要为不同类型的数据设置标签分别添加到axes中,然后在legend函数中设置图例的样式。
结果如图所示:
在计算距离时,特征值大的属性对结果的影响越大。如果认为所有的属性重要程度相同,可以对所有的属性进行归一化,使得它们的取值范围相同。使用最大最小值归一化,能够使结果在[0,1]区间。
#归一化特征值
def autoNorm(dataSet):
minVals = dataSet.min(0) #从每列中取最小值
maxVals = dataSet.max(0)
ranges = maxVals - minVals
rows = dataSet.shape[0] #数据集的行数
normDataSet = dataSet - tile(minVals, (rows, 1)) #分子
normDataSet = normDataSet / tile(ranges, (rows, 1)) #归一化后结果
return normDataSet, ranges, minVals
使用错误率评估分类器的性能,即分类器给出的错误结果次数除以测试数据的总数。通常从已有数据中选择90%的数据作为训练样本训练分类器,其余10%的样本测试分类器,检测分类器的正确率。
在函数中,使用errorCount对错误分类结果计数,除以预测的总数即可得到错误率。
def datingClassTest(filename, holdRatio = 0.10):
datingDataMat, datinglabels = file2matrix(filename) #解析文本文件
normDataSet, ranges, minVals = autoNorm(datingDataMat)
rows = datingDataMat.shape[0]
numTestVecs = int(rows * holdRatio) #得到验证集的数量
#预测并对错误结果计数
errorCount = 0.0
for i in range(numTestVecs):
classifierResult = kNN0(datingDataMat[i, ], normDataSet[numTestVecs :,], datinglabels[numTestVecs :], 3)
#打印预测结果和实际结果
print('kNN 分类结果为%d,实际结果为%d' % (classifierResult, datinglabels[i]))
if classifierResult != datinglabels[i]:
errorCount += 1
print('kNN分类器的错误率为%f' % (errorCount / rows))
结果如下所示:
kNN 分类结果为3,实际结果为1
kNN 分类结果为3,实际结果为2
kNN 分类结果为3,实际结果为3
kNN 分类结果为3,实际结果为3
kNN 分类结果为3,实际结果为3
...
kNN 分类结果为3,实际结果为2
kNN 分类结果为3,实际结果为3
kNN 分类结果为3,实际结果为3
kNN分类器的错误率为0.061000
使用kNN分类器分类。当输入某个人的信息时,给出对该人喜欢程度的预测值。
#约会网站预测函数
def classifyPerson():
#结果列表,分类结果和标签的对应关系,根据file2matrix中的dicFromLabels构造,标签按照从前到后的顺序对应1,2,3
resultList = ['largeDoses', 'smallDoses', 'didntLike']
gameTime = float(input('玩游戏所占时间百分比:'))
flyMiles = float(input('每年的飞行里程数:'))
iceCream = float(input('每年冰淇淋公升数:'))
filename = 'C:/Users/hp/Desktop/SH/python/MLInAction/machinelearninginaction/Ch02/datingTestSet.txt'
datingDataMat, datinglabels = file2matrix(filename)
normDataSet, ranges, minVals = autoNorm(datingDataMat)
inArr = array([flyMiles, gameTime, iceCream]) #分类器的输入数据
classifierResult = kNN0((inArr - minVals) / ranges, normDataSet, datinglabels, 3) #分类时对输入数据归一化处理
print('喜欢这个人的程度为:%s' % (resultList[classifierResult -1]))
结果如下所示:
玩游戏所占时间百分比:10
每年的飞行里程数:10000
每年冰淇淋公升数:0.5
喜欢这个人的程度为:smallDoses
识别数字0-9。所有的图片被处理为宽高为32像素*32像素的黑白图像。再将图像转换为文本文件,0为空白部分,1为字迹部分。
将32*32的二进制图像矩阵转换为1*1024的向量,便于使用分类器处理分类。
给定文件,循环读出文件的32行,并将每行的前32个字符存在NumPy数组中并返回数组。
def img2vector(filename):
returnVect = zeros((1,1024))
fr = open(filename, 'r')
try:
for i in range(32):
lineStr = fr.readline()
for j in range(32):
returnVect[0, i * 32 + j] = int(lineStr[j])
return returnVect
finally:
fr.close()
import os
def handwritingClassTest(trainPath, testPath, k):
#处理训练集,将所有的训练样本处理为特征矩阵和对应的类别标签
trainFileList = os.listdir(trainPath) #得到该路径下的文件名列表
mTrain = len(trainFileList) #得到文件数量
#将文件解析到训练特征矩阵和标签
trainDataMat = zeros((mTrain, 1024)) #训练特征矩阵
trainLabels = []
for i in range(mTrain):
trainFilePath = trainPath + '/' + trainFileList[i]
trainDataMat[i, :] = img2vector(trainFilePath)
trainLabel = int(trainFileList[i].split('_')[0])
trainLabels.append(trainLabel)
#处理测试集,计算错误率
testFilelist = os.listdir(testPath)
mTest = len(testFilelist)
errorCount = 0.0
for i in range(mTest):
testFilePath = testPath + '/' + testFilelist[i]
VectorUnderTest = img2vector(testFilePath)
testLabel = int(testFilelist[i].split('_')[0])
classifierResult = kNN0(VectorUnderTest, trainDataMat, trainLabels, k)
#打印预测结果和实际结果
# print('kNN 分类结果为%d,实际结果为%d' % (classifierResult, writeLabel))
if classifierResult != testLabel:
errorCount += 1
print('kNN 分类结果为%d,实际结果为%d' % (classifierResult, testLabel))
print('手写字符识别kNN分类器的错误率为%f' % (errorCount / mTest))
os.listdir(path=’.’)返回给定path下所有条目name构成的list。list以任意的顺序排列,不包括’.’和’..’。如果path为字节,返回的文件名也为字节,否则返回的文件名为字符串,fsencode()函数将字符串文件名编码为字节。scandir()函数能够返回目录的条目和文件属性信息。
打印错误分类的结果和错误率如下:
kNN 分类结果为7,实际结果为1
kNN 分类结果为9,实际结果为3
kNN 分类结果为9,实际结果为3
kNN 分类结果为3,实际结果为5
kNN 分类结果为6,实际结果为5
kNN 分类结果为6,实际结果为8
kNN 分类结果为3,实际结果为8
kNN 分类结果为1,实际结果为8
kNN 分类结果为1,实际结果为8
kNN 分类结果为1,实际结果为9
kNN 分类结果为7,实际结果为9
手写字符识别kNN分类器的错误率为0.011628
如果要在IDLE中调用该函数,首先使用os.chdir(‘path’)函数将目录改为kNN.py所在目录,然后导入kNN,调用handwritingClassTest函数。
>>> os.chdir('D:\Python\ML\kNN')
>>> import kNN
>>> kNN.handwritingClassTest(kNN.trainPath, kNN.validPath, 3)
kNN 分类结果为7,实际结果为1
kNN 分类结果为9,实际结果为3
kNN 分类结果为9,实际结果为3
kNN 分类结果为3,实际结果为5
kNN 分类结果为6,实际结果为5
kNN 分类结果为6,实际结果为8
kNN 分类结果为3,实际结果为8
kNN 分类结果为1,实际结果为8
kNN 分类结果为1,实际结果为8
kNN 分类结果为1,实际结果为9
kNN 分类结果为7,实际结果为9
手写字符识别kNN分类器的错误率为0.011628
kNN算法简单有效,不用训练。可以直接根据训练数据集预测待分类样本。但必须保存全部数据集,占用大量的存储空间,需要计算每个训练样本和待分类样本的距离,执行效率低。并且无法给出数据的内在含义。
k决策树是kNN的优化版,可以节省大量计算开销。