接下来将从零开始实现线性回归整个方法，包括数据流水线、模型、损失函数和小批量随机梯度下降优化器。虽然现代的深度学习框架几乎可以自动化地进行所有这些工作，但从零开始实现可以确保你真正知道自己在做什么。同时，了解更细致的工作原理将方便我们自定义模型、自定义层或自定义损失函数。在这一节中，我们将只使用张量和自动求导。%matplotlib inline import random import torch from d2l import torch as d2l生成数据集为了简单起见，将根据带有噪声的线性模型构造一个人造数据集，我们的任务是使用这个有限样本的数据集来恢复这个模型的参数。我们将使用低维数据，这样可以很容易地将其可视化。在下面的代码中，我们生成一个包含1000个样本的数据集，每个样本包含从标准正态分布中采样的2个特征。我们的合成数据集是一个矩阵$\textbf{X}\in \textbf{R}^{1000 \times 2}$。使用线性模型参数$\textbf{w} = [2, -3.4]^\top$、$b = 4.2$和噪声项$\epsilon$生成数据集及其标签：$$ \textbf{y}= \textbf{X} \textbf{w} + b + \epsilon. $$可以将$\epsilon$视为模型预测和标签时的潜在观测误差，在这里我们认为标准假设成立，即$\epsilon$服从均值为0的正态分布。为了简化问题，将标准差设为0.01，下面的代码生成合成数据集。def synthetic_data(w, b, num_examples): #@save """生成y=Xw+b+噪声""" X = torch.normal(0, 1, (num_examples, len(w))) y = torch.matmul(X, w) + b y += torch.normal(0, 0.01, y.shape) return X, y.reshape((-1, 1))true_w = torch.tensor([2, -3.4]) true_b = 4.2 features, labels = synthetic_data(true_w, true_b, 1000) print('features:', features[0],'\nlabel:', labels[0]) #features: tensor([-0.0575, 0.6582]) #label: tensor([1.8422])画出散点图可以看出两者之间的线性关系d2l.set_figsize() d2l.plt.scatter(features[:, (1)].detach().numpy(), labels.detach().numpy(), 1);读取数据集训练模型时要对数据集进行遍历，每次抽取一小批量样本，并使用它们来更新我们的模型。 由于这个过程是训练机器学习算法的基础，所以有必要定义一个函数， 该函数能打乱数据集中的样本并以小批量方式获取数据。在下面的代码中，定义一个data_iter函数， 该函数接收批量大小、特征矩阵和标签向量作为输入，生成大小为batch_size的小批量。 每个小批量包含一组特征和标签。def data_iter(batch_size, features, labels): num_examples = len(features) indices = list(range(num_examples)) # 这些样本是随机读取的，没有特定的顺序 random.shuffle(indices) for i in range(0, num_examples, batch_size): batch_indices = torch.tensor( indices[i: min(i + batch_size, num_examples)]) yield features[batch_indices], labels[batch_indices]通常，利用GPU并行运算的优势，处理合理大小的“小批量”。 每个样本都可以并行地进行模型计算，且每个样本损失函数的梯度也可以被并行计算。 GPU可以在处理几百个样本时，所花费的时间不比处理一个样本时多太多。直观感受一下小批量运算：读取第一个小批量数据样本并打印。 每个批量的特征维度显示批量大小和输入特征数。 同样的，批量的标签形状与batch_size相等。batch_size = 10 for X, y in data_iter(batch_size, features, labels): print(X, '\n', y) break #tensor([[ 0.9458, -0.0361], # [-1.8607, -1.0405], # [-0.3435, 0.8372], # [-0.1770, -0.0755], # [ 0.1739, 0.0721], # [ 0.2494, -0.1436], # [ 0.1927, -1.2595], # [ 0.7346, -0.0856], # [ 2.4317, 0.8365], # [-0.8829, 0.6800]]) # tensor([[6.2000], # [4.0230], # [0.6624], # [4.1128], # [4.2958], # [5.1983], # [8.8624], # [5.9600], # [6.2192], # [0.1082]])当我们运行迭代时，我们会连续地获得不同的小批量，直至遍历完整个数据集。 上面实现的迭代对于教学来说很好，但它的执行效率很低，可能会在实际问题上陷入麻烦。 例如，它要求我们将所有数据加载到内存中，并执行大量的随机内存访问。 在深度学习框架中实现的内置迭代器效率要高得多， 它可以处理存储在文件中的数据和数据流提供的数据。初始化模型参数w = torch.normal(0, 0.01, size=(2,1), requires_grad=True) b = torch.zeros(1, requires_grad=True)在初始化参数之后，我们的任务是更新这些参数，直到这些参数足够拟合我们的数据。 每次更新都需要计算损失函数关于模型参数的梯度。 有了这个梯度，我们就可以向减小损失的方向更新每个参数。 因为手动计算梯度很枯燥而且容易出错，所以没有人会手动计算梯度。 我们使用自动微分来计算梯度。定义模型接下来，我们必须定义模型，将模型的输入和参数同模型的输出关联起来，回想一下，要计算线性模型的输出，我们只需计算输入特征$\textbf{X}$和模型权重$\textbf{w}$的矩阵-向量乘法后加上偏置$b$。注意，上面的$\textbf{Xw}$是一个向量，而$b$是一个标量。回想一下广播机制：当我们用一个向量加一个标量时，标量会被加到向量的每个分量上。def linreg(X, w, b): #@save """线性回归模型""" return torch.matmul(X, w) + b定义损失函数因为需要计算损失函数的梯度，所以我们应该先定义损失函数。这里使用平方损失函数。在实现中，我们需要将真实值y的形状转换为和预测值y_hat的形状相同。def squared_loss(y_hat, y): #@save """均方损失""" return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2定义优化算法在每一步中，使用从数据集中随机抽取的一个小批量，然后根据参数计算损失的梯度。 接下来，朝着减少损失的方向更新我们的参数。 下面的函数实现小批量随机梯度下降更新。 该函数接受模型参数集合、学习速率和批量大小作为输入。每一步更新的大小由学习速率lr决定。 因为我们计算的损失是一个批量样本的总和，所以我们用批量大小（batch_size） 来规范化步长，这样步长大小就不会取决于我们对批量大小的选择。def sgd(params, lr, batch_size): #@save """小批量随机梯度下降""" with torch.no_grad(): for param in params: param -= lr * param.grad / batch_size param.grad.zero_()训练现在已经准备好了模型训练所有需要的要素，可以实现主要的训练过程部分了。在每次迭代中，我们读取一小批量训练样本，并通过我们的模型来获得一组预测，计算完损失后，开始反向传播，存储每个参数的梯度。最后，调用优化算法sgd来更新模型参数。概括一下，我们将执行以下循环：初始化参数重复以下训练，直到完成计算梯度$\textbf{g} \leftarrow \partial_{(\textbf{w},b)} \frac{1}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} l(\textbf{x}^{(i)}, y^{(i)}, \textbf{w}, b)$更新参数$(\textbf{w}, b) \leftarrow (\textbf{w}, b) - \eta \textbf{g}$在每个迭代周期（epoch）中，我们使用data_iter函数遍历整个数据集，并将训练数据集中所有样本都使用一次（假设样本数能够被批量大小整除）。这里的迭代周期个数num_epochs和学习率lr都是超参数，分别设为3和0.03。lr = 0.03 num_epochs = 3 net = linreg loss = squared_lossfor epoch in range(num_epochs): for X, y in data_iter(batch_size, features, labels): l = loss(net(X, w, b), y) # X和y的小批量损失 # 因为l形状是(batch_size,1)，而不是一个标量。l中的所有元素被加到一起， # 并以此计算关于[w,b]的梯度 l.sum().backward() sgd([w, b], lr, batch_size) # 使用参数的梯度更新参数 with torch.no_grad(): train_l = loss(net(features, w, b), labels) print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')因为使用的是自己合成的数据集，所以知道真正的参数是什么。 因此，我们可以通过比较真实参数和通过训练学到的参数来评估训练的成功程度。 事实上，真实参数和通过训练学到的参数确实非常接近。print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}') print(f'b的估计误差: {true_b - b}') #w的估计误差: tensor([-0.0004, -0.0009], grad_fn=<SubBackward0>) #b的估计误差: tensor([0.0013], grad_fn=<RsubBackward1>)注意，我们不应该想当然地认为我们能够完美地求解参数。在机器学习中，我们通常不太关心恢复真正的参数，而更关心如何高度准确预测参数。幸运的是，即使是在复杂的优化问题上，随机梯度下降通常也能找到非常好的解。其中一个原因是，在深度网络中存在许多参数组合能够实现高度精确的预测。Pytorch实现import numpy as np import torch from torch.utils import data from d2l import torch as d2l true_w = torch.tensor([2, -3.4]) true_b = 4.2 features, labels = d2l.synthetic_data(true_w, true_b, 1000)读取数据集可以调用框架中现有的API来读取数据，将features和labels作为API的参数传递，并通过数据迭代器指定batch_size。 此外，布尔值is_train表示是否希望数据迭代器对象在每个迭代周期内打乱数据。def load_array(data_arrays, batch_size, is_train=True): #@save """构造一个PyTorch数据迭代器""" dataset = data.TensorDataset(*data_arrays) return data.DataLoader(dataset, batch_size, shuffle=is_train) batch_size = 10 data_iter = load_array((features, labels), batch_size) next(iter(data_iter))输出如下[tensor([[ 0.1087, -1.3303], [ 1.7520, -0.0943], [-0.7183, -0.9455], [-1.9472, -0.8632], [-1.8407, 0.0059], [-0.0802, 0.3188], [-0.5427, 0.2455], [-0.4107, -0.7009], [-0.6534, -0.9351], [-1.5927, 0.4747]]), tensor([[ 8.9435], [ 8.0224], [ 5.9710], [ 3.2344], [ 0.4935], [ 2.9666], [ 2.2832], [ 5.7591], [ 6.0626], [-0.6023]])]定义模型对于标准深度学习模型，可以使用框架的预定义好的层。这使我们只需关注使用哪些层来构造模型，而不必关注层的实现细节。 我们首先定义一个模型变量net，它是一个Sequential类的实例。 Sequential类将多个层串联在一起。 当给定输入数据时，Sequential实例将数据传入到第一层， 然后将第一层的输出作为第二层的输入，以此类推。 在下面的例子中，我们的模型只包含一个层，因此实际上不需要Sequential。 但是由于以后几乎所有的模型都是多层的，在这里使用Sequential会让你熟悉“标准的流水线”。这一单层被称为全连接层（fully-connected layer）， 因为它的每一个输入都通过矩阵-向量乘法得到它的每个输出。# nn是神经网络的缩写 from torch import nn net = nn.Sequential(nn.Linear(2, 1))初始化模型参数在使用net之前，我们需要初始化模型参数。 如在线性回归模型中的权重和偏置。 深度学习框架通常有预定义的方法来初始化参数。 在这里，我们指定每个权重参数应该从均值为0、标准差为0.01的正态分布中随机采样， 偏置参数将初始化为零。正如我们在构造nn.Linear时指定输入和输出尺寸一样， 现在我们能直接访问参数以设定它们的初始值。 我们通过net[0]选择网络中的第一个图层， 然后使用weight.data和bias.data方法访问参数。 我们还可以使用替换方法normal_和fill_来重写参数值。net[0].weight.data.normal_(0, 0.01) net[0].bias.data.fill_(0)定义损失函数计算均方误差使用的是MSELoss类，也称为平方$L_2$范数。默认情况下，它返回所有样本损失的平均值。loss = nn.MSELoss()定义优化算法小批量随机梯度下降算法是一种优化神经网络的标准工具， PyTorch在optim模块中实现了该算法的许多变种。 当我们(实例化一个SGD实例)时，我们要指定优化的参数 （可通过net.parameters()从我们的模型中获得）以及优化算法所需的超参数字典。 小批量随机梯度下降只需要设置lr值，这里设置为0.03。trainer = torch.optim.SGD(net.parameters(), lr=0.03)训练通过深度学习框架的高级API来实现我们的模型只需要相对较少的代码。我们不必单独分配参数、不必定义我们的损失函数，也不必手动实现小批量随机梯度下降。当我们需要更复杂的模型时，高级API的优势将大大增加。当我们有了所有的基本组件，训练过程代码与我们从零开始实现时所做的非常相似。回顾一下：在每个迭代周期里，我们将完整遍历一次数据集（train_data），不停地从中获取一个小批量的输入和相应的标签。对于每一个小批量，我们会进行以下步骤:通过调用net(X)生成预测并计算损失l（前向传播）。通过进行反向传播来计算梯度。通过调用优化器来更新模型参数。为了更好的衡量训练效果，我们计算每个迭代周期后的损失，并打印它来监控训练过程。num_epochs = 3 for epoch in range(num_epochs): for X, y in data_iter: l = loss(net(X) ,y) trainer.zero_grad() l.backward() trainer.step() l = loss(net(features), labels) print(f'epoch {epoch + 1}, loss {l:f}') #epoch 1, loss 0.000221 #epoch 2, loss 0.000105 #epoch 3, loss 0.000105 w = net[0].weight.data print('w的估计误差：', true_w - w.reshape(true_w.shape)) b = net[0].bias.data print('b的估计误差：', true_b - b) #w的估计误差： tensor([1.6689e-06, 1.5545e-04]) #b的估计误差： tensor([6.9618e-05])
回归（regression）是能为一个或多个自变量与因变量之间关系建模的一类方法。在自然科学和社会科学领域，回归经常用来表示输入和输出之间的关系。在机器学习领域中的大多数任务通常都与预测（prediction）有关。当我们想预测一个数值时，就会涉及到回归问题。常见的例子包括：预测价格（房屋、股票等）、预测住院时间（针对住院病人等）、预测需求（零售销量等）。线性回归的基本元素线性回归（linear regression）可以追溯到19世纪初，它在回归的各种标准工具中最简单而且最流行。线性回归基于几个简单的假设：首先，假设自变量$x$和因变量$y$之间的关系是线性的，即$y$可以表示为$x$中元素的加权和，这里通常允许包含观测值的一些噪声；其次，我们假设任何噪声都比较正常，如噪声遵循正态分布。为了解释线性回归，举一个实际的例子：我们希望根据房屋的面积（平方英尺）和房龄（年）来估算房屋价格（美元）。为了开发一个能预测房价的模型，我们需要收集一个真实的数据集。这个数据集包括了房屋的销售价格、面积和房龄。在机器学习的术语中，该数据集称为训练数据集（training data set）或训练集（training set）。每行数据（比如一次房屋交易相对应的数据）称为样本（sample），也可以称为数据点（data point）或数据样本（data instance）。我们把试图预测的目标（比如预测房屋价格）称为标签（label）或目标（target）。预测所依据的自变量（面积和房龄）称为特征（feature）或协变量（covariate）。通常，我们使用$n$来表示数据集中的样本数，对索引为$i$的样本，其输入表示为$\textbf{x}^{(i)} = [x_1^{(i)}, x_2^{(i)}]^\top$，其对应的标签是$y^{(i)}$。线性模型线性假设是指目标（房屋价格）可以表示为特征（面积和房龄）的加权和，如下面的式子：$$ \textbf{price} = w_{\textbf{area}} \cdot \textbf{area} + w_{\textbf{age}} \cdot \textbf{age} + b. $$其中中的$w_{area}$和$w_{age}$称为权重（weight），权重决定了每个特征对我们预测值的影响，$b$称为偏置（bias）、偏移量（offset）或截距（intercept）。偏置是指当所有特征都取值为0时，预测值应该为多少。即使现实中不会有任何房子的面积是0或房龄正好是0年，我们仍然需要偏置项。如果没有偏置项，我们模型的表达能力将受到限制。严格来说，是输入特征的一个仿射变换（affine transformation）。仿射变换的特点是通过加权和对特征进行线性变换（linear transformation），并通过偏置项来进行平移（translation）。给定一个数据集，我们的目标是寻找模型的权重$\textbf{w}$和偏置$b$，使得根据模型做出的预测大体符合数据里的真实价格。输出的预测值由输入特征通过线性模型的仿射变换决定，仿射变换由所选权重和偏置确定。而在机器学习领域，我们通常使用的是高维数据集，建模时采用线性代数表示法会比较方便。当我们的输入包含$d$个特征时，我们将预测结果$\hat{y}$（通常使用“尖角”符号表示$y$的估计值）表示为：$$ \hat{y} = w_1 x_1 + ... + w_d x_d + b. $$将所有特征放到向量$\textbf{x} \in \mathbb{R}^d$中，并将所有权重放到向量$\textbf{w} \in \mathbb{R}^d$中，我们可以用点积形式来简洁地表达模型：$$ \hat{y} = \textbf{w}^\top \textbf{x} + b. $$向量$\textbf{x}$对应于单个数据样本的特征。用符号表示的矩阵$\textbf{X} \in \mathbb{R}^{n \times d}$可以很方便地引用我们整个数据集的$n$个样本。其中，$\textbf{X}$的每一行是一个样本，每一列是一种特征。对于特征集合$\textbf{X}$，预测值$\hat{\textbf{y}} \in \mathbb{R}^n$可以通过矩阵-向量乘法表示为：${\hat{\textbf{y}}} = \textbf{X} \textbf{w} + b$这个过程中的求和将使用广播机制，给定训练数据特征$\textbf{X}$和对应的已知标签$\textbf{y}$，线性回归的目标是找到一组权重向量$\textbf{w}$和偏置$b$：当给定从$\textbf{X}$的同分布中取样的新样本特征时，这组权重向量和偏置能够使得新样本预测标签的误差尽可能小。虽然我们相信给定$\textbf{x}$预测$y$的最佳模型会是线性的，但我们很难找到一个有$n$个样本的真实数据集，其中对于所有的$1 \leq i \leq n$，$y^{(i)}$完全等于$\textbf{w}^\top \textbf{x}^{(i)}+b$。无论我们使用什么手段来观察特征$\textbf{X}$和标签$\textbf{y}$，都可能会出现少量的观测误差。因此，即使确信特征与标签的潜在关系是线性的，我们也会加入一个噪声项来考虑观测误差带来的影响。在开始寻找最好的模型参数（model parameters）$\textbf{w}$和$b$之前，我们还需要两个东西：一种模型质量的度量方式和一种能够更新模型以提高模型预测质量的方法。损失函数在我们开始考虑如何用模型拟合（fit）数据之前，我们需要确定一个拟合程度的度量。损失函数（loss function）能够量化目标的实际值与预测值之间的差距。通常我们会选择非负数作为损失，且数值越小表示损失越小，完美预测时的损失为0。回归问题中最常用的损失函数是平方误差函数，当样本$i$的预测值为$\hat{y}^{(i)}$，其相应的真实标签为$y^{(i)}$时，平方误差可以定义为以下公式：$$ l^{(i)}(\textbf{w}, b) = \frac{1}{2} \left(\hat{y}^{(i)} - y^{(i)}\right)^2. $$常数$\frac{1}{2}$不会带来本质的差别，但这样在形式上稍微简单一些（因为当我们对损失函数求导后常数系数为1）。由于训练数据集并不受我们控制，所以经验误差只是关于模型参数的函数。为了进一步说明，来看下面的例子。我们为一维情况下的回归问题绘制图像，如图所示。由于平方误差函数中的二次方项，估计值$\hat{y}^{(i)}$和观测值$y^{(i)}$之间较大的差异将导致更大的损失。为了度量模型在整个数据集上的质量，我们需计算在训练集$n$个样本上的损失均值（也等价于求和）。$$ L(\textbf{w}, b) =\frac{1}{n}\sum_{i=1}^n l^{(i)}(\textbf{w}, b) =\frac{1}{n} \sum_{i=1}^n \frac{1}{2}\left(\textbf{w}^\top \textbf{x}^{(i)} + b - y^{(i)}\right)^2. $$在训练模型时，我们希望寻找一组参数（$\textbf{w}^*, b^*$），这组参数能最小化在所有训练样本上的总损失。如下式：$$ \textbf{w}^*, b^* = \operatorname*{argmin}_{\textbf{w}, b}\ L(\textbf{w}, b). $$解析解线性回归刚好是一个很简单的优化问题，与其他大部分模型不同，线性回归的解可以用一个公式简单地表达出来，这类解叫作解析解（analytical solution）。首先，我们将偏置$b$合并到参数$\textbf{w}$中，合并方法是在包含所有参数的矩阵中附加一列。我们的预测问题是最小化$\|\textbf{y} - \textbf{X}\textbf{w}\|^2$。这在损失平面上只有一个临界点，这个临界点对应于整个区域的损失极小点。将损失关于$\textbf{w}$的导数设为0，得到解析解：$$ \textbf{w}^* = (\textbf X^\top \textbf X)^{-1}\textbf X^\top \textbf{y}. $$像线性回归这样的简单问题存在解析解，但并不是所有的问题都存在解析解，解析解可以进行很好的数学分析，但解析解对问题的限制很严格，导致它无法广泛应用在深度学习里。随机梯度下降即使在我们无法得到解析解的情况下，我们仍然可以有效地训练模型，在许多任务上，那些难以优化的模型效果要更好，因此，弄清楚如何训练这些难以优化的模型是非常重要的。梯度下降（gradient descent）的方法几乎可以优化所有深度学习模型，它通过不断地在损失函数递减的方向上更新参数来降低误差。梯度下降最简单的用法是计算损失函数（数据集中所有样本的损失均值）关于模型参数的导数（在这里也可以称为梯度）。但实际中的执行可能会非常慢：因为在每一次更新参数之前，我们必须遍历整个数据集。因此，我们通常会在每次需要计算更新的时候随机抽取一小批样本，这种变体叫做小批量随机梯度下降（minibatch stochastic gradient descent）。在每次迭代中，我们首先随机抽样一个小批量$\textbf{B}$，它是由固定数量的训练样本组成的。然后，我们计算小批量的平均损失关于模型参数的导数（也可以称为梯度）。最后，我们将梯度乘以一个预先确定的正数$\eta$，并从当前参数的值中减掉。用下面的数学公式来表示这一更新过程（$\partial$表示偏导数）：$$ (\textbf{w},b) \leftarrow (\textbf{w},b) - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_{(\textbf{w},b)} l^{(i)}(\textbf{w},b). $$总结一下，算法的步骤如下：（1）初始化模型参数的值，如随机初始化；（2）从数据集中随机抽取小批量样本且在负梯度的方向上更新参数，并不断迭代这一步骤。对于平方损失和仿射变换，我们可以明确地写成如下形式:$$ \begin{aligned} \textbf{w} &\leftarrow \textbf{w} - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_{\textbf{w}} l^{(i)}(\textbf{w}, b) = \textbf{w} - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \textbf{x}^{(i)} \left(\textbf{w}^\top \textbf{x}^{(i)} + b - y^{(i)}\right),\\ b &\leftarrow b - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_b l^{(i)}(\textbf{w}, b) = b - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \left(\textbf{w}^\top \textbf{x}^{(i)} + b - y^{(i)}\right). \end{aligned} $$公式中的$\textbf{w}$和$\textbf{x}$都是向量。在这里，更优雅的向量表示法比系数表示法（如$w_1, w_2, \ldots, w_d$）更具可读性。$|\mathcal{B}|$表示每个小批量中的样本数，这也称为批量大小（batch size）。$\eta$表示学习率（learning rate）。批量大小和学习率的值通常是手动预先指定，而不是通过模型训练得到的。这些可以调整但不在训练过程中更新的参数称为超参数（hyperparameter）。调参（hyperparameter tuning）是选择超参数的过程，超参数通常是我们根据训练迭代结果来调整的，而训练迭代结果是在独立的验证数据集（validation dataset）上评估得到的。在训练了预先确定的若干迭代次数后（或者直到满足某些其他停止条件后），我们记录下模型参数的估计值，表示为$\hat{\textbf{w}}, \hat{b}$。但是，即使我们的函数确实是线性的且无噪声，这些估计值也不会使损失函数真正地达到最小值，因为算法会使得损失向最小值缓慢收敛，但却不能在有限的步数内非常精确地达到最小值。线性回归恰好是一个在整个域中只有一个最小值的学习问题，但是对于像深度神经网络这样复杂的模型来说，损失平面上通常包含多个最小值。深度学习实践者很少会去花费大力气寻找这样一组参数，使得在训练集上的损失达到最小。事实上，更难做到的是找到一组参数，这组参数能够在我们从未见过的数据上实现较低的损失，这一挑战被称为泛化（generalization）。矢量化加速为了说明矢量化为什么如此重要，我们考虑(对向量相加的两种方法)。我们实例化两个全为1的10000维向量。在一种方法中，我们将使用Python的for循环遍历向量；在另一种方法中，我们将依赖对+的调用。%matplotlib inline import math import time import numpy as np import torch from d2l import torch as d2l n = 10000 a = torch.ones(n) b = torch.ones(n)定义一个计时器class Timer: #@save """记录多次运行时间""" def __init__(self): self.times = [] self.start() def start(self): """启动计时器""" self.tik = time.time() def stop(self): """停止计时器并将时间记录在列表中""" self.times.append(time.time() - self.tik) return self.times[-1] def avg(self): """返回平均时间""" return sum(self.times) / len(self.times) def sum(self): """返回时间总和""" return sum(self.times) def cumsum(self): """返回累计时间""" return np.array(self.times).cumsum().tolist()首先，使用for循环，每次执行一位的加法。c = torch.zeros(n) timer = Timer() for i in range(n): c[i] = a[i] + b[i] f'{timer.stop():.5f} sec' #output: 0.09577 sec或者，我们使用重载的+运算符来计算按元素的和timer.start() d = a + b f'{timer.stop():.10f} sec' #output: 0.0009970665 sec结果很明显，第二种方法比第一种方法快得多，矢量化代码通常会带来数量级的加速，另外，我们将更多的数学运算放到库中，而无须自己编写那么多的计算，从而减少了出错的可能性。从线性回归到深度网络到目前为止，我们只谈论了线性模型，尽管神经网络涵盖了更多更为丰富的模型，我们依然可以用描述神经网络的方式来描述线性模型，从而把线性模型看作一个神经网络，首先，我们用“层”符号来重写这个模型。深度学习从业者喜欢绘制图表来可视化模型中正在发生的事情。 在下图，我们将线性回归模型描述为一个神经网络。 需要注意的是，该图只显示连接模式，即只显示每个输入如何连接到输出，隐去了权重和偏置的值。所示的神经网络中，输入为$x_1, \ldots, x_d$，因此输入层中的输入数（或称为特征维度，feature dimensionality）为$d$。网络的输出为$o_1$，因此输出层中的输出数是1。需要注意的是，输入值都是已经给定的，并且只有一个计算神经元。由于模型重点在发生计算的地方，所以通常我们在计算层数时不考虑输入层。也就是说，图中神经网络的层数为1，我们可以将线性回归模型视为仅由单个人工神经元组成的神经网络，或称为单层神经网络。对于线性回归，每个输入都与每个输出（在本例中只有一个输出）相连，我们将这种变换称为全连接层（fully-connected layer）或称为稠密层（dense layer）。
这里略去了课程中部分线性代数基础笔记，只记录了自己理解得不够深刻的部分张量算法的基本性质标量、向量、矩阵和任意数量轴的张量（本小节中的“张量”指代数对象）有一些实用的属性。例如，你可能已经从按元素操作的定义中注意到，任何按元素的一元运算都不会改变其操作数的形状。同样，给定具有相同形状的任意两个张量，任何按元素二元运算的结果都将是相同形状的张量。例如，将两个相同形状的矩阵相加，会在这两个矩阵上执行元素加法。A = torch.arange(20, dtype=torch.float32).reshape(5, 4) B = A.clone() # 通过分配新内存，将A的一个副本分配给B A, A + B #(tensor([[ 0., 1., 2., 3.], # [ 4., 5., 6., 7.], # [ 8., 9., 10., 11.], # [12., 13., 14., 15.], # [16., 17., 18., 19.]]), # tensor([[ 0., 2., 4., 6.], # [ 8., 10., 12., 14.], # [16., 18., 20., 22.], # [24., 26., 28., 30.], # [32., 34., 36., 38.]]))具体而言，两个矩阵的按元素乘法称为Hadamard积（Hadamard product）（数学符号$\odot$）。对于矩阵$\textbf{B} \in \textbf{R}^{m \times n}$，其中第$i$行和第$j$列的元素是$b_{ij}$。矩阵$\textbf{A}$和$\textbf{B}$的Hadamard积为：$$ \textbf{A} \odot \textbf{B} = \begin{bmatrix} a_{11} b_{11} & a_{12} b_{12} & \dots & a_{1n} b_{1n} \\ a_{21} b_{21} & a_{22} b_{22} & \dots & a_{2n} b_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{m1} b_{m1} & a_{m2} b_{m2} & \dots & a_{mn} b_{mn} \end{bmatrix}. $$A * B # tensor([[ 0., 1., 4., 9.], # [ 16., 25., 36., 49.], # [ 64., 81., 100., 121.], # [144., 169., 196., 225.], # [256., 289., 324., 361.]])将张量乘以或加上一个标量不会改变张量的形状，其中张量的每个元素都将与标量相加或相乘。a = 2 X = torch.arange(24).reshape(2, 3, 4) a + X, (a * X).shape #(tensor([[[ 2, 3, 4, 5], # [ 6, 7, 8, 9], # [10, 11, 12, 13]], # # [[14, 15, 16, 17], # [18, 19, 20, 21], # [22, 23, 24, 25]]]), # torch.Size([2, 3, 4]))降维我们可以对任意张量进行的一个有用的操作是计算其元素的和。在数学表示法中，我们使用$\sum$符号表示求和。为了表示长度为$d$的向量中元素的总和，可以记为$\sum_{i=1}^dx_i$。在代码中，我们可以调用计算求和的函数：x = torch.arange(4, dtype=torch.float32) x, x.sum() # (tensor([0., 1., 2., 3.]), tensor(6.))我们可以表示任意形状张量的元素和。例如，矩阵$\textbf{A}$中元素的和可以记为$\sum_{i=1}^{m} \sum_{j=1}^{n} a_{ij}$。A.shape, A.sum() # (torch.Size([5, 4]), tensor(190.))默认情况下，调用求和函数会沿所有的轴降低张量的维度，使它变为一个标量。我们还可以指定张量沿哪一个轴来通过求和降低维度。以矩阵为例，为了通过求和所有行的元素来降维（轴0），我们可以在调用函数时指定axis=0。由于输入矩阵沿0轴降维以生成输出向量，因此输入轴0的维数在输出形状中消失。A # tensor([[ 0., 1., 2., 3.], # [ 4., 5., 6., 7.], # [ 8., 9., 10., 11.], # [12., 13., 14., 15.], # [16., 17., 18., 19.]]) A_sum_axis0 = A.sum(axis=0) A_sum_axis0, A_sum_axis0.shape # (tensor([40., 45., 50., 55.]), torch.Size([4]))简单的说，按照第一个维度（轴0、即行）求和，消去了第一个维度。A_sum_axis1 = A.sum(axis=1) A_sum_axis1, A_sum_axis1.shape # (tensor([ 6., 22., 38., 54., 70.]), torch.Size([5]))按照第二个维度（轴1、即列）求和，消去了第二个维度A.sum(axis=[0, 1]) # Same as A.sum() # tensor(190.)同样，计算平均值的函数也可以沿指定轴降低张量的维度。A.mean(axis=0), A.sum(axis=0) / A.shape[0] # (tensor([ 8., 9., 10., 11.]), tensor([ 8., 9., 10., 11.]))非降维求和有时在调用函数来计算总和或均值时保持轴数不变会很有用sum_A = A.sum(axis=1, keepdims=True) sum_A # tensor([[ 6.], # [22.], # [38.], # [54.], # [70.]])由于sum_A在对每行进行求和后仍保持两个轴，我们可以通过广播将A除以sum_A。A #tensor([[ 0., 1., 2., 3.], # [ 4., 5., 6., 7.], # [ 8., 9., 10., 11.], # [12., 13., 14., 15.], # [16., 17., 18., 19.]]) A.shape # torch.Size([5, 4]) sum_A.shape # torch.Size([5, 1]) A / sum_A #tensor([[0.0000, 0.1667, 0.3333, 0.5000], # [0.1818, 0.2273, 0.2727, 0.3182], # [0.2105, 0.2368, 0.2632, 0.2895], # [0.2222, 0.2407, 0.2593, 0.2778], # [0.2286, 0.2429, 0.2571, 0.2714]]) 如果想沿某个轴计算A元素的累积总和，比如axis=0（按行计算），我们可以调用cumsum函数。此函数不会沿任何轴降低输入张量的维度。A.cumsum(axis=0) #tensor([[ 0., 1., 2., 3.], # [ 4., 6., 8., 10.], # [12., 15., 18., 21.], # [24., 28., 32., 36.], # [40., 45., 50., 55.]])点积（Dot Product）给定两个向量$\textbf{x},\textbf{y}\in\mathbb{R}^d$，它们的点积（dot product）$\textbf{x}^\top\textbf{y}$（或$\langle\textbf{x},\textbf{y}\rangle$），是相同位置的按元素乘积的和：$\textbf{x}^\top \textbf{y} = \sum_{i=1}^{d} x_i y_i$。y = torch.ones(4, dtype = torch.float32) x, y, torch.dot(x, y) # (tensor([0., 1., 2., 3.]), tensor([1., 1., 1., 1.]), tensor(6.))点积在很多场合都很有用。例如，给定一组由向量$\textbf{x} \in \mathbb{R}^d$表示的值，和一组由$\textbf{w} \in \mathbb{R}^d$表示的权重。$\textbf{x}$中的值根据权重$\textbf{w}$的加权和，可以表示为点积$\textbf{x}^\top \textbf{w}$。当权重为非负数且和为1（即$\left(\sum_{i=1}^{d}{w_i}=1\right)$）时，点积表示加权平均（weighted average）。将两个向量规范化得到单位长度后，点积表示它们夹角的余弦。范数线性代数中最有用的一些运算符是范数（norm）。非正式地说，一个向量的范数告诉我们一个向量有多大。这里考虑的大小（size）概念不涉及维度，而是分量的大小。在线性代数中，向量范数是将向量映射到标量的函数$f$。给定任意向量$\textbf{x}$，向量范数要满足一些属性。第一个性质是：如果按常数因子$\alpha$缩放向量的所有元素，其范数也会按相同常数因子的绝对值缩放：$$ f(\alpha \textbf{x}) = |\alpha| f(\textbf{x}). $$第二个性质是我们熟悉的三角不等式:$$ f(\textbf{x} + \textbf{y}) \leq f(\textbf{x}) + f(\textbf{y}). $$第三个性质简单地说范数必须是非负的:$$ f(\textbf{x}) \geq 0. $$这是有道理的。因为在大多数情况下，任何东西的最小的大小是0。最后一个性质要求范数最小为0，当且仅当向量全由0组成。$$ \forall i, [\textbf{x}]_i = 0 \Leftrightarrow f(\textbf{x})=0. $$范数听起来很像距离的度量。如果你还记得欧几里得距离和毕达哥拉斯定理，那么非负性的概念和三角不等式可能会给你一些启发。事实上，欧几里得距离是一个$L_2$范数：假设$n$维向量$\textbf{x}$中的元素是$x_1,\ldots,x_n$，其$L_2$范数是向量元素平方和的平方根：$$ \|\textbf{x}\|_2 = \sqrt{\sum_{i=1}^n x_i^2}, $$其中，在$L_2$范数中常常省略下标$2$，也就是说$\|\textbf{x}\|$等同于$\|\textbf{x}\|_2$。在代码中，可以按如下方式计算向量的$L_2$范数。u = torch.tensor([3.0, -4.0]) torch.norm(u) # tensor(5.)在深度学习中，更经常地使用$L_2$范数的平方。也会经常遇到$L_1$范数，它表示为向量元素的绝对值之和：$$ \|\textbf{x}\|_1 = \sum_{i=1}^n \left|x_i \right|. $$与$L_2$范数相比，$L_1$范数受异常值的影响较小。为了计算$L_1$范数，要将绝对值函数和按元素求和组合起来。torch.abs(u).sum()$L_2$范数和$L_1$范数都是更一般的$L_p$范数的特例：$$ \|\textbf{x}\|_p = \left(\sum_{i=1}^n \left|x_i \right|^p \right)^{1/p}. $$类似于向量的$L_2$范数，矩阵$\textbf{X} \in \mathbb{R}^{m \times n}$的Frobenius范数（Frobenius norm）是矩阵元素平方和的平方根：$$ \|\textbf{X}\|_F = \sqrt{\sum_{i=1}^m \sum_{j=1}^n x_{ij}^2}. $$Frobenius范数满足向量范数的所有性质，它就像是矩阵形向量的$L_2$范数。调用以下函数将计算矩阵的Frobenius范数。torch.norm(torch.ones((4, 9))) # tensor(6.)矩阵求导TODO
导入包import torch虽然被称为Pytorch，但是代码中使用torch张量张量表示由一个数值组成的数组，这个数组可能有多个维度。具有一个轴的张量对应数学上的向量（vector）；具有两个轴的张量对应数学上的矩阵（matrix）；具有两个轴以上的张量没有特殊的数学名称。可以使用 arange 创建一个行向量 x。这个行向量包含以0开始的前12个整数，它们默认创建为整数。也可指定创建类型为浮点数。张量中的每个值都称为张量的 元素（element）。例如，张量 x 中有 12 个元素。除非额外指定，新的张量将存储在内存中，并采用基于CPU的计算。x = torch.arange(12) x # tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) # 可以通过张量的shape属性来访问张量（沿每个轴的长度）的形状 x.shape # torch.Size([12]) x.numel() # 12要想改变一个张量的形状而不改变元素数量和元素值，可以调用reshape函数。我们不需要通过手动指定每个维度来改变形状。 也就是说，如果我们的目标形状是（高度,宽度）， 那么在知道宽度后，高度会被自动计算得出，不必我们自己做除法。 在上面的例子中，为了获得一个3行的矩阵，我们手动指定了它有3行和4列。 幸运的是，我们可以通过-1来调用此自动计算出维度的功能。 即我们可以用x.reshape(-1,4)或x.reshape(3,-1)来取代x.reshape(3,4)。X = x.reshape(3, 4) X # tensor([[ 0, 1, 2, 3], # [ 4, 5, 6, 7], # [ 8, 9, 10, 11]]) X.reshape(2,-1) # tensor([[ 0, 1, 2, 3, 4, 5], # [ 6, 7, 8, 9, 10, 11]])有时，我们希望使用全0、全1、其他常量，或者从特定分布中随机采样的数字来初始化矩阵。# 创建一个形状为（2,3,4）的张量，其中所有元素都设置为0 torch.zeros((2, 3, 4)) # tensor([[[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 torch.ones((2, 3, 4)) # tensor([[[1., 1., 1., 1.], # [1., 1., 1., 1.], # [1., 1., 1., 1.]], # # [[1., 1., 1., 1.], # [1., 1., 1., 1.], # [1., 1., 1., 1.]]]) # 创建一个形状为（3,4）的张量。 其中的每个元素都从均值为0、标准差为1的标准高斯分布（正态分布）中随机采样。 torch.randn(3, 4) # tensor([[ 1.1704, -0.4649, -1.1481, -2.0655], # [-0.1964, 0.3888, 0.2516, -0.5367], # [-0.3829, -0.0578, 0.8739, -1.4293]])还可以用Python的嵌套数组来为所需张量中每个元素赋予确定值torch.tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]]) # tensor([[2, 1, 4, 3], # [1, 2, 3, 4], # [4, 3, 2, 1]])运算符x = torch.tensor([1.0, 2, 4, 8]) y = torch.tensor([2, 2, 2, 2]) x + y, x - y, x * y, x / y, x ** y # **运算符是求幂运算 # (tensor([ 3., 4., 6., 10.]), # tensor([-1., 0., 2., 6.]), # tensor([ 2., 4., 8., 16.]), # tensor([0.5000, 1.0000, 2.0000, 4.0000]), # tensor([ 1., 4., 16., 64.])) torch.exp(x) # tensor([2.7183e+00, 7.3891e+00, 5.4598e+01, 2.9810e+03])也可以把多个张量连结（concatenate）在一起，把它们端对端地叠起来形成一个更大的张量。只需要提供张量列表，并给出沿哪个轴连结。下面的例子分别演示了当沿行（轴-0，形状的第一个元素）和按列（轴-1，形状的第二个元素）连结两个矩阵时，会发生什么情况。可以看到，第一个输出张量的轴-0长度（$6$）是两个输入张量轴-0长度的总和（$3 + 3$）；第二个输出张量的轴-1长度（$8$）是两个输入张量轴-1长度的总和（$4 + 4$）。X = torch.arange(12, dtype=torch.float32).reshape((3,4)) Y = torch.tensor([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]]) torch.cat((X, Y), dim=0), torch.cat((X, Y), dim=1) #(tensor([[ 0., 1., 2., 3.], # [ 4., 5., 6., 7.], # [ 8., 9., 10., 11.], # [ 2., 1., 4., 3.], # [ 1., 2., 3., 4.], # [ 4., 3., 2., 1.]]), # tensor([[ 0., 1., 2., 3., 2., 1., 4., 3.], # [ 4., 5., 6., 7., 1., 2., 3., 4.], # [ 8., 9., 10., 11., 4., 3., 2., 1.]]))简单地说，前者是上下堆叠，后者是左右堆叠。广播机制在某些情况下，即使形状不同，我们仍然可以通过调用广播机制（broadcasting mechanism）来执行按元素操作。这种机制的工作方式如下：首先，通过适当复制元素来扩展一个或两个数组，以便在转换之后，两个张量具有相同的形状。其次，对生成的数组执行按元素操作。在大多数情况下，我们将沿着数组中长度为1的轴进行广播，如下例子：a = torch.arange(3).reshape((3, 1)) b = torch.arange(2).reshape((1, 2)) a, b #(tensor([[0], # [1], # [2]]), # tensor([[0, 1]])) a + b # 此处体现了广播机制 # tensor([[0, 1], # [1, 2], # [2, 3]])由于a和b分别是$3\times1$和$1\times2$矩阵，如果让它们相加，它们的形状不匹配。广播机制将两个矩阵广播为一个更大的$3\times2$矩阵，矩阵a将复制列，矩阵b将复制行，然后再按元素相加。索引和切片索引和切片操作与Python和pandas中的数组操作基本一致。张量中的元素可以通过索引访问，第一个元素的索引是0，最后一个元素索引是-1；可以指定范围以包含第一个元素和最后一个之前的元素。X[-1], X[1:3] # (tensor([ 8., 9., 10., 11.]), # tensor([[ 4., 5., 6., 7.], # [ 8., 9., 10., 11.]]))除了读取，还可以进行写入操作# 单个赋值 X[1, 2] = 9 X # tensor([[ 0., 1., 2., 3.], # [ 4., 5., 9., 7.], # [ 8., 9., 10., 11.]]) # 多个赋值 X[0:2, :] = 12 X # tensor([[12., 12., 12., 12.], # [12., 12., 12., 12.], # [ 8., 9., 10., 11.]])节省内存运行一些操作可能会导致为新结果分配内存。例如，如果用Y = X + Y，将取消引用Y指向的张量，而是指向新分配的内存处的张量。在下面的例子中，用Python的id()函数演示了这一点，它给我们提供了内存中引用对象的确切地址。运行Y = Y + X后，我们会发现id(Y)指向另一个位置。这是因为Python首先计算Y + X，为结果分配新的内存，然后使Y指向内存中的这个新位置。before = id(Y) Y = Y + X id(Y) == before # False这可能是不可取的，原因有两个：首先，我们不想总是不必要地分配内存。 在机器学习中，我们可能有数百兆的参数，并且在一秒内多次更新所有参数。 通常情况下，我们希望原地执行这些更新。 其次，如果我们不原地更新，其他引用仍然会指向旧的内存位置， 这样我们的某些代码可能会无意中引用旧的参数。幸运的是，执行原地操作非常简单。我们可以使用切片表示法将操作的结果分配给先前分配的数组，例如Y[:] = <expression>。为了说明这一点，首先创建一个新的矩阵Z，其形状与另一个Y相同，使用zeros_like来分配一个全$0$的块。Z = torch._like(Y) print('id(Z):', id(Z)) Z[:] = X + Y print('id(Z):', id(Z)) # id(Z): 1822658475968 # id(Z): 1822658475968如果在后续计算中没有重复使用X，也可以使用X[:] = X + Y或X += Y来减少操作的内存开销。before = id(X) X += Y id(X) == before # True转换为其他Python对象A = X.numpy() B = torch.tensor(A) type(A), type(B) # (numpy.ndarray, torch.Tensor) a = torch.tensor([3.5]) a, a.item(), float(a), int(a) # (tensor([3.5000]), 3.5, 3.5, 3)小结深度学习中存储和操作数据的主要接口是张量（$n$维数组），Pytorch中张量的基本操作与Python数组、Numpy中基本一致，但要特别注意Pytorch中的广播机制。
导入测试数据集这里使用的是手写体数字识别的数据import numpy as np import matplotlib.pyplot as plt import matplotlib from sklearn import datasets digits = datasets.load_digits() digits.keys() # dict_keys(['data', 'target', 'target_names', 'images', 'DESCR']) X = digits.data X.shape # (1797, 64) y = digits.target y.shape # (1797,) digits.target_names # array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) # 可以看到这个数据集是乱序的，所以可以不进行打乱操作 y[:100] # array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, # 2, 3, 4, 5, 6, 7, 8, 9, 0, 9, 5, 5, 6, 5, 0, 9, 8, 9, 8, 4, 1, 7, # 7, 3, 5, 1, 0, 0, 2, 2, 7, 8, 2, 0, 1, 2, 6, 3, 3, 7, 3, 3, 4, 6, # 6, 6, 4, 9, 1, 5, 0, 9, 5, 2, 8, 2, 0, 0, 1, 7, 6, 3, 2, 1, 7, 4, # 6, 3, 1, 3, 9, 1, 7, 6, 8, 4, 3, 1]) # 查看一下第666个数据 some_digit = X[666] y[666] # 0 # 将这张图打印出来 some_digit_image = some_digit.reshape(8,8) plt.imshow(some_digit_image,cmap=matplotlib.cm.binary) plt.show()可以看到这是数字0，之后我们使用自己封装的函数对数据集进行测试使用kNN算法进行测试# 以下两个都是自己封装的函数 from model_selection import train_test_split from kNN import KNNClassifier X_train,X_test,y_train,y_test = train_test_split(X,y,test_ratio = 0.2) my_knn_clf = KNNClassifier(k=3) my_knn_clf.fit(X_train,y_train) y_predict= my_knn_clf.predict(X_test) # 计算准确率 sum(y_predict == y_test) / len(y_test) # 0.9916434540389972使用上述过程即可使用kNN算法(k取3)，且对测试集进行预测的准确率达到99.16%以上封装自己的准确率函数def accuracy_score(y_true, y_predict): """计算y_true和y_predict之间的准确率""" assert len(y_true) == len(y_predict), \ "the size of y_true must be equal to the size of y_predict" return np.sum(y_true == y_predict) / len(y_true)测试一下from metrics import accuracy_score accuracy_score(y_test,y_predict) # 0.9916434540389972使用scikit-learn封装的accuracy_scorescikit-learn中也为我们封装好了计算准确率的函数，可以直接调用from sklearn.model_selection import train_test_split from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import accuracy_score # 得到训练集和测试集 X_train,X_test,y_train,y_test = train_test_split(X,y,test_size = 0.2, random_state = 666) # 得到knn分类器 knn_clf = KNeighborsClassifier(n_neighbors=3) knn_clf.fit(X_train,y_train) # KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski', # metric_params=None, n_jobs=None, n_neighbors=3, p=2, # weights='uniform') y_predict = knn_clf.predict(X_test) accuracy_score(y_test,y_predict) # 0.9888888888888889
导入测试数据集这里使用scikit-learn自带的鸢尾花数据import numpy as np import matplotlib.pyplot as plt from sklearn import datasets iris = datasets.load_iris() X = iris.data y = iris.target X.shape # (150, 4) y.shape # (150,)实现查看y的时候发现，发现lable是按照从小到大进行排序的，所以取训练集不能直接取前n个为训练集，后n个为测试集，这样得到的模型肯定是不准确的。这个时候可以使用permutation方法，获取到随机打乱的一组索引，之后自定义训练集和测试集的比例，这里设置测试集的比例为0.2，使用numpy的fancy indexing就可以切割得到完全随机的训练集和测试集。# 获取随机打乱的索引 shuffle_indexes = np.random.permutation(len(X)) # array([106, 61, 127, 139, 128, 0, 108, 142, 92, 20, 113, 17, 11, # 88, 140, 116, 18, 89, 104, 57, 38, 135, 131, 99, 133, 66, # 40, 121, 86, 93, 134, 145, 39, 52, 98, 50, 90, 24, 51, # 2, 120, 55, 75, 107, 67, 59, 36, 80, 119, 82, 143, 69, # 137, 81, 14, 19, 64, 65, 28, 60, 77, 7, 105, 47, 83, # 138, 109, 3, 71, 63, 31, 44, 102, 43, 146, 122, 110, 21, # 79, 29, 12, 53, 136, 148, 58, 149, 42, 114, 41, 97, 4, # 118, 85, 26, 96, 94, 48, 45, 8, 103, 16, 101, 130, 54, # 91, 27, 9, 132, 129, 23, 95, 78, 124, 74, 46, 25, 62, # 30, 6, 117, 115, 33, 111, 15, 76, 112, 72, 68, 5, 56, # 49, 144, 125, 37, 32, 13, 70, 84, 10, 1, 123, 100, 22, # 34, 147, 35, 73, 141, 126, 87]) # 设置测试集比例 test_ratio = 0.2 # 注意这里可能取到浮点数，需要强制取整 test_size = int(len(X)*test_ratio) # 得到训练集和测试集的索引 test_indexes = shuffle_indexes[:test_size] train_indexes = shuffle_indexes[test_size:] # 使用fancy indexing 即可得到训练集和测试集数据 X_train = X[train_indexes] y_train = y[train_indexes] X_test = X[test_indexes] y_test = y[test_indexes] print(X_train.shape) # (120, 4) print(y_train.shape) # (120,)自定义train_test_splitimport numpy as np def train_test_split(X, y, test_ratio=0.2, seed=None): """将数据 X 和 y 按照test_ratio分割成X_train, X_test, y_train, y_test""" assert X.shape[0] == y.shape[0], \ "the size of X must be equal to the size of y" assert 0.0 <= test_ratio <= 1.0, \ "test_ration must be valid" if seed: np.random.seed(seed) shuffled_indexes = np.random.permutation(len(X)) test_size = int(len(X) * test_ratio) test_indexes = shuffled_indexes[:test_size] train_indexes = shuffled_indexes[test_size:] X_train = X[train_indexes] y_train = y[train_indexes] X_test = X[test_indexes] y_test = y[test_indexes] return X_train, X_test, y_train, y_test# 使用我们封装的算法 from model_selection import train_test_split X_train,X_test,y_train,y_test = train_test_split(X,y) # 此处引用的是自己封装的knn算法，不是scikit-learn封装的 from kNN import KNNClassifier my_knn_clf = KNNClassifier(k=3) my_knn_clf.fit(X_train,y_train) y_predict = my_knn_clf.predict(X_test) # array([1, 2, 2, 0, 1, 2, 0, 2, 0, 2, 2, 1, 1, 1, 1, 1, 2, 2, 1, 0, 2, 1, # 2, 1, 0, 0, 1, 2, 2, 0]) y_test # array([1, 2, 2, 0, 1, 2, 0, 2, 0, 2, 2, 1, 1, 1, 1, 1, 2, 2, 1, 0, 2, 1, # 2, 1, 0, 0, 1, 2, 2, 0]) # 得到y_predict和y_test之后需要对其进行比较，查看准确率 sum(y_predict == y_test) # 30 # 准确率 sum(y_predict == y_test)/len(y_test) # 1.0使用scikit-learn中的train_test_splitscikit-learn中为我们封装好了分割数据集的方法，我们可以直接调用from sklearn.model_selection import train_test_split X_train,X_test,y_train,y_test = train_test_split(X,y,test_size = 0.2, random_state = 666) print(X_train.shape) print(y_train.shape) # (120, 4) # (120,) print(X_test.shape) print(y_test.shape) # (30, 4) # (30,)
进入Jupyter Notebook命令行窗口运行jupyter notebook命令，浏览器访问'http://localhost:8888/tree'即可键盘快捷键快捷键是 Jupyter Notebooks 最好用的功能之一。当你想运行任何代码块时，你只需要按下Ctrl + Enter 即可。Jupyter Notebooks 提供的键盘快捷键非常多，为我们节省了大量时间。Jupyter Notebook 提供了两种不同的键盘输入模式 - 命令和编辑。命令模式将键盘与 Notebook 命令绑定，并由具有蓝色左边距的带有灰色单元格边框来表示。编辑模式允许你将文本（或代码）输入活动单元格，并以绿色单元格边框表示。使用 Esc 和 Enter 可以在命令和编辑模式之间跳转。常用快捷键已加粗，如果快捷键被系统中的其它应用占用，则可能会失效。命令模式快捷键（按 Esc 键开启）:快捷键作用说明Enter转入编辑模式 Shift-Enter运行本单元，选中下个单元新单元默认为命令模式Ctrl-Enter运行本单元 Alt-Enter运行本单元，在其下插入新单元新单元默认为编辑模式Y单元转入代码状态 M单元转入 markdown 状态 R单元转入 raw 状态 1设定 1 级标题仅在 markdown 状态下时建议使用标题相关快捷键，如果单元处于其他状态，则会强制切换到 markdown 状态2设定 2 级标题 3设定 3 级标题 4设定 4 级标题 5设定 5 级标题 6设定 6 级标题 Up选中上方单元 K选中上方单元 Down选中下方单元 J选中下方单元 Shift-K连续选择上方单元 Shift-J连续选择下方单元 A在上方插入新单元 B在下方插入新单元 X剪切选中的单元 C复制选中的单元 Shift-V粘贴到上方单元 V粘贴到下方单元 Z恢复删除的最后一个单元 D,D删除选中的单元连续按两个 D 键Shift-M合并选中的单元 Ctrl-S保存当前 NoteBook S保存当前 NoteBook L开关行号编辑框的行号是可以开启和关闭的O转换输出 Shift-O转换输出滚动 Esc关闭页面 Q关闭页面 H显示快捷键帮助 I,I中断 NoteBook 内核 0,0重启 NoteBook 内核 Shift忽略 Shift-Space向上滚动 Space向下滚动 编辑模式快捷键（ 按 Enter 键启动）:快捷键作用说明Tab代码补全或缩进 Shift-Tab提示输出帮助信息，部分函数、类、方法等会显示其定义原型，如果在其后加 ? 再运行会显示更加详细的帮助Ctrl-]缩进向右缩进Ctrl-[解除缩进向左缩进Ctrl-A全选 Ctrl-Z撤销 Ctrl-Shift-Z重做 Ctrl-Y重做 Ctrl-Home跳到单元开头 Ctrl-Up跳到单元开头 Ctrl-End跳到单元末尾 Ctrl-Down跳到单元末尾 Ctrl-Left跳到左边一个字首 Ctrl-Right跳到右边一个字首 Ctrl-Backspace删除前面一个字 Ctrl-Delete删除后面一个字 Esc切换到命令模式 Ctrl-M切换到命令模式 Shift-Enter运行本单元，选中下一单元新单元默认为命令模式Ctrl-Enter运行本单元 Alt-Enter运行本单元，在下面插入一单元新单元默认为编辑模式Ctrl-Shift--分割单元按光标所在行进行分割Ctrl-Shift-Subtract分割单元 Ctrl-S保存当前 NoteBook Shift忽略 Up光标上移或转入上一单元 Down光标下移或转入下一单元 Ctrl-/注释整行/撤销注释仅代码状态有效魔法命令%lsmagic打印当前可以用的魔法命令，当我们想使用一个魔法命令，而不知怎么拼写函数名时，可以使用%lsmagic来查询%run%run后面接着一个相对地址的py文件，表示运行这个py文件%timeit代码执行计时import numpy as np %timeit np.sin(24) #708 ns ± 17.1 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)