题目:
使用逻辑回归,识别手写数字
import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
import scipy.optimize as opt # 最小化
from scipy.io import loadmat #用于加载MATLAB格式的数据
from sklearn.metrics import classification_report
处理训练集
用于从给定的路径加载数据
def load_data(path, transpose=True):
data = loadmat(path)
X = data['X']
y = data['y']
y = y.reshape(y.shape[0])
print(type(X))
if transpose:
X = np.array([im.reshape((20,20)).T.reshape(400) for im in X])
return X, y
这段代码定义了一个名为 load_data
的函数,它用于从给定的路径加载数据
-
函数参数:
path
: 数据文件的路径。transpose
: 一个布尔值,默认为True
。表示是否需要对加载的图像数据进行转置。
-
功能:
- 使用
loadmat
函数(这个函数来自scipy.io
模块,用于加载MATLAB格式的数据)从给定的路径加载数据。 - 从加载的数据中提取键为 'X' 和 'y' 的值,分别赋给
X
和y
。这意味着数据文件中应该包含这两个键,分别代表图像数据和标签。 - 将
y
数据的形状从二维或更高维度更改为一维。y = y.reshape(y.shape[0])
- 使用
在
load_data
函数中,y = y.reshape(y.shape[0])
这行代码的作用就是将y
重新塑形,使其成为一个一维数组,其中y.shape[0]
表示y
的第一个维度的大小,即样本数量。这样做通常是为了确保y
的形状与X
的行数(样本数量)相匹配,并且便于后续处理。
4. 打印 X
的数据类型,这可能是为了调试或确认数据的加载方式。print(type(X))
如果
X
中的图像已经是 400x1 的形状,那么转置操作可能会导致错误或不必要的操作。确保你的数据格式与这段代码的预期输入相匹配。
5. 如果 transpose
参数为 True
,则对 X
中的每个图像进行转置操作。具体来说,它将每个图像从 20x20 的形状重新塑造为 400x1 的形状。这里使用了列表推导式来完成这个操作。
X = np.array([im.reshape((20,20)).T.reshape(400) for im in X])
im
是对列表(或其他可迭代对象)X
中的每一个元素的临时引用- 遍历
X
中的每一个元素(即每一个图像im
),并对每个图像执行一系列的操作:
im.reshape((20,20))
: 将当前图像im
重新塑形为一个 20x20 的二维数组。这假设原始图像可以被完整地重塑为 20x20 的形状,即它的总像素数(原始高度乘以原始宽度)等于 400。
.T
: 对 20x20 的图像执行转置操作,将其变为一个 20x20 的转置矩阵。在图像处理的上下文中,这通常意味着将图像的行和列交换。
.reshape(400)
: 将转置后的 20x20 图像再次重塑为一个一维数组,形状为 (400,)。最后,这个列表推导式生成了一个新的列表,其中包含了所有经过上述处理步骤的图像数据。然后,
np.array()
函数将这个列表转换为一个 NumPy 数组,赋值给X
。
raw_x, raw_y = load_data('ex3data1.mat')
raw_x.shape, raw_y.shape
# ((5000, 400), (5000,))
# raw_x是一个(包含5000个样本,每个样本有四百个特征值)的矩阵
#raw_y是一个包含5000个样本的标签向量
1. load_data函数被调用,并传入一个字符串参数'ex3data1.mat',这个函数的作用是从MATLAB格式的文件'ex3data1.mat'中加载数据,并返回两个变量raw_x和raw_y。
2. 是在询问raw_x和raw_y这两个数组的维度。raw_x.shape会返回raw_x数组的形状(即维度),而raw_y.shape会返回raw_y数组的形状。
- 需要知道
raw_x
有多少行和多少列,以便正确地将其划分为训练集和测试集,或者为了调整模型输入而重新塑形它。- 同样,了解
raw_y
的形状可以帮助你确定是否有必要进行任何标签编码或转换。
创建图像
def plot_an_image(image):
fig, ax = plt.subplots(figsize=(1, 1))
ax.matshow(image.reshape((20,20)), cmap=matplotlib.cm.binary)
plt.xticks(np.array([]))
plt.yticks(np.array([]))
plt.show()
这里使用了matplotlib
库来创建图像,并使用了numpy
库来处理数组
-
def plot_an_image(image):
- 这是一个函数定义,函数名为
plot_an_image
,它接受一个参数image
。
- 这是一个函数定义,函数名为
-
fig, ax = plt.subplots(figsize=(1, 1))
- 使用
matplotlib.pyplot
的subplots
函数来创建一个新的图形窗口和一个坐标轴。figsize=(1, 1)
指定了图形窗口的大小为1x1单位。
- 使用
-
ax.matshow(image.reshape((20,20)), cmap=matplotlib.cm.binary)
- 使用
ax
(坐标轴对象)的matshow
方法来显示一个矩阵。 image.reshape((20,20))
将输入的image
数组重新塑形为一个20x20的矩阵。这里假设image
的总元素数(即原始形状的长x宽)为400,这样它才能被重新塑形为20x20。cmap=matplotlib.cm.binary
指定了颜色映射为二进制,这意味着矩阵中的值将被映射为黑色和白色,其中0通常表示黑色,1表示白色。
- 使用
-
plt.xticks(np.array([]))
和plt.yticks(np.array([]))
- 这两行代码移除了x轴和y轴上的刻度标签,使得图像更加简洁
这里调用了上面创建图像的函数
随机选取raw_x
中的一个样本,显示其图像,并打印出对应的标签。👇
pick_one = np.random.randint(0, 5000)
plot_an_image(raw_x[pick_one, :])
print('this should be {}'.format(raw_y[pick_one]))
比如得到:
这段代码首先使用
numpy
的random.randint
函数从0到4999(包含0但不包含5000)之间随机选择一个整数,并将其赋值给pick_one
。接着,它调用之前定义的
plot_an_image
函数来绘制并显示raw_x
中对应pick_one
索引的样本图像。这里假设raw_x
是一个二维数组,其中每一行代表一个样本,每一列代表一个特征。因此,raw_x[pick_one, :]
将选取raw_x
中第pick_one
行的所有列,即选取一个样本。最后,它打印出
raw_y
中对应pick_one
索引的标签。这里假设raw_y
是一个一维数组,其中每个元素代表对应样本的标签或分类。print('this should be {}'.format(raw_y[pick_one]))
将输出类似"this should be 7"这样的字符串,其中7是raw_y
中第pick_one
个元素的值。综上所述,这段代码的目的是随机选取
raw_x
中的一个样本,显示其图像,并打印出对应的标签。
def sigmoid(z):
return 1 / (1 + np.exp(-z))
准备数据
X = np.insert(raw_x, 0, np.ones(raw_x.shape[0]), axis=1)
X.shape
👆(5000, 401)
最终,
X
将是一个新的数组,它与raw_x
有相同的行数,但列数多了一列。这新增的一列是全1的,并且位于所有原有列的最前面。这种操作在机器学习中有时是很有用的,特别是当你需要给每个样本添加一个偏置项时。通过将偏置项设置为1,并将其作为一个单独的特征列添加到数据集中,你可以轻松地在模型中包含一个偏置项。
多元分类
多元分类,也称为多类分类,是机器学习中的一个重要任务,其目标是将实例(或样本)划分到多个类别中的某一个。每个类别都对应一个不同的标签,且这些标签是互斥的,即一个实例只能属于一个类别。
将类别标签转换为模型可以使用的格式的方法
# 初始化一个空列表y,用于存储后面生成的子列表
y = []
# 遍历`raw_y`中的每个元素`i`
for k in range(1, 11):
# 如果`i`等于`k`,则在当前子列表中对应位置添加1;否则添加0。
y.append([1 if i==k else 0 for i in raw_y])
y = np.array([y[-1]] + y[:-1])
y.shape # (10, 5000)
y
# 最终,`y`是一个one-hot编码形式的数组,其中每一行对应一个类别标签,每一列对应一个样本。
array([[1, 1, 1, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], ..., [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 1, 1, 1]])
这段代码的目的是将原始的标签数组
raw_y
转换为一个 one-hot 编码的数组y
。在机器学习中,one-hot 编码是一种常见的将类别标签转换为模型可以使用的格式的方法。对于具有多个类别的分类问题,one-hot 编码将每个类别标签转换为一个二进制向量,其中向量的长度等于类别的数量,且只有一个位置是 1(表示该样本属于该类别),其余位置都是 0。在这个特定的例子中,假设
raw_y
是一个包含整数标签的数组,这些整数表示不同的类别(从 1 到 10)。代码的目的是创建一个新的数组y
,其中每一行对应一个类别,每一列对应raw_y
中的一个样本。如果raw_y
中的某个样本属于第k
个类别,则y
中对应第k
行的那一列将是 1,其余位置都是 0。通过这样做,代码将原始的整数标签转换为了一个二进制的、更易于模型处理的格式。例如,如果
raw_y
中的某个样本的标签是 3,那么在y
中对应的那一列将是[0, 0, 0, 1, 0, 0, 0, 0, 0, 0]
。最终,
y
的形状是(10, 5000)
,其中 10 是类别的数量,5000 是样本的数量。这种格式对于许多机器学习算法来说是非常有用的,特别是那些期望输入是 one-hot 编码的算法。
代价函数
def cost(theta, X, y):
first = y * np.log(sigmoid(X @ theta.T))
second = (1 - y) * np.log(1 - sigmoid(X @ theta.T))
return -np.mean(first + second)
def regularized_cost(theta, X, y, l):
reg = l / (2 * len(X)) * (theta[1:] ** 2).sum()
return cost(theta, X, y) + reg
梯度下降
def gradient(theta, X, y, l):
error = sigmoid(X@theta.T) - y
grad = X.T @ error / len(X)
reg = theta * l / len(X)
reg[0] = 0
return grad + reg
def logistic_regression(X, y, l=1):
theta = np.zeros(X.shape[1])
res = opt.minimize(fun = regularized_cost, x0=theta, args=(X, y, l), method='TNC', jac=gradient, options={'disp': True})
return res.x
预测分析
def predict(theta, X):
prob = sigmoid(X @ theta)
return [1 if i >= 0.5 else 0 for i in prob]
训练1维
计算 模型预测准确性的百分比
theta_0 = logistic_regression(X, y[0])
theta_0.shape
#
# y_pred中的每一列代表模型对相应样本的预测结果
y_pred = predict(theta_0, X)
# 误差
# y[0]取出了y的第一行,即第一个类别
print('Accuracy = {}'.format(np.mean(y[0] == y_pred)))
计算模型预测的第一个类别(通常是正类或者我们关心的类别)的准确性
表示模型预测准确性的百分比
# Accuracy = 0.9974
-
y[0] == y_pred
: 这个操作是逐元素地比较y[0]
和y_pred
中的元素是否相等。如果y[0]
中的某个位置是1,并且y_pred
中对应位置也是1,那么比较的结果就是True
,否则是False
。这将返回一个布尔数组,其形状与y_pred
相同。 -
np.mean(y[0] == y_pred)
: 这个操作计算了布尔数组中True
元素的比例,即计算了预测正确的样本所占的比例,从而得到了模型的准确性。 -
'Accurary = {}'.format(...)
: 这是一个字符串格式化操作,它将前面计算得到的准确性值插入到字符串中,从而得到一个包含准确性值的字符串。
训练k维
theta_k = np.array([logistic_regression(X, y[k]) for k in range(10)])
theta_k.shape
# (10, 401)
X:(5000, 401)
y:(10, 5000)
theta_k:(10,401)
# prob_matrix 是一个变量,用于存储计算出的概率矩阵
prob_matrix = sigmoid(X @ theta_k.T)
np.set_printoptions(suppress=True)
prob_matrix
array([[0.99577396, 0. , 0.00053364, ..., 0.00006469, 0.00003904, 0.00171709], [0.99834687, 0.0000001 , 0.00005602, ..., 0.00009679, 0.0000029 , 0.00008478], [0.99140084, 0. , 0.00056919, ..., 0.00000654, 0.02653221, 0.0019758 ], ..., [0.00000068, 0.04139908, 0.00320838, ..., 0.00012723, 0.00297294, 0.70789152], [0.00001843, 0.00000013, 0.00000009, ..., 0.00164835, 0.06816915, 0.8609958 ], [0.02881654, 0. , 0.00012967, ..., 0.36605901, 0.00497141, 0.14807259]])
-
np.set_printoptions(suppress=True)
: 这行代码设置了 NumPy 的打印选项,其中suppress=True
表示在打印数组时,不会显示小的浮点数(接近于零的值)。这可以使输出的概率矩阵更加简洁,避免显示大量的接近于零的值。
y_pred = np.argmax(prob_matrix, axis=1) # 返回每行最大的列索引
y_pred = np.array([10 if i == 0 else i for i in y_pred])
y_pred
# array([10, 10, 10, ..., 9, 9, 7], dtype=int64)
- 为什么这个不加1?(y_pred = np.argmax(prob_matrix, axis=1))
- 如果您的类别标签是从 0 开始的,但您不希望有任何样本被分类为第 0 个类别,那么您需要进行替换操作,将索引 0 替换为一个特殊的值(如 10)。
确保
prob_matrix
的列与您的类别标签正确对应,这样np.argmax
才能返回正确的索引。
print(classification_report(raw_y, y_pred))
y_pred
是用于存储模型预测结果的数组。这个数组是通过找到 prob_matrix
中每行最大值的索引来得到的,这些索引代表了模型对每个样本预测的类别标签。
precision recall f1-score support 1 0.95 0.99 0.97 500 2 0.95 0.92 0.93 500 3 0.95 0.91 0.93 500 4 0.95 0.95 0.95 500 5 0.92 0.92 0.92 500 6 0.97 0.98 0.97 500 7 0.95 0.95 0.95 500 8 0.93 0.92 0.92 500 9 0.92 0.92 0.92 500 10 0.97 0.99 0.98 500 accuracy 0.94 5000 macro avg 0.94 0.94 0.94 5000 weighted avg 0.94 0.94 0.94 5000
神经网络——前向传播
已经给出训练得到的theta1,theta2,通过前向传播计算得到预测结果
def load_weight(path):
data = loadmat(path)
return data['Theta1'], data['Theta2']
theta1, theta2 = load_weight('ex3weights.mat')
theta1.shape, theta2.shape
#((25, 401), (10, 26))
X, y = load_data('ex3data1.mat', transpose=False)
X = np.insert(X, 0, np.ones(X.shape[0]), axis=1)
X.shape, y.shape
#((5000, 401), (5000,))
输入层
a1 = X
z2 = a1 @ theta1.T
z2 = np.insert(z2, 0, np.ones(z2.shape[0]), axis=1)
z2.shape
# (5000, 26)
第二层
a2 = sigmoid(z2)
a2.shape
# (5000, 26)
z3 = a2 @ theta2.T
z3.shape
# (5000, 10)
输出层
a3 = sigmoid(z3)
a3.shape
# (5000, 10)
a3
array([[0.00013825, 0.0020554 , 0.00304012, ..., 0.00049102, 0.00774326, 0.99622946], [0.00058776, 0.00285027, 0.00414688, ..., 0.00292311, 0.00235617, 0.99619667], [0.00010868, 0.0038266 , 0.03058551, ..., 0.07514539, 0.0065704 , 0.93586278], ..., [0.06278247, 0.00450406, 0.03545109, ..., 0.0026367 , 0.68944816, 0.00002744], [0.00101909, 0.00073436, 0.00037856, ..., 0.01456166, 0.97598976, 0.00023337], [0.00005908, 0.00054172, 0.0000259 , ..., 0.00700508, 0.73281465, 0.09166961]])
y_pred = np.argmax(a3, axis=1)+1
y_pred
# array([10, 10, 10, ..., 9, 9, 9], dtype=int64)
在给出的代码片段
y_pred = np.argmax(a3, axis=1) + 1
中,y_pred
是一个数组,它存储了模型对每个样本的预测类别标签。这里的a3
很可能是一个经过某些变换(例如,通过 softmax 函数)后的概率矩阵,其中每一行代表一个样本在不同类别上的概率分布。
np.argmax(a3, axis=1)
的作用是沿着每一行(即每个样本的概率分布)找到最大值的索引。这个索引代表了模型对该样本预测最有可能的类别。由于np.argmax
返回的索引是从 0 开始的,如果a3
中的列对应于从 1 开始的类别标签(例如,第一列对应类别 1,第二列对应类别 2,等等),那么需要加 1 来将索引转换为正确的类别标签。简而言之,
y_pred = np.argmax(a3, axis=1) + 1
的意思是:
- 对于
a3
中的每一行(每个样本的概率分布),找到最大概率对应的列的索引。- 将这个索引加 1,以得到正确的类别标签。
这样,
y_pred
数组中的每个元素都是模型预测的样本所属类别的标签,且这些标签是从 1 开始的(而不是从 0 开始)。这在处理多类分类问题时很常见,尤其是当类别标签从 1 开始编号时。
print(classification_report(y, y_pred))
precision recall f1-score support 1 0.97 0.98 0.97 500 2 0.98 0.97 0.97 500 3 0.98 0.96 0.97 500 4 0.97 0.97 0.97 500 5 0.98 0.98 0.98 500 6 0.97 0.99 0.98 500 7 0.98 0.97 0.97 500 8 0.98 0.98 0.98 500 9 0.97 0.96 0.96 500 10 0.98 0.99 0.99 500 accuracy 0.98 5000 macro avg 0.98 0.98 0.98 5000 weighted avg 0.98 0.98 0.98 5000