import numpy as np
import struct
import matplotlib.pyplot as plt

# 训练集文件
train_images_idx3_ubyte_file = 'train-images-idx3-ubyte'
# 训练集标签文件
train_labels_idx1_ubyte_file = 'train-labels-idx1-ubyte'
# 测试集文件
test_images_idx3_ubyte_file = 't10k-images-idx3-ubyte'
# 测试集标签文件
test_labels_idx1_ubyte_file = 't10k-labels-idx1-ubyte'

def decode_idx3_ubyte(idx3_ubyte_file):
    bin_data = open(idx3_ubyte_file, 'rb').read()
    offset = 0
    fmt_header = '>iiii'
    magic_number, num_images, num_rows, num_cols = struct.unpack_from(fmt_header, bin_data, offset)
    image_size = num_rows * num_cols
    offset += struct.calcsize(fmt_header)
    fmt_image = '>' + str(image_size) + 'B'
    images = np.empty((num_images, num_rows, num_cols))
    for i in range(num_images):
        images[i] = np.array(struct.unpack_from(fmt_image, bin_data, offset)).reshape((num_rows, num_cols))
        offset += struct.calcsize(fmt_image)
    return images

def decode_idx1_ubyte(idx1_ubyte_file):
    bin_data = open(idx1_ubyte_file, 'rb').read()
    offset = 0
    fmt_header = '>ii'
    magic_number, num_images = struct.unpack_from(fmt_header, bin_data, offset)
    offset += struct.calcsize(fmt_header)
    fmt_image = '>B'
    labels = np.empty(num_images)
    for i in range(num_images):
        labels[i] = struct.unpack_from(fmt_image, bin_data, offset)[0]
        offset += struct.calcsize(fmt_image)
    return labels

def load_train_images(idx_ubyte_file=train_images_idx3_ubyte_file):
    return decode_idx3_ubyte(idx_ubyte_file)

def load_train_labels(idx_ubyte_file=train_labels_idx1_ubyte_file):
    return decode_idx1_ubyte(idx_ubyte_file)

def load_test_images(idx_ubyte_file=test_images_idx3_ubyte_file):
    return decode_idx3_ubyte(idx_ubyte_file)

def load_test_labels(idx_ubyte_file=test_labels_idx1_ubyte_file):
    return decode_idx1_ubyte(idx_ubyte_file)

# 加载数据集
train_data = load_train_images()
train_label = load_train_labels()
#test_data = load_test_images()
#test_label = load_test_labels()

%matplotlib inline

import matplotlib
import matplotlib.pyplot as plt

# 查看数据集是否加载成功
# plt.imshow(train_data[1])

# 打乱数据集
shuffle_index = np.random.permutation(60000)
train_data = train_data[shuffle_index]
train_label = train_label[shuffle_index]

# 二分类,识别是否是5
five_label = (train_label == 5)

# 使用 随机梯度下降分类器SGD
from sklearn.linear_model import SGDClassifier
sgd_clf = SGDClassifier(random_state=42)

# 我们加载的图片shape是(60000,28,28)的
# SGD分类器需要reshape为(60000,784)
train_data = train_data.reshape(60000, 784)

sgd_clf.fit(train_data, five_label)

# 我们看看效果怎么样
ret = sgd_clf.predict(train_data)

err_cnt = 0
i = 0
while i < 60000:
    if ret[i] != five_label[i]:
        err_cnt += 1
    i += 1
print("准确:", (1 - err_cnt/60000) * 100, "%")

# 单看这个二类分类的结果的话,拟合是挺好的
# 但是有一个问题我们要考虑一下
# 这个结果不可信,为什么不可信,我们先来看一下five_label里面是5的个数

five_cnt=0
for i in five_label:
    if i == True:
        five_cnt += 1
print("5 个数:", five_cnt)

# 我们可以看到,5的个数才5421个
# 意思就是如果分类器预测的全不是5,那么准确率仍然有90%左右
# 这么来看的话,我们不能单纯的相信这个准确率了
# 我们要采取一种措施,能够得到可信的结果

# 对分类器来说,一个好得多的性能评估指标是混淆矩阵
# 混淆矩阵的大体思路就是,输出类别A被分类成B的次数

from sklearn.model_selection import cross_val_predict
ret = cross_val_predict(sgd_clf, train_data, five_label, cv=3)

from sklearn.metrics import confusion_matrix
print("混淆矩阵:", confusion_matrix(five_label, ret))

# 通过混淆矩阵,我们计算准确和召回的公式如下
# 准确 = TP / (TP + FP)
# 召回 = TP / (TP + FN)

# sklearn 提供了方法可以直接帮助我们计算准确和召回
from sklearn.metrics import precision_score, recall_score
print("准确率:", precision_score(five_label, ret))
print("召回率:", recall_score(five_label, ret))

# 我们要知道这里计算的准确和上面那个计算的准确的区别
# 上面计算的那个准确,我们只是统计的和label不一样的数据占的总体的比例,这个统计太笼统,不容易发现问题
# 这里我们把性能指标分成了准确和召回两个指标,准确和召回两个指标,能够更加细致的反应出模型的效果

# 如果我们想要比较两个模型的效果,可以使用F1 score
# F1值是准确率和召回率的调和平均,普通的平均值平等的看待所有的值
# 调和平均会给小的值更大的权重
# 如果想要分类器得到一个高的F1值,需要准确率和召回率同时高
from sklearn.metrics import f1_score
print("f1:", f1_score(five_label, ret))

# 有些时候,我们不需要F1值
# 比如,如果我们在训练一个儿童不良视频过滤模型的时候,我们可能要求准确要尽量100%,召回可以低一些
# 再比如,如果我们在训练一个防盗视频监控,可能召回要高一些,准确要低一些,这样能尽量抓到所有的盗贼

# 人工调整 准确率/召回率 的阈值
# 我们刚刚举例了两种情况下,我们可能对 准确率 和 召回率 的要求是不一样的
# 有的时候,我们对准确率要求高,有的时候我们对召回率要求比较高
# 刚刚我们使用的SGD模型,是可以调整准确率和召回率的
# 我们可以让SGD产出一个决策分数,然后我们手动调整准确和召回的阈值,来控制准确和召回
five_scores = cross_val_predict(sgd_clf, train_data, five_label, cv=3, method="decision_function")
threshold = 0
ret = (five_scores > threshold)
print("准确:", precision_score(five_label, ret))
print("召回:", recall_score(five_label, ret))

# 上面这个five_scores就是sgd模型给的决策分数
# threshold就是我们的阈值
# 可以看到,这个准确和召回和上面的一样,是因为阈值都是0的原因
# 我们可以把阈值调高一些看看有什么变化
threshold = 50000
ret = (five_scores > threshold)
print("准确:", precision_score(five_label, ret))
print("召回:", recall_score(five_label, ret))

# 我们可以看到,这个时候准确提升了很多,自然,召回下降了也很多
# 通过这种方法,我们就可以手动调整模型的准召

# sklearn给我们提供了一个方法
# 可以让我们获得每个阈值下的准确和召回
# 我们可以将这个可视化看一下

from sklearn.metrics import precision_recall_curve
precisions, recalls, thresholds = precision_recall_curve(five_label, five_scores)
plt.plot(thresholds, precisions[:-1], "b--", label="precision")
plt.plot(thresholds, recalls[:-1], "g--", label="recall")
plt.xlabel("threshold")
plt.legend(loc="upper left")
plt.ylim([0, 1])
plt.show()

# 通过这个可视化,我们可以清晰的看到不同阈值时不同的准确和召回
# 注意:阈值时0的时候,上面的那个不一定是交点的位置,可以使用画图工具的直线圈一下就能看清楚了

# 除了上面这个方法,还有一个方法可以让我们选择一个好的 准确/召回 的折中
# 我们可以画出 准确率/召回率 的曲线

plt.plot(recalls[:-1], precisions[:-1])
plt.show()

# 我们可以看到,在召回率80%左右的时候,准确率开始急剧下降
# 我们可以根据我们的需要,去选择一个合适的点,获得我们想要的效果

# 上面这个 准确率/召回率 曲线,我们可以叫做 PR曲线

# 除了 PR 曲线之外,还有一个ROC曲线,也是常用来做 准确率和召回率 折中的
# ROC曲线是 真正例率/假正例率
# 对于ROC,参考 https://blog.csdn.net/tkingreturn/article/details/17640599

原创文章,转载请注明地址: 文章地址