Skip to main content

分类算法

KNN算法

这个算法既可以解决分类问题,也可以用于回归问题,但工业上用于分类的情况更多。

KNN 先记录所有已知数据,再利用一个距离函数,

找出已知数据中距离未知事件最近的 K 组数据,

最后按照这 K 组数据里最常见的类别预测该事件。

from sklearn.neighbors import KNeighborsClassifier
import numpy as np

# 创建一些示例数据
X = np.array([[1, 2], [2, 3], [2, 5], [3, 2], [3, 3], [4, 5]]) # 特征
y = np.array([0, 0, 1, 0, 1, 1]) # 目标标签

# 创建K-最近邻分类器
k = 3 # 选择K的值
model = KNeighborsClassifier(n_neighbors=k).fit(X, y)

# 预测新数据点
new_data_point = np.array([[3, 4]]) # 要预测的新数据点

# .predicts()方法返回一个数组,数组中包含了预测的类别
predicted_class = model.predict(new_data_point)

print("预测类别:", predicted_class)

简单实战

from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split

# 导入鸢尾花数据库
from sklearn.datasets import load_iris

# 加载数据集,数据集包含数据的特征、标签、类别等许多信息
iris = load_iris()
# 获取数据特征(即花的高度、宽度等)
iris_X = iris.data
# 获取数据标签(即花的品种,用0、1、2代替)
iris_y = iris.target
print(len(iris_X)) # 查看数据集的大小

# # 获取前2条数据,从0开始到2结束,不包括2。写法1
# print(iris_X[0:2])

# # 获取前2条数据,从0开始到2结束,不包括2。写法2,省略0
# print(iris_X[:2])

# # 获取前2条数据,从0开始到2结束,不包括2。写法3,省略0
# print(iris_X[:2,:])

# # 获取前2条数据,从0开始到2结束,不包括2。写法4,省略0,只取第一列
# print(iris_X[:2,0])

# # 查看花的类别
# print(iris_y)
# # 查看花的数据
# print(iris_X)
# # 合在一起查看
# print(list(zip(iris_X,iris_y)))

# 把数据打乱,并分成测试数据和训练数据,test_size是测试数据的比例,0.3表示为30%
X_train, X_test, y_train, y_test = train_test_split(iris_X, iris_y, test_size=0.3)

'''
train_test_split详解

此方法会将数据和标签均分成两部分并打乱,一部分用于训练,一部分用于测试。

所以返回的数据有4个,我们用1、2、3、4给他们做上记号.

数据X [------70%---(1)-- | -30%(2)-]
标签y [------70%---(3)-- | -30%(4)-]

与上图对应,依次是:

训练的数据X(1), 测试的数据X(2),
↑↓ ↑↓
训练的标签y(3), 测试的标签y(4)

用(1)、(3)喂出一个模型

让模型预测(2),获得预测结果

将预测结果与(4)进行比较来测试模型的准确率
'''

# 查看训练数据,已经被随机打乱了
# print(y_train)
# 实例化KNN分类器
knn = KNeighborsClassifier()
# .fit()方法用于训练模型,即让模型从数据中学习
knn.fit(X_train, y_train)
# .predicts()方法返回一个数组,数组中包含了预测的类别
print(knn.predict(X_test))
# 查看真实数据
print(y_test)

效果评估

right = 0
error = 0
for i in zip(knn.predict(X_test),y_test):
#print(i)
if i[0] == i[1]:
right +=1
else:
error +=1
print(right,error)
print('正确率:{}%'.format(right/(right+error)*100))

效果评估的改进

print('正确率:{}%'.format(knn.score(X_test,y_test)*100))

# 正确率:100.0%

实时分类器

描述

KNN 算法先记录所有已知数据,再利用一个距离函数,找出已知数据中距离未知事件最近的 K 组数据,最后按照这 K 组数据里最常见的类别预测该事件。可以解决分类问题。

请编写一段程序读取用户的摄像头,让用户通过按键或点击的方式实时训练并查看当前摄像头的预测结果。

题解

'''
新建`.py`并将下方代码复制进去,确保已经安装好了下方的模块库。

pip install opencv-python
pip install tensorflow


1. 等待模型加载(加载完成后会弹出摄像头)

2. 按下键盘的A则获取当前摄像头截图加入A训练集

3. 以此类推添加B、C训练集

4. 观察屏幕输出的预测结果
'''
import cv2
import tensorflow as tf
from tensorflow.keras.applications import MobileNet
from tensorflow.keras.preprocessing import image
from tensorflow.keras.applications.mobilenet import preprocess_input
from tensorflow.keras.models import Model
import numpy as np


# 定义KNN分类器类
class KNNClassifier:
def __init__(self):
# 初始化存储不同类别样本特征的字典,包含A、B、C三个类别
self.examples = {"A": [], "B": [], "C": []}

def add_example(self, activation, class_id):
# 将新的特征向量添加到对应类别的样本集中
self.examples[class_id].append(activation)

def predict_class(self, activation):
# 初始化存储各类别距离的字典
distances = {}
# 遍历所有类别及其对应的样本
for class_id, examples in self.examples.items():
# 计算当前特征向量与该类别所有样本的平均欧氏距离
distances[class_id] = np.mean(
# np.linalg.norm 计算向量范数 范数是衡量向量"大小"或"长度"的数学概念。最常用的是2范数(也叫欧氏范数),就是我们熟悉的向量长度公式。
[np.linalg.norm(act - activation) for act in examples]
)

# 找到距离最小的类别作为预测结果
predicted_class = min(distances, key=distances.get)
# 根据距离计算置信度,距离越小置信度越高
confidence = 1 / (1 + distances[predicted_class])
# 返回预测类别和置信度
return predicted_class, confidence


def main():
# 创建KNN分类器实例
classifier = KNNClassifier()
# 初始化摄像头,参数0表示默认摄像头
webcam = cv2.VideoCapture(0)

print("Loading MobileNet...")
# 加载预训练的MobileNet模型,使用ImageNet权重
base_model = tf.keras.applications.MobileNet(weights="imagenet")
# 创建特征提取模型,输出conv_preds层的特征(用于分类的高级特征)
model = Model(
inputs=base_model.input, outputs=base_model.get_layer("conv_preds").output
)

print("Successfully loaded model")

# 定义三个分类类别
classes = ["A", "B", "C"]

# 主循环,持续处理摄像头画面
while True:
# 从摄像头读取一帧图像
ret, frame = webcam.read()
# 将图像调整为224x224像素,这是MobileNet的输入尺寸要求
frame = cv2.resize(frame, (224, 224))
# 将OpenCV图像转换为Keras可处理的数组格式
img = image.img_to_array(frame)
# 增加批次维度,从(224,224,3)变为(1,224,224,3)
img = np.expand_dims(img, axis=0)
# 使用MobileNet专用的预处理函数,标准化像素值
img = preprocess_input(img)

# 通过模型提取图像的高级特征向量
activation = model.predict(img)

# 检测键盘输入,等待1毫秒
key = cv2.waitKey(1)
# 如果按下'a'键,将当前特征添加到类别A的训练样本中
if key == ord("a"):
classifier.add_example(activation, "A")
# 如果按下'b'键,将当前特征添加到类别B的训练样本中
elif key == ord("b"):
classifier.add_example(activation, "B")
# 如果按下'c'键,将当前特征添加到类别C的训练样本中
elif key == ord("c"):
classifier.add_example(activation, "C")

# 只有当至少有一个A类别的样本时才进行预测
if len(classifier.examples["A"]) > 0:
# 使用KNN算法预测当前图像的类别
predicted_class, confidence = classifier.predict_class(activation)
# 打印预测结果和置信度
print(f"Prediction: {predicted_class}, Confidence: {confidence}")

# 显示摄像头画面窗口
cv2.imshow("Webcam", frame)

# 如果按下ESC键(ASCII码27),退出循环
if key == 27: # ESC key to break from the loop
break

# 释放摄像头资源
webcam.release()
# 关闭所有OpenCV窗口
cv2.destroyAllWindows()


# 运行主函数
main()

朴素贝叶斯

这个算法是建立在贝叶斯理论上的分类方法。

它的假设条件是自变量之间相互独立。

简言之,朴素贝叶斯假定某一特征的出现与其它特征无关。即给定类别,特征之间没有相关性。这个假设是“朴素”的来源。

比如说,如果一个水果它是红色的,圆状的,直径大概 7cm 左右,我们可能猜测它为苹果。即使这些特征之间存在一定关系,在朴素贝叶斯算法中我们都认为红色,圆状和直径在判断一个水果是苹果的可能性上是相互独立的。

一个二分类的案例假设:

我今天收到了 100 封邮件,其中有 80 封是垃圾邮件,20 封是正常邮件。

P(垃圾邮件) = 80/100 = 0.8
P(正常邮件) = 20/100 = 0.2

我选定了一些词作为特征,这些词可能出现在邮件中,也可能不出现。这些词有“免费”,“恭喜”,“辛苦”等。

我发现垃圾邮件中有 20 封含有“免费”这个词,50 封含有“恭喜”这个词,0 封含有“辛苦”这个词。

P(免费|垃圾邮件) = 20/80 = 0.25
P(恭喜|垃圾邮件) = 50/80 = 0.625
P(辛苦|垃圾邮件) = 0/80 = 0

正常邮件中有 5 封含有“免费”这个词。6 封含有“恭喜”这个词,2 封含有“辛苦”这个词。

P(免费|正常邮件) = 5/20 = 0.25
P(恭喜|正常邮件) = 6/20 = 0.3
P(辛苦|正常邮件) = 2/20 = 0.1

现在我收到了一封邮件,这封邮件内容为:“恭喜您获得了一次免费的机会”,我想知道这封邮件是垃圾邮件的概率是多少?

P(垃圾邮件|免费,恭喜) = P(免费|垃圾邮件)* P(恭喜|垃圾邮件)* P(垃圾邮件)= 0.25 * 0.625 * 0.8 = 0.125

P(正常邮件|免费,恭喜) = P(免费|正常邮件)* P(恭喜|正常邮件)* P(正常邮件)= 0.25 * 0.3 * 0.2 = 0.015

因为 P(垃圾邮件|免费,恭喜) > P(正常邮件|免费,恭喜),所以这封邮件被判定为垃圾邮件。

如果狡猾的垃圾邮件制造者把邮件内容改为:“恭喜您获得了一次免费的机会,辛苦您动动手指参加我们的免费活动”,那么这封邮件被判定为垃圾邮件的概率就会变成 0,因为“辛苦”这个词在正常邮件中有出现,在垃圾邮件中没有出现。

改进:拉普拉斯平滑法

在每个关键词上人为的增加一个出现的次数,这样就不会出现概率为 0 的情况了。(下面的公式免费的平方表示这个关键词出现 2 次)

P(垃圾邮件|免费,恭喜,辛苦) = P(免费|垃圾邮件)* P(恭喜|垃圾邮件)* P(辛苦|垃圾邮件)* P(垃圾邮件)= (20+1/80)² * (50+1/80) * (0+1/80) * 0.8 = 0.0351421875

P(正常邮件|免费,恭喜,辛苦) = P(免费|正常邮件)* P(恭喜|正常邮件)* P(辛苦|正常邮件)* P(正常邮件)= (5+1/20)² * (6+1/20) * (2+1/20) * 0.2 =0.012885

# 参考答案
import numpy as np

class NaiveBayes:
def __init__(self):
self.class_probs = {} # 存储每个类别的先验概率 P(c)
self.word_probs = {} # 存储每个类别中单词的条件概率 P(w|c)
self.vocab = set() # 保存所有出现的单词构成的词汇表
self.smooth = 1 # 拉普拉斯平滑参数

def fit(self, X, y):
# 获取唯一类别和其数量
classes, class_counts = np.unique(y, return_counts=True)
self.class_probs = {label: count / len(y) for label, count in zip(classes, class_counts)} # 先验概率

# 初始化词汇表和词频统计
word_count = {label: {} for label in classes} # 每个类别的词频表
class_word_totals = {label: 0 for label in classes} # 每个类别单词总数

# 遍历每个样本进行分词和统计
for text, label in zip(X, y):
words = text.split(" ")
for word in words:
self.vocab.add(word) # 添加到词汇表
if word not in word_count[label]:
word_count[label][word] = 0
word_count[label][word] += 1 # 更新词频
class_word_totals[label] += 1 # 当前类别单词总数加1

# 计算条件概率 P(w|c) 加拉普拉斯平滑
vocab_size = len(self.vocab) # 词汇表大小
self.word_probs = {label: {} for label in classes}
for label in classes:
for word in self.vocab:
count = word_count[label].get(word, 0) # 获取词频,若未出现则为0
self.word_probs[label][word] = (count + self.smooth) / (
class_word_totals[label] + vocab_size * self.smooth
)

def predict(self, X):
predictions = [] # 存储所有样本的预测结果
for text in X:
words = text.split(" ")
class_scores = {} # 存储每个类别的后验概率

# 计算后验概率 P(c|w1,w2,...,wn)
for label in self.class_probs:
class_scores[label] = self.class_probs[label]
for word in words:
if word in self.word_probs[label]: # 如果词在词汇表中
class_scores[label] *= self.word_probs[label][word]
else:
# 若单词未在词汇表中,跳过计算
class_scores[label] *= 1/len(self.vocab)

# 选择后验概率最大的类别作为预测结果
predictions.append(max(class_scores, key=class_scores.get))

return predictions

def score(self, X, y):
predictions = self.predict(X)
return np.mean(predictions == y)


# 数据
data = np.array([
("恭喜 你 赢得 了 大奖 !","诈骗"),
("请 立即 更新 您 的 账户 信息","诈骗"),
("您的 账户 存在 异常 ,请 尽快 处理","诈骗"),
("这是 您 的 账单 ,请 查看","正常"),
("您的 订单 已 发货","正常"),
("请 确认 您 的 注册 信息","正常"),
("您 有 新的 消息 ,请 查看","正常"),
("点击 此 链接 获取 优惠券","诈骗"),
("您的 账户 已 被 锁定 ,请 立即 联系","诈骗"),
("恭喜 您 获得 免费 试用 !","诈骗"),
("请 不要 分享 您 的 密码","正常"),
("您的 订阅 即将 到期 ,请 续费","正常"),
("您 有 未 读 邮件 ,请 查看","正常"),
("立即 行动 ,获取 限时 优惠 !","诈骗"),
("您的 信用卡 信息 需要 更新","诈骗"),
])
X = data[:, 0] # 文本数据
y = data[:, 1] # 标签数据

# 创建模型并训练
model = NaiveBayes()
model.fit(X, y)

# 输出结果,比较预测类别与实际类别
print(model.score(X, y))


使用sklearn模块完成

from sklearn.naive_bayes import GaussianNB
import numpy as np

# 创建一些示例数据
X = np.array([[1], [2], [3], [4], [5]]) # 特征
y = np.array([0, 0, 1, 1, 1]) # 目标标签

# 创建朴素贝叶斯分类器 (高斯朴素贝叶斯)
model = GaussianNB()

# .fit() 方法用于拟合模型
model.fit(X, y)

# 要预测的新数据点
new_data_point = np.array([[6]])

# .predict() 方法返回预测的类别
predicted_class = model.predict(new_data_point)
# .predict_proba() 方法返回每个类别的概率
predicted_proba = model.predict_proba(new_data_point)

print("预测类别:", predicted_class)
print("类别概率:", predicted_proba)

简单示例

from sklearn.naive_bayes import GaussianNB
from sklearn.model_selection import train_test_split
import sklearn.datasets
# 加载数据
data = sklearn.datasets.load_iris()
# .data 属性包含特征
X = data.data
# .target 属性包含目标标签
y = data.target
# 划分数据集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
# 创建朴素贝叶斯分类器 (高斯朴素贝叶斯)
model = GaussianNB()
# 拟合模型
model.fit(X_train, y_train)

效果评估

from sklearn.metrics import accuracy_score

# 计算准确率
accuracy = accuracy_score(y_test, model.predict(X_test))
accuracy

查看分类错误的样本信息

import pandas as pd

# 把测试数据、目标标签、预测结果合并到一起
# pd.DataFrame()函数用于创建DataFrame
# pd.concat()函数用于合并多个DataFrame
# axis=1 表示按列合并
df = pd.concat(
[pd.DataFrame(X_test,columns=data.feature_names),
pd.DataFrame(y_test,columns=['target']),
pd.DataFrame(model.predict(X_test),columns=['predict'])
],axis=1 )

# 筛选target列与predict列不相等的数据
df.loc[df['target']!=df['predict']]

支持向量机

这是一个分类算法。

在这个算法中我们将每一个数据作为一个点在一个 n 维空间上作图(n 是特征数),每一个特征值就代表对应坐标值的大小。

比如说我们有两个特征:一个人的身高和发长。我们可以将这两个变量在一个二维空间上作图,图上的每个点都有两个坐标值(这些坐标轴也叫做支持向量)。

在这个示例中,我们首先导入了 scikit-learn 库中的 svm 模块以及 NumPy 库。然后,我们创建了一些示例数据 X 和 y,其中 X 是特征,y 是目标标签。

接下来,我们创建了一个 SVM 分类器,使用线性核函数(kernel='linear')。

然后,我们使用 fit 方法拟合了模型,并在新数据点上使用 predict 方法进行预测,以获取新数据点的类别。

from sklearn import svm
import numpy as np

# 创建一些示例数据
X = np.array([[1, 2], [2, 3], [2, 5], [3, 2], [3, 3], [4, 5]]) # 特征
y = np.array([0, 0, 1, 0, 1, 1]) # 目标标签

# 创建SVM分类器
model = svm.SVC(kernel="linear")

# 拟合模型
model.fit(X, y)

# 要预测的新数据点
new_data_point = np.array([[3, 4]])
# 预测新数据点的类别
predicted_class = model.predict(new_data_point)

print("预测类别:", predicted_class)

简单示例

from sklearn.model_selection import train_test_split
from sklearn.datasets import load_breast_cancer
from sklearn import svm

# 导入一个数据量较多的数据,乳腺癌
iris = load_breast_cancer()
# 获取数据集
X, y = iris.data ,iris.target

# 直接使用数据
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
# 创建SVM分类器
clf = svm.SVC()
# 拟合模型
clf.fit(X_train, y_train)

效果评估

# .score()返回的是准确度
# 和之前学习的accuracy_score、cross_val_score一样都是检测准确度的方法
print('预测是准确度为{}%'.format(clf.score(X_test, y_test)*100))

数据 normalization

from sklearn import preprocessing
# normalization是指将数据按比例缩放,使之落入一个小的特定区间
# 先标准化数据再使用数据
X2 = preprocessing.scale(X) # normalization step
# print(X2)
X2_train, X2_test, y2_train, y2_test = train_test_split(X2, y, test_size=0.3)

clf2 = svm.SVC()
clf2.fit(X2_train, y2_train)
print('预测是准确度为{}%'.format(clf2.score(X2_test, y2_test)*100))
# 简单验证后发现处理后的数据显然表现更好,预测是准确度为98.83040935672514%

from sklearn import model_selection

# 但是并不是每次验证得到的结果都是一致的,验证具有随机性,因此需要交叉验证
# 把数据分成5份,分别做测试集,提取分数并求平均值,显然处理后的数据表现更好
print(model_selection.cross_validate(clf,X_test, y_test,cv=5)['test_score'].mean())
print(model_selection.cross_validate(clf2,X2_test, y2_test,cv=5)['test_score'].mean())
'''
0.8947899159663866 # 未处理的数据
0.9825210084033614 # 处理后的数据
'''

防止过拟合

from __future__ import print_function
from sklearn.model_selection import learning_curve
from sklearn.datasets import load_digits
from sklearn.svm import SVC
import matplotlib.pyplot as plt
import numpy as np
# 载入数据
digits = load_digits()
X = digits.data
y = digits.target

# learning_curve()函数用于计算在不同大小的训练集上训练得到的模型在验证集上的得分情况
# 进而分析模型是否过拟合或者欠拟合
# 这里我们使用SVC模型,gamma=0.01
# gamma参数用于控制模型的复杂度,gamma越大,模型越复杂,越容易过拟合
# 通过train_sizes参数来指定训练集的大小
# 通过cv参数来指定交叉验证的次数
# 通过scoring参数来指定评价指标,这里使用的是负均方误差
train_sizes, train_loss, test_loss= learning_curve(
SVC(gamma=0.01), X, y, cv=10, scoring="neg_mean_squared_error",
train_sizes=[0.1, 0.25, 0.5, 0.75, 1])

# 计算平均值和标准差
train_loss_mean = -np.mean(train_loss, axis=1)
test_loss_mean = -np.mean(test_loss, axis=1)

# 绘制曲线
# 这里我们使用的是负均方误差,因此数值越小,模型越好
# ro-表示红色圆形实线,go-表示绿色圆形实线
plt.plot(train_sizes, train_loss_mean, 'ro-',
label="Training")
plt.plot(train_sizes, test_loss_mean, 'go-',
label="test-Cross-validation")
plt.xlabel("Training examples")
plt.ylabel("Loss")
# 显示图例,loc="best"表示自动选择最佳位置
plt.legend(loc="best")
plt.show()
# 这个图表示:
# 刚开始只有200个数据的时候,误差很大,这是因为数据量太少,模型无法很好地拟合数据
# 随着数据量的增加,误差逐渐减小,这是因为模型可以更好地拟合数据
# 但是随着数据集进一步增加,误差反而增大了,这说明模型出现了过拟合