搭建一个简单识别猫的神经网路
(一)数据加载部分
一、导入必要的库
import numpy as np # 是用Python进行科学计算的基本软件包。 import matplotlib.pyplot as plt # 是与H5文件中存储的数据集进行交互的常用软件包。 import h5py # 是一个著名的库,用于在Python中绘制图表。 from lr_utils import load_dataset # 一个加载资料包里面的数据的简单功能的库
lr_utils.py
代码如下,你也可以自行打开它查看:
import numpy as np import h5py def load_dataset(): train_dataset = h5py.File('datasets/train_catvnoncat.h5', "r") train_set_x_orig = np.array(train_dataset["train_set_x"][:]) # your train set features train_set_y_orig = np.array(train_dataset["train_set_y"][:]) # your train set labels test_dataset = h5py.File('datasets/test_catvnoncat.h5', "r") test_set_x_orig = np.array(test_dataset["test_set_x"][:]) # your test set features test_set_y_orig = np.array(test_dataset["test_set_y"][:]) # your test set labels classes = np.array(test_dataset["list_classes"][:]) # the list of classes train_set_y_orig = train_set_y_orig.reshape((1, train_set_y_orig.shape[0])) test_set_y_orig = test_set_y_orig.reshape((1, test_set_y_orig.shape[0])) return train_set_x_orig, train_set_y_orig, test_set_x_orig, test_set_y_orig, classes
返回值函数解析:
train_set_x_orig :保存的是训练集里面的图像数据(本训练集有209张64x64的图像)
train_set_y_orig :保存的是训练集的图像对应的分类值(【0 | 1】,0表示不是猫,1表示是猫)
test_set_x_orig :保存的是测试集里面的图像数据(本训练集有50张64x64的图像)
test_set_y_orig : 保存的是测试集的图像对应的分类值(【0 | 1】,0表示不是猫,1表示是猫)。
classes : 保存的是以bytes类型保存的两个字符串数据,数据为:[b’non-cat’, b’cat’]。
二、我们可以看一下我们加载的文件里面的图片都是些什么样子的,比如我就查看一下训练集里面的第26张图片,当然你也可以改变index的值查看一下其他的图片。
train_set_x_orig, train_set_y_orig, test_set_x_orig, test_set_y_orig, classes = load_dataset() index = 26 plt.imshow(train_set_x_orig[26]) plt.show()
也可同时查看多个图片,如下所示。进一步使用方法:可参考
plt.subplot(2,3,1) plt.imshow(train_set_x_orig[6]) plt.subplot(2,3,2) plt.imshow(train_set_x_orig[1]) plt.subplot(2,3,3) plt.imshow(train_set_x_orig[2]) plt.subplot(2,3,4) plt.imshow(train_set_x_orig[3]) plt.subplot(2,3,5) plt.imshow(train_set_x_orig[4]) plt.subplot(2,3,6) plt.imshow(train_set_x_orig[5])
现在我们可以结合一下训练集里面的数据来看一下我到底都加载了一些什么东西。
print("y=" + str(train_set_y_orig[:,index]) + ", it's a " + classes[np.squeeze(train_set_y_orig[:,index])].decode("utf-8") + " picture")
打印结果:
【拓展1】:Numpy库学习—squeeze()函数
语法:numpy.squeeze(a,axis = None)
1)a表示输入的数组; 2)axis用于指定需要删除的维度,但是指定的维度必须为单维度,否则将会报错; 3)axis的取值可为None 或 int 或 tuple of ints, 可选。若axis为空,则删除所有单维度的条目; 4)返回值:数组 5) 不会修改原数组;
作用:从数组的形状中删除单维度条目,即把shape中为1的维度去掉
场景:在机器学习和深度学习中,通常算法的结果是可以表示向量的数组(即包含两对或以上的方括号形式[[]]),如果直接利用这个数组进行画图可能显示界面为空(见后面的示例)。我们可以利用squeeze()函数将表示向量的数组转换为秩为1的数组,这样利用matplotlib库函数画图时,就可以正常的显示结果了。
例1:
例2:
结论:根据上述例1~2可知,np.squeeze()函数可以删除数组形状中的单维度条目,即把shape中为1的维度去掉,但是对非单维的维度不起作用。
例3:
例4:
例5:
例6:matplotlib画图实例
(1)无法显示图示案例
(2)正常显示图示案例
例7:
三、查看一下加载的图像数据集具体情况:
m_train = train_set_y_orig.shape[1] #训练集里图片的数量。 m_test = test_set_y_orig.shape[1] #测试集里图片的数量。 num_px = train_set_x_orig.shape[1] #训练、测试集里面的图片的宽度和高度(均为64x64)。 #现在看一看我们加载的东西的具体情况 print ("训练集的数量: m_train = " + str(m_train)) print ("测试集的数量 : m_test = " + str(m_test)) print ("每张图片的宽/高 : num_px = " + str(num_px)) print ("每张图片的大小 : (" + str(num_px) + ", " + str(num_px) + ", 3)") print ("训练集_图片的维数 : " + str(train_set_x_orig.shape)) print ("训练集_标签的维数 : " + str(train_set_y_orig.shape)) print ("测试集_图片的维数: " + str(test_set_x_orig.shape)) print ("测试集_标签的维数: " + str(test_set_y_orig.shape))
参数解释:
m_train
:训练集里图片的数量。m_test
:测试集里图片的数量。num_px
: 训练、测试集里面的图片的宽度和高度(均为64x64)。
请记住,train_set_x_orig
是一个维度为(m_train,num_px,num_px,3
)的数组。
运行结果:
训练集的数量: m_train = 209 测试集的数量 : m_test = 50 每张图片的宽/高 : num_px = 64 每张图片的大小 : (64, 64, 3) 训练集_图片的维数 : (209, 64, 64, 3) 训练集_标签的维数 : (1, 209) 测试集_图片的维数: (50, 64, 64, 3) 测试集_标签的维数: (1, 50)
四、降维处理:
为了方便,要把维度为(64,64,3)的numpy数组重新构造为(64 x 64 x 3,1)的数组,要乘以3的原因是每张图片是由64x64像素构成的,而每个像素点由(R,G,B)三原色构成的,所以要乘以3。在此之后,训练和测试数据集是一个numpy数组,【每列代表一个平坦的图像】 ,应该有 12288(特征数量)行和 209(样本数量)列。
当将形状(a,b,c,d)的矩阵X平铺成形状(b * c * d,a)的矩阵X_flatten时,可以使用以下代码:
#X_flatten = X.reshape(X.shape [0],-1).T #X.T是X的转置 #将训练集的维度降低并转置。 train_set_x_flatten = train_set_x_orig.reshape(train_set_x_orig.shape[0],-1).T #将测试集的维度降低并转置。 test_set_x_flatten = test_set_x_orig.reshape(test_set_x_orig.shape[0], -1).T
这一段意思是指把数组变为209行的矩阵(因为训练集里有209张图片),但是我懒得算列有多少,于是我就用-1告诉程序你帮我算,最后程序算出来时12288列,我再最后用一个T表示转置,这就变成了12288行,209列。测试集亦如此。
打印结果:
print ("训练集降维最后的维度: " + str(train_set_x_flatten.shape)) print ("训练集_标签的维数 : " + str(train_set_y_orig.shape)) print ("测试集降维之后的维度: " + str(test_set_x_flatten.shape)) print ("测试集_标签的维数 : " + str(test_set_y_orig.shape))
运行结果:
训练集降维最后的维度: (12288, 209) 训练集_标签的维数 : (1, 209) 测试集降维之后的维度: (12288, 50) 测试集_标签的维数 : (1, 50)
五、标准化数据集
为了表示彩色图像,必须为每个像素指定红色,绿色和蓝色通道(RGB),因此像素值实际上是从0到255范围内的三个数字的向量。机器学习中一个常见的预处理步骤是对数据集进行居中和标准化,这意味着可以减去每个示例中整个numpy数组的平均值,然后将每个示例除以整个numpy数组的标准偏差。但对于图片数据集,它更简单,更方便,几乎可以将数据集的每一行除以255(像素通道的最大值),因为在RGB中不存在比255大的数据,所以我们可以放心的除以255,让标准化的数据位于 [0,1] 之间,现在标准化我们的数据集:
train_set_x = train_set_x_flatten / 255
test_set_x = test_set_x_flatten / 255
(二)搭建神经网络
数学表达式:
搭建神经网络步骤:(具体可参考)
-
定义模型结构(例如输入特征的数量)
-
初始化模型的参数
-
循环:
3.1 计算当前损失(正向传播)
3.2 计算当前梯度(反向传播)
3.3 更新参数(梯度下降)
一、构建sigmoid(),需要使用 计算来做出预测。
#定义sigmoid函数 def sigmoid(z): """ z - 任何大小的标量或numpy数组。 返回: s - sigmoid(z) """ s = 1 / (1 + np.exp(-z)) return s
测试sigmoid函数:
#测试sigmoid print("===================测试sigmoid================") print("sigmoid(0)="+str(sigmoid(0))) print("sigmoid(9.2)="+str(sigmoid(9.2)))
运行结果:
二、初始化参数w、b
# 初始化参数w、b def initialize_with_zeros(dim): """ 此函数为w创建一个维度为(dim,1)的0向量,并将b初始化为0。 参数: dim - 我们想要的w矢量的大小(或者这种情况下的参数数量) 返回: w - 维度为(dim,1)的初始化向量。 b - 初始化的标量(对应于偏差) """ #创建一个一维向量 w = np.zeros(shape = (dim,1)) b = 0 #使用断言来确保我要的数据是正确的 assert(w.shape == (dim,1)) #w的维度是(dim,1) assert(isinstance(b,float)or isinstance(b,int))#b的类型是float或者是int return (w,b)
三、执行“前向”和“后向”传播步骤来学习参数。要实现一个计算成本函数及其渐变的函数propagate()
。
''' 计算向前(损失)、向后传播(梯度) ''' def propagate(w, b, X, Y): """ 实现前向和后向传播的成本函数及其梯度。 参数: w - 权重,大小不等的数组(num_px * num_px * 3,1) b - 偏差,一个标量 X - 矩阵类型为(num_px * num_px * 3,训练数量) Y - 真正的“标签”矢量(如果非猫则为0,如果是猫则为1),矩阵维度为(1,训练数据数量) 返回: cost- 逻辑回归的负对数似然成本 dw - 相对于w的损失梯度,因此与w相同的形状 db - 相对于b的损失梯度,因此与b的形状相同 """ m = X.shape[1]#计算矩阵长度 #正向传播 A = sigmoid(np.dot(w.T,X)+b) #计算激活值 cost = (-1/m)*np.sum(Y*np.log(A)+(1-Y)*(np.log(1-A)))#计算成本 #反向传播 dw = (1/m) * np.dot(X,(A-Y).T) db = (1/m) * np.sum(A-Y) #使用断言确保我的数据是正确的 assert(dw.shape == w.shape) assert(db.dtype == float) cost = np.squeeze(cost) assert(cost.shape == ()) #创建一个字典,把dw和db保存起来。 grads = { "dw": dw, "db": db } return (grads , cost)
测试函数propagate():
#测试propagate print("====================测试propagate==================") #初始化一些参数 w,b,X,Y = np.array([[1],[2]]),2,np.array([[1,2],[3,4]]),np.array([[1,0]]) grads,cost = propagate(w,b,X,Y) print("dw = "+str(grads["dw"])) print("db = "+str(grads["db"])) print("cost ="+str(cost)) print("grads = "+str(grads))
运行结果:
【拓展2】:assert(断言)
assert(断言)用于判断一个表达式,在表达式条件为 false 的时候触发异常。
断言可以在条件不满足程序运行的情况下直接返回错误,而不必等待程序运行后出现崩溃的情况,例如我们的代码只能在 Linux 系统下运行,可以先判断当前系统是否符合条件。
语法格式如下:
assert expression
等价于:
if not expression: raise AssertionError
assert 后面也可以紧跟参数:
assert expression [, arguments]
等价于:
if not expression: raise AssertionError(arguments)
以下为 assert 使用实例:
>>> assert True # 条件为 true 正常执行 >>> assert False # 条件为 false 触发异常 Traceback (most recent call last): File "<stdin>", line 1, in <module> AssertionError >>> assert 1==1 # 条件为 true 正常执行 >>> assert 1==2 # 条件为 false 触发异常 Traceback (most recent call last): File "<stdin>", line 1, in <module> AssertionError >>> assert 1==2, '1 不等于 2' Traceback (most recent call last): File "<stdin>", line 1, in <module> AssertionError: 1 不等于 2 >>>
此处参考:https://www.runoob.com/python3/python3-assert.html
四、梯度下降法优化w和b
目标是通过最小化成本函数 J 来学习 w 和 b。对于参数 θ ,更新规则是,其中α 是学习率。
def optimize(w , b , X , Y , num_iterations , learning_rate , print_cost = True): """ 此函数通过运行梯度下降算法来优化w和b 参数: w - 权重,大小不等的数组(num_px * num_px * 3,1) b - 偏差,一个标量 X - 维度为(num_px * num_px * 3,训练数据的数量)的数组。 Y - 真正的“标签”矢量(如果非猫则为0,如果是猫则为1),矩阵维度为(1,训练数据的数量) num_iterations - 优化循环的迭代次数 learning_rate - 梯度下降更新规则的学习率 print_cost - 每100步打印一次损失值 返回: params - 包含权重w和偏差b的字典 grads - 包含权重和偏差相对于成本函数的梯度的字典 成本 - 优化期间计算的所有成本列表,将用于绘制学习曲线。 提示: 我们需要写下两个步骤并遍历它们: 1)计算当前参数的成本和梯度,使用propagate()。 2)使用w和b的梯度下降法则更新参数。 """ costs = [] for i in range(num_iterations): grads, cost = propagate(w, b, X, Y) dw = grads["dw"] db = grads["db"] w = w - learning_rate * dw b = b - learning_rate * db #记录成本 if i % 100 == 0: costs.append(cost) #打印成本数据 if (print_cost) and (i % 100 == 0): print("迭代的次数: %i , 误差值: %f" % (i,cost)) params = { "w" : w, "b" : b } grads = { "dw": dw, "db": db } return (params , grads , costs)
测试优化函数optimize
#测试optimize print("====================测试optimize====================") w, b, X, Y = np.array([[1], [2]]), 2, np.array([[1,2], [3,4]]), np.array([[1, 0]]) params , grads , costs = optimize(w , b , X , Y , num_iterations=100 , learning_rate = 0.009 , print_cost = True) print ("w = " + str(params["w"])) print ("b = " + str(params["b"])) print ("dw = " + str(grads["dw"])) print ("db = " + str(grads["db"]))
运行结果:
五、将预测值存储在向量Y_prediction
中。
optimize
函数会输出已学习的w
和b
的值,我们可以使用w
和b
来预测数据集X
的标签。
现在我们要实现预测函数predict()
。计算预测有两个步骤:
1、计算;
2、将a
的值变为0(如果激活值<= 0.5)或者为1(如果激活值> 0.5);
def predict(w , b , X ): """ 使用学习逻辑回归参数logistic (w,b)预测标签是0还是1, 参数: w - 权重,大小不等的数组(num_px * num_px * 3,1) b - 偏差,一个标量 X - 维度为(num_px * num_px * 3,训练数据的数量)的数据 返回: Y_prediction - 包含X中所有图片的所有预测【0 | 1】的一个numpy数组(向量) """ m = X.shape[1] #图片的数量 Y_prediction = np.zeros((1,m)) w = w.reshape(X.shape[0],1) #计预测猫在图片中出现的概率 A = sigmoid(np.dot(w.T , X) + b) for i in range(A.shape[1]): #将概率a [0,i]转换为实际预测p [0,i] Y_prediction[0,i] = 1 if A[0,i] > 0.5 else 0 #使用断言 assert(Y_prediction.shape == (1,m)) return Y_prediction
测试predict:
#测试predict print("====================测试predict====================") w, b, X, Y = np.array([[1], [2]]), 2, np.array([[1,2], [3,4]]), np.array([[1, 0]]) print("predictions = " + str(predict(w, b, X)))
运行结果:
(三)通过调用之前实现的函数来构建逻辑回归模型
一、建立模型
就目前而言,我们基本上把所有的东西都做完了,现在我们要把这些函数统统整合到一个model()
函数中,届时只需要调用一个model()
就基本上完成所有的事了。
def model(X_train , Y_train , X_test , Y_test , num_iterations = 2000 , learning_rate = 0.5 , print_cost = True): """ 通过调用之前实现的函数来构建逻辑回归模型 参数: X_train - numpy的数组,维度为(num_px * num_px * 3,m_train)的训练集 Y_train - numpy的数组,维度为(1,m_train)(矢量)的训练标签集 X_test - numpy的数组,维度为(num_px * num_px * 3,m_test)的测试集 Y_test - numpy的数组,维度为(1,m_test)的(向量)的测试标签集 num_iterations - 表示用于优化参数的迭代次数的超参数 learning_rate - 表示optimize()更新规则中使用的学习速率的超参数 print_cost - 设置为true以每100次迭代打印成本 返回: d - 包含有关模型信息的字典。 """ w , b = initialize_with_zeros(X_train.shape[0]) parameters , grads , costs = optimize(w , b , X_train , Y_train,num_iterations , learning_rate , print_cost) #从字典“参数”中检索参数w和b w , b = parameters["w"] , parameters["b"] #预测测试/训练集的例子 Y_prediction_test = predict(w , b, X_test) Y_prediction_train = predict(w , b, X_train) #打印训练后的准确性 print("训练集准确性:" , format(100 - np.mean(np.abs(Y_prediction_train - Y_train)) * 100) ,"%") print("测试集准确性:" , format(100 - np.mean(np.abs(Y_prediction_test - Y_test)) * 100) ,"%") d = { "costs" : costs, "Y_prediction_test" : Y_prediction_test, "Y_prediciton_train" : Y_prediction_train, "w" : w, "b" : b, "learning_rate" : learning_rate, "num_iterations" : num_iterations } return d
测试model:
print("====================测试model====================") #这里加载的是真实的数据,请参见上面的代码部分。 d = model(train_set_x, train_set_y_orig, test_set_x, test_set_y_orig, num_iterations = 2000, learning_rate = 0.005, print_cost = True)
运行结果:
我们更改一下学习率和迭代次数,有可能会发现训练集的准确性可能会提高,但是测试集准确性会下降,这是由于过拟合造成的,但是我们并不需要担心,我们以后会使用更好的算法来解决这些问题的。
二、绘制图
#绘制图 costs = np.squeeze(d['costs']) plt.plot(costs) plt.ylabel('cost') plt.xlabel('iterations (per hundreds)') plt.title("Learning rate =" + str(d["learning_rate"])) plt.show()
结果:
对其进一步分析,研究学习率alpha的可能选择。为了让渐变下降起作用,我们必须明智地选择学习速率。学习率 α alphaα 决定了我们更新参数的速度。如果学习率过高,我们可能会“超过”最优值。同样,如果它太小,我们将需要太多迭代才能收敛到最佳值。这就是为什么使用良好调整的学习率至关重要的原因。我们可以比较一下我们模型的学习曲线和几种学习速率的选择。也可以尝试使用不同于我们初始化的learning_rates变量包含的三个值,并看一下会发生什么。
learning_rates = [0.005, 0.01, 0.001, 0.0001] models = {} for i in learning_rates: print ("learning rate is: " + str(i)) print("num_iterations=2000") models[str(i)] = model(train_set_x, train_set_y_orig, test_set_x, test_set_y_orig, num_iterations = 2000, learning_rate = i, print_cost = True) print (' ' + "-------------------------------------------------------" + ' ') for i in learning_rates: plt.plot(np.squeeze(models[str(i)]["costs"]), label= str(models[str(i)]["learning_rate"])) plt.ylabel('cost') plt.xlabel('iterations') legend = plt.legend(loc='upper center', shadow=True) frame = legend.get_frame() frame.set_facecolor('0.90') plt.show()
运行结果:
learning rate is: 0.005 num_iterations=2000 迭代的次数: 0 , 误差值: 0.693147 迭代的次数: 100 , 误差值: 0.584508 迭代的次数: 200 , 误差值: 0.466949 迭代的次数: 300 , 误差值: 0.376007 迭代的次数: 400 , 误差值: 0.331463 迭代的次数: 500 , 误差值: 0.303273 迭代的次数: 600 , 误差值: 0.279880 迭代的次数: 700 , 误差值: 0.260042 迭代的次数: 800 , 误差值: 0.242941 迭代的次数: 900 , 误差值: 0.228004 迭代的次数: 1000 , 误差值: 0.214820 迭代的次数: 1100 , 误差值: 0.203078 迭代的次数: 1200 , 误差值: 0.192544 迭代的次数: 1300 , 误差值: 0.183033 迭代的次数: 1400 , 误差值: 0.174399 迭代的次数: 1500 , 误差值: 0.166521 迭代的次数: 1600 , 误差值: 0.159305 迭代的次数: 1700 , 误差值: 0.152667 迭代的次数: 1800 , 误差值: 0.146542 迭代的次数: 1900 , 误差值: 0.140872 训练集准确性: 99.04306220095694 % 测试集准确性: 70.0 % ------------------------------------------------------- learning rate is: 0.01 num_iterations=2000 迭代的次数: 0 , 误差值: 0.693147 迭代的次数: 100 , 误差值: 0.823921 迭代的次数: 200 , 误差值: 0.418944 迭代的次数: 300 , 误差值: 0.617350 迭代的次数: 400 , 误差值: 0.522116 迭代的次数: 500 , 误差值: 0.387709 迭代的次数: 600 , 误差值: 0.236254 迭代的次数: 700 , 误差值: 0.154222 迭代的次数: 800 , 误差值: 0.135328 迭代的次数: 900 , 误差值: 0.124971 迭代的次数: 1000 , 误差值: 0.116478 迭代的次数: 1100 , 误差值: 0.109193 迭代的次数: 1200 , 误差值: 0.102804 迭代的次数: 1300 , 误差值: 0.097130 迭代的次数: 1400 , 误差值: 0.092043 迭代的次数: 1500 , 误差值: 0.087453 迭代的次数: 1600 , 误差值: 0.083286 迭代的次数: 1700 , 误差值: 0.079487 迭代的次数: 1800 , 误差值: 0.076007 迭代的次数: 1900 , 误差值: 0.072809 训练集准确性: 99.52153110047847 % 测试集准确性: 70.0 % ------------------------------------------------------- learning rate is: 0.001 num_iterations=2000 迭代的次数: 0 , 误差值: 0.693147 迭代的次数: 100 , 误差值: 0.591289 迭代的次数: 200 , 误差值: 0.555796 迭代的次数: 300 , 误差值: 0.528977 迭代的次数: 400 , 误差值: 0.506881 迭代的次数: 500 , 误差值: 0.487880 迭代的次数: 600 , 误差值: 0.471108 迭代的次数: 700 , 误差值: 0.456046 迭代的次数: 800 , 误差值: 0.442350 迭代的次数: 900 , 误差值: 0.429782 迭代的次数: 1000 , 误差值: 0.418164 迭代的次数: 1100 , 误差值: 0.407362 迭代的次数: 1200 , 误差值: 0.397269 迭代的次数: 1300 , 误差值: 0.387802 迭代的次数: 1400 , 误差值: 0.378888 迭代的次数: 1500 , 误差值: 0.370471 迭代的次数: 1600 , 误差值: 0.362500 迭代的次数: 1700 , 误差值: 0.354934 迭代的次数: 1800 , 误差值: 0.347737 迭代的次数: 1900 , 误差值: 0.340877 训练集准确性: 91.38755980861244 % 测试集准确性: 68.0 % ------------------------------------------------------- learning rate is: 0.0001 num_iterations=2000 迭代的次数: 0 , 误差值: 0.693147 迭代的次数: 100 , 误差值: 0.643677 迭代的次数: 200 , 误差值: 0.635737 迭代的次数: 300 , 误差值: 0.628572 迭代的次数: 400 , 误差值: 0.622040 迭代的次数: 500 , 误差值: 0.616029 迭代的次数: 600 , 误差值: 0.610455 迭代的次数: 700 , 误差值: 0.605248 迭代的次数: 800 , 误差值: 0.600354 迭代的次数: 900 , 误差值: 0.595729 迭代的次数: 1000 , 误差值: 0.591339 迭代的次数: 1100 , 误差值: 0.587153 迭代的次数: 1200 , 误差值: 0.583149 迭代的次数: 1300 , 误差值: 0.579307 迭代的次数: 1400 , 误差值: 0.575611 迭代的次数: 1500 , 误差值: 0.572046 迭代的次数: 1600 , 误差值: 0.568601 迭代的次数: 1700 , 误差值: 0.565266 迭代的次数: 1800 , 误差值: 0.562032 迭代的次数: 1900 , 误差值: 0.558891 训练集准确性: 71.29186602870814 % 测试集准确性: 40.0 % -------------------------------------------------------
参考吴恩达教授总结-深度学习课后作业